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.
- package/CHANGELOG.md +91 -0
- package/README.md +161 -29
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/cjs/_shared/{controller.decorator-eT0oHb8D.cjs → controller.decorator-9nIxOp9y.cjs} +1 -1
- package/dist/cjs/_shared/{metadata-key.enum-DITxfZlw.cjs → metadata-key.enum-aABGdodb.cjs} +0 -4
- package/dist/cjs/_shared/theme-Dqwlnz0m.cjs +124 -0
- package/dist/cjs/common/index.cjs +2 -6
- package/dist/cjs/core/index.cjs +69 -7
- package/dist/cjs/decorator/index.cjs +2 -2
- package/dist/cjs/enum/index.cjs +1 -1
- package/dist/cjs/testing/index.cjs +2 -6
- package/dist/esm/bin/app-template/meocord.config.ts.template +8 -13
- package/dist/esm/bin/app-template/package.json.template +12 -12
- package/dist/esm/bin/app-template/vitest.config.ts.template +6 -2
- package/dist/esm/bin/builder-template/builder/context-menu.builder.template +2 -2
- package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +2 -2
- package/dist/esm/bin/builder-template/builder/slash.builder.template +2 -2
- package/dist/esm/bin/builder-template/controller/autocomplete.controller.template +4 -3
- package/dist/esm/bin/builder-template/controller/context-menu.controller.template +2 -2
- package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.template +2 -2
- package/dist/esm/bin/builder-template/controller/slash.controller.template +2 -2
- package/dist/esm/bin/helper/controller-generator.helper.js +43 -26
- package/dist/esm/bin/helper/guard-generator.helper.js +8 -2
- package/dist/esm/bin/helper/service-generator.helper.js +8 -2
- package/dist/esm/bin/meocord.js +165 -142
- package/dist/esm/build/native-addons.js +274 -0
- package/dist/esm/build/rsbuild-config.js +176 -0
- package/dist/esm/common/decorator.js +0 -4
- package/dist/esm/common/theme.js +1 -5
- package/dist/esm/core/meocord-factory.js +5 -1
- package/dist/esm/enum/controller.enum.js +1 -5
- package/dist/esm/enum/metadata-key.enum.js +0 -4
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/mock-fn.js +1 -5
- package/dist/esm/util/common.util.js +3 -3
- package/dist/esm/util/generator-cli.util.js +54 -10
- package/dist/esm/util/json.util.js +0 -4
- package/dist/esm/util/meocord-config-loader.util.js +19 -51
- package/dist/esm/util/meocord-source-config.util.js +55 -0
- package/dist/esm/util/platform.util.js +66 -0
- package/dist/esm/util/runtime.util.js +20 -7
- package/dist/esm/util/tsconfig.util.js +1 -1
- package/dist/esm/util/wait.util.js +1 -5
- package/dist/types/common/index.d.ts +0 -16
- package/dist/types/core/index.d.ts +0 -12
- package/dist/types/decorator/index.d.ts +1 -31
- package/dist/types/enum/index.d.ts +0 -5
- package/dist/types/interface/index.d.ts +46 -57
- package/dist/types/testing/index.d.ts +4 -19
- package/meocord.eslint.cjs +0 -6
- package/meocord.eslint.d.ts +0 -6
- package/meocord.eslint.mjs +0 -6
- package/package.json +20 -24
- package/dist/cjs/_shared/theme-EmG1ZEwV.cjs +0 -179
- 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 };
|
package/dist/esm/common/theme.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/esm/package.json.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 (!
|
|
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('
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
|
|
106
|
+
fs.writeFileSync(filePath, content, {
|
|
107
|
+
flag: 'wx'
|
|
108
|
+
});
|
|
109
|
+
logger.log(`Created ${relative}`);
|
|
71
110
|
formatWithLocalESLint(filePath);
|
|
72
111
|
} catch (error) {
|
|
73
|
-
|
|
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.
|