@meteorjs/rspack 1.1.0-beta.8 → 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,12 +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
- const mode = isProd ? 'production' : 'development';
231
- const projectDir = process.cwd();
232
- const projectConfigPath = Meteor.projectConfigPath || path.resolve(projectDir, 'rspack.config.js');
278
+ const isProfile = !!Meteor.isProfile;
279
+ const isVerbose = !!Meteor.isVerbose;
233
280
  const configPath = Meteor.configPath;
234
281
  const testEntry = Meteor.testEntry;
235
282
 
@@ -240,58 +287,70 @@ module.exports = async function (inMeteor = {}, argv = {}) {
240
287
  Meteor.isTsxEnabled || (isTypescriptEnabled && isReactEnabled) || false;
241
288
  const isBundleVisualizerEnabled = Meteor.isBundleVisualizerEnabled || false;
242
289
  const isAngularEnabled = Meteor.isAngularEnabled || false;
290
+ const enableSwcExternalHelpers = !isServer && swcExternalHelpers;
243
291
 
244
- // Determine entry points
245
- const entryPath = Meteor.entryPath;
246
-
247
- // Determine output points
248
- const outputPath = Meteor.outputPath;
249
- const outputDir = path.dirname(Meteor.outputPath || '');
250
-
251
- const outputFilename = Meteor.outputFilename;
252
-
253
- // Determine run point
254
- const runPath = Meteor.runPath;
255
-
256
- // Determine banner
257
- const bannerOutput = JSON.parse(Meteor.bannerOutput || process.env.RSPACK_BANNER || '""');
258
-
259
- // Determine output directories
260
- const clientOutputDir = path.resolve(projectDir, 'public');
261
- const serverOutputDir = path.resolve(projectDir, 'private');
262
-
263
- // Determine context for bundles and assets
264
- const buildContext = Meteor.buildContext || '_build';
265
- const assetsContext = Meteor.assetsContext || 'build-assets';
266
- const chunksContext = Meteor.chunksContext || 'build-chunks';
267
-
268
- // Determine build output and pass to Meteor
269
- const buildOutputDir = path.resolve(projectDir, buildContext, outputDir);
270
- 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
+ };
271
302
 
272
- const cacheStrategy = createCacheStrategy(
273
- mode,
274
- (Meteor.isClient && 'client') || 'server',
275
- { 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 }
276
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;
277
331
 
278
332
  // Expose Meteor's helpers to expand Rspack configs
279
- Meteor.compileWithMeteor = deps => compileWithMeteor(deps);
333
+ Meteor.compileWithMeteor = (deps) => compileWithMeteor(deps);
280
334
  Meteor.compileWithRspack = (deps, options = {}) =>
281
335
  compileWithRspack(deps, {
282
336
  options: mergeSplitOverlap(Meteor.swcConfigOptions, options),
283
337
  });
284
- Meteor.setCache = enabled =>
285
- setCache(
286
- !!enabled,
287
- enabled === 'memory' ? undefined : cacheStrategy
288
- );
338
+ Meteor.setCache = (enabled) =>
339
+ setCache(!!enabled, enabled === "memory" ? undefined : cacheStrategy);
289
340
  Meteor.splitVendorChunk = () => splitVendorChunk();
290
- Meteor.extendSwcConfig = (customSwcConfig) => extendSwcConfig(customSwcConfig);
341
+ Meteor.extendSwcConfig = (customSwcConfig) =>
342
+ extendSwcConfig(
343
+ mergeSplitOverlap(Meteor.swcConfigOptions, customSwcConfig)
344
+ );
345
+ Meteor.replaceSwcConfig = (customSwcConfig) =>
346
+ replaceSwcConfig(customSwcConfig);
291
347
  Meteor.extendConfig = (...configs) => mergeSplitOverlap(...configs);
292
- Meteor.disablePlugins = matchers => prepareMeteorRspackConfig({
293
- disablePlugins: matchers,
294
- });
348
+ Meteor.disablePlugins = (matchers) =>
349
+ prepareMeteorRspackConfig({
350
+ disablePlugins: matchers,
351
+ });
352
+ Meteor.enablePortableBuild = () => enablePortableBuild();
353
+ Meteor.persistDevFiles = (matchers) => persistDevFiles(matchers);
295
354
 
296
355
  // Add HtmlRspackPlugin function to Meteor
297
356
  Meteor.HtmlRspackPlugin = (options = {}) => {
@@ -315,6 +374,52 @@ module.exports = async function (inMeteor = {}, argv = {}) {
315
374
  });
316
375
  };
317
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
+
318
423
  // Get Meteor ignore entries
319
424
  const meteorIgnoreEntries = getMeteorIgnoreEntries(projectDir);
320
425
 
@@ -330,21 +435,17 @@ module.exports = async function (inMeteor = {}, argv = {}) {
330
435
  // Set default watch options
331
436
  const watchOptions = {
332
437
  ignored: [
333
- ...createIgnoreGlobConfig([
334
- ...meteorIgnoreEntries,
335
- ...additionalEntries,
336
- ]),
438
+ ...createIgnoreGlobConfig([...meteorIgnoreEntries, ...additionalEntries]),
337
439
  ],
338
440
  };
339
441
 
340
442
  if (Meteor.isDebug || Meteor.isVerbose) {
341
- console.log('[i] Rspack mode:', mode);
342
- console.log('[i] Meteor flags:', Meteor);
443
+ console.log("[i] Rspack mode:", mode);
444
+ console.log("[i] Meteor flags:", Meteor);
343
445
  }
344
446
 
345
- const enableSwcExternalHelpers = !isServer && swcExternalHelpers;
346
447
  const isDevEnvironment = isRun && isDev && !isTest && !isNative;
347
- const swcConfigRule = createSwcConfig({
448
+ swcConfigRule = createSwcConfig({
348
449
  isTypescriptEnabled,
349
450
  isReactEnabled,
350
451
  isJsxEnabled,
@@ -354,7 +455,6 @@ module.exports = async function (inMeteor = {}, argv = {}) {
354
455
  isClient,
355
456
  isAngularEnabled,
356
457
  });
357
- // Expose swc config to use in custom configs
358
458
  Meteor.swcConfigOptions = swcConfigRule.options;
359
459
 
360
460
  const externals = [
@@ -363,34 +463,34 @@ module.exports = async function (inMeteor = {}, argv = {}) {
363
463
  ...(isServer ? [/^bcrypt$/] : []),
364
464
  ];
365
465
  const alias = {
366
- '/': path.resolve(process.cwd()),
466
+ "/": path.resolve(process.cwd()),
367
467
  };
368
468
  const fallback = {
369
469
  ...(isClient && makeWebNodeBuiltinsAlias()),
370
470
  };
371
471
  const extensions = [
372
- '.ts',
373
- '.tsx',
374
- '.mts',
375
- '.cts',
376
- '.js',
377
- '.jsx',
378
- '.mjs',
379
- '.cjs',
380
- '.json',
381
- '.wasm',
472
+ ".ts",
473
+ ".tsx",
474
+ ".mts",
475
+ ".cts",
476
+ ".js",
477
+ ".jsx",
478
+ ".mjs",
479
+ ".cjs",
480
+ ".json",
481
+ ".wasm",
382
482
  ];
383
483
  const extraRules = [];
384
484
 
385
485
  const reactRefreshModule = isReactEnabled
386
- ? safeRequire('@rspack/plugin-react-refresh')
486
+ ? safeRequire("@rspack/plugin-react-refresh")
387
487
  : null;
388
488
 
389
489
  const requireExternalsPlugin = new RequireExternalsPlugin({
390
490
  filePath: path.join(buildContext, runPath),
391
491
  ...(Meteor.isBlazeEnabled && {
392
492
  externals: /\.html$/,
393
- isEagerImport: module => module.endsWith('.html'),
493
+ isEagerImport: (module) => module.endsWith(".html"),
394
494
  ...(isProd && {
395
495
  lastImports: [`./${outputFilename}`],
396
496
  }),
@@ -400,25 +500,26 @@ module.exports = async function (inMeteor = {}, argv = {}) {
400
500
 
401
501
  // Handle assets
402
502
  const assetExternalsPlugin = new AssetExternalsPlugin();
403
- const assetModuleFilename = _fileInfo => {
503
+ const assetModuleFilename = (_fileInfo) => {
404
504
  const filename = _fileInfo.filename;
405
- const isPublic = filename.startsWith('/') || filename.startsWith('public');
505
+ const isPublic = filename.startsWith("/") || filename.startsWith("public");
406
506
  if (isPublic) return `[name][ext][query]`;
407
507
  return `${assetsContext}/[hash][ext][query]`;
408
508
  };
409
509
 
410
510
  const rsdoctorModule = isBundleVisualizerEnabled
411
- ? safeRequire('@rsdoctor/rspack-plugin')
511
+ ? safeRequire("@rsdoctor/rspack-plugin")
412
512
  : null;
413
- const doctorPluginConfig = isRun && isBundleVisualizerEnabled && rsdoctorModule?.RsdoctorRspackPlugin
414
- ? [
415
- new rsdoctorModule.RsdoctorRspackPlugin({
416
- port: isClient
417
- ? (parseInt(Meteor.rsdoctorClientPort || '8888', 10))
418
- : (parseInt(Meteor.rsdoctorServerPort || '8889', 10)),
419
- }),
420
- ]
421
- : [];
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
+ : [];
422
523
  const bannerPluginConfig = !isBuild
423
524
  ? [
424
525
  new BannerPlugin({
@@ -429,7 +530,10 @@ module.exports = async function (inMeteor = {}, argv = {}) {
429
530
  : [];
430
531
  // Not supported in Meteor yet (Rspack 1.7+ is enabled by default)
431
532
  const lazyCompilationConfig = { lazyCompilation: false };
432
- const loggingConfig = { stats: 'none', infrastructureLogging: { level: "none" } };
533
+ const shouldLogVerbose = isProfile || isVerbose;
534
+ const loggingConfig = shouldLogVerbose
535
+ ? {}
536
+ : { stats: "errors-warnings", infrastructureLogging: { level: "warn" } };
433
537
 
434
538
  const clientEntry =
435
539
  isClient && isTest && isTestEager && isTestFullApp
@@ -457,11 +561,11 @@ module.exports = async function (inMeteor = {}, argv = {}) {
457
561
  : isClient && isTest && testEntry
458
562
  ? path.resolve(process.cwd(), testEntry)
459
563
  : path.resolve(process.cwd(), buildContext, entryPath);
460
- const clientNameConfig = `[${(isTest && 'test-') || ''}client-rspack]`;
564
+ const clientNameConfig = `[${(isTest && "test-") || ""}client-rspack]`;
461
565
  // Base client config
462
566
  let clientConfig = {
463
567
  name: clientNameConfig,
464
- target: 'web',
568
+ target: "web",
465
569
  mode,
466
570
  entry: clientEntry,
467
571
  output: {
@@ -470,7 +574,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
470
574
  const chunkName = _module.chunk?.name;
471
575
  const isMainChunk = !chunkName || chunkName === "main";
472
576
  const chunkSuffix = `${chunksContext}/[id]${
473
- isProd ? '.[chunkhash]' : ''
577
+ isProd ? ".[chunkhash]" : ""
474
578
  }.js`;
475
579
  if (isDevEnvironment) {
476
580
  if (isMainChunk) return outputFilename;
@@ -479,21 +583,21 @@ module.exports = async function (inMeteor = {}, argv = {}) {
479
583
  if (isMainChunk) return `../${buildContext}/${outputPath}`;
480
584
  return chunkSuffix;
481
585
  },
482
- libraryTarget: 'commonjs2',
483
- publicPath: '/',
484
- chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
586
+ libraryTarget: "commonjs2",
587
+ publicPath: "/",
588
+ chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
485
589
  assetModuleFilename,
486
590
  cssFilename: `${chunksContext}/[name]${
487
- isProd ? '.[contenthash]' : ''
591
+ isProd ? ".[contenthash]" : ""
488
592
  }.css`,
489
593
  cssChunkFilename: `${chunksContext}/[id]${
490
- isProd ? '.[contenthash]' : ''
594
+ isProd ? ".[contenthash]" : ""
491
595
  }.css`,
492
596
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
493
597
  },
494
598
  optimization: {
495
599
  usedExports: true,
496
- splitChunks: { chunks: 'async' },
600
+ splitChunks: { chunks: "async" },
497
601
  },
498
602
  module: {
499
603
  rules: [
@@ -502,7 +606,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
502
606
  ? [
503
607
  {
504
608
  test: /\.html$/i,
505
- loader: 'ignore-loader',
609
+ loader: "ignore-loader",
506
610
  },
507
611
  ]
508
612
  : []),
@@ -520,38 +624,43 @@ module.exports = async function (inMeteor = {}, argv = {}) {
520
624
  assetExternalsPlugin,
521
625
  ].filter(Boolean),
522
626
  new DefinePlugin({
523
- 'Meteor.isClient': JSON.stringify(true),
524
- 'Meteor.isServer': JSON.stringify(false),
525
- 'Meteor.isTest': JSON.stringify(isTestLike && !isTestFullApp),
526
- 'Meteor.isAppTest': JSON.stringify(isTestLike && isTestFullApp),
527
- 'Meteor.isDevelopment': JSON.stringify(isDev),
528
- 'Meteor.isProduction': JSON.stringify(isProd),
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
+ }),
529
635
  }),
530
636
  ...bannerPluginConfig,
531
637
  Meteor.HtmlRspackPlugin(),
532
638
  ...doctorPluginConfig,
533
639
  new NormalModuleReplacementPlugin(/^node:(.*)$/, (res) => {
534
- res.request = res.request.replace(/^node:/, '');
640
+ res.request = res.request.replace(/^node:/, "");
535
641
  }),
536
642
  ],
537
643
  watchOptions,
538
- devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
644
+ devtool:
645
+ isDevEnvironment || isNative || isTest
646
+ ? "source-map"
647
+ : "hidden-source-map",
539
648
  ...(isDevEnvironment && {
540
649
  devServer: {
541
650
  ...createRemoteDevServerConfig(),
542
- static: { directory: clientOutputDir, publicPath: '/__rspack__/' },
651
+ static: { directory: clientOutputDir, publicPath: "/__rspack__/" },
543
652
  hot: true,
544
653
  liveReload: true,
545
654
  ...(Meteor.isBlazeEnabled && { hot: false }),
546
- port: Meteor.devServerPort || 8080,
655
+ port: devServerPort,
547
656
  devMiddleware: {
548
- writeToDisk: filePath =>
549
- /\.(html)$/.test(filePath) && !filePath.includes('.hot-update.'),
657
+ writeToDisk: createPersistCallback({ once: ['sw.js'], always: ['.html'] }),
550
658
  },
551
659
  onListening(devServer) {
552
660
  if (!devServer) return;
553
661
  const { host, port } = devServer.options;
554
- const protocol = devServer.options.server?.type === "https" ? "https" : "http";
662
+ const protocol =
663
+ devServer.options.server?.type === "https" ? "https" : "http";
555
664
  const devServerUrl = `${protocol}://${host || "localhost"}:${port}`;
556
665
  outputMeteorRspack({ devServerUrl });
557
666
  },
@@ -586,18 +695,18 @@ module.exports = async function (inMeteor = {}, argv = {}) {
586
695
  : isServer && isTest && testEntry
587
696
  ? path.resolve(process.cwd(), testEntry)
588
697
  : path.resolve(projectDir, buildContext, entryPath);
589
- const serverNameConfig = `[${(isTest && 'test-') || ''}server-rspack]`;
698
+ const serverNameConfig = `[${(isTest && "test-") || ""}server-rspack]`;
590
699
  // Base server config
591
700
  let serverConfig = {
592
701
  name: serverNameConfig,
593
- target: 'node',
702
+ target: "node",
594
703
  mode,
595
704
  entry: serverEntry,
596
705
  output: {
597
706
  path: serverOutputDir,
598
707
  filename: () => `../${buildContext}/${outputPath}`,
599
- libraryTarget: 'commonjs2',
600
- chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
708
+ libraryTarget: "commonjs2",
709
+ chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
601
710
  assetModuleFilename,
602
711
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
603
712
  },
@@ -607,19 +716,34 @@ module.exports = async function (inMeteor = {}, argv = {}) {
607
716
  runtimeChunk: false,
608
717
  },
609
718
  module: {
610
- 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
+ ],
611
735
  parser: {
612
736
  javascript: {
613
737
  // Dynamic imports on the server are treated as bundled in the same chunk
614
- dynamicImportMode: 'eager',
738
+ dynamicImportMode: "eager",
615
739
  },
616
740
  },
617
741
  },
618
742
  resolve: {
619
743
  extensions,
620
744
  alias,
621
- modules: ['node_modules', path.resolve(projectDir)],
622
- conditionNames: ['import', 'require', 'node', 'default'],
745
+ modules: ["node_modules", path.resolve(projectDir)],
746
+ conditionNames: ["import", "require", "node", "default"],
623
747
  },
624
748
  externals,
625
749
  externalsPresets: { node: true },
@@ -627,18 +751,22 @@ module.exports = async function (inMeteor = {}, argv = {}) {
627
751
  new DefinePlugin(
628
752
  isTest && (isTestModule || isTestEager)
629
753
  ? {
630
- 'Meteor.isTest': JSON.stringify(isTest && !isTestFullApp),
631
- 'Meteor.isAppTest': JSON.stringify(isTest && isTestFullApp),
632
- '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
+ }),
633
759
  }
634
760
  : {
635
- 'Meteor.isClient': JSON.stringify(false),
636
- 'Meteor.isServer': JSON.stringify(true),
637
- 'Meteor.isTest': JSON.stringify(isTestLike && !isTestFullApp),
638
- 'Meteor.isAppTest': JSON.stringify(isTestLike && isTestFullApp),
639
- 'Meteor.isDevelopment': JSON.stringify(isDev),
640
- 'Meteor.isProduction': JSON.stringify(isProd),
641
- },
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
+ }
642
770
  ),
643
771
  ...bannerPluginConfig,
644
772
  requireExternalsPlugin,
@@ -646,111 +774,21 @@ module.exports = async function (inMeteor = {}, argv = {}) {
646
774
  ...doctorPluginConfig,
647
775
  ],
648
776
  watchOptions,
649
- devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
777
+ devtool:
778
+ isDevEnvironment || isNative || isTest
779
+ ? "source-map"
780
+ : "hidden-source-map",
650
781
  ...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
651
782
  cacheStrategy),
652
783
  ...lazyCompilationConfig,
653
784
  ...loggingConfig,
654
785
  };
655
786
 
656
- // Helper function to load and process config files
657
- async function loadAndProcessConfig(configPath, configType, Meteor, argv, isAngularEnabled) {
658
- try {
659
- // Load the config file
660
- let config;
661
- if (path.extname(configPath) === '.mjs') {
662
- // For ESM modules, we need to use dynamic import
663
- const fileUrl = `file://${configPath}`;
664
- const module = await import(fileUrl);
665
- config = module.default || module;
666
- } else {
667
- // For CommonJS modules, we can use require
668
- config = require(configPath)?.default || require(configPath);
669
- }
670
-
671
- // Process the config
672
- const rawConfig = typeof config === 'function' ? config(Meteor, argv) : config;
673
- const resolvedConfig = await Promise.resolve(rawConfig);
674
- const userConfig = resolvedConfig && '0' in resolvedConfig ? resolvedConfig[0] : resolvedConfig;
675
-
676
- // Define omitted paths and warning function
677
- const omitPaths = [
678
- "name",
679
- "target",
680
- "entry",
681
- "output.path",
682
- "output.filename",
683
- ...(Meteor.isServer ? ["optimization.splitChunks", "optimization.runtimeChunk"] : []),
684
- ].filter(Boolean);
685
-
686
- const warningFn = path => {
687
- if (isAngularEnabled) return;
688
- console.warn(
689
- `[${configType}] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`,
690
- );
691
- };
692
-
693
- // Clean omitted paths and merge Meteor Rspack fragments
694
- let nextConfig = cleanOmittedPaths(userConfig, {
695
- omitPaths,
696
- warningFn,
697
- });
698
- nextConfig = mergeMeteorRspackFragments(nextConfig);
699
-
700
- return nextConfig;
701
- } catch (error) {
702
- console.error(`Error loading ${configType} from ${configPath}:`, error);
703
- if (configType === 'rspack.config.js') {
704
- throw error; // Only rethrow for project config
705
- }
706
- return null;
707
- }
708
- }
709
-
710
- // Load and apply project-level overrides for the selected build
711
- // Check if we're in a Meteor package directory by looking at the path
712
- const isMeteorPackageConfig = projectDir.includes('/packages/rspack');
713
- // Track if user config overrides stats and infrastructureLogging
714
- let statsOverrided = false;
715
- if (fs.existsSync(projectConfigPath) && !isMeteorPackageConfig) {
716
- // Check if there's a .mjs or .cjs version of the config file
717
- const mjsConfigPath = projectConfigPath.replace(/\.js$/, '.mjs');
718
- const cjsConfigPath = projectConfigPath.replace(/\.js$/, '.cjs');
719
-
720
- let projectConfigPathToUse = projectConfigPath;
721
- if (fs.existsSync(mjsConfigPath)) {
722
- projectConfigPathToUse = mjsConfigPath;
723
- } else if (fs.existsSync(cjsConfigPath)) {
724
- projectConfigPathToUse = cjsConfigPath;
725
- }
726
-
727
- const nextUserConfig = await loadAndProcessConfig(
728
- projectConfigPathToUse,
729
- 'rspack.config.js',
730
- Meteor,
731
- argv,
732
- isAngularEnabled
733
- );
734
-
735
- // Track if user config overrides stats
736
- if (nextUserConfig) {
737
- if (nextUserConfig.stats != null) {
738
- statsOverrided = true;
739
- }
740
- if (Meteor.isClient) {
741
- clientConfig = mergeSplitOverlap(clientConfig, nextUserConfig);
742
- }
743
- if (Meteor.isServer) {
744
- serverConfig = mergeSplitOverlap(serverConfig, nextUserConfig);
745
- }
746
- }
747
- }
748
-
749
787
  // Establish Angular overrides to ensure proper integration
750
788
  const angularExpandConfig = isAngularEnabled
751
789
  ? {
752
790
  mode: isProd ? "production" : "development",
753
- devServer: { port: Meteor.devServerPort },
791
+ devServer: { port: devServerPort },
754
792
  stats: { preset: "normal" },
755
793
  infrastructureLogging: { level: "info" },
756
794
  ...(isProd && isClient && { output: { module: false } }),
@@ -777,34 +815,31 @@ module.exports = async function (inMeteor = {}, argv = {}) {
777
815
  }
778
816
  : {};
779
817
 
780
- let config = mergeSplitOverlap(
781
- isClient ? clientConfig : serverConfig,
782
- angularExpandConfig
783
- );
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);
784
837
  config = mergeSplitOverlap(config, testClientExpandConfig);
785
838
 
786
- // Check for override config file (extra file to override everything)
787
- if (projectConfigPath) {
788
- const configDir = path.dirname(projectConfigPath);
789
- const configFileName = path.basename(projectConfigPath);
790
- const configExt = path.extname(configFileName);
791
- const configNameWithoutExt = configFileName.replace(configExt, '');
792
- const configNameFull = `${configNameWithoutExt}.override${configExt}`;
793
- const overrideConfigPath = path.join(configDir, configNameFull);
794
-
795
- if (fs.existsSync(overrideConfigPath)) {
796
- const nextOverrideConfig = await loadAndProcessConfig(
797
- overrideConfigPath,
798
- configNameFull,
799
- Meteor,
800
- argv,
801
- isAngularEnabled
802
- );
803
-
804
- if (nextOverrideConfig) {
805
- // Apply override config as the last step
806
- config = mergeSplitOverlap(config, nextOverrideConfig);
807
- }
839
+ if (nextOverrideConfig) {
840
+ config = mergeSplitOverlap(config, nextOverrideConfig);
841
+ if (nextOverrideConfig.stats != null) {
842
+ statsOverrided = true;
808
843
  }
809
844
  }
810
845
 
@@ -814,22 +849,27 @@ module.exports = async function (inMeteor = {}, argv = {}) {
814
849
  delete config.disablePlugins;
815
850
  }
816
851
 
852
+ delete config["meteor.enablePortableBuild"];
853
+
817
854
  if (Meteor.isDebug || Meteor.isVerbose) {
818
- console.log('Config:', inspect(config, { depth: null, colors: true }));
855
+ console.log("Config:", inspect(config, { depth: null, colors: true }));
819
856
  }
820
857
 
821
858
  // Check if lazyCompilation is enabled and warn the user
822
- if (config.lazyCompilation === true || typeof config.lazyCompilation === 'object') {
859
+ if (
860
+ config.lazyCompilation === true ||
861
+ typeof config.lazyCompilation === "object"
862
+ ) {
823
863
  console.warn(
824
- '\n⚠️ Warning: lazyCompilation may not work correctly in the current Meteor-Rspack integration.\n' +
825
- ' This feature will be evaluated for support in future Meteor versions.\n' +
826
- ' 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"
827
867
  );
828
868
  }
829
869
 
830
870
  // Add MeteorRspackOutputPlugin as the last plugin to output compilation info
831
871
  const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({
832
- getData: (stats, { isRebuild, compilationCount }) => ({
872
+ getData: (stats, { isRebuild, compilationCount, compiler }) => ({
833
873
  name: config.name,
834
874
  mode: config.mode,
835
875
  hasErrors: stats.hasErrors(),
@@ -838,6 +878,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
838
878
  statsOverrided,
839
879
  compilationCount,
840
880
  isRebuild,
881
+ ...(!isRebuild && compiler && {
882
+ delegatedExtensions: extractDelegatedExtensions(stats, compiler),
883
+ }),
841
884
  }),
842
885
  });
843
886
  config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])];