@meteorjs/rspack 0.0.56 → 0.0.58

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.
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const { mergeWithCustomize } = require('webpack-merge');
7
+ const isEqual = require('fast-deep-equal');
7
8
 
8
9
  /**
9
10
  * File extensions to check when determining rule overlaps.
@@ -87,6 +88,10 @@ function splitOverlapRulesMerge(aRules, bRules) {
87
88
  for (let i = 0; i < result.length; i++) {
88
89
  const aRule = result[i];
89
90
 
91
+
92
+ const isMergeableRule = isEqual(aRule?.include || [], bRule?.include || []);
93
+ if (!isMergeableRule) continue;
94
+
90
95
  // Determine which extensions each rule matches (within our catalog)
91
96
  const aExts = EXT_CATALOG.filter(ext => ruleMatchesExt(aRule, ext));
92
97
  const bExts = EXT_CATALOG.filter(ext => ruleMatchesExt(bRule, ext));
@@ -0,0 +1,96 @@
1
+ // meteorRspackConfigFactory.js
2
+
3
+ const { mergeSplitOverlap } = require("./mergeRulesSplitOverlap.js");
4
+
5
+ const DEFAULT_PREFIX = "meteorRspackConfig";
6
+ let counter = 0;
7
+
8
+ /**
9
+ * Create a uniquely keyed Rspack config fragment.
10
+ * Example return: { meteorRspackConfig1: { ...customConfig } }
11
+ *
12
+ * @param {object} customConfig
13
+ * @param {{ key?: number|string, prefix?: string }} [opts]
14
+ * @returns {Record<string, object>}
15
+ */
16
+ function prepareMeteorRspackConfig(customConfig, opts = {}) {
17
+ if (!customConfig || typeof customConfig !== "object") {
18
+ throw new TypeError("customConfig must be an object");
19
+ }
20
+ const prefix = opts.prefix || DEFAULT_PREFIX;
21
+
22
+ let name;
23
+ if (opts.key != null) {
24
+ const k = String(opts.key).trim();
25
+ if (/^\d+$/.test(k)) name = `${prefix}${k}`;
26
+ else if (k.startsWith(prefix) && /^\d+$/.test(k.slice(prefix.length)))
27
+ name = k;
28
+ else
29
+ throw new Error(`opts.key must be a positive integer or "${prefix}<n>"`);
30
+
31
+ const n = parseInt(name.slice(prefix.length), 10);
32
+ if (Number.isFinite(n) && n > counter) counter = n;
33
+ } else {
34
+ counter += 1;
35
+ name = `${prefix}${counter}`;
36
+ }
37
+
38
+ return { [name]: customConfig };
39
+ }
40
+
41
+ /**
42
+ * Merge all `{prefix}<n>` fragments into `config` using `mergeSplitOverlap`,
43
+ * then remove those temporary keys. Mutates `config`.
44
+ *
45
+ * Order: fragments are applied in ascending numeric order (1, 2, 3, ...).
46
+ *
47
+ * @param {object} config
48
+ * @param {{ prefix?: string }} [opts]
49
+ * @returns {object} The same (mutated) config instance.
50
+ */
51
+ function mergeMeteorRspackFragments(config, opts = {}) {
52
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
53
+ throw new TypeError("config must be a plain object");
54
+ }
55
+ const prefix = opts.prefix || DEFAULT_PREFIX;
56
+
57
+ // Collect fragment keys like "meteorRspackConfig12"
58
+ const tempKeys = Object.keys(config)
59
+ .filter((k) => k.startsWith(prefix) && /^\d+$/.test(k.slice(prefix.length)))
60
+ .map((k) => [k, parseInt(k.slice(prefix.length), 10)])
61
+ .sort((a, b) => a[1] - b[1])
62
+ .map(([k]) => k);
63
+
64
+ if (tempKeys.length === 0) return config;
65
+
66
+ // Apply each fragment with your merge policy
67
+ for (const k of tempKeys) {
68
+ const fragment = config[k];
69
+ if (!fragment || typeof fragment !== "object" || Array.isArray(fragment)) {
70
+ throw new Error(`Fragment "${k}" must be a plain object`);
71
+ }
72
+ const merged = mergeSplitOverlap(config, fragment);
73
+
74
+ // Keep object identity: replace contents of `config` with `merged`
75
+ replaceObject(config, merged);
76
+ }
77
+
78
+ // Strip the temp keys at the end
79
+ for (const k of tempKeys) delete config[k];
80
+
81
+ return config;
82
+ }
83
+
84
+ function replaceObject(target, source) {
85
+ for (const key of Object.keys(target)) {
86
+ if (!(key in source)) delete target[key];
87
+ }
88
+ for (const key of Object.keys(source)) {
89
+ target[key] = source[key];
90
+ }
91
+ }
92
+
93
+ module.exports = {
94
+ prepareMeteorRspackConfig,
95
+ mergeMeteorRspackFragments,
96
+ };
@@ -0,0 +1,81 @@
1
+ const path = require("path");
2
+ const { prepareMeteorRspackConfig } = require("./meteorRspackConfigFactory");
3
+ const { builtinModules } = require("module");
4
+
5
+ /**
6
+ * Resolve a package directory from node resolution.
7
+ * @param {string} pkg
8
+ * @returns {string} absolute directory of the package
9
+ */
10
+ function pkgDir(pkg) {
11
+ const resolved = require.resolve(`${pkg}/package.json`, {
12
+ paths: [process.cwd()],
13
+ });
14
+ return path.dirname(resolved);
15
+ }
16
+
17
+ /**
18
+ * Wrap externals for Meteor runtime (marks deps as externals).
19
+ * Usage: compileWithMeteor(["sharp", "vimeo", "fs"])
20
+ *
21
+ * @param {string[]} deps - package names or module IDs
22
+ * @returns {Record<string, object>} `{ meteorRspackConfigX: { externals: [...] } }`
23
+ */
24
+ function compileWithMeteor(deps) {
25
+ const flat = deps.flat().filter(Boolean);
26
+ return prepareMeteorRspackConfig({
27
+ externals: flat,
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Add SWC transpilation rules limited to specific deps (monorepo-friendly).
33
+ * Usage: compileWithRspack(["@org/lib-a", "zod"])
34
+ *
35
+ * Requires global `Meteor.swcConfigOptions` (as in your setup).
36
+ *
37
+ * @param {string[]} deps - package names to include in SWC loader
38
+ * @returns {Record<string, object>} `{ meteorRspackConfigX: { module: { rules: [...] } } }`
39
+ */
40
+ function compileWithRspack(deps, { options = {} } = {}) {
41
+ const includeDirs = deps.flat().filter(Boolean).map(pkgDir);
42
+
43
+ return prepareMeteorRspackConfig({
44
+ module: {
45
+ rules: [
46
+ {
47
+ test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i,
48
+ include: includeDirs,
49
+ loader: "builtin:swc-loader",
50
+ options,
51
+ },
52
+ ],
53
+ },
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Build an alias map that disables ALL Node core modules in a web build.
59
+ * - Includes both 'fs' and 'node:fs' keys
60
+ * - Optional extras let you block non-core modules too
61
+ */
62
+ function makeWebNodeBuiltinsAlias(extras = []) {
63
+ // Strip potential 'node:' prefixes then add both forms
64
+ const core = new Set(builtinModules.map((m) => m.replace(/^node:/, "")));
65
+
66
+ const names = new Set();
67
+ for (const m of core) {
68
+ names.add(m); // e.g. 'fs'
69
+ names.add(`node:${m}`); // e.g. 'node:fs'
70
+ }
71
+ for (const x of extras) names.add(x);
72
+
73
+ // Map every name to false (causes hard error if imported)
74
+ return Object.fromEntries([...names].map((m) => [m, false]));
75
+ }
76
+
77
+ module.exports = {
78
+ compileWithMeteor,
79
+ compileWithRspack,
80
+ makeWebNodeBuiltinsAlias,
81
+ };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "author": "",
8
8
  "license": "ISC",
9
9
  "dependencies": {
10
+ "fast-deep-equal": "^3.1.3",
10
11
  "ignore-loader": "^0.1.2",
11
12
  "webpack-merge": "^6.0.1"
12
13
  },
package/rspack.config.js CHANGED
@@ -1,4 +1,4 @@
1
- const { DefinePlugin, BannerPlugin } = require('@rspack/core');
1
+ const { DefinePlugin, BannerPlugin, NormalModuleReplacementPlugin } = require('@rspack/core');
2
2
  const fs = require('fs');
3
3
  const { inspect } = require('node:util');
4
4
  const path = require('path');
@@ -10,6 +10,12 @@ const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js');
10
10
  const { RequireExternalsPlugin } = require('./plugins/RequireExtenalsPlugin.js');
11
11
  const { generateEagerTestFile } = require("./lib/test.js");
12
12
  const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore");
13
+ const { mergeMeteorRspackFragments } = require("./lib/meteorRspackConfigFactory.js");
14
+ const {
15
+ compileWithMeteor,
16
+ compileWithRspack,
17
+ makeWebNodeBuiltinsAlias,
18
+ } = require('./lib/meteorRspackHelpers.js');
13
19
 
14
20
  // Safe require that doesn't throw if the module isn't found
15
21
  function safeRequire(moduleName) {
@@ -27,17 +33,52 @@ function safeRequire(moduleName) {
27
33
  }
28
34
 
29
35
  // Persistent filesystem cache strategy
30
- function createCacheStrategy(mode, side) {
36
+ function createCacheStrategy(mode, side, { projectConfigPath, configPath } = {}) {
37
+ // Check for configuration files
38
+ const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
39
+ const hasTsconfig = fs.existsSync(tsconfigPath);
40
+ const babelRcConfig = path.join(process.cwd(), '.babelrc');
41
+ const hasBabelRcConfig = fs.existsSync(babelRcConfig);
42
+ const babelJsConfig = path.join(process.cwd(), 'babel.config.js');
43
+ const hasBabelJsConfig = fs.existsSync(babelJsConfig);
44
+ const swcrcPath = path.join(process.cwd(), '.swcrc');
45
+ const hasSwcrcConfig = fs.existsSync(swcrcPath);
46
+ const swcJsPath = path.join(process.cwd(), 'swc.config.js');
47
+ const hasSwcJsConfig = fs.existsSync(swcJsPath);
48
+ const postcssConfigPath = path.join(process.cwd(), 'postcss.config.js');
49
+ const hasPostcssConfig = fs.existsSync(postcssConfigPath);
50
+ const packageLockPath = path.join(process.cwd(), 'package-lock.json');
51
+ const hasPackageLock = fs.existsSync(packageLockPath);
52
+ const yarnLockPath = path.join(process.cwd(), 'yarn.lock');
53
+ const hasYarnLock = fs.existsSync(yarnLockPath);
54
+
55
+ // Build dependencies array
56
+ const buildDependencies = [
57
+ ...(projectConfigPath ? [projectConfigPath] : []),
58
+ ...(configPath ? [configPath] : []),
59
+ ...(hasTsconfig ? [tsconfigPath] : []),
60
+ ...(hasBabelRcConfig ? [babelRcConfig] : []),
61
+ ...(hasBabelJsConfig ? [babelJsConfig] : []),
62
+ ...(hasSwcrcConfig ? [swcrcPath] : []),
63
+ ...(hasSwcJsConfig ? [swcJsPath] : []),
64
+ ...(hasPostcssConfig ? [postcssConfigPath] : []),
65
+ ...(hasPackageLock ? [packageLockPath] : []),
66
+ ...(hasYarnLock ? [yarnLockPath] : []),
67
+ ].filter(Boolean);
68
+
31
69
  return {
32
70
  cache: true,
33
71
  experiments: {
34
72
  cache: {
35
- version: `swc-${mode}${(side && `-${side}`) || ""}`,
73
+ version: `cache-${mode}${(side && `-${side}`) || ""}`,
36
74
  type: "persistent",
37
75
  storage: {
38
76
  type: "filesystem",
39
77
  directory: `node_modules/.cache/rspack${(side && `/${side}`) || ""}`,
40
78
  },
79
+ ...(buildDependencies.length > 0 && {
80
+ buildDependencies: buildDependencies,
81
+ })
41
82
  },
42
83
  },
43
84
  };
@@ -140,6 +181,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
140
181
  const mode = isProd ? 'production' : 'development';
141
182
  const projectDir = process.cwd();
142
183
  const projectConfigPath = Meteor.projectConfigPath || path.resolve(projectDir, 'rspack.config.js');
184
+ const configPath = Meteor.configPath;
143
185
 
144
186
  const isTypescriptEnabled = Meteor.isTypescriptEnabled || false;
145
187
  const isJsxEnabled =
@@ -176,6 +218,13 @@ module.exports = async function (inMeteor = {}, argv = {}) {
176
218
  const buildOutputDir = path.resolve(projectDir, buildContext, outputDir);
177
219
  Meteor.buildOutputDir = buildOutputDir;
178
220
 
221
+ // Expose Meteor's helpers to expand Rspack configs
222
+ Meteor.compileWithMeteor = deps => compileWithMeteor(deps);
223
+ Meteor.compileWithRspack = deps =>
224
+ compileWithRspack(deps, {
225
+ options: Meteor.swcConfigOptions,
226
+ });
227
+
179
228
  // Add HtmlRspackPlugin function to Meteor
180
229
  Meteor.HtmlRspackPlugin = (options = {}) => {
181
230
  return new HtmlRspackPlugin({
@@ -225,7 +274,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
225
274
  console.log('[i] Meteor flags:', Meteor);
226
275
  }
227
276
 
228
- const enableSwcExternalHelpers = !!swcExternalHelpers;
277
+ const enableSwcExternalHelpers = !isServer && swcExternalHelpers;
229
278
  const isDevEnvironment = isRun && isDev && !isTest && !isNative;
230
279
  const swcConfigRule = createSwcConfig({
231
280
  isTypescriptEnabled,
@@ -247,6 +296,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
247
296
  const alias = {
248
297
  '/': path.resolve(process.cwd()),
249
298
  };
299
+ const fallback = {
300
+ ...(isClient && makeWebNodeBuiltinsAlias()),
301
+ };
250
302
  const extensions = [
251
303
  '.ts',
252
304
  '.tsx',
@@ -309,8 +361,19 @@ module.exports = async function (inMeteor = {}, argv = {}) {
309
361
  entry: path.resolve(process.cwd(), buildContext, entryPath),
310
362
  output: {
311
363
  path: clientOutputDir,
312
- filename: () =>
313
- isDevEnvironment ? outputFilename : `../${buildContext}/${outputPath}`,
364
+ filename: (_module) => {
365
+ const chunkName = _module.chunk?.name;
366
+ const isMainChunk = !chunkName || chunkName === "main";
367
+ const chunkSuffix = `${chunksContext}/[id]${
368
+ isProd ? '.[chunkhash]' : ''
369
+ }.js`;
370
+ if (isDevEnvironment) {
371
+ if (isMainChunk) return outputFilename;
372
+ return chunkSuffix;
373
+ }
374
+ if (isMainChunk) return `../${buildContext}/${outputPath}`;
375
+ return chunkSuffix;
376
+ },
314
377
  libraryTarget: 'commonjs',
315
378
  publicPath: '/',
316
379
  chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
@@ -341,7 +404,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
341
404
  ...extraRules,
342
405
  ],
343
406
  },
344
- resolve: { extensions, alias },
407
+ resolve: { extensions, alias, fallback },
345
408
  externals,
346
409
  plugins: [
347
410
  ...[
@@ -361,6 +424,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
361
424
  ...bannerPluginConfig,
362
425
  Meteor.HtmlRspackPlugin(),
363
426
  ...doctorPluginConfig,
427
+ new NormalModuleReplacementPlugin(/^node:(.*)$/, (res) => {
428
+ res.request = res.request.replace(/^node:/, '');
429
+ }),
364
430
  ],
365
431
  watchOptions,
366
432
  devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
@@ -377,7 +443,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
377
443
  },
378
444
  },
379
445
  }),
380
- ...merge(createCacheStrategy(mode, "client"), { experiments: { css: true } })
446
+ ...merge(createCacheStrategy(mode, "client", { projectConfigPath, configPath }), { experiments: { css: true } })
381
447
  };
382
448
 
383
449
 
@@ -414,7 +480,11 @@ module.exports = async function (inMeteor = {}, argv = {}) {
414
480
  assetModuleFilename: `${assetsContext}/[hash][ext][query]`,
415
481
  ...(isProd && { clean: { keep: keepOutsideBuild() } }),
416
482
  },
417
- optimization: { usedExports: true },
483
+ optimization: {
484
+ usedExports: true,
485
+ splitChunks: false,
486
+ runtimeChunk: false,
487
+ },
418
488
  module: {
419
489
  rules: [swcConfigRule, ...extraRules],
420
490
  parser: {
@@ -431,6 +501,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
431
501
  conditionNames: ['import', 'require', 'node', 'default'],
432
502
  },
433
503
  externals,
504
+ externalsPresets: { node: true },
434
505
  plugins: [
435
506
  new DefinePlugin(
436
507
  isTest && (isTestModule || isTestEager)
@@ -455,7 +526,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
455
526
  watchOptions,
456
527
  devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
457
528
  ...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
458
- createCacheStrategy(mode, "server")),
529
+ createCacheStrategy(mode, "server", { projectConfigPath, configPath })),
459
530
  };
460
531
 
461
532
  // Load and apply project-level overrides for the selected build
@@ -496,13 +567,16 @@ module.exports = async function (inMeteor = {}, argv = {}) {
496
567
  : projectConfig;
497
568
 
498
569
  const omitPaths = [
499
- 'name',
500
- 'target',
501
- 'entry',
502
- 'output.path',
503
- 'output.filename',
504
- 'output.publicPath',
505
- ];
570
+ "name",
571
+ "target",
572
+ "entry",
573
+ "output.path",
574
+ "output.filename",
575
+ "output.publicPath",
576
+ ...(Meteor.isServer
577
+ ? ["optimization.splitChunks", "optimization.runtimeChunk"]
578
+ : []),
579
+ ].filter(Boolean);
506
580
  const warningFn = path => {
507
581
  console.warn(
508
582
  `[rspack.config.js] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`,
@@ -523,7 +597,8 @@ module.exports = async function (inMeteor = {}, argv = {}) {
523
597
  }
524
598
  }
525
599
 
526
- const config = isClient ? clientConfig : serverConfig;
600
+ const sideConfig = isClient ? clientConfig : serverConfig;
601
+ const config = mergeMeteorRspackFragments(sideConfig);
527
602
 
528
603
  if (Meteor.isDebug || Meteor.isVerbose) {
529
604
  console.log('Config:', inspect(config, { depth: null, colors: true }));