@nectar-js/nectar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugins-CGvM19v9.js","names":["describe"],"sources":["../src/version.ts","../src/components/customId.ts","../src/compiler/diagnostics.ts","../src/compiler/load.ts","../src/compiler/segments.ts","../src/components/params.ts","../src/components/compile.ts","../src/components/registry.ts","../src/manifest/emit.ts","../src/plugins/transform.ts","../src/plugins/index.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\n\n/** From package.json, which sits one level up from both `src/` and `dist/`. */\nexport const { version } = createRequire(import.meta.url)(\"../package.json\") as {\n version: string;\n};\n","/** Discord rejects custom IDs longer than this. */\nexport const MAX_CUSTOM_ID_LENGTH = 100;\n\nconst PREFIX = \"n:\";\nconst SHORT_ID_LENGTH = 6;\n\n/** Characters a route with no parameters uses: the prefix and the short ID. */\nexport const BASE_OVERHEAD = PREFIX.length + SHORT_ID_LENGTH;\n\nexport class CustomIdTooLongError extends Error {\n constructor(\n readonly customId: string,\n readonly routeId: string,\n ) {\n super(\n `Custom ID for ${routeId} is ${customId.length} characters, Discord allows ${MAX_CUSTOM_ID_LENGTH}. Encode a shorter identifier instead of the full value.`,\n );\n this.name = \"CustomIdTooLongError\";\n }\n}\n\n/**\n * Builds `n:<shortId>:<v1>:<v2>...`. Values are escaped so they may contain `:` and `\\`.\n * Throws when the result is longer than Discord allows; it never truncates.\n */\nexport function encodeCustomId(shortId: string, values: readonly string[], routeId = shortId) {\n let out = PREFIX + shortId;\n for (const value of values) out += `:${escapeValue(value)}`;\n if (out.length > MAX_CUSTOM_ID_LENGTH) throw new CustomIdTooLongError(out, routeId);\n return out;\n}\n\nexport type DecodedCustomId =\n | { ok: true; shortId: string; values: string[] }\n | { ok: false; reason: \"not-nectar\" | \"malformed\" };\n\n/**\n * Splits a raw custom ID back into its short ID and positional values.\n * IDs without the Nectar prefix are reported as `not-nectar` so hand-built components pass through.\n */\nexport function decodeCustomId(raw: string): DecodedCustomId {\n if (!raw.startsWith(PREFIX)) return { ok: false, reason: \"not-nectar\" };\n\n const shortId = raw.slice(PREFIX.length, PREFIX.length + SHORT_ID_LENGTH);\n if (!/^[0-9a-z]{6}$/.test(shortId)) return { ok: false, reason: \"malformed\" };\n\n const values: string[] = [];\n let index = PREFIX.length + SHORT_ID_LENGTH;\n if (index === raw.length) return { ok: true, shortId, values };\n if (raw[index] !== \":\") return { ok: false, reason: \"malformed\" };\n index++;\n\n let current = \"\";\n while (index < raw.length) {\n const char = raw[index] as string;\n if (char === \"\\\\\") {\n const next = raw[index + 1];\n if (next !== \"\\\\\" && next !== \":\") return { ok: false, reason: \"malformed\" };\n current += next;\n index += 2;\n continue;\n }\n if (char === \":\") {\n values.push(current);\n current = \"\";\n index++;\n continue;\n }\n current += char;\n index++;\n }\n values.push(current);\n return { ok: true, shortId, values };\n}\n\nfunction escapeValue(value: string): string {\n return value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\":\", \"\\\\:\");\n}\n","export type Severity = \"error\" | \"warning\";\n\nexport interface Diagnostic {\n code: string;\n severity: Severity;\n message: string;\n /** Absolute path of the file or directory that caused the diagnostic. */\n file?: string;\n /** Canonical route identity, when the diagnostic is about a specific route. */\n route?: string;\n}\n\ninterface DiagnosticLocation {\n file?: string;\n route?: string;\n}\n\nexport class Diagnostics {\n readonly items: Diagnostic[] = [];\n\n error(code: string, message: string, location: DiagnosticLocation = {}): void {\n this.push(\"error\", code, message, location);\n }\n\n warn(code: string, message: string, location: DiagnosticLocation = {}): void {\n this.push(\"warning\", code, message, location);\n }\n\n get hasErrors(): boolean {\n return this.items.some((d) => d.severity === \"error\");\n }\n\n private push(\n severity: Severity,\n code: string,\n message: string,\n location: DiagnosticLocation,\n ): void {\n const item: Diagnostic = { code, severity, message };\n if (location.file !== undefined) item.file = location.file;\n if (location.route !== undefined) item.route = location.route;\n this.items.push(item);\n }\n}\n","import { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { registerHooks } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\n/**\n * Imports an application module by absolute path.\n *\n * Relies on Node's native TypeScript type stripping (unflagged since 22.18), so handler\n * files must use erasable syntax only: no enums, namespaces, or parameter properties.\n *\n * With reloading enabled (see `enableModuleReloading`) the URL carries a version query, so a\n * changed file evaluates again on the next import instead of coming back from the ESM cache.\n */\nexport async function loadModule(file: string): Promise<Record<string, unknown>> {\n const url = pathToFileURL(file).href;\n return (await import(reloading === null ? url : versioned(url))) as Record<string, unknown>;\n}\n\ninterface Reloading {\n /** Project root; only files under it (outside `node_modules`) are versioned. */\n root: string;\n /** Bumped by `invalidateModuleGraph` so every project module evaluates again. */\n generation: number;\n}\n\nlet reloading: Reloading | null = null;\n\n/**\n * Turns on cache busting for project files. Used by `nectar dev` only.\n *\n * Every import of a file under `root` gets `?nectar=<content hash>-<generation>` appended, the\n * direct ones here and the transitive ones through a resolve hook. A handler whose content\n * changed therefore gets a new URL and a fresh evaluation; its unchanged imports keep their\n * URL and are shared. Old instances stay in the ESM cache until the process exits.\n */\nexport function enableModuleReloading(root: string): void {\n if (reloading !== null) return;\n reloading = { root: path.resolve(root), generation: 0 };\n registerHooks({\n resolve(specifier, context, next) {\n const result = next(specifier, context);\n return { ...result, url: versioned(result.url) };\n },\n });\n}\n\n/**\n * Makes every project module evaluate again on its next import. For changes to files the\n * compiler does not track (helpers a handler imports), since nothing knows who imports them.\n */\nexport function invalidateModuleGraph(): void {\n if (reloading !== null) reloading.generation += 1;\n}\n\nfunction versioned(url: string): string {\n if (reloading === null || !url.startsWith(\"file:\") || url.includes(\"?\") || url.includes(\"#\")) {\n return url;\n }\n const file = fileURLToPath(url);\n const inside = !path.relative(reloading.root, file).startsWith(\"..\");\n if (!inside || file.split(path.sep).includes(\"node_modules\")) return url;\n let hash: string;\n try {\n hash = createHash(\"sha1\").update(readFileSync(file)).digest(\"base64url\").slice(0, 10);\n } catch {\n return url;\n }\n return `${url}?nectar=${hash}-${reloading.generation}`;\n}\n","export type Segment =\n | { type: \"static\"; name: string }\n | { type: \"dynamic\"; name: string }\n | { type: \"catchAll\"; name: string }\n | { type: \"group\"; name: string };\n\nexport type SegmentParseResult = { ok: true; segment: Segment } | { ok: false; reason: string };\n\nconst STATIC_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;\nconst PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Parses one directory name into a route segment.\n *\n * `name` static\n * `[name]` dynamic\n * `[...name]` catch-all\n * `(name)` group, organizational only\n */\nexport function parseSegment(dirName: string): SegmentParseResult {\n if (dirName.startsWith(\"[\") || dirName.endsWith(\"]\")) {\n if (!dirName.startsWith(\"[\") || !dirName.endsWith(\"]\")) {\n return fail(`\"${dirName}\" has an unmatched bracket. Dynamic segments look like [name].`);\n }\n const inner = dirName.slice(1, -1);\n const isCatchAll = inner.startsWith(\"...\");\n const name = isCatchAll ? inner.slice(3) : inner;\n if (!PARAM_NAME.test(name)) {\n return fail(\n `\"${dirName}\" is not a valid parameter name. Use letters, digits, and underscores, and do not start with a digit.`,\n );\n }\n return ok({ type: isCatchAll ? \"catchAll\" : \"dynamic\", name });\n }\n\n if (dirName.startsWith(\"(\") || dirName.endsWith(\")\")) {\n if (!dirName.startsWith(\"(\") || !dirName.endsWith(\")\")) {\n return fail(`\"${dirName}\" has an unmatched parenthesis. Route groups look like (name).`);\n }\n const name = dirName.slice(1, -1);\n if (!STATIC_NAME.test(name)) {\n return fail(\n `\"${dirName}\" is not a valid group name. Use letters, digits, hyphens, and underscores.`,\n );\n }\n return ok({ type: \"group\", name });\n }\n\n if (!STATIC_NAME.test(dirName)) {\n return fail(\n `\"${dirName}\" is not a valid segment name. Use letters, digits, hyphens, and underscores, and start with a letter or digit.`,\n );\n }\n return ok({ type: \"static\", name: dirName });\n}\n\n/** Renders a segment back into its directory form. Groups render as their directory name. */\nexport function formatSegment(segment: Segment): string {\n switch (segment.type) {\n case \"static\":\n return segment.name;\n case \"dynamic\":\n return `[${segment.name}]`;\n case \"catchAll\":\n return `[...${segment.name}]`;\n case \"group\":\n return `(${segment.name})`;\n }\n}\n\nfunction ok(segment: Segment): SegmentParseResult {\n return { ok: true, segment };\n}\n\nfunction fail(reason: string): SegmentParseResult {\n return { ok: false, reason };\n}\n","/**\n * A Standard Schema (https://standardschema.dev) validator, which zod, valibot, and arktype\n * all produce. Only the result's `issues` are looked at: a schema that transforms the value\n * does not change what the handler receives.\n */\nexport interface StandardSchemaLike<V = unknown> {\n \"~standard\": {\n validate(\n value: V,\n ):\n | { issues?: ReadonlyArray<unknown> | undefined }\n | Promise<{ issues?: ReadonlyArray<unknown> | undefined }>;\n };\n}\n\n/**\n * Checks one custom ID parameter. A function passes by returning anything but `false` and\n * fails by returning `false` or throwing. A schema fails by reporting issues.\n */\nexport type ParamValidator<V = string | string[]> = ((value: V) => unknown) | StandardSchemaLike<V>;\n\nexport type ParamValidators = Record<string, ParamValidator>;\n\n/**\n * Reads the validators `defineComponent` attached to a handler and checks them against the\n * route's parameters. Throws with a developer-facing message when the shape is wrong; the\n * compiler reports it as a diagnostic and the runtime as a load error.\n */\nexport function paramValidatorsOf(\n handler: unknown,\n route: { params: readonly string[] },\n): ParamValidators {\n const declared = (handler as { params?: unknown }).params;\n if (declared === undefined) return {};\n if (typeof declared !== \"object\" || declared === null || Array.isArray(declared)) {\n throw new Error(`\\`params\\` must be an object of validators, got ${typeof declared}.`);\n }\n const validators: ParamValidators = {};\n for (const [name, validator] of Object.entries(declared)) {\n if (!route.params.includes(name)) {\n throw new Error(\n `\\`params\\` validates \"${name}\", which is not a parameter of this route. ${\n route.params.length === 0 ? \"It has none.\" : `It has: ${route.params.join(\", \")}.`\n }`,\n );\n }\n if (!isValidator(validator)) {\n throw new Error(\n `\\`params.${name}\\` must be a function or a Standard Schema, got ${typeof validator}.`,\n );\n }\n validators[name] = validator;\n }\n return validators;\n}\n\nfunction isValidator(value: unknown): value is ParamValidator {\n if (typeof value === \"function\") return true;\n if (typeof value !== \"object\" || value === null) return false;\n const standard = (value as Record<string, unknown>)[\"~standard\"];\n return (\n typeof standard === \"object\" &&\n standard !== null &&\n typeof (standard as Record<string, unknown>).validate === \"function\"\n );\n}\n\n/**\n * Runs every validator against the decoded parameters. Resolves to the first parameter that\n * failed, or `null` when all passed. A validator that throws counts as a failure; the\n * caller decides what to log, so the value never leaves this function.\n */\nexport async function findInvalidParam(\n validators: ParamValidators,\n params: Record<string, string | string[]>,\n): Promise<string | null> {\n for (const [name, validator] of Object.entries(validators)) {\n const value = params[name];\n if (value === undefined) return name;\n try {\n if (typeof validator === \"function\") {\n if ((await validator(value)) === false) return name;\n continue;\n }\n const result = await validator[\"~standard\"].validate(value);\n if (result.issues !== undefined && result.issues.length > 0) return name;\n } catch {\n return name;\n }\n }\n return null;\n}\n","import path from \"node:path\";\nimport { Diagnostics } from \"../compiler/diagnostics.js\";\nimport { loadModule } from \"../compiler/load.js\";\nimport type { Route, RouteTable } from \"../compiler/routes.js\";\nimport { formatSegment } from \"../compiler/segments.js\";\nimport { BASE_OVERHEAD, encodeCustomId, MAX_CUSTOM_ID_LENGTH } from \"./customId.js\";\nimport { paramValidatorsOf } from \"./params.js\";\n\nexport type ComponentKind = \"button\" | \"select\" | \"modal\";\n\nexport type SelectKind = \"string\" | \"user\" | \"role\" | \"channel\" | \"mentionable\";\n\nconst SELECT_KINDS: ReadonlySet<string> = new Set([\n \"string\",\n \"user\",\n \"role\",\n \"channel\",\n \"mentionable\",\n]);\n\nexport interface ComponentRoute extends Route {\n category: \"component\";\n kind: ComponentKind;\n /** The `kind` export of a `select.ts`. `null` for buttons and modals. */\n selectKind: SelectKind | null;\n /** Name of the trailing catch-all parameter, if the route has one. */\n catchAll: string | null;\n /** Characters of the encoded custom ID taken by the prefix, short ID, and separators. */\n overhead: number;\n}\n\nexport interface CompiledComponents {\n routes: ComponentRoute[];\n diagnostics: Diagnostics;\n}\n\n/** Values for one route's parameters. A catch-all parameter takes an array. */\nexport type ComponentParams = Record<string, string | readonly string[]>;\n\n/** Validates the component routes of a route table and resolves their select kinds. */\nexport async function compileComponents(table: RouteTable): Promise<CompiledComponents> {\n const diagnostics = new Diagnostics();\n const candidates = table.routes.filter((r) => r.category === \"component\");\n\n // Each route reports into its own list, so diagnostics come out in route order rather than\n // in whatever order the imports finish.\n const results = await Promise.all(\n candidates.map(async (route) => {\n const own = new Diagnostics();\n return { route: await compileRoute(route, own), diagnostics: own };\n }),\n );\n const routes: ComponentRoute[] = [];\n for (const result of results) {\n diagnostics.items.push(...result.diagnostics.items);\n if (result.route !== null) routes.push(result.route);\n }\n\n detectShortIdCollisions(routes, diagnostics);\n detectDuplicatePatterns(routes, diagnostics);\n\n return { routes, diagnostics };\n}\n\n/** What the encoder needs from a route. Compiled, manifest, and registered routes all satisfy it. */\nexport interface EncodableRoute {\n id: string;\n shortId: string;\n params: string[];\n catchAll: string | null;\n}\n\n/**\n * Encodes a custom ID for a compiled route. Throws when a parameter is missing, a value is not\n * a string, or the result exceeds Discord's limit.\n */\nexport function customIdFor(route: EncodableRoute, params: ComponentParams = {}): string {\n const values: string[] = [];\n for (const name of route.params) {\n const value = params[name];\n if (name === route.catchAll) {\n if (value === undefined) continue;\n if (typeof value === \"string\") {\n values.push(value);\n continue;\n }\n values.push(...value);\n continue;\n }\n if (typeof value !== \"string\") {\n throw new TypeError(\n `Route ${route.id} needs a string for parameter \"${name}\", got ${describe(value)}.`,\n );\n }\n values.push(value);\n }\n for (const name of Object.keys(params)) {\n if (!route.params.includes(name)) {\n throw new TypeError(\n `Route ${route.id} has no parameter \"${name}\". ${route.params.length === 0 ? \"It takes none.\" : `It takes: ${route.params.join(\", \")}.`}`,\n );\n }\n }\n return encodeCustomId(route.shortId, values, route.id);\n}\n\nasync function compileRoute(\n route: Route,\n diagnostics: Diagnostics,\n): Promise<ComponentRoute | null> {\n const kind = route.kind as ComponentKind;\n const last = route.segments.at(-1);\n const catchAll = last?.type === \"catchAll\" ? last.name : null;\n const overhead = BASE_OVERHEAD + route.params.length;\n\n if (catchAll !== null) {\n diagnostics.warn(\n \"catch-all-route\",\n `${formatSegment(last as NonNullable<typeof last>)} accepts any number of values. Every value counts against Discord's ${MAX_CUSTOM_ID_LENGTH} character custom ID limit, and generation throws when it is exceeded.`,\n { file: route.file, route: route.id },\n );\n }\n\n let module: Record<string, unknown>;\n try {\n module = await loadModule(route.file);\n } catch (error) {\n diagnostics.error(\n \"module-load-failed\",\n `Could not import this file: ${error instanceof Error ? error.message : String(error)}`,\n { file: route.file, route: route.id },\n );\n return null;\n }\n\n if (!checkDeclaredRoute(module, route, diagnostics)) return null;\n try {\n paramValidatorsOf(module.default, route);\n } catch (error) {\n diagnostics.error(\n \"invalid-param-validator\",\n `${error instanceof Error ? error.message : String(error)} Pass validators as defineComponent's third argument: { params: { name: (value) => ... } }.`,\n { file: route.file, route: route.id },\n );\n return null;\n }\n\n let selectKind: SelectKind | null = null;\n if (kind === \"select\") {\n selectKind = validateSelectKind(module, route, diagnostics);\n if (selectKind === null) return null;\n }\n\n return { ...route, category: \"component\", kind, selectKind, catchAll, overhead };\n}\n\n/** A handler made with `defineComponent(path, ...)` must name the route its file sits in. */\nexport function checkDeclaredRoute(\n module: Record<string, unknown>,\n route: Route,\n diagnostics: Diagnostics,\n expected = route.path,\n): boolean {\n const handler = module.default;\n if (typeof handler !== \"function\") return true;\n const declared = (handler as { route?: unknown }).route;\n if (declared === undefined || declared === expected) return true;\n diagnostics.error(\n \"route-mismatch\",\n `This file is the route \"${expected}\" but its handler declares \"${String(declared)}\". Update the string or move the file.`,\n { file: route.file, route: route.id },\n );\n return false;\n}\n\nfunction validateSelectKind(\n module: Record<string, unknown>,\n route: Route,\n diagnostics: Diagnostics,\n): SelectKind | null {\n const kind = module.kind;\n if (kind === undefined) {\n diagnostics.error(\n \"missing-select-kind\",\n 'select.ts must export `kind`: \"string\", \"user\", \"role\", \"channel\", or \"mentionable\".',\n { file: route.file, route: route.id },\n );\n return null;\n }\n if (typeof kind !== \"string\" || !SELECT_KINDS.has(kind)) {\n diagnostics.error(\n \"invalid-select-kind\",\n `\\`kind\\` is ${describe(kind)}. Expected \"string\", \"user\", \"role\", \"channel\", or \"mentionable\".`,\n { file: route.file, route: route.id },\n );\n return null;\n }\n return kind as SelectKind;\n}\n\nfunction detectShortIdCollisions(routes: ComponentRoute[], diagnostics: Diagnostics): void {\n const seen = new Map<string, ComponentRoute>();\n for (const route of routes) {\n const existing = seen.get(route.shortId);\n if (existing === undefined || existing.id === route.id) {\n seen.set(route.shortId, route);\n continue;\n }\n diagnostics.error(\n \"short-id-collision\",\n `${route.id} and ${existing.id} hash to the same short ID \"${route.shortId}\", so their custom IDs would be indistinguishable. Rename one of the directories.`,\n { file: route.file, route: route.id },\n );\n }\n}\n\n/**\n * Two routes of the same kind whose paths differ only in parameter names, like\n * `tickets/[id]/close` and `tickets/[ticketId]/close`, would both claim the same custom IDs.\n */\nfunction detectDuplicatePatterns(routes: ComponentRoute[], diagnostics: Diagnostics): void {\n const seen = new Map<string, ComponentRoute>();\n for (const route of routes) {\n const shape = route.segments\n .filter((s) => s.type !== \"group\")\n .map((s) => (s.type === \"static\" ? s.name : s.type === \"dynamic\" ? \"[]\" : \"[...]\"))\n .join(\"/\");\n const key = `${route.kind}#${shape}`;\n const existing = seen.get(key);\n if (existing === undefined) {\n seen.set(key, route);\n continue;\n }\n if (existing.id === route.id) continue;\n diagnostics.error(\n \"duplicate-component-pattern\",\n `${route.id} has the same shape as ${existing.id} (${relative(existing.file)}). Parameter names do not make routes distinct.`,\n { file: route.file, route: route.id },\n );\n }\n}\n\nfunction describe(value: unknown): string {\n return typeof value === \"string\" ? JSON.stringify(value) : typeof value;\n}\n\nfunction relative(file: string): string {\n return path.relative(process.cwd(), file).split(path.sep).join(\"/\");\n}\n","import { type ComponentParams, customIdFor, type EncodableRoute } from \"./compile.js\";\n\nexport interface RegisteredComponentRoute extends EncodableRoute {\n path: string;\n}\n\n/**\n * Component routes the running app knows about, keyed by path. The runtime fills this from\n * the manifest before any handler runs, so `customId()` never needs the manifest itself.\n */\nconst routes = new Map<string, RegisteredComponentRoute>();\n\nexport function registerComponentRoutes(list: Iterable<RegisteredComponentRoute>): void {\n routes.clear();\n for (const route of list) routes.set(route.path, route);\n}\n\nexport function encodeComponentRoute(path: string, params: ComponentParams): string {\n const route = routes.get(path);\n if (route === undefined) {\n throw new Error(\n routes.size === 0\n ? `customId(\"${path}\") was called before the runtime registered any routes. Call it from a handler, or from code that runs after start().`\n : `No component route \"${path}\". Check the directory name under components/.`,\n );\n }\n return customIdFor(route, params);\n}\n","import { mkdirSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { RouteGraph } from \"../compiler/graph.js\";\nimport type { Route } from \"../compiler/routes.js\";\nimport { version } from \"../version.js\";\nimport { MANIFEST_VERSION, type Manifest, type ManifestRoute } from \"./schema.js\";\n\nexport const MANIFEST_FILE = \"manifest.json\";\n\n/** Serializes a route graph. `outDir` is where the manifest will live; paths are made relative to it. */\nexport function toManifest(graph: RouteGraph, outDir: string): Manifest {\n const rel = (file: string) => posix(path.relative(graph.appDir, file));\n const base = (route: Route) => {\n const chains = graph.chains.get(route.file) ?? { middleware: [], errors: [] };\n return {\n id: route.id,\n category: route.category,\n path: route.path,\n file: rel(route.file),\n middleware: chains.middleware.map(rel),\n errors: chains.errors.map(rel),\n plugins: graph.plugins.get(route.file) ?? [],\n };\n };\n\n const routes: ManifestRoute[] = [];\n for (const command of graph.commands) {\n for (const route of Object.values(command.handlers))\n routes.push({ ...base(route), kind: \"command\" });\n }\n for (const entry of graph.autocomplete) {\n routes.push({ ...base(entry.route), kind: \"autocomplete\", options: entry.options });\n }\n for (const route of graph.components) {\n routes.push({\n ...base(route),\n kind: route.kind,\n shortId: route.shortId,\n params: route.params,\n catchAll: route.catchAll,\n selectKind: route.selectKind,\n overhead: route.overhead,\n });\n }\n for (const event of graph.events) {\n for (const handler of event.handlers) {\n routes.push({\n ...base(handler.route),\n kind: \"event\",\n event: event.name,\n once: handler.once,\n order: handler.order,\n });\n }\n }\n routes.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id));\n\n return {\n version: MANIFEST_VERSION,\n nectar: version,\n appDir: posix(path.relative(path.resolve(outDir), graph.appDir)),\n routes,\n commands: graph.commands.map((c) => ({\n name: c.name,\n type: c.type,\n payload: c.payload,\n handlers: Object.fromEntries(Object.entries(c.handlers).map(([k, r]) => [k, r.id])),\n })),\n events: graph.events.map((e) => ({\n name: e.name,\n mode: e.mode,\n handlers: e.handlers.map((h) => h.route.id),\n })),\n };\n}\n\n/** Writes `manifest.json` into `outDir` with sorted keys, so identical graphs give identical bytes. */\nexport function writeManifest(manifest: Manifest, outDir: string): string {\n mkdirSync(outDir, { recursive: true });\n const file = path.join(outDir, MANIFEST_FILE);\n writeFileSync(file, `${stableStringify(manifest)}\\n`);\n return file;\n}\n\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, v: unknown) => (isPlainObject(v) ? sortKeys(v) : v), 2);\n}\n\nfunction sortKeys(object: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(object).sort()) out[key] = object[key];\n return out;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction posix(file: string): string {\n return file.split(path.sep).join(\"/\");\n}\n","import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { RouteGraph } from \"../compiler/graph.js\";\nimport { toManifest } from \"../manifest/emit.js\";\nimport type { NectarPlugin, PluginChange, PluginGraph } from \"./index.js\";\n\n/** A frozen copy of the graph in manifest shape, with absolute file paths. */\nexport function pluginGraph(graph: RouteGraph): PluginGraph {\n const manifest = toManifest(graph, graph.appDir);\n const absolute = (file: string) => path.join(graph.appDir, ...file.split(\"/\"));\n // Cloned because the manifest shares arrays and payloads with the graph itself.\n return deepFreeze(\n structuredClone({\n appDir: graph.appDir,\n routes: manifest.routes.map((route) => ({\n ...route,\n file: absolute(route.file),\n middleware: route.middleware.map(absolute),\n errors: route.errors.map(absolute),\n })),\n commands: manifest.commands,\n events: manifest.events,\n }),\n );\n}\n\n/**\n * Runs every plugin's `transform` in config order and applies the returned changes to the\n * graph. Problems become diagnostics; a plugin never mutates the graph directly.\n */\nexport async function applyPlugins(\n graph: RouteGraph,\n plugins: readonly NectarPlugin[],\n): Promise<void> {\n for (const plugin of plugins) {\n if (plugin.transform === undefined) continue;\n let changes: PluginChange[];\n try {\n changes = (await plugin.transform(pluginGraph(graph))) ?? [];\n } catch (error) {\n graph.diagnostics.error(\n \"plugin-failed\",\n `Plugin \"${plugin.name}\" threw while transforming routes: ${describe(error)}`,\n );\n continue;\n }\n if (!Array.isArray(changes)) {\n graph.diagnostics.error(\n \"plugin-invalid-change\",\n `Plugin \"${plugin.name}\" returned ${typeof changes} from transform. Return an array of changes, or nothing.`,\n );\n continue;\n }\n for (const change of changes) apply(graph, plugin.name, change);\n }\n}\n\nfunction apply(graph: RouteGraph, plugin: string, change: PluginChange): void {\n const type: unknown = isRecord(change) ? change.type : undefined;\n if (type !== \"middleware\" && type !== \"diagnostic\") {\n graph.diagnostics.error(\n \"plugin-invalid-change\",\n `Plugin \"${plugin}\" returned a change of type ${JSON.stringify(type)}. Known types: middleware, diagnostic.`,\n );\n return;\n }\n if (change.type === \"diagnostic\") {\n const { severity, code, message, file, route } = change;\n const where = {\n ...(file === undefined ? {} : { file }),\n ...(route === undefined ? {} : { route }),\n };\n if (severity === \"error\") graph.diagnostics.error(code, message, where);\n else graph.diagnostics.warn(code, message, where);\n return;\n }\n\n const routes = graph.routes.filter(\n (r) => r.id === change.route && (change.kind === undefined || r.kind === change.kind),\n );\n const target = change.kind === undefined ? change.route : `${change.route} (${change.kind})`;\n if (routes.length === 0) {\n graph.diagnostics.error(\n \"plugin-unknown-route\",\n `Plugin \"${plugin}\" adds middleware to route \"${target}\", which does not exist.`,\n );\n return;\n }\n if (routes.some((r) => r.category === \"event\")) {\n graph.diagnostics.error(\n \"plugin-invalid-change\",\n `Plugin \"${plugin}\" adds middleware to route \"${target}\", but event handlers don't run middleware.`,\n { route: change.route },\n );\n return;\n }\n if (\n typeof change.file !== \"string\" ||\n !path.isAbsolute(change.file) ||\n !existsSync(change.file)\n ) {\n graph.diagnostics.error(\n \"plugin-missing-file\",\n `Plugin \"${plugin}\" adds middleware from ${JSON.stringify(change.file)}, which is not an absolute path to an existing file.`,\n { route: change.route },\n );\n return;\n }\n const file = path.normalize(change.file);\n for (const route of routes) {\n const chains = graph.chains.get(route.file);\n if (chains === undefined || chains.middleware.includes(file)) continue;\n if (change.position === \"inner\") chains.middleware.push(file);\n else chains.middleware.unshift(file);\n const touched = graph.plugins.get(route.file) ?? [];\n if (!touched.includes(plugin)) graph.plugins.set(route.file, [...touched, plugin]);\n }\n}\n\nfunction deepFreeze<T>(value: T): T {\n if (typeof value === \"object\" && value !== null && !Object.isFrozen(value)) {\n Object.freeze(value);\n for (const inner of Object.values(value)) deepFreeze(inner);\n }\n return value;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n","import type { Client } from \"discord.js\";\nimport type { Project } from \"../cli/project.js\";\nimport type { Severity } from \"../compiler/diagnostics.js\";\nimport type { RouteKind } from \"../compiler/routes.js\";\nimport type { NectarServices } from \"../index.js\";\nimport type {\n Manifest,\n ManifestCommand,\n ManifestEvent,\n ManifestRoute,\n} from \"../manifest/schema.js\";\nimport type { SignalEmitter } from \"../runtime/signals.js\";\nimport type { Env, Logger } from \"../runtime/types.js\";\n\n/**\n * A plugin takes part in compilation and the runtime lifecycle. A library that only exports\n * functions for handlers to call does not need to be one.\n */\nexport interface NectarPlugin {\n /** Unique among the configured plugins. Named in diagnostics and in the manifest. */\n name: string;\n version?: string;\n /**\n * Runs after the route graph is validated and before the manifest is written. The graph is\n * frozen; return changes and the compiler applies and checks them. Plugins run in config\n * order, each seeing the changes of the ones before it.\n */\n transform?(graph: PluginGraph): Maybe<PluginChange[]> | Promise<Maybe<PluginChange[]>>;\n /** Declarations appended to `.nectar/types.d.ts`. */\n types?(graph: PluginGraph): Maybe<string>;\n /** Extra `nectar <name>` commands. */\n commands?: PluginCommand[];\n /**\n * Runs when the runtime starts, before any handler is imported and before login. A sharded\n * bot runs it in every process. Returned services land on `ctx.services` for every handler\n * and middleware.\n */\n start?(app: PluginApp): Maybe<Partial<NectarServices>> | Promise<Maybe<Partial<NectarServices>>>;\n /** Runs on shutdown, after in-flight interactions drain and before the client is destroyed. */\n stop?(app: PluginApp): void | Promise<void>;\n /**\n * Runs once per application, in the process that runs shard 0, after every `start`. For work\n * that must not repeat per shard process: a scheduled job, a web server, posting stats.\n */\n startGlobal?(app: PluginApp): void | Promise<void>;\n /** Runs on shutdown in the process that ran `startGlobal`, before any `stop`. */\n stopGlobal?(app: PluginApp): void | Promise<void>;\n}\n\n/** A hook may return nothing, so a body without `return` type-checks. */\n// biome-ignore lint/suspicious/noConfusingVoidType: that is the point\ntype Maybe<T> = T | undefined | void;\n\nexport type PluginChange =\n | {\n type: \"middleware\";\n /** Route ID, `<category>:<path>`. */\n route: string;\n /**\n * Only the route of this kind. A command and its autocomplete share an ID; without\n * `kind`, both get the middleware, as they would from a `middleware.ts`.\n */\n kind?: RouteKind;\n /** Absolute path of a module whose default export is a middleware. */\n file: string;\n /** `outer` (default) runs before the app's own middleware, `inner` right before the handler. */\n position?: \"outer\" | \"inner\";\n }\n | {\n type: \"diagnostic\";\n severity: Severity;\n code: string;\n message: string;\n file?: string;\n route?: string;\n };\n\ntype DeepReadonly<T> = T extends (infer U)[]\n ? readonly DeepReadonly<U>[]\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n/** The compiled app as a plugin sees it: the manifest shape with absolute file paths, frozen. */\nexport interface PluginGraph {\n readonly appDir: string;\n readonly routes: DeepReadonly<ManifestRoute[]>;\n readonly commands: DeepReadonly<ManifestCommand[]>;\n readonly events: DeepReadonly<ManifestEvent[]>;\n}\n\nexport interface PluginApp {\n readonly client: Client;\n readonly env: Env;\n readonly logger: Logger;\n readonly signals: SignalEmitter;\n readonly manifest: Manifest;\n}\n\nexport interface PluginCommand {\n name: string;\n description: string;\n options?: Record<string, { type: \"boolean\" | \"string\"; description: string }>;\n /** Returns the exit code. */\n run(ctx: PluginCommandContext): number | Promise<number>;\n}\n\nexport interface PluginCommandContext {\n project: Project;\n flags: Record<string, string | boolean | undefined>;\n out(line: string): void;\n err(line: string): void;\n}\n\nexport function definePlugin(plugin: NectarPlugin): NectarPlugin {\n return plugin;\n}\n\n/** A plugin misbehaved: threw from a hook, or provided something that clashes. */\nexport class PluginError extends Error {\n constructor(\n readonly plugin: string,\n readonly detail: string,\n ) {\n super(`Plugin \"${plugin}\": ${detail}`);\n this.name = \"PluginError\";\n }\n}\n\nexport { applyPlugins, pluginGraph } from \"./transform.js\";\n"],"mappings":"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"}
@@ -0,0 +1,545 @@
1
+ import { o as stableStringify } from "./plugins-CGvM19v9.js";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { GatewayIntentBits, IntentsBitField } from "discord.js";
6
+ import { ApplicationCommandType, ApplicationIntegrationType, Routes } from "discord-api-types/v10";
7
+ //#region src/config.ts
8
+ function defineConfig(config) {
9
+ return config;
10
+ }
11
+ /** The config one environment runs with: `environments[env]` over the rest. */
12
+ function configFor(config, env) {
13
+ const { environments, ...base } = config;
14
+ return {
15
+ ...base,
16
+ ...environments?.[env]
17
+ };
18
+ }
19
+ var ConfigError = class extends Error {
20
+ file;
21
+ detail;
22
+ constructor(file, detail) {
23
+ super(`${file}: ${detail}`);
24
+ this.file = file;
25
+ this.detail = detail;
26
+ this.name = "ConfigError";
27
+ }
28
+ };
29
+ const ENVS = /* @__PURE__ */ new Set([
30
+ "development",
31
+ "test",
32
+ "production"
33
+ ]);
34
+ const LEVELS = /* @__PURE__ */ new Set([
35
+ "debug",
36
+ "info",
37
+ "warn",
38
+ "error"
39
+ ]);
40
+ /** Checks a loaded config's shape. Discord validates intent and partial values itself at login. */
41
+ function validateConfig(value, file) {
42
+ const fail = (detail) => {
43
+ throw new ConfigError(file, detail);
44
+ };
45
+ if (!isRecord(value)) fail("the default export must be an object. Use defineConfig({ ... }).");
46
+ const config = value;
47
+ for (const key of ["token", "applicationId"]) if (config[key] !== void 0 && typeof config[key] !== "string") fail(`\`${key}\` must be a string, usually read from process.env.`);
48
+ if (config.intents === void 0) fail("`intents` is required. Use [] for none.");
49
+ if (!isBitfield(config.intents)) fail("`intents` must be an array of intent names or bits, a single bit, or a bigint.");
50
+ if (config.partials !== void 0 && !Array.isArray(config.partials)) fail("`partials` must be an array.");
51
+ if (config.client !== void 0 && !isRecord(config.client)) fail("`client` must be an object.");
52
+ if (config.eager !== void 0 && typeof config.eager !== "boolean") fail("`eager` must be a boolean.");
53
+ if (config.env !== void 0 && (typeof config.env !== "string" || !ENVS.has(config.env))) fail("`env` must be \"development\", \"test\", or \"production\".");
54
+ if (config.logger !== void 0) {
55
+ if (!isRecord(config.logger)) fail("`logger` must be an object.");
56
+ const { level, sink } = config.logger;
57
+ if (level !== void 0 && (typeof level !== "string" || !LEVELS.has(level))) fail("`logger.level` must be \"debug\", \"info\", \"warn\", or \"error\".");
58
+ if (sink !== void 0 && typeof sink !== "function") fail("`logger.sink` must be a function that receives log records.");
59
+ }
60
+ if (config.observe !== void 0 && typeof config.observe !== "function") fail("`observe` must be a function that receives signals.");
61
+ if (config.plugins !== void 0) validatePlugins(config.plugins, fail);
62
+ for (const key of ["appDir", "outDir"]) {
63
+ const dir = config[key];
64
+ if (dir !== void 0 && (typeof dir !== "string" || dir === "")) fail(`\`${key}\` must be a non-empty string.`);
65
+ }
66
+ if (config.dev !== void 0) {
67
+ if (!isRecord(config.dev)) fail("`dev` must be an object.");
68
+ const guilds = config.dev.guilds;
69
+ if (guilds !== void 0 && !isGuildList(guilds)) fail("`dev.guilds` must be an array of guild ID strings.");
70
+ }
71
+ if (config.commands !== void 0) {
72
+ if (!isRecord(config.commands)) fail("`commands` must be an object.");
73
+ const target = config.commands.target;
74
+ if (target !== void 0 && target !== "global" && !isGuildList(target)) fail("`commands.target` must be \"global\" or an array of guild ID strings.");
75
+ }
76
+ if (config.environments !== void 0) validateEnvironments(config, file, fail);
77
+ return config;
78
+ }
79
+ /** Each override must name an environment, and the config it produces must be valid. */
80
+ function validateEnvironments(config, file, fail) {
81
+ if (!isRecord(config.environments)) fail("`environments` must be an object.");
82
+ for (const [name, override] of Object.entries(config.environments)) {
83
+ const where = `\`environments.${name}\``;
84
+ if (!ENVS.has(name)) fail(`${where}: use "development", "test", or "production" as the key.`);
85
+ if (!isRecord(override)) fail(`${where} must be an object.`);
86
+ for (const key of ["env", "environments"]) if (key in override) fail(`${where} cannot set \`${key}\`.`);
87
+ try {
88
+ validateConfig(configFor(config, name), file);
89
+ } catch (error) {
90
+ if (error instanceof ConfigError) fail(`${where}: ${error.detail}`);
91
+ throw error;
92
+ }
93
+ }
94
+ }
95
+ /** Names `nectar` already answers to. A plugin command cannot take one. */
96
+ const BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
97
+ "dev",
98
+ "build",
99
+ "check",
100
+ "routes",
101
+ "manifest",
102
+ "sync",
103
+ "start",
104
+ "clean",
105
+ "info",
106
+ "help"
107
+ ]);
108
+ function validatePlugins(value, fail) {
109
+ if (!Array.isArray(value)) fail("`plugins` must be an array of plugins.");
110
+ const names = /* @__PURE__ */ new Set();
111
+ const commands = /* @__PURE__ */ new Map();
112
+ value.forEach((plugin, index) => {
113
+ if (!isRecord(plugin) || typeof plugin.name !== "string" || plugin.name === "") fail(`\`plugins[${index}]\` must be an object with a non-empty \`name\`. Use definePlugin({ ... }).`);
114
+ const name = plugin.name;
115
+ if (names.has(name)) fail(`Plugin "${name}" is listed twice.`);
116
+ names.add(name);
117
+ for (const hook of [
118
+ "transform",
119
+ "types",
120
+ "start",
121
+ "stop",
122
+ "startGlobal",
123
+ "stopGlobal"
124
+ ]) if (plugin[hook] !== void 0 && typeof plugin[hook] !== "function") fail(`Plugin "${name}": \`${hook}\` must be a function.`);
125
+ if (plugin.commands === void 0) return;
126
+ if (!Array.isArray(plugin.commands)) fail(`Plugin "${name}": \`commands\` must be an array.`);
127
+ for (const command of plugin.commands) {
128
+ if (!isRecord(command) || typeof command.name !== "string" || !/^[a-z][a-z0-9-]*$/.test(command.name) || typeof command.description !== "string" || typeof command.run !== "function") fail(`Plugin "${name}": every command needs a lowercase \`name\`, a \`description\`, and a \`run\` function.`);
129
+ const commandName = command.name;
130
+ if (BUILTIN_COMMANDS.has(commandName)) fail(`Plugin "${name}": command "${commandName}" is built into nectar. Pick another name.`);
131
+ const owner = commands.get(commandName);
132
+ if (owner !== void 0 && owner !== name) fail(`Plugins "${owner}" and "${name}" both define the command "${commandName}".`);
133
+ commands.set(commandName, name);
134
+ }
135
+ });
136
+ }
137
+ function isGuildList(value) {
138
+ return Array.isArray(value) && value.every((g) => typeof g === "string" && /^\d+$/.test(g));
139
+ }
140
+ function isRecord(value) {
141
+ return typeof value === "object" && value !== null && !Array.isArray(value);
142
+ }
143
+ function isBitfield(value) {
144
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "string") return true;
145
+ if (Array.isArray(value)) return value.every((v) => typeof v === "number" || typeof v === "string" || typeof v === "bigint");
146
+ return isRecord(value) && "bitfield" in value;
147
+ }
148
+ //#endregion
149
+ //#region src/events/intents.ts
150
+ const PRIVILEGED = /* @__PURE__ */ new Set([
151
+ "GuildMembers",
152
+ "GuildPresences",
153
+ "MessageContent"
154
+ ]);
155
+ const guildOrDm = (guild, dm) => [guild, dm];
156
+ /**
157
+ * Which intent a gateway event needs. A list means any one of them is enough: message events
158
+ * arrive with either the guild or the direct message intent. Events missing here need none.
159
+ */
160
+ const REQUIRED = {
161
+ guildCreate: ["Guilds"],
162
+ guildUpdate: ["Guilds"],
163
+ guildDelete: ["Guilds"],
164
+ guildAvailable: ["Guilds"],
165
+ guildUnavailable: ["Guilds"],
166
+ channelCreate: ["Guilds"],
167
+ channelUpdate: ["Guilds"],
168
+ channelDelete: ["Guilds"],
169
+ channelPinsUpdate: ["Guilds"],
170
+ threadCreate: ["Guilds"],
171
+ threadUpdate: ["Guilds"],
172
+ threadDelete: ["Guilds"],
173
+ threadListSync: ["Guilds"],
174
+ threadMemberUpdate: ["Guilds"],
175
+ threadMembersUpdate: ["GuildMembers"],
176
+ stageInstanceCreate: ["Guilds"],
177
+ stageInstanceUpdate: ["Guilds"],
178
+ stageInstanceDelete: ["Guilds"],
179
+ roleCreate: ["Guilds"],
180
+ roleUpdate: ["Guilds"],
181
+ roleDelete: ["Guilds"],
182
+ guildMemberAdd: ["GuildMembers"],
183
+ guildMemberUpdate: ["GuildMembers"],
184
+ guildMemberRemove: ["GuildMembers"],
185
+ guildMemberAvailable: ["GuildMembers"],
186
+ guildMembersChunk: ["GuildMembers"],
187
+ userUpdate: ["GuildMembers"],
188
+ guildBanAdd: ["GuildModeration"],
189
+ guildBanRemove: ["GuildModeration"],
190
+ guildAuditLogEntryCreate: ["GuildModeration"],
191
+ emojiCreate: ["GuildExpressions"],
192
+ emojiUpdate: ["GuildExpressions"],
193
+ emojiDelete: ["GuildExpressions"],
194
+ stickerCreate: ["GuildExpressions"],
195
+ stickerUpdate: ["GuildExpressions"],
196
+ stickerDelete: ["GuildExpressions"],
197
+ guildSoundboardSoundCreate: ["GuildExpressions"],
198
+ guildSoundboardSoundUpdate: ["GuildExpressions"],
199
+ guildSoundboardSoundDelete: ["GuildExpressions"],
200
+ guildSoundboardSoundsUpdate: ["GuildExpressions"],
201
+ guildIntegrationsUpdate: ["GuildIntegrations"],
202
+ webhooksUpdate: ["GuildWebhooks"],
203
+ inviteCreate: ["GuildInvites"],
204
+ inviteDelete: ["GuildInvites"],
205
+ voiceStateUpdate: ["GuildVoiceStates"],
206
+ voiceChannelEffectSend: ["GuildVoiceStates"],
207
+ presenceUpdate: ["GuildPresences"],
208
+ messageCreate: guildOrDm("GuildMessages", "DirectMessages"),
209
+ messageUpdate: guildOrDm("GuildMessages", "DirectMessages"),
210
+ messageDelete: guildOrDm("GuildMessages", "DirectMessages"),
211
+ messageDeleteBulk: ["GuildMessages"],
212
+ messageReactionAdd: guildOrDm("GuildMessageReactions", "DirectMessageReactions"),
213
+ messageReactionRemove: guildOrDm("GuildMessageReactions", "DirectMessageReactions"),
214
+ messageReactionRemoveAll: guildOrDm("GuildMessageReactions", "DirectMessageReactions"),
215
+ messageReactionRemoveEmoji: guildOrDm("GuildMessageReactions", "DirectMessageReactions"),
216
+ typingStart: guildOrDm("GuildMessageTyping", "DirectMessageTyping"),
217
+ messagePollVoteAdd: guildOrDm("GuildMessagePolls", "DirectMessagePolls"),
218
+ messagePollVoteRemove: guildOrDm("GuildMessagePolls", "DirectMessagePolls"),
219
+ guildScheduledEventCreate: ["GuildScheduledEvents"],
220
+ guildScheduledEventUpdate: ["GuildScheduledEvents"],
221
+ guildScheduledEventDelete: ["GuildScheduledEvents"],
222
+ guildScheduledEventUserAdd: ["GuildScheduledEvents"],
223
+ guildScheduledEventUserRemove: ["GuildScheduledEvents"],
224
+ autoModerationRuleCreate: ["AutoModerationConfiguration"],
225
+ autoModerationRuleUpdate: ["AutoModerationConfiguration"],
226
+ autoModerationRuleDelete: ["AutoModerationConfiguration"],
227
+ autoModerationActionExecution: ["AutoModerationExecution"]
228
+ };
229
+ /** The intents an event route depends on, for tooling. Empty when it needs none. */
230
+ function requiredIntents(event) {
231
+ return REQUIRED[event] ?? [];
232
+ }
233
+ /**
234
+ * One warning per event whose handlers can never fire with the configured intents. Never
235
+ * changes the config: privileged intents in particular must be a deliberate choice, made here
236
+ * and in the developer portal.
237
+ */
238
+ function checkIntents(events, intents, configFile) {
239
+ const enabled = new IntentsBitField(IntentsBitField.resolve(intents));
240
+ const diagnostics = [];
241
+ for (const event of events) {
242
+ const needed = requiredIntents(event.name);
243
+ if (needed.length === 0 || needed.some((intent) => enabled.has(GatewayIntentBits[intent]))) continue;
244
+ const first = event.handlers[0];
245
+ if (first === void 0) continue;
246
+ const names = needed.map((i) => `"${i}"`).join(" or ");
247
+ const privileged = needed.filter((i) => PRIVILEGED.has(i));
248
+ diagnostics.push({
249
+ code: "missing-intent",
250
+ severity: "warning",
251
+ message: `"${event.name}" never fires without the ${names} intent. Add it to \`intents\` in ${configFile}.${privileged.length === 0 ? "" : ` ${privileged.map((i) => `"${i}"`).join(" and ")} is privileged: enable it under Bot in the Discord developer portal as well.`}`,
252
+ file: first.route.file,
253
+ route: first.route.id
254
+ });
255
+ }
256
+ return diagnostics;
257
+ }
258
+ //#endregion
259
+ //#region src/registration/normalize.ts
260
+ /** `<type>:<name>`. Discord allows the same name across command types. */
261
+ function commandKey(command) {
262
+ return `${command.type ?? ApplicationCommandType.ChatInput}:${command.name}`;
263
+ }
264
+ function normalizeCommand(command) {
265
+ const c = command;
266
+ const type = c.type ?? ApplicationCommandType.ChatInput;
267
+ return compact({
268
+ type,
269
+ name: c.name,
270
+ name_localizations: localizations(c.name_localizations),
271
+ description: type === ApplicationCommandType.ChatInput ? c.description ?? "" : "",
272
+ description_localizations: localizations(c.description_localizations),
273
+ options: options(c.options),
274
+ default_member_permissions: c.default_member_permissions == null ? void 0 : String(c.default_member_permissions),
275
+ nsfw: c.nsfw === true ? true : void 0,
276
+ contexts: numbers(c.contexts),
277
+ integration_types: numbers(c.integration_types) ?? [ApplicationIntegrationType.GuildInstall]
278
+ });
279
+ }
280
+ function options(value) {
281
+ if (!Array.isArray(value) || value.length === 0) return void 0;
282
+ return value.map((option) => {
283
+ const o = option;
284
+ return compact({
285
+ type: o.type,
286
+ name: o.name,
287
+ name_localizations: localizations(o.name_localizations),
288
+ description: o.description,
289
+ description_localizations: localizations(o.description_localizations),
290
+ required: o.required === true ? true : void 0,
291
+ autocomplete: o.autocomplete === true ? true : void 0,
292
+ choices: choices(o.choices),
293
+ options: options(o.options),
294
+ channel_types: numbers(o.channel_types),
295
+ min_value: number(o.min_value),
296
+ max_value: number(o.max_value),
297
+ min_length: number(o.min_length),
298
+ max_length: number(o.max_length)
299
+ });
300
+ });
301
+ }
302
+ function choices(value) {
303
+ if (!Array.isArray(value) || value.length === 0) return void 0;
304
+ return value.map((choice) => compact({
305
+ name: choice.name,
306
+ value: choice.value,
307
+ name_localizations: localizations(choice.name_localizations)
308
+ }));
309
+ }
310
+ function localizations(value) {
311
+ if (typeof value !== "object" || value === null) return void 0;
312
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
313
+ if (entries.length === 0) return void 0;
314
+ entries.sort(([a], [b]) => a.localeCompare(b));
315
+ return Object.fromEntries(entries);
316
+ }
317
+ /** Sorted, deduplicated. Discord treats these as sets. */
318
+ function numbers(value) {
319
+ if (!Array.isArray(value)) return void 0;
320
+ return [...new Set(value)].sort((a, b) => a - b);
321
+ }
322
+ function number(value) {
323
+ return typeof value === "number" ? value : void 0;
324
+ }
325
+ function compact(object) {
326
+ const out = {};
327
+ for (const [key, value] of Object.entries(object)) if (value !== void 0) out[key] = value;
328
+ return out;
329
+ }
330
+ //#endregion
331
+ //#region src/registration/diff.ts
332
+ /** Compares what the app wants registered with what Discord currently has. */
333
+ function diffCommands(desired, remote) {
334
+ const want = new Map(desired.map((c) => [commandKey(c), stableStringify(normalizeCommand(c))]));
335
+ const have = new Map(remote.map((c) => [commandKey(c), stableStringify(normalizeCommand(c))]));
336
+ const diff = {
337
+ added: [],
338
+ removed: [],
339
+ changed: [],
340
+ unchanged: [],
341
+ hasChanges: false
342
+ };
343
+ for (const [key, body] of want) {
344
+ const current = have.get(key);
345
+ if (current === void 0) diff.added.push(label(key));
346
+ else if (current === body) diff.unchanged.push(label(key));
347
+ else diff.changed.push(label(key));
348
+ }
349
+ for (const key of have.keys()) if (!want.has(key)) diff.removed.push(label(key));
350
+ for (const list of [
351
+ diff.added,
352
+ diff.removed,
353
+ diff.changed,
354
+ diff.unchanged
355
+ ]) list.sort();
356
+ diff.hasChanges = diff.added.length + diff.removed.length + diff.changed.length > 0;
357
+ return diff;
358
+ }
359
+ function label(key) {
360
+ return key.startsWith("1:") ? key.slice(2) : key;
361
+ }
362
+ //#endregion
363
+ //#region src/registration/remote.ts
364
+ /** `global` or `guild:<id>`. Used for cache keys and messages. */
365
+ function scopeKey(scope) {
366
+ return scope === "global" ? "global" : `guild:${scope.guild}`;
367
+ }
368
+ function scopeRoute(applicationId, scope) {
369
+ return scope === "global" ? Routes.applicationCommands(applicationId) : Routes.applicationGuildCommands(applicationId, scope.guild);
370
+ }
371
+ async function fetchCommands(rest, applicationId, scope) {
372
+ return await rest.get(scopeRoute(applicationId, scope));
373
+ }
374
+ /** Bulk overwrite: Discord replaces the scope's whole command set with `commands`. */
375
+ async function putCommands(rest, applicationId, scope, commands) {
376
+ try {
377
+ await rest.put(scopeRoute(applicationId, scope), { body: commands });
378
+ } catch (error) {
379
+ throw RegistrationError.from(error, scope, commands) ?? error;
380
+ }
381
+ }
382
+ //#endregion
383
+ //#region src/registration/errors.ts
384
+ /** Discord rejected a bulk overwrite. Wraps the `DiscordAPIError` with per-command detail. */
385
+ var RegistrationError = class RegistrationError extends Error {
386
+ scope;
387
+ problems;
388
+ cause;
389
+ constructor(scope, problems, cause) {
390
+ super(`Discord rejected the ${scopeKey(scope)} command registration:\n${problems.map((p) => ` ${p.command ?? "(request)"}${p.field === "" ? "" : ` ${p.field}`}: ${p.message}`).join("\n")}`);
391
+ this.scope = scope;
392
+ this.problems = problems;
393
+ this.cause = cause;
394
+ this.name = "RegistrationError";
395
+ }
396
+ /** `null` when `error` is not a Discord API error. */
397
+ static from(error, scope, commands) {
398
+ if (!isDiscordApiError(error)) return null;
399
+ const problems = [];
400
+ if (error.rawError.errors !== void 0) collect(error.rawError.errors, [], commands, problems);
401
+ if (problems.length === 0) problems.push({
402
+ command: null,
403
+ field: "",
404
+ message: error.rawError.message
405
+ });
406
+ return new RegistrationError(scope, problems, error);
407
+ }
408
+ };
409
+ function isDiscordApiError(error) {
410
+ if (typeof error !== "object" || error === null || !("rawError" in error)) return false;
411
+ const raw = error.rawError;
412
+ return typeof raw === "object" && raw !== null && typeof raw.message === "string";
413
+ }
414
+ /**
415
+ * Discord nests errors by request path, `{ "0": { options: { "1": { description: { _errors } } } } }`.
416
+ * The top-level index is the command in the bulk body; deeper indices are options and choices.
417
+ */
418
+ function collect(node, path, commands, out) {
419
+ if (typeof node === "string") {
420
+ out.push(problem(path, commands, node));
421
+ return;
422
+ }
423
+ if (typeof node !== "object" || node === null) return;
424
+ for (const [key, value] of Object.entries(node)) if (key === "_errors" && Array.isArray(value)) for (const entry of value) {
425
+ const message = typeof entry === "object" && entry !== null && "message" in entry ? String(entry.message) : String(entry);
426
+ out.push(problem(path, commands, message));
427
+ }
428
+ else collect(value, [...path, /^\d+$/.test(key) ? Number(key) : key], commands, out);
429
+ }
430
+ function problem(path, commands, message) {
431
+ const [first, ...rest] = path;
432
+ const command = typeof first === "number" ? commands[first] : void 0;
433
+ if (command === void 0) return {
434
+ command: null,
435
+ field: path.join("."),
436
+ message
437
+ };
438
+ const field = [];
439
+ let cursor = command;
440
+ for (const segment of rest) if (typeof segment === "number" && Array.isArray(cursor)) {
441
+ cursor = cursor[segment];
442
+ const name = cursor?.name;
443
+ field.push(typeof name === "string" ? name : String(segment));
444
+ } else {
445
+ cursor = cursor?.[segment];
446
+ field.push(String(segment));
447
+ }
448
+ return {
449
+ command: command.name,
450
+ field: field.join("."),
451
+ message
452
+ };
453
+ }
454
+ //#endregion
455
+ //#region src/registration/sync.ts
456
+ const REGISTRATION_CACHE_FILE = "registration.json";
457
+ const CACHE_VERSION = 1;
458
+ /** Thrown instead of applying when the guard trips and `force` is not set. */
459
+ var UnsafeSyncError = class extends Error {
460
+ reasons;
461
+ constructor(reasons) {
462
+ super(`Refusing to register commands:\n${reasons.map((r) => ` ${r}`).join("\n")}\nPass force to do it anyway.`);
463
+ this.reasons = reasons;
464
+ this.name = "UnsafeSyncError";
465
+ }
466
+ };
467
+ /**
468
+ * Reconciles every scope: read remote, diff, bulk overwrite only when something differs.
469
+ * All scopes are read and checked before any is written, so a guard failure changes nothing.
470
+ */
471
+ async function syncCommands(options) {
472
+ const { rest, applicationId, commands, scopes, cacheDir, dryRun = false, force = false } = options;
473
+ const cache = cacheDir === void 0 ? null : readCache(cacheDir);
474
+ const hash = hashCommands(commands);
475
+ const unsafe = [];
476
+ if (cache !== null && cache.applicationId !== applicationId) unsafe.push(`The application ID changed from ${cache.applicationId} to ${applicationId}. Commands registered under the old application are left as they are.`);
477
+ const targets = new Set(scopes.map(scopeKey));
478
+ for (const key of Object.keys(cache?.scopes ?? {})) if (!targets.has(key)) unsafe.push(`${key} received commands last time but is no longer a target. Its commands stay registered on Discord until removed.`);
479
+ const results = [];
480
+ for (const scope of scopes) {
481
+ const key = scopeKey(scope);
482
+ if (cache !== null && cache.applicationId === applicationId && cache.scopes[key] === hash) {
483
+ results.push({
484
+ scope,
485
+ diff: null,
486
+ applied: false
487
+ });
488
+ continue;
489
+ }
490
+ const remote = await fetchCommands(rest, applicationId, scope);
491
+ const diff = diffCommands(commands, remote);
492
+ if (commands.length === 0 && remote.length > 0) unsafe.push(`${key} has ${remote.length} command(s) registered and the app declares none.`);
493
+ results.push({
494
+ scope,
495
+ diff,
496
+ applied: false
497
+ });
498
+ }
499
+ if (dryRun) return {
500
+ scopes: results,
501
+ unsafe
502
+ };
503
+ if (unsafe.length > 0 && !force) throw new UnsafeSyncError(unsafe);
504
+ const next = {
505
+ version: CACHE_VERSION,
506
+ applicationId,
507
+ scopes: {}
508
+ };
509
+ for (const result of results) {
510
+ if (result.diff?.hasChanges) {
511
+ await putCommands(rest, applicationId, result.scope, commands);
512
+ result.applied = true;
513
+ }
514
+ next.scopes[scopeKey(result.scope)] = hash;
515
+ if (cacheDir !== void 0) writeCache(cacheDir, next);
516
+ }
517
+ return {
518
+ scopes: results,
519
+ unsafe
520
+ };
521
+ }
522
+ /** Order-insensitive, like the diff: reordering commands is not a change. */
523
+ function hashCommands(commands) {
524
+ const normalized = commands.map((c) => [commandKey(c), normalizeCommand(c)]).sort(([a], [b]) => a.localeCompare(b)).map(([, c]) => c);
525
+ return createHash("sha256").update(stableStringify(normalized)).digest("hex");
526
+ }
527
+ function readCache(dir) {
528
+ let text;
529
+ try {
530
+ text = readFileSync(path.join(dir, REGISTRATION_CACHE_FILE), "utf8");
531
+ } catch {
532
+ return null;
533
+ }
534
+ const parsed = JSON.parse(text);
535
+ if (typeof parsed !== "object" || parsed === null || parsed.version !== CACHE_VERSION) return null;
536
+ return parsed;
537
+ }
538
+ function writeCache(dir, cache) {
539
+ mkdirSync(dir, { recursive: true });
540
+ writeFileSync(path.join(dir, REGISTRATION_CACHE_FILE), `${stableStringify(cache)}\n`);
541
+ }
542
+ //#endregion
543
+ export { checkIntents as a, configFor as c, scopeKey as i, defineConfig as l, syncCommands as n, requiredIntents as o, RegistrationError as r, ConfigError as s, UnsafeSyncError as t, validateConfig as u };
544
+
545
+ //# sourceMappingURL=registration-CaE0QBT6.js.map