@wolfstar/create-http-framework 2.5.2 → 2.6.0-next-20260926200949
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.
- package/README.md +32 -3
- package/dist/index.js +222 -17
- package/package.json +3 -3
- package/template/base/.env.hbs +10 -0
- package/template/base/.gitignore.hbs +1 -0
- package/template/base/README.md.hbs +30 -1
- package/template/base/src/lib/types/augments.ts.hbs +8 -0
- package/template/base/src/main.js.hbs +9 -1
- package/template/base/src/main.ts.hbs +48 -1
- package/template/features/cache/src/lib/cache.js.hbs +27 -0
- package/template/features/cache/src/lib/cache.ts.hbs +27 -0
- package/template/features/gateway/src/main.js.hbs +54 -0
- package/template/features/gateway/src/main.ts.hbs +96 -0
- package/template/features/redis/compose.yaml +5 -0
- package/template/features/sharder/src/main.js.hbs +25 -0
- package/template/features/sharder/src/main.ts.hbs +25 -0
- package/template/features/sharder/src/shard.js.hbs +49 -0
- package/template/features/sharder/src/shard.ts.hbs +61 -0
- package/template/features/i18n/src/main.js.hbs +0 -36
- package/template/features/i18n/src/main.ts.hbs +0 -36
package/README.md
CHANGED
|
@@ -34,13 +34,16 @@ The CLI will guide you through the following prompts:
|
|
|
34
34
|
- **Project name** — an npm-compatible name for your bot
|
|
35
35
|
- **Package manager** — npm, yarn, pnpm, or bun
|
|
36
36
|
- **Language** — TypeScript or JavaScript
|
|
37
|
-
- **Build tool** — tsdown, TypeScript 6, or the TypeScript 7 release candidate
|
|
37
|
+
- **Build tool** — tsdown, Vite, Vite + Nitro, TypeScript 6, or the TypeScript 7 release candidate (Vite and Vite + Nitro are experimental)
|
|
38
38
|
- **Linter and formatter** — Oxlint / ESLint and Oxfmt / Prettier
|
|
39
39
|
- **Port** — the port the HTTP server will listen on (default: `3000`)
|
|
40
40
|
- **Optional features** (multiselect) —
|
|
41
41
|
- **i18n** — add [`@wolfstar/plugin-i18next`](https://github.com/wolfstar-project/stars-components/tree/main/packages/plugin-i18next)
|
|
42
42
|
- **Subcommands** — add an example command that uses subcommands
|
|
43
43
|
- **Testing** — set up Vitest with [`@wolfstar/http-framework-test-utils`](https://github.com/wolfstar-project/stars-components/tree/main/packages/http-framework-test-utils)
|
|
44
|
+
- **Gateway** — receive gateway events next to the HTTP interactions with [`@wolfstar/plugin-gateway`](https://github.com/wolfstar-project/plugins/tree/main/packages/plugin-gateway)
|
|
45
|
+
- **Cache** — cache gateway entities with [`@wolfstar/plugin-cache`](https://github.com/wolfstar-project/plugins/tree/main/packages/plugin-cache), in memory or in Redis (asked as a follow-up)
|
|
46
|
+
- **Sharder** — spread gateway shards across cluster workers with [`@wolfstar/plugin-sharder`](https://github.com/wolfstar-project/plugins/tree/main/packages/plugin-sharder)
|
|
44
47
|
- **Auto-install** — install dependencies immediately after scaffolding
|
|
45
48
|
|
|
46
49
|
## Options
|
|
@@ -52,13 +55,17 @@ The CLI will guide you through the following prompts:
|
|
|
52
55
|
| `--interactive` | `-i` | Force interactive prompts even when an AI agent is detected |
|
|
53
56
|
| `--package-manager <pm>` | | Choose npm, yarn, pnpm, or bun |
|
|
54
57
|
| `--language <lang>` | | Choose TypeScript (`ts`) or JavaScript (`js`) |
|
|
55
|
-
| `--build <tool>` | | Choose `tsc6`, `tsc7`, or `
|
|
58
|
+
| `--build <tool>` | | Choose `tsc6`, `tsc7`, `tsdown`, `vite`, or `vite-nitro` for TypeScript |
|
|
56
59
|
| `--lint <linter>` | | Choose `none`, `eslint`, or `oxlint` |
|
|
57
60
|
| `--format <formatter>` | | Choose `none`, `prettier`, or `oxfmt` |
|
|
58
61
|
| `--port <number>` | | Set the HTTP port (default: `3000`) |
|
|
59
62
|
| `--i18n` / `--no-i18n` | | Enable or disable `@wolfstar/plugin-i18next` scaffolding |
|
|
60
63
|
| `--subcommands` / `--no-subcommands` | | Enable or disable the example subcommand command |
|
|
61
64
|
| `--testing` / `--no-testing` | | Enable or disable the Vitest + `@wolfstar/http-framework-test-utils` setup |
|
|
65
|
+
| `--gateway` / `--no-gateway` | | Enable or disable `@wolfstar/plugin-gateway` scaffolding |
|
|
66
|
+
| `--cache` / `--no-cache` | | Enable or disable `@wolfstar/plugin-cache` (turns `--gateway` on) |
|
|
67
|
+
| `--redis` / `--no-redis` | | Cache in Redis instead of memory (turns `--cache` on) |
|
|
68
|
+
| `--sharder` / `--no-sharder` | | Enable or disable `@wolfstar/plugin-sharder` (turns `--gateway` on) |
|
|
62
69
|
| `--install` / `--no-install` | | Enable or disable dependency installation |
|
|
63
70
|
| `--help` | `-h` | Print usage and exit |
|
|
64
71
|
|
|
@@ -82,6 +89,7 @@ my-discord-bot/
|
|
|
82
89
|
│ │ ├── setup/
|
|
83
90
|
│ │ │ ├── all.ts # Aggregates setup imports
|
|
84
91
|
│ │ │ └── logger.ts # Logger configuration
|
|
92
|
+
│ │ ├── cache.ts # Gateway entity cache (--cache)
|
|
85
93
|
│ │ └── types/
|
|
86
94
|
│ │ └── augments.ts # Module augmentations (TypeScript only)
|
|
87
95
|
│ ├── locales/
|
|
@@ -90,11 +98,13 @@ my-discord-bot/
|
|
|
90
98
|
│ │ └── ping.json # Example locale resource (--i18n)
|
|
91
99
|
│ ├── @types/
|
|
92
100
|
│ │ └── i18next.d.ts # Generated i18next augmentation (--i18n)
|
|
101
|
+
│ ├── shard.ts # Gateway client run by each shard worker (--sharder)
|
|
93
102
|
│ └── main.ts # Entry point — starts the HTTP server
|
|
94
103
|
├── tests/
|
|
95
104
|
│ └── ping.test.ts # Example test (--testing)
|
|
96
105
|
├── vitest.config.ts # Vitest configuration (--testing)
|
|
97
106
|
├── vitest.setup.ts # Vitest setup file (--testing)
|
|
107
|
+
├── compose.yaml # Local Redis server (--redis)
|
|
98
108
|
├── README.md # Generated project README
|
|
99
109
|
├── .env # Environment variables (DISCORD_TOKEN, DISCORD_PUBLIC_KEY)
|
|
100
110
|
├── .gitignore
|
|
@@ -105,8 +115,27 @@ my-discord-bot/
|
|
|
105
115
|
- `src/locales/en-US/commands/ping.json` and `src/@types/i18next.d.ts` are only generated when **i18n** is enabled.
|
|
106
116
|
- `src/@types/i18next.d.ts` is produced by `@wolfstar/i18next-type-generator`; the CLI runs it automatically at the end of scaffolding, and it can be re-run any time locale files change (see [`generate:i18n`](#i18n-type-generation) below).
|
|
107
117
|
- `src/commands/math.ts` is only generated when **Subcommands** is enabled.
|
|
118
|
+
- `src/lib/cache.ts` is only generated with **Cache**, `src/shard.ts` with **Sharder**, and `compose.yaml` with **Redis**.
|
|
108
119
|
- `tests/ping.test.ts`, `vitest.config.ts`, and `vitest.setup.ts` are only generated when **Testing** is enabled.
|
|
109
120
|
|
|
121
|
+
### Vite and Nitro
|
|
122
|
+
|
|
123
|
+
`--build vite` bundles the bot with [Vite](https://vite.dev) into a single `dist/main.js`, and `--build vite-nitro` hands it to [Nitro](https://nitro.build), which writes a deployable `.output/` (the `node-server` preset, pinned in `stars.config.ts`). Both are TypeScript only and turn on `experimental.enableVite` (and `enableNitro`) in the generated `stars.config.ts`.
|
|
124
|
+
|
|
125
|
+
- A bundle has no `commands` directory to scan, so `src/main.ts` imports and loads the example commands explicitly. Add your own commands there.
|
|
126
|
+
- With Nitro, `src/main.ts` default-exports the client instead of calling `listen()`, and the port comes from `PORT` in `.env`.
|
|
127
|
+
- `main` and `start` are wired to the `node-server` preset's output, `.output/server/index.mjs`. Changing `nitro.preset` to target another platform means deploying that platform's `.output/` instead of running `npm start` — and with `--gateway`, only a preset that keeps a server alive will hold the gateway connection open.
|
|
128
|
+
- Nitro cannot be combined with `--sharder`: the sharder's manager process has no client for Nitro to forward requests to.
|
|
129
|
+
|
|
130
|
+
### Gateway, cache and sharder
|
|
131
|
+
|
|
132
|
+
The gateway plugins are for bots that also need gateway events, so they require a long-lived process and Node.js `>=24.17.0`.
|
|
133
|
+
|
|
134
|
+
- `--gateway` makes `src/main.*` create a `GatewayClient` (it still answers HTTP interactions).
|
|
135
|
+
- `--cache` adds `src/lib/cache.*`, an in-memory cache by default. `--redis` (or the follow-up prompt) swaps it for `ioredis`, adds a `compose.yaml` to start a local Redis, and `REDIS_URL` to `.env`.
|
|
136
|
+
- `--sharder` turns `src/main.*` into the shard manager and adds `src/shard.*`, which runs in every cluster worker (`SHARDER_CLUSTERS` in `.env`).
|
|
137
|
+
- `--cache` and `--sharder` switch `--gateway` on, and `--redis` switches `--cache` on.
|
|
138
|
+
|
|
110
139
|
### i18n type generation
|
|
111
140
|
|
|
112
141
|
When i18n is enabled, the generated `package.json` includes a `generate:i18n` script that (re)generates `src/@types/i18next.d.ts` from the JSON files under `src/locales/`:
|
|
@@ -119,4 +148,4 @@ The CLI runs this script automatically once at the end of scaffolding; re-run it
|
|
|
119
148
|
|
|
120
149
|
## Requirements
|
|
121
150
|
|
|
122
|
-
- Node.js `>=20`
|
|
151
|
+
- Node.js `>=20` (`>=24.17.0` for projects using the gateway)
|
package/dist/index.js
CHANGED
|
@@ -45,8 +45,40 @@ const LANGUAGES = ["ts", "js"];
|
|
|
45
45
|
const BUILD_TOOLS = [
|
|
46
46
|
"tsc6",
|
|
47
47
|
"tsc7",
|
|
48
|
-
"tsdown"
|
|
48
|
+
"tsdown",
|
|
49
|
+
"vite",
|
|
50
|
+
"vite-nitro"
|
|
49
51
|
];
|
|
52
|
+
/** `vite` and `vite-nitro` both bundle the app with Vite, so pieces are loaded explicitly instead of scanned from disk. */
|
|
53
|
+
function isViteBuild(buildTool) {
|
|
54
|
+
return buildTool === "vite" || buildTool === "vite-nitro";
|
|
55
|
+
}
|
|
56
|
+
/** `vite-nitro` hands the built client to Nitro, which owns the HTTP server. */
|
|
57
|
+
function isNitroBuild(buildTool) {
|
|
58
|
+
return buildTool === "vite-nitro";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Applies the dependencies between the gateway features: Redis needs the cache, and the cache and the sharder both
|
|
62
|
+
* need the gateway client. `implied` names the features that were switched on to satisfy another one.
|
|
63
|
+
*/
|
|
64
|
+
function resolveGatewayFeatures(features) {
|
|
65
|
+
const resolved = { ...features };
|
|
66
|
+
const implied = [];
|
|
67
|
+
if (resolved.redis && !resolved.cache) {
|
|
68
|
+
resolved.cache = true;
|
|
69
|
+
implied.push("cache");
|
|
70
|
+
}
|
|
71
|
+
if ((resolved.cache || resolved.sharder) && !resolved.gateway) {
|
|
72
|
+
resolved.gateway = true;
|
|
73
|
+
implied.push("gateway");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
features: resolved,
|
|
77
|
+
implied
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** The Nitro entry must default-export the HTTP client, which the sharder's manager process never has. */
|
|
81
|
+
const SHARDER_WITH_NITRO_ERROR = "--sharder cannot be combined with --build vite-nitro: Nitro needs a client to forward requests to in every process.";
|
|
50
82
|
const LINTERS = [
|
|
51
83
|
"none",
|
|
52
84
|
"eslint",
|
|
@@ -74,7 +106,7 @@ async function fetchVersion(packageName) {
|
|
|
74
106
|
/**
|
|
75
107
|
* Resolves the latest versions of only the packages required by the chosen selections.
|
|
76
108
|
* `ts-node` is intentionally never included. TypeScript 7.0 (tsc7) is pinned to the rc instead
|
|
77
|
-
* of being fetched, because `typescript@latest` resolves to the 6.x line.
|
|
109
|
+
* of being fetched, because `typescript@latest` resolves to the 6.x line. Nitro v3 is still a beta, so callers pin it exactly.
|
|
78
110
|
*/
|
|
79
111
|
async function fetchDependencyVersions(selections) {
|
|
80
112
|
const names = /* @__PURE__ */ new Set([
|
|
@@ -88,6 +120,10 @@ async function fetchDependencyVersions(selections) {
|
|
|
88
120
|
]);
|
|
89
121
|
if (selections.i18n) names.add("@wolfstar/plugin-i18next").add("@wolfstar/i18next-type-generator");
|
|
90
122
|
if (selections.testing) names.add("vitest").add("@wolfstar/http-framework-test-utils");
|
|
123
|
+
if (selections.gateway) names.add("@wolfstar/plugin-gateway");
|
|
124
|
+
if (selections.cache) names.add("@wolfstar/plugin-cache");
|
|
125
|
+
if (selections.redis) names.add("ioredis");
|
|
126
|
+
if (selections.sharder) names.add("@wolfstar/plugin-sharder");
|
|
91
127
|
if (selections.language === "ts") {
|
|
92
128
|
names.add("@types/node");
|
|
93
129
|
switch (selections.buildTool) {
|
|
@@ -95,7 +131,13 @@ async function fetchDependencyVersions(selections) {
|
|
|
95
131
|
names.add("typescript");
|
|
96
132
|
break;
|
|
97
133
|
case "tsc7": break;
|
|
98
|
-
case "tsdown":
|
|
134
|
+
case "tsdown":
|
|
135
|
+
names.add("tsdown").add("typescript");
|
|
136
|
+
break;
|
|
137
|
+
case "vite":
|
|
138
|
+
case "vite-nitro":
|
|
139
|
+
names.add("vite").add("typescript");
|
|
140
|
+
if (isNitroBuild(selections.buildTool)) names.add("nitro");
|
|
99
141
|
}
|
|
100
142
|
}
|
|
101
143
|
if (selections.linter === "eslint") {
|
|
@@ -183,12 +225,17 @@ function sortKeys(record) {
|
|
|
183
225
|
function json(value) {
|
|
184
226
|
return `${JSON.stringify(value, null, " ")}\n`;
|
|
185
227
|
}
|
|
228
|
+
/** The file `start` runs and `main` points at: sources for JavaScript, the build output for TypeScript. */
|
|
229
|
+
function entryFile(ctx) {
|
|
230
|
+
if (ctx.language === "js") return "src/main.js";
|
|
231
|
+
return isNitroBuild(ctx.buildTool) ? ".output/server/index.mjs" : "dist/main.js";
|
|
232
|
+
}
|
|
186
233
|
function buildScripts(ctx) {
|
|
187
234
|
const scripts = {
|
|
188
235
|
dev: "stars dev",
|
|
189
|
-
...ctx.language === "ts" && ctx.buildTool === "tsdown" ? { postinstall: "stars prepare" } : {},
|
|
236
|
+
...ctx.language === "ts" && (ctx.buildTool === "tsdown" || isViteBuild(ctx.buildTool)) ? { postinstall: "stars prepare" } : {},
|
|
190
237
|
...ctx.language === "js" ? {} : { build: "stars build" },
|
|
191
|
-
start:
|
|
238
|
+
start: `node ${entryFile(ctx)}`
|
|
192
239
|
};
|
|
193
240
|
if (ctx.linter === "oxlint") {
|
|
194
241
|
scripts["lint"] = "oxlint src";
|
|
@@ -219,6 +266,10 @@ function buildDependencies(ctx) {
|
|
|
219
266
|
"gradient-string": caret(v["gradient-string"])
|
|
220
267
|
};
|
|
221
268
|
if (ctx.i18n) dependencies["@wolfstar/plugin-i18next"] = caret(v["@wolfstar/plugin-i18next"]);
|
|
269
|
+
if (ctx.gateway) dependencies["@wolfstar/plugin-gateway"] = caret(v["@wolfstar/plugin-gateway"]);
|
|
270
|
+
if (ctx.cache) dependencies["@wolfstar/plugin-cache"] = caret(v["@wolfstar/plugin-cache"]);
|
|
271
|
+
if (ctx.redis) dependencies["ioredis"] = caret(v["ioredis"]);
|
|
272
|
+
if (ctx.sharder) dependencies["@wolfstar/plugin-sharder"] = caret(v["@wolfstar/plugin-sharder"]);
|
|
222
273
|
return sortKeys(dependencies);
|
|
223
274
|
}
|
|
224
275
|
function buildDevDependencies(ctx) {
|
|
@@ -236,6 +287,12 @@ function buildDevDependencies(ctx) {
|
|
|
236
287
|
case "tsdown":
|
|
237
288
|
dev["tsdown"] = caret(v["tsdown"]);
|
|
238
289
|
dev["typescript"] = caret(v["typescript"]);
|
|
290
|
+
break;
|
|
291
|
+
case "vite":
|
|
292
|
+
case "vite-nitro":
|
|
293
|
+
dev["vite"] = caret(v["vite"]);
|
|
294
|
+
dev["typescript"] = caret(v["typescript"]);
|
|
295
|
+
if (isNitroBuild(ctx.buildTool)) dev["nitro"] = v["nitro"];
|
|
239
296
|
}
|
|
240
297
|
}
|
|
241
298
|
if (ctx.linter === "eslint") {
|
|
@@ -254,7 +311,7 @@ function buildDevDependencies(ctx) {
|
|
|
254
311
|
}
|
|
255
312
|
function packageJson(ctx) {
|
|
256
313
|
const devDependencies = buildDevDependencies(ctx);
|
|
257
|
-
const main = ctx
|
|
314
|
+
const main = entryFile(ctx);
|
|
258
315
|
return json({
|
|
259
316
|
name: ctx.name,
|
|
260
317
|
version: "1.0.0",
|
|
@@ -264,7 +321,7 @@ function packageJson(ctx) {
|
|
|
264
321
|
scripts: buildScripts(ctx),
|
|
265
322
|
dependencies: buildDependencies(ctx),
|
|
266
323
|
...Object.keys(devDependencies).length > 0 ? { devDependencies } : {},
|
|
267
|
-
engines: { node: ">=20" }
|
|
324
|
+
engines: { node: ctx.gateway ? ">=24.17.0" : ">=20" }
|
|
268
325
|
});
|
|
269
326
|
}
|
|
270
327
|
const sharedCompilerOptions = {
|
|
@@ -283,6 +340,19 @@ const sharedCompilerOptions = {
|
|
|
283
340
|
/** Writes the tsconfig(s). The tsc branches use a composite build so `tsc -b src` resolves `src/tsconfig.json`. */
|
|
284
341
|
function writeTsconfig(targetDir, ctx) {
|
|
285
342
|
if (ctx.language === "js") return;
|
|
343
|
+
if (isViteBuild(ctx.buildTool)) {
|
|
344
|
+
writeFile(join(targetDir, "tsconfig.json"), json({
|
|
345
|
+
extends: "./.stars/tsconfig.json",
|
|
346
|
+
compilerOptions: { types: ["node"] },
|
|
347
|
+
include: ["src/**/*.ts", ".stars/*.d.ts"],
|
|
348
|
+
exclude: [
|
|
349
|
+
"node_modules",
|
|
350
|
+
"dist",
|
|
351
|
+
".output"
|
|
352
|
+
]
|
|
353
|
+
}));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
286
356
|
if (ctx.buildTool === "tsdown") {
|
|
287
357
|
writeFile(join(targetDir, "tsconfig.json"), json({
|
|
288
358
|
extends: "./.stars/tsconfig.json",
|
|
@@ -312,14 +382,16 @@ function writeTsconfig(targetDir, ctx) {
|
|
|
312
382
|
}
|
|
313
383
|
/**
|
|
314
384
|
* Writes the `stars.config.*` file read by the `stars` CLI (`dev`, `build`, `info`, `codegen` scripts). Conventional
|
|
315
|
-
* JavaScript and tsdown projects need no options; an explicit tsc selection is the only generated override.
|
|
385
|
+
* JavaScript and tsdown projects need no options; an explicit tsc, Vite or Nitro selection is the only generated override.
|
|
316
386
|
*/
|
|
317
387
|
function writeStarsConfig(targetDir, ctx) {
|
|
318
388
|
const isJs = ctx.language === "js";
|
|
389
|
+
let options = !isJs && (ctx.buildTool === "tsc6" || ctx.buildTool === "tsc7") ? "{ build: { tool: 'tsc' } }" : "{}";
|
|
390
|
+
if (!isJs && isViteBuild(ctx.buildTool)) options = `{ build: { tool: 'vite' }, experimental: { enableVite: true${isNitroBuild(ctx.buildTool) ? ", enableNitro: true, nitro: { preset: 'node-server' }" : ""} } }`;
|
|
319
391
|
const content = [
|
|
320
392
|
"import { defineConfig } from '@wolfstar/http-framework/config';",
|
|
321
393
|
"",
|
|
322
|
-
`export default defineConfig(${
|
|
394
|
+
`export default defineConfig(${options});`,
|
|
323
395
|
""
|
|
324
396
|
].join("\n");
|
|
325
397
|
writeFile(join(targetDir, isJs ? "stars.config.js" : "stars.config.ts"), content);
|
|
@@ -421,6 +493,31 @@ function removeI18nDeclaration(outputDir) {
|
|
|
421
493
|
const typesDir = dirname(target);
|
|
422
494
|
if (existsSync(typesDir) && readdirSync(typesDir).length === 0) rmSync(typesDir, { recursive: true });
|
|
423
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* What the Handlebars sources see: the persisted {@link TemplateContext} plus values derived from it. Deriving at render
|
|
498
|
+
* time keeps the manifest small and lets manifests written before these fields existed still render.
|
|
499
|
+
*/
|
|
500
|
+
function toRenderContext(context) {
|
|
501
|
+
const typescript = context.language === "ts";
|
|
502
|
+
const commands = [{
|
|
503
|
+
className: "PingCommand",
|
|
504
|
+
file: "ping"
|
|
505
|
+
}];
|
|
506
|
+
if (context.subcommandsAdvanced) commands.push({
|
|
507
|
+
className: "SettingsCommand",
|
|
508
|
+
file: "settings"
|
|
509
|
+
});
|
|
510
|
+
else if (context.subcommands) commands.push({
|
|
511
|
+
className: "MathCommand",
|
|
512
|
+
file: "math"
|
|
513
|
+
});
|
|
514
|
+
return {
|
|
515
|
+
...context,
|
|
516
|
+
vite: typescript && isViteBuild(context.buildTool),
|
|
517
|
+
nitro: typescript && isNitroBuild(context.buildTool),
|
|
518
|
+
commands
|
|
519
|
+
};
|
|
520
|
+
}
|
|
424
521
|
function walkDir(dir) {
|
|
425
522
|
const entries = readdirSync(dir);
|
|
426
523
|
const files = [];
|
|
@@ -438,6 +535,10 @@ function walkDir(dir) {
|
|
|
438
535
|
function resolveFeatureDirs(ctx) {
|
|
439
536
|
const dirs = [];
|
|
440
537
|
if (ctx.i18n) dirs.push("i18n");
|
|
538
|
+
if (ctx.gateway) dirs.push("gateway");
|
|
539
|
+
if (ctx.cache) dirs.push("cache");
|
|
540
|
+
if (ctx.redis) dirs.push("redis");
|
|
541
|
+
if (ctx.sharder) dirs.push("sharder");
|
|
441
542
|
if (ctx.subcommandsAdvanced) dirs.push(ctx.i18n ? "subcommands-advanced-i18n" : "subcommands-advanced");
|
|
442
543
|
else if (ctx.subcommands) dirs.push(ctx.i18n ? "subcommands-i18n" : "subcommands");
|
|
443
544
|
if (ctx.testing) {
|
|
@@ -464,7 +565,7 @@ function collectOutputPaths(root, language) {
|
|
|
464
565
|
}
|
|
465
566
|
function renderSource(absoluteSource, context) {
|
|
466
567
|
const rawContent = readFileSync(absoluteSource, "utf-8");
|
|
467
|
-
return absoluteSource.endsWith(".hbs") ? Handlebars.compile(rawContent)(context) : rawContent;
|
|
568
|
+
return absoluteSource.endsWith(".hbs") ? Handlebars.compile(rawContent)(toRenderContext(context)) : rawContent;
|
|
468
569
|
}
|
|
469
570
|
function escapeRegExp(text) {
|
|
470
571
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -483,8 +584,7 @@ function isSimpleInterpolationOnly(rawSource) {
|
|
|
483
584
|
* existed — every run after this one writes a manifest, closing the gap for good.
|
|
484
585
|
*
|
|
485
586
|
* `null` for files with block helpers (`{{#if}}`, …), which can't be turned into a simple wildcard
|
|
486
|
-
* pattern this way;
|
|
487
|
-
* exact-render fallback available if that changes.
|
|
587
|
+
* pattern this way; the caller compares those against {@link buildLegacyVariantMatchers} instead.
|
|
488
588
|
*/
|
|
489
589
|
function buildStaleFileMatcher(absoluteSource) {
|
|
490
590
|
const rawContent = readFileSync(absoluteSource, "utf-8");
|
|
@@ -494,6 +594,33 @@ function buildStaleFileMatcher(absoluteSource) {
|
|
|
494
594
|
return new RegExp(`^${literalChunks.join("[\\s\\S]*?")}$`);
|
|
495
595
|
}
|
|
496
596
|
/**
|
|
597
|
+
* Matchers for a source using block helpers (`main.*` is the one that matters), covering exactly the ways the generator
|
|
598
|
+
* that predated the manifest could have written it: `tsdown`, with or without i18n, and none of the later features.
|
|
599
|
+
* Anything older than that renders differently and is preserved rather than cleaned, which is the safe direction to
|
|
600
|
+
* be wrong in. Like {@link buildStaleFileMatcher} it leaves the interpolated name and port as wildcards, by rendering
|
|
601
|
+
* placeholders there.
|
|
602
|
+
*/
|
|
603
|
+
function buildLegacyVariantMatchers(absoluteSource, context) {
|
|
604
|
+
const placeholder = "\0";
|
|
605
|
+
const legacy = {
|
|
606
|
+
...context,
|
|
607
|
+
name: placeholder,
|
|
608
|
+
port: placeholder,
|
|
609
|
+
gateway: false,
|
|
610
|
+
cache: false,
|
|
611
|
+
redis: false,
|
|
612
|
+
sharder: false
|
|
613
|
+
};
|
|
614
|
+
return [false, true].map((i18n) => {
|
|
615
|
+
const rendered = renderSource(absoluteSource, {
|
|
616
|
+
...legacy,
|
|
617
|
+
buildTool: "tsdown",
|
|
618
|
+
i18n
|
|
619
|
+
});
|
|
620
|
+
return new RegExp(`^${rendered.split(placeholder).map(escapeRegExp).join("[\\s\\S]*?")}$`);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
497
624
|
* Maps every output-relative path `root` could ever produce — for either language — to the source
|
|
498
625
|
* file(s) that render it. Unlike {@link collectOutputPaths} this ignores `context.language`, so it
|
|
499
626
|
* also surfaces paths left behind by a previous run under the *other* language (e.g. `src/main.ts`
|
|
@@ -548,7 +675,7 @@ function removeStaleGeneratedFiles(outputDir, context) {
|
|
|
548
675
|
const actual = readFileSync(target, "utf-8");
|
|
549
676
|
if (manifestContext ? sources.some((source) => renderSource(source, manifestContext) === actual) : sources.some((source) => {
|
|
550
677
|
const matcher = buildStaleFileMatcher(source);
|
|
551
|
-
return matcher ? matcher
|
|
678
|
+
return (matcher ? [matcher] : buildLegacyVariantMatchers(source, context)).some((candidate) => candidate.test(actual));
|
|
552
679
|
})) rmSync(target);
|
|
553
680
|
else preserved.push(path);
|
|
554
681
|
}
|
|
@@ -569,7 +696,7 @@ function processDir(root, outputDir, context) {
|
|
|
569
696
|
const rawContent = readFileSync(absoluteSource, "utf-8");
|
|
570
697
|
const isHandlebars = absoluteSource.endsWith(".hbs");
|
|
571
698
|
const outputPath = join(outputDir, outputRelative);
|
|
572
|
-
const content = isHandlebars ? Handlebars.compile(rawContent)(context) : rawContent;
|
|
699
|
+
const content = isHandlebars ? Handlebars.compile(rawContent)(toRenderContext(context)) : rawContent;
|
|
573
700
|
writeFile(outputPath, content);
|
|
574
701
|
}
|
|
575
702
|
}
|
|
@@ -608,7 +735,7 @@ Options:
|
|
|
608
735
|
--interactive, -i Force interactive prompts even when an AI agent is detected
|
|
609
736
|
--package-manager <pm> npm | yarn | pnpm | bun (defaults to the detected one)
|
|
610
737
|
--language <lang> ts | js (default: ts)
|
|
611
|
-
--build <tool> tsc6 | tsc7 | tsdown — TypeScript only (default: tsdown)
|
|
738
|
+
--build <tool> tsc6 | tsc7 | tsdown | vite | vite-nitro — TypeScript only (default: tsdown)
|
|
612
739
|
--lint <linter> none | eslint | oxlint (default: oxlint)
|
|
613
740
|
--format <formatter> none | prettier | oxfmt (default: oxfmt)
|
|
614
741
|
--port <number> HTTP port (default: 3000)
|
|
@@ -616,6 +743,10 @@ Options:
|
|
|
616
743
|
--subcommands / --no-subcommands Toggle the subcommands example command (default: off)
|
|
617
744
|
--subcommands-advanced / --no-subcommands-advanced Toggle the subcommand groups example command (default: off; overrides --subcommands)
|
|
618
745
|
--testing / --no-testing Toggle the testing setup (vitest) (default: off)
|
|
746
|
+
--gateway / --no-gateway Toggle @wolfstar/plugin-gateway, gateway events next to the HTTP interactions (default: off)
|
|
747
|
+
--cache / --no-cache Toggle @wolfstar/plugin-cache, the gateway entity cache (default: off; enables --gateway)
|
|
748
|
+
--redis / --no-redis Store the cache in Redis instead of memory (default: off; enables --cache)
|
|
749
|
+
--sharder / --no-sharder Toggle @wolfstar/plugin-sharder, gateway shards across cluster workers (default: off; enables --gateway)
|
|
619
750
|
--install / --no-install Toggle dependency installation (default: on)
|
|
620
751
|
--ignore Write into an existing, non-empty directory without clearing it
|
|
621
752
|
--help, -h Print this message and exit
|
|
@@ -670,6 +801,10 @@ async function main() {
|
|
|
670
801
|
const cliSubcommands = argv["subcommands"];
|
|
671
802
|
const cliSubcommandsAdvanced = argv["subcommands-advanced"];
|
|
672
803
|
const cliTesting = argv["testing"];
|
|
804
|
+
const cliGateway = argv["gateway"];
|
|
805
|
+
const cliCache = argv["cache"];
|
|
806
|
+
const cliRedis = argv["redis"];
|
|
807
|
+
const cliSharder = argv["sharder"];
|
|
673
808
|
const cliInstall = argv["install"];
|
|
674
809
|
const { isAgent, agent } = await determineAgent();
|
|
675
810
|
const agentMode = isAgent && flagInteractive !== true;
|
|
@@ -816,6 +951,14 @@ async function main() {
|
|
|
816
951
|
{
|
|
817
952
|
value: "tsc7",
|
|
818
953
|
label: "TypeScript 7.0 rc (tsc)"
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
value: "vite",
|
|
957
|
+
label: "Vite (experimental)"
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
value: "vite-nitro",
|
|
961
|
+
label: "Vite + Nitro (experimental)"
|
|
819
962
|
}
|
|
820
963
|
],
|
|
821
964
|
initialValue: "tsdown"
|
|
@@ -885,11 +1028,20 @@ async function main() {
|
|
|
885
1028
|
let wantsSubcommands;
|
|
886
1029
|
let wantsSubcommandsAdvanced;
|
|
887
1030
|
let wantsTesting;
|
|
1031
|
+
let wantsGateway;
|
|
1032
|
+
let wantsCache;
|
|
1033
|
+
let wantsRedis;
|
|
1034
|
+
let wantsSharder;
|
|
1035
|
+
const nitro = language === "ts" && isNitroBuild(buildTool);
|
|
888
1036
|
if (nonInteractive) {
|
|
889
1037
|
wantsI18n = cliI18n ?? false;
|
|
890
1038
|
wantsSubcommands = cliSubcommands ?? false;
|
|
891
1039
|
wantsSubcommandsAdvanced = cliSubcommandsAdvanced ?? false;
|
|
892
1040
|
wantsTesting = cliTesting ?? false;
|
|
1041
|
+
wantsGateway = cliGateway ?? false;
|
|
1042
|
+
wantsCache = cliCache ?? false;
|
|
1043
|
+
wantsRedis = cliRedis ?? false;
|
|
1044
|
+
wantsSharder = cliSharder ?? false;
|
|
893
1045
|
} else {
|
|
894
1046
|
const featuresResult = await multiselect({
|
|
895
1047
|
message: "Which optional features would you like to add?",
|
|
@@ -909,7 +1061,19 @@ async function main() {
|
|
|
909
1061
|
{
|
|
910
1062
|
value: "testing",
|
|
911
1063
|
label: "Testing setup (vitest)"
|
|
912
|
-
}
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
value: "gateway",
|
|
1067
|
+
label: "Gateway events (@wolfstar/plugin-gateway)"
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
value: "cache",
|
|
1071
|
+
label: "Gateway entity cache (@wolfstar/plugin-cache)"
|
|
1072
|
+
},
|
|
1073
|
+
...nitro ? [] : [{
|
|
1074
|
+
value: "sharder",
|
|
1075
|
+
label: "Gateway sharding across cluster workers (@wolfstar/plugin-sharder)"
|
|
1076
|
+
}]
|
|
913
1077
|
],
|
|
914
1078
|
initialValues: [],
|
|
915
1079
|
required: false
|
|
@@ -923,11 +1087,38 @@ async function main() {
|
|
|
923
1087
|
wantsSubcommands = features.has("subcommands");
|
|
924
1088
|
wantsSubcommandsAdvanced = features.has("subcommands-advanced");
|
|
925
1089
|
wantsTesting = features.has("testing");
|
|
1090
|
+
wantsGateway = features.has("gateway");
|
|
1091
|
+
wantsCache = features.has("cache");
|
|
1092
|
+
wantsSharder = features.has("sharder");
|
|
1093
|
+
wantsRedis = cliRedis ?? false;
|
|
1094
|
+
if (wantsCache && cliRedis === void 0) {
|
|
1095
|
+
const redisResult = await confirm({
|
|
1096
|
+
message: "Would you like to store the cache in Redis (ioredis) instead of memory?",
|
|
1097
|
+
initialValue: false
|
|
1098
|
+
});
|
|
1099
|
+
if (isCancel(redisResult)) {
|
|
1100
|
+
cancel("Operation cancelled.");
|
|
1101
|
+
process.exit(0);
|
|
1102
|
+
}
|
|
1103
|
+
wantsRedis = Boolean(redisResult);
|
|
1104
|
+
}
|
|
926
1105
|
}
|
|
927
1106
|
if (wantsSubcommands && wantsSubcommandsAdvanced) {
|
|
928
1107
|
log.warn("Both --subcommands and --subcommands-advanced were selected; using the advanced example (with groups).");
|
|
929
1108
|
wantsSubcommands = false;
|
|
930
1109
|
}
|
|
1110
|
+
if (wantsSharder && nitro) {
|
|
1111
|
+
cancel(SHARDER_WITH_NITRO_ERROR);
|
|
1112
|
+
process.exit(1);
|
|
1113
|
+
}
|
|
1114
|
+
const gatewayFeatures = resolveGatewayFeatures({
|
|
1115
|
+
gateway: wantsGateway,
|
|
1116
|
+
cache: wantsCache,
|
|
1117
|
+
redis: wantsRedis,
|
|
1118
|
+
sharder: wantsSharder
|
|
1119
|
+
});
|
|
1120
|
+
if (gatewayFeatures.implied.length > 0) log.warn(`Enabling ${gatewayFeatures.implied.map((feature) => `--${feature}`).join(" and ")}, which the selected features depend on.`);
|
|
1121
|
+
({gateway: wantsGateway, cache: wantsCache, redis: wantsRedis, sharder: wantsSharder} = gatewayFeatures.features);
|
|
931
1122
|
let wantsInstall;
|
|
932
1123
|
if (nonInteractive) wantsInstall = cliInstall;
|
|
933
1124
|
else {
|
|
@@ -948,6 +1139,10 @@ async function main() {
|
|
|
948
1139
|
subcommands: wantsSubcommands,
|
|
949
1140
|
subcommandsAdvanced: wantsSubcommandsAdvanced,
|
|
950
1141
|
testing: wantsTesting,
|
|
1142
|
+
gateway: wantsGateway,
|
|
1143
|
+
cache: wantsCache,
|
|
1144
|
+
redis: wantsRedis,
|
|
1145
|
+
sharder: wantsSharder,
|
|
951
1146
|
language,
|
|
952
1147
|
buildTool,
|
|
953
1148
|
linter,
|
|
@@ -962,7 +1157,12 @@ async function main() {
|
|
|
962
1157
|
i18n: wantsI18n,
|
|
963
1158
|
subcommands: wantsSubcommands,
|
|
964
1159
|
subcommandsAdvanced: wantsSubcommandsAdvanced,
|
|
965
|
-
testing: wantsTesting
|
|
1160
|
+
testing: wantsTesting,
|
|
1161
|
+
gateway: wantsGateway,
|
|
1162
|
+
cache: wantsCache,
|
|
1163
|
+
redis: wantsRedis,
|
|
1164
|
+
sharder: wantsSharder,
|
|
1165
|
+
buildTool
|
|
966
1166
|
});
|
|
967
1167
|
writeProjectFiles(targetDir, {
|
|
968
1168
|
name: projectName,
|
|
@@ -971,6 +1171,10 @@ async function main() {
|
|
|
971
1171
|
subcommands: wantsSubcommands,
|
|
972
1172
|
subcommandsAdvanced: wantsSubcommandsAdvanced,
|
|
973
1173
|
testing: wantsTesting,
|
|
1174
|
+
gateway: wantsGateway,
|
|
1175
|
+
cache: wantsCache,
|
|
1176
|
+
redis: wantsRedis,
|
|
1177
|
+
sharder: wantsSharder,
|
|
974
1178
|
packageManager,
|
|
975
1179
|
language,
|
|
976
1180
|
buildTool,
|
|
@@ -1019,6 +1223,7 @@ async function main() {
|
|
|
1019
1223
|
}
|
|
1020
1224
|
const extraNotes = [];
|
|
1021
1225
|
if (wantsI18n) extraNotes.push(` After editing locale files, regenerate i18next types with: ${getRunScript(packageManager, "generate:i18n")}`);
|
|
1226
|
+
if (wantsRedis) extraNotes.push(" Start a local Redis server with: docker compose up -d");
|
|
1022
1227
|
if (wantsTesting) extraNotes.push(` Run the test suite with: ${getRunScript(packageManager, "test")}`);
|
|
1023
1228
|
outro(`Done! To get started:\n\n cd ${projectName}\n${wantsInstall ? "" : ` ${getInstallScript(packageManager)}\n`} ${getRunScript(packageManager, "dev")}${extraNotes.length ? `\n\n${extraNotes.join("\n")}` : ""}`);
|
|
1024
1229
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wolfstar/create-http-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0-next-20260926200949",
|
|
4
4
|
"description": "Create a new WolfStar HTTP Framework bot project",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"@types/handlebars": "^4.1.0",
|
|
47
47
|
"@types/mri": "^1.2.0",
|
|
48
48
|
"@types/node": "22.15.21",
|
|
49
|
-
"@vitest/coverage-v8": "^
|
|
49
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
50
50
|
"golar": "^0.1.10",
|
|
51
51
|
"tsdown": "^0.23.0",
|
|
52
52
|
"typescript": "~7.0.2",
|
|
53
|
-
"vitest": "^
|
|
53
|
+
"vitest": "^5.0.0"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=20"
|
package/template/base/.env.hbs
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
DISCORD_TOKEN=your_bot_token_here
|
|
2
2
|
DISCORD_PUBLIC_KEY=your_public_key_here
|
|
3
3
|
DISCORD_CLIENT_ID=your_client_id_here
|
|
4
|
+
{{#if nitro}}
|
|
5
|
+
PORT={{port}}
|
|
6
|
+
{{else}}
|
|
4
7
|
HTTP_ADDRESS=0.0.0.0
|
|
5
8
|
HTTP_PORT={{port}}
|
|
9
|
+
{{/if}}
|
|
6
10
|
HTTP_POST_PATH=/
|
|
11
|
+
{{#if redis}}
|
|
12
|
+
REDIS_URL=redis://localhost:6379
|
|
13
|
+
{{/if}}
|
|
14
|
+
{{#if sharder}}
|
|
15
|
+
SHARDER_CLUSTERS=2
|
|
16
|
+
{{/if}}
|
|
@@ -10,13 +10,38 @@ A Discord bot built with [`@wolfstar/http-framework`](https://www.npmjs.com/pack
|
|
|
10
10
|
{{#if i18n}}
|
|
11
11
|
3. This project uses [`@wolfstar/plugin-i18next`](https://www.npmjs.com/package/@wolfstar/plugin-i18next). Locale resources live under `src/locales/`; after editing them, regenerate the typed i18next resources with `npm run generate:i18n`.
|
|
12
12
|
{{/if}}
|
|
13
|
+
{{#if gateway}}
|
|
14
|
+
|
|
15
|
+
This project also connects to the Discord gateway with [`@wolfstar/plugin-gateway`](https://www.npmjs.com/package/@wolfstar/plugin-gateway): `src/main.*` creates a `GatewayClient` that answers HTTP interactions and emits gateway events. It needs a long-lived process and Node.js 24.17 or newer. Add the intents your listeners need to the client options, privileged ones must also be enabled in the Discord Developer Portal.
|
|
16
|
+
{{#if cache}}
|
|
17
|
+
{{#if redis}}
|
|
18
|
+
|
|
19
|
+
Entities are cached in Redis with [`@wolfstar/plugin-cache`](https://www.npmjs.com/package/@wolfstar/plugin-cache): start a local server with `docker compose up -d` (it listens on `REDIS_URL` from `.env`), or point `REDIS_URL` at your own.
|
|
20
|
+
{{else}}
|
|
21
|
+
|
|
22
|
+
Entities are cached in memory with [`@wolfstar/plugin-cache`](https://www.npmjs.com/package/@wolfstar/plugin-cache), so nothing outlives the process. Swap `createInMemoryCache` for `createRedisCache` in `src/lib/cache.*` to persist them.
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{#if sharder}}
|
|
26
|
+
|
|
27
|
+
Gateway shards are spread across `SHARDER_CLUSTERS` cluster workers by [`@wolfstar/plugin-sharder`](https://www.npmjs.com/package/@wolfstar/plugin-sharder): `src/main.*` is the manager and `src/shard.*` runs in every worker.
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{/if}}
|
|
30
|
+
{{#if vite}}
|
|
31
|
+
|
|
32
|
+
The project is bundled with [Vite](https://vite.dev) into a single file, so the commands are loaded explicitly in `src/main.*`: import every new command there and load it with `container.stores.loadPiece`.
|
|
33
|
+
{{/if}}
|
|
34
|
+
{{#if nitro}}
|
|
35
|
+
|
|
36
|
+
[Nitro](https://nitro.build) serves the bot: `npm run build` writes a deployable `.output/` and `PORT` in `.env` sets the port. `package.json`'s `main` and `start` point at the `node-server` preset's output, `.output/server/index.mjs`, so `npm start` only works on that preset. Targeting another platform means changing `nitro.preset` in `stars.config.ts` and deploying the `.output/` that preset writes, not running it with `npm start`.{{#if gateway}} The gateway connection is held open by the process, so a preset that keeps a server alive is required — the edge presets do not qualify.{{/if}}
|
|
37
|
+
{{/if}}
|
|
13
38
|
|
|
14
39
|
## Scripts
|
|
15
40
|
|
|
16
41
|
The `dev` and `build` scripts are powered by the [`stars`](https://www.npmjs.com/package/@wolfstar/cli) CLI and read the `stars.config` file at the project root.
|
|
17
42
|
|
|
18
43
|
- `npm run dev` — build, run and restart the bot on changes, with an interactive terminal UI (`npm run dev -- --no-tui` for plain logs).
|
|
19
|
-
- `npm run build` — compile the project (TypeScript projects only).
|
|
44
|
+
- `npm run build` — {{#if nitro}}build the deployable `.output/` directory{{else}}compile the project{{/if}} (TypeScript projects only).
|
|
20
45
|
- `npm start` — run the bot.
|
|
21
46
|
- `npx stars info` — print the resolved configuration.
|
|
22
47
|
{{#if testing}}
|
|
@@ -33,4 +58,8 @@ The `dev` and `build` scripts are powered by the [`stars`](https://www.npmjs.com
|
|
|
33
58
|
- `/settings profile get` / `/settings profile set` / `/settings notifications enable` / `/settings notifications disable` — subcommand groups example.
|
|
34
59
|
{{/if}}
|
|
35
60
|
|
|
61
|
+
{{#if nitro}}
|
|
62
|
+
The HTTP server listens on `PORT` from `.env` — `{{port}}` by default.
|
|
63
|
+
{{else}}
|
|
36
64
|
The HTTP server listens on `HTTP_ADDRESS`:`HTTP_PORT` from `.env` — `{{port}}` by default.
|
|
65
|
+
{{/if}}
|
|
@@ -9,5 +9,13 @@ declare module '@wolfstar/env-utilities' {
|
|
|
9
9
|
HTTP_ADDRESS: string;
|
|
10
10
|
HTTP_PORT: IntegerString;
|
|
11
11
|
HTTP_POST_PATH: string;
|
|
12
|
+
{{#if redis}}
|
|
13
|
+
|
|
14
|
+
REDIS_URL: string;
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{#if sharder}}
|
|
17
|
+
|
|
18
|
+
SHARDER_CLUSTERS: IntegerString;
|
|
19
|
+
{{/if}}
|
|
12
20
|
}
|
|
13
21
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
{{#if i18n}}
|
|
2
|
+
import '@wolfstar/plugin-i18next/register';
|
|
3
|
+
{{/if}}
|
|
1
4
|
import { setup } from './lib/setup/all.js';
|
|
2
5
|
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
3
6
|
import { Client, container } from '@wolfstar/http-framework';
|
|
@@ -6,7 +9,12 @@ import { morning } from 'gradient-string';
|
|
|
6
9
|
|
|
7
10
|
await setup();
|
|
8
11
|
|
|
9
|
-
const client = new Client(
|
|
12
|
+
const client = new Client({{#if i18n}}{
|
|
13
|
+
i18n: {
|
|
14
|
+
defaultLanguageDirectory: new URL('locales', import.meta.url).pathname,
|
|
15
|
+
defaultName: 'en-US'
|
|
16
|
+
}
|
|
17
|
+
}{{/if}});
|
|
10
18
|
await client.load();
|
|
11
19
|
|
|
12
20
|
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
@@ -1,13 +1,59 @@
|
|
|
1
|
+
{{#if nitro}}
|
|
2
|
+
{{#if i18n}}
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
{{/if}}
|
|
5
|
+
{{/if}}
|
|
6
|
+
{{#if i18n}}
|
|
7
|
+
import '@wolfstar/plugin-i18next/register';
|
|
8
|
+
{{/if}}
|
|
1
9
|
import { setup } from './lib/setup/all.js';
|
|
10
|
+
{{#unless nitro}}
|
|
2
11
|
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
12
|
+
{{/unless}}
|
|
3
13
|
import { Client, container } from '@wolfstar/http-framework';
|
|
14
|
+
{{#unless nitro}}
|
|
4
15
|
import { createStarsBanner } from '@wolfstar/start-banner';
|
|
5
16
|
import { morning } from 'gradient-string';
|
|
17
|
+
{{/unless}}
|
|
18
|
+
{{#if vite}}
|
|
19
|
+
{{#each commands}}
|
|
20
|
+
import { {{className}} } from './commands/{{file}}.js';
|
|
21
|
+
{{/each}}
|
|
22
|
+
{{/if}}
|
|
6
23
|
|
|
7
24
|
await setup();
|
|
25
|
+
{{#if nitro}}
|
|
26
|
+
{{#if i18n}}
|
|
8
27
|
|
|
9
|
-
|
|
28
|
+
// Nitro bundles this file into `.output/server`, next to the `.output/locales` copy `stars build` makes; in development
|
|
29
|
+
// it runs from `src`, where the locales sit beside it.
|
|
30
|
+
const bundledLocales = new URL('../locales', import.meta.url);
|
|
31
|
+
const localesDirectory = existsSync(bundledLocales) ? bundledLocales.pathname : new URL('locales', import.meta.url).pathname;
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{/if}}
|
|
34
|
+
|
|
35
|
+
const client = new Client({{#if i18n}}{
|
|
36
|
+
i18n: {
|
|
37
|
+
defaultLanguageDirectory: {{#if nitro}}localesDirectory{{else}}new URL('locales', import.meta.url).pathname{{/if}},
|
|
38
|
+
defaultName: 'en-US'
|
|
39
|
+
}
|
|
40
|
+
}{{/if}});
|
|
41
|
+
{{#if vite}}
|
|
42
|
+
|
|
43
|
+
// The bundle is a single file, so there is no `commands` directory next to it to scan: load the pieces explicitly.
|
|
44
|
+
{{#each commands}}
|
|
45
|
+
await container.stores.loadPiece({ name: '{{file}}', piece: {{className}}, store: 'commands' });
|
|
46
|
+
{{/each}}
|
|
47
|
+
await client.load({ baseUserDirectory: null });
|
|
48
|
+
{{else}}
|
|
10
49
|
await client.load();
|
|
50
|
+
{{/if}}
|
|
51
|
+
{{#if nitro}}
|
|
52
|
+
|
|
53
|
+
// No `listen()`: the server entry Nitro generates imports this default export and forwards every request to
|
|
54
|
+
// `client.fetch(request)`, which verifies the signature and answers the interaction. Nitro serves on `PORT`.
|
|
55
|
+
export default client;
|
|
56
|
+
{{else}}
|
|
11
57
|
|
|
12
58
|
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
13
59
|
const port = envParseInteger('HTTP_PORT', {{port}});
|
|
@@ -28,3 +74,4 @@ console.log(
|
|
|
28
74
|
);
|
|
29
75
|
|
|
30
76
|
container.logger.info('Ready');
|
|
77
|
+
{{/if}}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{#if redis}}
|
|
2
|
+
import { envParseString } from '@wolfstar/env-utilities';
|
|
3
|
+
{{/if}}
|
|
4
|
+
import { {{#if redis}}createRedisCache{{else}}createInMemoryCache{{/if}} } from '@wolfstar/plugin-cache';
|
|
5
|
+
{{#if redis}}
|
|
6
|
+
import { Redis } from 'ioredis';
|
|
7
|
+
{{/if}}
|
|
8
|
+
|
|
9
|
+
{{#if redis}}
|
|
10
|
+
/** Caches the gateway's entities in the Redis server at `REDIS_URL`, see `compose.yaml` to start one locally. */
|
|
11
|
+
export function createCache() {
|
|
12
|
+
return createRedisCache({
|
|
13
|
+
redis: new Redis(envParseString('REDIS_URL')),
|
|
14
|
+
// Entries live at `<prefix>:<entity>:<key>`, so several bots can share one database.
|
|
15
|
+
prefix: '{{name}}'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
{{else}}
|
|
19
|
+
/**
|
|
20
|
+
* Keeps the gateway's entities in memory: nothing outlives the process. Swapping the store never changes the call sites,
|
|
21
|
+
* the gateway client and its managers only see the `Cache` interface (`createRedisCache` is the persistent one).
|
|
22
|
+
*/
|
|
23
|
+
export function createCache() {
|
|
24
|
+
// Keep the latest 1,000 messages, every other entity is unbounded.
|
|
25
|
+
return createInMemoryCache({ maxSize: { messages: 1_000 } });
|
|
26
|
+
}
|
|
27
|
+
{{/if}}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{#if redis}}
|
|
2
|
+
import { envParseString } from '@wolfstar/env-utilities';
|
|
3
|
+
{{/if}}
|
|
4
|
+
import { {{#if redis}}createRedisCache{{else}}createInMemoryCache{{/if}}, type Cache } from '@wolfstar/plugin-cache';
|
|
5
|
+
{{#if redis}}
|
|
6
|
+
import { Redis } from 'ioredis';
|
|
7
|
+
{{/if}}
|
|
8
|
+
|
|
9
|
+
{{#if redis}}
|
|
10
|
+
/** Caches the gateway's entities in the Redis server at `REDIS_URL`, see `compose.yaml` to start one locally. */
|
|
11
|
+
export function createCache(): Cache {
|
|
12
|
+
return createRedisCache({
|
|
13
|
+
redis: new Redis(envParseString('REDIS_URL')),
|
|
14
|
+
// Entries live at `<prefix>:<entity>:<key>`, so several bots can share one database.
|
|
15
|
+
prefix: '{{name}}'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
{{else}}
|
|
19
|
+
/**
|
|
20
|
+
* Keeps the gateway's entities in memory: nothing outlives the process. Swapping the store never changes the call sites,
|
|
21
|
+
* the gateway client and its managers only see the `Cache` interface (`createRedisCache` is the persistent one).
|
|
22
|
+
*/
|
|
23
|
+
export function createCache(): Cache {
|
|
24
|
+
// Keep the latest 1,000 messages, every other entity is unbounded.
|
|
25
|
+
return createInMemoryCache({ maxSize: { messages: 1_000 } });
|
|
26
|
+
}
|
|
27
|
+
{{/if}}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{{#if i18n}}
|
|
2
|
+
import '@wolfstar/plugin-i18next/register';
|
|
3
|
+
{{/if}}
|
|
4
|
+
import { setup } from './lib/setup/all.js';
|
|
5
|
+
{{#if cache}}
|
|
6
|
+
import { createCache } from './lib/cache.js';
|
|
7
|
+
{{/if}}
|
|
8
|
+
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
9
|
+
import { container } from '@wolfstar/http-framework';
|
|
10
|
+
import { GatewayClient } from '@wolfstar/plugin-gateway';
|
|
11
|
+
import { createStarsBanner } from '@wolfstar/start-banner';
|
|
12
|
+
import { GatewayIntentBits } from 'discord-api-types/v10';
|
|
13
|
+
import { morning } from 'gradient-string';
|
|
14
|
+
|
|
15
|
+
await setup();
|
|
16
|
+
|
|
17
|
+
const client = new GatewayClient({
|
|
18
|
+
{{#if i18n}}
|
|
19
|
+
i18n: {
|
|
20
|
+
defaultLanguageDirectory: new URL('locales', import.meta.url).pathname,
|
|
21
|
+
defaultName: 'en-US'
|
|
22
|
+
},
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if cache}}
|
|
25
|
+
// Every dispatch is written into the cache before its event is emitted.
|
|
26
|
+
cache: createCache(),
|
|
27
|
+
{{/if}}
|
|
28
|
+
// Add the intents your listeners need, privileged ones (`MessageContent`, `GuildMembers`, `GuildPresences`) must also
|
|
29
|
+
// be enabled under Bot → Privileged Gateway Intents in the Developer Portal.
|
|
30
|
+
intents: GatewayIntentBits.Guilds
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
client.on('error', (error) => container.logger.error(error));
|
|
34
|
+
|
|
35
|
+
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
36
|
+
const port = envParseInteger('HTTP_PORT', {{port}});
|
|
37
|
+
// Loads the pieces, starts the HTTP interactions endpoint, then connects every gateway shard.
|
|
38
|
+
await client.start({ listen: { address, port } });
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
morning.multiline(
|
|
42
|
+
createStarsBanner({
|
|
43
|
+
name: ['{{name}}'],
|
|
44
|
+
extra: [
|
|
45
|
+
'',
|
|
46
|
+
`Loaded: ${container.stores.get('commands').size} commands`,
|
|
47
|
+
` : ${container.stores.get('interaction-handlers').size} interaction handlers`,
|
|
48
|
+
`Listening: ${address}:${port}`
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
container.logger.info('Ready');
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{{#if nitro}}
|
|
2
|
+
{{#if i18n}}
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
{{/if}}
|
|
5
|
+
{{/if}}
|
|
6
|
+
{{#if i18n}}
|
|
7
|
+
import '@wolfstar/plugin-i18next/register';
|
|
8
|
+
{{/if}}
|
|
9
|
+
import { setup } from './lib/setup/all.js';
|
|
10
|
+
{{#if cache}}
|
|
11
|
+
import { createCache } from './lib/cache.js';
|
|
12
|
+
{{/if}}
|
|
13
|
+
{{#unless nitro}}
|
|
14
|
+
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
15
|
+
{{/unless}}
|
|
16
|
+
import { container } from '@wolfstar/http-framework';
|
|
17
|
+
import { GatewayClient } from '@wolfstar/plugin-gateway';
|
|
18
|
+
{{#unless nitro}}
|
|
19
|
+
import { createStarsBanner } from '@wolfstar/start-banner';
|
|
20
|
+
{{/unless}}
|
|
21
|
+
import { GatewayIntentBits } from 'discord-api-types/v10';
|
|
22
|
+
{{#unless nitro}}
|
|
23
|
+
import { morning } from 'gradient-string';
|
|
24
|
+
{{/unless}}
|
|
25
|
+
{{#if vite}}
|
|
26
|
+
{{#each commands}}
|
|
27
|
+
import { {{className}} } from './commands/{{file}}.js';
|
|
28
|
+
{{/each}}
|
|
29
|
+
{{/if}}
|
|
30
|
+
|
|
31
|
+
await setup();
|
|
32
|
+
{{#if nitro}}
|
|
33
|
+
{{#if i18n}}
|
|
34
|
+
|
|
35
|
+
// Nitro bundles this file into `.output/server`, next to the `.output/locales` copy `stars build` makes; in development
|
|
36
|
+
// it runs from `src`, where the locales sit beside it.
|
|
37
|
+
const bundledLocales = new URL('../locales', import.meta.url);
|
|
38
|
+
const localesDirectory = existsSync(bundledLocales) ? bundledLocales.pathname : new URL('locales', import.meta.url).pathname;
|
|
39
|
+
{{/if}}
|
|
40
|
+
{{/if}}
|
|
41
|
+
|
|
42
|
+
const client = new GatewayClient({
|
|
43
|
+
{{#if i18n}}
|
|
44
|
+
i18n: {
|
|
45
|
+
defaultLanguageDirectory: {{#if nitro}}localesDirectory{{else}}new URL('locales', import.meta.url).pathname{{/if}},
|
|
46
|
+
defaultName: 'en-US'
|
|
47
|
+
},
|
|
48
|
+
{{/if}}
|
|
49
|
+
{{#if cache}}
|
|
50
|
+
// Every dispatch is written into the cache before its event is emitted.
|
|
51
|
+
cache: createCache(),
|
|
52
|
+
{{/if}}
|
|
53
|
+
// Add the intents your listeners need, privileged ones (`MessageContent`, `GuildMembers`, `GuildPresences`) must also
|
|
54
|
+
// be enabled under Bot → Privileged Gateway Intents in the Developer Portal.
|
|
55
|
+
intents: GatewayIntentBits.Guilds
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
client.on('error', (error) => container.logger.error(error));
|
|
59
|
+
{{#if vite}}
|
|
60
|
+
|
|
61
|
+
// The bundle is a single file, so there is no `commands` directory next to it to scan: load the pieces explicitly.
|
|
62
|
+
{{#each commands}}
|
|
63
|
+
await container.stores.loadPiece({ name: '{{file}}', piece: {{className}}, store: 'commands' });
|
|
64
|
+
{{/each}}
|
|
65
|
+
{{/if}}
|
|
66
|
+
{{#if nitro}}
|
|
67
|
+
|
|
68
|
+
await client.load({ baseUserDirectory: null });
|
|
69
|
+
// Nitro answers the HTTP interactions (see the default export), the gateway connection stays with this process: run it
|
|
70
|
+
// on a preset that keeps a server alive, such as `node-server`.
|
|
71
|
+
await client.connect();
|
|
72
|
+
|
|
73
|
+
export default client;
|
|
74
|
+
{{else}}
|
|
75
|
+
|
|
76
|
+
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
77
|
+
const port = envParseInteger('HTTP_PORT', {{port}});
|
|
78
|
+
// Loads the pieces, starts the HTTP interactions endpoint, then connects every gateway shard.
|
|
79
|
+
await client.start({ listen: { address, port }{{#if vite}}, load: { baseUserDirectory: null }{{/if}} });
|
|
80
|
+
|
|
81
|
+
console.log(
|
|
82
|
+
morning.multiline(
|
|
83
|
+
createStarsBanner({
|
|
84
|
+
name: ['{{name}}'],
|
|
85
|
+
extra: [
|
|
86
|
+
'',
|
|
87
|
+
`Loaded: ${container.stores.get('commands').size} commands`,
|
|
88
|
+
` : ${container.stores.get('interaction-handlers').size} interaction handlers`,
|
|
89
|
+
`Listening: ${address}:${port}`
|
|
90
|
+
]
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
container.logger.info('Ready');
|
|
96
|
+
{{/if}}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { setup } from './lib/setup/all.js';
|
|
2
|
+
import { envParseInteger } from '@wolfstar/env-utilities';
|
|
3
|
+
import { ShardClient, ShardManager } from '@wolfstar/plugin-sharder';
|
|
4
|
+
|
|
5
|
+
await setup();
|
|
6
|
+
|
|
7
|
+
// The manager and the shards share this script: `ShardClient.context` is only set in the processes the manager
|
|
8
|
+
// spawned.
|
|
9
|
+
if (ShardClient.context === null) {
|
|
10
|
+
// Discord's recommended gateway shard count (fetched with `DISCORD_TOKEN`), split across `SHARDER_CLUSTERS`
|
|
11
|
+
// `node:cluster` workers. Cluster workers share the HTTP port, so Node balances the interactions across them.
|
|
12
|
+
const manager = new ShardManager({
|
|
13
|
+
strategy: 'cluster',
|
|
14
|
+
clusters: envParseInteger('SHARDER_CLUSTERS', 2),
|
|
15
|
+
// Identifies are paced by the manager across every worker, see `identifyThrottler` in `src/shard.ts`.
|
|
16
|
+
spawn: { delay: 0 }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
manager.on('shardReady', (channel) => console.log(`Worker ${channel.id} is ready (gateway shards ${channel.shards.join(', ')})`));
|
|
20
|
+
manager.on('shardGiveUp', (channel) => console.error(`Worker ${channel.id} keeps crashing, giving up on it`));
|
|
21
|
+
|
|
22
|
+
await manager.spawn();
|
|
23
|
+
} else {
|
|
24
|
+
await import('./shard.js');
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { setup } from './lib/setup/all.js';
|
|
2
|
+
import { envParseInteger } from '@wolfstar/env-utilities';
|
|
3
|
+
import { ShardClient, ShardManager } from '@wolfstar/plugin-sharder';
|
|
4
|
+
|
|
5
|
+
await setup();
|
|
6
|
+
|
|
7
|
+
// The manager and the shards share this script: `ShardClient.context` is only set in the processes the manager
|
|
8
|
+
// spawned.
|
|
9
|
+
if (ShardClient.context === null) {
|
|
10
|
+
// Discord's recommended gateway shard count (fetched with `DISCORD_TOKEN`), split across `SHARDER_CLUSTERS`
|
|
11
|
+
// `node:cluster` workers. Cluster workers share the HTTP port, so Node balances the interactions across them.
|
|
12
|
+
const manager = new ShardManager({
|
|
13
|
+
strategy: 'cluster',
|
|
14
|
+
clusters: envParseInteger('SHARDER_CLUSTERS', 2),
|
|
15
|
+
// Identifies are paced by the manager across every worker, see `identifyThrottler` in `src/shard.ts`.
|
|
16
|
+
spawn: { delay: 0 }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
manager.on('shardReady', (channel) => console.log(`Worker ${channel.id} is ready (gateway shards ${channel.shards.join(', ')})`));
|
|
20
|
+
manager.on('shardGiveUp', (channel) => console.error(`Worker ${channel.id} keeps crashing, giving up on it`));
|
|
21
|
+
|
|
22
|
+
await manager.spawn();
|
|
23
|
+
} else {
|
|
24
|
+
await import('./shard.js');
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{{#if i18n}}
|
|
2
|
+
import '@wolfstar/plugin-i18next/register';
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#if cache}}
|
|
5
|
+
import { createCache } from './lib/cache.js';
|
|
6
|
+
{{/if}}
|
|
7
|
+
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
8
|
+
import { container } from '@wolfstar/http-framework';
|
|
9
|
+
import { GatewayClient } from '@wolfstar/plugin-gateway';
|
|
10
|
+
import { ShardClient } from '@wolfstar/plugin-sharder';
|
|
11
|
+
import { GatewayIntentBits } from 'discord-api-types/v10';
|
|
12
|
+
|
|
13
|
+
const shard = new ShardClient();
|
|
14
|
+
|
|
15
|
+
const client = new GatewayClient({
|
|
16
|
+
{{#if i18n}}
|
|
17
|
+
i18n: {
|
|
18
|
+
defaultLanguageDirectory: new URL('locales', import.meta.url).pathname,
|
|
19
|
+
defaultName: 'en-US'
|
|
20
|
+
},
|
|
21
|
+
{{/if}}
|
|
22
|
+
{{#if cache}}
|
|
23
|
+
cache: createCache(),
|
|
24
|
+
{{/if}}
|
|
25
|
+
// The gateway shards the manager assigned to this worker, and the total across every worker.
|
|
26
|
+
...shard.gatewayOptions,
|
|
27
|
+
// Identifies go through the manager, which grants one per `max_concurrency` bucket across every worker.
|
|
28
|
+
gateway: { buildIdentifyThrottler: () => shard.identifyThrottler },
|
|
29
|
+
intents: GatewayIntentBits.Guilds
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Reuse the manager's `GET /gateway/bot` rather than requesting it again from every worker.
|
|
33
|
+
client.gateway.fetchGatewayInformation = () => shard.fetchGatewayInformation();
|
|
34
|
+
client.on('error', (error) => container.logger.error(error));
|
|
35
|
+
|
|
36
|
+
shard.setCloseHandler(() => client.destroy());
|
|
37
|
+
|
|
38
|
+
// The worker is ready once every gateway shard it runs is. The manager only starts the next worker once this one
|
|
39
|
+
// reports in, so a worker it assigned no gateway shard to (a layout can hand every shard to one channel of the
|
|
40
|
+
// cluster) reports in right away instead of waiting for a `shardReady` that never fires.
|
|
41
|
+
let pending = shard.shards.length;
|
|
42
|
+
if (pending === 0) void shard.ready();
|
|
43
|
+
client.on('shardReady', () => {
|
|
44
|
+
if (--pending === 0) void shard.ready();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
48
|
+
const port = envParseInteger('HTTP_PORT', {{port}});
|
|
49
|
+
await client.start({ listen: { address, port } });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{{#if i18n}}
|
|
2
|
+
import '@wolfstar/plugin-i18next/register';
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#if cache}}
|
|
5
|
+
import { createCache } from './lib/cache.js';
|
|
6
|
+
{{/if}}
|
|
7
|
+
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
8
|
+
import { container } from '@wolfstar/http-framework';
|
|
9
|
+
import { GatewayClient } from '@wolfstar/plugin-gateway';
|
|
10
|
+
import { ShardClient } from '@wolfstar/plugin-sharder';
|
|
11
|
+
import { GatewayIntentBits } from 'discord-api-types/v10';
|
|
12
|
+
{{#if vite}}
|
|
13
|
+
{{#each commands}}
|
|
14
|
+
import { {{className}} } from './commands/{{file}}.js';
|
|
15
|
+
{{/each}}
|
|
16
|
+
{{/if}}
|
|
17
|
+
|
|
18
|
+
const shard = new ShardClient();
|
|
19
|
+
|
|
20
|
+
const client = new GatewayClient({
|
|
21
|
+
{{#if i18n}}
|
|
22
|
+
i18n: {
|
|
23
|
+
defaultLanguageDirectory: new URL('locales', import.meta.url).pathname,
|
|
24
|
+
defaultName: 'en-US'
|
|
25
|
+
},
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{#if cache}}
|
|
28
|
+
cache: createCache(),
|
|
29
|
+
{{/if}}
|
|
30
|
+
// The gateway shards the manager assigned to this worker, and the total across every worker.
|
|
31
|
+
...shard.gatewayOptions,
|
|
32
|
+
// Identifies go through the manager, which grants one per `max_concurrency` bucket across every worker.
|
|
33
|
+
gateway: { buildIdentifyThrottler: () => shard.identifyThrottler },
|
|
34
|
+
intents: GatewayIntentBits.Guilds
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Reuse the manager's `GET /gateway/bot` rather than requesting it again from every worker.
|
|
38
|
+
client.gateway.fetchGatewayInformation = () => shard.fetchGatewayInformation();
|
|
39
|
+
client.on('error', (error) => container.logger.error(error));
|
|
40
|
+
|
|
41
|
+
shard.setCloseHandler(() => client.destroy());
|
|
42
|
+
|
|
43
|
+
// The worker is ready once every gateway shard it runs is. The manager only starts the next worker once this one
|
|
44
|
+
// reports in, so a worker it assigned no gateway shard to (a layout can hand every shard to one channel of the
|
|
45
|
+
// cluster) reports in right away instead of waiting for a `shardReady` that never fires.
|
|
46
|
+
let pending = shard.shards.length;
|
|
47
|
+
if (pending === 0) void shard.ready();
|
|
48
|
+
client.on('shardReady', () => {
|
|
49
|
+
if (--pending === 0) void shard.ready();
|
|
50
|
+
});
|
|
51
|
+
{{#if vite}}
|
|
52
|
+
|
|
53
|
+
// The bundle is a single file, so there is no `commands` directory next to it to scan: load the pieces explicitly.
|
|
54
|
+
{{#each commands}}
|
|
55
|
+
await container.stores.loadPiece({ name: '{{file}}', piece: {{className}}, store: 'commands' });
|
|
56
|
+
{{/each}}
|
|
57
|
+
{{/if}}
|
|
58
|
+
|
|
59
|
+
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
60
|
+
const port = envParseInteger('HTTP_PORT', {{port}});
|
|
61
|
+
await client.start({ listen: { address, port }{{#if vite}}, load: { baseUserDirectory: null }{{/if}} });
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import '@wolfstar/plugin-i18next/register';
|
|
2
|
-
import { setup } from './lib/setup/all.js';
|
|
3
|
-
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
4
|
-
import { Client, container } from '@wolfstar/http-framework';
|
|
5
|
-
import { createStarsBanner } from '@wolfstar/start-banner';
|
|
6
|
-
import { morning } from 'gradient-string';
|
|
7
|
-
|
|
8
|
-
await setup();
|
|
9
|
-
|
|
10
|
-
const client = new Client({
|
|
11
|
-
i18n: {
|
|
12
|
-
defaultLanguageDirectory: new URL('locales', import.meta.url).pathname,
|
|
13
|
-
defaultName: 'en-US'
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
await client.load();
|
|
17
|
-
|
|
18
|
-
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
19
|
-
const port = envParseInteger('HTTP_PORT', {{port}});
|
|
20
|
-
await client.listen({ address, port });
|
|
21
|
-
|
|
22
|
-
console.log(
|
|
23
|
-
morning.multiline(
|
|
24
|
-
createStarsBanner({
|
|
25
|
-
name: ['{{name}}'],
|
|
26
|
-
extra: [
|
|
27
|
-
'',
|
|
28
|
-
`Loaded: ${container.stores.get('commands').size} commands`,
|
|
29
|
-
` : ${container.stores.get('interaction-handlers').size} interaction handlers`,
|
|
30
|
-
`Listening: ${address}:${port}`
|
|
31
|
-
]
|
|
32
|
-
})
|
|
33
|
-
)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
container.logger.info('Ready');
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import '@wolfstar/plugin-i18next/register';
|
|
2
|
-
import { setup } from './lib/setup/all.js';
|
|
3
|
-
import { envParseInteger, envParseString } from '@wolfstar/env-utilities';
|
|
4
|
-
import { Client, container } from '@wolfstar/http-framework';
|
|
5
|
-
import { createStarsBanner } from '@wolfstar/start-banner';
|
|
6
|
-
import { morning } from 'gradient-string';
|
|
7
|
-
|
|
8
|
-
await setup();
|
|
9
|
-
|
|
10
|
-
const client = new Client({
|
|
11
|
-
i18n: {
|
|
12
|
-
defaultLanguageDirectory: new URL('locales', import.meta.url).pathname,
|
|
13
|
-
defaultName: 'en-US'
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
await client.load();
|
|
17
|
-
|
|
18
|
-
const address = envParseString('HTTP_ADDRESS', '0.0.0.0');
|
|
19
|
-
const port = envParseInteger('HTTP_PORT', {{port}});
|
|
20
|
-
await client.listen({ address, port });
|
|
21
|
-
|
|
22
|
-
console.log(
|
|
23
|
-
morning.multiline(
|
|
24
|
-
createStarsBanner({
|
|
25
|
-
name: ['{{name}}'],
|
|
26
|
-
extra: [
|
|
27
|
-
'',
|
|
28
|
-
`Loaded: ${container.stores.get('commands').size} commands`,
|
|
29
|
-
` : ${container.stores.get('interaction-handlers').size} interaction handlers`,
|
|
30
|
-
`Listening: ${address}:${port}`
|
|
31
|
-
]
|
|
32
|
-
})
|
|
33
|
-
)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
container.logger.info('Ready');
|