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