@meteorjs/rspack 3.0.0-beta.0 → 3.0.0-beta.2

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/lib/ignore.js CHANGED
@@ -85,15 +85,25 @@ function createIgnoreGlobConfig(entries = []) {
85
85
  /**
86
86
  * Creates a regex pattern to match the specified glob patterns.
87
87
  * Converts glob patterns with * and ** into regex equivalents.
88
- *
88
+ *
89
89
  * @param {string[]} globPatterns - Array of glob patterns from createIgnoreGlobConfig
90
+ * @param {string} [rootPath] - Absolute root that matched paths must belong to
90
91
  * @returns {RegExp} - Regex pattern to match the specified patterns
91
92
  */
92
- function createIgnoreRegex(globPatterns) {
93
+ function createIgnoreRegex(globPatterns, rootPath) {
93
94
  if (!Array.isArray(globPatterns) || globPatterns.length === 0) {
94
95
  throw new Error('globPatterns must be a non-empty array');
95
96
  }
96
97
 
98
+ // Rspack applies context exclusions to absolute module paths. Anchor rooted
99
+ // patterns here so matching starts inside the app, not in a parent folder.
100
+ const pathPrefix = rootPath
101
+ ? `^${rootPath
102
+ .replace(/\\/g, '/')
103
+ .replace(/\/+$/, '')
104
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=/|$)`
105
+ : '(?:^|/)';
106
+
97
107
  // Process each glob pattern and convert to regex
98
108
  const regexPatterns = globPatterns.map(pattern => {
99
109
  // Skip negation patterns for the regex
@@ -115,9 +125,7 @@ function createIgnoreRegex(globPatterns) {
115
125
  // Convert the ** placeholder to its regex equivalent (any number of characters including /)
116
126
  regexPattern = regexPattern.replace(new RegExp(DOUBLE_ASTERISK_PLACEHOLDER, 'g'), '.*');
117
127
 
118
- // For absolute paths, we don't want to force the pattern to match from the beginning
119
- // but we still want to ensure it matches to the end of the path segment
120
- regexPattern = '(?:^|/)' + regexPattern;
128
+ regexPattern = pathPrefix + regexPattern;
121
129
 
122
130
  return regexPattern;
123
131
  }).filter(pattern => pattern !== null);
@@ -20,7 +20,7 @@ function extractLocalDependencies(configFilePath) {
20
20
 
21
21
  // Parse the file into an AST
22
22
  const ast = swc.parseSync(content, {
23
- syntax: 'ecmascript',
23
+ syntax: /\.[mc]?tsx?$/.test(configFilePath) ? 'typescript' : 'ecmascript',
24
24
  dynamicImport: true,
25
25
  target: 'es2020',
26
26
  });
@@ -30,10 +30,10 @@ function extractLocalDependencies(configFilePath) {
30
30
  let modulePath = null;
31
31
 
32
32
  // Handle require() calls: require('./plugin')
33
- if (node.type === 'CallExpression' &&
34
- node.callee.type === 'Identifier' &&
35
- node.callee.value === 'require' &&
36
- node.arguments.length > 0) {
33
+ if (node.type === 'CallExpression' &&
34
+ node.callee.type === 'Identifier' &&
35
+ node.callee.value === 'require' &&
36
+ node.arguments.length > 0) {
37
37
  const arg = node.arguments[0];
38
38
  if (arg.expression?.type === 'StringLiteral') {
39
39
  modulePath = arg.expression.value;
@@ -42,8 +42,8 @@ function extractLocalDependencies(configFilePath) {
42
42
 
43
43
  // Handle dynamic import() calls: import('./plugin')
44
44
  if (node.type === 'CallExpression' &&
45
- node.callee.type === 'Import' &&
46
- node.arguments.length > 0) {
45
+ node.callee.type === 'Import' &&
46
+ node.arguments.length > 0) {
47
47
  const arg = node.arguments[0];
48
48
  if (arg.expression?.type === 'StringLiteral') {
49
49
  modulePath = arg.expression.value;
@@ -163,7 +163,7 @@ function resolveLocalModule(modulePath, configDir, projectDir) {
163
163
  const resolvedReal = fs.realpathSync(resolvedPath);
164
164
  const projectReal = fs.realpathSync(projectDir);
165
165
 
166
- const isWithinProject =
166
+ const isWithinProject =
167
167
  resolvedReal === projectReal ||
168
168
  resolvedReal.startsWith(projectReal + path.sep);
169
169
  const hasNodeModulesSegment = resolvedReal.split(path.sep).includes('node_modules');
@@ -3,7 +3,7 @@
3
3
  * overlapping file extensions in module rules.
4
4
  */
5
5
 
6
- const { mergeWithCustomize } = require('webpack-merge');
6
+ const { mergeWithCustomize } = require('rspack-merge');
7
7
  const isEqual = require('fast-deep-equal');
8
8
 
9
9
  /**
@@ -56,17 +56,12 @@ function compileWithRspack(deps, { options = {} } = {}) {
56
56
  */
57
57
  function setCache(
58
58
  enabled,
59
- cacheConfig = { cache: true, experiments: { cache: true } }
59
+ cacheConfig = { cache: true }
60
60
  ) {
61
61
  return prepareMeteorRspackConfig(
62
62
  enabled
63
63
  ? cacheConfig
64
- : {
65
- cache: false, // disable cache
66
- experiments: {
67
- cache: false, // disable persistent cache (experimental flag)
68
- },
69
- }
64
+ : { cache: false }
70
65
  );
71
66
  }
72
67
 
package/lib/swc.js CHANGED
@@ -14,7 +14,13 @@ function getMeteorAppSwcrc(file = '.swcrc') {
14
14
 
15
15
  if (file.endsWith('.ts')) {
16
16
  try {
17
- const swc = require('@swc/core');
17
+ let swc;
18
+ try {
19
+ swc = require(require.resolve('@swc/core', { paths: [process.cwd()] }));
20
+ } catch (e) {
21
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
22
+ swc = require('@swc/core');
23
+ }
18
24
  const result = swc.transformSync(content, {
19
25
  jsc: {
20
26
  parser: {
package/lib/test.js CHANGED
@@ -44,7 +44,8 @@ const generateEagerTestFile = ({
44
44
 
45
45
  // Create regex from ignore entries
46
46
  const excludeFoldersRegex = createIgnoreRegex(
47
- createIgnoreGlobConfig(ignoreEntries)
47
+ createIgnoreGlobConfig(ignoreEntries),
48
+ projectDir,
48
49
  );
49
50
  // Create regex from meteor ignore entries
50
51
  const excludeMeteorIgnoreRegex = inMeteorIgnoreEntries.length > 0
@@ -74,14 +75,14 @@ const generateEagerTestFile = ({
74
75
  exclude: ${excludeFoldersRegex.toString()},
75
76
  mode: 'eager',
76
77
  });
77
- ctx.keys().filter((k) => {
78
+ await Promise.all(ctx.keys().filter((k) => {
78
79
  ${
79
80
  excludeMeteorIgnoreRegex
80
81
  ? `// Only exclude based on *relative* path segments.
81
82
  return !MeteorIgnoreRegex.test(k);`
82
83
  : "return true;"
83
84
  }
84
- }).forEach(ctx);
85
+ }).map(ctx));
85
86
  ${
86
87
  extraEntry
87
88
  ? `const extra = import.meta.webpackContext('${toPosix(path.dirname(
@@ -0,0 +1,66 @@
1
+ const { ProvidePlugin } = require("@rspack/core");
2
+
3
+ let meteorNodeStubs;
4
+ try {
5
+ const stubPath = require.resolve("meteor-node-stubs", { paths: [process.cwd()] });
6
+ meteorNodeStubs = require(stubPath);
7
+ } catch (e) {
8
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
9
+ try {
10
+ meteorNodeStubs = require("meteor-node-stubs");
11
+ } catch (fallbackError) {
12
+ if (fallbackError.code !== 'MODULE_NOT_FOUND') throw fallbackError;
13
+ // It's optional. If not installed, meteorNodeStubs remains undefined.
14
+ }
15
+ }
16
+
17
+ const TEST_CLIENT_NODE_POLYFILLS = [
18
+ "assert",
19
+ "buffer",
20
+ "constants",
21
+ "crypto",
22
+ "events",
23
+ "fs",
24
+ "http",
25
+ "https",
26
+ "os",
27
+ "path",
28
+ "querystring",
29
+ "stream",
30
+ "string_decoder",
31
+ "sys",
32
+ "timers",
33
+ "tty",
34
+ "url",
35
+ "util",
36
+ "vm",
37
+ "zlib",
38
+ ];
39
+
40
+ function createTestClientNodePolyfillConfig() {
41
+ const fallback = Object.fromEntries(
42
+ TEST_CLIENT_NODE_POLYFILLS.map((name) => [name, meteorNodeStubs?.[name] ?? false]),
43
+ );
44
+
45
+ const plugins = [];
46
+
47
+ if (meteorNodeStubs?.buffer) {
48
+ plugins.push(
49
+ new ProvidePlugin({
50
+ Buffer: [meteorNodeStubs.buffer, "Buffer"],
51
+ })
52
+ );
53
+ }
54
+
55
+ return {
56
+ resolve: {
57
+ alias: {
58
+ "timers/promises$": require.resolve("isomorphic-timers-promises"),
59
+ },
60
+ fallback,
61
+ },
62
+ plugins,
63
+ };
64
+ }
65
+
66
+ module.exports = { createTestClientNodePolyfillConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "3.0.0-beta.0",
3
+ "version": "3.0.0-beta.2",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -13,16 +13,22 @@
13
13
  "dependencies": {
14
14
  "fast-deep-equal": "^3.1.3",
15
15
  "ignore-loader": "^0.1.2",
16
- "node-polyfill-webpack-plugin": "^4.1.0",
17
- "webpack-merge": "^6.0.1"
16
+ "isomorphic-timers-promises": "^1.0.1",
17
+ "rspack-merge": "^1.0.1"
18
18
  },
19
19
  "devDependencies": {
20
20
  "semver": "^7.7.4"
21
21
  },
22
22
  "peerDependencies": {
23
- "@rspack/cli": ">=2.0.0",
24
- "@rspack/core": ">=2.0.0",
25
- "@rspack/dev-server": ">=2.0.0",
26
- "@swc/core": ">=1.3.0"
23
+ "@rspack/cli": ">=2.2.0",
24
+ "@rspack/core": ">=2.2.0",
25
+ "@rspack/dev-server": ">=2.2.0",
26
+ "@swc/core": ">=1.3.0",
27
+ "meteor-node-stubs": ">=1.2.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "meteor-node-stubs": {
31
+ "optional": true
32
+ }
27
33
  }
28
34
  }
@@ -13,6 +13,18 @@ const path = require('path');
13
13
  // Normalize a path to always use forward slashes (POSIX style).
14
14
  // Module identifiers in bundled JS must use '/' regardless of OS.
15
15
  const toPosix = (p) => p.replace(/\\/g, '/');
16
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ const buildStandaloneRequireRegex = (pkg) =>
18
+ new RegExp(`^\\s*require\\('${escapeRegExp(pkg)}'\\);?.*(\\r?\\n)?`, 'gm');
19
+ const buildStandaloneImportRegex = (pkg) =>
20
+ new RegExp(
21
+ `^[\\t ]*import[\\t ]+'${escapeRegExp(pkg)}';?[^\\r\\n]*(?:\\r?\\n|$)`,
22
+ 'gm'
23
+ );
24
+ const MANAGED_IMPORT_BLOCK_REGEX =
25
+ /^\/\/\s*\(function\s+(?:eagerExternalImports\d+|lastImports(?:\d+)?)\s*\(\)\s*\{\s*\r?\n[\s\S]*?^\/\/\s*\}\)\s*(?:\r?\n|$)/gm;
26
+ const STANDALONE_REQUIRE_REGEX = /^\s*require\('([^']+)'\)/gm;
27
+ const STANDALONE_IMPORT_REGEX = /^\s*import\s+'([^']+)'/gm;
16
28
 
17
29
  class RequireExternalsPlugin {
18
30
  constructor({
@@ -152,6 +164,13 @@ class RequireExternalsPlugin {
152
164
  }
153
165
 
154
166
  compiler.hooks.done.tap({ name: this.pluginName, stage: -10 }, (stats) => {
167
+ // Preserve the last known-good Meteor runtime entry when compilation
168
+ // fails. Rewriting externals from incomplete stats would make Meteor
169
+ // restart against a broken Rspack bundle.
170
+ if (stats.hasErrors()) {
171
+ return;
172
+ }
173
+
155
174
  // 1) Ensure globalThis.module / exports block is present if enabled
156
175
  if (this._enableGlobalPolyfill) {
157
176
  this._ensureGlobalThisModule();
@@ -160,8 +179,24 @@ class RequireExternalsPlugin {
160
179
  // 2) Re-load existing requires from disk on every run
161
180
  const existing = this._readExistingRequires();
162
181
 
163
- // 2a) Compute the *current* externals in this build
164
- const info = stats.toJson({ modules: true });
182
+ // 2a) Compute the *current* externals in this build.
183
+ // Only module names are needed, so start from `all: false` instead of
184
+ // serializing the whole stats object (chunks, assets, reasons, ...)
185
+ // on every compilation — a fixed cost per rebuild that grows with app
186
+ // size. `cachedModules: true` keeps unchanged modules in the list on
187
+ // incremental rebuilds, so their requires are not wrongly removed.
188
+ // `orphanModules: true` is required in production: module
189
+ // concatenation absorbs the externals' importers, marking the
190
+ // external modules themselves as orphans, and without this flag
191
+ // they vanish from the list entirely — which stripped every
192
+ // mirrored require (meteor/*, Blaze .html) from the meteor entry.
193
+ // See meteor/meteor#14568.
194
+ const info = stats.toJson({
195
+ all: false,
196
+ modules: true,
197
+ cachedModules: true,
198
+ orphanModules: true,
199
+ });
165
200
  const current = new Set();
166
201
  for (const m of info.modules) {
167
202
  const matchInfo = this._isExternalModule(m.name);
@@ -173,15 +208,22 @@ class RequireExternalsPlugin {
173
208
  }
174
209
  }
175
210
 
176
- // 2b) Remove any requires that are no longer in `current`
211
+ // 2b) Remove any dependencies that are no longer in `current`
177
212
  const toRemove = [...existing].filter(p => !current.has(p));
178
213
  if (toRemove.length) {
179
214
  let content = fs.readFileSync(this.filePath, 'utf-8');
180
215
 
181
- // Strip stale require(...) lines
216
+ // Strip stale requires and imports generated by this plugin. Other
217
+ // standalone imports link Meteor entry and Rspack output files and
218
+ // must remain intact.
182
219
  for (const pkg of toRemove) {
183
- const re = new RegExp(`^.*require\\('${pkg}'\\);?.*(\\r?\\n)?`, 'gm');
184
- content = content.replace(re, '');
220
+ const requireRe = buildStandaloneRequireRegex(pkg);
221
+ const importRe = buildStandaloneImportRegex(pkg);
222
+ content = content.replace(requireRe, '');
223
+ content = content.replace(
224
+ MANAGED_IMPORT_BLOCK_REGEX,
225
+ (block) => block.replace(importRe, '')
226
+ );
185
227
  }
186
228
 
187
229
  // Strip out any now-empty helper functions:
@@ -206,11 +248,11 @@ class RequireExternalsPlugin {
206
248
  // Re-populate `existing` so the add-diff is accurate
207
249
  existing.clear();
208
250
  // Check for require statements
209
- for (const match of content.matchAll(/require\('([^']+)'\)/g)) {
251
+ for (const match of content.matchAll(STANDALONE_REQUIRE_REGEX)) {
210
252
  existing.add(match[1]);
211
253
  }
212
254
  // Also check for import statements (used in the new format)
213
- for (const match of content.matchAll(/import\s+'([^']+)'/g)) {
255
+ for (const match of content.matchAll(STANDALONE_IMPORT_REGEX)) {
214
256
  existing.add(match[1]);
215
257
  }
216
258
  }
@@ -478,19 +520,22 @@ class RequireExternalsPlugin {
478
520
 
479
521
  _readExistingRequires() {
480
522
  const existing = new Set();
523
+ // Generated Rspack bridge imports are not managed externals. Relative
524
+ // imports such as Blaze HTML files still belong to this plugin.
525
+ const isRspackBridgeImport = (modulePath) =>
526
+ typeof modulePath === 'string' &&
527
+ /(?:^|[/\\])[^/\\]*-rspack\.(?:js|cjs)$/.test(modulePath);
481
528
  try {
482
529
  const content = fs.readFileSync(this.filePath, 'utf-8');
483
530
  // Check for require statements
484
- const requireRegex = /require\('([^']+)'\)/g;
485
531
  let match;
486
- while ((match = requireRegex.exec(content)) !== null) {
487
- existing.add(match[1]);
532
+ while ((match = STANDALONE_REQUIRE_REGEX.exec(content)) !== null) {
533
+ if (!isRspackBridgeImport(match[1])) existing.add(match[1]);
488
534
  }
489
535
 
490
536
  // Also check for import statements (used in the new format)
491
- const importRegex = /import\s+'([^']+)'/g;
492
- while ((match = importRegex.exec(content)) !== null) {
493
- existing.add(match[1]);
537
+ while ((match = STANDALONE_IMPORT_REGEX.exec(content)) !== null) {
538
+ if (!isRspackBridgeImport(match[1])) existing.add(match[1]);
494
539
  }
495
540
  } catch {
496
541
  // ignore if file missing or unreadable
package/rspack.config.js CHANGED
@@ -2,8 +2,7 @@ const { DefinePlugin, BannerPlugin, NormalModuleReplacementPlugin } = require('@
2
2
  const fs = require('fs');
3
3
  const { inspect } = require('node:util');
4
4
  const path = require('path');
5
- const { merge } = require('webpack-merge');
6
- const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
5
+ const { merge } = require('rspack-merge');
7
6
 
8
7
  const { cleanOmittedPaths, mergeSplitOverlap } = require("./lib/mergeRulesSplitOverlap.js");
9
8
  const { getMeteorAppSwcConfig } = require('./lib/swc.js');
@@ -30,7 +29,7 @@ const {
30
29
  const { loadUserAndOverrideConfig } = require('./lib/meteorRspackConfigHelpers.js');
31
30
  const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory");
32
31
  const { extractLocalDependencies } = require('./lib/localDependenciesHelpers.js');
33
-
32
+ const { createTestClientNodePolyfillConfig } = require("./lib/testClientNodePolyfills.js");
34
33
 
35
34
  // Safe require that doesn't throw if the module isn't found
36
35
  function safeRequire(moduleName) {
@@ -95,21 +94,22 @@ function createCacheStrategy(
95
94
  ].filter(Boolean);
96
95
 
97
96
  return {
98
- cache: true,
99
- experiments: {
100
- cache: {
101
- version: `cache-${mode}${(side && `-${side}`) || ""}`,
102
- type: "persistent",
103
- storage: {
104
- type: "filesystem",
105
- directory: `node_modules/.cache/rspack/${
106
- [buildContext, side].filter(Boolean).join('-') || 'default'
107
- }`,
108
- },
109
- ...(buildDependencies.length > 0 && {
110
- buildDependencies: buildDependencies,
111
- })
97
+ cache: {
98
+ version: `cache-${mode}${(side && `-${side}`) || ""}`,
99
+ type: "persistent",
100
+ storage: {
101
+ type: "filesystem",
102
+ // Rspack invalidates a persistent cache on version mismatch. Keep
103
+ // development and production in separate directories so switching
104
+ // between `meteor run` and `meteor build` leaves both caches warm.
105
+ // See meteor/meteor#14568.
106
+ directory: `node_modules/.cache/rspack/${
107
+ [buildContext, side, mode].filter(Boolean).join('-') || 'default'
108
+ }`,
112
109
  },
110
+ ...(buildDependencies.length > 0 && {
111
+ buildDependencies: buildDependencies,
112
+ })
113
113
  },
114
114
  };
115
115
  }
@@ -127,15 +127,13 @@ function createSwcConfig({
127
127
  }) {
128
128
  const defaultConfig = {
129
129
  jsc: {
130
- baseUrl: process.cwd(),
131
- paths: { '/*': ['*', '/*'] },
132
130
  parser: {
133
131
  syntax: isTypescriptEnabled ? 'typescript' : 'ecmascript',
134
132
  ...(isTsxEnabled && { tsx: true }),
135
133
  ...(isJsxEnabled && { jsx: true }),
136
134
  ...(isAngularEnabled && { decorators: true }),
137
135
  },
138
- target: 'es2015',
136
+ target: isClient ? 'es2015' : 'es2022',
139
137
  ...(isReactEnabled && {
140
138
  transform: {
141
139
  react: {
@@ -404,7 +402,11 @@ module.exports = async function (inMeteor = {}, argv = {}) {
404
402
  cacheStrategy = createCacheStrategy(
405
403
  mode,
406
404
  (Meteor.isClient && "client") || "server",
407
- { projectConfigPath, configPath }
405
+ // buildContext must be passed here too: this reassignment is the
406
+ // effective cache strategy, and omitting it made the cache directory
407
+ // collide across build contexts (e.g. custom METEOR_LOCAL_DIR setups).
408
+ // See meteor/meteor#14568.
409
+ { projectConfigPath, configPath, buildContext }
408
410
  );
409
411
 
410
412
  // Determine run point
@@ -426,7 +428,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
426
428
  // Additional ignore entries
427
429
  const additionalEntries = [
428
430
  "**/.meteor/local/**",
429
- "**/dist/**",
431
+ path.join(projectDir, "dist", "**").replace(/\\/g, "/"),
430
432
  ...(isTest && isTestEager
431
433
  ? [`**/${buildContext}/**`, "**/.meteor/local/**", "node_modules/**"]
432
434
  : []),
@@ -439,7 +441,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
439
441
  ],
440
442
  };
441
443
 
442
- if (Meteor.isDebug || Meteor.isVerbose) {
444
+ if (Meteor.isVerbose) {
443
445
  console.log("[i] Rspack mode:", mode);
444
446
  console.log("[i] Meteor flags:", Meteor);
445
447
  }
@@ -462,9 +464,6 @@ module.exports = async function (inMeteor = {}, argv = {}) {
462
464
  ...(isReactEnabled ? [/^react$/, /^react-dom$/] : []),
463
465
  ...(isServer ? [/^bcrypt$/] : []),
464
466
  ];
465
- const alias = {
466
- "/": path.resolve(process.cwd()),
467
- };
468
467
  const fallback = {
469
468
  ...(isClient && makeWebNodeBuiltinsAlias()),
470
469
  };
@@ -485,13 +484,15 @@ module.exports = async function (inMeteor = {}, argv = {}) {
485
484
  const reactRefreshModule = isReactEnabled
486
485
  ? safeRequire("@rspack/plugin-react-refresh")
487
486
  : null;
487
+ const ReactRefreshRspackPlugin =
488
+ reactRefreshModule?.ReactRefreshRspackPlugin || reactRefreshModule;
488
489
 
489
490
  const requireExternalsPlugin = new RequireExternalsPlugin({
490
491
  filePath: path.join(buildContext, runPath),
491
492
  ...(Meteor.isBlazeEnabled && {
492
493
  externals: /\.html$/,
493
494
  isEagerImport: (module) => module.endsWith(".html"),
494
- ...(isProd && {
495
+ ...((isProd || (isTest && isClient)) && {
495
496
  lastImports: [`./${outputFilename}`],
496
497
  }),
497
498
  }),
@@ -620,7 +621,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
620
621
  if (isMainChunk) return `../${buildContext}/${outputPath}`;
621
622
  return chunkSuffix;
622
623
  },
623
- libraryTarget: "commonjs2",
624
+ library: { type: "commonjs2" },
624
625
  publicPath: "/",
625
626
  chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
626
627
  assetModuleFilename,
@@ -650,14 +651,24 @@ module.exports = async function (inMeteor = {}, argv = {}) {
650
651
  { test: /\.css$/, type: "css/auto" },
651
652
  ...extraRules,
652
653
  ],
654
+ parser: {
655
+ javascript: {
656
+ // Relax Rspack 2.0 strict ESM linking; SWC-stripped TS type re-exports otherwise fail the build.
657
+ exportsPresence: "warn",
658
+ },
659
+ },
660
+ },
661
+ resolve: {
662
+ extensions,
663
+ fallback,
664
+ roots: [path.resolve(projectDir)],
653
665
  },
654
- resolve: { extensions, alias, fallback },
655
666
  externals,
656
667
  externalsType: "commonjs2",
657
668
  plugins: [
658
669
  ...[
659
- ...(isReactEnabled && reactRefreshModule && isDevEnvironment
660
- ? [new reactRefreshModule()]
670
+ ...(isReactEnabled && ReactRefreshRspackPlugin && isDevEnvironment
671
+ ? [new ReactRefreshRspackPlugin()]
661
672
  : []),
662
673
  requireExternalsPlugin,
663
674
  assetExternalsPlugin,
@@ -740,7 +751,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
740
751
  output: {
741
752
  path: serverOutputDir,
742
753
  filename: () => `../${buildContext}/${outputPath}`,
743
- libraryTarget: "commonjs2",
754
+ library: { type: "commonjs2" },
744
755
  chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
745
756
  assetModuleFilename,
746
757
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
@@ -771,14 +782,16 @@ module.exports = async function (inMeteor = {}, argv = {}) {
771
782
  javascript: {
772
783
  // Dynamic imports on the server are treated as bundled in the same chunk
773
784
  dynamicImportMode: "eager",
785
+ // Relax Rspack 2.0 strict ESM linking; SWC-stripped TS type re-exports otherwise fail the build.
786
+ exportsPresence: "warn",
774
787
  },
775
788
  },
776
789
  },
777
790
  resolve: {
778
791
  extensions,
779
- alias,
780
792
  modules: ["node_modules", path.resolve(projectDir)],
781
793
  conditionNames: ["import", "require", "node", "default"],
794
+ roots: [path.resolve(projectDir)],
782
795
  },
783
796
  externals,
784
797
  externalsType: "commonjs2",
@@ -814,8 +827,12 @@ module.exports = async function (inMeteor = {}, argv = {}) {
814
827
  isDevEnvironment || isNative || isTest
815
828
  ? "source-map"
816
829
  : "hidden-source-map",
817
- ...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
818
- cacheStrategy),
830
+ // Apply the persistent cache to production builds too (the client
831
+ // config always has it); previously `meteor build` recompiled the
832
+ // entire server bundle cold every time. Eager server test builds are
833
+ // still excluded, since their generated entry changes on every run.
834
+ // See meteor/meteor#14568.
835
+ ...(!(isTest && isTestEager) && cacheStrategy),
819
836
  ...lazyCompilationConfig,
820
837
  ...loggingConfig,
821
838
  };
@@ -827,7 +844,18 @@ module.exports = async function (inMeteor = {}, argv = {}) {
827
844
  devServer: { port: devServerPort },
828
845
  stats: { preset: "normal" },
829
846
  infrastructureLogging: { level: "info" },
830
- ...(isProd && isClient && { output: { module: false } }),
847
+ ...(isProd &&
848
+ isClient && {
849
+ output: {
850
+ // Nx Angular config emits ESM chunks in production by default.
851
+ // Meteor serves and minifies classic browser bundles here.
852
+ module: false,
853
+ scriptType: false,
854
+ chunkFormat: "array-push",
855
+ chunkLoading: "jsonp",
856
+ workerChunkLoading: "import-scripts",
857
+ },
858
+ }),
831
859
  }
832
860
  : {};
833
861
 
@@ -847,7 +875,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
847
875
  optimization: {
848
876
  splitChunks: false,
849
877
  },
850
- plugins: [new NodePolyfillPlugin()],
878
+ ...createTestClientNodePolyfillConfig(),
851
879
  }
852
880
  : {};
853
881
 
@@ -904,8 +932,8 @@ module.exports = async function (inMeteor = {}, argv = {}) {
904
932
 
905
933
  delete config["meteor.enablePortableBuild"];
906
934
 
907
- if (Meteor.isDebug || Meteor.isVerbose) {
908
- console.log("Config:", inspect(config, { depth: null, colors: true }));
935
+ if (Meteor.isVerbose) {
936
+ console.log("Config:", inspect(config, { depth: null, colors: true }));
909
937
  }
910
938
 
911
939
  // Check if lazyCompilation is enabled and warn the user
@@ -922,19 +950,42 @@ module.exports = async function (inMeteor = {}, argv = {}) {
922
950
 
923
951
  // Add MeteorRspackOutputPlugin as the last plugin to output compilation info
924
952
  const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({
925
- getData: (stats, { isRebuild, compilationCount, compiler }) => ({
926
- name: config.name,
927
- mode: config.mode,
928
- hasErrors: stats.hasErrors(),
929
- hasWarnings: stats.hasWarnings(),
930
- timestamp: Date.now(),
931
- statsOverrided,
932
- compilationCount,
933
- isRebuild,
934
- ...(!isRebuild && compiler && {
935
- delegatedExtensions: extractDelegatedExtensions(stats, compiler),
936
- }),
937
- }),
953
+ getData: (stats, { isRebuild, compilationCount, compiler }) => {
954
+ const generatedAppPaths = [
955
+ path.join('.meteor', 'local'),
956
+ path.join('.meteor', 'types'),
957
+ buildContext,
958
+ ];
959
+ const changedFiles = [
960
+ ...(compiler?.modifiedFiles || []),
961
+ ...(compiler?.removedFiles || []),
962
+ ];
963
+ const hasAppSourceChanges =
964
+ changedFiles.length === 0 ||
965
+ changedFiles.some(file => {
966
+ const relativePath = path.relative(projectDir, file);
967
+ return !generatedAppPaths.some(generatedPath =>
968
+ relativePath === generatedPath ||
969
+ relativePath.startsWith(`${generatedPath}${path.sep}`)
970
+ );
971
+ });
972
+
973
+ return {
974
+ name: config.name,
975
+ mode: config.mode,
976
+ hasErrors: stats.hasErrors(),
977
+ hasWarnings: stats.hasWarnings(),
978
+ hasAppSourceChanges,
979
+ hash: stats.hash,
980
+ timestamp: Date.now(),
981
+ statsOverrided,
982
+ compilationCount,
983
+ isRebuild,
984
+ ...(!isRebuild && compiler && {
985
+ delegatedExtensions: extractDelegatedExtensions(stats, compiler),
986
+ }),
987
+ };
988
+ },
938
989
  });
939
990
  config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])];
940
991
 
@@ -1,10 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(chmod +x:*)",
5
- "Bash(bash:*)",
6
- "Bash(npm install:*)",
7
- "Bash(node:*)"
8
- ]
9
- }
10
- }