@meteorjs/rspack 1.1.0-beta.3 → 1.1.0-beta.31

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