From c9b2de0621f5de03ca2c4f36d13284ee6b67ba3a Mon Sep 17 00:00:00 2001 From: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com> Date: Fri, 22 Mar 2024 02:15:27 -0500 Subject: [PATCH] high hopes --- src/commands/build.ts | 89 +++++++++++++++----- src/utilities/defaultEsbuildConfig.ts | 4 +- src/utilities/getConfig.ts | 1 + src/utilities/routebuilder.ts | 92 +++++++++++++++++++++ templates/cf.js | 112 ++++++++++++++++++++++++++ 5 files changed, 276 insertions(+), 22 deletions(-) create mode 100644 src/utilities/routebuilder.ts create mode 100644 templates/cf.js diff --git a/src/commands/build.ts b/src/commands/build.ts index b7e3534..4fc83fe 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -1,6 +1,6 @@ import esbuild from 'esbuild'; import { getConfig } from '../utilities/getConfig'; -import { resolve } from 'node:path'; +import p from 'node:path'; import { glob } from 'glob'; import { configDotenv } from 'dotenv'; import assert from 'node:assert'; @@ -11,7 +11,8 @@ import { pathExists, pathExistsSync } from 'find-up'; import { mkdir, writeFile } from 'fs/promises'; import * as Preprocessor from '../utilities/preprocessor'; import { bold, magentaBright } from 'colorette'; - +import { readFile } from 'fs/promises' +import { fileURLToPath} from 'node:url' type BuildOptions = { /** * Define __VERSION__ @@ -54,21 +55,13 @@ export async function build(options: Record) { } const sernConfig = await getConfig(); let buildConfig: Partial = {}; - - const entryPoints = await glob(`./src/**/*{${validExtensions.join(',')}}`, { - //for some reason, my ignore glob wasn't registering correctly' - ignore: { - ignored: (p) => p.name.endsWith('.d.ts'), - }, - }); - - const buildConfigPath = resolve(options.project ?? 'sern.build.js'); + const buildConfigPath = p.resolve(options.project ?? 'sern.build.js'); const resolveBuildConfig = (path: string|undefined, language: string) => { if(language === 'javascript') { - return path ?? resolve('jsconfig.json') + return path ?? p.resolve('jsconfig.json') } - return path ?? resolve('tsconfig.json') + return path ?? p.resolve('tsconfig.json') } const defaultBuildConfig = { @@ -77,7 +70,7 @@ export async function build(options: Record) { mode: options.mode ?? 'development', dropLabels: [], tsconfig: resolveBuildConfig(options.tsconfig, sernConfig.language), - env: options.env ?? resolve('.env'), + env: options.env ?? p.resolve('.env'), }; if (pathExistsSync(buildConfigPath)) { //throwable, buildConfigPath may not exist @@ -89,7 +82,6 @@ export async function build(options: Record) { buildConfig = defaultBuildConfig; console.log('No build config found, defaulting'); } - let env = {} as Record; configDotenv({ path: buildConfig.env, processEnv: env }); const modeButNotNodeEnvExists = env.MODE && !env.NODE_ENV; @@ -128,18 +120,74 @@ export async function build(options: Record) { console.log(' ', magentaBright('tsconfig'), buildConfig.tsconfig); console.log(' ', magentaBright('env'), buildConfig.env); - const sernDir = resolve('.sern'), - genDir = resolve(sernDir, 'generated'), - ambientFilePath = resolve(sernDir, 'ambient.d.ts'), - packageJsonPath = resolve('package.json'), - sernTsConfigPath = resolve(sernDir, 'tsconfig.json'), + const sernDir = p.resolve('.sern'), + genDir = p.resolve(sernDir, 'generated'), + ambientFilePath = p.resolve(sernDir, 'ambient.d.ts'), + packageJsonPath = p.resolve('package.json'), + sernTsConfigPath = p.resolve(sernDir, 'tsconfig.json'), packageJson = () => require(packageJsonPath); if (!(await pathExists(genDir))) { console.log('Making .sern/generated dir, does not exist'); await mkdir(genDir, { recursive: true }); } + if(sernConfig.type == 'serverless') { + //we build for cloudflare workers rn + const callsite = fileURLToPath(import.meta.url); + const template = await readFile(p.resolve(callsite, "../../../templates/cf.js"), 'utf8'); + const entryPoints = await glob(`./src/**/*{${validExtensions.join(',')}}`, { + //for some reason, my ignore glob wasn't registering correctly' + ignore: { + ignored: (p) => { + return p.name.endsWith('.d.ts') + }, + childrenIgnored: p => p.isNamed('commands') + }, + }); + const commandsPaths = await glob(`**/*`, { + ignore: { + ignored: p => p.isDirectory() + }, + cwd: "./src/commands/" + }); + console.log(entryPoints) + console.log(commandsPaths) + const commandsImports = commandsPaths.map(file => { + const fname = p.parse(file) + return `import ${fname.name} from "./${p.join(`./commands/${file}`).replace(/\\/g, '/')}"` + }); + console.log(commandsImports) + await esbuild.build({ + entryPoints: commandsPaths.map(file => p.join("src", "commands", file)), + plugins: [imageLoader, ...(buildConfig.esbuildPlugins ?? [])], + ...defaultEsbuild(buildConfig.format!, buildConfig.tsconfig, "./dist/commands"), + outdir: "./dist/commands", + dropLabels: [buildConfig.mode === 'production' ? '__DEV__' : '__PROD__', ...buildConfig.dropLabels!], + }); + //may need to invest in magicast for this lol + const importedModulesTemplate = template + .replace("\"use modules\";", commandsImports.join("\n")) + .replace("\"use handle\";", ` + if(interaction.data.name === "${p.parse(commandsPaths.shift()!).name}") { + return; + } + ${commandsPaths.map(imp => { + return `else if(interaction.data.name === "${p.parse(imp).name}" ) { }` + }).join("\n")} + `); + await writeFile("./dist/out.js", importedModulesTemplate); + } else { + + + const entryPoints = await glob(`./src/**/*{${validExtensions.join(',')}}`, { + //for some reason, my ignore glob wasn't registering correctly' + ignore: { + ignored: (p) => p.name.endsWith('.d.ts'), + }, + }); + + try { const defVersion = () => JSON.stringify(packageJson().version); const define = { @@ -165,4 +213,5 @@ export async function build(options: Record) { console.error(e); process.exit(1); } + } } diff --git a/src/utilities/defaultEsbuildConfig.ts b/src/utilities/defaultEsbuildConfig.ts index 633c722..d64f2e5 100644 --- a/src/utilities/defaultEsbuildConfig.ts +++ b/src/utilities/defaultEsbuildConfig.ts @@ -1,12 +1,12 @@ import type esbuild from 'esbuild'; import { resolve } from 'path'; -export default (format: 'cjs' | 'esm', tsconfig: string|undefined) => +export default (format: 'cjs' | 'esm', tsconfig: string|undefined, outdir='dist') => ({ platform: 'node', format, tsconfig: tsconfig, logLevel: 'info', minify: false, - outdir: resolve('dist'), + outdir: resolve(outdir), } satisfies esbuild.BuildOptions); diff --git a/src/utilities/getConfig.ts b/src/utilities/getConfig.ts index ff44033..adb7e0f 100644 --- a/src/utilities/getConfig.ts +++ b/src/utilities/getConfig.ts @@ -13,6 +13,7 @@ export async function getConfig(): Promise { } export interface sernConfig { + type?: "serverless" | "websocket" language: 'typescript' | 'javascript'; defaultPrefix?: string; paths: { diff --git a/src/utilities/routebuilder.ts b/src/utilities/routebuilder.ts new file mode 100644 index 0000000..ca9b34a --- /dev/null +++ b/src/utilities/routebuilder.ts @@ -0,0 +1,92 @@ +import { readdir, stat } from 'fs/promises'; +import { basename, join, parse, dirname } from 'path'; +import assert from 'assert'; + + +/** + * Import any module based on the absolute path. + * This can accept four types of exported modules + * commonjs, javascript : + * ```js + * exports = commandModule({ }) + * + * //or + * exports.default = commandModule({ }) + * ``` + * esm javascript, typescript, and commonjs typescript + * export default commandModule({}) + */ +export async function importModule(absPath: string) { + let fileModule = await import(absPath); + + let commandModule = fileModule.default; + + assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`); + if ('default' in commandModule ) { + commandModule = commandModule.default; + } + return { module: commandModule } as T; +} + + +export const fmtFileName = (fileName: string) => parse(fileName).name; + + +export const getfilename = (path: string) => fmtFileName(basename(path)); + + + +async function deriveFileInfo(dir: string, file: string) { + const fullPath = join(dir, file); + return { fullPath, + fileStats: await stat(fullPath), + base: basename(file) }; +} + +function parseWildcardName(filename: string): string | null { + const wildcardMatch = filename.match(/\[(.*?)\]/); + return wildcardMatch ? wildcardMatch[1] : null; +} + +export class RouteEntry { + public import_path: string + public filename: string + public parent: string + public wildcardName: string | null; + constructor(public route: string) { + this.import_path = "file:///"+route + this.filename = getfilename(this.route) + this.parent = dirname(this.route); + this.wildcardName = parseWildcardName(this.filename); + } +} + +export interface ReadPathsConfig { + dir: string + onDir?: (dir: string) => Promise|boolean + onEntry?: (etry: RouteEntry) => RouteEntry +} + +export async function* readPaths( + config: ReadPathsConfig +): AsyncGenerator { + const files = await readdir(config.dir); + for (const file of files) { + const { fullPath, fileStats } = await deriveFileInfo(config.dir, file); + if (fileStats.isDirectory()) { + if(config.onDir && await config.onDir(fullPath)) { + yield* readPaths({ ...config, dir: fullPath }); + } else { + yield* readPaths({ ...config, dir: fullPath }); + } + } else { + const nowindowsPath = fullPath.replace(/\\/g, '/'); + if(config.onEntry) { + yield config.onEntry(new RouteEntry(nowindowsPath)); + } else { + yield new RouteEntry(nowindowsPath); + } + } + } +} + diff --git a/templates/cf.js b/templates/cf.js new file mode 100644 index 0000000..fe23a95 --- /dev/null +++ b/templates/cf.js @@ -0,0 +1,112 @@ +// i got this idea from chooks22 +"use modules"; + +import { + InteractionResponseFlags, + InteractionType, + verifyKey, + InteractionResponseType +} from 'discord-interactions'; +//this will import all the modules statically + +class JsonResponse extends Response { + constructor(body, init) { + const jsonBody = JSON.stringify(body); + init = init || { + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + }; + super(jsonBody, init); + } +} + +const router = Router(); + +/** + * A simple :wave: hello page to verify the worker is working. + */ +router.get('/', (request, env) => { + return new Response(`👋 ${env.DISCORD_APPLICATION_ID}`); +}); + +/** + * Main route for all requests sent from Discord. All incoming messages will + * include a JSON payload described here: + * https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object + */ +router.post('/', async (request, env) => { + const { isValid, interaction } = await server.verifyDiscordRequest( + request, + env, + ); + if (!isValid || !interaction) { + return new Response('Bad request signature.', { status: 401 }); + } + + if (interaction.type === InteractionType.PING) { + // The `PING` message is used during the initial webhook handshake, and is + // required to configure the webhook in the developer portal. + return new JsonResponse({ + type: InteractionResponseType.PONG, + }); + } + + if (interaction.type === InteractionType.APPLICATION_COMMAND) { + "use handle"; + // Most user commands will come as `APPLICATION_COMMAND`. +// switch (interaction.data.name.toLowerCase()) { +// case AWW_COMMAND.name.toLowerCase(): { +// const cuteUrl = await getCuteUrl(); +// return new JsonResponse({ +// type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, +// data: { +// content: cuteUrl, +// }, +// }); +// } +// case INVITE_COMMAND.name.toLowerCase(): { +// const applicationId = env.DISCORD_APPLICATION_ID; +// const INVITE_URL = `https://discord.com/oauth2/authorize?client_id=${applicationId}&scope=applications.commands`; +// return new JsonResponse({ +// type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, +// data: { +// content: INVITE_URL, +// flags: InteractionResponseFlags.EPHEMERAL, +// }, +// }); +// } +// default: +// return new JsonResponse({ error: 'Unknown Type' }, { status: 400 }); +// } + } + + console.error('Unknown Type'); + return new JsonResponse({ error: 'Unknown Type' }, { status: 400 }); +}); +router.all('*', () => new Response('Not Found.', { status: 404 })); + + +async function verifyDiscordRequest(request, env) { + const signature = request.headers.get('x-signature-ed25519'); + const timestamp = request.headers.get('x-signature-timestamp'); + const body = await request.text(); + const isValidRequest = + signature && + timestamp && + verifyKey(body, signature, timestamp, env.DISCORD_PUBLIC_KEY); + if (!isValidRequest) { + return { isValid: false }; + } + + return { interaction: JSON.parse(body), isValid: true }; +} + + +const server = { + verifyDiscordRequest: verifyDiscordRequest, + fetch: async function (request, env) { + return router.handle(request, env); + }, +}; +export default server;