meocord 4.0.0-beta.0 → 4.0.0-beta.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # meocord
2
2
 
3
+ ## 4.0.0-beta.2
4
+
5
+ ### Major Changes
6
+
7
+ - [#36](https://github.com/l7aromeo/meocord/pull/36) [`45e564e`](https://github.com/l7aromeo/meocord/commit/45e564e6baff17313037372ec56baa1711a3584c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - **Breaking:** `app.start()` rejects when the login fails. It logged the error and resolved, so a bot
8
+ with an invalid token went on to log "Application started" and exit with code 0, which Docker's
9
+ `restart: on-failure`, systemd and CI all read as success. New applications' `main.ts` sets
10
+ `process.exitCode = 1` when startup fails; add the same to an existing entry point's `catch`. See the
11
+ [migration guide](https://github.com/l7aromeo/meocord/blob/main/docs/MIGRATING.md#4-build-and-start).
12
+
13
+ ### Patch Changes
14
+
15
+ - [#37](https://github.com/l7aromeo/meocord/pull/37) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Type the CommonJS build as CommonJS. Every entry point's `require` condition resolved to the ESM
16
+ declarations, so a CommonJS TypeScript project was told `meocord/core` is an ES module it cannot
17
+ `require`, even with `skipLibCheck`. Each entry now ships `.d.cts` declarations for `require`, and
18
+ `meocord/eslint` types its `module.exports` array as what `require` returns.
19
+
20
+ - [#35](https://github.com/l7aromeo/meocord/pull/35) [`b93a771`](https://github.com/l7aromeo/meocord/commit/b93a771b19664345fec4222d5a2c29dcf5b31a0d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Generate code that passes a new application's own `lint`. A generated guard failed `tsc` and ESLint
21
+ — `GuardInterface` imported as a value, and an unused `context` parameter — and generated message and
22
+ reaction controllers imported names they never used, which only the application's ESLint, run in the
23
+ background after generating, removed. New applications also typecheck `meocord.config.ts`: the
24
+ template's `tsconfig.json` includes it instead of excluding it, so a type error in the config fails
25
+ `lint`, and editors resolve `paths` aliases imported there. `noEmit` stays on, so `tsc` writes nothing
26
+ beside it.
27
+
28
+ ## 4.0.0-beta.1
29
+
30
+ ### Major Changes
31
+
32
+ - [#33](https://github.com/l7aromeo/meocord/pull/33) [`21b06ae`](https://github.com/l7aromeo/meocord/commit/21b06ae6810cfe203abadcadc6f7b7247c7b305b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Require Node.js 22.13 or newer, up from 22.0. A built bot loads its compiled config with `require()`
33
+ of an ES module. Node 22.12 runs that without a flag but still warns on every start and crashes on a
34
+ config that throws; 22.13 is the first 22 release that does neither. Bun is unaffected. See the
35
+ [migration guide](https://github.com/l7aromeo/meocord/blob/main/docs/MIGRATING.md#before-you-start-nodejs-2213).
36
+
37
+ ### Patch Changes
38
+
39
+ - [#33](https://github.com/l7aromeo/meocord/pull/33) [`21b06ae`](https://github.com/l7aromeo/meocord/commit/21b06ae6810cfe203abadcadc6f7b7247c7b305b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Keep jiti out of bots built with `bundleDependencies`. A built bot loaded `dist/meocord.config.mjs`,
40
+ already compiled JavaScript, through jiti, and the logger and the factory both reach that loader,
41
+ so jiti was bundled into every bot: 190 KB, 89% of a minimal bot's `main.js`, and a
42
+ `Critical dependency` warning on every build. The compiled config is now loaded with `require()`,
43
+ and jiti is only used by the CLI to read `meocord.config.ts`. A minimal bot bundles to 22 KB.
44
+
45
+ A built bot no longer falls back to reading `meocord.config.ts` when `dist/meocord.config.mjs` is
46
+ missing, and `meocord build` now fails when the config does not compile, instead of warning and
47
+ producing a bot that cannot start.
48
+
49
+ - [#31](https://github.com/l7aromeo/meocord/pull/31) [`f50bafc`](https://github.com/l7aromeo/meocord/commit/f50bafce5bcf04b555a7638fedeb6986cf7ea9d5) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Resolve asset imports to files under `dist` in development builds too. `meocord start --dev` gave
50
+ `import logo from './logo.png'` the path `/assets/logo.png`, at the root of the filesystem, so a bot
51
+ reading an imported font or image failed in development while production worked.
52
+
53
+ - [#32](https://github.com/l7aromeo/meocord/pull/32) [`72b228c`](https://github.com/l7aromeo/meocord/commit/72b228ceaca2dca33ed660aff0ec1bc2aa59bfc2) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Pack only the build platform's native binaries with `bundleDependencies`. Every installed platform
54
+ package was copied into `dist/node_modules`, and bun installs both the glibc and the musl build on
55
+ Linux, so a glibc build of a bot using sharp carried about 19 MB of musl binaries it could never
56
+ load. A package whose `os`, `cpu` or `libc` does not match the platform building is now left out,
57
+ whichever package manager installed it.
58
+
3
59
  ## 4.0.0-beta.0
4
60
 
5
61
  ### Major Changes
package/README.md CHANGED
@@ -53,7 +53,7 @@
53
53
 
54
54
  ### Prerequisites
55
55
 
56
- - **Runtime**: Node.js 22 or newer, or Bun 1.x+
56
+ - **Runtime**: Node.js 22.13 or newer, or Bun 1.x+
57
57
  - **TypeScript**: 5.0+ with `skipLibCheck` enabled, as generated apps have it; 5.8+ with it off
58
58
  - **Package manager**: npm, yarn, pnpm, or bun
59
59
  - **Peer dependencies**: `discord.js` 14 and `dotenv` 18 — `meocord create` installs both
@@ -1055,7 +1055,7 @@ dist/
1055
1055
  └── meocord.platform.json (if there are native addons)
1056
1056
  ```
1057
1057
 
1058
- Plain JavaScript dependencies are bundled into `main.js`. **Native addons** — packages that ship a compiled `.node` binary, like `sharp`, canvas bindings or database drivers — cannot be inlined into JavaScript, so MeoCord finds them itself while building, keeps them out of the bundle, and copies each one, with its platform binary and what it needs at runtime, into `dist/node_modules`. There is nothing to list: the build tells you which it packed.
1058
+ Plain JavaScript dependencies are bundled into `main.js`. **Native addons** — packages that ship a compiled `.node` binary, like `sharp`, canvas bindings or database drivers — cannot be inlined into JavaScript, so MeoCord finds them itself while building, keeps them out of the bundle, and copies each one, with its platform binary and what it needs at runtime, into `dist/node_modules`. There is nothing to list: the build tells you which it packed. Only binaries for the platform building are copied, going by the `os`, `cpu` and `libc` each platform package declares — so a glibc build carries no musl binaries even where the package manager installed both, as bun does.
1059
1059
 
1060
1060
  ```
1061
1061
  Native addons packed into dist: meo-canvas, sharp
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ var node_util = require('node:util');
4
+ var dayjs = require('dayjs');
5
+ var utc = require('dayjs/plugin/utc.js');
6
+ var timezone = require('dayjs/plugin/timezone.js');
7
+ var path = require('path');
8
+ var fs = require('fs');
9
+ var module$1 = require('module');
10
+ var chalk = require('chalk');
11
+
12
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
13
+ let cachedConfig;
14
+ let configLoaded = false;
15
+ /**
16
+ * Loads the configuration a built application runs with: `dist/meocord.config.mjs`, which
17
+ * `meocord build` compiles from `meocord.config.ts`.
18
+ *
19
+ * Reads only the compiled file, which is plain JavaScript and needs no transpiler, tsconfig or
20
+ * source. Reading `meocord.config.ts` is {@link loadMeoCordSourceConfig}'s job, and it lives in its
21
+ * own module because it needs jiti: this one is imported by the logger and the factory, so a
22
+ * bot built with `bundleDependencies` bundles whatever it imports.
23
+ *
24
+ * The result is cached after the first load.
25
+ *
26
+ * @returns The configuration, or undefined when there is no compiled config or it fails to load.
27
+ */ function loadMeoCordConfig() {
28
+ if (configLoaded) return cachedConfig;
29
+ configLoaded = true;
30
+ cachedConfig = loadCompiledConfig();
31
+ return cachedConfig;
32
+ }
33
+ /**
34
+ * `require` of an ES module, which Node supports cleanly from 22.13 and bun always has.
35
+ * Synchronous, like the logger and the factory that call it.
36
+ */ function loadCompiledConfig() {
37
+ const compiledPath = path.resolve(process.cwd(), 'dist', 'meocord.config.mjs');
38
+ if (!fs.existsSync(compiledPath)) return undefined;
39
+ try {
40
+ // Called through a variable so a bundler does not try to resolve the path while building the
41
+ // application; the file is only there once the build is done.
42
+ const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/theme-Dqwlnz0m.cjs', document.baseURI).href)));
43
+ const loaded = load(compiledPath);
44
+ return loaded.default ?? loaded;
45
+ } catch (error) {
46
+ console.error(`[MeoCord] Failed to load dist/meocord.config.mjs: ${error instanceof Error ? error.message : error}`);
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ dayjs.extend(utc);
52
+ dayjs.extend(timezone);
53
+ class Logger {
54
+ log(...args) {
55
+ this.logWithContext('log', args);
56
+ }
57
+ info(...args) {
58
+ this.logWithContext('log', args);
59
+ }
60
+ warn(...args) {
61
+ this.logWithContext('warn', args);
62
+ }
63
+ error(...args) {
64
+ this.logWithContext('error', args);
65
+ }
66
+ debug(...args) {
67
+ this.logWithContext('debug', args);
68
+ }
69
+ verbose(...args) {
70
+ this.logWithContext('log', args);
71
+ }
72
+ formatMessage(message, logType) {
73
+ if (typeof message === 'object' && message !== null) {
74
+ return node_util.inspect(message, {
75
+ showHidden: true,
76
+ depth: null,
77
+ colors: true,
78
+ compact: false,
79
+ showProxy: true
80
+ });
81
+ }
82
+ return (this.colorMap[logType] || ((msg)=>msg))(message);
83
+ }
84
+ logWithContext(logLevel, messages) {
85
+ if (messages.length === 0) return;
86
+ const config = loadMeoCordConfig();
87
+ const logType = logLevel.toUpperCase();
88
+ const applyColor = this.colorMap[logType] || ((msg)=>msg);
89
+ const formattedMessages = messages.map((message)=>this.formatMessage(message, logType));
90
+ const coloredAppName = config?.appName ? applyColor(chalk.bold(`[${config.appName}]`)) : undefined;
91
+ const timestamp = chalk.bold(dayjs().format('dddd, MMMM D, YYYY HH:mm:ss [UTC]Z'));
92
+ const coloredLogLevel = applyColor(chalk.bold(`[${logType}]`));
93
+ const coloredContext = this.context ? chalk.yellow.bold(`[${this.context}]`) : '';
94
+ const logTexts = [
95
+ coloredAppName,
96
+ timestamp,
97
+ coloredLogLevel,
98
+ coloredContext,
99
+ ...formattedMessages
100
+ ].filter((log)=>!!log);
101
+ console[logLevel](...logTexts);
102
+ }
103
+ constructor(context){
104
+ this.context = context;
105
+ this.colorMap = {
106
+ LOG: chalk.green,
107
+ INFO: chalk.cyan,
108
+ WARN: chalk.yellow,
109
+ ERROR: chalk.red,
110
+ DEBUG: chalk.magenta
111
+ };
112
+ }
113
+ }
114
+
115
+ class Theme {
116
+ }
117
+ Theme.successColor = '#28A745';
118
+ Theme.infoColor = '#17A2B8';
119
+ Theme.errorColor = '#DC3545';
120
+ Theme.warningColor = '#FFC107';
121
+
122
+ exports.Logger = Logger;
123
+ exports.Theme = Theme;
124
+ exports.loadMeoCordConfig = loadMeoCordConfig;
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var theme = require('../_shared/theme-BXdSoifk.cjs');
3
+ var theme = require('../_shared/theme-Dqwlnz0m.cjs');
4
4
  require('node:util');
5
5
  require('dayjs');
6
6
  require('dayjs/plugin/utc.js');
7
7
  require('dayjs/plugin/timezone.js');
8
8
  require('path');
9
9
  require('fs');
10
- require('jiti');
10
+ require('module');
11
11
  require('chalk');
12
12
 
13
13
  /**
@@ -3,7 +3,7 @@
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
5
  var discord_js = require('discord.js');
6
- var theme = require('../_shared/theme-BXdSoifk.cjs');
6
+ var theme = require('../_shared/theme-Dqwlnz0m.cjs');
7
7
  var controller_decorator = require('../_shared/controller.decorator-9nIxOp9y.cjs');
8
8
  var lodashEs = require('lodash-es');
9
9
  var enum_index = require('../enum/index.cjs');
@@ -15,7 +15,7 @@ require('node:util');
15
15
  require('dayjs');
16
16
  require('dayjs/plugin/utc.js');
17
17
  require('dayjs/plugin/timezone.js');
18
- require('jiti');
18
+ require('module');
19
19
  require('chalk');
20
20
 
21
21
  const createErrorEmbed = (description)=>{
@@ -106,28 +106,30 @@ class MeoCordApp {
106
106
  }
107
107
  return this.controllerInstancesCache.get(controllerClass);
108
108
  }
109
- async start() {
110
- try {
111
- this.logger.log('Starting bot...');
112
- this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
113
- this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
114
- await this.registerCommands();
115
- }));
116
- this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
117
- this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
118
- this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
119
- user,
120
- action: enum_index.ReactionHandlerAction.ADD
121
- })));
122
- this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
123
- user,
124
- action: enum_index.ReactionHandlerAction.REMOVE
125
- })));
126
- await this.bot.login(this.discordToken);
127
- this.logger.log('Bot is online!');
128
- } catch (error) {
129
- this.logger.error('Error during bot startup:', error);
130
- }
109
+ /**
110
+ * Registers the Discord event handlers and logs in.
111
+ *
112
+ * Rejects when the login fails -- an invalid token, or Discord unreachable -- so the caller can
113
+ * stop with a non-zero exit code. A bot that never came online is a failed start, and a
114
+ * supervisor such as Docker's `restart: on-failure` or systemd can only tell if the process says so.
115
+ */ async start() {
116
+ this.logger.log('Starting bot...');
117
+ this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
118
+ this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
119
+ await this.registerCommands();
120
+ }));
121
+ this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
122
+ this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
123
+ this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
124
+ user,
125
+ action: enum_index.ReactionHandlerAction.ADD
126
+ })));
127
+ this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
128
+ user,
129
+ action: enum_index.ReactionHandlerAction.REMOVE
130
+ })));
131
+ await this.bot.login(this.discordToken);
132
+ this.logger.log('Bot is online!');
131
133
  }
132
134
  async registerCommands() {
133
135
  // Keyed by type and name together, because that pair is what Discord treats as one
@@ -593,7 +595,7 @@ class MeoCordFactory {
593
595
  }
594
596
  const meocordConfig = theme.loadMeoCordConfig();
595
597
  if (!meocordConfig) {
596
- throw new Error('MeoCord config not found. Ensure meocord.config.ts exists.');
598
+ throw new Error('MeoCord config not found: dist/meocord.config.mjs is missing or failed to load. Run `meocord build`.');
597
599
  }
598
600
  // Before anything is resolved: a controller or service is what first loads a native addon, and
599
601
  // one built for another platform would otherwise fail there with a linker error.
@@ -11,4 +11,8 @@ async function bootstrap() {
11
11
  logger.log('Application started')
12
12
  }
13
13
 
14
- bootstrap().catch(error => logger.error('Error during startup:', error))
14
+ bootstrap().catch(error => {
15
+ logger.error('Error during startup:', error)
16
+ // A bot that never came online is a failed start: exit non-zero, so Docker, systemd or CI see it.
17
+ process.exitCode = 1
18
+ })
@@ -25,6 +25,6 @@
25
25
  },
26
26
  "types": ["node"]
27
27
  },
28
- "include": ["src/**/*.ts"],
29
- "exclude": ["meocord.config.ts", "dist", "vitest.config.ts", "node_modules", "src/**/*.spec.ts"]
28
+ "include": ["src/**/*.ts", "meocord.config.ts"],
29
+ "exclude": ["dist", "vitest.config.ts", "node_modules", "src/**/*.spec.ts"]
30
30
  }
@@ -1,5 +1,5 @@
1
- import { MessageHandler, Controller, Command } from 'meocord/decorator'
2
- import { EmbedBuilder, Message } from 'discord.js'
1
+ import { MessageHandler, Controller } from 'meocord/decorator'
2
+ import { Message } from 'discord.js'
3
3
  import { Logger } from 'meocord/common'
4
4
 
5
5
  @Controller()
@@ -1,7 +1,7 @@
1
- import { MessageReaction, User } from 'discord.js'
1
+ import { MessageReaction } from 'discord.js'
2
2
  import { Controller, ReactionHandler } from 'meocord/decorator'
3
3
  import { Logger } from 'meocord/common'
4
- import { CommandType, ReactionHandlerAction } from 'meocord/enum'
4
+ import { ReactionHandlerAction } from 'meocord/enum'
5
5
  import { type ReactionHandlerOptions } from 'meocord/interface'
6
6
 
7
7
  @Controller()
@@ -10,12 +10,12 @@ export class {{className}}ReactionController {
10
10
 
11
11
  @ReactionHandler('😋')
12
12
  async handleReaction(reaction: MessageReaction, { user, action }: ReactionHandlerOptions) {
13
- this.logger.log(`Reaction 😋 ${action === ReactionHandlerAction.ADD ? 'added' : 'removed'}.`);
13
+ this.logger.log(`Reaction 😋 ${action === ReactionHandlerAction.ADD ? 'added' : 'removed'}.`)
14
14
 
15
- if (!reaction.message) return;
15
+ if (!reaction.message) return
16
16
 
17
17
  if (action === ReactionHandlerAction.ADD) {
18
- await reaction.message.reply(`${user.username} reacted with 😋!`);
18
+ await reaction.message.reply(`${user.username} reacted with 😋!`)
19
19
  }
20
20
  }
21
21
 
@@ -1,10 +1,10 @@
1
1
  import { Guard } from 'meocord/decorator'
2
- import { GuardInterface } from 'meocord/interface'
2
+ import { type GuardInterface } from 'meocord/interface'
3
3
  import { BaseInteraction, Message, MessageReaction } from 'discord.js'
4
4
 
5
5
  @Guard()
6
6
  export class {{className}}Guard implements GuardInterface {
7
- async canActivate(context: BaseInteraction | Message | MessageReaction): Promise<boolean> {
7
+ async canActivate(_context: BaseInteraction | Message | MessageReaction): Promise<boolean> {
8
8
  // TODO: Implement the guard logic to determine if the interaction is allowed
9
9
  return true
10
10
  }
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'url';
23
23
  import { assertNoWebpackHook, createRsbuildConfig, DISCORD_OPTIONAL_NATIVES } from '../build/rsbuild-config.js';
24
24
  import { createNativeExternals, nativeCarrier, copyPackagesInto, bundledModuleFiles, assertNoBundledNativeAddons, findBundledNativeAddons } from '../build/native-addons.js';
25
25
  import { writePlatformManifest, PLATFORM_MANIFEST } from '../util/platform.util.js';
26
- import { loadMeoCordSourceConfig } from '../util/meocord-config-loader.util.js';
26
+ import { loadMeoCordSourceConfig } from '../util/meocord-source-config.util.js';
27
27
 
28
28
  const __filename$1 = fileURLToPath(import.meta.url);
29
29
  const __dirname$1 = path.dirname(__filename$1);
@@ -380,8 +380,11 @@ copies or substantial portions of the Software.
380
380
  });
381
381
  await rsbuild.build();
382
382
  this.logger.info('Config compiled to dist/meocord.config.mjs');
383
- } catch {
384
- this.logger.warn('Failed to compile meocord.config.ts — runtime will fall back to source config.');
383
+ } catch (error) {
384
+ // The built application reads only the compiled config, so without it the bot cannot start.
385
+ this.logger.error(`Failed to compile meocord.config.ts: ${error instanceof Error ? error.message : error}`);
386
+ await wait(100);
387
+ process.exit(1);
385
388
  }
386
389
  }
387
390
  /**
@@ -1,5 +1,6 @@
1
1
  import { readdirSync, readFileSync, existsSync, cpSync } from 'fs';
2
2
  import path from 'path';
3
+ import { currentPlatform } from '../util/platform.util.js';
3
4
 
4
5
  /**
5
6
  * The package a resolved module file belongs to, and the directory it was installed in.
@@ -70,8 +71,9 @@ import path from 'path';
70
71
  * Where an optional dependency of the package in `from` was installed, if it was.
71
72
  *
72
73
  * Nested under the package, beside it (pnpm's layout, and npm's hoisting within a scope), or
73
- * hoisted to the project root. A platform package that did not match this machine is not
74
- * installed at all, which is how only the right binary is found.
74
+ * hoisted to the project root. Being installed does not make it this platform's: some package
75
+ * managers install platform packages for other C libraries too, so {@link copyPackagesInto} also
76
+ * checks what each one declares it is built for.
75
77
  */ function resolveDependencyDir(name, from, root) {
76
78
  const candidates = [
77
79
  path.join(from, 'node_modules', name),
@@ -176,28 +178,65 @@ import path from 'path';
176
178
  found
177
179
  };
178
180
  }
181
+ /**
182
+ * Whether a value passes a package.json `os`, `cpu` or `libc` list, the way npm reads one: a `!`
183
+ * entry excludes its value, and a list with plain entries admits only those.
184
+ */ function allowedBy(list, value) {
185
+ if (!Array.isArray(list) || list.length === 0) return true;
186
+ const entries = list.filter((entry)=>typeof entry === 'string');
187
+ if (entries.includes(`!${value}`)) return false;
188
+ const admitted = entries.filter((entry)=>!entry.startsWith('!'));
189
+ return admitted.length === 0 || admitted.includes(value);
190
+ }
191
+ /**
192
+ * Whether an installed package is built for the platform, going by the `os`, `cpu` and `libc` its
193
+ * package.json declares. A package that declares none of them runs anywhere.
194
+ *
195
+ * `libc` is compared only when the build platform's C library is known, so a runtime that cannot
196
+ * report it keeps the package rather than dropping it on a guess.
197
+ */ function isBuiltFor(dir, platform) {
198
+ let pkg;
199
+ try {
200
+ pkg = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
201
+ } catch {
202
+ return true;
203
+ }
204
+ if (!allowedBy(pkg.os, platform.platform) || !allowedBy(pkg.cpu, platform.arch)) return false;
205
+ return !platform.libc || allowedBy(pkg.libc, platform.libc);
206
+ }
207
+ /** Whether a path is a package inside a `node_modules` directory, declared for another platform. */ function isOtherPlatformPackage(source, platform) {
208
+ const parent = path.basename(path.dirname(source));
209
+ const scopeParent = path.basename(path.dirname(path.dirname(source)));
210
+ const isPackageDir = parent === 'node_modules' || parent.startsWith('@') && scopeParent === 'node_modules';
211
+ return isPackageDir && existsSync(path.join(source, 'package.json')) && !isBuiltFor(source, platform);
212
+ }
179
213
  /**
180
214
  * Copies packages, with everything they need at runtime, into `<outDir>/node_modules`.
181
215
  *
182
216
  * This is what lets a bundled application run with nothing installed beside it: the packages that
183
217
  * could not be bundled travel inside the output directory, where Node finds them by walking up
184
218
  * from the bundle. Each package's installed dependencies and optional dependencies are followed --
185
- * that is where a platform binary lives -- and one not installed for this platform is skipped.
186
- * Type-only `@types` packages are left behind; some packages list them as runtime dependencies,
187
- * and they only add weight.
219
+ * that is where a platform binary lives. A dependency is copied only when its `os`, `cpu` and `libc`
220
+ * admit the build platform: package managers differ in which of those they filter
221
+ * installs on -- bun installs both the glibc and the musl build on Linux -- and the output has to
222
+ * carry only binaries the platform check in `dist` would let load. Type-only `@types` packages are
223
+ * left behind; some packages list them as runtime dependencies, and they only add weight.
188
224
  *
225
+ * @param platform - What the binaries are for. The platform building, which is what `dist`
226
+ * records in its platform manifest.
189
227
  * @returns The names copied.
190
- */ function copyPackagesInto(packages, root, outDir) {
228
+ */ function copyPackagesInto(packages, root, outDir, platform = currentPlatform()) {
191
229
  const copied = new Set();
192
230
  const copy = (name, dir)=>{
193
231
  if (copied.has(name) || name.startsWith('@types/')) return;
194
232
  copied.add(name);
195
233
  const target = path.join(outDir, 'node_modules', name);
196
234
  // A package's own nested node_modules comes along with it, so only hoisted dependencies
197
- // need finding separately.
235
+ // need finding separately. The nested ones get the same platform check on the way.
198
236
  cpSync(dir, target, {
199
237
  recursive: true,
200
- dereference: true
238
+ dereference: true,
239
+ filter: (source)=>source === dir || !isOtherPlatformPackage(source, platform)
201
240
  });
202
241
  const { dependencies, optional } = readDependencies(dir);
203
242
  for (const dependency of [
@@ -206,7 +245,7 @@ import path from 'path';
206
245
  ]){
207
246
  if (existsSync(path.join(dir, 'node_modules', dependency, 'package.json'))) continue;
208
247
  const dependencyDir = resolveDependencyDir(dependency, dir, root);
209
- if (dependencyDir) copy(dependency, dependencyDir);
248
+ if (dependencyDir && isBuiltFor(dependencyDir, platform)) copy(dependency, dependencyDir);
210
249
  }
211
250
  };
212
251
  for (const [name, dir] of packages)copy(name, dir);
@@ -232,4 +271,4 @@ import path from 'path';
232
271
  throw new Error(`bundleDependencies bundled ${found.size === 1 ? 'a package that loads' : 'packages that load'} a native addon:\n` + `${lines.join('\n')}\n` + 'A native addon is a compiled binary for one platform and cannot travel inside the bundle. The build ' + 'would run on this machine and fail in production the first time the addon loads. Keep ' + `${found.size === 1 ? 'it' : 'them'} out of the bundle and install ${found.size === 1 ? 'it' : 'them'} ` + `on the server:\n\n externals: [${names}]`);
233
272
  }
234
273
 
235
- export { assertNoBundledNativeAddons, bundledModuleFiles, copyPackagesInto, createNativeExternals, findBundledNativeAddons, nativeCarrier, packageFromPath, packageNameOfRequest };
274
+ export { assertNoBundledNativeAddons, bundledModuleFiles, copyPackagesInto, createNativeExternals, findBundledNativeAddons, isBuiltFor, nativeCarrier, packageFromPath, packageNameOfRequest };
@@ -38,7 +38,14 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
38
38
  const { mode, bundleDependencies = false, externals = [] } = options;
39
39
  const cwd = process.cwd();
40
40
  const entry = options.entry ?? path.resolve(cwd, 'src', 'main.ts');
41
+ const assetPrefix = assetPrefixFor(path.resolve(cwd, 'dist'));
41
42
  return {
43
+ // Rsbuild takes the asset prefix from `dev.assetPrefix` in development and from
44
+ // `output.assetPrefix` in production, and the development default is `/`. Both are set, so an
45
+ // asset import is the same path on disk in either mode.
46
+ dev: {
47
+ assetPrefix
48
+ },
42
49
  source: {
43
50
  entry: {
44
51
  main: entry
@@ -108,7 +115,7 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
108
115
  // What `import image from './x.png'` evaluates to at runtime. A bot passes that string
109
116
  // to fs or to a Discord attachment, so it has to be a real path on disk, which Rsbuild's
110
117
  // web-oriented default is not.
111
- assetPrefix: assetPrefixFor(path.resolve(cwd, 'dist')),
118
+ assetPrefix,
112
119
  // Rsbuild inlines assets under 4 KB as base64 data URIs, so the same import would give a
113
120
  // path for a large file and a `data:` string for a small one. A bot reads its assets
114
121
  // with fs, where a data URI is ENOENT, so every asset is emitted as a file.
@@ -35,7 +35,7 @@ class MeoCordFactory {
35
35
  }
36
36
  const meocordConfig = loadMeoCordConfig();
37
37
  if (!meocordConfig) {
38
- throw new Error('MeoCord config not found. Ensure meocord.config.ts exists.');
38
+ throw new Error('MeoCord config not found: dist/meocord.config.mjs is missing or failed to load. Run `meocord build`.');
39
39
  }
40
40
  // Before anything is resolved: a controller or service is what first loads a native addon, and
41
41
  // one built for another platform would otherwise fail there with a linker error.
@@ -88,28 +88,30 @@ class MeoCordApp {
88
88
  }
89
89
  return this.controllerInstancesCache.get(controllerClass);
90
90
  }
91
- async start() {
92
- try {
93
- this.logger.log('Starting bot...');
94
- this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
95
- this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
96
- await this.registerCommands();
97
- }));
98
- this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
99
- this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
100
- this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
101
- user,
102
- action: ReactionHandlerAction.ADD
103
- })));
104
- this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
105
- user,
106
- action: ReactionHandlerAction.REMOVE
107
- })));
108
- await this.bot.login(this.discordToken);
109
- this.logger.log('Bot is online!');
110
- } catch (error) {
111
- this.logger.error('Error during bot startup:', error);
112
- }
91
+ /**
92
+ * Registers the Discord event handlers and logs in.
93
+ *
94
+ * Rejects when the login fails -- an invalid token, or Discord unreachable -- so the caller can
95
+ * stop with a non-zero exit code. A bot that never came online is a failed start, and a
96
+ * supervisor such as Docker's `restart: on-failure` or systemd can only tell if the process says so.
97
+ */ async start() {
98
+ this.logger.log('Starting bot...');
99
+ this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
100
+ this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
101
+ await this.registerCommands();
102
+ }));
103
+ this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
104
+ this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
105
+ this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
106
+ user,
107
+ action: ReactionHandlerAction.ADD
108
+ })));
109
+ this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
110
+ user,
111
+ action: ReactionHandlerAction.REMOVE
112
+ })));
113
+ await this.bot.login(this.discordToken);
114
+ this.logger.log('Bot is online!');
113
115
  }
114
116
  async registerCommands() {
115
117
  // Keyed by type and name together, because that pair is what Discord treats as one
@@ -1,4 +1,4 @@
1
- var version = "4.0.0-beta.0";
1
+ var version = "4.0.0-beta.2";
2
2
  var packageJson = {
3
3
  version: version};
4
4
 
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { loadMeoCordConfig } from './meocord-config-loader.util.js';
3
+ import { loadMeoCordCliConfig } from './meocord-source-config.util.js';
4
4
  import wait from './wait.util.js';
5
5
  import chalk from 'chalk';
6
6
 
@@ -47,7 +47,7 @@ import chalk from 'chalk';
47
47
  await wait(100);
48
48
  process.exit(1);
49
49
  }
50
- loadMeoCordConfig();
50
+ loadMeoCordCliConfig();
51
51
  }
52
52
  /**
53
53
  * Ensures a Discord token is configured.
@@ -56,7 +56,7 @@ import chalk from 'chalk';
56
56
  * credentials — only connecting to the gateway does. Requiring one to build meant a
57
57
  * freshly created application could not be built until a token had been obtained.
58
58
  */ async function validateDiscordToken() {
59
- if (!loadMeoCordConfig()?.discordToken) {
59
+ if (!loadMeoCordCliConfig()?.discordToken) {
60
60
  console.error(chalk.red('Discord token is missing!'));
61
61
  await wait(100);
62
62
  process.exit(1);