@meteorjs/rspack 2.1.0-beta.0 → 2.2.0-beta.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.
@@ -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');
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
@@ -74,14 +74,14 @@ const generateEagerTestFile = ({
74
74
  exclude: ${excludeFoldersRegex.toString()},
75
75
  mode: 'eager',
76
76
  });
77
- ctx.keys().filter((k) => {
77
+ await Promise.all(ctx.keys().filter((k) => {
78
78
  ${
79
79
  excludeMeteorIgnoreRegex
80
80
  ? `// Only exclude based on *relative* path segments.
81
81
  return !MeteorIgnoreRegex.test(k);`
82
82
  : "return true;"
83
83
  }
84
- }).forEach(ctx);
84
+ }).map(ctx));
85
85
  ${
86
86
  extraEntry
87
87
  ? `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": "2.1.0-beta.0",
3
+ "version": "2.2.0-beta.0",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -13,7 +13,7 @@
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",
16
+ "isomorphic-timers-promises": "^1.0.1",
17
17
  "webpack-merge": "^6.0.1"
18
18
  },
19
19
  "devDependencies": {
@@ -22,6 +22,12 @@
22
22
  "peerDependencies": {
23
23
  "@rspack/cli": ">=1.3.0",
24
24
  "@rspack/core": ">=1.3.0",
25
- "@swc/core": ">=1.3.0"
25
+ "@swc/core": ">=1.3.0",
26
+ "meteor-node-stubs": ">=1.2.0"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "meteor-node-stubs": {
30
+ "optional": true
31
+ }
26
32
  }
27
33
  }
@@ -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({
@@ -160,8 +172,24 @@ class RequireExternalsPlugin {
160
172
  // 2) Re-load existing requires from disk on every run
161
173
  const existing = this._readExistingRequires();
162
174
 
163
- // 2a) Compute the *current* externals in this build
164
- const info = stats.toJson({ modules: true });
175
+ // 2a) Compute the *current* externals in this build.
176
+ // Only module names are needed, so start from `all: false` instead of
177
+ // serializing the whole stats object (chunks, assets, reasons, ...)
178
+ // on every compilation — a fixed cost per rebuild that grows with app
179
+ // size. `cachedModules: true` keeps unchanged modules in the list on
180
+ // incremental rebuilds, so their requires are not wrongly removed.
181
+ // `orphanModules: true` is required in production: module
182
+ // concatenation absorbs the externals' importers, marking the
183
+ // external modules themselves as orphans, and without this flag
184
+ // they vanish from the list entirely — which stripped every
185
+ // mirrored require (meteor/*, Blaze .html) from the meteor entry.
186
+ // See meteor/meteor#14568.
187
+ const info = stats.toJson({
188
+ all: false,
189
+ modules: true,
190
+ cachedModules: true,
191
+ orphanModules: true,
192
+ });
165
193
  const current = new Set();
166
194
  for (const m of info.modules) {
167
195
  const matchInfo = this._isExternalModule(m.name);
@@ -173,15 +201,22 @@ class RequireExternalsPlugin {
173
201
  }
174
202
  }
175
203
 
176
- // 2b) Remove any requires that are no longer in `current`
204
+ // 2b) Remove any dependencies that are no longer in `current`
177
205
  const toRemove = [...existing].filter(p => !current.has(p));
178
206
  if (toRemove.length) {
179
207
  let content = fs.readFileSync(this.filePath, 'utf-8');
180
208
 
181
- // Strip stale require(...) lines
209
+ // Strip stale requires and imports generated by this plugin. Other
210
+ // standalone imports link Meteor entry and Rspack output files and
211
+ // must remain intact.
182
212
  for (const pkg of toRemove) {
183
- const re = new RegExp(`^.*require\\('${pkg}'\\);?.*(\\r?\\n)?`, 'gm');
184
- content = content.replace(re, '');
213
+ const requireRe = buildStandaloneRequireRegex(pkg);
214
+ const importRe = buildStandaloneImportRegex(pkg);
215
+ content = content.replace(requireRe, '');
216
+ content = content.replace(
217
+ MANAGED_IMPORT_BLOCK_REGEX,
218
+ (block) => block.replace(importRe, '')
219
+ );
185
220
  }
186
221
 
187
222
  // Strip out any now-empty helper functions:
@@ -206,11 +241,11 @@ class RequireExternalsPlugin {
206
241
  // Re-populate `existing` so the add-diff is accurate
207
242
  existing.clear();
208
243
  // Check for require statements
209
- for (const match of content.matchAll(/require\('([^']+)'\)/g)) {
244
+ for (const match of content.matchAll(STANDALONE_REQUIRE_REGEX)) {
210
245
  existing.add(match[1]);
211
246
  }
212
247
  // Also check for import statements (used in the new format)
213
- for (const match of content.matchAll(/import\s+'([^']+)'/g)) {
248
+ for (const match of content.matchAll(STANDALONE_IMPORT_REGEX)) {
214
249
  existing.add(match[1]);
215
250
  }
216
251
  }
@@ -478,19 +513,22 @@ class RequireExternalsPlugin {
478
513
 
479
514
  _readExistingRequires() {
480
515
  const existing = new Set();
516
+ // Generated Rspack bridge imports are not managed externals. Relative
517
+ // imports such as Blaze HTML files still belong to this plugin.
518
+ const isRspackBridgeImport = (modulePath) =>
519
+ typeof modulePath === 'string' &&
520
+ /(?:^|[/\\])[^/\\]*-rspack\.(?:js|cjs)$/.test(modulePath);
481
521
  try {
482
522
  const content = fs.readFileSync(this.filePath, 'utf-8');
483
523
  // Check for require statements
484
- const requireRegex = /require\('([^']+)'\)/g;
485
524
  let match;
486
- while ((match = requireRegex.exec(content)) !== null) {
487
- existing.add(match[1]);
525
+ while ((match = STANDALONE_REQUIRE_REGEX.exec(content)) !== null) {
526
+ if (!isRspackBridgeImport(match[1])) existing.add(match[1]);
488
527
  }
489
528
 
490
529
  // 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]);
530
+ while ((match = STANDALONE_IMPORT_REGEX.exec(content)) !== null) {
531
+ if (!isRspackBridgeImport(match[1])) existing.add(match[1]);
494
532
  }
495
533
  } catch {
496
534
  // ignore if file missing or unreadable
package/rspack.config.js CHANGED
@@ -3,7 +3,6 @@ const fs = require('fs');
3
3
  const { inspect } = require('node:util');
4
4
  const path = require('path');
5
5
  const { merge } = require('webpack-merge');
6
- const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
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) {
@@ -102,8 +101,13 @@ function createCacheStrategy(
102
101
  type: "persistent",
103
102
  storage: {
104
103
  type: "filesystem",
104
+ // Include the mode in the directory (not just the version):
105
+ // rspack invalidates a persistent cache on version mismatch, so
106
+ // sharing one directory between development and production would
107
+ // wipe the cache on every `meteor run` <-> `meteor build` switch
108
+ // instead of keeping both warm. See meteor/meteor#14568.
105
109
  directory: `node_modules/.cache/rspack/${
106
- [buildContext, side].filter(Boolean).join('-') || 'default'
110
+ [buildContext, side, mode].filter(Boolean).join('-') || 'default'
107
111
  }`,
108
112
  },
109
113
  ...(buildDependencies.length > 0 && {
@@ -127,15 +131,13 @@ function createSwcConfig({
127
131
  }) {
128
132
  const defaultConfig = {
129
133
  jsc: {
130
- baseUrl: process.cwd(),
131
- paths: { '/*': ['*', '/*'] },
132
134
  parser: {
133
135
  syntax: isTypescriptEnabled ? 'typescript' : 'ecmascript',
134
136
  ...(isTsxEnabled && { tsx: true }),
135
137
  ...(isJsxEnabled && { jsx: true }),
136
138
  ...(isAngularEnabled && { decorators: true }),
137
139
  },
138
- target: 'es2015',
140
+ target: isClient ? 'es2015' : 'es2022',
139
141
  ...(isReactEnabled && {
140
142
  transform: {
141
143
  react: {
@@ -404,7 +406,11 @@ module.exports = async function (inMeteor = {}, argv = {}) {
404
406
  cacheStrategy = createCacheStrategy(
405
407
  mode,
406
408
  (Meteor.isClient && "client") || "server",
407
- { projectConfigPath, configPath }
409
+ // buildContext must be passed here too: this reassignment is the
410
+ // effective cache strategy, and omitting it made the cache directory
411
+ // collide across build contexts (e.g. custom METEOR_LOCAL_DIR setups).
412
+ // See meteor/meteor#14568.
413
+ { projectConfigPath, configPath, buildContext }
408
414
  );
409
415
 
410
416
  // Determine run point
@@ -426,7 +432,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
426
432
  // Additional ignore entries
427
433
  const additionalEntries = [
428
434
  "**/.meteor/local/**",
429
- "**/dist/**",
435
+ path.join(projectDir, "dist", "**").replace(/\\/g, "/"),
430
436
  ...(isTest && isTestEager
431
437
  ? [`**/${buildContext}/**`, "**/.meteor/local/**", "node_modules/**"]
432
438
  : []),
@@ -491,7 +497,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
491
497
  ...(Meteor.isBlazeEnabled && {
492
498
  externals: /\.html$/,
493
499
  isEagerImport: (module) => module.endsWith(".html"),
494
- ...(isProd && {
500
+ ...((isProd || (isTest && isClient)) && {
495
501
  lastImports: [`./${outputFilename}`],
496
502
  }),
497
503
  }),
@@ -650,7 +656,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
650
656
  ...extraRules,
651
657
  ],
652
658
  },
653
- resolve: { extensions, alias, fallback },
659
+ resolve: { extensions, alias, fallback, roots: [path.resolve(projectDir)] },
654
660
  externals,
655
661
  plugins: [
656
662
  ...[
@@ -774,6 +780,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
774
780
  alias,
775
781
  modules: ["node_modules", path.resolve(projectDir)],
776
782
  conditionNames: ["import", "require", "node", "default"],
783
+ roots: [path.resolve(projectDir)],
777
784
  },
778
785
  externals,
779
786
  externalsPresets: { node: true },
@@ -808,8 +815,12 @@ module.exports = async function (inMeteor = {}, argv = {}) {
808
815
  isDevEnvironment || isNative || isTest
809
816
  ? "source-map"
810
817
  : "hidden-source-map",
811
- ...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
812
- cacheStrategy),
818
+ // Apply the persistent cache to production builds too (the client
819
+ // config always has it); previously `meteor build` recompiled the
820
+ // entire server bundle cold every time. Eager server test builds are
821
+ // still excluded, since their generated entry changes on every run.
822
+ // See meteor/meteor#14568.
823
+ ...(!(isTest && isTestEager) && cacheStrategy),
813
824
  ...lazyCompilationConfig,
814
825
  ...loggingConfig,
815
826
  };
@@ -841,7 +852,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
841
852
  optimization: {
842
853
  splitChunks: false,
843
854
  },
844
- plugins: [new NodePolyfillPlugin()],
855
+ ...createTestClientNodePolyfillConfig(),
845
856
  }
846
857
  : {};
847
858