@meteorjs/rspack 1.1.0-beta.9 → 2.0.1

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.
@@ -132,10 +132,14 @@ function splitVendorChunk() {
132
132
  }
133
133
 
134
134
  /**
135
- * Extend SWC loader config
136
- * Usage: extendSwcConfig()
135
+ * Extend SWC loader config by smart-merging custom options on top of Meteor's
136
+ * defaults (via `mergeSplitOverlap`). Only the properties you specify are
137
+ * overridden; everything else is preserved.
137
138
  *
138
- * @returns {Record<string, object>} `{ meteorRspackConfigX: { optimization: { ... } } }`
139
+ * Usage: Meteor.extendSwcConfig({ jsc: { parser: { decorators: true } } })
140
+ *
141
+ * @param {object} swcConfig - SWC loader options to merge with defaults
142
+ * @returns {Record<string, object>} config fragment for spreading into rspack config
139
143
  */
140
144
  function extendSwcConfig(swcConfig) {
141
145
  return prepareMeteorRspackConfig({
@@ -152,6 +156,44 @@ function extendSwcConfig(swcConfig) {
152
156
  });
153
157
  }
154
158
 
159
+ /**
160
+ * Replace the SWC loader config entirely, discarding Meteor's defaults.
161
+ * Use this when you need full control over SWC options and don't want any
162
+ * automatic merging with Meteor's built-in configuration.
163
+ *
164
+ * Usage: Meteor.replaceSwcConfig({ jsc: { parser: { syntax: 'typescript' }, target: 'es2020' } })
165
+ *
166
+ * @param {object} swcConfig - Complete SWC loader options (replaces defaults)
167
+ * @returns {Record<string, object>} config fragment for spreading into rspack config
168
+ */
169
+ function replaceSwcConfig(swcConfig) {
170
+ return prepareMeteorRspackConfig({
171
+ module: {
172
+ rules: [
173
+ {
174
+ test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i,
175
+ exclude: /node_modules|\.meteor\/local/,
176
+ loader: 'builtin:swc-loader',
177
+ options: swcConfig,
178
+ },
179
+ ],
180
+ },
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Signal that `Meteor.isDevelopment` and `Meteor.isProduction` should be omitted
186
+ * from DefinePlugin, making the bundle portable across Meteor environments.
187
+ * Usage: return Meteor.enablePortableBuild() in your rspack.config.js
188
+ *
189
+ * @returns {Record<string, object>} config fragment with `meteor.enablePortableBuild: true`
190
+ */
191
+ function enablePortableBuild() {
192
+ return prepareMeteorRspackConfig({
193
+ "meteor.enablePortableBuild": true,
194
+ });
195
+ }
196
+
155
197
  /**
156
198
  * Remove plugins from a Rspack config by name, RegExp, predicate, or array of them.
157
199
  * When using a function predicate, it receives both the plugin and its index in the plugins array.
@@ -202,6 +244,92 @@ function disablePlugins(config, matchers) {
202
244
  return config;
203
245
  }
204
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
+
205
333
  function outputMeteorRspack(data) {
206
334
  const jsonString = JSON.stringify(data);
207
335
  const output = `[Meteor-Rspack]${jsonString}[/Meteor-Rspack]`;
@@ -214,7 +342,11 @@ module.exports = {
214
342
  setCache,
215
343
  splitVendorChunk,
216
344
  extendSwcConfig,
345
+ replaceSwcConfig,
217
346
  makeWebNodeBuiltinsAlias,
218
347
  disablePlugins,
219
348
  outputMeteorRspack,
349
+ enablePortableBuild,
350
+ persistDevFiles,
351
+ createPersistCallback,
220
352
  };
package/lib/swc.js CHANGED
@@ -9,11 +9,38 @@ const vm = require('vm');
9
9
  function getMeteorAppSwcrc(file = '.swcrc') {
10
10
  try {
11
11
  const filePath = `${process.cwd()}/${file}`;
12
- if (file.endsWith('.js')) {
12
+ if (file.endsWith('.js') || file.endsWith('.ts')) {
13
13
  let content = fs.readFileSync(filePath, 'utf-8');
14
- // Check if the content uses ES module syntax (export default)
14
+
15
+ if (file.endsWith('.ts')) {
16
+ try {
17
+ const swc = require('@swc/core');
18
+ const result = swc.transformSync(content, {
19
+ jsc: {
20
+ parser: {
21
+ syntax: 'typescript',
22
+ },
23
+ target: 'es2015',
24
+ },
25
+ });
26
+ content = result.code;
27
+ } catch (swcError) {
28
+ content = content
29
+ .replace(/import\s+type\s+.*?from\s+['"][^'"]+['"];?/g, '')
30
+ .replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, '')
31
+ .replace(/import\s+['"][^'"]+['"];?/g, '')
32
+ .replace(/export\s+default\s+/, 'module.exports = ')
33
+ .replace(/export\s+/g, '')
34
+ .replace(/:\s*\w+(\[\])?(\s*=)/g, '$2')
35
+ .replace(/\(([^)]*?):\s*\w+(\[\])?\)/g, '($1)')
36
+ .replace(/\):\s*\w+(\[\])?\s*\{/g, ') {')
37
+ .replace(/interface\s+\w+\s*\{[^}]*\}/g, '')
38
+ .replace(/type\s+\w+\s*=\s*[^;]+;/g, '')
39
+ .replace(/as\s+\w+(\[\])?/g, '');
40
+ }
41
+ }
42
+
15
43
  if (content.includes('export default')) {
16
- // Transform ES module syntax to CommonJS
17
44
  content = content.replace(/export\s+default\s+/, 'module.exports = ');
18
45
  }
19
46
  const script = new vm.Script(`
@@ -27,7 +54,9 @@ function getMeteorAppSwcrc(file = '.swcrc') {
27
54
  })()
28
55
  `);
29
56
  const context = vm.createContext({ process });
30
- return script.runInContext(context);
57
+ const result = script.runInContext(context);
58
+ // Handle CJS interop wrapper (e.g. { __esModule: true, default: config })
59
+ return result && result.__esModule && result.default ? result.default : result;
31
60
  } else {
32
61
  // For .swcrc and other JSON files, parse as JSON
33
62
  return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -45,12 +74,13 @@ function getMeteorAppSwcrc(file = '.swcrc') {
45
74
  function getMeteorAppSwcConfig() {
46
75
  const hasSwcRc = fs.existsSync(`${process.cwd()}/.swcrc`);
47
76
  const hasSwcJs = !hasSwcRc && fs.existsSync(`${process.cwd()}/swc.config.js`);
77
+ const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${process.cwd()}/swc.config.ts`);
48
78
 
49
- if (!hasSwcRc && !hasSwcJs) {
79
+ if (!hasSwcRc && !hasSwcJs && !hasSwcTs) {
50
80
  return undefined;
51
81
  }
52
82
 
53
- const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc';
83
+ const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc';
54
84
  const config = getMeteorAppSwcrc(swcFile);
55
85
 
56
86
  // Set baseUrl to process.cwd() if it exists
package/lib/test.js CHANGED
@@ -2,6 +2,10 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { createIgnoreRegex, createIgnoreGlobConfig } = require("./ignore.js");
4
4
 
5
+ // Normalize a path to always use forward slashes (POSIX style).
6
+ // Module identifiers in bundled JS must use '/' regardless of OS.
7
+ const toPosix = (p) => p.replace(/\\/g, '/');
8
+
5
9
  /**
6
10
  * Generates eager test files dynamically
7
11
  * @param {Object} options - Options for generating the test file
@@ -42,7 +46,6 @@ const generateEagerTestFile = ({
42
46
  const excludeFoldersRegex = createIgnoreRegex(
43
47
  createIgnoreGlobConfig(ignoreEntries)
44
48
  );
45
- console.log("inMeteorIgnoreEntries", inMeteorIgnoreEntries);
46
49
  // Create regex from meteor ignore entries
47
50
  const excludeMeteorIgnoreRegex = inMeteorIgnoreEntries.length > 0
48
51
  ? createIgnoreRegex(createIgnoreGlobConfig(inMeteorIgnoreEntries))
@@ -58,14 +61,14 @@ const generateEagerTestFile = ({
58
61
  : "/\\.(?:test|spec)s?\\.[^.]+$/";
59
62
 
60
63
  const content = `${
61
- globalImportPath ? `import '${globalImportPath}';\n\n` : ""
64
+ globalImportPath ? `import '${toPosix(globalImportPath)}';\n\n` : ""
62
65
  }${
63
66
  excludeMeteorIgnoreRegex
64
67
  ? `const MeteorIgnoreRegex = ${excludeMeteorIgnoreRegex.toString()};`
65
68
  : ""
66
69
  }
67
70
  {
68
- const ctx = import.meta.webpackContext('${projectDir}', {
71
+ const ctx = import.meta.webpackContext('${toPosix(projectDir)}', {
69
72
  recursive: true,
70
73
  regExp: ${regExp},
71
74
  exclude: ${excludeFoldersRegex.toString()},
@@ -81,9 +84,9 @@ const generateEagerTestFile = ({
81
84
  }).forEach(ctx);
82
85
  ${
83
86
  extraEntry
84
- ? `const extra = import.meta.webpackContext('${path.dirname(
87
+ ? `const extra = import.meta.webpackContext('${toPosix(path.dirname(
85
88
  extraEntry
86
- )}', {
89
+ ))}', {
87
90
  recursive: false,
88
91
  regExp: ${new RegExp(`${path.basename(extraEntry)}$`).toString()},
89
92
  mode: 'eager',
package/package.json CHANGED
@@ -1,19 +1,27 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "1.1.0-beta.9",
3
+ "version": "2.0.1",
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
+ "scripts": {
10
+ "bump": "node ./scripts/bump-version.js",
11
+ "publish:beta": "bash ./scripts/publish-beta.sh"
12
+ },
9
13
  "dependencies": {
10
14
  "fast-deep-equal": "^3.1.3",
11
15
  "ignore-loader": "^0.1.2",
12
16
  "node-polyfill-webpack-plugin": "^4.1.0",
13
17
  "webpack-merge": "^6.0.1"
14
18
  },
19
+ "devDependencies": {
20
+ "semver": "^7.7.4"
21
+ },
15
22
  "peerDependencies": {
16
23
  "@rspack/cli": ">=1.3.0",
17
- "@rspack/core": ">=1.3.0"
24
+ "@rspack/core": ">=1.3.0",
25
+ "@swc/core": ">=1.3.0"
18
26
  }
19
27
  }
@@ -6,6 +6,96 @@
6
6
 
7
7
  const { outputMeteorRspack } = require('../lib/meteorRspackHelpers');
8
8
 
9
+ /**
10
+ * Extracts file extensions that rspack is configured to handle
11
+ * from the resolved module.rules test patterns.
12
+ * @param {import('@rspack/core').Compiler} compiler
13
+ * @returns {Set<string>} Set of extensions like .css, .less, .scss
14
+ */
15
+ function extractConfiguredExtensions(compiler) {
16
+ const delegatableExtensions = ['.css', '.less', '.scss', '.sass', '.styl'];
17
+ const found = new Set();
18
+
19
+ function inspectRules(rules) {
20
+ for (const rule of rules) {
21
+ if (!rule) continue;
22
+ if (rule.test) {
23
+ const testStr = rule.test instanceof RegExp
24
+ ? rule.test.source
25
+ : String(rule.test);
26
+ for (const ext of delegatableExtensions) {
27
+ const escaped = ext.replace('.', '\\.');
28
+ if (testStr.includes(escaped)) {
29
+ found.add(ext);
30
+ }
31
+ }
32
+ }
33
+ if (rule.oneOf) inspectRules(rule.oneOf);
34
+ if (rule.rules) inspectRules(rule.rules);
35
+ }
36
+ }
37
+
38
+ inspectRules(compiler.options.module?.rules || []);
39
+ return found;
40
+ }
41
+
42
+ /**
43
+ * Extracts file extensions that rspack both has rules for AND actually compiled
44
+ * from files within entry folder paths (e.g. client/, server/).
45
+ * An extension is only delegated if Rspack compiled a file with that extension
46
+ * from an entry folder. Files in non-entry folders (e.g. imports/) don't count,
47
+ * since delegation only ignores entry folder files for Meteor.
48
+ * @param {import('@rspack/core').Stats} stats
49
+ * @param {import('@rspack/core').Compiler} compiler
50
+ * @returns {string[]} Array of extensions like ['.css', '.less', '.scss']
51
+ */
52
+ function extractDelegatedExtensions(stats, compiler) {
53
+ const configured = extractConfiguredExtensions(compiler);
54
+ if (configured.size === 0) return [];
55
+
56
+ const path = require('path');
57
+ const fs = require('fs');
58
+ const appRoot = compiler.options.context || process.cwd();
59
+
60
+ // Read entry folders from package.json meteor.mainModule
61
+ const entryFolders = new Set();
62
+ try {
63
+ const pkgPath = path.join(appRoot, 'package.json');
64
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
65
+ const mainModule = pkg?.meteor?.mainModule || {};
66
+ for (const entry of Object.values(mainModule)) {
67
+ if (typeof entry === 'string') {
68
+ const folder = entry.split('/')[0];
69
+ if (folder) entryFolders.add(folder);
70
+ }
71
+ }
72
+ } catch (e) {
73
+ // If we can't read package.json, fall back to config-only
74
+ return Array.from(configured);
75
+ }
76
+
77
+ if (entryFolders.size === 0) return Array.from(configured);
78
+
79
+ const found = new Set();
80
+
81
+ for (const module of stats.compilation.modules) {
82
+ const resource = module.resource || module.userRequest;
83
+ if (!resource) continue;
84
+
85
+ const relativePath = path.relative(appRoot, resource);
86
+ const topFolder = relativePath.split(path.sep)[0];
87
+ if (!entryFolders.has(topFolder)) continue;
88
+
89
+ const ext = path.extname(resource);
90
+ if (configured.has(ext)) {
91
+ found.add(ext);
92
+ if (found.size === configured.size) break;
93
+ }
94
+ }
95
+
96
+ return Array.from(found);
97
+ }
98
+
9
99
  class MeteorRspackOutputPlugin {
10
100
  constructor(options = {}) {
11
101
  this.pluginName = 'MeteorRspackOutputPlugin';
@@ -26,6 +116,7 @@ class MeteorRspackOutputPlugin {
26
116
  ...(this.getData(stats, {
27
117
  compilationCount: this.compilationCount,
28
118
  isRebuild: this.compilationCount > 1,
119
+ compiler,
29
120
  }) || {}),
30
121
  };
31
122
  outputMeteorRspack(data);
@@ -33,4 +124,4 @@ class MeteorRspackOutputPlugin {
33
124
  }
34
125
  }
35
126
 
36
- module.exports = { MeteorRspackOutputPlugin };
127
+ module.exports = { MeteorRspackOutputPlugin, extractDelegatedExtensions };
@@ -10,6 +10,10 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
 
13
+ // Normalize a path to always use forward slashes (POSIX style).
14
+ // Module identifiers in bundled JS must use '/' regardless of OS.
15
+ const toPosix = (p) => p.replace(/\\/g, '/');
16
+
13
17
  class RequireExternalsPlugin {
14
18
  constructor({
15
19
  filePath,
@@ -46,7 +50,7 @@ class RequireExternalsPlugin {
46
50
  // Prepare paths
47
51
  this.filePath = path.resolve(process.cwd(), filePath);
48
52
  this.backRoot = '../'.repeat(
49
- filePath.replace(/^\.?\/+/, '').split('/').length - 1
53
+ filePath.replace(/^\.?[/\\]+/, '').split(/[/\\]/).length - 1
50
54
  );
51
55
 
52
56
  // Initialize funcCount based on existing helpers in the file
@@ -96,14 +100,16 @@ class RequireExternalsPlugin {
96
100
  pkg &&
97
101
  (path.isAbsolute(pkg) ||
98
102
  pkg.startsWith('./') ||
103
+ pkg.startsWith('.\\') ||
99
104
  pkg.startsWith('../') ||
105
+ pkg.startsWith('..\\') ||
100
106
  !!depInfo.ext)
101
107
  ) {
102
108
  const module = this.externalsMeta.get(pkg);
103
109
  if (module) {
104
- return `${this.backRoot}${module.relativeRequest}`;
110
+ return `${this.backRoot}${toPosix(module.relativeRequest)}`;
105
111
  }
106
- return `${this.backRoot}${name}`;
112
+ return `${this.backRoot}${toPosix(name)}`;
107
113
  }
108
114
 
109
115
  return pkg;
@@ -132,7 +138,7 @@ class RequireExternalsPlugin {
132
138
  this.externalsMeta.set(externalRequest, {
133
139
  originalRequest: request,
134
140
  externalRequest,
135
- relativeRequest: path.join(relContext, request),
141
+ relativeRequest: toPosix(path.join(relContext, request)),
136
142
  });
137
143
 
138
144
  // tell Rspack "don't bundle this, import it at runtime"