@meteorjs/rspack 1.1.0-beta.9 → 2.0.1

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,48 @@ 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]`;
565
+
566
+ // Default onListening provided by meteor-rspack. Kept as a named
567
+ // reference so we can detect a user-supplied override after merge
568
+ // and compose (run default first, then user's).
569
+ const meteorDefaultOnListening = function (devServer) {
570
+ if (!devServer) return;
571
+ const { host, port } = devServer.options;
572
+ const protocol =
573
+ devServer.options.server?.type === "https" ? "https" : "http";
574
+ const devServerUrl = `${protocol}://${host || "localhost"}:${port}`;
575
+ outputMeteorRspack({ devServerUrl });
576
+
577
+ // Windows-only: webpack-dev-server tracks accepted sockets
578
+ // but doesn't attach 'error'. On Windows, teardown of a
579
+ // closed proxy connection sends RST, producing an unhandled
580
+ // ECONNRESET that crashes the dev server. Unix peers send
581
+ // FIN and never hit this.
582
+ if (process.platform === "win32") {
583
+ const server = devServer.server;
584
+ if (!server || server.__meteorRspackErrorGuard) return;
585
+ server.__meteorRspackErrorGuard = true;
586
+
587
+ server.on("connection", (socket) => {
588
+ if (!socket || socket.__meteorRspackGuarded) return;
589
+ socket.__meteorRspackGuarded = true;
590
+ socket.on("error", (err) => {
591
+ if (err && err.code === "ECONNRESET") return;
592
+ console.warn(
593
+ `[meteor-rspack] dev server socket error: ${
594
+ err && (err.code || err.message)
595
+ }`
596
+ );
597
+ });
598
+ });
599
+ }
600
+ };
601
+
464
602
  // Base client config
465
603
  let clientConfig = {
466
604
  name: clientNameConfig,
467
- target: 'web',
605
+ target: "web",
468
606
  mode,
469
607
  entry: clientEntry,
470
608
  output: {
@@ -473,7 +611,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
473
611
  const chunkName = _module.chunk?.name;
474
612
  const isMainChunk = !chunkName || chunkName === "main";
475
613
  const chunkSuffix = `${chunksContext}/[id]${
476
- isProd ? '.[chunkhash]' : ''
614
+ isProd ? ".[chunkhash]" : ""
477
615
  }.js`;
478
616
  if (isDevEnvironment) {
479
617
  if (isMainChunk) return outputFilename;
@@ -482,21 +620,21 @@ module.exports = async function (inMeteor = {}, argv = {}) {
482
620
  if (isMainChunk) return `../${buildContext}/${outputPath}`;
483
621
  return chunkSuffix;
484
622
  },
485
- libraryTarget: 'commonjs2',
486
- publicPath: '/',
487
- chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
623
+ libraryTarget: "commonjs2",
624
+ publicPath: "/",
625
+ chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
488
626
  assetModuleFilename,
489
627
  cssFilename: `${chunksContext}/[name]${
490
- isProd ? '.[contenthash]' : ''
628
+ isProd ? ".[contenthash]" : ""
491
629
  }.css`,
492
630
  cssChunkFilename: `${chunksContext}/[id]${
493
- isProd ? '.[contenthash]' : ''
631
+ isProd ? ".[contenthash]" : ""
494
632
  }.css`,
495
633
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
496
634
  },
497
635
  optimization: {
498
636
  usedExports: true,
499
- splitChunks: { chunks: 'async' },
637
+ splitChunks: { chunks: "async" },
500
638
  },
501
639
  module: {
502
640
  rules: [
@@ -505,7 +643,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
505
643
  ? [
506
644
  {
507
645
  test: /\.html$/i,
508
- loader: 'ignore-loader',
646
+ loader: "ignore-loader",
509
647
  },
510
648
  ]
511
649
  : []),
@@ -523,41 +661,39 @@ module.exports = async function (inMeteor = {}, argv = {}) {
523
661
  assetExternalsPlugin,
524
662
  ].filter(Boolean),
525
663
  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),
664
+ "Meteor.isClient": JSON.stringify(true),
665
+ "Meteor.isServer": JSON.stringify(false),
666
+ "Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp),
667
+ "Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp),
668
+ ...(!isPortableBuild && {
669
+ "Meteor.isDevelopment": JSON.stringify(isDev),
670
+ "Meteor.isProduction": JSON.stringify(isProd),
671
+ }),
532
672
  }),
533
673
  ...bannerPluginConfig,
534
674
  Meteor.HtmlRspackPlugin(),
535
675
  ...doctorPluginConfig,
536
676
  new NormalModuleReplacementPlugin(/^node:(.*)$/, (res) => {
537
- res.request = res.request.replace(/^node:/, '');
677
+ res.request = res.request.replace(/^node:/, "");
538
678
  }),
539
679
  ],
540
680
  watchOptions,
541
- devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
681
+ devtool:
682
+ isDevEnvironment || isNative || isTest
683
+ ? "source-map"
684
+ : "hidden-source-map",
542
685
  ...(isDevEnvironment && {
543
686
  devServer: {
544
687
  ...createRemoteDevServerConfig(),
545
- static: { directory: clientOutputDir, publicPath: '/__rspack__/' },
688
+ static: { directory: clientOutputDir, publicPath: "/__rspack__/" },
546
689
  hot: true,
547
690
  liveReload: true,
548
691
  ...(Meteor.isBlazeEnabled && { hot: false }),
549
- port: Meteor.devServerPort || 8080,
692
+ port: devServerPort,
550
693
  devMiddleware: {
551
- writeToDisk: filePath =>
552
- /\.(html)$/.test(filePath) && !filePath.includes('.hot-update.'),
553
- },
554
- onListening(devServer) {
555
- if (!devServer) return;
556
- const { host, port } = devServer.options;
557
- const protocol = devServer.options.server?.type === "https" ? "https" : "http";
558
- const devServerUrl = `${protocol}://${host || "localhost"}:${port}`;
559
- outputMeteorRspack({ devServerUrl });
694
+ writeToDisk: createPersistCallback({ once: ['sw.js'], always: ['.html'] }),
560
695
  },
696
+ onListening: meteorDefaultOnListening,
561
697
  },
562
698
  }),
563
699
  ...merge(cacheStrategy, { experiments: { css: true } }),
@@ -589,18 +725,18 @@ module.exports = async function (inMeteor = {}, argv = {}) {
589
725
  : isServer && isTest && testEntry
590
726
  ? path.resolve(process.cwd(), testEntry)
591
727
  : path.resolve(projectDir, buildContext, entryPath);
592
- const serverNameConfig = `[${(isTest && 'test-') || ''}server-rspack]`;
728
+ const serverNameConfig = `[${(isTest && "test-") || ""}server-rspack]`;
593
729
  // Base server config
594
730
  let serverConfig = {
595
731
  name: serverNameConfig,
596
- target: 'node',
732
+ target: "node",
597
733
  mode,
598
734
  entry: serverEntry,
599
735
  output: {
600
736
  path: serverOutputDir,
601
737
  filename: () => `../${buildContext}/${outputPath}`,
602
- libraryTarget: 'commonjs2',
603
- chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
738
+ libraryTarget: "commonjs2",
739
+ chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
604
740
  assetModuleFilename,
605
741
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
606
742
  },
@@ -610,19 +746,34 @@ module.exports = async function (inMeteor = {}, argv = {}) {
610
746
  runtimeChunk: false,
611
747
  },
612
748
  module: {
613
- rules: [swcConfigRule, ...extraRules],
749
+ rules: [
750
+ swcConfigRule,
751
+ // Mirror the client rule: ignore .html so rspack doesn't try to
752
+ // parse them as JavaScript. Meteor's template compiler handles
753
+ // .html files separately, and RequireExternalsPlugin below wires
754
+ // the imports to Meteor's module system.
755
+ ...(Meteor.isBlazeEnabled
756
+ ? [
757
+ {
758
+ test: /\.html$/i,
759
+ loader: 'ignore-loader',
760
+ },
761
+ ]
762
+ : []),
763
+ ...extraRules,
764
+ ],
614
765
  parser: {
615
766
  javascript: {
616
767
  // Dynamic imports on the server are treated as bundled in the same chunk
617
- dynamicImportMode: 'eager',
768
+ dynamicImportMode: "eager",
618
769
  },
619
770
  },
620
771
  },
621
772
  resolve: {
622
773
  extensions,
623
774
  alias,
624
- modules: ['node_modules', path.resolve(projectDir)],
625
- conditionNames: ['import', 'require', 'node', 'default'],
775
+ modules: ["node_modules", path.resolve(projectDir)],
776
+ conditionNames: ["import", "require", "node", "default"],
626
777
  },
627
778
  externals,
628
779
  externalsPresets: { node: true },
@@ -630,18 +781,22 @@ module.exports = async function (inMeteor = {}, argv = {}) {
630
781
  new DefinePlugin(
631
782
  isTest && (isTestModule || isTestEager)
632
783
  ? {
633
- 'Meteor.isTest': JSON.stringify(isTest && !isTestFullApp),
634
- 'Meteor.isAppTest': JSON.stringify(isTest && isTestFullApp),
635
- 'Meteor.isDevelopment': JSON.stringify(isDev),
784
+ "Meteor.isTest": JSON.stringify(isTest && !isTestFullApp),
785
+ "Meteor.isAppTest": JSON.stringify(isTest && isTestFullApp),
786
+ ...(!isPortableBuild && {
787
+ "Meteor.isDevelopment": JSON.stringify(isDev),
788
+ }),
636
789
  }
637
790
  : {
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
- },
791
+ "Meteor.isClient": JSON.stringify(false),
792
+ "Meteor.isServer": JSON.stringify(true),
793
+ "Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp),
794
+ "Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp),
795
+ ...(!isPortableBuild && {
796
+ "Meteor.isDevelopment": JSON.stringify(isDev),
797
+ "Meteor.isProduction": JSON.stringify(isProd),
798
+ }),
799
+ }
645
800
  ),
646
801
  ...bannerPluginConfig,
647
802
  requireExternalsPlugin,
@@ -649,111 +804,21 @@ module.exports = async function (inMeteor = {}, argv = {}) {
649
804
  ...doctorPluginConfig,
650
805
  ],
651
806
  watchOptions,
652
- devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
807
+ devtool:
808
+ isDevEnvironment || isNative || isTest
809
+ ? "source-map"
810
+ : "hidden-source-map",
653
811
  ...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
654
812
  cacheStrategy),
655
813
  ...lazyCompilationConfig,
656
814
  ...loggingConfig,
657
815
  };
658
816
 
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
817
  // Establish Angular overrides to ensure proper integration
753
818
  const angularExpandConfig = isAngularEnabled
754
819
  ? {
755
820
  mode: isProd ? "production" : "development",
756
- devServer: { port: Meteor.devServerPort },
821
+ devServer: { port: devServerPort },
757
822
  stats: { preset: "normal" },
758
823
  infrastructureLogging: { level: "info" },
759
824
  ...(isProd && isClient && { output: { module: false } }),
@@ -780,34 +845,48 @@ module.exports = async function (inMeteor = {}, argv = {}) {
780
845
  }
781
846
  : {};
782
847
 
783
- let config = mergeSplitOverlap(
784
- isClient ? clientConfig : serverConfig,
785
- angularExpandConfig
786
- );
848
+ // Second pass: re-run only when a mode override was detected, so the user config
849
+ // can depend on fully-computed Meteor flags and helpers (swcConfigOptions, buildOutputDir, etc.).
850
+ if (nextUserConfig?.mode || nextOverrideConfig?.mode || isAngularEnabled) {
851
+ ({ nextUserConfig, nextOverrideConfig } = await loadUserAndOverrideConfig(
852
+ projectConfigPath,
853
+ Meteor,
854
+ argv
855
+ ));
856
+ }
857
+ let statsOverrided = false;
858
+ let config = isClient ? clientConfig : serverConfig;
859
+ if (nextUserConfig) {
860
+ config = mergeSplitOverlap(config, nextUserConfig);
861
+ if (nextUserConfig.stats != null) {
862
+ statsOverrided = true;
863
+ }
864
+ }
865
+
866
+ config = mergeSplitOverlap(config, angularExpandConfig);
787
867
  config = mergeSplitOverlap(config, testClientExpandConfig);
788
868
 
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
- }
869
+ if (nextOverrideConfig) {
870
+ config = mergeSplitOverlap(config, nextOverrideConfig);
871
+ if (nextOverrideConfig.stats != null) {
872
+ statsOverrided = true;
873
+ }
874
+ }
875
+
876
+ // If the user or an override replaced devServer.onListening, compose
877
+ // so our default runs first (attaches the Windows socket guard and
878
+ // reports the dev server URL) and the user's hook runs second.
879
+ if (isClient && config.devServer) {
880
+ const finalOnListening = config.devServer.onListening;
881
+ if (
882
+ typeof finalOnListening === "function" &&
883
+ finalOnListening !== meteorDefaultOnListening
884
+ ) {
885
+ const userOnListening = finalOnListening;
886
+ config.devServer.onListening = function (devServer) {
887
+ meteorDefaultOnListening(devServer);
888
+ userOnListening(devServer);
889
+ };
811
890
  }
812
891
  }
813
892
 
@@ -817,22 +896,27 @@ module.exports = async function (inMeteor = {}, argv = {}) {
817
896
  delete config.disablePlugins;
818
897
  }
819
898
 
899
+ delete config["meteor.enablePortableBuild"];
900
+
820
901
  if (Meteor.isDebug || Meteor.isVerbose) {
821
- console.log('Config:', inspect(config, { depth: null, colors: true }));
902
+ console.log("Config:", inspect(config, { depth: null, colors: true }));
822
903
  }
823
904
 
824
905
  // Check if lazyCompilation is enabled and warn the user
825
- if (config.lazyCompilation === true || typeof config.lazyCompilation === 'object') {
906
+ if (
907
+ config.lazyCompilation === true ||
908
+ typeof config.lazyCompilation === "object"
909
+ ) {
826
910
  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',
911
+ "\n⚠️ Warning: lazyCompilation may not work correctly in the current Meteor-Rspack integration.\n" +
912
+ " This feature will be evaluated for support in future Meteor versions.\n" +
913
+ " If you encounter any issues, please disable it in your rspack config.\n"
830
914
  );
831
915
  }
832
916
 
833
917
  // Add MeteorRspackOutputPlugin as the last plugin to output compilation info
834
918
  const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({
835
- getData: (stats, { isRebuild, compilationCount }) => ({
919
+ getData: (stats, { isRebuild, compilationCount, compiler }) => ({
836
920
  name: config.name,
837
921
  mode: config.mode,
838
922
  hasErrors: stats.hasErrors(),
@@ -841,6 +925,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
841
925
  statsOverrided,
842
926
  compilationCount,
843
927
  isRebuild,
928
+ ...(!isRebuild && compiler && {
929
+ delegatedExtensions: extractDelegatedExtensions(stats, compiler),
930
+ }),
844
931
  }),
845
932
  });
846
933
  config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])];