refactor: update deno template and loader logic (#11060)

* refactor: update deno template and loader logic

* yeet

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Almeida
2025-09-04 13:36:26 +01:00
committed by GitHub
parent 5a656b849f
commit 8ca279e0c3
18 changed files with 96 additions and 148 deletions

View File

@@ -6,7 +6,7 @@ import { styleText } from 'node:util';
import { Option, program } from 'commander'; import { Option, program } from 'commander';
import prompts from 'prompts'; import prompts from 'prompts';
import validateProjectName from 'validate-npm-package-name'; import validateProjectName from 'validate-npm-package-name';
import packageJSON from '../package.json' assert { type: 'json' }; import packageJSON from '../package.json' with { type: 'json' };
import { createDiscordBot } from '../src/create-discord-bot.js'; import { createDiscordBot } from '../src/create-discord-bot.js';
import { resolvePackageManager } from '../src/helpers/packageManager.js'; import { resolvePackageManager } from '../src/helpers/packageManager.js';
import { DEFAULT_PROJECT_NAME, PACKAGE_MANAGERS } from '../src/util/constants.js'; import { DEFAULT_PROJECT_NAME, PACKAGE_MANAGERS } from '../src/util/constants.js';

View File

@@ -14,6 +14,11 @@ export type PackageManager = 'bun' | 'deno' | 'npm' | 'pnpm' | 'yarn';
export function resolvePackageManager(): PackageManager { export function resolvePackageManager(): PackageManager {
const npmConfigUserAgent = process.env.npm_config_user_agent; const npmConfigUserAgent = process.env.npm_config_user_agent;
// @ts-expect-error: We're not using Deno's types, so its global is not declared
if (typeof Deno !== 'undefined') {
return 'deno';
}
// If this is not present, return the default package manager. // If this is not present, return the default package manager.
if (!npmConfigUserAgent) { if (!npmConfigUserAgent) {
return DEFAULT_PACKAGE_MANAGER; return DEFAULT_PACKAGE_MANAGER;

View File

@@ -1,9 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc.json",
"printWidth": 120,
"useTabs": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf"
}

View File

@@ -1,3 +1,3 @@
{ {
"recommendations": ["denoland.vscode-deno", "tamasfe.even-better-toml", "codezombiech.gitignore"] "recommendations": ["denoland.vscode-deno"]
} }

View File

@@ -2,14 +2,11 @@
"editor.defaultFormatter": "denoland.vscode-deno", "editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true, "source.fixAll": "always",
"source.organizeImports": false "source.organizeImports": "always"
}, },
"editor.trimAutoWhitespace": false, "editor.trimAutoWhitespace": false,
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.eol": "\n", "files.eol": "\n",
"npm.packageManager": "[REPLACE_ME]", "deno.enable": "[REPLACE_BOOL]"
"deno.enable": "[REPLACE_BOOL]",
"deno.lint": "[REPLACE_BOOL]",
"deno.unstable": false
} }

View File

@@ -2,40 +2,25 @@
"$schema": "https://raw.githubusercontent.com/denoland/deno/main/cli/schemas/config-file.v1.json", "$schema": "https://raw.githubusercontent.com/denoland/deno/main/cli/schemas/config-file.v1.json",
"tasks": { "tasks": {
"lint": "deno lint", "lint": "deno lint",
"deploy": "deno run --allow-read --allow-env --allow-net src/util/deploy.ts", "deploy": "deno run --env-file --allow-read --allow-env --allow-net src/util/deploy.ts",
"format": "deno fmt", "format": "deno fmt",
"fmt": "deno fmt", "fmt": "deno fmt",
"start": "deno run --allow-read --allow-env --allow-net src/index.ts", "start": "deno run --env-file --allow-read --allow-env --allow-net src/index.ts",
},
"fmt": {
"useTabs": true,
"lineWidth": 120,
"singleQuote": true,
}, },
"lint": { "lint": {
"include": ["src/"],
"rules": { "rules": {
"tags": ["recommended"], "tags": ["recommended"],
"exclude": ["require-await", "no-await-in-sync-fn"], "exclude": ["require-await", "no-await-in-sync-fn"],
}, },
}, },
"fmt": { "imports": {
"useTabs": true, "@discordjs/core": "npm:@discordjs/core@^2.2.1",
"lineWidth": 120, "discord.js": "npm:discord.js@^14.22.1",
"semiColons": true, "zod": "npm:zod@^3.25.76",
"singleQuote": true,
"proseWrap": "preserve",
"include": ["src/"],
},
"compilerOptions": {
"alwaysStrict": true,
"emitDecoratorMetadata": true,
"verbatimModuleSyntax": true,
"lib": ["deno.window"],
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"strict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"exactOptionalPropertyTypes": false,
"noImplicitOverride": true,
}, },
} }

View File

@@ -1,5 +1,5 @@
import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'npm:discord.js@^14.22.0'; import type { CommandInteraction, RESTPostAPIApplicationCommandsJSONBody } from 'discord.js';
import { z } from 'npm:zod@^3.25.76'; import { z } from 'zod';
import type { StructurePredicate } from '../util/loaders.ts'; import type { StructurePredicate } from '../util/loaders.ts';
/** /**

View File

@@ -1,21 +1,21 @@
import type { ClientEvents } from 'npm:discord.js@^14.22.0'; import type { ClientEvents } from 'discord.js';
import { z } from 'npm:zod@^3.25.76'; import { z } from 'zod';
import type { StructurePredicate } from '../util/loaders.ts'; import type { StructurePredicate } from '../util/loaders.ts';
/** /**
* Defines the structure of an event. * Defines the structure of an event.
*/ */
export type Event<T extends keyof ClientEvents = keyof ClientEvents> = { export type Event<EventName extends keyof ClientEvents = keyof ClientEvents> = {
/** /**
* The function to execute when the event is emitted. * The function to execute when the event is emitted.
* *
* @param parameters - The parameters of the event * @param parameters - The parameters of the event
*/ */
execute(...parameters: ClientEvents[T]): Promise<void> | void; execute(...parameters: ClientEvents[EventName]): Promise<void> | void;
/** /**
* The name of the event to listen to * The name of the event to listen to
*/ */
name: T; name: EventName;
/** /**
* Whether or not the event should only be listened to once * Whether or not the event should only be listened to once
* *

View File

@@ -1,4 +1,4 @@
import { Events } from 'npm:discord.js@^14.22.0'; import { Events } from 'discord.js';
import type { Event } from './index.ts'; import type { Event } from './index.ts';
import { loadCommands } from '../util/loaders.ts'; import { loadCommands } from '../util/loaders.ts';

View File

@@ -1,4 +1,4 @@
import { Events } from 'npm:discord.js@^14.22.0'; import { Events } from 'discord.js';
import type { Event } from './index.ts'; import type { Event } from './index.ts';
export default { export default {

View File

@@ -1,6 +1,4 @@
import 'https://deno.land/std@0.223.0/dotenv/load.ts'; import { Client, GatewayIntentBits } from 'discord.js';
import { URL } from 'node:url';
import { Client, GatewayIntentBits } from 'npm:discord.js@^14.22.0';
import { loadEvents } from './util/loaders.ts'; import { loadEvents } from './util/loaders.ts';
// Initialize the client // Initialize the client

View File

@@ -1,7 +1,5 @@
import 'https://deno.land/std@0.223.0/dotenv/load.ts'; import { API } from '@discordjs/core/http-only';
import { URL } from 'node:url'; import { REST } from 'discord.js';
import { API } from 'npm:@discordjs/core@^2.2.1/http-only';
import { REST } from 'npm:discord.js@^14.22.0';
import { loadCommands } from './loaders.ts'; import { loadCommands } from './loaders.ts';
const commands = await loadCommands(new URL('../commands/', import.meta.url)); const commands = await loadCommands(new URL('../commands/', import.meta.url));

View File

@@ -1,6 +1,7 @@
import type { PathLike } from 'node:fs'; import type { PathLike } from 'node:fs';
import { readdir, stat } from 'node:fs/promises'; import { glob, stat } from 'node:fs/promises';
import { URL } from 'node:url'; import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Command } from '../commands/index.ts'; import type { Command } from '../commands/index.ts';
import { predicate as commandPredicate } from '../commands/index.ts'; import { predicate as commandPredicate } from '../commands/index.ts';
import type { Event } from '../events/index.ts'; import type { Event } from '../events/index.ts';
@@ -9,7 +10,7 @@ import { predicate as eventPredicate } from '../events/index.ts';
/** /**
* A predicate to check if the structure is valid * A predicate to check if the structure is valid
*/ */
export type StructurePredicate<T> = (structure: unknown) => structure is T; export type StructurePredicate<Structure> = (structure: unknown) => structure is Structure;
/** /**
* Loads all the structures in the provided directory * Loads all the structures in the provided directory
@@ -19,11 +20,11 @@ export type StructurePredicate<T> = (structure: unknown) => structure is T;
* @param recursive - Whether to recursively load the structures in the directory * @param recursive - Whether to recursively load the structures in the directory
* @returns * @returns
*/ */
export async function loadStructures<T>( export async function loadStructures<Structure>(
dir: PathLike, dir: PathLike,
predicate: StructurePredicate<T>, predicate: StructurePredicate<Structure>,
recursive = true, recursive = true,
): Promise<T[]> { ): Promise<Structure[]> {
// Get the stats of the directory // Get the stats of the directory
const statDir = await stat(dir); const statDir = await stat(dir);
@@ -32,34 +33,24 @@ export async function loadStructures<T>(
throw new Error(`The directory '${dir}' is not a directory.`); throw new Error(`The directory '${dir}' is not a directory.`);
} }
// Get all the files in the directory
const files = await readdir(dir);
// Create an empty array to store the structures // Create an empty array to store the structures
const structures: T[] = []; const structures: Structure[] = [];
// Loop through all the files in the directory // Create a glob pattern to match the .ts files
for (const file of files) { const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
const fileUrl = new URL(`${dir}/${file}`, import.meta.url); const pattern = resolve(basePath, recursive ? '**/*.ts' : '*.ts');
// Get the stats of the file // Loop through all the matching files in the directory
const statFile = await stat(fileUrl); for await (const file of glob(pattern)) {
// If the file is index.ts, skip the file
// If the file is a directory and recursive is true, recursively load the structures in the directory if (file.endsWith('/index.ts')) {
if (statFile.isDirectory() && recursive) {
structures.push(...(await loadStructures(fileUrl, predicate, recursive)));
continue;
}
// If the file is index.ts or the file does not end with .ts, skip the file
if (file === 'index.ts' || !file.endsWith('.ts')) {
continue; continue;
} }
// Import the structure dynamically from the file // Import the structure dynamically from the file
const structure = (await import(fileUrl.toString())).default; const { default: structure } = await import(file);
// If the structure is a valid structure, add it // If the default export is a valid structure, add it
if (predicate(structure)) { if (predicate(structure)) {
structures.push(structure); structures.push(structure);
} }

View File

@@ -3,10 +3,10 @@ import { z } from 'zod';
/** /**
* Defines the structure of an event. * Defines the structure of an event.
* *
* @template {keyof import('discord.js').ClientEvents} [T=keyof import('discord.js').ClientEvents] * @template {keyof import('discord.js').ClientEvents} [EventName=keyof import('discord.js').ClientEvents]
* @typedef {object} Event * @typedef {object} Event
* @property {(...parameters: import('discord.js').ClientEvents[T]) => Promise<void> | void} execute The function to execute the command * @property {(...parameters: import('discord.js').ClientEvents[EventName]) => Promise<void> | void} execute The function to execute the command
* @property {T} name The name of the event to listen to * @property {EventName} name The name of the event to listen to
* @property {boolean} [once] Whether or not the event should only be listened to once * @property {boolean} [once] Whether or not the event should only be listened to once
*/ */

View File

@@ -1,23 +1,23 @@
import { readdir, stat } from 'node:fs/promises'; import { glob, stat } from 'node:fs/promises';
import { URL } from 'node:url'; import { fileURLToPath, resolve, URL } from 'node:url';
import { predicate as commandPredicate } from '../commands/index.js'; import { predicate as commandPredicate } from '../commands/index.js';
import { predicate as eventPredicate } from '../events/index.js'; import { predicate as eventPredicate } from '../events/index.js';
/** /**
* A predicate to check if the structure is valid. * A predicate to check if the structure is valid.
* *
* @template T * @template Structure
* @typedef {(structure: unknown) => structure is T} StructurePredicate * @typedef {(structure: unknown) => structure is Structure} StructurePredicate
*/ */
/** /**
* Loads all the structures in the provided directory. * Loads all the structures in the provided directory.
* *
* @template T * @template Structure
* @param {import('node:fs').PathLike} dir - The directory to load the structures from * @param {import('node:fs').PathLike} dir - The directory to load the structures from
* @param {StructurePredicate<T>} predicate - The predicate to check if the structure is valid * @param {StructurePredicate<Structure>} predicate - The predicate to check if the structure is valid
* @param {boolean} recursive - Whether to recursively load the structures in the directory * @param {boolean} recursive - Whether to recursively load the structures in the directory
* @returns {Promise<T[]>} * @returns {Promise<Structure[]>}
*/ */
export async function loadStructures(dir, predicate, recursive = true) { export async function loadStructures(dir, predicate, recursive = true) {
// Get the stats of the directory // Get the stats of the directory
@@ -28,35 +28,25 @@ export async function loadStructures(dir, predicate, recursive = true) {
throw new Error(`The directory '${dir}' is not a directory.`); throw new Error(`The directory '${dir}' is not a directory.`);
} }
// Get all the files in the directory
const files = await readdir(dir);
// Create an empty array to store the structures // Create an empty array to store the structures
/** @type {T[]} */ /** @type {Structure[]} */
const structures = []; const structures = [];
// Loop through all the files in the directory // Create a glob pattern to match the .js files
for (const file of files) { const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
const fileUrl = new URL(`${dir}/${file}`, import.meta.url); const pattern = resolve(basePath, recursive ? '**/*.js' : '*.js');
// Get the stats of the file // Loop through all the matching files in the directory
const statFile = await stat(fileUrl); for await (const file of glob(pattern)) {
// If the file is index.js, skip the file
// If the file is a directory and recursive is true, recursively load the structures in the directory if (file.endsWith('/index.js')) {
if (statFile.isDirectory() && recursive) {
structures.push(...(await loadStructures(fileUrl, predicate, recursive)));
continue;
}
// If the file is index.js or the file does not end with .js, skip the file
if (file === 'index.js' || !file.endsWith('.js')) {
continue; continue;
} }
// Import the structure dynamically from the file // Import the structure dynamically from the file
const structure = (await import(fileUrl.toString())).default; const { default: structure } = await import(file);
// If the structure is a valid structure, add it // If the default export is a valid structure, add it
if (predicate(structure)) { if (predicate(structure)) {
structures.push(structure); structures.push(structure);
} }
@@ -68,7 +58,7 @@ export async function loadStructures(dir, predicate, recursive = true) {
/** /**
* @param {import('node:fs').PathLike} dir * @param {import('node:fs').PathLike} dir
* @param {boolean} [recursive] * @param {boolean} [recursive]
* @returns {Promise<Map<string,import('../commands/index.js').Command>>} * @returns {Promise<Map<string, import('../commands/index.js').Command>>}
*/ */
export async function loadCommands(dir, recursive = true) { export async function loadCommands(dir, recursive = true) {
return (await loadStructures(dir, commandPredicate, recursive)).reduce( return (await loadStructures(dir, commandPredicate, recursive)).reduce(

View File

@@ -3,8 +3,8 @@
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true, "source.fixAll": "explicit",
"source.organizeImports": false "source.organizeImports": "never"
}, },
"editor.trimAutoWhitespace": false, "editor.trimAutoWhitespace": false,
"files.insertFinalNewline": true, "files.insertFinalNewline": true,

View File

@@ -5,17 +5,17 @@ import type { StructurePredicate } from '../util/loaders.[REPLACE_IMPORT_EXT]';
/** /**
* Defines the structure of an event. * Defines the structure of an event.
*/ */
export type Event<T extends keyof ClientEvents = keyof ClientEvents> = { export type Event<EventName extends keyof ClientEvents = keyof ClientEvents> = {
/** /**
* The function to execute when the event is emitted. * The function to execute when the event is emitted.
* *
* @param parameters - The parameters of the event * @param parameters - The parameters of the event
*/ */
execute(...parameters: ClientEvents[T]): Promise<void> | void; execute(...parameters: ClientEvents[EventName]): Promise<void> | void;
/** /**
* The name of the event to listen to * The name of the event to listen to
*/ */
name: T; name: EventName;
/** /**
* Whether or not the event should only be listened to once * Whether or not the event should only be listened to once
* *

View File

@@ -1,6 +1,7 @@
import type { PathLike } from 'node:fs'; import type { PathLike } from 'node:fs';
import { readdir, stat } from 'node:fs/promises'; import { glob, stat } from 'node:fs/promises';
import { URL } from 'node:url'; import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Command } from '../commands/index.[REPLACE_IMPORT_EXT]'; import type { Command } from '../commands/index.[REPLACE_IMPORT_EXT]';
import { predicate as commandPredicate } from '../commands/index.[REPLACE_IMPORT_EXT]'; import { predicate as commandPredicate } from '../commands/index.[REPLACE_IMPORT_EXT]';
import type { Event } from '../events/index.[REPLACE_IMPORT_EXT]'; import type { Event } from '../events/index.[REPLACE_IMPORT_EXT]';
@@ -9,7 +10,7 @@ import { predicate as eventPredicate } from '../events/index.[REPLACE_IMPORT_EXT
/** /**
* A predicate to check if the structure is valid * A predicate to check if the structure is valid
*/ */
export type StructurePredicate<T> = (structure: unknown) => structure is T; export type StructurePredicate<Structure> = (structure: unknown) => structure is Structure;
/** /**
* Loads all the structures in the provided directory * Loads all the structures in the provided directory
@@ -19,11 +20,11 @@ export type StructurePredicate<T> = (structure: unknown) => structure is T;
* @param recursive - Whether to recursively load the structures in the directory * @param recursive - Whether to recursively load the structures in the directory
* @returns * @returns
*/ */
export async function loadStructures<T>( export async function loadStructures<Structure>(
dir: PathLike, dir: PathLike,
predicate: StructurePredicate<T>, predicate: StructurePredicate<Structure>,
recursive = true, recursive = true,
): Promise<T[]> { ): Promise<Structure[]> {
// Get the stats of the directory // Get the stats of the directory
const statDir = await stat(dir); const statDir = await stat(dir);
@@ -32,35 +33,27 @@ export async function loadStructures<T>(
throw new Error(`The directory '${dir}' is not a directory.`); throw new Error(`The directory '${dir}' is not a directory.`);
} }
// Get all the files in the directory
const files = await readdir(dir);
// Create an empty array to store the structures // Create an empty array to store the structures
const structures: T[] = []; const structures: Structure[] = [];
// Loop through all the files in the directory // Create a glob pattern to match the .[REPLACE_IMPORT_EXT] files
for (const file of files) { const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
const fileUrl = new URL(`${dir}/${file}`, import.meta.url); const pattern = resolve(basePath, recursive ? '**/*.[REPLACE_IMPORT_EXT]' : '*.[REPLACE_IMPORT_EXT]');
// Get the stats of the file // Loop through all the matching files in the directory
const statFile = await stat(fileUrl); for await (const file of glob(pattern)) {
// If the file is index.[REPLACE_IMPORT_EXT], skip the file
// If the file is a directory and recursive is true, recursively load the structures in the directory if (file.endsWith('/index.[REPLACE_IMPORT_EXT]')) {
if (statFile.isDirectory() && recursive) {
structures.push(...(await loadStructures(fileUrl, predicate, recursive)));
continue;
}
// If the file is index.[REPLACE_IMPORT_EXT] or the file does not end with .[REPLACE_IMPORT_EXT], skip the file
if (file === 'index.[REPLACE_IMPORT_EXT]' || !file.endsWith('.[REPLACE_IMPORT_EXT]')) {
continue; continue;
} }
// Import the structure dynamically from the file // Import the structure dynamically from the file
const structure = (await import(fileUrl.toString())).default; const { default: structure } = await import(file);
// If the structure is a valid structure, add it // If the default export is a valid structure, add it
if (predicate(structure)) structures.push(structure); if (predicate(structure)) {
structures.push(structure);
}
} }
return structures; return structures;