@meteorjs/rspack 1.1.0-beta.2 → 1.1.0-beta.21

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.
@@ -0,0 +1,121 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { cleanOmittedPaths } = require("./mergeRulesSplitOverlap.js");
4
+ const { mergeMeteorRspackFragments } = require("./meteorRspackConfigFactory.js");
5
+
6
+ // Helper function to load and process config files
7
+ async function loadAndProcessConfig(configPath, configType, Meteor, argv, disableWarnings) {
8
+ try {
9
+ // Load the config file
10
+ let config;
11
+ if (path.extname(configPath) === '.mjs') {
12
+ // For ESM modules, we need to use dynamic import
13
+ const fileUrl = `file://${configPath}`;
14
+ const module = await import(fileUrl);
15
+ config = module.default || module;
16
+ } else {
17
+ // For CommonJS modules, we can use require
18
+ config = require(configPath)?.default || require(configPath);
19
+ }
20
+
21
+ // Process the config
22
+ const rawConfig = typeof config === 'function' ? config(Meteor, argv) : config;
23
+ const resolvedConfig = await Promise.resolve(rawConfig);
24
+ const userConfig = resolvedConfig && '0' in resolvedConfig ? resolvedConfig[0] : resolvedConfig;
25
+
26
+ // Define omitted paths and warning function
27
+ const omitPaths = [
28
+ "name",
29
+ "target",
30
+ "entry",
31
+ "output.path",
32
+ "output.filename",
33
+ ...(Meteor.isServer ? ["optimization.splitChunks", "optimization.runtimeChunk"] : []),
34
+ ].filter(Boolean);
35
+
36
+ const warningFn = path => {
37
+ if (disableWarnings) return;
38
+ console.warn(
39
+ `[${configType}] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`,
40
+ );
41
+ };
42
+
43
+ // Clean omitted paths and merge Meteor Rspack fragments
44
+ let nextConfig = cleanOmittedPaths(userConfig, {
45
+ omitPaths,
46
+ warningFn,
47
+ });
48
+ nextConfig = mergeMeteorRspackFragments(nextConfig);
49
+
50
+ return nextConfig;
51
+ } catch (error) {
52
+ console.error(`Error loading ${configType} from ${configPath}:`, error);
53
+ if (configType === 'rspack.config.js') {
54
+ throw error; // Only rethrow for project config
55
+ }
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Loads both the user's Rspack configuration and its potential override.
62
+ *
63
+ * @param {string|undefined} projectConfigPath
64
+ * @param {object} Meteor
65
+ * @param {object} argv
66
+ * @returns {Promise<{ nextUserConfig: object|null, nextOverrideConfig: object|null }>}
67
+ */
68
+ async function loadUserAndOverrideConfig(projectConfigPath, Meteor, argv) {
69
+ let nextUserConfig = null;
70
+ let nextOverrideConfig = null;
71
+
72
+ const projectDir = process.cwd();
73
+ const isMeteorPackageConfig = projectDir.includes("/packages/rspack");
74
+
75
+ if (projectConfigPath) {
76
+ const configDir = path.dirname(projectConfigPath);
77
+ const configFileName = path.basename(projectConfigPath);
78
+ const configExt = path.extname(configFileName);
79
+ const configNameWithoutExt = configFileName.replace(configExt, '');
80
+ const configNameFull = `${configNameWithoutExt}.override${configExt}`;
81
+ const overrideConfigPath = path.join(configDir, configNameFull);
82
+
83
+ if (fs.existsSync(overrideConfigPath)) {
84
+ nextOverrideConfig = await loadAndProcessConfig(
85
+ overrideConfigPath,
86
+ configNameFull,
87
+ Meteor,
88
+ argv,
89
+ Meteor.isAngularEnabled
90
+ );
91
+ }
92
+
93
+ if (fs.existsSync(projectConfigPath) && !isMeteorPackageConfig) {
94
+ // Check if there's a .mjs or .cjs version of the config file
95
+ const mjsConfigPath = projectConfigPath.replace(/\.js$/, '.mjs');
96
+ const cjsConfigPath = projectConfigPath.replace(/\.js$/, '.cjs');
97
+
98
+ let projectConfigPathToUse = projectConfigPath;
99
+ if (fs.existsSync(mjsConfigPath)) {
100
+ projectConfigPathToUse = mjsConfigPath;
101
+ } else if (fs.existsSync(cjsConfigPath)) {
102
+ projectConfigPathToUse = cjsConfigPath;
103
+ }
104
+
105
+ nextUserConfig = await loadAndProcessConfig(
106
+ projectConfigPathToUse,
107
+ 'rspack.config.js',
108
+ Meteor,
109
+ argv,
110
+ Meteor.isAngularEnabled
111
+ );
112
+ }
113
+ }
114
+
115
+ return { nextUserConfig, nextOverrideConfig };
116
+ }
117
+
118
+ module.exports = {
119
+ loadAndProcessConfig,
120
+ loadUserAndOverrideConfig,
121
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "1.1.0-beta.2",
3
+ "version": "1.1.0-beta.21",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
package/rspack.config.js CHANGED
@@ -12,7 +12,6 @@ const { RequireExternalsPlugin } = require('./plugins/RequireExtenalsPlugin.js')
12
12
  const { AssetExternalsPlugin } = require('./plugins/AssetExternalsPlugin.js');
13
13
  const { generateEagerTestFile } = require("./lib/test.js");
14
14
  const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore");
15
- const { mergeMeteorRspackFragments } = require("./lib/meteorRspackConfigFactory.js");
16
15
  const {
17
16
  compileWithMeteor,
18
17
  compileWithRspack,
@@ -22,6 +21,7 @@ const {
22
21
  makeWebNodeBuiltinsAlias,
23
22
  disablePlugins,
24
23
  } = require('./lib/meteorRspackHelpers.js');
24
+ const { loadUserAndOverrideConfig } = require('./lib/meteorRspackConfigHelpers.js');
25
25
  const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory");
26
26
 
27
27
  // Safe require that doesn't throw if the module isn't found
@@ -204,15 +204,39 @@ module.exports = async function (inMeteor = {}, argv = {}) {
204
204
  const Meteor = { ...inMeteor };
205
205
  // Convert string boolean values to actual booleans
206
206
  for (const key in Meteor) {
207
- if (Meteor[key] === 'true' || Meteor[key] === true) {
207
+ if (Meteor[key] === "true" || Meteor[key] === true) {
208
208
  Meteor[key] = true;
209
- } else if (Meteor[key] === 'false' || Meteor[key] === false) {
209
+ } else if (Meteor[key] === "false" || Meteor[key] === false) {
210
210
  Meteor[key] = false;
211
211
  }
212
212
  }
213
213
 
214
- const isProd = !!Meteor.isProduction || argv.mode === 'production';
215
- const isDev = !!Meteor.isDevelopment || !isProd;
214
+ const isTestLike = !!Meteor.isTestLike;
215
+ const swcExternalHelpers = !!Meteor.swcExternalHelpers;
216
+ const isNative = !!Meteor.isNative;
217
+
218
+ const projectDir = process.cwd();
219
+ const projectConfigPath =
220
+ Meteor.projectConfigPath || path.resolve(projectDir, "rspack.config.js");
221
+
222
+ // Compute build paths before loading user config (needed by Meteor helpers below)
223
+ const buildContext = Meteor.buildContext || "_build";
224
+ const outputPath = Meteor.outputPath;
225
+ const outputDir = path.dirname(Meteor.outputPath || "");
226
+ Meteor.buildOutputDir = path.resolve(projectDir, buildContext, outputDir);
227
+
228
+ // Defined here so it can be called both before and after the first config load;
229
+ // without loaded configs it falls through to argv/Meteor flags.
230
+ const getModeFromConfig = (userConfig = undefined, overrideConfig = undefined) => {
231
+ if (overrideConfig?.mode) return overrideConfig.mode;
232
+ if (userConfig?.mode) return userConfig.mode;
233
+ if (argv.mode) return argv.mode;
234
+ if (Meteor.isProduction) return "production";
235
+ if (Meteor.isDevelopment) return "development";
236
+ return null;
237
+ };
238
+
239
+ // Meteor flags derived purely from input; independent of loaded user/override configs
216
240
  const isTest = !!Meteor.isTest;
217
241
  const isClient = !!Meteor.isClient;
218
242
  const isServer = !!Meteor.isServer;
@@ -222,74 +246,54 @@ module.exports = async function (inMeteor = {}, argv = {}) {
222
246
  const isTestModule = !!Meteor.isTestModule;
223
247
  const isTestEager = !!Meteor.isTestEager;
224
248
  const isTestFullApp = !!Meteor.isTestFullApp;
225
- const isTestLike = !!Meteor.isTestLike;
226
- const swcExternalHelpers = !!Meteor.swcExternalHelpers;
227
- const isNative = !!Meteor.isNative;
228
- const mode = isProd ? 'production' : 'development';
229
- const projectDir = process.cwd();
230
- const projectConfigPath = Meteor.projectConfigPath || path.resolve(projectDir, 'rspack.config.js');
231
- const configPath = Meteor.configPath;
232
- const testEntry = Meteor.testEntry;
233
-
234
249
  const isTypescriptEnabled = Meteor.isTypescriptEnabled || false;
235
- const isJsxEnabled =
236
- Meteor.isJsxEnabled || (!isTypescriptEnabled && isReactEnabled) || false;
237
- const isTsxEnabled =
238
- Meteor.isTsxEnabled || (isTypescriptEnabled && isReactEnabled) || false;
250
+ const isJsxEnabled = Meteor.isJsxEnabled || (!isTypescriptEnabled && isReactEnabled) || false;
251
+ const isTsxEnabled = Meteor.isTsxEnabled || (isTypescriptEnabled && isReactEnabled) || false;
239
252
  const isBundleVisualizerEnabled = Meteor.isBundleVisualizerEnabled || false;
240
253
  const isAngularEnabled = Meteor.isAngularEnabled || false;
254
+ const enableSwcExternalHelpers = !isServer && swcExternalHelpers;
241
255
 
242
- // Determine entry points
243
- const entryPath = Meteor.entryPath;
244
-
245
- // Determine output points
246
- const outputPath = Meteor.outputPath;
247
- const outputDir = path.dirname(Meteor.outputPath || '');
248
-
249
- const outputFilename = Meteor.outputFilename;
250
-
251
- // Determine run point
252
- const runPath = Meteor.runPath;
253
-
254
- // Determine banner
255
- const bannerOutput = JSON.parse(Meteor.bannerOutput || process.env.RSPACK_BANNER || '""');
256
-
257
- // Determine output directories
258
- const clientOutputDir = path.resolve(projectDir, 'public');
259
- const serverOutputDir = path.resolve(projectDir, 'private');
260
-
261
- // Determine context for bundles and assets
262
- const buildContext = Meteor.buildContext || '_build';
263
- const assetsContext = Meteor.assetsContext || 'build-assets';
264
- const chunksContext = Meteor.chunksContext || 'build-chunks';
265
-
266
- // Determine build output and pass to Meteor
267
- const buildOutputDir = path.resolve(projectDir, buildContext, outputDir);
268
- Meteor.buildOutputDir = buildOutputDir;
269
-
270
- const cacheStrategy = createCacheStrategy(
271
- mode,
272
- (Meteor.isClient && 'client') || 'server',
273
- { projectConfigPath, configPath }
256
+ // Initial mode before user/override configs are loaded
257
+ const initialCurrentMode = getModeFromConfig();
258
+ const initialIsProd = initialCurrentMode ? initialCurrentMode === "production" : !!Meteor.isProduction;
259
+ const initialIsDev = initialCurrentMode ? initialCurrentMode === "development" : !!Meteor.isDevelopment || !initialIsProd;
260
+ const initialMode = initialIsProd ? "production" : "development";
261
+
262
+ // Initialized with pre-load values so helpers work during the first config load;
263
+ // reassigned after load once mode is fully resolved.
264
+ let cacheStrategy = createCacheStrategy(
265
+ initialMode,
266
+ (isClient && "client") || "server",
267
+ { projectConfigPath, configPath: Meteor.configPath }
274
268
  );
269
+ let swcConfigRule = createSwcConfig({
270
+ isTypescriptEnabled,
271
+ isReactEnabled,
272
+ isJsxEnabled,
273
+ isTsxEnabled,
274
+ externalHelpers: enableSwcExternalHelpers,
275
+ isDevEnvironment: isRun && initialIsDev && !isTest && !isNative,
276
+ isClient,
277
+ isAngularEnabled,
278
+ });
279
+ Meteor.swcConfigOptions = swcConfigRule.options;
275
280
 
276
281
  // Expose Meteor's helpers to expand Rspack configs
277
- Meteor.compileWithMeteor = deps => compileWithMeteor(deps);
282
+ Meteor.compileWithMeteor = (deps) => compileWithMeteor(deps);
278
283
  Meteor.compileWithRspack = (deps, options = {}) =>
279
284
  compileWithRspack(deps, {
280
285
  options: mergeSplitOverlap(Meteor.swcConfigOptions, options),
281
286
  });
282
- Meteor.setCache = enabled =>
283
- setCache(
284
- !!enabled,
285
- enabled === 'memory' ? undefined : cacheStrategy
286
- );
287
+ Meteor.setCache = (enabled) =>
288
+ setCache(!!enabled, enabled === "memory" ? undefined : cacheStrategy);
287
289
  Meteor.splitVendorChunk = () => splitVendorChunk();
288
- Meteor.extendSwcConfig = (customSwcConfig) => extendSwcConfig(customSwcConfig);
290
+ Meteor.extendSwcConfig = (customSwcConfig) =>
291
+ extendSwcConfig(customSwcConfig);
289
292
  Meteor.extendConfig = (...configs) => mergeSplitOverlap(...configs);
290
- Meteor.disablePlugins = matchers => prepareMeteorRspackConfig({
291
- disablePlugins: matchers,
292
- });
293
+ Meteor.disablePlugins = (matchers) =>
294
+ prepareMeteorRspackConfig({
295
+ disablePlugins: matchers,
296
+ });
293
297
 
294
298
  // Add HtmlRspackPlugin function to Meteor
295
299
  Meteor.HtmlRspackPlugin = (options = {}) => {
@@ -313,6 +317,51 @@ module.exports = async function (inMeteor = {}, argv = {}) {
313
317
  });
314
318
  };
315
319
 
320
+ // First pass: resolve user/override configs early so mode overrides (e.g. "production")
321
+ // are available before computing isProd/isDev and the rest of the build flags.
322
+ let { nextUserConfig, nextOverrideConfig } =
323
+ await loadUserAndOverrideConfig(projectConfigPath, Meteor, argv);
324
+
325
+ // Determine the final mode with loaded configs
326
+ const currentMode = getModeFromConfig(nextUserConfig, nextOverrideConfig);
327
+ const isProd = currentMode
328
+ ? currentMode === "production"
329
+ : !!Meteor.isProduction;
330
+ const isDev = currentMode
331
+ ? currentMode === "development"
332
+ : !!Meteor.isDevelopment || !isProd;
333
+ const mode = isProd ? "production" : "development";
334
+ const configPath = Meteor.configPath;
335
+ const testEntry = Meteor.testEntry;
336
+
337
+ // Determine entry points
338
+ const entryPath = Meteor.entryPath || "";
339
+
340
+ // Determine output points
341
+ const outputFilename = Meteor.outputFilename;
342
+
343
+ // Determine context for bundles and assets
344
+ const assetsContext = Meteor.assetsContext || "build-assets";
345
+ const chunksContext = Meteor.chunksContext || "build-chunks";
346
+
347
+ cacheStrategy = createCacheStrategy(
348
+ mode,
349
+ (Meteor.isClient && "client") || "server",
350
+ { projectConfigPath, configPath }
351
+ );
352
+
353
+ // Determine run point
354
+ const runPath = Meteor.runPath || "";
355
+
356
+ // Determine banner
357
+ const bannerOutput = JSON.parse(
358
+ Meteor.bannerOutput || process.env.RSPACK_BANNER || '""'
359
+ );
360
+
361
+ // Determine output directories
362
+ const clientOutputDir = path.resolve(projectDir, "public");
363
+ const serverOutputDir = path.resolve(projectDir, "private");
364
+
316
365
  // Get Meteor ignore entries
317
366
  const meteorIgnoreEntries = getMeteorIgnoreEntries(projectDir);
318
367
 
@@ -328,21 +377,17 @@ module.exports = async function (inMeteor = {}, argv = {}) {
328
377
  // Set default watch options
329
378
  const watchOptions = {
330
379
  ignored: [
331
- ...createIgnoreGlobConfig([
332
- ...meteorIgnoreEntries,
333
- ...additionalEntries,
334
- ]),
380
+ ...createIgnoreGlobConfig([...meteorIgnoreEntries, ...additionalEntries]),
335
381
  ],
336
382
  };
337
383
 
338
384
  if (Meteor.isDebug || Meteor.isVerbose) {
339
- console.log('[i] Rspack mode:', mode);
340
- console.log('[i] Meteor flags:', Meteor);
385
+ console.log("[i] Rspack mode:", mode);
386
+ console.log("[i] Meteor flags:", Meteor);
341
387
  }
342
388
 
343
- const enableSwcExternalHelpers = !isServer && swcExternalHelpers;
344
389
  const isDevEnvironment = isRun && isDev && !isTest && !isNative;
345
- const swcConfigRule = createSwcConfig({
390
+ swcConfigRule = createSwcConfig({
346
391
  isTypescriptEnabled,
347
392
  isReactEnabled,
348
393
  isJsxEnabled,
@@ -352,7 +397,6 @@ module.exports = async function (inMeteor = {}, argv = {}) {
352
397
  isClient,
353
398
  isAngularEnabled,
354
399
  });
355
- // Expose swc config to use in custom configs
356
400
  Meteor.swcConfigOptions = swcConfigRule.options;
357
401
 
358
402
  const externals = [
@@ -361,34 +405,34 @@ module.exports = async function (inMeteor = {}, argv = {}) {
361
405
  ...(isServer ? [/^bcrypt$/] : []),
362
406
  ];
363
407
  const alias = {
364
- '/': path.resolve(process.cwd()),
408
+ "/": path.resolve(process.cwd()),
365
409
  };
366
410
  const fallback = {
367
411
  ...(isClient && makeWebNodeBuiltinsAlias()),
368
412
  };
369
413
  const extensions = [
370
- '.ts',
371
- '.tsx',
372
- '.mts',
373
- '.cts',
374
- '.js',
375
- '.jsx',
376
- '.mjs',
377
- '.cjs',
378
- '.json',
379
- '.wasm',
414
+ ".ts",
415
+ ".tsx",
416
+ ".mts",
417
+ ".cts",
418
+ ".js",
419
+ ".jsx",
420
+ ".mjs",
421
+ ".cjs",
422
+ ".json",
423
+ ".wasm",
380
424
  ];
381
425
  const extraRules = [];
382
426
 
383
427
  const reactRefreshModule = isReactEnabled
384
- ? safeRequire('@rspack/plugin-react-refresh')
428
+ ? safeRequire("@rspack/plugin-react-refresh")
385
429
  : null;
386
430
 
387
431
  const requireExternalsPlugin = new RequireExternalsPlugin({
388
432
  filePath: path.join(buildContext, runPath),
389
433
  ...(Meteor.isBlazeEnabled && {
390
434
  externals: /\.html$/,
391
- isEagerImport: module => module.endsWith('.html'),
435
+ isEagerImport: (module) => module.endsWith(".html"),
392
436
  ...(isProd && {
393
437
  lastImports: [`./${outputFilename}`],
394
438
  }),
@@ -398,25 +442,26 @@ module.exports = async function (inMeteor = {}, argv = {}) {
398
442
 
399
443
  // Handle assets
400
444
  const assetExternalsPlugin = new AssetExternalsPlugin();
401
- const assetModuleFilename = _fileInfo => {
445
+ const assetModuleFilename = (_fileInfo) => {
402
446
  const filename = _fileInfo.filename;
403
- const isPublic = filename.startsWith('/') || filename.startsWith('public');
447
+ const isPublic = filename.startsWith("/") || filename.startsWith("public");
404
448
  if (isPublic) return `[name][ext][query]`;
405
449
  return `${assetsContext}/[hash][ext][query]`;
406
450
  };
407
451
 
408
452
  const rsdoctorModule = isBundleVisualizerEnabled
409
- ? safeRequire('@rsdoctor/rspack-plugin')
453
+ ? safeRequire("@rsdoctor/rspack-plugin")
410
454
  : null;
411
- const doctorPluginConfig = isRun && isBundleVisualizerEnabled && rsdoctorModule?.RsdoctorRspackPlugin
412
- ? [
413
- new rsdoctorModule.RsdoctorRspackPlugin({
414
- port: isClient
415
- ? (parseInt(Meteor.rsdoctorClientPort || '8888', 10))
416
- : (parseInt(Meteor.rsdoctorServerPort || '8889', 10)),
417
- }),
418
- ]
419
- : [];
455
+ const doctorPluginConfig =
456
+ isRun && isBundleVisualizerEnabled && rsdoctorModule?.RsdoctorRspackPlugin
457
+ ? [
458
+ new rsdoctorModule.RsdoctorRspackPlugin({
459
+ port: isClient
460
+ ? parseInt(Meteor.rsdoctorClientPort || "8888", 10)
461
+ : parseInt(Meteor.rsdoctorServerPort || "8889", 10),
462
+ }),
463
+ ]
464
+ : [];
420
465
  const bannerPluginConfig = !isBuild
421
466
  ? [
422
467
  new BannerPlugin({
@@ -452,16 +497,11 @@ module.exports = async function (inMeteor = {}, argv = {}) {
452
497
  : isClient && isTest && testEntry
453
498
  ? path.resolve(process.cwd(), testEntry)
454
499
  : path.resolve(process.cwd(), buildContext, entryPath);
455
- console.log(
456
- "--> (rspack.config.js-Line: 431)\n clientEntry: ",
457
- clientEntry,
458
- entryPath
459
- );
460
- const clientNameConfig = `[${(isTest && 'test-') || ''}client-rspack]`;
500
+ const clientNameConfig = `[${(isTest && "test-") || ""}client-rspack]`;
461
501
  // Base client config
462
502
  let clientConfig = {
463
503
  name: clientNameConfig,
464
- target: 'web',
504
+ target: "web",
465
505
  mode,
466
506
  entry: clientEntry,
467
507
  output: {
@@ -470,7 +510,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
470
510
  const chunkName = _module.chunk?.name;
471
511
  const isMainChunk = !chunkName || chunkName === "main";
472
512
  const chunkSuffix = `${chunksContext}/[id]${
473
- isProd ? '.[chunkhash]' : ''
513
+ isProd ? ".[chunkhash]" : ""
474
514
  }.js`;
475
515
  if (isDevEnvironment) {
476
516
  if (isMainChunk) return outputFilename;
@@ -479,21 +519,21 @@ module.exports = async function (inMeteor = {}, argv = {}) {
479
519
  if (isMainChunk) return `../${buildContext}/${outputPath}`;
480
520
  return chunkSuffix;
481
521
  },
482
- libraryTarget: 'commonjs2',
483
- publicPath: '/',
484
- chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
522
+ libraryTarget: "commonjs2",
523
+ publicPath: "/",
524
+ chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
485
525
  assetModuleFilename,
486
526
  cssFilename: `${chunksContext}/[name]${
487
- isProd ? '.[contenthash]' : ''
527
+ isProd ? ".[contenthash]" : ""
488
528
  }.css`,
489
529
  cssChunkFilename: `${chunksContext}/[id]${
490
- isProd ? '.[contenthash]' : ''
530
+ isProd ? ".[contenthash]" : ""
491
531
  }.css`,
492
532
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
493
533
  },
494
534
  optimization: {
495
535
  usedExports: true,
496
- splitChunks: { chunks: 'async' },
536
+ splitChunks: { chunks: "async" },
497
537
  },
498
538
  module: {
499
539
  rules: [
@@ -502,7 +542,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
502
542
  ? [
503
543
  {
504
544
  test: /\.html$/i,
505
- loader: 'ignore-loader',
545
+ loader: "ignore-loader",
506
546
  },
507
547
  ]
508
548
  : []),
@@ -520,33 +560,36 @@ module.exports = async function (inMeteor = {}, argv = {}) {
520
560
  assetExternalsPlugin,
521
561
  ].filter(Boolean),
522
562
  new DefinePlugin({
523
- 'Meteor.isClient': JSON.stringify(true),
524
- 'Meteor.isServer': JSON.stringify(false),
525
- 'Meteor.isTest': JSON.stringify(isTestLike && !isTestFullApp),
526
- 'Meteor.isAppTest': JSON.stringify(isTestLike && isTestFullApp),
527
- 'Meteor.isDevelopment': JSON.stringify(isDev),
528
- 'Meteor.isProduction': JSON.stringify(isProd),
563
+ "Meteor.isClient": JSON.stringify(true),
564
+ "Meteor.isServer": JSON.stringify(false),
565
+ "Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp),
566
+ "Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp),
567
+ "Meteor.isDevelopment": JSON.stringify(isDev),
568
+ "Meteor.isProduction": JSON.stringify(isProd),
529
569
  }),
530
570
  ...bannerPluginConfig,
531
571
  Meteor.HtmlRspackPlugin(),
532
572
  ...doctorPluginConfig,
533
573
  new NormalModuleReplacementPlugin(/^node:(.*)$/, (res) => {
534
- res.request = res.request.replace(/^node:/, '');
574
+ res.request = res.request.replace(/^node:/, "");
535
575
  }),
536
576
  ],
537
577
  watchOptions,
538
- devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
578
+ devtool:
579
+ isDevEnvironment || isNative || isTest
580
+ ? "source-map"
581
+ : "hidden-source-map",
539
582
  ...(isDevEnvironment && {
540
583
  devServer: {
541
584
  ...createRemoteDevServerConfig(),
542
- static: { directory: clientOutputDir, publicPath: '/__rspack__/' },
585
+ static: { directory: clientOutputDir, publicPath: "/__rspack__/" },
543
586
  hot: true,
544
587
  liveReload: true,
545
588
  ...(Meteor.isBlazeEnabled && { hot: false }),
546
589
  port: Meteor.devServerPort || 8080,
547
590
  devMiddleware: {
548
- writeToDisk: filePath =>
549
- /\.(html)$/.test(filePath) && !filePath.includes('.hot-update.'),
591
+ writeToDisk: (filePath) =>
592
+ /\.(html)$/.test(filePath) && !filePath.includes(".hot-update."),
550
593
  },
551
594
  },
552
595
  }),
@@ -576,23 +619,18 @@ module.exports = async function (inMeteor = {}, argv = {}) {
576
619
  : isServer && isTest && testEntry
577
620
  ? path.resolve(process.cwd(), testEntry)
578
621
  : path.resolve(projectDir, buildContext, entryPath);
579
- const serverNameConfig = `[${(isTest && 'test-') || ''}server-rspack]`;
580
- console.log(
581
- "--> (rspack.config.js-Line: 576)\n serverEntry: ",
582
- serverEntry,
583
- entryPath
584
- );
622
+ const serverNameConfig = `[${(isTest && "test-") || ""}server-rspack]`;
585
623
  // Base server config
586
624
  let serverConfig = {
587
625
  name: serverNameConfig,
588
- target: 'node',
626
+ target: "node",
589
627
  mode,
590
628
  entry: serverEntry,
591
629
  output: {
592
630
  path: serverOutputDir,
593
631
  filename: () => `../${buildContext}/${outputPath}`,
594
- libraryTarget: 'commonjs2',
595
- chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
632
+ libraryTarget: "commonjs2",
633
+ chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
596
634
  assetModuleFilename,
597
635
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
598
636
  },
@@ -606,15 +644,15 @@ module.exports = async function (inMeteor = {}, argv = {}) {
606
644
  parser: {
607
645
  javascript: {
608
646
  // Dynamic imports on the server are treated as bundled in the same chunk
609
- dynamicImportMode: 'eager',
647
+ dynamicImportMode: "eager",
610
648
  },
611
649
  },
612
650
  },
613
651
  resolve: {
614
652
  extensions,
615
653
  alias,
616
- modules: ['node_modules', path.resolve(projectDir)],
617
- conditionNames: ['import', 'require', 'node', 'default'],
654
+ modules: ["node_modules", path.resolve(projectDir)],
655
+ conditionNames: ["import", "require", "node", "default"],
618
656
  },
619
657
  externals,
620
658
  externalsPresets: { node: true },
@@ -622,18 +660,18 @@ module.exports = async function (inMeteor = {}, argv = {}) {
622
660
  new DefinePlugin(
623
661
  isTest && (isTestModule || isTestEager)
624
662
  ? {
625
- 'Meteor.isTest': JSON.stringify(isTest && !isTestFullApp),
626
- 'Meteor.isAppTest': JSON.stringify(isTest && isTestFullApp),
627
- 'Meteor.isDevelopment': JSON.stringify(isDev),
663
+ "Meteor.isTest": JSON.stringify(isTest && !isTestFullApp),
664
+ "Meteor.isAppTest": JSON.stringify(isTest && isTestFullApp),
665
+ "Meteor.isDevelopment": JSON.stringify(isDev),
628
666
  }
629
667
  : {
630
- 'Meteor.isClient': JSON.stringify(false),
631
- 'Meteor.isServer': JSON.stringify(true),
632
- 'Meteor.isTest': JSON.stringify(isTestLike && !isTestFullApp),
633
- 'Meteor.isAppTest': JSON.stringify(isTestLike && isTestFullApp),
634
- 'Meteor.isDevelopment': JSON.stringify(isDev),
635
- 'Meteor.isProduction': JSON.stringify(isProd),
636
- },
668
+ "Meteor.isClient": JSON.stringify(false),
669
+ "Meteor.isServer": JSON.stringify(true),
670
+ "Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp),
671
+ "Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp),
672
+ "Meteor.isDevelopment": JSON.stringify(isDev),
673
+ "Meteor.isProduction": JSON.stringify(isProd),
674
+ }
637
675
  ),
638
676
  ...bannerPluginConfig,
639
677
  requireExternalsPlugin,
@@ -641,99 +679,15 @@ module.exports = async function (inMeteor = {}, argv = {}) {
641
679
  ...doctorPluginConfig,
642
680
  ],
643
681
  watchOptions,
644
- devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
682
+ devtool:
683
+ isDevEnvironment || isNative || isTest
684
+ ? "source-map"
685
+ : "hidden-source-map",
645
686
  ...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
646
687
  cacheStrategy),
647
688
  ...lazyCompilationConfig,
648
689
  };
649
690
 
650
- // Helper function to load and process config files
651
- async function loadAndProcessConfig(configPath, configType, Meteor, argv, isAngularEnabled) {
652
- try {
653
- // Load the config file
654
- let config;
655
- if (path.extname(configPath) === '.mjs') {
656
- // For ESM modules, we need to use dynamic import
657
- const fileUrl = `file://${configPath}`;
658
- const module = await import(fileUrl);
659
- config = module.default || module;
660
- } else {
661
- // For CommonJS modules, we can use require
662
- config = require(configPath)?.default || require(configPath);
663
- }
664
-
665
- // Process the config
666
- const rawConfig = typeof config === 'function' ? config(Meteor, argv) : config;
667
- const resolvedConfig = await Promise.resolve(rawConfig);
668
- const userConfig = resolvedConfig && '0' in resolvedConfig ? resolvedConfig[0] : resolvedConfig;
669
-
670
- // Define omitted paths and warning function
671
- const omitPaths = [
672
- "name",
673
- "target",
674
- "entry",
675
- "output.path",
676
- "output.filename",
677
- ...(Meteor.isServer ? ["optimization.splitChunks", "optimization.runtimeChunk"] : []),
678
- ].filter(Boolean);
679
-
680
- const warningFn = path => {
681
- if (isAngularEnabled) return;
682
- console.warn(
683
- `[${configType}] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`,
684
- );
685
- };
686
-
687
- // Clean omitted paths and merge Meteor Rspack fragments
688
- let nextConfig = cleanOmittedPaths(userConfig, {
689
- omitPaths,
690
- warningFn,
691
- });
692
- nextConfig = mergeMeteorRspackFragments(nextConfig);
693
-
694
- return nextConfig;
695
- } catch (error) {
696
- console.error(`Error loading ${configType} from ${configPath}:`, error);
697
- if (configType === 'rspack.config.js') {
698
- throw error; // Only rethrow for project config
699
- }
700
- return null;
701
- }
702
- }
703
-
704
- // Load and apply project-level overrides for the selected build
705
- // Check if we're in a Meteor package directory by looking at the path
706
- const isMeteorPackageConfig = projectDir.includes('/packages/rspack');
707
- if (fs.existsSync(projectConfigPath) && !isMeteorPackageConfig) {
708
- // Check if there's a .mjs or .cjs version of the config file
709
- const mjsConfigPath = projectConfigPath.replace(/\.js$/, '.mjs');
710
- const cjsConfigPath = projectConfigPath.replace(/\.js$/, '.cjs');
711
-
712
- let projectConfigPathToUse = projectConfigPath;
713
- if (fs.existsSync(mjsConfigPath)) {
714
- projectConfigPathToUse = mjsConfigPath;
715
- } else if (fs.existsSync(cjsConfigPath)) {
716
- projectConfigPathToUse = cjsConfigPath;
717
- }
718
-
719
- const nextUserConfig = await loadAndProcessConfig(
720
- projectConfigPathToUse,
721
- 'rspack.config.js',
722
- Meteor,
723
- argv,
724
- isAngularEnabled
725
- );
726
-
727
- if (nextUserConfig) {
728
- if (Meteor.isClient) {
729
- clientConfig = mergeSplitOverlap(clientConfig, nextUserConfig);
730
- }
731
- if (Meteor.isServer) {
732
- serverConfig = mergeSplitOverlap(serverConfig, nextUserConfig);
733
- }
734
- }
735
- }
736
-
737
691
  // Establish Angular overrides to ensure proper integration
738
692
  const angularExpandConfig = isAngularEnabled
739
693
  ? {
@@ -765,35 +719,25 @@ module.exports = async function (inMeteor = {}, argv = {}) {
765
719
  }
766
720
  : {};
767
721
 
722
+ // Second pass: re-run only when a mode override was detected, so the user config
723
+ // can depend on fully-computed Meteor flags and helpers (swcConfigOptions, buildOutputDir, etc.).
724
+ if (nextUserConfig?.mode || nextOverrideConfig?.mode) {
725
+ ({ nextUserConfig, nextOverrideConfig } =
726
+ await loadUserAndOverrideConfig(projectConfigPath, Meteor, argv));
727
+ }
728
+
768
729
  let config = mergeSplitOverlap(
769
730
  isClient ? clientConfig : serverConfig,
770
731
  angularExpandConfig
771
732
  );
772
733
  config = mergeSplitOverlap(config, testClientExpandConfig);
773
734
 
774
- // Check for override config file (extra file to override everything)
775
- if (projectConfigPath) {
776
- const configDir = path.dirname(projectConfigPath);
777
- const configFileName = path.basename(projectConfigPath);
778
- const configExt = path.extname(configFileName);
779
- const configNameWithoutExt = configFileName.replace(configExt, '');
780
- const configNameFull = `${configNameWithoutExt}.override${configExt}`;
781
- const overrideConfigPath = path.join(configDir, configNameFull);
782
-
783
- if (fs.existsSync(overrideConfigPath)) {
784
- const nextOverrideConfig = await loadAndProcessConfig(
785
- overrideConfigPath,
786
- configNameFull,
787
- Meteor,
788
- argv,
789
- isAngularEnabled
790
- );
791
-
792
- if (nextOverrideConfig) {
793
- // Apply override config as the last step
794
- config = mergeSplitOverlap(config, nextOverrideConfig);
795
- }
796
- }
735
+ if (nextUserConfig) {
736
+ config = mergeSplitOverlap(config, nextUserConfig);
737
+ }
738
+
739
+ if (nextOverrideConfig) {
740
+ config = mergeSplitOverlap(config, nextOverrideConfig);
797
741
  }
798
742
 
799
743
  const shouldDisablePlugins = config?.disablePlugins != null;
@@ -803,15 +747,18 @@ module.exports = async function (inMeteor = {}, argv = {}) {
803
747
  }
804
748
 
805
749
  if (Meteor.isDebug || Meteor.isVerbose) {
806
- console.log('Config:', inspect(config, { depth: null, colors: true }));
750
+ console.log("Config:", inspect(config, { depth: null, colors: true }));
807
751
  }
808
752
 
809
753
  // Check if lazyCompilation is enabled and warn the user
810
- if (config.lazyCompilation === true || typeof config.lazyCompilation === 'object') {
754
+ if (
755
+ config.lazyCompilation === true ||
756
+ typeof config.lazyCompilation === "object"
757
+ ) {
811
758
  console.warn(
812
- '\n⚠️ Warning: lazyCompilation may not work correctly in the current Meteor-Rspack integration.\n' +
813
- ' This feature will be evaluated for support in future Meteor versions.\n' +
814
- ' If you encounter any issues, please disable it in your rspack config.\n',
759
+ "\n⚠️ Warning: lazyCompilation may not work correctly in the current Meteor-Rspack integration.\n" +
760
+ " This feature will be evaluated for support in future Meteor versions.\n" +
761
+ " If you encounter any issues, please disable it in your rspack config.\n"
815
762
  );
816
763
  }
817
764