meocord 3.2.2 → 4.0.0-beta.0

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 (52) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +160 -28
  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-EmG1ZEwV.cjs → theme-BXdSoifk.cjs} +4 -15
  7. package/dist/cjs/common/index.cjs +1 -5
  8. package/dist/cjs/core/index.cjs +67 -5
  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 +160 -140
  26. package/dist/esm/build/native-addons.js +235 -0
  27. package/dist/esm/build/rsbuild-config.js +169 -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 +4 -0
  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/generator-cli.util.js +54 -10
  36. package/dist/esm/util/json.util.js +0 -4
  37. package/dist/esm/util/meocord-config-loader.util.js +14 -2
  38. package/dist/esm/util/platform.util.js +66 -0
  39. package/dist/esm/util/runtime.util.js +20 -7
  40. package/dist/esm/util/tsconfig.util.js +1 -1
  41. package/dist/esm/util/wait.util.js +1 -5
  42. package/dist/types/common/index.d.ts +0 -16
  43. package/dist/types/core/index.d.ts +0 -12
  44. package/dist/types/decorator/index.d.ts +1 -31
  45. package/dist/types/enum/index.d.ts +0 -5
  46. package/dist/types/interface/index.d.ts +46 -57
  47. package/dist/types/testing/index.d.ts +4 -19
  48. package/meocord.eslint.cjs +0 -6
  49. package/meocord.eslint.d.ts +0 -6
  50. package/meocord.eslint.mjs +0 -6
  51. package/package.json +19 -23
  52. package/webpack.config.js +0 -147
@@ -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-config-loader.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,82 +337,49 @@ 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
- });
381
+ await rsbuild.build();
382
+ this.logger.info('Config compiled to dist/meocord.config.mjs');
354
383
  } catch {
355
384
  this.logger.warn('Failed to compile meocord.config.ts — runtime will fall back to source config.');
356
385
  }
@@ -396,30 +425,22 @@ copies or substantial portions of the Software.
396
425
  this.clearConsole();
397
426
  this.logger.log('Starting watch mode...');
398
427
  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
428
  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
- }
429
+ let watching;
430
+ const watch = async ()=>{
431
+ const { rsbuild } = await this.createBundler('development');
432
+ // Runs after every rebuild, which is where the application is restarted. A failed
433
+ // rebuild reports its own errors and does not reach here, so the process already
434
+ // running is left alone rather than being replaced by a broken build.
435
+ rsbuild.onAfterBuild(()=>{
436
+ this.restartApp();
437
+ isRunning = true;
438
+ });
439
+ watching = await rsbuild.build({
440
+ watch: true
421
441
  });
422
- watch();
442
+ };
443
+ await watch();
423
444
  let debounceWatcher;
424
445
  const fsWatcher = fs.watch(path.resolve(process.cwd(), 'meocord.config.ts'), ()=>{
425
446
  clearTimeout(debounceWatcher);
@@ -431,8 +452,8 @@ copies or substantial portions of the Software.
431
452
  this.appProcess.kill();
432
453
  this.appProcess = null;
433
454
  }
434
- await new Promise((resolve)=>compiler.close(resolve));
435
- watch();
455
+ await watching?.close();
456
+ await watch();
436
457
  }
437
458
  }, 300);
438
459
  });
@@ -449,11 +470,11 @@ copies or substantial portions of the Software.
449
470
  fsWatcher.close();
450
471
  if (this.appProcess && !this.appProcess.killed) {
451
472
  this.appProcess.on('exit', async ()=>{
452
- await new Promise((resolve)=>compiler.close(resolve));
473
+ await watching?.close();
453
474
  process.exit(0);
454
475
  });
455
476
  } else {
456
- await new Promise((resolve)=>compiler.close(resolve));
477
+ await watching?.close();
457
478
  process.exit(0);
458
479
  }
459
480
  });
@@ -518,7 +539,6 @@ copies or substantial portions of the Software.
518
539
  this.logger = new Logger(this.appName);
519
540
  this.projectRoot = process.cwd();
520
541
  this.mainJSPath = path.join(this.projectRoot, 'dist', 'main.js');
521
- this.webpackConfigPath = path.resolve(__dirname$1, '..', '..', '..', 'webpack.config.js');
522
542
  this.generatorCLI = new GeneratorCLI(this.appName);
523
543
  this.appGeneratorHelper = new AppGeneratorHelper();
524
544
  this.version = resolveOwnVersion(__dirname$1, packageJson.version);
@@ -0,0 +1,235 @@
1
+ import { readdirSync, readFileSync, existsSync, cpSync } from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * The package a resolved module file belongs to, and the directory it was installed in.
6
+ *
7
+ * Takes the innermost `node_modules` segment, so a nested copy is attributed to itself rather
8
+ * than to the package it is nested under. Undefined for application code.
9
+ */ function packageFromPath(file) {
10
+ // A module identifier can carry loaders before the resource (`loader!/path/to/file`) and a
11
+ // query after it; only the path itself says where the package lives.
12
+ const resource = file.slice(file.lastIndexOf('!') + 1).split('?')[0];
13
+ const segments = resource.split(/[\\/]/);
14
+ const index = segments.lastIndexOf('node_modules');
15
+ if (index === -1 || index + 1 >= segments.length) return undefined;
16
+ const scoped = segments[index + 1].startsWith('@');
17
+ if (scoped && index + 2 >= segments.length) return undefined;
18
+ const end = index + (scoped ? 3 : 2);
19
+ return {
20
+ name: segments.slice(index + 1, end).join('/'),
21
+ dir: segments.slice(0, end).join(path.sep)
22
+ };
23
+ }
24
+ /**
25
+ * Every source file a build pulled into its output.
26
+ *
27
+ * Concatenated modules nest their members, and a multi-environment build nests whole
28
+ * compilations as children, so both are walked.
29
+ */ function bundledModuleFiles(stats) {
30
+ if (!stats) return [];
31
+ const json = stats.toJson({
32
+ all: false,
33
+ modules: true,
34
+ nestedModules: true,
35
+ // A production build concatenates most of node_modules into a few modules, and the members
36
+ // of a concatenation are orphans -- hidden from stats unless asked for. Without this every
37
+ // concatenated package, sharp included, was invisible here.
38
+ orphanModules: true,
39
+ modulesSpace: Infinity,
40
+ nestedModulesSpace: Infinity
41
+ });
42
+ const collect = (modules)=>(modules ?? []).flatMap((module)=>[
43
+ module.nameForCondition ?? module.identifier ?? '',
44
+ ...collect(module.modules),
45
+ // Stats may group modules by path, nesting them under `children`.
46
+ ...collect(module.children)
47
+ ]);
48
+ const compilations = json.children?.length ? json.children : [
49
+ json
50
+ ];
51
+ return compilations.flatMap((compilation)=>collect(compilation.modules)).filter(Boolean);
52
+ }
53
+ /** Whether a directory holds a compiled addon, not counting packages nested inside it. */ function containsNativeBinary(dir, depth = 0) {
54
+ if (depth > 5) return false;
55
+ let entries;
56
+ try {
57
+ entries = readdirSync(dir, {
58
+ withFileTypes: true
59
+ });
60
+ } catch {
61
+ return false;
62
+ }
63
+ return entries.some((entry)=>{
64
+ if (entry.isFile()) return entry.name.endsWith('.node');
65
+ if (!entry.isDirectory() || entry.name === 'node_modules') return false;
66
+ return containsNativeBinary(path.join(dir, entry.name), depth + 1);
67
+ });
68
+ }
69
+ /**
70
+ * Where an optional dependency of the package in `from` was installed, if it was.
71
+ *
72
+ * 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.
75
+ */ function resolveDependencyDir(name, from, root) {
76
+ const candidates = [
77
+ path.join(from, 'node_modules', name),
78
+ path.join(path.dirname(from), name),
79
+ path.join(path.dirname(path.dirname(from)), name),
80
+ path.join(root, 'node_modules', name)
81
+ ];
82
+ return candidates.find((dir)=>existsSync(path.join(dir, 'package.json')));
83
+ }
84
+ /** A package's declared dependencies, runtime and optional, or none if it cannot be read. */ function readDependencies(dir) {
85
+ try {
86
+ const pkg = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
87
+ return {
88
+ dependencies: Object.keys(pkg.dependencies ?? {}),
89
+ optional: Object.keys(pkg.optionalDependencies ?? {})
90
+ };
91
+ } catch {
92
+ return {
93
+ dependencies: [],
94
+ optional: []
95
+ };
96
+ }
97
+ }
98
+ /**
99
+ * The package holding the compiled addon an installed package loads, or undefined for plain
100
+ * JavaScript.
101
+ *
102
+ * The binary is rarely in the package that gets imported. node-gyp packages keep it in their own
103
+ * directory, under `build/Release`. napi-rs packages and sharp ship a JavaScript loader and publish
104
+ * each platform's binary as a separate package, declared as an optional dependency so only the
105
+ * matching one installs. Both layouts are checked.
106
+ */ function nativeCarrier(dir, name, root) {
107
+ if (containsNativeBinary(dir)) return name;
108
+ return readDependencies(dir).optional.find((dependency)=>{
109
+ const dependencyDir = resolveDependencyDir(dependency, dir, root);
110
+ return dependencyDir !== undefined && containsNativeBinary(dependencyDir);
111
+ });
112
+ }
113
+ /** Bundled packages that load a native addon, mapped to the package holding the binary. */ function findBundledNativeAddons(bundledFiles, root) {
114
+ const packages = new Map();
115
+ for (const file of bundledFiles){
116
+ const found = packageFromPath(file);
117
+ if (found && !packages.has(found.dir)) packages.set(found.dir, found.name);
118
+ }
119
+ const natives = new Map();
120
+ for (const [dir, name] of packages){
121
+ if (natives.has(name)) continue;
122
+ const carrier = nativeCarrier(dir, name, root);
123
+ if (carrier) natives.set(name, carrier);
124
+ }
125
+ return natives;
126
+ }
127
+ /**
128
+ * The package a bare import request names, or undefined for a relative path, an absolute path, or
129
+ * a Node builtin -- none of which live in node_modules.
130
+ */ function packageNameOfRequest(request) {
131
+ if (!request || request.startsWith('.') || request.startsWith('/') || /^[a-z]+:/i.test(request)) return undefined;
132
+ if (/^[A-Za-z]:[\\/]/.test(request)) return undefined;
133
+ const [first, second] = request.split('/');
134
+ if (!first.startsWith('@')) return first;
135
+ return second ? `${first}/${second}` : undefined;
136
+ }
137
+ /** Where `name` resolves from `context` the way Node would: the nearest node_modules walking up. */ function resolveInstalledPackage(name, context, root) {
138
+ let dir = context;
139
+ for(;;){
140
+ const candidate = path.join(dir, 'node_modules', name);
141
+ if (existsSync(path.join(candidate, 'package.json'))) return candidate;
142
+ const parent = path.dirname(dir);
143
+ if (parent === dir) break;
144
+ dir = parent;
145
+ }
146
+ const hoisted = path.join(root, 'node_modules', name);
147
+ return existsSync(path.join(hoisted, 'package.json')) ? hoisted : undefined;
148
+ }
149
+ /**
150
+ * An Rspack `externals` function that keeps every native addon out of the bundle.
151
+ *
152
+ * Decided while the bundler resolves imports, so it sees every request -- including one made by a
153
+ * plain JavaScript dependency deep in the graph -- and never has to be told what to look for. Each
154
+ * native package it keeps out is recorded in `found`, so the build can copy it into the output.
155
+ */ function createNativeExternals(root) {
156
+ const found = new Map();
157
+ const plain = new Set();
158
+ const externals = ({ request, context }, callback)=>{
159
+ const name = request ? packageNameOfRequest(request) : undefined;
160
+ if (!name || plain.has(name)) return callback();
161
+ if (found.has(name)) return callback(undefined, request);
162
+ const dir = resolveInstalledPackage(name, context ?? root, root);
163
+ const carrier = dir ? nativeCarrier(dir, name, root) : undefined;
164
+ if (!dir || !carrier) {
165
+ plain.add(name);
166
+ return callback();
167
+ }
168
+ found.set(name, {
169
+ dir,
170
+ carrier
171
+ });
172
+ callback(undefined, request);
173
+ };
174
+ return {
175
+ externals,
176
+ found
177
+ };
178
+ }
179
+ /**
180
+ * Copies packages, with everything they need at runtime, into `<outDir>/node_modules`.
181
+ *
182
+ * This is what lets a bundled application run with nothing installed beside it: the packages that
183
+ * could not be bundled travel inside the output directory, where Node finds them by walking up
184
+ * 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.
188
+ *
189
+ * @returns The names copied.
190
+ */ function copyPackagesInto(packages, root, outDir) {
191
+ const copied = new Set();
192
+ const copy = (name, dir)=>{
193
+ if (copied.has(name) || name.startsWith('@types/')) return;
194
+ copied.add(name);
195
+ const target = path.join(outDir, 'node_modules', name);
196
+ // A package's own nested node_modules comes along with it, so only hoisted dependencies
197
+ // need finding separately.
198
+ cpSync(dir, target, {
199
+ recursive: true,
200
+ dereference: true
201
+ });
202
+ const { dependencies, optional } = readDependencies(dir);
203
+ for (const dependency of [
204
+ ...dependencies,
205
+ ...optional
206
+ ]){
207
+ if (existsSync(path.join(dir, 'node_modules', dependency, 'package.json'))) continue;
208
+ const dependencyDir = resolveDependencyDir(dependency, dir, root);
209
+ if (dependencyDir) copy(dependency, dependencyDir);
210
+ }
211
+ };
212
+ for (const [name, dir] of packages)copy(name, dir);
213
+ return [
214
+ ...copied
215
+ ];
216
+ }
217
+ /**
218
+ * Refuses a bundled build that swallowed a native addon.
219
+ *
220
+ * Such a build does not fail on its own. It succeeds, the bot starts, and on the build machine it
221
+ * even works, because the bundle resolves the addon back into that machine's node_modules by
222
+ * absolute path. Anywhere else it fails the first time the addon loads -- often a command, long
223
+ * after startup. Failing the build is the only point this can be caught.
224
+ */ function assertNoBundledNativeAddons(found) {
225
+ if (found.size === 0) return;
226
+ const lines = [
227
+ ...found
228
+ ].map(([bundled, carrier])=>carrier === bundled ? ` - ${bundled}` : ` - ${bundled} (its binary ships in ${carrier})`);
229
+ const names = [
230
+ ...found.keys()
231
+ ].map((name)=>`'${name}'`).join(', ');
232
+ 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
+ }
234
+
235
+ export { assertNoBundledNativeAddons, bundledModuleFiles, copyPackagesInto, createNativeExternals, findBundledNativeAddons, nativeCarrier, packageFromPath, packageNameOfRequest };