@meteorjs/rspack 1.1.0-beta.9 → 2.0.0

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