meocord 3.2.2 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +161 -29
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/dist/cjs/_shared/{controller.decorator-eT0oHb8D.cjs → controller.decorator-9nIxOp9y.cjs} +1 -1
  5. package/dist/cjs/_shared/{metadata-key.enum-DITxfZlw.cjs → metadata-key.enum-aABGdodb.cjs} +0 -4
  6. package/dist/cjs/_shared/theme-Dqwlnz0m.cjs +124 -0
  7. package/dist/cjs/common/index.cjs +2 -6
  8. package/dist/cjs/core/index.cjs +69 -7
  9. package/dist/cjs/decorator/index.cjs +2 -2
  10. package/dist/cjs/enum/index.cjs +1 -1
  11. package/dist/cjs/testing/index.cjs +2 -6
  12. package/dist/esm/bin/app-template/meocord.config.ts.template +8 -13
  13. package/dist/esm/bin/app-template/package.json.template +12 -12
  14. package/dist/esm/bin/app-template/vitest.config.ts.template +6 -2
  15. package/dist/esm/bin/builder-template/builder/context-menu.builder.template +2 -2
  16. package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +2 -2
  17. package/dist/esm/bin/builder-template/builder/slash.builder.template +2 -2
  18. package/dist/esm/bin/builder-template/controller/autocomplete.controller.template +4 -3
  19. package/dist/esm/bin/builder-template/controller/context-menu.controller.template +2 -2
  20. package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.template +2 -2
  21. package/dist/esm/bin/builder-template/controller/slash.controller.template +2 -2
  22. package/dist/esm/bin/helper/controller-generator.helper.js +43 -26
  23. package/dist/esm/bin/helper/guard-generator.helper.js +8 -2
  24. package/dist/esm/bin/helper/service-generator.helper.js +8 -2
  25. package/dist/esm/bin/meocord.js +165 -142
  26. package/dist/esm/build/native-addons.js +274 -0
  27. package/dist/esm/build/rsbuild-config.js +176 -0
  28. package/dist/esm/common/decorator.js +0 -4
  29. package/dist/esm/common/theme.js +1 -5
  30. package/dist/esm/core/meocord-factory.js +5 -1
  31. package/dist/esm/enum/controller.enum.js +1 -5
  32. package/dist/esm/enum/metadata-key.enum.js +0 -4
  33. package/dist/esm/package.json.js +1 -1
  34. package/dist/esm/testing/mock-fn.js +1 -5
  35. package/dist/esm/util/common.util.js +3 -3
  36. package/dist/esm/util/generator-cli.util.js +54 -10
  37. package/dist/esm/util/json.util.js +0 -4
  38. package/dist/esm/util/meocord-config-loader.util.js +19 -51
  39. package/dist/esm/util/meocord-source-config.util.js +55 -0
  40. package/dist/esm/util/platform.util.js +66 -0
  41. package/dist/esm/util/runtime.util.js +20 -7
  42. package/dist/esm/util/tsconfig.util.js +1 -1
  43. package/dist/esm/util/wait.util.js +1 -5
  44. package/dist/types/common/index.d.ts +0 -16
  45. package/dist/types/core/index.d.ts +0 -12
  46. package/dist/types/decorator/index.d.ts +1 -31
  47. package/dist/types/enum/index.d.ts +0 -5
  48. package/dist/types/interface/index.d.ts +46 -57
  49. package/dist/types/testing/index.d.ts +4 -19
  50. package/meocord.eslint.cjs +0 -6
  51. package/meocord.eslint.d.ts +0 -6
  52. package/meocord.eslint.mjs +0 -6
  53. package/package.json +20 -24
  54. package/dist/cjs/_shared/theme-EmG1ZEwV.cjs +0 -179
  55. package/webpack.config.js +0 -147
@@ -0,0 +1,274 @@
1
+ import { readdirSync, readFileSync, existsSync, cpSync } from 'fs';
2
+ import path from 'path';
3
+ import { currentPlatform } from '../util/platform.util.js';
4
+
5
+ /**
6
+ * The package a resolved module file belongs to, and the directory it was installed in.
7
+ *
8
+ * Takes the innermost `node_modules` segment, so a nested copy is attributed to itself rather
9
+ * than to the package it is nested under. Undefined for application code.
10
+ */ function packageFromPath(file) {
11
+ // A module identifier can carry loaders before the resource (`loader!/path/to/file`) and a
12
+ // query after it; only the path itself says where the package lives.
13
+ const resource = file.slice(file.lastIndexOf('!') + 1).split('?')[0];
14
+ const segments = resource.split(/[\\/]/);
15
+ const index = segments.lastIndexOf('node_modules');
16
+ if (index === -1 || index + 1 >= segments.length) return undefined;
17
+ const scoped = segments[index + 1].startsWith('@');
18
+ if (scoped && index + 2 >= segments.length) return undefined;
19
+ const end = index + (scoped ? 3 : 2);
20
+ return {
21
+ name: segments.slice(index + 1, end).join('/'),
22
+ dir: segments.slice(0, end).join(path.sep)
23
+ };
24
+ }
25
+ /**
26
+ * Every source file a build pulled into its output.
27
+ *
28
+ * Concatenated modules nest their members, and a multi-environment build nests whole
29
+ * compilations as children, so both are walked.
30
+ */ function bundledModuleFiles(stats) {
31
+ if (!stats) return [];
32
+ const json = stats.toJson({
33
+ all: false,
34
+ modules: true,
35
+ nestedModules: true,
36
+ // A production build concatenates most of node_modules into a few modules, and the members
37
+ // of a concatenation are orphans -- hidden from stats unless asked for. Without this every
38
+ // concatenated package, sharp included, was invisible here.
39
+ orphanModules: true,
40
+ modulesSpace: Infinity,
41
+ nestedModulesSpace: Infinity
42
+ });
43
+ const collect = (modules)=>(modules ?? []).flatMap((module)=>[
44
+ module.nameForCondition ?? module.identifier ?? '',
45
+ ...collect(module.modules),
46
+ // Stats may group modules by path, nesting them under `children`.
47
+ ...collect(module.children)
48
+ ]);
49
+ const compilations = json.children?.length ? json.children : [
50
+ json
51
+ ];
52
+ return compilations.flatMap((compilation)=>collect(compilation.modules)).filter(Boolean);
53
+ }
54
+ /** Whether a directory holds a compiled addon, not counting packages nested inside it. */ function containsNativeBinary(dir, depth = 0) {
55
+ if (depth > 5) return false;
56
+ let entries;
57
+ try {
58
+ entries = readdirSync(dir, {
59
+ withFileTypes: true
60
+ });
61
+ } catch {
62
+ return false;
63
+ }
64
+ return entries.some((entry)=>{
65
+ if (entry.isFile()) return entry.name.endsWith('.node');
66
+ if (!entry.isDirectory() || entry.name === 'node_modules') return false;
67
+ return containsNativeBinary(path.join(dir, entry.name), depth + 1);
68
+ });
69
+ }
70
+ /**
71
+ * Where an optional dependency of the package in `from` was installed, if it was.
72
+ *
73
+ * Nested under the package, beside it (pnpm's layout, and npm's hoisting within a scope), or
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.
77
+ */ function resolveDependencyDir(name, from, root) {
78
+ const candidates = [
79
+ path.join(from, 'node_modules', name),
80
+ path.join(path.dirname(from), name),
81
+ path.join(path.dirname(path.dirname(from)), name),
82
+ path.join(root, 'node_modules', name)
83
+ ];
84
+ return candidates.find((dir)=>existsSync(path.join(dir, 'package.json')));
85
+ }
86
+ /** A package's declared dependencies, runtime and optional, or none if it cannot be read. */ function readDependencies(dir) {
87
+ try {
88
+ const pkg = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
89
+ return {
90
+ dependencies: Object.keys(pkg.dependencies ?? {}),
91
+ optional: Object.keys(pkg.optionalDependencies ?? {})
92
+ };
93
+ } catch {
94
+ return {
95
+ dependencies: [],
96
+ optional: []
97
+ };
98
+ }
99
+ }
100
+ /**
101
+ * The package holding the compiled addon an installed package loads, or undefined for plain
102
+ * JavaScript.
103
+ *
104
+ * The binary is rarely in the package that gets imported. node-gyp packages keep it in their own
105
+ * directory, under `build/Release`. napi-rs packages and sharp ship a JavaScript loader and publish
106
+ * each platform's binary as a separate package, declared as an optional dependency so only the
107
+ * matching one installs. Both layouts are checked.
108
+ */ function nativeCarrier(dir, name, root) {
109
+ if (containsNativeBinary(dir)) return name;
110
+ return readDependencies(dir).optional.find((dependency)=>{
111
+ const dependencyDir = resolveDependencyDir(dependency, dir, root);
112
+ return dependencyDir !== undefined && containsNativeBinary(dependencyDir);
113
+ });
114
+ }
115
+ /** Bundled packages that load a native addon, mapped to the package holding the binary. */ function findBundledNativeAddons(bundledFiles, root) {
116
+ const packages = new Map();
117
+ for (const file of bundledFiles){
118
+ const found = packageFromPath(file);
119
+ if (found && !packages.has(found.dir)) packages.set(found.dir, found.name);
120
+ }
121
+ const natives = new Map();
122
+ for (const [dir, name] of packages){
123
+ if (natives.has(name)) continue;
124
+ const carrier = nativeCarrier(dir, name, root);
125
+ if (carrier) natives.set(name, carrier);
126
+ }
127
+ return natives;
128
+ }
129
+ /**
130
+ * The package a bare import request names, or undefined for a relative path, an absolute path, or
131
+ * a Node builtin -- none of which live in node_modules.
132
+ */ function packageNameOfRequest(request) {
133
+ if (!request || request.startsWith('.') || request.startsWith('/') || /^[a-z]+:/i.test(request)) return undefined;
134
+ if (/^[A-Za-z]:[\\/]/.test(request)) return undefined;
135
+ const [first, second] = request.split('/');
136
+ if (!first.startsWith('@')) return first;
137
+ return second ? `${first}/${second}` : undefined;
138
+ }
139
+ /** Where `name` resolves from `context` the way Node would: the nearest node_modules walking up. */ function resolveInstalledPackage(name, context, root) {
140
+ let dir = context;
141
+ for(;;){
142
+ const candidate = path.join(dir, 'node_modules', name);
143
+ if (existsSync(path.join(candidate, 'package.json'))) return candidate;
144
+ const parent = path.dirname(dir);
145
+ if (parent === dir) break;
146
+ dir = parent;
147
+ }
148
+ const hoisted = path.join(root, 'node_modules', name);
149
+ return existsSync(path.join(hoisted, 'package.json')) ? hoisted : undefined;
150
+ }
151
+ /**
152
+ * An Rspack `externals` function that keeps every native addon out of the bundle.
153
+ *
154
+ * Decided while the bundler resolves imports, so it sees every request -- including one made by a
155
+ * plain JavaScript dependency deep in the graph -- and never has to be told what to look for. Each
156
+ * native package it keeps out is recorded in `found`, so the build can copy it into the output.
157
+ */ function createNativeExternals(root) {
158
+ const found = new Map();
159
+ const plain = new Set();
160
+ const externals = ({ request, context }, callback)=>{
161
+ const name = request ? packageNameOfRequest(request) : undefined;
162
+ if (!name || plain.has(name)) return callback();
163
+ if (found.has(name)) return callback(undefined, request);
164
+ const dir = resolveInstalledPackage(name, context ?? root, root);
165
+ const carrier = dir ? nativeCarrier(dir, name, root) : undefined;
166
+ if (!dir || !carrier) {
167
+ plain.add(name);
168
+ return callback();
169
+ }
170
+ found.set(name, {
171
+ dir,
172
+ carrier
173
+ });
174
+ callback(undefined, request);
175
+ };
176
+ return {
177
+ externals,
178
+ found
179
+ };
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
+ }
213
+ /**
214
+ * Copies packages, with everything they need at runtime, into `<outDir>/node_modules`.
215
+ *
216
+ * This is what lets a bundled application run with nothing installed beside it: the packages that
217
+ * could not be bundled travel inside the output directory, where Node finds them by walking up
218
+ * from the bundle. Each package's installed dependencies and optional dependencies are followed --
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.
224
+ *
225
+ * @param platform - What the binaries are for. The platform building, which is what `dist`
226
+ * records in its platform manifest.
227
+ * @returns The names copied.
228
+ */ function copyPackagesInto(packages, root, outDir, platform = currentPlatform()) {
229
+ const copied = new Set();
230
+ const copy = (name, dir)=>{
231
+ if (copied.has(name) || name.startsWith('@types/')) return;
232
+ copied.add(name);
233
+ const target = path.join(outDir, 'node_modules', name);
234
+ // A package's own nested node_modules comes along with it, so only hoisted dependencies
235
+ // need finding separately. The nested ones get the same platform check on the way.
236
+ cpSync(dir, target, {
237
+ recursive: true,
238
+ dereference: true,
239
+ filter: (source)=>source === dir || !isOtherPlatformPackage(source, platform)
240
+ });
241
+ const { dependencies, optional } = readDependencies(dir);
242
+ for (const dependency of [
243
+ ...dependencies,
244
+ ...optional
245
+ ]){
246
+ if (existsSync(path.join(dir, 'node_modules', dependency, 'package.json'))) continue;
247
+ const dependencyDir = resolveDependencyDir(dependency, dir, root);
248
+ if (dependencyDir && isBuiltFor(dependencyDir, platform)) copy(dependency, dependencyDir);
249
+ }
250
+ };
251
+ for (const [name, dir] of packages)copy(name, dir);
252
+ return [
253
+ ...copied
254
+ ];
255
+ }
256
+ /**
257
+ * Refuses a bundled build that swallowed a native addon.
258
+ *
259
+ * Such a build does not fail on its own. It succeeds, the bot starts, and on the build machine it
260
+ * even works, because the bundle resolves the addon back into that machine's node_modules by
261
+ * absolute path. Anywhere else it fails the first time the addon loads -- often a command, long
262
+ * after startup. Failing the build is the only point this can be caught.
263
+ */ function assertNoBundledNativeAddons(found) {
264
+ if (found.size === 0) return;
265
+ const lines = [
266
+ ...found
267
+ ].map(([bundled, carrier])=>carrier === bundled ? ` - ${bundled}` : ` - ${bundled} (its binary ships in ${carrier})`);
268
+ const names = [
269
+ ...found.keys()
270
+ ].map((name)=>`'${name}'`).join(', ');
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}]`);
272
+ }
273
+
274
+ export { assertNoBundledNativeAddons, bundledModuleFiles, copyPackagesInto, createNativeExternals, findBundledNativeAddons, isBuiltFor, nativeCarrier, packageFromPath, packageNameOfRequest };
@@ -0,0 +1,176 @@
1
+ import path from 'path';
2
+ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
3
+
4
+ /**
5
+ * Native accelerators discord.js reaches for at runtime and works without.
6
+ *
7
+ * `bufferutil` and `utf-8-validate` are `ws`'s optional peers; `zlib-sync` is loaded by
8
+ * `@discordjs/ws` without being declared at all, falling back to uncompressed identify when it
9
+ * is absent. A bundler cannot tell "optional" from "missing", so bundling dependencies failed on
10
+ * `Can't resolve 'zlib-sync'` for every bot until these were left as runtime imports -- where a
11
+ * missing one is caught and ignored, exactly as it is when nothing is bundled.
12
+ *
13
+ * They are externalised as `node-commonjs` rather than plain names. A plain name in an ESM build
14
+ * becomes a hoisted top-level `import`, which throws `Cannot find package 'bufferutil'` before the
15
+ * bot runs a line -- the opposite of the try/catch `ws` wraps around it. `node-commonjs` keeps a
16
+ * runtime `require` at the original call site, so the library's own fallback still applies.
17
+ */ const DISCORD_OPTIONAL_NATIVES = [
18
+ 'zlib-sync',
19
+ 'bufferutil',
20
+ 'utf-8-validate'
21
+ ];
22
+ /**
23
+ * The prefix an asset import is joined to at runtime: the output directory, with forward slashes.
24
+ *
25
+ * Rspack writes this into the bundle as a string literal without escaping it. A Windows path
26
+ * came through as `"D:\\a\\meocord\\dist/"` in the source, where `\\a` is a bell character and
27
+ * the other backslashes vanish -- every asset import evaluated to `D:ameocorddist/assets/...`.
28
+ * Windows accepts forward slashes everywhere a path is read, so they are used on every platform.
29
+ */ function assetPrefixFor(distDir) {
30
+ return `${distDir.replace(/\\/g, '/').replace(/\/+$/, '')}/`;
31
+ }
32
+ /**
33
+ * The bundler configuration MeoCord builds an application with.
34
+ *
35
+ * Built in code rather than read from a config file, so it is typed, testable, and not
36
+ * something a consumer can edit by accident.
37
+ */ function createRsbuildConfig(options) {
38
+ const { mode, bundleDependencies = false, externals = [] } = options;
39
+ const cwd = process.cwd();
40
+ const entry = options.entry ?? path.resolve(cwd, 'src', 'main.ts');
41
+ const assetPrefix = assetPrefixFor(path.resolve(cwd, 'dist'));
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
+ },
49
+ source: {
50
+ entry: {
51
+ main: entry
52
+ },
53
+ // Equivalent to experimentalDecorators. The decorators themselves are only half of
54
+ // what MeoCord needs -- see tools.swc below for the half that carries the metadata.
55
+ decorators: {
56
+ version: 'legacy'
57
+ },
58
+ tsconfigPath: prepareModifiedTsConfig()
59
+ },
60
+ tools: {
61
+ swc: {
62
+ jsc: {
63
+ // Rsbuild leaves externalHelpers on, which emits the decorator helpers as imports
64
+ // from `@swc/helpers` -- which in turn wants `tslib` resolvable in the application.
65
+ // Inlining them keeps the output self-contained, which matters most when
66
+ // bundleDependencies is on and there is no node_modules to resolve through.
67
+ externalHelpers: false,
68
+ transform: {
69
+ legacyDecorator: true,
70
+ // Inversify resolves constructor arguments from `design:paramtypes`, which swc
71
+ // emits only with this on. Without it every @Controller throws while its class is
72
+ // being defined. It is deep-merged into builtin:swc-loader rather than replacing
73
+ // it, so the rest of Rsbuild's swc defaults still apply.
74
+ decoratorMetadata: true
75
+ }
76
+ }
77
+ }
78
+ },
79
+ output: {
80
+ target: 'node',
81
+ // Rsbuild 2 already defaults Node builds to ESM; stated so the output format does not
82
+ // silently change with a future default.
83
+ module: true,
84
+ // Inverted deliberately: autoExternal leaves dependencies as runtime imports, so
85
+ // bundling them means turning it off.
86
+ autoExternal: !bundleDependencies,
87
+ externals: [
88
+ Object.fromEntries(DISCORD_OPTIONAL_NATIVES.map((name)=>[
89
+ name,
90
+ `node-commonjs ${name}`
91
+ ])),
92
+ ...externals
93
+ ],
94
+ // Rsbuild would put the bundle in dist/static/js and assets in dist/static/*. The
95
+ // application's entry is dist/main.js, which is what `meocord start` runs.
96
+ distPath: {
97
+ root: path.resolve(cwd, 'dist'),
98
+ js: '',
99
+ image: 'assets',
100
+ svg: 'assets',
101
+ font: 'assets',
102
+ media: 'assets'
103
+ },
104
+ // No content hash: a bot reads its assets from disk rather than serving them from a CDN,
105
+ // so there is no cache to bust, and stable names keep `dist/assets/` predictable. Each
106
+ // accepts a function too, for applications whose same-named files in different folders
107
+ // would otherwise collide.
108
+ filename: {
109
+ js: '[name].js',
110
+ image: '[name][ext]',
111
+ svg: '[name][ext]',
112
+ font: '[name][ext]',
113
+ media: '[name][ext]'
114
+ },
115
+ // What `import image from './x.png'` evaluates to at runtime. A bot passes that string
116
+ // to fs or to a Discord attachment, so it has to be a real path on disk, which Rsbuild's
117
+ // web-oriented default is not.
118
+ assetPrefix,
119
+ // Rsbuild inlines assets under 4 KB as base64 data URIs, so the same import would give a
120
+ // path for a large file and a `data:` string for a small one. A bot reads its assets
121
+ // with fs, where a data URI is ENOENT, so every asset is emitted as a file.
122
+ dataUriLimit: 0,
123
+ // Rsbuild emits no source maps in production by default, which would leave a crashed
124
+ // bot's stack trace pointing into the bundle instead of the source.
125
+ sourceMap: {
126
+ js: mode === 'production' ? 'source-map' : 'eval-source-map'
127
+ },
128
+ // Rsbuild empties dist before building by default. MeoCord runs two builds into the same
129
+ // directory -- the application, and meocord.config.ts into dist/meocord.config.mjs -- so
130
+ // cleaning would let whichever runs second erase the other.
131
+ cleanDistPath: false,
132
+ minify: {
133
+ // Off by default for Node targets in Rsbuild 2, so production builds would ship
134
+ // unminified unless this is stated.
135
+ js: mode === 'production',
136
+ jsOptions: {
137
+ minimizerOptions: {
138
+ // Inversify resolves dependencies by class identity, so a mangled class name
139
+ // breaks injection in production while development stays fine.
140
+ mangle: {
141
+ keep_classnames: true,
142
+ keep_fnames: true
143
+ },
144
+ compress: {
145
+ keep_classnames: true,
146
+ keep_fnames: true
147
+ }
148
+ }
149
+ }
150
+ }
151
+ },
152
+ mode,
153
+ performance: {
154
+ // The bot is not served over a network; splitting it only makes startup resolve more files.
155
+ chunkSplit: {
156
+ strategy: 'all-in-one'
157
+ }
158
+ }
159
+ };
160
+ }
161
+ /**
162
+ * Refuses a MeoCord config that declares a `webpack` hook, which MeoCord does not run.
163
+ *
164
+ * Without this, such a config builds "successfully" with the customisation silently dropped --
165
+ * assets landing in the wrong place, markdown imported as a path instead of its text. Refusing
166
+ * is kinder than a green build that ships something else.
167
+ *
168
+ * Read as a property rather than tested with `in`: a config loaded from source comes back as
169
+ * jiti's interop proxy, where property access reaches the default export but `in` and
170
+ * Object.keys only see `default` -- so `'webpack' in config` is false even when it is set.
171
+ */ function assertNoWebpackHook(config) {
172
+ if (config?.webpack === undefined) return;
173
+ throw new Error('meocord.config.ts still declares a `webpack` hook, which MeoCord no longer runs. ' + 'Rename it to `rsbuild` and reshape its body for Rsbuild: images, fonts, svg and media ' + 'need no rules any more, `output.filename` accepts a function for custom asset paths, ' + 'and raw rules go through `tools.rspack`. See the 4.0.0 release notes.');
174
+ }
175
+
176
+ export { DISCORD_OPTIONAL_NATIVES, assertNoWebpackHook, assetPrefixFor, createRsbuildConfig };
@@ -1,8 +1,4 @@
1
1
  /**
2
- * MeoCord Framework
3
- * Copyright (c) 2025-present Ukasyah Rahmatullah Zada
4
- * SPDX-License-Identifier: MIT
5
- */ /**
6
2
  * Composes multiple class or method decorators into a single decorator.
7
3
  *
8
4
  * @example
@@ -1,8 +1,4 @@
1
- /**
2
- * MeoCord Framework
3
- * Copyright (c) 2025-present Ukasyah Rahmatullah Zada
4
- * SPDX-License-Identifier: MIT
5
- */ class Theme {
1
+ class Theme {
6
2
  }
7
3
  Theme.successColor = '#28A745';
8
4
  Theme.infoColor = '#17A2B8';
@@ -5,6 +5,7 @@ import { Logger } from '../common/logger.js';
5
5
  import '../common/theme.js';
6
6
  import { MeoCordApp } from './meocord.app.js';
7
7
  import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
8
+ import { assertBuiltForThisPlatform } from '../util/platform.util.js';
8
9
  import { MetadataKey } from '../enum/metadata-key.enum.js';
9
10
 
10
11
  /**
@@ -34,8 +35,11 @@ class MeoCordFactory {
34
35
  }
35
36
  const meocordConfig = loadMeoCordConfig();
36
37
  if (!meocordConfig) {
37
- 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`.');
38
39
  }
40
+ // Before anything is resolved: a controller or service is what first loads a native addon, and
41
+ // one built for another platform would otherwise fail there with a linker error.
42
+ assertBuiltForThisPlatform();
39
43
  const container = new Container();
40
44
  // Bind the Discord client as a constant value
41
45
  const discordClient = new Client(options.clientOptions);
@@ -1,8 +1,4 @@
1
- /**
2
- * MeoCord Framework
3
- * Copyright (c) 2025-present Ukasyah Rahmatullah Zada
4
- * SPDX-License-Identifier: MIT
5
- */ var ControllerType = /*#__PURE__*/ function(ControllerType) {
1
+ var ControllerType = /*#__PURE__*/ function(ControllerType) {
6
2
  ControllerType["BUTTON"] = "button";
7
3
  ControllerType["MODAL_SUBMIT"] = "modal-submit";
8
4
  ControllerType["SELECT_MENU"] = "select-menu";
@@ -1,8 +1,4 @@
1
1
  /**
2
- * MeoCord Framework
3
- * Copyright (c) 2025-present Ukasyah Rahmatullah Zada
4
- * SPDX-License-Identifier: MIT
5
- */ /**
6
2
  * Centralised metadata keys used across the framework's `Reflect` calls.
7
3
  *
8
4
  * Keeping them here prevents typos, documents the Inversify 8 key rename,
@@ -1,4 +1,4 @@
1
- var version = "3.2.2";
1
+ var version = "4.0.0-beta.1";
2
2
  var packageJson = {
3
3
  version: version};
4
4
 
@@ -1,8 +1,4 @@
1
- /**
2
- * MeoCord Framework
3
- * Copyright (c) 2025-present Ukasyah Rahmatullah Zada
4
- * SPDX-License-Identifier: MIT
5
- */ // ---------------------------------------------------------------------------
1
+ // ---------------------------------------------------------------------------
6
2
  // Framework-agnostic mock function
7
3
  //
8
4
  // A minimal mock-fn implementation used internally by the testing utilities so
@@ -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);
@@ -31,14 +31,15 @@ const logger = new Logger('MeoCord');
31
31
  * @throws Will exit the process if the name is undefined, invalid,
32
32
  * or the generated class name is invalid.
33
33
  */ function validateAndFormatName(originalName) {
34
+ // Shared by every generator -- controllers, services and guards -- so the messages name none.
34
35
  if (!originalName) {
35
- logger.error('Guard name is required.');
36
+ logger.error('A name is required.');
36
37
  process.exit(1);
37
38
  }
38
39
  const parts = originalName.split('/');
39
40
  const fileName = parts.pop();
40
41
  if (!fileName) {
41
- logger.error('Invalid guard name.');
42
+ logger.error(`Invalid name: "${originalName}".`);
42
43
  process.exit(1);
43
44
  }
44
45
  const kebabCaseName = kebabCase(fileName);
@@ -46,9 +47,39 @@ const logger = new Logger('MeoCord');
46
47
  return {
47
48
  parts,
48
49
  kebabCaseName,
49
- className
50
+ className,
51
+ commandName: commandNameFor(parts, kebabCaseName)
50
52
  };
51
53
  }
54
+ /**
55
+ * The Discord command name a generated controller registers.
56
+ *
57
+ * The whole path, not only its last segment: `admin/ban` becomes `admin-ban`. Files are kept
58
+ * apart by their directories, but a Discord command name is global to the application, so two
59
+ * controllers named `ban` in different folders must not both register `ban`.
60
+ *
61
+ * @param parts - The folders the name is nested in.
62
+ * @param kebabCaseName - The name's last segment, already kebab-cased.
63
+ */ function commandNameFor(parts, kebabCaseName) {
64
+ return [
65
+ ...parts.map((part)=>kebabCase(part)),
66
+ kebabCaseName
67
+ ].filter(Boolean).join('-');
68
+ }
69
+ /**
70
+ * Stops a generator before it writes anything, if any file it would write already exists.
71
+ *
72
+ * Checked for every file up front rather than one at a time, so a refusal never leaves half a
73
+ * component behind, and a file the user has edited is never replaced.
74
+ *
75
+ * @param filePaths - Absolute paths the generator is about to create.
76
+ */ function assertFilesAbsent(filePaths) {
77
+ const existing = filePaths.filter((filePath)=>fs.existsSync(filePath));
78
+ if (existing.length === 0) return;
79
+ const names = existing.map((filePath)=>path.relative(process.cwd(), filePath)).join(', ');
80
+ logger.error(`Refusing to overwrite ${existing.length === 1 ? 'an existing file' : 'existing files'}: ${names}. ` + 'Nothing was generated. Choose another name, or move the existing file first.');
81
+ process.exit(1);
82
+ }
52
83
  /**
53
84
  * Ensures that a given directory exists. Creates the directory and any necessary parent directories if they do not exist.
54
85
  * @param directory - The absolute path of the directory to create.
@@ -60,17 +91,30 @@ const logger = new Logger('MeoCord');
60
91
  }
61
92
  }
62
93
  /**
63
- * Writes the provided content to a file and runs ESLint on the file for formatting.
64
- * @param filePath - The absolute path of the file to create or overwrite.
94
+ * Writes the provided content to a new file and runs ESLint on the file for formatting.
95
+ *
96
+ * Never replaces a file: the write is exclusive, so an existing file is left exactly as it was.
97
+ * Generators check with {@link assertFilesAbsent} before writing anything; this is the last line
98
+ * behind that. A failed write sets a non-zero exit code rather than ending with the success status
99
+ * of a run that produced nothing.
100
+ *
101
+ * @param filePath - The absolute path of the file to create.
65
102
  * @param content - The content to write to the file.
66
- * @throws Logs an error if the file creation or ESLint command fails.
67
103
  */ function generateFile(filePath, content) {
104
+ const relative = path.relative(process.cwd(), filePath);
68
105
  try {
69
- fs.writeFileSync(filePath, content);
70
- logger.log(`Created ${path.relative(process.cwd(), filePath)}`);
106
+ fs.writeFileSync(filePath, content, {
107
+ flag: 'wx'
108
+ });
109
+ logger.log(`Created ${relative}`);
71
110
  formatWithLocalESLint(filePath);
72
111
  } catch (error) {
73
- logger.error(`Failed to create ${filePath}`, error);
112
+ process.exitCode = 1;
113
+ if (error?.code === 'EEXIST') {
114
+ logger.error(`${relative} already exists; left it untouched.`);
115
+ return;
116
+ }
117
+ logger.error(`Failed to create ${relative}`, error);
74
118
  }
75
119
  }
76
120
  /**
@@ -121,4 +165,4 @@ const logger = new Logger('MeoCord');
121
165
  return template;
122
166
  }
123
167
 
124
- export { buildTemplate, createDirectoryIfNotExists, generateFile, populateTemplate, toClassName, validateAndFormatName };
168
+ export { assertFilesAbsent, buildTemplate, commandNameFor, createDirectoryIfNotExists, generateFile, populateTemplate, toClassName, validateAndFormatName };
@@ -1,8 +1,4 @@
1
1
  /**
2
- * MeoCord Framework
3
- * Copyright (c) 2025-present Ukasyah Rahmatullah Zada
4
- * SPDX-License-Identifier: MIT
5
- */ /**
6
2
  * Helper function to fix common JSON formatting issues in tsconfig.json, such as:
7
3
  * - Removing single-line comments.
8
4
  * - Removing trailing commas.