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
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
3
  import '../../common/theme.js';
4
- import { validateAndFormatName, buildTemplate, createDirectoryIfNotExists, generateFile } from '../../util/generator-cli.util.js';
4
+ import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, generateFile } from '../../util/generator-cli.util.js';
5
5
 
6
6
  class ServiceGeneratorHelper {
7
7
  /**
@@ -20,13 +20,19 @@ class ServiceGeneratorHelper {
20
20
  const { parts, kebabCaseName, className } = validateAndFormatName(serviceName);
21
21
  const serviceDir = path.join(process.cwd(), 'src', 'services', ...parts);
22
22
  const serviceFile = path.join(serviceDir, `${kebabCaseName}.service.ts`);
23
+ const specFile = path.join(serviceDir, `${kebabCaseName}.service.spec.ts`);
24
+ // Both files checked before either is written, so an existing service is never replaced.
25
+ assertFilesAbsent([
26
+ serviceFile,
27
+ specFile
28
+ ]);
23
29
  const serviceTemplate = buildTemplate(className, 'service.template');
24
30
  const specTemplate = buildTemplate(className, 'service.spec.template', {
25
31
  kebabCaseName
26
32
  });
27
33
  createDirectoryIfNotExists(serviceDir);
28
34
  generateFile(serviceFile, serviceTemplate);
29
- generateFile(path.join(serviceDir, `${kebabCaseName}.service.spec.ts`), specTemplate);
35
+ generateFile(specFile, specTemplate);
30
36
  }
31
37
  constructor(appName){
32
38
  this.appName = appName;
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'path';
3
- import webpack from 'webpack';
3
+ import { createRsbuild } from '@rsbuild/core';
4
4
  import { Logger } from '../common/logger.js';
5
5
  import '../common/theme.js';
6
6
  import { spawn } from 'node:child_process';
@@ -10,7 +10,6 @@ import { GeneratorCLI } from './generator.js';
10
10
  import { runtimePrefixFor, AppGeneratorHelper } from './helper/app-generator.helper.js';
11
11
  import * as fs from 'node:fs';
12
12
  import { setEnvironment, compileAndValidateConfig, validateDiscordToken } from '../util/common.util.js';
13
- import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
14
13
  import { Command } from 'commander';
15
14
  import { simpleGit } from 'simple-git';
16
15
  import { execSync } from 'child_process';
@@ -21,8 +20,10 @@ import { resolveOwnVersion } from '../util/package-version.util.js';
21
20
  import { buildAppCommand, resolveRuntime } from '../util/runtime.util.js';
22
21
  import packageJson from '../package.json.js';
23
22
  import { fileURLToPath } from 'url';
24
- import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
25
- import nodeExternals from 'webpack-node-externals';
23
+ import { assertNoWebpackHook, createRsbuildConfig, DISCORD_OPTIONAL_NATIVES } from '../build/rsbuild-config.js';
24
+ import { createNativeExternals, nativeCarrier, copyPackagesInto, bundledModuleFiles, assertNoBundledNativeAddons, findBundledNativeAddons } from '../build/native-addons.js';
25
+ import { writePlatformManifest, PLATFORM_MANIFEST } from '../util/platform.util.js';
26
+ import { loadMeoCordSourceConfig } from '../util/meocord-source-config.util.js';
26
27
 
27
28
  const __filename$1 = fileURLToPath(import.meta.url);
28
29
  const __dirname$1 = path.dirname(__filename$1);
@@ -216,6 +217,91 @@ copies or substantial portions of the Software.
216
217
  p.outro(`MeoCord app "${kebabCaseAppName}" is ready!`);
217
218
  }
218
219
  /**
220
+ * Builds the bundler configuration for this project and creates an Rsbuild instance.
221
+ *
222
+ * The base configuration comes from {@link createRsbuildConfig}; the application's
223
+ * `rsbuild` hook, if it declares one, is given the chance to modify it.
224
+ */ async createBundler(mode) {
225
+ // Read from source on every build: the compiled copy in dist is the previous build's.
226
+ const meocordConfig = loadMeoCordSourceConfig();
227
+ assertNoWebpackHook(meocordConfig);
228
+ const base = createRsbuildConfig({
229
+ mode,
230
+ bundleDependencies: meocordConfig?.bundleDependencies,
231
+ externals: meocordConfig?.externals
232
+ });
233
+ const config = meocordConfig?.rsbuild?.(base) ?? base;
234
+ // A native addon cannot be inlined into JavaScript, so when bundling, each one is kept out of
235
+ // the bundle as the bundler meets it and recorded for build() to copy into dist. Added after
236
+ // the application's hook, so a hook that sets externals of its own does not drop it.
237
+ const natives = meocordConfig?.bundleDependencies ? createNativeExternals(this.projectRoot) : undefined;
238
+ if (natives) {
239
+ var _config;
240
+ (_config = config).output ?? (_config.output = {});
241
+ const existing = config.output.externals;
242
+ config.output.externals = [
243
+ ...Array.isArray(existing) ? existing : existing ? [
244
+ existing
245
+ ] : [],
246
+ natives.externals
247
+ ];
248
+ }
249
+ const rsbuild = await createRsbuild({
250
+ cwd: this.projectRoot,
251
+ config
252
+ });
253
+ return {
254
+ rsbuild,
255
+ meocordConfig,
256
+ natives
257
+ };
258
+ }
259
+ /**
260
+ * Makes a bundled build's dist runnable on its own: copies every package the bundle still
261
+ * imports -- the native addons found while building, anything listed in `externals`, and
262
+ * discord.js's optional accelerators if they are installed -- into `dist/node_modules`, and
263
+ * marks dist as ESM. Deploying is then copying dist, with no install step.
264
+ */ packDependencies(meocordConfig, natives) {
265
+ const dist = path.join(this.projectRoot, 'dist');
266
+ const packages = new Map([
267
+ ...natives
268
+ ].map(([name, { dir }])=>[
269
+ name,
270
+ dir
271
+ ]));
272
+ const nativeNames = new Set(natives.keys());
273
+ const listed = (meocordConfig.externals ?? []).filter((item)=>typeof item === 'string');
274
+ for (const name of [
275
+ ...listed,
276
+ ...DISCORD_OPTIONAL_NATIVES
277
+ ]){
278
+ const dir = path.join(this.projectRoot, 'node_modules', name);
279
+ if (packages.has(name) || !fs.existsSync(path.join(dir, 'package.json'))) continue;
280
+ packages.set(name, dir);
281
+ if (nativeCarrier(dir, name, this.projectRoot)) nativeNames.add(name);
282
+ }
283
+ // Replaced rather than merged, so a package dropped from the application does not linger.
284
+ fs.rmSync(path.join(dist, 'node_modules'), {
285
+ recursive: true,
286
+ force: true
287
+ });
288
+ const copied = copyPackagesInto(packages, this.projectRoot, dist);
289
+ fs.writeFileSync(path.join(dist, 'package.json'), `${JSON.stringify({
290
+ type: 'module'
291
+ }, null, 2)}\n`);
292
+ if (nativeNames.size > 0) {
293
+ writePlatformManifest(dist);
294
+ this.logger.info(`Native addons packed into dist: ${[
295
+ ...nativeNames
296
+ ].join(', ')}`);
297
+ } else {
298
+ fs.rmSync(path.join(dist, PLATFORM_MANIFEST), {
299
+ force: true
300
+ });
301
+ }
302
+ if (copied.length > 0) this.logger.info(`dist/node_modules holds ${copied.length} packages; nothing else to install.`);
303
+ }
304
+ /**
219
305
  * Builds the MeoCord application in the specified mode.
220
306
  *
221
307
  * @param mode - The build mode ('production' or 'development').
@@ -223,45 +309,21 @@ copies or substantial portions of the Software.
223
309
  try {
224
310
  this.clearConsole();
225
311
  this.logger.info(`Building ${mode} version...`);
226
- const webpackConfig = (await import(this.webpackConfigPath)).default;
227
- const compiler = webpack({
228
- ...webpackConfig,
229
- mode
230
- });
231
- if (!compiler) {
232
- this.logger.error('Failed to create webpack compiler instance.');
233
- throw new Error('Failed to create webpack compiler instance.');
234
- }
235
- // Workaround for Bun: Keep the event loop alive while webpack runs
236
- // Bun sometimes exits before async callbacks fire
237
- let keepAliveTimer = null;
238
- await new Promise((resolve, reject)=>{
239
- keepAliveTimer = setInterval(()=>{
240
- // Keeps event loop active
241
- }, 100);
242
- compiler.run((err, stats)=>{
243
- if (keepAliveTimer) {
244
- clearInterval(keepAliveTimer);
245
- keepAliveTimer = null;
246
- }
247
- if (err) {
248
- this.logger.error(`Build encountered an error: ${err.message}`);
249
- return reject(`Build encountered an error: ${err.message}`);
250
- }
251
- if (stats?.hasErrors()) {
252
- this.logger.error('Build failed due to errors in the compilation process:', stats.compilation.errors);
253
- } else {
254
- this.logger.info(`${capitalize(mode)} build completed successfully.`);
255
- }
256
- compiler.close((closeErr)=>{
257
- if (closeErr) {
258
- this.logger.error(`Error occurred while closing the compiler: ${closeErr.message}`);
259
- return reject(`Error occurred while closing the compiler: ${closeErr.message}`);
260
- }
261
- resolve();
262
- });
312
+ const { rsbuild, meocordConfig, natives } = await this.createBundler(mode);
313
+ const bundledFiles = [];
314
+ if (meocordConfig?.bundleDependencies) {
315
+ rsbuild.onAfterBuild(({ stats })=>{
316
+ bundledFiles.push(...bundledModuleFiles(stats));
263
317
  });
264
- });
318
+ }
319
+ await rsbuild.build();
320
+ if (meocordConfig?.bundleDependencies && natives) {
321
+ // A safety net behind the externals function: a native addon that still reached the
322
+ // bundle would run on this machine -- through its node_modules -- and fail in production.
323
+ assertNoBundledNativeAddons(findBundledNativeAddons(bundledFiles, this.projectRoot));
324
+ this.packDependencies(meocordConfig, natives.found);
325
+ }
326
+ this.logger.info(`${capitalize(mode)} build completed successfully.`);
265
327
  } catch (error) {
266
328
  this.logger.error(`Build process failed: ${error.message}`);
267
329
  await wait(100);
@@ -275,84 +337,54 @@ copies or substantial portions of the Software.
275
337
  const configPath = path.resolve(this.projectRoot, 'meocord.config.ts');
276
338
  if (!fs.existsSync(configPath)) return;
277
339
  try {
278
- const tsConfigPath = prepareModifiedTsConfig();
279
- const compiler = webpack({
340
+ // Deliberately built without the application's `rsbuild` hook: this compiles the very
341
+ // file that declares that hook, so applying it here would let a config shape its own
342
+ // compilation. It does follow `bundleDependencies`, though: the config imports packages
343
+ // too (dotenv, usually), and a bot bundled to run without node_modules would otherwise
344
+ // fail on its own config file.
345
+ const meocordConfig = loadMeoCordSourceConfig();
346
+ const bundleDependencies = meocordConfig?.bundleDependencies ?? false;
347
+ const base = createRsbuildConfig({
348
+ mode: 'development',
280
349
  entry: configPath,
281
- target: 'node',
282
- mode: 'none',
283
- externals: [
284
- nodeExternals({
285
- importType: 'module'
286
- })
287
- ],
288
- module: {
289
- rules: [
290
- {
291
- test: /\.ts$/,
292
- use: {
293
- loader: 'swc-loader',
294
- options: {
295
- jsc: {
296
- parser: {
297
- syntax: 'typescript',
298
- tsx: false,
299
- decorators: true
300
- },
301
- transform: {
302
- decoratorMetadata: true,
303
- legacyDecorator: true
304
- }
305
- }
306
- }
307
- },
308
- exclude: /node_modules/
350
+ bundleDependencies,
351
+ externals: meocordConfig?.externals
352
+ });
353
+ const rsbuild = await createRsbuild({
354
+ cwd: this.projectRoot,
355
+ config: {
356
+ ...base,
357
+ source: {
358
+ ...base.source,
359
+ entry: {
360
+ 'meocord.config': configPath
309
361
  }
310
- ]
311
- },
312
- resolve: {
313
- extensions: [
314
- '.ts',
315
- '.js'
316
- ],
317
- plugins: [
318
- new TsconfigPathsPlugin({
319
- configFile: tsConfigPath
320
- })
321
- ]
322
- },
323
- output: {
324
- filename: 'meocord.config.mjs',
325
- path: path.resolve(this.projectRoot, 'dist'),
326
- library: {
327
- type: 'module'
362
+ },
363
+ output: {
364
+ ...base.output,
365
+ distPath: {
366
+ root: path.resolve(this.projectRoot, 'dist'),
367
+ js: ''
368
+ },
369
+ filename: {
370
+ js: '[name].mjs'
371
+ },
372
+ minify: {
373
+ js: false
374
+ },
375
+ sourceMap: false,
376
+ // Runs beside the application build in the same dist; cleaning would delete it.
377
+ cleanDistPath: false
328
378
  }
329
- },
330
- experiments: {
331
- outputModule: true
332
- },
333
- optimization: {
334
- minimize: false
335
379
  }
336
380
  });
337
- await new Promise((resolve)=>{
338
- compiler.run((err, stats)=>{
339
- if (err || stats?.hasErrors()) {
340
- this.logger.warn('Failed to compile meocord.config.ts — runtime will fall back to source config.');
341
- resolve();
342
- return;
343
- }
344
- compiler.close((closeErr)=>{
345
- if (closeErr) {
346
- resolve();
347
- return;
348
- }
349
- this.logger.info('Config compiled to dist/meocord.config.mjs');
350
- resolve();
351
- });
352
- });
353
- });
354
- } catch {
355
- this.logger.warn('Failed to compile meocord.config.ts — runtime will fall back to source config.');
381
+ await rsbuild.build();
382
+ this.logger.info('Config compiled to dist/meocord.config.mjs');
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);
356
388
  }
357
389
  }
358
390
  /**
@@ -396,30 +428,22 @@ copies or substantial portions of the Software.
396
428
  this.clearConsole();
397
429
  this.logger.log('Starting watch mode...');
398
430
  await this.compileConfig();
399
- const webpackConfig = (await import(this.webpackConfigPath)).default;
400
- const compiler = webpack({
401
- ...webpackConfig,
402
- mode: 'development'
403
- });
404
- if (!compiler) {
405
- this.logger.error('Failed to create webpack compiler instance.');
406
- await wait(100);
407
- process.exit(1);
408
- }
409
431
  let isRunning = false;
410
- const watch = ()=>compiler.watch({}, (err, stats)=>{
411
- if (err) {
412
- this.logger.error(`Webpack Error: ${err.message}`);
413
- return;
414
- }
415
- if (stats?.hasErrors()) {
416
- this.logger.error('Build failed due to errors in the compilation process:', stats.compilation.errors);
417
- } else {
418
- this.restartApp();
419
- isRunning = true;
420
- }
432
+ let watching;
433
+ const watch = async ()=>{
434
+ const { rsbuild } = await this.createBundler('development');
435
+ // Runs after every rebuild, which is where the application is restarted. A failed
436
+ // rebuild reports its own errors and does not reach here, so the process already
437
+ // running is left alone rather than being replaced by a broken build.
438
+ rsbuild.onAfterBuild(()=>{
439
+ this.restartApp();
440
+ isRunning = true;
441
+ });
442
+ watching = await rsbuild.build({
443
+ watch: true
421
444
  });
422
- watch();
445
+ };
446
+ await watch();
423
447
  let debounceWatcher;
424
448
  const fsWatcher = fs.watch(path.resolve(process.cwd(), 'meocord.config.ts'), ()=>{
425
449
  clearTimeout(debounceWatcher);
@@ -431,8 +455,8 @@ copies or substantial portions of the Software.
431
455
  this.appProcess.kill();
432
456
  this.appProcess = null;
433
457
  }
434
- await new Promise((resolve)=>compiler.close(resolve));
435
- watch();
458
+ await watching?.close();
459
+ await watch();
436
460
  }
437
461
  }, 300);
438
462
  });
@@ -449,11 +473,11 @@ copies or substantial portions of the Software.
449
473
  fsWatcher.close();
450
474
  if (this.appProcess && !this.appProcess.killed) {
451
475
  this.appProcess.on('exit', async ()=>{
452
- await new Promise((resolve)=>compiler.close(resolve));
476
+ await watching?.close();
453
477
  process.exit(0);
454
478
  });
455
479
  } else {
456
- await new Promise((resolve)=>compiler.close(resolve));
480
+ await watching?.close();
457
481
  process.exit(0);
458
482
  }
459
483
  });
@@ -518,7 +542,6 @@ copies or substantial portions of the Software.
518
542
  this.logger = new Logger(this.appName);
519
543
  this.projectRoot = process.cwd();
520
544
  this.mainJSPath = path.join(this.projectRoot, 'dist', 'main.js');
521
- this.webpackConfigPath = path.resolve(__dirname$1, '..', '..', '..', 'webpack.config.js');
522
545
  this.generatorCLI = new GeneratorCLI(this.appName);
523
546
  this.appGeneratorHelper = new AppGeneratorHelper();
524
547
  this.version = resolveOwnVersion(__dirname$1, packageJson.version);