14 Commits

Author SHA1 Message Date
Jacob Nguyen
898cae0f12 fix: Update Dockerfile.TS.sern 2025-01-28 11:32:35 -06:00
github-actions[bot]
7f4203370c chore(main): release 1.3.4 (#147)
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Continuous Delivery / Publishing Dev (push) Has been cancelled
Continuous Integration / Testing (push) Has been cancelled
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-01-22 11:34:04 -06:00
Glitch
23d70c6e03 fix: tsconfig generator (#145) 2025-01-22 11:30:50 -06:00
Jacob Nguyen
90fae47ff4 Update continuous-integration.yml 2024-09-26 10:07:00 -05:00
github-actions[bot]
9d2c7979dd chore(main): release 1.3.3 (#144)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-06 22:28:32 -05:00
jacob
9c2d6d7389 fix: bunpnpminstall 2024-08-06 22:25:59 -05:00
Jacob Nguyen
24e97a3695 experimental source maps 2024-08-03 11:20:59 -05:00
github-actions[bot]
70886c28f0 chore(main): release 1.3.2 (#143)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-31 09:25:13 -05:00
Duro
53ca446a38 fix: make tsconfig work with comments/trailing commas (#142) 2024-07-31 18:24:04 +05:30
github-actions[bot]
dac05ab7ce chore(main): release 1.3.1 (#141)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-27 11:09:00 -05:00
Jacob Nguyen
2bb169f600 chore: release 1.3.1
Release-As: 1.3.1
2024-07-27 11:06:37 -05:00
Jacob Nguyen
89302d62c4 oops 2024-07-27 11:04:26 -05:00
github-actions[bot]
a63192ab10 chore(main): release 1.3.0 (#140)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-27 10:55:38 -05:00
Jacob Nguyen
20364fa20d feat: plugin, no prompt, bug fixes (#139)
* high hopes

* s

* plugin calling

* add

* eol

* more progress

* fmt

* step 1

* refactor out

* /internal route

* prg

* write handler

* consolidate

* fix version gen

* prototype

* out

* fix merge

* bundle presence and event modules

* extrapolate into plugin

* pluginify and simplify

* fix up typing gen for process env and watch mode

* watch

* add nonpromptable plugins install

* fix regression and clean up publish script

* ea
2024-07-27 10:45:53 -05:00
22 changed files with 4650 additions and 4396 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: 17
node-version: 20
- name: Install Node.js dependencies
run: npm i && npm run build

View File

@@ -2,6 +2,46 @@
All notable changes to this project will be documented in this file.
## [1.3.4](https://github.com/sern-handler/cli/compare/v1.3.3...v1.3.4) (2025-01-22)
### Bug Fixes
* tsconfig generator ([#145](https://github.com/sern-handler/cli/issues/145)) ([23d70c6](https://github.com/sern-handler/cli/commit/23d70c6e03860d1a69e3bffedd6910aba82071ab))
## [1.3.3](https://github.com/sern-handler/cli/compare/v1.3.2...v1.3.3) (2024-08-07)
### Bug Fixes
* bunpnpminstall ([9c2d6d7](https://github.com/sern-handler/cli/commit/9c2d6d7389c41a071ab86570cebf987b02bbf450))
## [1.3.2](https://github.com/sern-handler/cli/compare/v1.3.1...v1.3.2) (2024-07-31)
### Bug Fixes
* make tsconfig work with comments/trailing commas ([#142](https://github.com/sern-handler/cli/issues/142)) ([53ca446](https://github.com/sern-handler/cli/commit/53ca446a38076ed7b165dbbb41f346f028b7e394))
## [1.3.1](https://github.com/sern-handler/cli/compare/v1.3.0...v1.3.1) (2024-07-27)
### Miscellaneous Chores
* release 1.3.1 ([2bb169f](https://github.com/sern-handler/cli/commit/2bb169f600172af8c8316b6c9420684a4de34e0d))
## [1.3.0](https://github.com/sern-handler/cli/compare/v1.2.1...v1.3.0) (2024-07-27)
### Features
* plugin, no prompt, bug fixes ([#139](https://github.com/sern-handler/cli/issues/139)) ([20364fa](https://github.com/sern-handler/cli/commit/20364fa20d1f3bf70a1c0cfefbc1d6c9365a4925))
### Bug Fixes
* fix publish command for bun & pnpm ([#137](https://github.com/sern-handler/cli/issues/137)) ([d581142](https://github.com/sern-handler/cli/commit/d581142f082ed888036e58aa33e9d88d84d34c2f))
## [1.2.1](https://github.com/sern-handler/cli/compare/v1.2.0...v1.2.1) (2024-02-08)

View File

@@ -61,3 +61,23 @@ Run `npm create @sern/bot` for an interactive setup on a brand new project using
sern runs on your plugins. Contribute to our [repository](https://github.com/sern-handler/awesome-plugins) and then install the plugins via our cli! <br>
Run `sern plugins` to see all installable plugins.
## Development
```sh
git clone https://github.com/sern-handler/cli.git
```
## insall i
```sh
npm i
```
## build it
```sh
npm run build
```
## make it usable globally
- if sern is installed globally already, you may need to uninstall it.
```sh
npm link
```

8200
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +1,63 @@
{
"name": "@sern/cli",
"version": "1.2.1",
"description": "Official CLI for @sern/handler",
"exports": "./dist/index.js",
"bin": {
"sern": "./dist/index.js"
},
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --check .",
"fix": "prettier --write .",
"build": "tsup",
"watch": "tsup --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/cli.git"
},
"keywords": [
"cli",
"discord",
"discord.js",
"sern",
"sern-handler"
],
"author": "EvolutionX",
"license": "MIT",
"bugs": {
"url": "https://github.com/sern-handler/cli/issues"
},
"homepage": "https://sern.dev",
"dependencies": {
"@esbuild-kit/cjs-loader": "^2.4.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"colorette": "2.0.20",
"commander": "11.0.0",
"dotenv": "^16.3.1",
"esbuild": "^0.19.1",
"execa": "7.2.0",
"find-up": "6.3.0",
"glob": "^10.3.3",
"ora": "6.3.1",
"prompts": "2.4.2",
"undici": "5.23.0"
},
"devDependencies": {
"@babel/parser": "^7.22.5",
"@favware/npm-deprecate": "1.0.7",
"@types/prompts": "2.4.4",
"prettier": "2.8.8",
"tsup": "^6.7.0",
"typescript": "5.2.2"
},
"engines": {
"node": ">= 18.16.x"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
}
"name": "@sern/cli",
"version": "1.3.4",
"description": "Official CLI for @sern/handler",
"exports": "./dist/index.js",
"bin": {
"sern": "./dist/index.js"
},
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --check .",
"fix": "prettier --write .",
"build": "tsup",
"watch": "tsup --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/cli.git"
},
"keywords": [
"cli",
"discord",
"discord.js",
"sern",
"sern-handler"
],
"author": "EvolutionX",
"license": "MIT",
"bugs": {
"url": "https://github.com/sern-handler/cli/issues"
},
"homepage": "https://sern.dev",
"dependencies": {
"@esbuild-kit/cjs-loader": "^2.4.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"colorette": "2.0.20",
"commander": "11.0.0",
"dotenv": "^16.3.1",
"esbuild": "^0.19.1",
"execa": "7.2.0",
"find-up": "6.3.0",
"glob": "^10.3.3",
"ora": "6.3.1",
"prompts": "2.4.2",
"undici": "5.23.0"
},
"devDependencies": {
"@babel/parser": "^7.22.5",
"@favware/npm-deprecate": "1.0.7",
"@types/prompts": "2.4.4",
"prettier": "2.8.8",
"tsup": "^6.7.0",
"typescript": "5.2.2"
},
"engines": {
"node": ">= 18.16.x"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
}
}

View File

@@ -1,17 +1,18 @@
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';
import defaultEsbuild from '../utilities/defaultEsbuildConfig';
import { require } from '../utilities/require';
import { pathExists, pathExistsSync } from 'find-up';
import { mkdir, writeFile } from 'fs/promises';
import { mkdir, writeFile, readFile } from 'fs/promises';
import * as Preprocessor from '../utilities/preprocessor';
import { bold, magentaBright } from 'colorette';
const validExtensions = ['.ts', '.js', '.json', '.png', '.jpg', '.jpeg', '.webp'];
import { parseTsConfig } from '../utilities/parseTsconfig';
const VALID_EXTENSIONS = ['.ts', '.js' ];
type BuildOptions = {
/**
@@ -24,10 +25,6 @@ type BuildOptions = {
* default = esm
*/
format?: 'cjs' | 'esm';
/**
* extra esbuild plugins to build with sern.
*/
esbuildPlugins?: esbuild.Plugin[];
/**
* https://esbuild.github.io/api/#drop-labels
**/
@@ -47,79 +44,81 @@ type BuildOptions = {
* default to .env.
*/
env?: string;
/**
* flag: default false
*/
sourcemap?: boolean;
};
const CommandHandlerPlugin = (buildConfig: Partial<BuildOptions>, ambientFilePath: string, sernTsConfigPath: string) => {
return {
name: "commandhandler",
setup(build) {
const options = build.initialOptions
const defVersion = () => JSON.stringify(require(p.resolve('package.json')).version);
options.define = {
...buildConfig.define ?? {},
__DEV__: `${buildConfig.mode === 'development'}`,
__PROD__: `${buildConfig.mode === 'production'}`,
__VERSION__: `${buildConfig.defineVersion ? `${defVersion()}` : 'undefined'}`
} ?? {}
Preprocessor.writeTsConfig(buildConfig.format!, sernTsConfigPath, writeFile);
Preprocessor.writeAmbientFile(ambientFilePath, options.define!, writeFile);
}
} as esbuild.Plugin
}
const resolveBuildConfig = (path: string|undefined, language: string) => {
if(language === 'javascript') {
return path ?? 'jsconfig.json'
}
return path ?? 'tsconfig.json'
}
export async function build(options: Record<string, any>) {
if (!options.supressWarnings) {
console.info(`${magentaBright('EXPERIMENTAL')}: This API has not been stabilized. add -W or --suppress-warnings flag to suppress`);
}
console.log(options)
const sernConfig = await getConfig();
let buildConfig: Partial<BuildOptions> = {};
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 resolveBuildConfig = (path: string|undefined, language: string) => {
if(language === 'javascript') {
return path ?? resolve('jsconfig.json')
}
return path ?? resolve('tsconfig.json')
}
let buildConfig: BuildOptions;
const buildConfigPath = p.resolve(options.project ?? 'sern.build.js');
const defaultBuildConfig = {
defineVersion: true,
format: options.format ?? 'esm',
mode: options.mode ?? 'development',
dropLabels: [],
sourcemap: options.sourceMaps,
tsconfig: resolveBuildConfig(options.tsconfig, sernConfig.language),
env: options.env ?? resolve('.env'),
env: options.env ?? '.env',
include: []
};
if (pathExistsSync(buildConfigPath)) {
//throwable, buildConfigPath may not exist
buildConfig = {
...defaultBuildConfig,
...(await import('file:///' + buildConfigPath)).default,
};
buildConfig = { ...defaultBuildConfig, ...(await import('file:///' + buildConfigPath)).default };
} else {
buildConfig = defaultBuildConfig;
console.log('No build config found, defaulting');
}
let env = {} as Record<string, string>;
configDotenv({ path: buildConfig.env, processEnv: env });
const modeButNotNodeEnvExists = env.MODE && !env.NODE_ENV;
if (modeButNotNodeEnvExists) {
console.warn('Use NODE_ENV instead of MODE');
console.warn('MODE has no effect.');
console.warn(`https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production`);
}
if (env.NODE_ENV) {
buildConfig.mode = env.NODE_ENV as 'production' | 'development';
configDotenv({ path: buildConfig.env });
if (process.env.NODE_ENV) {
buildConfig.mode = process.env.NODE_ENV as 'production' | 'development';
console.log(magentaBright('NODE_ENV:'), 'Found NODE_ENV variable, setting `mode` to this.');
}
assert(buildConfig.mode === 'development' || buildConfig.mode === 'production', 'Mode is not `production` or `development`');
try {
let config = require(buildConfig.tsconfig!);
config.extends && console.warn("Extend the generated tsconfig")
let config = await parseTsConfig(buildConfig.tsconfig!);
config?.extends && console.warn("Extend the generated tsconfig")
} catch(e) {
console.warn("no tsconfig / jsconfig found");
console.warn(`Please create a ${sernConfig.language === 'javascript' ? 'jsconfig.json' : 'tsconfig.json' }`);
console.warn("It should have at least extend the generated one sern makes.")
console.warn(`
{
"extends": "./.sern/tsconfig.json",
}`.trim())
throw e;
console.error("no tsconfig / jsconfig found");
console.error(`Please create a ${sernConfig.language === 'javascript' ? 'jsconfig.json' : 'tsconfig.json' }`);
console.error('It should have at least extend the generated one sern makes.\n \
{ "extends": "./.sern/tsconfig.json" }');
throw e;
}
console.log(bold('Building with:'));
@@ -128,42 +127,35 @@ export async function build(options: Record<string, any>) {
console.log(' ', magentaBright('mode'), buildConfig.mode);
console.log(' ', magentaBright('tsconfig'), buildConfig.tsconfig);
console.log(' ', magentaBright('env'), buildConfig.env);
console.log(' ', magentaBright('sourceMaps'), buildConfig.sourcemap);
const sernDir = resolve('.sern'),
genDir = resolve(sernDir, 'generated'),
ambientFilePath = resolve(sernDir, 'ambient.d.ts'),
packageJsonPath = resolve('package.json'),
sernTsConfigPath = resolve(sernDir, 'tsconfig.json'),
packageJson = () => require(packageJsonPath);
const sernDir = p.resolve('.sern'),
[ambientFilePath, sernTsConfigPath, genDir] =
['ambient.d.ts', 'tsconfig.json', 'generated'].map(f => p.resolve(sernDir, f));
if (!(await pathExists(genDir))) {
console.log('Making .sern/generated dir, does not exist');
await mkdir(genDir, { recursive: true });
}
const entryPoints = await glob(`src/**/*{${VALID_EXTENSIONS.join(',')}}`,{
ignore: {
ignored: (p) => p.name.endsWith('.d.ts'),
}
});
//https://esbuild.github.io/content-types/#tsconfig-json
const ctx = await esbuild.context({
entryPoints,
plugins: [CommandHandlerPlugin(buildConfig, ambientFilePath, sernTsConfigPath)],
sourcemap: buildConfig.sourcemap,
...defaultEsbuild(buildConfig.format!, buildConfig.tsconfig),
dropLabels: [buildConfig.mode === 'production' ? '__DEV__' : '__PROD__', ...buildConfig.dropLabels!],
});
try {
const defVersion = () => JSON.stringify(packageJson().version);
const define = {
...(buildConfig.define ?? {}),
__DEV__: `${buildConfig.mode === 'development'}`,
__PROD__: `${buildConfig.mode === 'production'}`,
} satisfies Record<string, string>;
buildConfig.defineVersion && Object.assign(define, { __VERSION__: defVersion() });
await Preprocessor.writeTsConfig(buildConfig.format!, sernTsConfigPath, writeFile);
await Preprocessor.writeAmbientFile(ambientFilePath, define, writeFile);
//https://esbuild.github.io/content-types/#tsconfig-json
await esbuild.build({
entryPoints,
plugins: [...(buildConfig.esbuildPlugins ?? [])],
...defaultEsbuild(buildConfig.format!, buildConfig.tsconfig),
define,
dropLabels: [buildConfig.mode === 'production' ? '__DEV__' : '__PROD__', ...buildConfig.dropLabels!],
});
} catch (e) {
console.error(e);
process.exit(1);
await ctx.rebuild()
if(options.watch) {
await ctx.watch()
} else {
await ctx.dispose()
}
}

View File

@@ -2,12 +2,13 @@ import { greenBright } from 'colorette';
import fs from 'fs';
import prompt from 'prompts';
import { fetch } from 'undici';
import { pluginsQ } from '../prompts/plugin.js';
import { fromCwd } from '../utilities/fromCwd.js';
import esbuild from 'esbuild';
import { getLang } from '../utilities/getLang.js';
import { getConfig } from '../utilities/getConfig.js';
import type { PromptObject } from 'prompts';
import { resolve } from 'path';
import { require } from '../utilities/require.js';
interface PluginData {
description: string;
hash: string;
@@ -15,25 +16,57 @@ interface PluginData {
author: string[];
link: string;
example: string;
version: '1.0.0';
version: string;
}
const link = `https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json`;
export async function fetchPluginData(): Promise<PluginData[]> {
return fetch(link)
.then(res => res.json())
.then(data => (data as PluginData[]))
.catch(() => [])
}
export function pluginsQ(choices: PluginData[]): PromptObject[] {
return [{
name: 'list',
type: 'autocompleteMultiselect',
message: 'What plugins do you want to install?',
choices: choices.map(e => ({ title: e.name, value: e })),
min: 1,
}];
}
/**
* Installs plugins to project
*/
export async function plugins() {
const e: PluginData[] = (await prompt([await pluginsQ()])).list;
if (!e) process.exit(1);
const lang = await getLang();
for await (const plgData of e) {
export async function plugins(args: string[], opts: Record<string, unknown>) {
const plugins = await fetchPluginData();
let selectedPlugins : PluginData[];
if(args.length) {
const normalizedArgs = args.map(str => str.toLowerCase())
console.log("Trying to find plugins to install...");
const results = plugins.reduce((acc, cur) => {
if(normalizedArgs.includes(cur.name.toLowerCase())) {
return [...acc, cur]
}
return acc;
}, [] as PluginData[]);
selectedPlugins = results;
} else {
selectedPlugins = (await prompt(pluginsQ(plugins))).list;
}
if (!selectedPlugins.length) {
process.exit(1);
}
const { language } = await getConfig();
for await (const plgData of selectedPlugins) {
const pluginText = await download(plgData.link);
const dir = fromCwd('/src/plugins');
const linkNoExtension = `${process.cwd()}/src/plugins/${plgData.name}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (lang === 'typescript') {
if (language === 'typescript') {
fs.writeFileSync(linkNoExtension + '.ts', pluginText);
} else {
const { type = undefined } = require(resolve('package.json'));
@@ -54,7 +87,7 @@ export async function plugins() {
}
}
const pluginNames = e.map((data) => {
const pluginNames = selectedPlugins.map((data) => {
return 'Installed ' + data.name + ' ' + 'from ' + data.author.join(',');
});
console.log(`Successfully downloaded plugin(s):\n${greenBright(pluginNames.join('\n'))}`);

View File

@@ -11,6 +11,7 @@ export async function publish(commandDir: string | undefined, args: Partial<Publ
// pass in args into the command.
const rootPath = new URL('../', import.meta.url),
publishScript = new URL('../dist/create-publish.js', rootPath);
// assign args.import to empty array if non existent
args.import ??= [];
@@ -18,13 +19,14 @@ export async function publish(commandDir: string | undefined, args: Partial<Publ
const isBunOrPnpm = rootPath.pathname.includes('.bun') || rootPath.pathname.includes('.pnpm');
const dotenvLocation = new URL(`${isBunOrPnpm ? '../../' : '../'}node_modules/dotenv/config.js`, rootPath),
esmLoader = new URL(`${isBunOrPnpm ? '../../' : '../'}node_modules/@esbuild-kit/esm-loader/dist/index.js`, rootPath);
const esmLoader = new URL(`${isBunOrPnpm ? '../../' : '../'}node_modules/@esbuild-kit/esm-loader/dist/index.js`, rootPath);
await import('dotenv/config');
// We dynamically load the create-publish script in a child process so that we can pass the special
// loader flag to require typescript files
const command = fork(fileURLToPath(publishScript), [], {
execArgv: ['--loader', esmLoader.toString(), '-r', fileURLToPath(dotenvLocation), '--no-warnings'],
execArgv: ['--loader', esmLoader.toString(), '--no-warnings'],
});
// send paths object so we dont have to recalculate it in script
command.send({ config, preloads: args.import, commandDir });

View File

@@ -2,65 +2,38 @@
* This file is meant to be run with the esm / cjs esbuild-kit loader to properly import typescript modules
*/
import { readdir, stat, mkdir, writeFile } from 'fs/promises';
import { join, basename, extname, resolve } from 'node:path';
import { readdir, mkdir, writeFile } from 'fs/promises';
import { basename, resolve, posix as pathpsx } from 'node:path';
import { pathExistsSync } from 'find-up';
import assert from 'assert';
import { once } from 'node:events';
import * as Rest from './rest';
import type { sernConfig } from './utilities/getConfig';
import type { SernConfig } from './utilities/getConfig';
import type { PublishableData, PublishableModule, Typeable } from './create-publish.d.ts';
import { cyanBright, greenBright, redBright } from 'colorette';
import { inspect } from 'node:util'
import ora from 'ora';
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file),
};
}
function isSkippable(filename: string) {
// empty string is for non extension files (directories)
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
}
async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
if (fileStats.isDirectory()) {
// TODO: refactor so that i dont repeat myself for files (line 71)
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
} else {
yield* readPaths(fullPath, shouldDebug);
}
} else {
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored: ${fullPath}`);
} else {
yield 'file:///' + fullPath;
}
const files = await readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = pathpsx.join(dir, file.name);
if (file.isDirectory()) {
if (!file.name.startsWith('!')) {
yield* readPaths(fullPath, shouldDebug);
}
} else if (!file.name.startsWith('!')) {
yield "file:///"+resolve(fullPath);
}
} catch (err) {
throw err;
}
}
// recieved sern config
const [{ config, preloads, commandDir }] = await once(process, 'message'),
{ paths } = config as sernConfig;
{ paths } = config as SernConfig;
for (const preload of preloads) {
console.log("preloading: ", preload);
await import('file:///' + resolve(preload));
}
@@ -85,6 +58,9 @@ for await (const absPath of filePaths) {
const filenameNoExtension = filename.substring(0, filename.lastIndexOf('.'));
commandModule.name ??= filenameNoExtension;
commandModule.description ??= '';
commandModule.meta = {
absPath
}
commandModule.absPath = absPath;
if (typeof config === 'function') {
config = config(absPath, commandModule);
@@ -146,7 +122,7 @@ const makePublishData = ({ commandModule, config }: Record<string, Record<string
description: makeDescription(applicationType, commandModule.description as string),
absPath: commandModule.absPath as string,
options: optionsTransformer((commandModule?.options ?? []) as Typeable[]),
dm_permission: config?.dmPermission,
dm_permission: config?.dmPermission ?? false,
default_member_permissions: serialize(config?.defaultMemberPermissions),
//@ts-ignore
integration_types: (config?.integrationTypes ?? ['Guild']).map(
@@ -156,11 +132,13 @@ const makePublishData = ({ commandModule, config }: Record<string, Record<string
} else if (s == "User") {
return "1"
} else {
throw Error("IntegrationType is not one of Guild or User");
throw Error("IntegrationType is not one of Guild (0) or User (1)");
}
}),
//@ts-ignore
contexts: config?.contexts ? config.contexts : undefined
contexts: config?.contexts ?? undefined,
name_localizations: commandModule.name_localizations,
description_localizations: commandModule.description_localizations
},
config,
};
@@ -243,7 +221,6 @@ function associateGuildIdsWithData(data: PublishableModule[]): Map<string, Publi
return guildIdMap;
}
const guildCommandMap = associateGuildIdsWithData(guildedCommands);
let guildCommandMapResponse = new Map<string, Record<string, unknown>>();
for (const [guildId, array] of guildCommandMap.entries()) {

View File

@@ -23,7 +23,7 @@ program
program
.command('plugins')
.description('Install plugins from https://github.com/sern-handler/awesome-plugins')
.option('-n --name', 'Name of plugin')
.argument('[names...]', 'Names of plugins to install')
.action((...args) => importDynamic('plugins.js').then((m) => m.plugins(...args)));
program
@@ -52,23 +52,16 @@ program //
.option('-y, --yes', "Say yes to all prompts")
.option('-e, --env [path]', "Supply a path to a .env")
.action(async (...args) => importDynamic('command-clear.js').then((m) => m.commandClear(...args))));
program
.command('app')
.description('manage your discord application')
.addCommand(
new Command('update')
.description("Refresh your discord application.")
.option('-W --suppress-warnings', 'suppress experimental warning')
.action(async (...args) => importDynamic('app-update.js').then(m => m.appUpdate(...args))))
program
.command('build')
.description('Build your bot')
.option('-f --format [fmt]', 'The module system of your application. `cjs` or `esm`', 'esm')
.option('-m --mode [mode]', 'the mode for sern to build in. `production` or `development`', 'development')
.option('-w --watch')
.option('-W --suppress-warnings', 'suppress experimental warning')
.option('-p --project [filePath]', 'build with the provided sern.build file')
.option('-e --env', 'path to .env file')
.option('--source-maps', 'Whether to add source-maps to configuration', false)
.option('--tsconfig [filePath]', 'Use this tsconfig')
.action(async (...args) => importDynamic('build.js').then((m) => m.build(...args)));

View File

@@ -1,31 +0,0 @@
import type { Choice, PromptObject } from 'prompts';
import { fetch } from 'undici';
async function gimmechoices(): Promise<Choice[]> {
const link = `https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json`;
const resp = await fetch(link).catch(() => null);
if (!resp) return [{ title: 'No plugins found!', value: '', disabled: true }];
const data = (await resp.json()) as Data[];
const choices = data.map((e) => ({
title: e.name,
value: e,
}));
return choices;
}
export async function pluginsQ(): Promise<PromptObject> {
return {
name: 'list',
type: 'autocompleteMultiselect',
message: 'What plugins do you want to install?',
choices: await gimmechoices(),
min: 1,
};
}
interface Data {
name: string;
link: string;
}

View File

@@ -4,9 +4,6 @@ export interface sernConfig {
base: string;
commands: string;
};
scripts?: {
prepublish?: string;
};
buildPath: string;
rest?: Record<string, Record<string, unknown>>;
}
@@ -14,6 +11,6 @@ export interface sernConfig {
export interface TheoreticalEnv {
DISCORD_TOKEN: string;
APPLICATION_ID?: string;
MODE: 'PROD' | 'DEV';
MODE: 'production' | 'environment';
[name: string]: string;
}

View File

@@ -1,12 +1,11 @@
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,
tsconfig,
logLevel: 'info',
minify: false,
outdir: resolve('dist'),
outdir: resolve(outdir),
} satisfies esbuild.BuildOptions);

View File

@@ -1,6 +1,7 @@
import { findUp } from 'find-up';
import { readFile, rename, writeFile } from 'node:fs/promises';
import { fromCwd } from './fromCwd.js';
import { parseTsConfig } from './parseTsconfig.js';
/**
* It takes a string, finds the package.json file in the directory of the string, and changes the name
@@ -61,10 +62,12 @@ export async function editDirs(
});
if (tsconfig) {
const output = JSON.parse(await readFile(tsconfig, 'utf8'));
const output = await parseTsConfig(tsconfig);
if (!output) throw new Error("Can't read your tsconfig.json.");
if (!output.compilerOptions) throw new Error("Can't find compilerOptions in your tsconfig.json.");
output.compilerOptions.rootDir = srcName;
// This will strip comments/trailing commas from the tsconfig.json file
await writeFile(tsconfig, JSON.stringify(output, null, 2));
}

View File

@@ -1,17 +1,19 @@
import { readFile } from 'node:fs/promises';
import { findUp } from 'find-up';
import { readFile } from 'node:fs/promises';
import assert from 'node:assert';
export async function getConfig(): Promise<sernConfig> {
export async function getConfig(): Promise<SernConfig> {
const sernLocation = await findUp('sern.config.json');
assert(sernLocation, "Can't find sern.config.json");
const output = JSON.parse(await readFile(sernLocation, 'utf8')) as sernConfig;
const output = JSON.parse(await readFile(sernLocation, 'utf8')) as SernConfig;
assert(output, "Can't read your sern.config.json.");
return output;
}
export interface sernConfig {
export interface SernConfig {
language: 'typescript' | 'javascript';
defaultPrefix?: string;
paths: {
@@ -19,21 +21,7 @@ export interface sernConfig {
commands: string;
events?: string;
};
app?: {
customInstallUrl?: string;
description?: string;
roleConnectionsVerificationUrl?: string;
installParams?: {
type: 'install params object';
};
integrationTypesConfig?: {
type: 'dictionary with keys of application integration types';
description: 'In preview. Default scopes and permissions for each supported installation context. Value for each key is an integration type configuration object';
};
flags?: number;
icon?: '?image data';
coverImage?: '?image data';
interactionsEndpointUrl?: string;
tags: string[];
build?: {
}
}

View File

@@ -1,18 +0,0 @@
import { findUp } from 'find-up';
import { readFile } from 'node:fs/promises';
/**
* It finds the sern.config.json file, reads it, and returns the language property
* @returns The language of the project.
*/
export async function getLang(): Promise<'typescript' | 'javascript'> {
const sernLocation = await findUp('sern.config.json');
if (!sernLocation) throw new Error("Can't find sern.config.json");
const output = JSON.parse(await readFile(sernLocation, 'utf8'));
if (!output) throw new Error("Can't read your sern.config.json.");
return output.language;
}

View File

@@ -0,0 +1,50 @@
import { resolve } from 'node:path';
import { readFile } from 'node:fs/promises';
interface CompilerOptions {
target: string;
module: string;
lib: string[];
allowJs: boolean;
checkJs: boolean;
jsx: string;
declaration: boolean;
sourceMap: boolean;
outDir: string;
rootDir: string;
strict: boolean;
esModuleInterop: boolean;
forceConsistentCasingInFileNames: boolean;
noEmit: boolean;
importHelpers: boolean;
isolatedModules: boolean;
moduleResolution: string;
resolveJsonModule: boolean;
noEmitHelpers: boolean;
}
interface TsConfig {
compilerOptions: CompilerOptions;
files: string[];
include: string[];
exclude: string[];
extends: string;
}
const cleanJson = (json: string) =>
json
.replace(/\/\/.*$/gm, '')
.replace(/\/\*[\s\S]*?\*\//gm, '')
.replace(/,\s*([}\]])/g, '$1');
export const parseTsConfig = async (path: string) => {
const absPath = resolve(path);
const fileContent = await readFile(absPath, 'utf-8');
const cleanContent = cleanJson(fileContent);
try {
return JSON.parse(cleanContent) as Partial<TsConfig>;
} catch (e) {
return null;
}
};

View File

@@ -37,7 +37,6 @@ const writeAmbientFile = async (path: string, define: Record<string, string>, wr
const writeTsConfig = async (format: 'cjs' | 'esm', configPath: string, fw: FileWriter) => {
//maybe better way to do this
const target = format === 'esm' ? { target: 'esnext' } : {};
const sernTsConfig = {
compilerOptions: {
//module determines top level await. CJS doesn't have that abliity afaik
@@ -45,7 +44,7 @@ const writeTsConfig = async (format: 'cjs' | 'esm', configPath: string, fw: File
moduleResolution: 'node',
strict: true,
skipLibCheck: true,
...target,
target: 'esnext',
rootDirs: ['./generated', '../src'],
},
include: ['./ambient.d.ts', '../src'],

View File

@@ -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<T>(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>|boolean
onEntry?: (etry: RouteEntry) => RouteEntry
}
export async function* readPaths(
config: ReadPathsConfig
): AsyncGenerator<RouteEntry> {
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);
}
}
}
}

117
templates/cf.js Normal file
View File

@@ -0,0 +1,117 @@
// 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);
}
}
function createContext(rawcontext) {
return rawcontext
}
async function executeModule(
emitter,
logger,
errHandler,
{ module, task, args },
) {
try {
await module.execute(args);
//emitter.emit('module.activate', /*resultPayload(PayloadType.Success, module)*/);
} catch(e) {
throw e
}
}
async function applyPlugins(module, payload) {
let success = true;
for (const plg of module.onEvent) {
const res = await plg.execute(payload);
if(!res.isOk()) {
success = false;
}
}
return success;
}
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_AUTOCOMPLETE) {
"use autocomplete";
} else if (interaction.type === InteractionType.APPLICATION_COMMAND) {
"use slash";
}
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;

View File

@@ -1,15 +1,15 @@
FROM node:latest
RUN npm install -g @sern/cli typescript@latest
WORKDIR /app
COPY package.json ./
RUN npm install
RUN npm install -D typescript
COPY . .
RUN tsc --build || npx -p typescript tsc --build
RUN sern build
CMD node dist/index.js

View File

@@ -1,5 +1,6 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
const shared = {
entry: [
'src/index.ts',