meocord 4.0.0-beta.0 → 4.0.0-beta.1

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,36 @@
1
1
  # meocord
2
2
 
3
+ ## 4.0.0-beta.1
4
+
5
+ ### Major Changes
6
+
7
+ - [#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()`
8
+ of an ES module. Node 22.12 runs that without a flag but still warns on every start and crashes on a
9
+ config that throws; 22.13 is the first 22 release that does neither. Bun is unaffected. See the
10
+ [migration guide](https://github.com/l7aromeo/meocord/blob/main/docs/MIGRATING.md#before-you-start-nodejs-2213).
11
+
12
+ ### Patch Changes
13
+
14
+ - [#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`,
15
+ already compiled JavaScript, through jiti, and the logger and the factory both reach that loader,
16
+ so jiti was bundled into every bot: 190 KB, 89% of a minimal bot's `main.js`, and a
17
+ `Critical dependency` warning on every build. The compiled config is now loaded with `require()`,
18
+ and jiti is only used by the CLI to read `meocord.config.ts`. A minimal bot bundles to 22 KB.
19
+
20
+ A built bot no longer falls back to reading `meocord.config.ts` when `dist/meocord.config.mjs` is
21
+ missing, and `meocord build` now fails when the config does not compile, instead of warning and
22
+ producing a bot that cannot start.
23
+
24
+ - [#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
25
+ `import logo from './logo.png'` the path `/assets/logo.png`, at the root of the filesystem, so a bot
26
+ reading an imported font or image failed in development while production worked.
27
+
28
+ - [#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
29
+ package was copied into `dist/node_modules`, and bun installs both the glibc and the musl build on
30
+ Linux, so a glibc build of a bot using sharp carried about 19 MB of musl binaries it could never
31
+ load. A package whose `os`, `cpu` or `libc` does not match the platform building is now left out,
32
+ whichever package manager installed it.
33
+
3
34
  ## 4.0.0-beta.0
4
35
 
5
36
  ### 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)=>{
@@ -593,7 +593,7 @@ class MeoCordFactory {
593
593
  }
594
594
  const meocordConfig = theme.loadMeoCordConfig();
595
595
  if (!meocordConfig) {
596
- throw new Error('MeoCord config not found. Ensure meocord.config.ts exists.');
596
+ throw new Error('MeoCord config not found: dist/meocord.config.mjs is missing or failed to load. Run `meocord build`.');
597
597
  }
598
598
  // Before anything is resolved: a controller or service is what first loads a native addon, and
599
599
  // one built for another platform would otherwise fail there with a linker error.
@@ -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.
@@ -1,4 +1,4 @@
1
- var version = "4.0.0-beta.0";
1
+ var version = "4.0.0-beta.1";
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);
@@ -1,87 +1,43 @@
1
1
  import path from 'path';
2
- import { existsSync, readFileSync } from 'fs';
3
- import { createJiti } from 'jiti';
4
- import { fixJSON } from './json.util.js';
2
+ import { existsSync } from 'fs';
3
+ import { createRequire } from 'module';
5
4
 
6
5
  let cachedConfig;
7
6
  let configLoaded = false;
8
7
  /**
9
- * Loads the MeoCord configuration, checking compiled output first then falling back to source.
8
+ * Loads the configuration a built application runs with: `dist/meocord.config.mjs`, which
9
+ * `meocord build` compiles from `meocord.config.ts`.
10
10
  *
11
- * Resolution order:
12
- * 1. `dist/meocord.config.mjs` — pre-compiled by `meocord build`, no tsconfig or source files needed
13
- * 2. `meocord.config.ts` — loaded via jiti with tsconfig path aliases (dev mode)
11
+ * Reads only the compiled file, which is plain JavaScript and needs no transpiler, tsconfig or
12
+ * source. Reading `meocord.config.ts` is {@link loadMeoCordSourceConfig}'s job, and it lives in its
13
+ * own module because it needs jiti: this one is imported by the logger and the factory, so a
14
+ * bot built with `bundleDependencies` bundles whatever it imports.
14
15
  *
15
- * The result is cached after the first successful load.
16
+ * The result is cached after the first load.
16
17
  *
17
- * @returns {MeoCordConfig | undefined} The loaded configuration object, or undefined if loading fails.
18
+ * @returns The configuration, or undefined when there is no compiled config or it fails to load.
18
19
  */ function loadMeoCordConfig() {
19
20
  if (configLoaded) return cachedConfig;
20
21
  configLoaded = true;
21
- cachedConfig = loadCompiledConfig() ?? loadSourceConfig();
22
+ cachedConfig = loadCompiledConfig();
22
23
  return cachedConfig;
23
24
  }
24
25
  /**
25
- * Loads the pre-compiled config from dist/meocord.config.mjs.
26
- * This file is generated by `meocord build` and requires no tsconfig or source files.
26
+ * `require` of an ES module, which Node supports cleanly from 22.13 and bun always has.
27
+ * Synchronous, like the logger and the factory that call it.
27
28
  */ function loadCompiledConfig() {
28
29
  const compiledPath = path.resolve(process.cwd(), 'dist', 'meocord.config.mjs');
29
30
  if (!existsSync(compiledPath)) return undefined;
30
31
  try {
31
- const jiti = createJiti(import.meta.url, {
32
- interopDefault: true
33
- });
34
- return jiti(compiledPath);
35
- } catch {
36
- // Fall through to source config
37
- return undefined;
38
- }
39
- }
40
- /**
41
- * Loads the source config from meocord.config.ts via jiti with tsconfig path alias resolution.
42
- * Used in development mode where source files and tsconfig.json are available.
43
- */ /**
44
- * Loads meocord.config.ts from source, every time, for configuring a build.
45
- *
46
- * {@link loadMeoCordConfig} prefers the compiled `dist/meocord.config.mjs` and caches the result,
47
- * which is right at runtime -- production has no source -- and wrong while building. The compiled
48
- * file is the previous build's output, so a build that read it ran on the config as it was last
49
- * time: an edit to `meocord.config.ts` took effect only on the build after next. Whatever read the
50
- * config first in the process also fixed it for the rest, so the watcher's reload on a config
51
- * change reloaded nothing. Build time always has the source, so it reads the source.
52
- */ function loadMeoCordSourceConfig() {
53
- return loadSourceConfig();
54
- }
55
- function loadSourceConfig() {
56
- const configPath = path.resolve(process.cwd(), 'meocord.config.ts');
57
- if (!existsSync(configPath)) return undefined;
58
- try {
59
- const tsConfigPath = path.resolve(process.cwd(), 'tsconfig.json');
60
- const aliases = {};
61
- if (existsSync(tsConfigPath)) {
62
- const tsConfig = JSON.parse(fixJSON(readFileSync(tsConfigPath, 'utf-8')));
63
- const paths = tsConfig?.compilerOptions?.paths;
64
- if (paths) {
65
- for (const [key, values] of Object.entries(paths)){
66
- const aliasKey = key.replace('/*', '');
67
- aliases[aliasKey] = path.resolve(process.cwd(), values[0].replace('/*', ''));
68
- }
69
- }
70
- }
71
- const jiti = createJiti(import.meta.url, {
72
- interopDefault: true,
73
- alias: aliases,
74
- moduleCache: false
75
- });
76
- return jiti(configPath);
32
+ // Called through a variable so a bundler does not try to resolve the path while building the
33
+ // application; the file is only there once the build is done.
34
+ const load = createRequire(import.meta.url);
35
+ const loaded = load(compiledPath);
36
+ return loaded.default ?? loaded;
77
37
  } catch (error) {
78
- if (error instanceof Error) {
79
- console.error(`[MeoCord] Failed to load config: ${error.message}`);
80
- } else {
81
- console.error(`[MeoCord] Failed to load config: Unknown error`);
82
- }
38
+ console.error(`[MeoCord] Failed to load dist/meocord.config.mjs: ${error instanceof Error ? error.message : error}`);
83
39
  return undefined;
84
40
  }
85
41
  }
86
42
 
87
- export { loadMeoCordConfig, loadMeoCordSourceConfig };
43
+ export { loadMeoCordConfig };
@@ -0,0 +1,55 @@
1
+ import path from 'path';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { createJiti } from 'jiti';
4
+ import { fixJSON } from './json.util.js';
5
+ import { loadMeoCordConfig } from './meocord-config-loader.util.js';
6
+
7
+ /**
8
+ * Loads meocord.config.ts from source, every time, for configuring a build.
9
+ *
10
+ * {@link loadMeoCordConfig} reads the compiled `dist/meocord.config.mjs` and caches the result,
11
+ * which is right at runtime -- production has no source -- and wrong while building. The compiled
12
+ * file is the previous build's output, so a build that read it would run on the config as it was
13
+ * last time, and the watcher's reload on a config change would reload nothing. Build time always
14
+ * has the source, so it reads the source.
15
+ *
16
+ * Only the CLI imports this module. It needs jiti to run TypeScript, and a bot has no use for jiti.
17
+ */ function loadMeoCordSourceConfig() {
18
+ const configPath = path.resolve(process.cwd(), 'meocord.config.ts');
19
+ if (!existsSync(configPath)) return undefined;
20
+ try {
21
+ const tsConfigPath = path.resolve(process.cwd(), 'tsconfig.json');
22
+ const aliases = {};
23
+ if (existsSync(tsConfigPath)) {
24
+ const tsConfig = JSON.parse(fixJSON(readFileSync(tsConfigPath, 'utf-8')));
25
+ const paths = tsConfig?.compilerOptions?.paths;
26
+ if (paths) {
27
+ for (const [key, values] of Object.entries(paths)){
28
+ const aliasKey = key.replace('/*', '');
29
+ aliases[aliasKey] = path.resolve(process.cwd(), values[0].replace('/*', ''));
30
+ }
31
+ }
32
+ }
33
+ const jiti = createJiti(import.meta.url, {
34
+ interopDefault: true,
35
+ alias: aliases,
36
+ moduleCache: false
37
+ });
38
+ return jiti(configPath);
39
+ } catch (error) {
40
+ if (error instanceof Error) {
41
+ console.error(`[MeoCord] Failed to load config: ${error.message}`);
42
+ } else {
43
+ console.error(`[MeoCord] Failed to load config: Unknown error`);
44
+ }
45
+ return undefined;
46
+ }
47
+ }
48
+ /**
49
+ * The configuration as the CLI sees it: the compiled config when a build has produced one, and the
50
+ * source otherwise -- `meocord start` checks the token before anything has been built.
51
+ */ function loadMeoCordCliConfig() {
52
+ return loadMeoCordConfig() ?? loadMeoCordSourceConfig();
53
+ }
54
+
55
+ export { loadMeoCordCliConfig, loadMeoCordSourceConfig };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "meocord",
3
3
  "description": "Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.",
4
- "version": "4.0.0-beta.0",
4
+ "version": "4.0.0-beta.1",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=22"
7
+ "node": ">=22.13"
8
8
  },
9
9
  "scripts": {
10
10
  "format": "prettier --write \"**/*.md\" \"src/bin/app-template/**/*.template\"",
@@ -1,168 +0,0 @@
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 jiti = require('jiti');
10
- var chalk = require('chalk');
11
-
12
- var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
13
- /**
14
- * Helper function to fix common JSON formatting issues in tsconfig.json, such as:
15
- * - Removing single-line comments.
16
- * - Removing trailing commas.
17
- * - Stripping newlines.
18
- *
19
- * @param {string} jsonString - The raw JSON string to fix.
20
- * @returns {string} The corrected JSON string.
21
- */ function fixJSON(jsonString) {
22
- return jsonString.replace(/\/\/.*$/gm, '') // Remove single-line comments
23
- .replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas before } or ]
24
- .replace(/,\s*$/, '') // Remove trailing commas at the end of the file
25
- .replace(/^\s*[\r\n]/gm, '') // Replace empty lines only
26
- ;
27
- }
28
-
29
- let cachedConfig;
30
- let configLoaded = false;
31
- /**
32
- * Loads the MeoCord configuration, checking compiled output first then falling back to source.
33
- *
34
- * Resolution order:
35
- * 1. `dist/meocord.config.mjs` — pre-compiled by `meocord build`, no tsconfig or source files needed
36
- * 2. `meocord.config.ts` — loaded via jiti with tsconfig path aliases (dev mode)
37
- *
38
- * The result is cached after the first successful load.
39
- *
40
- * @returns {MeoCordConfig | undefined} The loaded configuration object, or undefined if loading fails.
41
- */ function loadMeoCordConfig() {
42
- if (configLoaded) return cachedConfig;
43
- configLoaded = true;
44
- cachedConfig = loadCompiledConfig() ?? loadSourceConfig();
45
- return cachedConfig;
46
- }
47
- /**
48
- * Loads the pre-compiled config from dist/meocord.config.mjs.
49
- * This file is generated by `meocord build` and requires no tsconfig or source files.
50
- */ function loadCompiledConfig() {
51
- const compiledPath = path.resolve(process.cwd(), 'dist', 'meocord.config.mjs');
52
- if (!fs.existsSync(compiledPath)) return undefined;
53
- try {
54
- const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/theme-BXdSoifk.cjs', document.baseURI).href)), {
55
- interopDefault: true
56
- });
57
- return jiti$1(compiledPath);
58
- } catch {
59
- // Fall through to source config
60
- return undefined;
61
- }
62
- }
63
- function loadSourceConfig() {
64
- const configPath = path.resolve(process.cwd(), 'meocord.config.ts');
65
- if (!fs.existsSync(configPath)) return undefined;
66
- try {
67
- const tsConfigPath = path.resolve(process.cwd(), 'tsconfig.json');
68
- const aliases = {};
69
- if (fs.existsSync(tsConfigPath)) {
70
- const tsConfig = JSON.parse(fixJSON(fs.readFileSync(tsConfigPath, 'utf-8')));
71
- const paths = tsConfig?.compilerOptions?.paths;
72
- if (paths) {
73
- for (const [key, values] of Object.entries(paths)){
74
- const aliasKey = key.replace('/*', '');
75
- aliases[aliasKey] = path.resolve(process.cwd(), values[0].replace('/*', ''));
76
- }
77
- }
78
- }
79
- const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/theme-BXdSoifk.cjs', document.baseURI).href)), {
80
- interopDefault: true,
81
- alias: aliases,
82
- moduleCache: false
83
- });
84
- return jiti$1(configPath);
85
- } catch (error) {
86
- if (error instanceof Error) {
87
- console.error(`[MeoCord] Failed to load config: ${error.message}`);
88
- } else {
89
- console.error(`[MeoCord] Failed to load config: Unknown error`);
90
- }
91
- return undefined;
92
- }
93
- }
94
-
95
- dayjs.extend(utc);
96
- dayjs.extend(timezone);
97
- class Logger {
98
- log(...args) {
99
- this.logWithContext('log', args);
100
- }
101
- info(...args) {
102
- this.logWithContext('log', args);
103
- }
104
- warn(...args) {
105
- this.logWithContext('warn', args);
106
- }
107
- error(...args) {
108
- this.logWithContext('error', args);
109
- }
110
- debug(...args) {
111
- this.logWithContext('debug', args);
112
- }
113
- verbose(...args) {
114
- this.logWithContext('log', args);
115
- }
116
- formatMessage(message, logType) {
117
- if (typeof message === 'object' && message !== null) {
118
- return node_util.inspect(message, {
119
- showHidden: true,
120
- depth: null,
121
- colors: true,
122
- compact: false,
123
- showProxy: true
124
- });
125
- }
126
- return (this.colorMap[logType] || ((msg)=>msg))(message);
127
- }
128
- logWithContext(logLevel, messages) {
129
- if (messages.length === 0) return;
130
- const config = loadMeoCordConfig();
131
- const logType = logLevel.toUpperCase();
132
- const applyColor = this.colorMap[logType] || ((msg)=>msg);
133
- const formattedMessages = messages.map((message)=>this.formatMessage(message, logType));
134
- const coloredAppName = config?.appName ? applyColor(chalk.bold(`[${config.appName}]`)) : undefined;
135
- const timestamp = chalk.bold(dayjs().format('dddd, MMMM D, YYYY HH:mm:ss [UTC]Z'));
136
- const coloredLogLevel = applyColor(chalk.bold(`[${logType}]`));
137
- const coloredContext = this.context ? chalk.yellow.bold(`[${this.context}]`) : '';
138
- const logTexts = [
139
- coloredAppName,
140
- timestamp,
141
- coloredLogLevel,
142
- coloredContext,
143
- ...formattedMessages
144
- ].filter((log)=>!!log);
145
- console[logLevel](...logTexts);
146
- }
147
- constructor(context){
148
- this.context = context;
149
- this.colorMap = {
150
- LOG: chalk.green,
151
- INFO: chalk.cyan,
152
- WARN: chalk.yellow,
153
- ERROR: chalk.red,
154
- DEBUG: chalk.magenta
155
- };
156
- }
157
- }
158
-
159
- class Theme {
160
- }
161
- Theme.successColor = '#28A745';
162
- Theme.infoColor = '#17A2B8';
163
- Theme.errorColor = '#DC3545';
164
- Theme.warningColor = '#FFC107';
165
-
166
- exports.Logger = Logger;
167
- exports.Theme = Theme;
168
- exports.loadMeoCordConfig = loadMeoCordConfig;