From ea0feb59b1e4f32ef20c9e0ed532e64a5304b56e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 10:37:36 +0000 Subject: [PATCH 1/8] feat: add @wolfstar/plugin-subcommands-advanced Port modular slash-subcommand classes from @kaname-png/plugin-subcommands-advanced to @wolfstar/http-framework, with wiring via CommandLoaderStrategy and tests using http-framework-test-utils. --- .changeset/subcommands-advanced-plugin.md | 5 + README.md | 7 +- .../plugin-subcommands-advanced/CHANGELOG.md | 7 + .../plugin-subcommands-advanced/README.md | 124 ++++ .../plugin-subcommands-advanced/build.mjs | 14 + .../plugin-subcommands-advanced/package.json | 67 ++ .../plugin-subcommands-advanced/src/index.ts | 42 ++ .../src/lib/structures/command.ts | 120 +++ .../src/lib/utils/decorators.ts | 96 +++ .../src/lib/utils/functions.ts | 111 +++ .../src/lib/utils/hooks.ts | 59 ++ .../src/lib/utils/strategy.ts | 20 + .../src/lib/utils/types.ts | 77 ++ .../src/lib/utils/wiring.ts | 146 ++++ .../src/register.ts | 37 + .../tests/subcommands-advanced.test.ts | 182 +++++ .../plugin-subcommands-advanced/tsconfig.json | 8 + pnpm-lock.yaml | 683 +++++++++++++++++- pnpm-workspace.yaml | 10 + vitest.config.ts | 8 + 20 files changed, 1792 insertions(+), 31 deletions(-) create mode 100644 .changeset/subcommands-advanced-plugin.md create mode 100644 packages/plugin-subcommands-advanced/CHANGELOG.md create mode 100644 packages/plugin-subcommands-advanced/README.md create mode 100644 packages/plugin-subcommands-advanced/build.mjs create mode 100644 packages/plugin-subcommands-advanced/package.json create mode 100644 packages/plugin-subcommands-advanced/src/index.ts create mode 100644 packages/plugin-subcommands-advanced/src/lib/structures/command.ts create mode 100644 packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts create mode 100644 packages/plugin-subcommands-advanced/src/lib/utils/functions.ts create mode 100644 packages/plugin-subcommands-advanced/src/lib/utils/hooks.ts create mode 100644 packages/plugin-subcommands-advanced/src/lib/utils/strategy.ts create mode 100644 packages/plugin-subcommands-advanced/src/lib/utils/types.ts create mode 100644 packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts create mode 100644 packages/plugin-subcommands-advanced/src/register.ts create mode 100644 packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts create mode 100644 packages/plugin-subcommands-advanced/tsconfig.json diff --git a/.changeset/subcommands-advanced-plugin.md b/.changeset/subcommands-advanced-plugin.md new file mode 100644 index 0000000..ad0b08b --- /dev/null +++ b/.changeset/subcommands-advanced-plugin.md @@ -0,0 +1,5 @@ +--- +"@wolfstar/plugin-subcommands-advanced": major +--- + +Add `@wolfstar/plugin-subcommands-advanced`: modular slash subcommands as separate command classes for `@wolfstar/http-framework`, adapted from `@kaname-png/plugin-subcommands-advanced`. diff --git a/README.md b/README.md index 5f41068..b55a849 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ ## Packages -| Package | Version | Downloads | -| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| [`@wolfstar/plugin-api`](https://npmx.dev/package/@wolfstar/plugin-api) | [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) | [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) | +| Package | Version | Downloads | +| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`@wolfstar/plugin-api`](https://npmx.dev/package/@wolfstar/plugin-api) | [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) | [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/plugin-api)](https://npmx.dev/package/@wolfstar/plugin-api) | +| [`@wolfstar/plugin-subcommands-advanced`](https://npmx.dev/package/@wolfstar/plugin-subcommands-advanced) | [![version](https://npmx.dev/api/registry/badge/version/@wolfstar/plugin-subcommands-advanced)](https://npmx.dev/package/@wolfstar/plugin-subcommands-advanced) | [![downloads](https://npmx.dev/api/registry/badge/downloads/@wolfstar/plugin-subcommands-advanced)](https://npmx.dev/package/@wolfstar/plugin-subcommands-advanced) | --- diff --git a/packages/plugin-subcommands-advanced/CHANGELOG.md b/packages/plugin-subcommands-advanced/CHANGELOG.md new file mode 100644 index 0000000..9b2a2e5 --- /dev/null +++ b/packages/plugin-subcommands-advanced/CHANGELOG.md @@ -0,0 +1,7 @@ +# @wolfstar/plugin-subcommands-advanced + +## 1.0.0 + +### Major Changes + +- Initial release: modular slash subcommands as separate command classes for `@wolfstar/http-framework`. diff --git a/packages/plugin-subcommands-advanced/README.md b/packages/plugin-subcommands-advanced/README.md new file mode 100644 index 0000000..d8a52ec --- /dev/null +++ b/packages/plugin-subcommands-advanced/README.md @@ -0,0 +1,124 @@ +# `@wolfstar/plugin-subcommands-advanced` + +Plugin for [`@wolfstar/http-framework`](https://www.npmjs.com/package/@wolfstar/http-framework) that lets you split slash **subcommands** (and subcommand groups) into **separate command classes**, instead of putting every handler method on the parent. + +Adapted from [`@kaname-png/plugin-subcommands-advanced`](https://github.com/sawa-ko/neko-plugins/tree/main/packages/subcommands-advanced) for WolfStar’s HTTP interaction framework. + +## Installation + +```bash +pnpm add @wolfstar/http-framework @wolfstar/plugin-subcommands-advanced +``` + +## Usage + +Import the register entrypoint **before** creating the client: + +```typescript +import "@wolfstar/plugin-subcommands-advanced/register"; +import { Client, RegisterCommand } from "@wolfstar/http-framework"; +import { + Command, + Subcommand, + RegisterAsSubcommand, + RegisterAsSubcommandGroup, +} from "@wolfstar/plugin-subcommands-advanced"; + +const client = new Client({ + subcommandsAdvanced: { + // optional: piece names become parent/sub or parent/group/sub + nameCommandsAutogenerated: true, + }, +}); +``` + +Recommended layout: + +```text +commands/ +└── utils/ + ├── parent.ts // parent chat-input command + ├── ping.ts // subcommand + └── poll/ + └── create.ts // grouped subcommand +``` + +### Parent command + +```typescript +import { RegisterCommand } from "@wolfstar/http-framework"; +import { Subcommand } from "@wolfstar/plugin-subcommands-advanced"; + +@RegisterCommand((builder) => + builder + .setName("utils") + .setDescription("Utility commands") + .addSubcommandGroup((group) => group.setName("poll").setDescription("Poll tools")), +) +export class UtilsCommand extends Subcommand {} +``` + +### Direct subcommand + +```typescript +import { Command, RegisterAsSubcommand } from "@wolfstar/plugin-subcommands-advanced"; + +@RegisterAsSubcommand("utils", (builder) => builder.setName("ping").setDescription("Ping the bot")) +export class PingCommand extends Command { + public override chatInputRun(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "Pong!" }); + } +} +``` + +### Grouped subcommand + +```typescript +import { Command, RegisterAsSubcommandGroup } from "@wolfstar/plugin-subcommands-advanced"; + +@RegisterAsSubcommandGroup("utils", "poll", (builder) => + builder.setName("create").setDescription("Create a poll"), +) +export class PollCreateCommand extends Command { + public override chatInputRun(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "Created!" }); + } +} +``` + +### Constructor options (no decorators) + +```typescript +import { Command } from "@wolfstar/plugin-subcommands-advanced"; + +export class PingCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { + ...options, + registerSubCommand: { + parentCommandName: "utils", + slashSubcommand: (builder) => builder.setName("ping").setDescription("Ping!"), + }, + }); + } + + public override chatInputRun(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "Pong!" }); + } +} +``` + +## How it works + +`@wolfstar/http-framework` routes subcommands to **methods on the parent** command instance. This plugin: + +1. Lets child command classes register themselves into in-memory registries (by parent name). +2. After all command pieces are constructed, rebuilds the parent’s chat-input resolver and `CommandRouter`, installing thin delegate methods that call each child’s `chatInputRun`. + +You do not need to call the optional `hooks.subcommands` / `hooks.groups` helpers unless you are registering the parent imperatively and want to attach builders yourself. + +## Notes + +- HTTP-only: there is no message-command support (unlike the Sapphire original). +- Child classes should **not** use `@RegisterCommand` — only the parent registers the top-level slash command. +- Subcommand groups must still be declared on the parent (via `@RegisterCommand` builder or `registerApplicationCommands`). diff --git a/packages/plugin-subcommands-advanced/build.mjs b/packages/plugin-subcommands-advanced/build.mjs new file mode 100644 index 0000000..ebdb4b9 --- /dev/null +++ b/packages/plugin-subcommands-advanced/build.mjs @@ -0,0 +1,14 @@ +import { build } from "tsdown"; + +await build({ + entry: ["src/index.ts", "src/register.ts"], + format: "esm", + target: "es2022", + // Avoid bundling broken transitive `.d.mts` from `@sapphire/utilities` via http-framework. + dts: { resolve: false }, + clean: true, + sourcemap: true, + fixedExtension: false, + outDir: "dist", + config: false, +}); diff --git a/packages/plugin-subcommands-advanced/package.json b/packages/plugin-subcommands-advanced/package.json new file mode 100644 index 0000000..488575f --- /dev/null +++ b/packages/plugin-subcommands-advanced/package.json @@ -0,0 +1,67 @@ +{ + "name": "@wolfstar/plugin-subcommands-advanced", + "version": "1.0.0", + "description": "Plugin for @wolfstar/http-framework to modularize slash subcommands into separate command classes", + "keywords": [ + "discord", + "http-framework", + "plugin", + "subcommands", + "wolfstar" + ], + "homepage": "https://github.com/wolfstar-project/plugins/tree/main/packages/plugin-subcommands-advanced", + "bugs": { + "url": "https://github.com/wolfstar-project/plugins/issues" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/wolfstar-project/plugins.git", + "directory": "packages/plugin-subcommands-advanced" + }, + "files": [ + "dist" + ], + "type": "module", + "sideEffects": [ + "./dist/register.js" + ], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./register": { + "types": "./dist/register.d.ts", + "import": "./dist/register.js" + } + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "scripts": { + "build": "node ./build.mjs", + "typecheck": "tsc --noEmit", + "lint": "oxlint src --fix", + "test": "vitest run", + "prepack": "pnpm build" + }, + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "^2.1.1", + "discord-api-types": "^0.38.33" + }, + "devDependencies": { + "@wolfstar/http-framework": "^3.1.0", + "@wolfstar/http-framework-test-utils": "^3.0.1" + }, + "peerDependencies": { + "@wolfstar/http-framework": "^3.1.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/plugin-subcommands-advanced/src/index.ts b/packages/plugin-subcommands-advanced/src/index.ts new file mode 100644 index 0000000..fe0f511 --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/index.ts @@ -0,0 +1,42 @@ +export { Command, Subcommand } from "./lib/structures/command.js"; +export { + RegisterAsSubcommand, + RegisterAsSubcommandGroup, + RegisterSubCommand, + RegisterSubCommandGroup, +} from "./lib/utils/decorators.js"; +export { + analyzeSubCommandParsed, + analyzeSubcommandGroupParsed, + parseSlashSubcommand, + subCommandsGroupRegistry, + subCommandsRegistry, + subcommandGroupMethodName, + subcommandMethodName, +} from "./lib/utils/functions.js"; +export { RegisterSubcommandsHooks } from "./lib/utils/hooks.js"; +export { SubcommandsAdvancedLoaderStrategy } from "./lib/utils/strategy.js"; +export type { + PluginSubcommandOptions, + RegisterSubCommandGroupOptions, + RegisterSubCommandOptions, + SlashSubcommandResolvable, + SubcommandCommandOptions, + SubcommandMappingCollection, +} from "./lib/utils/types.js"; +export { + clearSubcommandRegistries, + hasRegisteredSubcommands, + wireParentSubcommands, +} from "./lib/utils/wiring.js"; + +import type { PluginSubcommandOptions } from "./lib/utils/types.js"; + +declare module "@wolfstar/http-framework" { + interface ClientOptions { + /** + * Options for `@wolfstar/plugin-subcommands-advanced`. + */ + subcommandsAdvanced?: PluginSubcommandOptions; + } +} diff --git a/packages/plugin-subcommands-advanced/src/lib/structures/command.ts b/packages/plugin-subcommands-advanced/src/lib/structures/command.ts new file mode 100644 index 0000000..e8c1eec --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/lib/structures/command.ts @@ -0,0 +1,120 @@ +import { Command as FrameworkCommand, container } from "@wolfstar/http-framework"; +import { + analyzeSubCommandParsed, + analyzeSubcommandGroupParsed, + parseSlashSubcommand, +} from "../utils/functions.js"; +import { RegisterSubcommandsHooks } from "../utils/hooks.js"; +import type { SubcommandCommandOptions } from "../utils/types.js"; + +export type AdvancedCommandOptions = FrameworkCommand.Options & SubcommandCommandOptions; + +/** + * Command class that can register itself as a modular subcommand (or grouped subcommand) + * of a parent {@link Subcommand} / chat-input command. + * + * @example + * ```typescript + * import { Command } from '@wolfstar/plugin-subcommands-advanced'; + * + * export class PingCommand extends Command { + * public constructor(context: Command.LoaderContext, options: Command.Options) { + * super(context, { + * ...options, + * registerSubCommand: { + * parentCommandName: 'utils', + * slashSubcommand: (builder) => builder.setName('ping').setDescription('Ping!') + * } + * }); + * } + * + * public override chatInputRun(interaction: Command.ChatInputInteraction) { + * return interaction.reply({ content: 'Pong!' }); + * } + * } + * ``` + */ +export class Command< + Options extends AdvancedCommandOptions = AdvancedCommandOptions, +> extends FrameworkCommand { + public constructor(context: FrameworkCommand.LoaderContext, options: Options = {} as Options) { + const nameCommandsAutogenerated = + container.client?.options.subcommandsAdvanced?.nameCommandsAutogenerated === true; + let name = options.name; + + if (options.registerSubCommand && nameCommandsAutogenerated) { + const parsed = parseSlashSubcommand(options.registerSubCommand.slashSubcommand); + name = `${options.registerSubCommand.parentCommandName}/${parsed.name}`; + } + + if (options.registerSubcommandInGroup && nameCommandsAutogenerated) { + const parsed = parseSlashSubcommand(options.registerSubcommandInGroup.slashSubcommand); + name = `${options.registerSubcommandInGroup.parentCommandName}/${options.registerSubcommandInGroup.groupName}/${parsed.name}`; + } + + super(context, { ...options, name } as Options); + + if (this.options.registerSubCommand) { + const { parentCommandName, slashSubcommand } = this.options.registerSubCommand; + analyzeSubCommandParsed(this, parentCommandName, slashSubcommand); + } + + if (this.options.registerSubcommandInGroup) { + const { parentCommandName, groupName, slashSubcommand } = + this.options.registerSubcommandInGroup; + analyzeSubcommandGroupParsed(this, parentCommandName, groupName, slashSubcommand); + } + } +} + +export namespace Command { + export type Options = AdvancedCommandOptions; + export type JSON = FrameworkCommand.JSON; + /** @deprecated Use {@linkcode LoaderContext} instead. */ + export type Context = LoaderContext; + export type LoaderContext = FrameworkCommand.LoaderContext; + export type ChatInputInteraction = FrameworkCommand.ChatInputInteraction; + export type AutocompleteInteraction = FrameworkCommand.AutocompleteInteraction; + export type ApplicationCommandInteraction = FrameworkCommand.ApplicationCommandInteraction; + export type Registry = FrameworkCommand.Registry; +} + +/** + * Parent command marker. Extend this for the chat-input command that owns modular + * subcommands. Wiring of children is performed automatically by the plugin loader + * strategy after all pieces are constructed. + * + * @example + * ```typescript + * import { RegisterCommand } from '@wolfstar/http-framework'; + * import { Subcommand } from '@wolfstar/plugin-subcommands-advanced'; + * + * @RegisterCommand((builder) => + * builder + * .setName('utils') + * .setDescription('Utility commands') + * .addSubcommandGroup((group) => group.setName('poll').setDescription('Polls')) + * ) + * export class UtilsCommand extends Subcommand {} + * ``` + */ +export class Subcommand< + Options extends FrameworkCommand.Options = FrameworkCommand.Options, +> extends FrameworkCommand { + /** + * Optional hooks for imperative {@link registerApplicationCommands} usage. + */ + public readonly hooks = RegisterSubcommandsHooks; +} + +export namespace Subcommand { + export type Options = FrameworkCommand.Options; + export type JSON = FrameworkCommand.JSON; + /** @deprecated Use {@linkcode LoaderContext} instead. */ + export type Context = LoaderContext; + export type LoaderContext = FrameworkCommand.LoaderContext; + export type ChatInputInteraction = FrameworkCommand.ChatInputInteraction; + export type AutocompleteInteraction = FrameworkCommand.AutocompleteInteraction; + export type ApplicationCommandInteraction = FrameworkCommand.ApplicationCommandInteraction; + export type Registry = FrameworkCommand.Registry; +} diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts b/packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts new file mode 100644 index 0000000..a302dd4 --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts @@ -0,0 +1,96 @@ +import { Command as FrameworkCommand, container } from "@wolfstar/http-framework"; +import { + analyzeSubCommandParsed, + analyzeSubcommandGroupParsed, + parseSlashSubcommand, +} from "./functions.js"; +import type { SlashSubcommandResolvable } from "./types.js"; + +type CommandConstructor = new (...args: any[]) => FrameworkCommand; + +/** + * Class decorator that registers the decorated command as a direct subcommand of `parentCommandName`. + * + * @example + * ```typescript + * import { Command, RegisterAsSubcommand } from '@wolfstar/plugin-subcommands-advanced'; + * + * @RegisterAsSubcommand('utils', (builder) => builder.setName('ping').setDescription('Ping!')) + * export class PingCommand extends Command { + * public override chatInputRun(interaction: Command.ChatInputInteraction) { + * return interaction.reply({ content: 'Pong!' }); + * } + * } + * ``` + */ +export function RegisterAsSubcommand( + parentCommandName: string, + slashSubcommand: SlashSubcommandResolvable, +) { + return function decorate(target: T): T { + return class extends target { + public constructor(...args: any[]) { + const [context, baseOptions = {}] = args as [ + FrameworkCommand.LoaderContext, + FrameworkCommand.Options, + ]; + const parsed = parseSlashSubcommand(slashSubcommand); + const name = + container.client?.options.subcommandsAdvanced?.nameCommandsAutogenerated === true + ? `${parentCommandName}/${parsed.name}` + : baseOptions.name; + + super(context, { ...baseOptions, name }); + analyzeSubCommandParsed(this, parentCommandName, slashSubcommand); + } + } as T; + }; +} + +/** + * Class decorator that registers the decorated command as a subcommand inside a group. + * + * @example + * ```typescript + * import { Command, RegisterAsSubcommandGroup } from '@wolfstar/plugin-subcommands-advanced'; + * + * @RegisterAsSubcommandGroup('utils', 'poll', (builder) => + * builder.setName('create').setDescription('Create a poll') + * ) + * export class PollCreateCommand extends Command { + * public override chatInputRun(interaction: Command.ChatInputInteraction) { + * return interaction.reply({ content: 'Created!' }); + * } + * } + * ``` + */ +export function RegisterAsSubcommandGroup( + parentCommandName: string, + groupName: string, + slashSubcommand: SlashSubcommandResolvable, +) { + return function decorate(target: T): T { + return class extends target { + public constructor(...args: any[]) { + const [context, baseOptions = {}] = args as [ + FrameworkCommand.LoaderContext, + FrameworkCommand.Options, + ]; + const parsed = parseSlashSubcommand(slashSubcommand); + const name = + container.client?.options.subcommandsAdvanced?.nameCommandsAutogenerated === true + ? `${parentCommandName}/${groupName}/${parsed.name}` + : baseOptions.name; + + super(context, { ...baseOptions, name }); + analyzeSubcommandGroupParsed(this, parentCommandName, groupName, slashSubcommand); + } + } as T; + }; +} + +/** @deprecated Use {@link RegisterAsSubcommand} */ +export const RegisterSubCommand = RegisterAsSubcommand; + +/** @deprecated Use {@link RegisterAsSubcommandGroup} */ +export const RegisterSubCommandGroup = RegisterAsSubcommandGroup; diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/functions.ts b/packages/plugin-subcommands-advanced/src/lib/utils/functions.ts new file mode 100644 index 0000000..1822ad1 --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/lib/utils/functions.ts @@ -0,0 +1,111 @@ +import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { Collection } from "@discordjs/collection"; +import { container, type Command } from "@wolfstar/http-framework"; +import type { SlashSubcommandResolvable, SubcommandMappingCollection } from "./types.js"; + +/** + * Registry of direct subcommands keyed by parent command name, then subcommand name. + */ +export const subCommandsRegistry: Collection< + string, + Collection +> = new Collection(); + +/** + * Registry of grouped subcommands keyed by parent → group → subcommand name. + */ +export const subCommandsGroupRegistry: Collection< + string, + Collection> +> = new Collection(); + +/** + * Normalize slash subcommand input into a builder. + */ +export function parseSlashSubcommand( + subcommand: SlashSubcommandResolvable, +): SlashCommandSubcommandBuilder { + if (typeof subcommand === "function") { + const builder = new SlashCommandSubcommandBuilder(); + return subcommand(builder, container) ?? builder; + } + + if (subcommand instanceof SlashCommandSubcommandBuilder) { + return subcommand; + } + + if ( + typeof subcommand === "object" && + subcommand !== null && + "toJSON" in subcommand && + typeof subcommand.toJSON === "function" + ) { + const json = (subcommand as { toJSON: () => { name: string; description: string } }).toJSON(); + return new SlashCommandSubcommandBuilder().setName(json.name).setDescription(json.description); + } + + const data = subcommand as { name: string; description: string }; + return new SlashCommandSubcommandBuilder().setName(data.name).setDescription(data.description); +} + +/** + * Stable method name linked on the parent command for a direct subcommand. + */ +export function subcommandMethodName(subcommandName: string): string { + return `__wolfstar_sc_${subcommandName}`; +} + +/** + * Stable method name linked on the parent command for a grouped subcommand. + */ +export function subcommandGroupMethodName(groupName: string, subcommandName: string): string { + return `__wolfstar_sc_${groupName}_${subcommandName}`; +} + +/** + * Register a command piece as a direct subcommand of `parentCommandName`. + */ +export function analyzeSubCommandParsed( + piece: Command, + parentCommandName: string, + subcommand: SlashSubcommandResolvable, +): Command { + const subcommandParsed = parseSlashSubcommand(subcommand); + const subcommandName = subcommandParsed.name; + const methodName = subcommandMethodName(subcommandName); + const entry: SubcommandMappingCollection = { + slashCommand: subcommandParsed, + commandPiece: piece, + methodName, + }; + + const registry = subCommandsRegistry.ensure(parentCommandName, () => new Collection()); + registry.set(subcommandName, entry); + + return piece; +} + +/** + * Register a command piece as a subcommand inside a group of `parentCommandName`. + */ +export function analyzeSubcommandGroupParsed( + piece: Command, + parentCommandName: string, + groupName: string, + subcommand: SlashSubcommandResolvable, +): Command { + const subcommandParsed = parseSlashSubcommand(subcommand); + const subcommandName = subcommandParsed.name; + const methodName = subcommandGroupMethodName(groupName, subcommandName); + const entry: SubcommandMappingCollection = { + slashCommand: subcommandParsed, + commandPiece: piece, + methodName, + }; + + const parentRegistry = subCommandsGroupRegistry.ensure(parentCommandName, () => new Collection()); + const groupRegistry = parentRegistry.ensure(groupName, () => new Collection()); + groupRegistry.set(subcommandName, entry); + + return piece; +} diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/hooks.ts b/packages/plugin-subcommands-advanced/src/lib/utils/hooks.ts new file mode 100644 index 0000000..e86d7b9 --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/lib/utils/hooks.ts @@ -0,0 +1,59 @@ +import type { SlashCommandBuilder } from "@discordjs/builders"; +import type { Command } from "@wolfstar/http-framework"; +import { subCommandsGroupRegistry, subCommandsRegistry } from "./functions.js"; + +/** + * Optional hooks for parent commands that register chat-input commands imperatively + * via {@link Command.registerApplicationCommands}. Prefer relying on the automatic + * wiring performed by {@link SubcommandsAdvancedLoaderStrategy}; these helpers are + * useful when you want to inspect or manually attach builders during registration. + */ +export const RegisterSubcommandsHooks = { + /** + * Attach registered direct subcommands onto a {@link SlashCommandBuilder}. + */ + subcommands(piece: Command, context?: SlashCommandBuilder): void { + const subcommands = subCommandsRegistry.get(piece.router.chatInputName ?? piece.name); + if (!subcommands) { + console.warn( + `[plugin-subcommands-advanced]: No direct subcommands registered for parent "${piece.router.chatInputName ?? piece.name}".`, + ); + return; + } + + if (!context) return; + + for (const { slashCommand } of subcommands.values()) { + context.addSubcommand(slashCommand); + } + }, + + /** + * Attach registered grouped subcommands onto matching groups on a {@link SlashCommandBuilder}. + */ + groups(piece: Command, context?: SlashCommandBuilder): void { + const groups = subCommandsGroupRegistry.get(piece.router.chatInputName ?? piece.name); + if (!groups) { + console.warn( + `[plugin-subcommands-advanced]: No subcommand groups registered for parent "${piece.router.chatInputName ?? piece.name}".`, + ); + return; + } + + if (!context) return; + + for (const [groupName, commands] of groups) { + const group = context.options.find((option) => { + const data = + "toJSON" in option && typeof option.toJSON === "function" ? option.toJSON() : null; + return data && typeof data === "object" && "name" in data && data.name === groupName; + }) as { addSubcommand?(subcommand: unknown): unknown } | undefined; + + if (!group?.addSubcommand) continue; + + for (const { slashCommand } of commands.values()) { + group.addSubcommand(slashCommand); + } + } + }, +}; diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/strategy.ts b/packages/plugin-subcommands-advanced/src/lib/utils/strategy.ts new file mode 100644 index 0000000..6233925 --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/lib/utils/strategy.ts @@ -0,0 +1,20 @@ +import { CommandLoaderStrategy, type Command, type CommandStore } from "@wolfstar/http-framework"; +import { hasRegisteredSubcommands, wireParentSubcommands } from "./wiring.js"; + +/** + * Loader strategy that wires modular subcommands onto parent commands before + * chat-input mappings are published to {@link CommandStore.router}. + * + * {@link CommandStore.loadAll} constructs every piece first (children populate the + * registries), then inserts them — {@link onLoad} therefore sees a complete registry. + */ +export class SubcommandsAdvancedLoaderStrategy extends CommandLoaderStrategy { + public override onLoad(store: CommandStore, piece: Command) { + const parentName = piece.router.chatInputName; + if (parentName && hasRegisteredSubcommands(parentName)) { + wireParentSubcommands(piece); + } + + return super.onLoad(store, piece); + } +} diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/types.ts b/packages/plugin-subcommands-advanced/src/lib/utils/types.ts new file mode 100644 index 0000000..d8fe0aa --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/lib/utils/types.ts @@ -0,0 +1,77 @@ +import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import type { APIApplicationCommandSubcommandOption } from "discord-api-types/v10"; +import type { container, Command } from "@wolfstar/http-framework"; + +/** + * Resolvable slash-subcommand payload accepted by the plugin. + */ +export type SlashSubcommandResolvable = + | SlashCommandSubcommandBuilder + | { toJSON(): Pick } + | Pick + | (( + builder: SlashCommandSubcommandBuilder, + Container: typeof container, + ) => SlashCommandSubcommandBuilder | void); + +/** + * Options to register a command class as a subcommand of a parent command. + */ +export interface RegisterSubCommandOptions { + /** + * The chat-input name of the parent command (the name passed to {@link RegisterCommand}). + */ + parentCommandName: string; + /** + * Slash subcommand data (builder, JSON, or callback). Required for Discord registration. + */ + slashSubcommand: SlashSubcommandResolvable; +} + +/** + * Options to register a command class as a subcommand inside a parent subcommand group. + */ +export interface RegisterSubCommandGroupOptions { + /** + * The chat-input name of the parent command. + */ + parentCommandName: string; + /** + * The name of the subcommand group already registered on the parent. + */ + groupName: string; + /** + * Slash subcommand data (builder, JSON, or callback). Required for Discord registration. + */ + slashSubcommand: SlashSubcommandResolvable; +} + +/** + * Extra {@link Command} options accepted by the advanced subcommands {@link Command} class. + */ +export interface SubcommandCommandOptions { + registerSubCommand?: RegisterSubCommandOptions; + registerSubcommandInGroup?: RegisterSubCommandGroupOptions; +} + +/** + * Entry stored in the subcommand / group registries. + */ +export interface SubcommandMappingCollection { + slashCommand: SlashCommandSubcommandBuilder; + commandPiece: Command; + methodName: string; +} + +/** + * Plugin options under `ClientOptions.subcommandsAdvanced`. + */ +export interface PluginSubcommandOptions { + /** + * Auto-generate piece names as `parent/sub` or `parent/group/sub` so multiple + * subcommand classes do not collide in `@sapphire/pieces`. + * + * @default false + */ + nameCommandsAutogenerated?: boolean; +} diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts b/packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts new file mode 100644 index 0000000..c7d3020 --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts @@ -0,0 +1,146 @@ +import { applicationCommandRegistry, CommandRouter, type Command } from "@wolfstar/http-framework"; +import { + ApplicationCommandOptionType, + type APIApplicationCommandSubcommandGroupOption, + type RESTPostAPIChatInputApplicationCommandsJSONBody, +} from "discord-api-types/v10"; +import { subCommandsGroupRegistry, subCommandsRegistry } from "./functions.js"; +import type { SubcommandMappingCollection } from "./types.js"; + +const wiredParents = new WeakSet(); + +/** + * Whether this piece is a parent that has registered modular subcommands. + */ +export function hasRegisteredSubcommands(parentName: string): boolean { + return subCommandsRegistry.has(parentName) || subCommandsGroupRegistry.has(parentName); +} + +/** + * Install delegate methods on the parent and rebuild its chat-input resolver + router + * so modular child command classes handle each subcommand. + * + * Must run after every command piece has been constructed (e.g. from + * {@link CommandLoaderStrategy.onLoad}), so the registries contain every child. + * + * Delegates are installed on the parent **prototype** so reconstructed instances + * (Store `loadAll` rebuilds pieces) still satisfy {@link CommandRouter}'s method checks. + */ +export function wireParentSubcommands(parent: Command): void { + const parentName = parent.router.chatInputName; + if (!parentName || !hasRegisteredSubcommands(parentName)) return; + if (wiredParents.has(parent)) return; + + const ctor = parent.constructor as typeof Command; + const existing = applicationCommandRegistry.get(ctor); + const chatInput = existing?.chatInput; + if (!chatInput) { + console.warn( + `[plugin-subcommands-advanced]: Parent command "${parentName}" has no chat-input registration; skipping wiring.`, + ); + return; + } + + const snapshot = chatInput.toJSON(); + applicationCommandRegistry.delete(ctor); + + const resolver = applicationCommandRegistry.ensure(ctor).makeChatInput(); + resolver.setCommand(baseCommandData(snapshot)); + + for (const group of snapshot.options ?? []) { + if (group.type === ApplicationCommandOptionType.SubcommandGroup) { + resolver.addSubcommandGroup(groupDataWithoutSubcommands(group)); + } + } + + const direct = subCommandsRegistry.get(parentName); + if (direct) { + for (const entry of direct.values()) { + installDelegate(ctor, parentName, entry); + resolver.addSubcommand(entry.slashCommand, entry.methodName); + } + } + + const groups = subCommandsGroupRegistry.get(parentName); + if (groups) { + for (const [groupName, commands] of groups) { + for (const entry of commands.values()) { + installDelegate(ctor, parentName, entry); + resolver.addSubcommand(entry.slashCommand, entry.methodName, groupName); + } + } + } + + Object.defineProperty(parent, "router", { + value: new CommandRouter(parent), + configurable: true, + enumerable: true, + writable: false, + }); + + wiredParents.add(parent); +} + +function baseCommandData(snapshot: RESTPostAPIChatInputApplicationCommandsJSONBody) { + const { options: _options, ...rest } = snapshot; + return rest; +} + +function groupDataWithoutSubcommands(group: APIApplicationCommandSubcommandGroupOption) { + const { options: _options, type: _type, ...rest } = group; + return rest; +} + +function installDelegate( + parentCtor: typeof Command, + parentName: string, + entry: SubcommandMappingCollection, +): void { + const { methodName } = entry; + + Object.defineProperty(parentCtor.prototype, methodName, { + configurable: true, + enumerable: false, + writable: true, + value(this: Command, interaction: Command.ApplicationCommandInteraction, args: object) { + const resolved = lookupEntry(parentName, methodName); + if (!resolved) { + throw new Error( + `[plugin-subcommands-advanced]: No child command registered for parent "${parentName}" method "${methodName}".`, + ); + } + return resolved.commandPiece.chatInputRun(interaction, args); + }, + }); +} + +function lookupEntry( + parentName: string, + methodName: string, +): SubcommandMappingCollection | undefined { + const direct = subCommandsRegistry.get(parentName); + if (direct) { + for (const entry of direct.values()) { + if (entry.methodName === methodName) return entry; + } + } + + const groups = subCommandsGroupRegistry.get(parentName); + if (groups) { + for (const commands of groups.values()) { + for (const entry of commands.values()) { + if (entry.methodName === methodName) return entry; + } + } + } + + return undefined; +} + +/** + * Reset subcommand registries (useful between tests). + */ +export function clearSubcommandRegistries(): void { + subCommandsRegistry.clear(); + subCommandsGroupRegistry.clear(); +} diff --git a/packages/plugin-subcommands-advanced/src/register.ts b/packages/plugin-subcommands-advanced/src/register.ts new file mode 100644 index 0000000..125dab4 --- /dev/null +++ b/packages/plugin-subcommands-advanced/src/register.ts @@ -0,0 +1,37 @@ +import { + Client, + container, + Plugin, + postInitialization, + type ClientOptions, + type CommandStore, +} from "@wolfstar/http-framework"; +import "./index.js"; +import { SubcommandsAdvancedLoaderStrategy } from "./lib/utils/strategy.js"; + +/** + * Installs the advanced subcommands loader strategy so modular child command + * classes are wired onto their parent chat-input commands after load. + * + * Activate by importing the side-effecting entrypoint before creating the client: + * + * ```ts + * import '@wolfstar/plugin-subcommands-advanced/register'; + * ``` + */ +export class SubcommandsAdvancedPlugin extends Plugin { + public static [postInitialization](this: Client, _options: ClientOptions): void { + const store = container.stores.get("commands") as CommandStore; + Object.defineProperty(store, "strategy", { + value: new SubcommandsAdvancedLoaderStrategy(), + configurable: true, + enumerable: true, + writable: true, + }); + } +} + +Client.plugins.registerPostInitializationHook( + SubcommandsAdvancedPlugin[postInitialization]!, + "WolfStar-SubcommandsAdvanced-PostInitialization", +); diff --git a/packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts b/packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts new file mode 100644 index 0000000..4e42d6a --- /dev/null +++ b/packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts @@ -0,0 +1,182 @@ +import { ApplicationCommandOptionType, InteractionResponseType } from "discord-api-types/v10"; +import { container, RegisterCommand } from "@wolfstar/http-framework"; +import { + ChatInputApplicationCommandInteractionData, + createTestHarness, + makeCommand, +} from "@wolfstar/http-framework-test-utils"; +import { + clearSubcommandRegistries, + Command, + RegisterAsSubcommand, + RegisterAsSubcommandGroup, + Subcommand, + subCommandsRegistry, + wireParentSubcommands, +} from "../src/index.js"; +import "../src/register.js"; + +async function loadParentAndChildren( + parent: ReturnType, + children: ReturnType[], +) { + const store = container.stores.get("commands"); + await store.unloadAll(); + + for (const child of children) { + store.set(child.name, child); + } + + wireParentSubcommands(parent); + store.set(parent.name, parent); + if (parent.router.chatInputName) { + store.router.addChatInputMapping(parent.router.chatInputName, parent); + } +} + +describe("plugin-subcommands-advanced", () => { + afterEach(async () => { + clearSubcommandRegistries(); + await container.stores.get("commands").unloadAll(); + }); + + it("wires a modular subcommand onto the parent and runs the child chatInputRun", async () => { + @RegisterCommand({ name: "utils", description: "Utility commands" }) + class UtilsCommand extends Subcommand {} + + @RegisterAsSubcommand("utils", (builder) => + builder.setName("ping").setDescription("Ping the bot"), + ) + class PingCommand extends Command { + public override chatInputRun(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "Pong from child!" }); + } + } + + const { runner } = createTestHarness({ + subcommandsAdvanced: { nameCommandsAutogenerated: true }, + }); + + const ping = makeCommand(PingCommand); + const parent = makeCommand(UtilsCommand); + await loadParentAndChildren(parent, [ping]); + + expect(subCommandsRegistry.get("utils")?.has("ping")).toBe(true); + expect( + parent.router.routeChatInputInteraction({ + id: "0", + name: "utils", + type: 1, + options: [{ type: ApplicationCommandOptionType.Subcommand, name: "ping", options: [] }], + } as never), + ).toBe("__wolfstar_sc_ping"); + + const result = await runner.run({ + ...ChatInputApplicationCommandInteractionData, + data: { + id: "0", + name: "utils", + type: 1, + options: [{ type: ApplicationCommandOptionType.Subcommand, name: "ping", options: [] }], + }, + }); + + expect(result.statusCode).toBe(200); + expect(result.json()).toMatchObject({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { content: "Pong from child!" }, + }); + }); + + it("wires a grouped modular subcommand", async () => { + @RegisterCommand((builder) => + builder + .setName("utils") + .setDescription("Utility commands") + .addSubcommandGroup((group) => group.setName("poll").setDescription("Poll tools")), + ) + class UtilsCommand extends Subcommand {} + + @RegisterAsSubcommandGroup("utils", "poll", (builder) => + builder.setName("create").setDescription("Create a poll"), + ) + class PollCreateCommand extends Command { + public override chatInputRun(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "Poll created!" }); + } + } + + const { runner } = createTestHarness({ + subcommandsAdvanced: { nameCommandsAutogenerated: true }, + }); + + const create = makeCommand(PollCreateCommand); + const parent = makeCommand(UtilsCommand); + await loadParentAndChildren(parent, [create]); + + const result = await runner.run({ + ...ChatInputApplicationCommandInteractionData, + data: { + id: "0", + name: "utils", + type: 1, + options: [ + { + type: ApplicationCommandOptionType.SubcommandGroup, + name: "poll", + options: [ + { type: ApplicationCommandOptionType.Subcommand, name: "create", options: [] }, + ], + }, + ], + }, + }); + + expect(result.statusCode).toBe(200); + expect(result.json()).toMatchObject({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { content: "Poll created!" }, + }); + }); + + it("supports registerSubCommand constructor options", async () => { + @RegisterCommand({ name: "tools", description: "Tools" }) + class ToolsCommand extends Subcommand {} + + class EchoCommand extends Command { + public constructor(context: Command.LoaderContext, options: Command.Options) { + super(context, { + ...options, + registerSubCommand: { + parentCommandName: "tools", + slashSubcommand: (builder) => builder.setName("echo").setDescription("Echo"), + }, + }); + } + + public override chatInputRun(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "echo" }); + } + } + + const { runner } = createTestHarness(); + const echo = makeCommand(EchoCommand); + const parent = makeCommand(ToolsCommand); + await loadParentAndChildren(parent, [echo]); + + const result = await runner.run({ + ...ChatInputApplicationCommandInteractionData, + data: { + id: "0", + name: "tools", + type: 1, + options: [{ type: ApplicationCommandOptionType.Subcommand, name: "echo", options: [] }], + }, + }); + + expect(result.statusCode).toBe(200); + expect(result.json()).toMatchObject({ + data: { content: "echo" }, + }); + }); +}); diff --git a/packages/plugin-subcommands-advanced/tsconfig.json b/packages/plugin-subcommands-advanced/tsconfig.json new file mode 100644 index 0000000..5a24989 --- /dev/null +++ b/packages/plugin-subcommands-advanced/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22b3215..373e6e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + vitest>vite: 6.4.3 + '@vitest/mocker>vite': 6.4.3 + importers: .: @@ -82,7 +86,7 @@ importers: version: 0.3.1 vitest: specifier: ^4.1.10 - version: 4.1.10(@types/node@22.15.21)(@vitest/coverage-v8@4.1.10)(vite@8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@22.15.21)(@vitest/coverage-v8@4.1.10)(vite@6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) packages/plugin-api: dependencies: @@ -97,6 +101,25 @@ importers: specifier: ^3.0.0 version: 3.0.0 + packages/plugin-subcommands-advanced: + dependencies: + '@discordjs/builders': + specifier: ^1.13.0 + version: 1.14.1 + '@discordjs/collection': + specifier: ^2.1.1 + version: 2.1.1 + discord-api-types: + specifier: ^0.38.33 + version: 0.38.49 + devDependencies: + '@wolfstar/http-framework': + specifier: ^3.1.0 + version: 3.1.0 + '@wolfstar/http-framework-test-utils': + specifier: ^3.0.1 + version: 3.0.1(@wolfstar/http-framework@3.1.0)(vitest@4.1.10) + packages: '@andrewbranch/untar.js@1.0.3': @@ -336,6 +359,162 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@favware/npm-deprecate@2.0.0': resolution: {integrity: sha512-SbaEvcF07FH/ngpyjP4rYSB8EZ33Vwh/GbRHhRz8/aI23W8yrom0xgTkrTbCQHkdSISHF3rpjZVFIl4lma6BUg==} engines: {node: '>=18'} @@ -1094,6 +1273,144 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.62.3': + resolution: {integrity: sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.3': + resolution: {integrity: sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.3': + resolution: {integrity: sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.3': + resolution: {integrity: sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.3': + resolution: {integrity: sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.3': + resolution: {integrity: sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': + resolution: {integrity: sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.3': + resolution: {integrity: sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.3': + resolution: {integrity: sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.3': + resolution: {integrity: sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.3': + resolution: {integrity: sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.3': + resolution: {integrity: sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.3': + resolution: {integrity: sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.3': + resolution: {integrity: sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.3': + resolution: {integrity: sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.3': + resolution: {integrity: sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.3': + resolution: {integrity: sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.3': + resolution: {integrity: sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.3': + resolution: {integrity: sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.3': + resolution: {integrity: sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.3': + resolution: {integrity: sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.3': + resolution: {integrity: sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.3': + resolution: {integrity: sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.3': + resolution: {integrity: sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.3': + resolution: {integrity: sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==} + cpu: [x64] + os: [win32] + '@sapphire/async-queue@1.5.5': resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -1325,7 +1642,7 @@ packages: resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: 6.4.3 peerDependenciesMeta: msw: optional: true @@ -1351,10 +1668,24 @@ packages: resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@wolfstar/http-framework-test-utils@3.0.1': + resolution: {integrity: sha512-qlDImK2yJJzPR+55jU3dVEAZuBx50DUFBzL3x15Awc5zbMEHIexosbV4r/ZzvFG1PufnocN/j8XyFUV/NWKrbw==} + engines: {node: '>=20'} + peerDependencies: + '@wolfstar/http-framework': ^3.1.0 + vitest: '>=4.1.9' + peerDependenciesMeta: + vitest: + optional: true + '@wolfstar/http-framework@3.0.0': resolution: {integrity: sha512-N7/qMr69FTkkDUHoOTfb5Sl+HKB5KcFPd4GUi4cW13RTxJPof3uRBkGoOhnzrDLjeboSYFFvlGNCBFB1pSqfyg==} engines: {node: '>=20'} + '@wolfstar/http-framework@3.1.0': + resolution: {integrity: sha512-OKJKg9x/6KCyAKvbvUrMSG7G2Sy38/Uz5uXJvMvcvk/ZWoLJQnmYs55xx47r3HL5RPUJJg4MaPS9S0/Wy/dXWg==} + engines: {node: '>=20'} + '@yuku-codegen/binding-darwin-arm64@0.8.0': resolution: {integrity: sha512-7cSJH6PaKLRBdCfiB4pM6EukvgOk5xV4tyuLOIOEqrHsbnV7brtyff7CjhZbeGozdIHoOnKOi5R7rrmCWN3QSw==} cpu: [arm64] @@ -1801,6 +2132,11 @@ packages: es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2641,6 +2977,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.62.3: + resolution: {integrity: sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -2940,34 +3281,31 @@ packages: resolution: {integrity: sha512-Njrh4U8UODGajoZ44QS2C/BsoEM9DTI/aCqY5swsizb+/ap0FamvnCMcZAxrR5+aoC0ZqkawEfpC/N2SBc+xeA==} engines: {node: '>=18.12.0'} - vite@8.1.3: - resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} - engines: {node: ^20.19.0 || >=22.12.0} + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 - esbuild: ^0.27.0 || ^0.28.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true jiti: optional: true less: optional: true + lightningcss: + optional: true sass: optional: true sass-embedded: @@ -2999,7 +3337,7 @@ packages: '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: 6.4.3 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3517,6 +3855,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@favware/npm-deprecate@2.0.0(supports-color@7.2.0)': dependencies: '@sapphire/fetch': 3.0.5 @@ -3983,6 +4399,81 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.62.3': + optional: true + + '@rollup/rollup-android-arm64@4.62.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.3': + optional: true + + '@rollup/rollup-darwin-x64@4.62.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.3': + optional: true + '@sapphire/async-queue@1.5.5': {} '@sapphire/fetch@3.0.5': {} @@ -4164,7 +4655,7 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@22.15.21)(@vitest/coverage-v8@4.1.10)(vite@8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@22.15.21)(@vitest/coverage-v8@4.1.10)(vite@6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) '@vitest/expect@4.1.10': dependencies: @@ -4175,13 +4666,22 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0) + vite: 6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + + '@vitest/mocker@4.1.10(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + optional: true '@vitest/pretty-format@4.1.10': dependencies: @@ -4209,6 +4709,12 @@ snapshots: '@vladfrangu/async_event_emitter@2.4.7': {} + '@wolfstar/http-framework-test-utils@3.0.1(@wolfstar/http-framework@3.1.0)(vitest@4.1.10)': + dependencies: + '@wolfstar/http-framework': 3.1.0 + optionalDependencies: + vitest: 4.1.10(@types/node@22.20.0)(@vitest/coverage-v8@4.1.10)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + '@wolfstar/http-framework@3.0.0': dependencies: '@discordjs/builders': 1.14.1 @@ -4221,6 +4727,18 @@ snapshots: '@vladfrangu/async_event_emitter': 2.4.7 discord-api-types: 0.38.49 + '@wolfstar/http-framework@3.1.0': + dependencies: + '@discordjs/builders': 1.14.1 + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.6.1 + '@discordjs/util': 1.2.0 + '@sapphire/pieces': 4.4.1 + '@sapphire/result': 2.8.0 + '@sapphire/utilities': 3.18.2 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.49 + '@yuku-codegen/binding-darwin-arm64@0.8.0': optional: true @@ -4549,7 +5067,8 @@ snapshots: detect-indent@6.1.0: {} - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true dir-glob@3.0.1: dependencies: @@ -4595,6 +5114,35 @@ snapshots: es-toolkit@1.49.0: {} + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -5004,6 +5552,7 @@ snapshots: lightningcss-linux-x64-musl: 1.32.0 lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + optional: true lines-and-columns@1.2.4: {} @@ -5481,6 +6030,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.0 '@rolldown/binding-win32-x64-msvc': 1.2.0 + rollup@4.62.3: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.3 + '@rollup/rollup-android-arm64': 4.62.3 + '@rollup/rollup-darwin-arm64': 4.62.3 + '@rollup/rollup-darwin-x64': 4.62.3 + '@rollup/rollup-freebsd-arm64': 4.62.3 + '@rollup/rollup-freebsd-x64': 4.62.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.3 + '@rollup/rollup-linux-arm-musleabihf': 4.62.3 + '@rollup/rollup-linux-arm64-gnu': 4.62.3 + '@rollup/rollup-linux-arm64-musl': 4.62.3 + '@rollup/rollup-linux-loong64-gnu': 4.62.3 + '@rollup/rollup-linux-loong64-musl': 4.62.3 + '@rollup/rollup-linux-ppc64-gnu': 4.62.3 + '@rollup/rollup-linux-ppc64-musl': 4.62.3 + '@rollup/rollup-linux-riscv64-gnu': 4.62.3 + '@rollup/rollup-linux-riscv64-musl': 4.62.3 + '@rollup/rollup-linux-s390x-gnu': 4.62.3 + '@rollup/rollup-linux-x64-gnu': 4.62.3 + '@rollup/rollup-linux-x64-musl': 4.62.3 + '@rollup/rollup-openbsd-x64': 4.62.3 + '@rollup/rollup-openharmony-arm64': 4.62.3 + '@rollup/rollup-win32-arm64-msvc': 4.62.3 + '@rollup/rollup-win32-ia32-msvc': 4.62.3 + '@rollup/rollup-win32-x64-gnu': 4.62.3 + '@rollup/rollup-win32-x64-msvc': 4.62.3 + fsevents: 2.3.3 + run-async@2.4.1: {} run-parallel@1.2.0: @@ -5723,23 +6303,41 @@ snapshots: verkit@0.3.0: {} - vite@8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0): + vite@6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 postcss: 8.5.16 - rolldown: 1.1.4 + rollup: 4.62.3 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.15.21 fsevents: 2.3.3 jiti: 2.7.0 + lightningcss: 1.32.0 + yaml: 2.9.0 + + vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.16 + rollup: 4.62.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.0 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 yaml: 2.9.0 + optional: true - vitest@4.1.10(@types/node@22.15.21)(@vitest/coverage-v8@4.1.10)(vite@8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.10(@types/node@22.15.21)(@vitest/coverage-v8@4.1.10)(vite@6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -5756,7 +6354,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.3(@types/node@22.15.21)(jiti@2.7.0)(yaml@2.9.0) + vite: 6.4.3(@types/node@22.15.21)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.21 @@ -5764,6 +6362,35 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.10(@types/node@22.20.0)(@vitest/coverage-v8@4.1.10)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.0 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + transitivePeerDependencies: + - msw + optional: true + walk-up-path@4.0.0: {} wcwidth@1.0.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9013768..ebe9b3b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,13 @@ packages: - "packages/*" allowBuilds: "@swc/core": true + esbuild: true +# Pin Vitest's Vite to 6 so TypeScript experimental decorators continue to +# transform via esbuild (Vite 8 / oxc does not support them). +overrides: + "vitest>vite": "6.4.3" + "@vitest/mocker>vite": "6.4.3" +minimumReleaseAgeExclude: + - "@wolfstar/http-framework-test-utils@3.0.1" + - "@wolfstar/http-framework@3.1.0" + - "vite@6.4.3" diff --git a/vitest.config.ts b/vitest.config.ts index ef8cc0a..e022470 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,14 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + esbuild: { + target: "es2022", + tsconfigRaw: { + compilerOptions: { + experimentalDecorators: true, + }, + }, + }, test: { globals: true, coverage: { From a7590e2e544e9f10154be24965c50e0da1c3d2f0 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Wed, 29 Jul 2026 10:39:38 +0000 Subject: [PATCH 2/8] fix: remove deprecated decorator aliases flagged by knip --- packages/plugin-subcommands-advanced/src/index.ts | 7 +------ .../src/lib/utils/decorators.ts | 6 ------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/plugin-subcommands-advanced/src/index.ts b/packages/plugin-subcommands-advanced/src/index.ts index fe0f511..0926961 100644 --- a/packages/plugin-subcommands-advanced/src/index.ts +++ b/packages/plugin-subcommands-advanced/src/index.ts @@ -1,10 +1,5 @@ export { Command, Subcommand } from "./lib/structures/command.js"; -export { - RegisterAsSubcommand, - RegisterAsSubcommandGroup, - RegisterSubCommand, - RegisterSubCommandGroup, -} from "./lib/utils/decorators.js"; +export { RegisterAsSubcommand, RegisterAsSubcommandGroup } from "./lib/utils/decorators.js"; export { analyzeSubCommandParsed, analyzeSubcommandGroupParsed, diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts b/packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts index a302dd4..d3e3848 100644 --- a/packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts +++ b/packages/plugin-subcommands-advanced/src/lib/utils/decorators.ts @@ -88,9 +88,3 @@ export function RegisterAsSubcommandGroup( } as T; }; } - -/** @deprecated Use {@link RegisterAsSubcommand} */ -export const RegisterSubCommand = RegisterAsSubcommand; - -/** @deprecated Use {@link RegisterAsSubcommandGroup} */ -export const RegisterSubCommandGroup = RegisterAsSubcommandGroup; From 31bf347616b223add01666e1fae76c9f4a7b0ca9 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Wed, 29 Jul 2026 10:50:14 +0000 Subject: [PATCH 3/8] fix: preserve framework-native subcommands when rebuilding the resolver --- .../src/lib/utils/wiring.ts | 29 +++++++- .../tests/subcommands-advanced.test.ts | 70 ++++++++++++++++++- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts b/packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts index c7d3020..2923f98 100644 --- a/packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts +++ b/packages/plugin-subcommands-advanced/src/lib/utils/wiring.ts @@ -47,9 +47,14 @@ export function wireParentSubcommands(parent: Command): void { const resolver = applicationCommandRegistry.ensure(ctor).makeChatInput(); resolver.setCommand(baseCommandData(snapshot)); - for (const group of snapshot.options ?? []) { - if (group.type === ApplicationCommandOptionType.SubcommandGroup) { - resolver.addSubcommandGroup(groupDataWithoutSubcommands(group)); + for (const option of snapshot.options ?? []) { + if (option.type === ApplicationCommandOptionType.SubcommandGroup) { + resolver.addSubcommandGroup(groupDataWithoutSubcommands(option), getLinkedMethod(option)); + for (const subcommand of option.options ?? []) { + resolver.addSubcommand(subcommand, getLinkedMethod(subcommand), option.name); + } + } else if (option.type === ApplicationCommandOptionType.Subcommand) { + resolver.addSubcommand(option, getLinkedMethod(option)); } } @@ -91,6 +96,24 @@ function groupDataWithoutSubcommands(group: APIApplicationCommandSubcommandGroup return rest; } +/** + * `http-framework` links a subcommand (or group) option to its handler method by + * defining a non-enumerable symbol property on the resolved option object. The + * symbol is not exported, so it is recovered here by its description to preserve + * framework-native subcommand routing when the resolver is rebuilt. + */ +const LINKED_METHOD_SYMBOL_DESCRIPTION = "decorated-command.method.link"; + +function getLinkedMethod(option: object): string | null { + for (const symbol of Object.getOwnPropertySymbols(option)) { + if (symbol.description === LINKED_METHOD_SYMBOL_DESCRIPTION) { + return (Reflect.get(option, symbol) as string | undefined) ?? null; + } + } + + return null; +} + function installDelegate( parentCtor: typeof Command, parentName: string, diff --git a/packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts b/packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts index 4e42d6a..de24d62 100644 --- a/packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts +++ b/packages/plugin-subcommands-advanced/tests/subcommands-advanced.test.ts @@ -1,5 +1,10 @@ import { ApplicationCommandOptionType, InteractionResponseType } from "discord-api-types/v10"; -import { container, RegisterCommand } from "@wolfstar/http-framework"; +import { + container, + RegisterCommand, + RegisterSubcommand, + RegisterSubcommandGroup, +} from "@wolfstar/http-framework"; import { ChatInputApplicationCommandInteractionData, createTestHarness, @@ -139,6 +144,69 @@ describe("plugin-subcommands-advanced", () => { }); }); + it("preserves framework-native subcommands when wiring modular children", async () => { + @RegisterCommand({ name: "mixed", description: "Mixed commands" }) + class MixedCommand extends Subcommand { + @RegisterSubcommandGroup((builder) => builder.setName("admin").setDescription("Admin tools")) + @RegisterSubcommand((builder) => builder.setName("ban").setDescription("Ban a user"), "admin") + public ban(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "native ban" }); + } + + @RegisterSubcommand((builder) => + builder.setName("native").setDescription("Native subcommand"), + ) + public native(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "native!" }); + } + } + + @RegisterAsSubcommand("mixed", (builder) => + builder.setName("modular").setDescription("Modular subcommand"), + ) + class ModularCommand extends Command { + public override chatInputRun(interaction: Command.ChatInputInteraction) { + return interaction.reply({ content: "modular!" }); + } + } + + const { runner } = createTestHarness({ + subcommandsAdvanced: { nameCommandsAutogenerated: true }, + }); + + const modular = makeCommand(ModularCommand); + const parent = makeCommand(MixedCommand); + await loadParentAndChildren(parent, [modular]); + + const run = (options: object[]) => + runner.run({ + ...ChatInputApplicationCommandInteractionData, + data: { id: "0", name: "mixed", type: 1, options }, + }); + + const nativeResult = await run([ + { type: ApplicationCommandOptionType.Subcommand, name: "native", options: [] }, + ]); + expect(nativeResult.statusCode).toBe(200); + expect(nativeResult.json()).toMatchObject({ data: { content: "native!" } }); + + const groupedNativeResult = await run([ + { + type: ApplicationCommandOptionType.SubcommandGroup, + name: "admin", + options: [{ type: ApplicationCommandOptionType.Subcommand, name: "ban", options: [] }], + }, + ]); + expect(groupedNativeResult.statusCode).toBe(200); + expect(groupedNativeResult.json()).toMatchObject({ data: { content: "native ban" } }); + + const modularResult = await run([ + { type: ApplicationCommandOptionType.Subcommand, name: "modular", options: [] }, + ]); + expect(modularResult.statusCode).toBe(200); + expect(modularResult.json()).toMatchObject({ data: { content: "modular!" } }); + }); + it("supports registerSubCommand constructor options", async () => { @RegisterCommand({ name: "tools", description: "Tools" }) class ToolsCommand extends Subcommand {} From cfd3a5e723eb652ffaa8e122887476559f20fc29 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 11:35:20 +0000 Subject: [PATCH 4/8] build: share tsdown config like stars-components Add scripts/tsdown.config.ts and tsconfig.build.json, switch both packages to `tsdown` with dist/esm output, and drop the build.mjs workaround. --- packages/plugin-api/package.json | 19 ++- packages/plugin-api/tsdown.config.ts | 16 +-- .../plugin-subcommands-advanced/build.mjs | 14 -- .../plugin-subcommands-advanced/package.json | 21 +-- .../tsdown.config.ts | 8 ++ scripts/tsdown.config.ts | 121 ++++++++++++++++++ tsconfig.build.json | 10 ++ 7 files changed, 170 insertions(+), 39 deletions(-) delete mode 100644 packages/plugin-subcommands-advanced/build.mjs create mode 100644 packages/plugin-subcommands-advanced/tsdown.config.ts create mode 100644 scripts/tsdown.config.ts create mode 100644 tsconfig.build.json diff --git a/packages/plugin-api/package.json b/packages/plugin-api/package.json index f17eb1b..af02d8d 100644 --- a/packages/plugin-api/package.json +++ b/packages/plugin-api/package.json @@ -21,18 +21,23 @@ ], "type": "module", "sideEffects": [ - "./dist/register.js" + "./dist/esm/register.js" ], - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/esm/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } }, "./register": { - "types": "./dist/register.d.ts", - "import": "./dist/register.js" + "import": { + "types": "./dist/esm/register.d.ts", + "default": "./dist/esm/register.js" + } } }, "publishConfig": { diff --git a/packages/plugin-api/tsdown.config.ts b/packages/plugin-api/tsdown.config.ts index 9e8b6c5..2baa067 100644 --- a/packages/plugin-api/tsdown.config.ts +++ b/packages/plugin-api/tsdown.config.ts @@ -1,12 +1,8 @@ import { defineConfig } from "tsdown"; +import { createTsdownOptions } from "../../scripts/tsdown.config"; -export default defineConfig({ - entry: ["src/index.ts", "src/register.ts"], - format: "esm", - target: "es2022", - dts: true, - clean: true, - sourcemap: true, - fixedExtension: false, - outDir: "dist", -}); +export default defineConfig( + createTsdownOptions({ + entry: ["src/index.ts", "src/register.ts"], + }), +); diff --git a/packages/plugin-subcommands-advanced/build.mjs b/packages/plugin-subcommands-advanced/build.mjs deleted file mode 100644 index ebdb4b9..0000000 --- a/packages/plugin-subcommands-advanced/build.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { build } from "tsdown"; - -await build({ - entry: ["src/index.ts", "src/register.ts"], - format: "esm", - target: "es2022", - // Avoid bundling broken transitive `.d.mts` from `@sapphire/utilities` via http-framework. - dts: { resolve: false }, - clean: true, - sourcemap: true, - fixedExtension: false, - outDir: "dist", - config: false, -}); diff --git a/packages/plugin-subcommands-advanced/package.json b/packages/plugin-subcommands-advanced/package.json index 488575f..a76904a 100644 --- a/packages/plugin-subcommands-advanced/package.json +++ b/packages/plugin-subcommands-advanced/package.json @@ -24,18 +24,23 @@ ], "type": "module", "sideEffects": [ - "./dist/register.js" + "./dist/esm/register.js" ], - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/esm/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } }, "./register": { - "types": "./dist/register.d.ts", - "import": "./dist/register.js" + "import": { + "types": "./dist/esm/register.d.ts", + "default": "./dist/esm/register.js" + } } }, "publishConfig": { @@ -43,7 +48,7 @@ "provenance": true }, "scripts": { - "build": "node ./build.mjs", + "build": "tsdown", "typecheck": "tsc --noEmit", "lint": "oxlint src --fix", "test": "vitest run", diff --git a/packages/plugin-subcommands-advanced/tsdown.config.ts b/packages/plugin-subcommands-advanced/tsdown.config.ts new file mode 100644 index 0000000..2baa067 --- /dev/null +++ b/packages/plugin-subcommands-advanced/tsdown.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsdown"; +import { createTsdownOptions } from "../../scripts/tsdown.config"; + +export default defineConfig( + createTsdownOptions({ + entry: ["src/index.ts", "src/register.ts"], + }), +); diff --git a/scripts/tsdown.config.ts b/scripts/tsdown.config.ts new file mode 100644 index 0000000..d682ccf --- /dev/null +++ b/scripts/tsdown.config.ts @@ -0,0 +1,121 @@ +import { relative, resolve as resolveDir } from "node:path"; +import type { TsdownPluginOption, UserConfig } from "tsdown"; + +export type FormatConfig = + Exclude, string | string[]> extends Record + ? NonNullable + : never; + +function toPluginArray(plugins?: TsdownPluginOption): TsdownPluginOption[] { + if (plugins == null || plugins === false) { + return []; + } + + return (Array.isArray(plugins) ? plugins : [plugins]).filter( + (plugin): plugin is Exclude => + plugin != null && plugin !== false, + ); +} + +export interface FormatConfigCJS extends FormatConfig { + disabled?: boolean; +} + +type LegacyFormatPlugins = { plugins?: UserConfig["plugins"] }; + +function splitFormatOptions(options?: T) { + if (!options) { + return { + formatOptions: undefined as T | undefined, + plugins: undefined as UserConfig["plugins"] | undefined, + }; + } + + const { plugins, ...formatOptions } = options as T & LegacyFormatPlugins; + return { formatOptions: formatOptions as T, plugins }; +} + +const esmOutExtensions = () => ({ js: ".js", dts: ".d.ts" }); +const cjsOutExtensions = () => ({ js: ".cjs", dts: ".d.cts" }); + +const attwOptions = { + entrypoints: ["."], + enabled: true, + level: "error", + profile: "node16", +} satisfies NonNullable>; + +const baseOptions: UserConfig = { + clean: true, + // Do not bundle transitive package `.d.ts` graphs (e.g. broken `@sapphire/utilities` exports). + dts: { resolve: false }, + entry: ["src/index.ts"], + minify: false, + deps: { + neverBundle: true, + onlyBundle: false, + }, + sourcemap: true, + target: "es2022", + treeshake: true, + attw: attwOptions, + tsconfig: relative( + import.meta.dirname, + resolveDir(import.meta.dirname, "../tsconfig.build.json"), + ), + publint: { + enabled: true, + level: "error", + }, +}; + +/** + * Shared tsdown options for publishable packages in this monorepo. + * Mirrors `wolfstar-project/stars-components/scripts/tsdown.config.ts`. + */ +export function createTsdownOptions(options?: EnhancedTsdownOptions): UserConfig { + const { cjsOptions, esmOptions, entry, target, plugins } = options ?? {}; + const { formatOptions: cjsFormatOptions, plugins: cjsPlugins } = splitFormatOptions(cjsOptions); + const { formatOptions: esmFormatOptions, plugins: esmPlugins } = splitFormatOptions(esmOptions); + const { disabled: cjsDisabled = true, ...cjsRest } = cjsFormatOptions ?? { disabled: true }; + const mergedPlugins = [ + ...toPluginArray(plugins), + ...toPluginArray(cjsPlugins), + ...toPluginArray(esmPlugins), + ]; + + const esmFormat = { + outDir: "dist/esm", + outExtensions: esmOutExtensions, + ...esmFormatOptions, + }; + + return { + ...baseOptions, + attw: { + ...attwOptions, + profile: cjsDisabled ? "esm-only" : "node16", + }, + entry: entry ?? baseOptions.entry, + ...(target ? { target } : {}), + ...(mergedPlugins.length > 0 ? { plugins: mergedPlugins } : {}), + format: cjsDisabled + ? { esm: esmFormat } + : { + esm: esmFormat, + cjs: { + outDir: "dist/cjs", + outExtensions: cjsOutExtensions, + ...cjsRest, + }, + }, + } satisfies UserConfig; +} + +export interface EnhancedTsdownOptions { + cjsOptions?: FormatConfigCJS; + esmOptions?: FormatConfig; + entry?: UserConfig["entry"]; + plugins?: UserConfig["plugins"]; + target?: UserConfig["target"]; +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..4c2e5d3 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noEmit": false + }, + "exclude": ["**/dist/**", "**/node_modules/**", "**/tests/**", "**/vitest.config.ts"] +} From 41a9e634f2aaacfb3240b93dab8484cadf221d12 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 11:35:31 +0000 Subject: [PATCH 5/8] docs: note shared tsdown config in AGENTS.md --- AGENTS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f009d39..4f155a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ ### Commands (defined in root `package.json`) -- `pnpm build` — `turbo run build` (tsdown → `dist/`). +- `pnpm build` — `turbo run build` (tsdown → `dist/esm/`, shared options in `scripts/tsdown.config.ts`). - `pnpm test` — `vitest run` (unit + in-process HTTP integration tests). - `pnpm typecheck` — `turbo run typecheck` (`tsc --noEmit`). - `pnpm lint` / `pnpm lint:fix` — oxlint + oxfmt. @@ -21,8 +21,9 @@ ### Gotchas -- `pnpm clean` is broken: it runs `node scripts/clean.mjs`, but `scripts/` does not exist in the repo. Do not rely on it. +- `pnpm clean` is broken: it runs `node scripts/clean.mjs`, but that file does not exist (only `scripts/tsdown.config.ts` is present). Do not rely on it. - Git hooks are active (husky): `pre-commit` runs nano-staged (oxfmt + `oxlint --fix`) and `commit-msg` runs commitlint. Commit messages **must** follow Conventional Commits. +- Vitest is pinned to Vite 6 via `pnpm-workspace.yaml` overrides so TypeScript experimental decorators still transform through esbuild (Vite 8 / oxc does not). ### Exercising the core functionality (ApiServer) From ac7a99608dafa301ccb1688e1fd98edfbb99314b Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Wed, 29 Jul 2026 11:38:03 +0000 Subject: [PATCH 6/8] build: load shared tsdown config via unrun --- packages/plugin-api/package.json | 2 +- packages/plugin-subcommands-advanced/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-api/package.json b/packages/plugin-api/package.json index af02d8d..bba24c0 100644 --- a/packages/plugin-api/package.json +++ b/packages/plugin-api/package.json @@ -45,7 +45,7 @@ "provenance": true }, "scripts": { - "build": "tsdown", + "build": "tsdown --config-loader unrun", "typecheck": "tsc --noEmit", "lint": "oxlint src --fix", "test": "vitest run", diff --git a/packages/plugin-subcommands-advanced/package.json b/packages/plugin-subcommands-advanced/package.json index a76904a..bae4485 100644 --- a/packages/plugin-subcommands-advanced/package.json +++ b/packages/plugin-subcommands-advanced/package.json @@ -48,7 +48,7 @@ "provenance": true }, "scripts": { - "build": "tsdown", + "build": "tsdown --config-loader unrun", "typecheck": "tsc --noEmit", "lint": "oxlint src --fix", "test": "vitest run", From 44bd7049c36c4a607794138730031bbbcfa034e2 Mon Sep 17 00:00:00 2001 From: RedStar Date: Wed, 29 Jul 2026 14:16:04 +0200 Subject: [PATCH 7/8] fix: add .github/labels.yml for Automatic Label Sync workflow The reusable label-sync workflow merges this file with the shared sapphiredev/.github core labels, but it was missing here, causing the workflow to fail (open overwrite-labels/.github/labels.yml: no such file or directory). --- .github/labels.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/labels.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..92d2367 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,23 @@ +- name: bug + color: "d73a4a" + description: "Something isn't working" +- name: enhancement + color: "a2eeef" + description: New feature or request +- name: good first issue + color: "7057ff" + description: Good for newcomers +- name: invalid + color: "e4e669" + description: This doesn't seem right +- name: question + color: "d876e3" + description: Further information is requested +- name: wontfix + color: "ffffff" + description: This will not be worked on +- name: packages:plugin-api + color: "ededed" +- name: packages:plugins-subcommands-advanced + color: "1d76db" + description: Changes in plugins-subcommands-advanced From 5e0dd466e513eeac076ca776889c41dbd6f62400 Mon Sep 17 00:00:00 2001 From: RedStar Date: Wed, 29 Jul 2026 15:54:46 +0200 Subject: [PATCH 8/8] Revert "fix: add .github/labels.yml for Automatic Label Sync workflow" This reverts commit 44bd7049c36c4a607794138730031bbbcfa034e2. --- .github/labels.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/labels.yml diff --git a/.github/labels.yml b/.github/labels.yml deleted file mode 100644 index 92d2367..0000000 --- a/.github/labels.yml +++ /dev/null @@ -1,23 +0,0 @@ -- name: bug - color: "d73a4a" - description: "Something isn't working" -- name: enhancement - color: "a2eeef" - description: New feature or request -- name: good first issue - color: "7057ff" - description: Good for newcomers -- name: invalid - color: "e4e669" - description: This doesn't seem right -- name: question - color: "d876e3" - description: Further information is requested -- name: wontfix - color: "ffffff" - description: This will not be worked on -- name: packages:plugin-api - color: "ededed" -- name: packages:plugins-subcommands-advanced - color: "1d76db" - description: Changes in plugins-subcommands-advanced