@meteorjs/rspack 1.1.0-beta.35 → 1.1.0-beta.37

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/index.d.ts CHANGED
@@ -29,12 +29,14 @@ type MeteorEnv = Record<string, any> & {
29
29
  * A function that creates an instance of HtmlRspackPlugin with default options.
30
30
  * @param options - Optional configuration options that will be merged with defaults
31
31
  * @returns An instance of HtmlRspackPlugin
32
+ * @example Meteor.HtmlRspackPlugin({ title: 'My App' })
32
33
  */
33
34
  HtmlRspackPlugin: (options?: HtmlRspackPluginOptions) => HtmlRspackPlugin;
34
35
  /**
35
36
  * Wrap externals for Meteor runtime.
36
37
  * @param deps - Package names or module IDs
37
38
  * @returns A config object with externals configuration
39
+ * @example ...Meteor.compileWithMeteor(['sharp', 'thread-stream'])
38
40
  */
39
41
  compileWithMeteor: (deps: RuleSetConditions) => Record<string, object>;
40
42
  /**
@@ -42,6 +44,7 @@ type MeteorEnv = Record<string, any> & {
42
44
  * @param deps - Package names to include in SWC loader
43
45
  * @param options - Optional configuration options
44
46
  * @returns A config object with module rules configuration
47
+ * @example ...Meteor.compileWithRspack(['grubba-rpc', 'zod'])
45
48
  */
46
49
  compileWithRspack: (deps: RuleSetConditions, options?: SwcLoaderOptions) => Record<string, object>;
47
50
  /**
@@ -49,32 +52,35 @@ type MeteorEnv = Record<string, any> & {
49
52
  * @param enabled - Whether to enable caching
50
53
  * @param cacheConfig - Optional cache configuration
51
54
  * @returns A config object with cache configuration
55
+ * @example ...Meteor.setCache(false)
52
56
  */
53
57
  setCache: (enabled: boolean | 'memory') => Record<string, object>;
54
58
  /**
55
59
  * Enable Rspack split vendor chunk.
56
60
  * @returns A config object with optimization configuration
61
+ * @example ...Meteor.splitVendorChunk()
57
62
  */
58
63
  splitVendorChunk: () => Record<string, object>;
59
64
  /**
60
65
  * Extend the SWC loader config by smart-merging custom options on top of
61
- * Meteor's defaults. Only the properties you specify are overridden;
66
+ * Meteor's defaults. Only the properties you specify are overridden;
62
67
  * everything else is preserved.
63
68
  * @param swcConfig - SWC loader options to merge with defaults
64
69
  * @returns A config object with SWC loader config
70
+ * @example ...Meteor.extendSwcConfig({ jsc: { parser: { decorators: true } } })
65
71
  */
66
72
  extendSwcConfig: (swcConfig: SwcLoaderOptions) => Record<string, object>;
67
73
  /**
68
74
  * Replace the SWC loader config entirely, discarding Meteor's defaults.
69
- * Use this when you need full control over SWC options and don't want any
70
- * automatic merging with Meteor's built-in configuration.
71
75
  * @param swcConfig - Complete SWC loader options (replaces defaults)
72
76
  * @returns A config object with SWC loader config
77
+ * @example ...Meteor.replaceSwcConfig({ jsc: { target: 'es2020' } })
73
78
  */
74
79
  replaceSwcConfig: (swcConfig: SwcLoaderOptions) => Record<string, object>;
75
80
  /**
76
81
  * Extend Rspack configs.
77
82
  * @returns A config object with merged configs
83
+ * @example ...Meteor.extendConfig(configA, configB)
78
84
  */
79
85
  extendConfig: (...configs: Record<string, object>[]) => Record<string, object>;
80
86
 
@@ -82,16 +88,43 @@ type MeteorEnv = Record<string, any> & {
82
88
  * Remove plugins from a Rspack config by name, RegExp, predicate, or array of them.
83
89
  * @param matchers - String, RegExp, function, or array of them to match plugin names
84
90
  * @returns The modified config object
91
+ * @example ...Meteor.disablePlugins(['DefinePlugin', /HtmlRspack/])
85
92
  */
86
93
  disablePlugins: (
87
94
  matchers: string | RegExp | ((plugin: any, index: number) => boolean) | Array<string | RegExp | ((plugin: any, index: number) => boolean)>
88
95
  ) => Record<string, any>;
89
96
  /**
90
97
  * Omit `Meteor.isDevelopment` and `Meteor.isProduction` from the DefinePlugin so
91
- * the bundle is not tied to a specific Meteor environment (portable / isomorphic builds).
98
+ * the bundle is not tied to a specific Meteor environment (portable builds).
92
99
  * @returns A config fragment with `meteor.enablePortableBuild: true`
100
+ * @example ...Meteor.enablePortableBuild()
93
101
  */
94
102
  enablePortableBuild: () => Record<string, any>;
103
+ /**
104
+ * Persist build-output files to disk during development.
105
+ * HTML files are always persisted automatically.
106
+ *
107
+ * Matchers: `string` (endsWith), `RegExp`, or `(filePath) => boolean`.
108
+ * Array form defaults to `always`. Object form supports `once` and `always`.
109
+ * - `always` — written on every build (default)
110
+ * - `once` — first build only (e.g. service workers)
111
+ *
112
+ * @param matchers - Array or `{ once?, always? }` of matchers
113
+ * @returns A config fragment with `devServer.devMiddleware.writeToDisk`
114
+ *
115
+ * @example
116
+ * ...Meteor.persistDevFiles({ once: ['sw.js'], always: ['manifest.json'] })
117
+ */
118
+ persistDevFiles: (
119
+ matchers:
120
+ | (string | RegExp | ((filePath: string) => boolean))[]
121
+ | {
122
+ /** Files written on the first build only. */
123
+ once?: (string | RegExp | ((filePath: string) => boolean))[];
124
+ /** Files written on every build. */
125
+ always?: (string | RegExp | ((filePath: string) => boolean))[];
126
+ }
127
+ ) => Record<string, object>;
95
128
  }
96
129
 
97
130
  export type ConfigFactory = (
@@ -244,6 +244,92 @@ function disablePlugins(config, matchers) {
244
244
  return config;
245
245
  }
246
246
 
247
+ /**
248
+ * Create a `writeToDisk` callback that persists specific files to disk
249
+ * during development.
250
+ *
251
+ * Accepts an array (defaults to "always" strategy) or an object with
252
+ * `once` and/or `always` keys for mixed strategies.
253
+ *
254
+ * Matchers can be:
255
+ * - **string**: matched with `endsWith` (e.g. `'sw.js'`, `'.html'`)
256
+ * - **RegExp**: tested against the full file path
257
+ * - **function**: `(filePath: string) => boolean`
258
+ *
259
+ * Strategies:
260
+ * - `always`: Write on every build (default). Use for files that should
261
+ * always reflect the latest build output.
262
+ * - `once`: Write on the first build only. Skipped on HMR rebuilds to
263
+ * avoid triggering service worker re-registration or file
264
+ * watcher restarts.
265
+ *
266
+ * @example
267
+ * // Simple: array defaults to "always"
268
+ * ...Meteor.persistDevFiles(['manifest.json'])
269
+ *
270
+ * // Mixed strategies with strings, regex, and functions
271
+ * ...Meteor.persistDevFiles({
272
+ * once: ['sw.js', /\.worker\.js$/],
273
+ * always: ['manifest.json', (filePath) => filePath.includes('/custom/')],
274
+ * })
275
+ *
276
+ * @param {(string|RegExp|Function)[] | { once?: (string|RegExp|Function)[], always?: (string|RegExp|Function)[] }} matchers
277
+ * @returns {Record<string, object>} config fragment with devServer.devMiddleware.writeToDisk
278
+ */
279
+ /**
280
+ * Build the writeToDisk callback from matchers.
281
+ * Shared by persistDevFiles (fragment) and internal usage (direct).
282
+ * @private
283
+ */
284
+ function createPersistCallback(matchers) {
285
+ const once = [];
286
+ const always = [];
287
+
288
+ if (Array.isArray(matchers)) {
289
+ always.push(...matchers);
290
+ } else {
291
+ if (matchers.once) once.push(...matchers.once);
292
+ if (matchers.always) always.push(...matchers.always);
293
+ }
294
+
295
+ // HTML files are always persisted, Meteor's web server relies on them
296
+ if (!always.includes('.html')) {
297
+ always.push('.html');
298
+ }
299
+
300
+ const match = (filePath, pattern) => {
301
+ if (typeof pattern === 'function') return pattern(filePath);
302
+ if (typeof pattern === 'string') return filePath.endsWith(pattern);
303
+ return pattern.test(filePath);
304
+ };
305
+
306
+ const written = new Set();
307
+
308
+ return (filePath) => {
309
+ for (const pattern of always) {
310
+ if (match(filePath, pattern)) return true;
311
+ }
312
+ for (let i = 0; i < once.length; i++) {
313
+ if (match(filePath, once[i])) {
314
+ if (written.has(i)) return false;
315
+ written.add(i);
316
+ return true;
317
+ }
318
+ }
319
+ return false;
320
+ };
321
+ }
322
+
323
+ function persistDevFiles(matchers) {
324
+ return prepareMeteorRspackConfig({
325
+ devServer: {
326
+ devMiddleware: {
327
+ writeToDisk: createPersistCallback(matchers),
328
+ },
329
+ },
330
+ });
331
+ }
332
+
247
333
  function outputMeteorRspack(data) {
248
334
  const jsonString = JSON.stringify(data);
249
335
  const output = `[Meteor-Rspack]${jsonString}[/Meteor-Rspack]`;
@@ -261,4 +347,6 @@ module.exports = {
261
347
  disablePlugins,
262
348
  outputMeteorRspack,
263
349
  enablePortableBuild,
350
+ persistDevFiles,
351
+ createPersistCallback,
264
352
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "1.1.0-beta.35",
3
+ "version": "1.1.0-beta.37",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
package/rspack.config.js CHANGED
@@ -24,6 +24,8 @@ const {
24
24
  disablePlugins,
25
25
  outputMeteorRspack,
26
26
  enablePortableBuild,
27
+ persistDevFiles,
28
+ createPersistCallback,
27
29
  } = require('./lib/meteorRspackHelpers.js');
28
30
  const { loadUserAndOverrideConfig } = require('./lib/meteorRspackConfigHelpers.js');
29
31
  const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory");
@@ -348,6 +350,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
348
350
  disablePlugins: matchers,
349
351
  });
350
352
  Meteor.enablePortableBuild = () => enablePortableBuild();
353
+ Meteor.persistDevFiles = (matchers) => persistDevFiles(matchers);
351
354
 
352
355
  // Add HtmlRspackPlugin function to Meteor
353
356
  Meteor.HtmlRspackPlugin = (options = {}) => {
@@ -416,6 +419,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
416
419
  const clientOutputDir = path.resolve(projectDir, "public");
417
420
  const serverOutputDir = path.resolve(projectDir, "private");
418
421
 
422
+
419
423
  // Get Meteor ignore entries
420
424
  const meteorIgnoreEntries = getMeteorIgnoreEntries(projectDir);
421
425
 
@@ -558,6 +562,43 @@ module.exports = async function (inMeteor = {}, argv = {}) {
558
562
  ? path.resolve(process.cwd(), testEntry)
559
563
  : path.resolve(process.cwd(), buildContext, entryPath);
560
564
  const clientNameConfig = `[${(isTest && "test-") || ""}client-rspack]`;
565
+
566
+ // Default onListening provided by meteor-rspack. Kept as a named
567
+ // reference so we can detect a user-supplied override after merge
568
+ // and compose (run default first, then user's).
569
+ const meteorDefaultOnListening = function (devServer) {
570
+ if (!devServer) return;
571
+ const { host, port } = devServer.options;
572
+ const protocol =
573
+ devServer.options.server?.type === "https" ? "https" : "http";
574
+ const devServerUrl = `${protocol}://${host || "localhost"}:${port}`;
575
+ outputMeteorRspack({ devServerUrl });
576
+
577
+ // Windows-only: webpack-dev-server tracks accepted sockets
578
+ // but doesn't attach 'error'. On Windows, teardown of a
579
+ // closed proxy connection sends RST, producing an unhandled
580
+ // ECONNRESET that crashes the dev server. Unix peers send
581
+ // FIN and never hit this.
582
+ if (process.platform === "win32") {
583
+ const server = devServer.server;
584
+ if (!server || server.__meteorRspackErrorGuard) return;
585
+ server.__meteorRspackErrorGuard = true;
586
+
587
+ server.on("connection", (socket) => {
588
+ if (!socket || socket.__meteorRspackGuarded) return;
589
+ socket.__meteorRspackGuarded = true;
590
+ socket.on("error", (err) => {
591
+ if (err && err.code === "ECONNRESET") return;
592
+ console.warn(
593
+ `[meteor-rspack] dev server socket error: ${
594
+ err && (err.code || err.message)
595
+ }`
596
+ );
597
+ });
598
+ });
599
+ }
600
+ };
601
+
561
602
  // Base client config
562
603
  let clientConfig = {
563
604
  name: clientNameConfig,
@@ -650,17 +691,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
650
691
  ...(Meteor.isBlazeEnabled && { hot: false }),
651
692
  port: devServerPort,
652
693
  devMiddleware: {
653
- writeToDisk: (filePath) =>
654
- /\.(html)$/.test(filePath) || filePath.endsWith('sw.js'),
655
- },
656
- onListening(devServer) {
657
- if (!devServer) return;
658
- const { host, port } = devServer.options;
659
- const protocol =
660
- devServer.options.server?.type === "https" ? "https" : "http";
661
- const devServerUrl = `${protocol}://${host || "localhost"}:${port}`;
662
- outputMeteorRspack({ devServerUrl });
694
+ writeToDisk: createPersistCallback({ once: ['sw.js'], always: ['.html'] }),
663
695
  },
696
+ onListening: meteorDefaultOnListening,
664
697
  },
665
698
  }),
666
699
  ...merge(cacheStrategy, { experiments: { css: true } }),
@@ -713,7 +746,22 @@ module.exports = async function (inMeteor = {}, argv = {}) {
713
746
  runtimeChunk: false,
714
747
  },
715
748
  module: {
716
- rules: [swcConfigRule, ...extraRules],
749
+ rules: [
750
+ swcConfigRule,
751
+ // Mirror the client rule: ignore .html so rspack doesn't try to
752
+ // parse them as JavaScript. Meteor's template compiler handles
753
+ // .html files separately, and RequireExternalsPlugin below wires
754
+ // the imports to Meteor's module system.
755
+ ...(Meteor.isBlazeEnabled
756
+ ? [
757
+ {
758
+ test: /\.html$/i,
759
+ loader: 'ignore-loader',
760
+ },
761
+ ]
762
+ : []),
763
+ ...extraRules,
764
+ ],
717
765
  parser: {
718
766
  javascript: {
719
767
  // Dynamic imports on the server are treated as bundled in the same chunk
@@ -825,6 +873,23 @@ module.exports = async function (inMeteor = {}, argv = {}) {
825
873
  }
826
874
  }
827
875
 
876
+ // If the user or an override replaced devServer.onListening, compose
877
+ // so our default runs first (attaches the Windows socket guard and
878
+ // reports the dev server URL) and the user's hook runs second.
879
+ if (isClient && config.devServer) {
880
+ const finalOnListening = config.devServer.onListening;
881
+ if (
882
+ typeof finalOnListening === "function" &&
883
+ finalOnListening !== meteorDefaultOnListening
884
+ ) {
885
+ const userOnListening = finalOnListening;
886
+ config.devServer.onListening = function (devServer) {
887
+ meteorDefaultOnListening(devServer);
888
+ userOnListening(devServer);
889
+ };
890
+ }
891
+ }
892
+
828
893
  const shouldDisablePlugins = config?.disablePlugins != null;
829
894
  if (shouldDisablePlugins) {
830
895
  config = disablePlugins(config, config.disablePlugins);