@meteorjs/rspack 0.0.11 → 0.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -22,12 +22,25 @@ export class RequireExternalsPlugin {
22
22
  // It can be used to customize how external modules are mapped to file paths
23
23
  // If not provided, the default behavior is to map the external module name.
24
24
  externalMap = null,
25
+ // Enable global polyfill for module and exports
26
+ // If true, globalThis.module and globalThis.exports will be defined if they don't exist
27
+ enableGlobalPolyfill = true,
28
+ // Check function to determine if an external import should be eager
29
+ // If provided, it will be called with the package name and should return true for eager imports
30
+ // If not provided or returns false, the import will be lazy (default behavior)
31
+ isEagerImport = null,
32
+ // Array of module paths that should always be imported at the end of the file
33
+ // These will be treated as eager imports but will always be placed after all other imports
34
+ lastImports = null,
25
35
  } = {}) {
26
36
  this.pluginName = 'RequireExternalsPlugin';
27
37
 
28
38
  // Prepare externals
29
39
  this._externals = externals;
30
40
  this._externalMap = externalMap;
41
+ this._enableGlobalPolyfill = enableGlobalPolyfill;
42
+ this._isEagerImport = isEagerImport;
43
+ this._lastImports = lastImports;
31
44
  this._defaultExternalPrefix = 'external ';
32
45
 
33
46
  // Prepare paths
@@ -77,11 +90,14 @@ export class RequireExternalsPlugin {
77
90
  _extractPackageName(name) {
78
91
  let pkg = name.slice(this._defaultExternalPrefix.length);
79
92
  if (pkg.startsWith('"') && pkg.endsWith('"')) pkg = pkg.slice(1, -1);
80
-
93
+ const depInfo = path.parse(name);
81
94
  // If the extracted package name is a path, use the path as is
82
95
  if (
83
96
  pkg &&
84
- (path.isAbsolute(pkg) || pkg.startsWith('./') || pkg.startsWith('../'))
97
+ (path.isAbsolute(pkg) ||
98
+ pkg.startsWith('./') ||
99
+ pkg.startsWith('../') ||
100
+ !!depInfo.ext)
85
101
  ) {
86
102
  const module = this.externalsMeta.get(pkg);
87
103
  if (module) {
@@ -130,8 +146,10 @@ export class RequireExternalsPlugin {
130
146
  }
131
147
 
132
148
  compiler.hooks.done.tap({ name: this.pluginName, stage: -10 }, (stats) => {
133
- // 1) Ensure globalThis.module / exports block is present
134
- this._ensureGlobalThisModule();
149
+ // 1) Ensure globalThis.module / exports block is present if enabled
150
+ if (this._enableGlobalPolyfill) {
151
+ this._ensureGlobalThisModule();
152
+ }
135
153
 
136
154
  // 2) Re-load existing requires from disk on every run
137
155
  const existing = this._readExistingRequires();
@@ -163,21 +181,39 @@ export class RequireExternalsPlugin {
163
181
  // Strip out any now-empty helper functions:
164
182
  // function lazyExternalImportsX() {
165
183
  // }
166
- const emptyFnRe = /^function\s+lazyExternalImports\d+\s*\(\)\s*{\s*}\s*(\r?\n)?/gm;
167
- content = content.replace(emptyFnRe, '');
184
+ // or new format:
185
+ // // (function eagerExternalImportsX() {
186
+ // // })
187
+ // or lastImports format:
188
+ // // (function lastImports() {
189
+ // // })
190
+ const emptyLazyFnRe = /^function\s+lazyExternalImports\d+\s*\(\)\s*{\s*}\s*(\r?\n)?/gm;
191
+ const emptyEagerFnRe = /^\/\/\s*\(function\s+eagerExternalImports\d+\s*\(\)\s*{\s*\n\/\/\s*\}\)\s*(\r?\n)?/gm;
192
+ const emptyLastFnRe = /^\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n\/\/\s*\}\)\s*(\r?\n)?/gm;
193
+ content = content.replace(emptyLazyFnRe, '');
194
+ content = content.replace(emptyEagerFnRe, '');
195
+ content = content.replace(emptyLastFnRe, '');
168
196
 
169
197
  // Write the cleaned file back
170
198
  fs.writeFileSync(this.filePath, content, 'utf-8');
171
199
 
172
200
  // Re-populate `existing` so the add-diff is accurate
173
201
  existing.clear();
202
+ // Check for require statements
174
203
  for (const match of content.matchAll(/require\('([^']+)'\)/g)) {
175
204
  existing.add(match[1]);
176
205
  }
206
+ // Also check for import statements (used in the new format)
207
+ for (const match of content.matchAll(/import\s+'([^']+)'/g)) {
208
+ existing.add(match[1]);
209
+ }
177
210
  }
178
211
 
179
- // 3) Collect any new externals from this build
180
- const newRequires = [];
212
+ // 3) Collect any new externals from this build and separate into eager, lazy, and last
213
+ const newLazyRequires = [];
214
+ const newEagerRequires = [];
215
+ const newLastRequires = [];
216
+
181
217
  for (const module of info.modules) {
182
218
  const name = module.name;
183
219
  const matchInfo = this._isExternalModule(name);
@@ -186,19 +222,180 @@ export class RequireExternalsPlugin {
186
222
  const pkg = this._extractPackageName(name, matchInfo);
187
223
  if (pkg && !existing.has(pkg)) {
188
224
  existing.add(pkg);
189
- newRequires.push(`require('${pkg}')`);
225
+
226
+ // Check if this should be a last import
227
+ if (this._lastImports && Array.isArray(this._lastImports) && this._lastImports.includes(pkg)) {
228
+ newLastRequires.push(`require('${pkg}')`);
229
+ }
230
+ // Check if this should be an eager import
231
+ else if (this._isEagerImport && typeof this._isEagerImport === 'function' && this._isEagerImport(pkg)) {
232
+ newEagerRequires.push(`require('${pkg}')`);
233
+ } else {
234
+ // Default to lazy import
235
+ newLazyRequires.push(`require('${pkg}')`);
236
+ }
190
237
  }
191
238
  }
192
239
 
193
- // 4) Append new imports if any
194
- if (newRequires.length) {
240
+ // 4) Append new lazy imports if any
241
+ if (newLazyRequires.length) {
195
242
  const fnName = `lazyExternalImports${this._funcCount++}`;
196
- const body = newRequires.map(req => ` ${req};`).join('\n');
243
+ const body = newLazyRequires.map(req => ` ${req};`).join('\n');
197
244
  const fnCode = `\nfunction ${fnName}() {\n${body}\n}\n`;
198
245
  try {
199
246
  fs.appendFileSync(this.filePath, fnCode);
200
247
  } catch (err) {
201
- console.error(`Failed to append imports to ${this.filePath}:`, err);
248
+ console.error(`Failed to append lazy imports to ${this.filePath}:`, err);
249
+ }
250
+ }
251
+
252
+ // 5) Append new eager imports if any
253
+ if (newEagerRequires.length) {
254
+ const fnName = `eagerExternalImports${this._funcCount++}`;
255
+ // Convert require statements to import statements
256
+ const body = newEagerRequires
257
+ .map(req => {
258
+ // Extract the module path from require('path')
259
+ const modulePath = req.match(/require\('([^']+)'\)/)[1];
260
+ return `import '${modulePath}';`;
261
+ })
262
+ .join('\n');
263
+ // Use comments instead of actual function
264
+ const fnCode = `\n// (function ${fnName}() {\n${body}\n// })\n`;
265
+ try {
266
+ fs.appendFileSync(this.filePath, fnCode);
267
+ } catch (err) {
268
+ console.error(`Failed to append eager imports to ${this.filePath}:`, err);
269
+ }
270
+ }
271
+
272
+ // 6) Handle lastImports - these should always be at the end of the file
273
+ // First, check if lastImports already exist in the file
274
+ let lastImportsExist = false;
275
+ let lastImportsAtEnd = false;
276
+ let content = '';
277
+
278
+ if (fs.existsSync(this.filePath)) {
279
+ content = fs.readFileSync(this.filePath, 'utf-8');
280
+
281
+ // Check if lastImports exist in the file
282
+ const lastImportsRe = /\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n([\s\S]*?)\/\/\s*\}\)/g;
283
+ const match = lastImportsRe.exec(content);
284
+
285
+ if (match) {
286
+ lastImportsExist = true;
287
+
288
+ // Check if lastImports are at the end of the file
289
+ // We'll consider them at the end if there's only whitespace after them
290
+ const afterLastImports = content.substring(match.index + match[0].length);
291
+ if (/^\s*$/.test(afterLastImports)) {
292
+ lastImportsAtEnd = true;
293
+ }
294
+ }
295
+ }
296
+
297
+ // If lastImports exist but are not at the end, move them to the end
298
+ if (lastImportsExist && !lastImportsAtEnd) {
299
+ // Remove the existing lastImports
300
+ const lastImportsRe = /\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n[\s\S]*?\/\/\s*\}\)\s*(\r?\n)?/g;
301
+ content = content.replace(lastImportsRe, '');
302
+
303
+ // Extract the imports from the existing lastImports
304
+ const importRe = /import\s+'([^']+)'/g;
305
+ const existingLastImports = [];
306
+ let match;
307
+
308
+ while ((match = importRe.exec(content)) !== null) {
309
+ if (this._lastImports && Array.isArray(this._lastImports) && this._lastImports.includes(match[1])) {
310
+ existingLastImports.push(`import '${match[1]}';`);
311
+ }
312
+ }
313
+
314
+ // Add any new lastImports
315
+ if (this._lastImports && Array.isArray(this._lastImports)) {
316
+ for (const pkg of this._lastImports) {
317
+ if (!existingLastImports.some(imp => imp === `import '${pkg}';`) && existing.has(pkg)) {
318
+ existingLastImports.push(`import '${pkg}';`);
319
+ }
320
+ }
321
+ }
322
+
323
+ // Add the lastImports to the end of the file
324
+ if (existingLastImports.length > 0) {
325
+ const body = existingLastImports.join('\n');
326
+ const fnCode = `\n// (function lastImports() {\n${body}\n// })\n`;
327
+ fs.writeFileSync(this.filePath, content + fnCode);
328
+ } else {
329
+ fs.writeFileSync(this.filePath, content);
330
+ }
331
+ }
332
+ // If lastImports don't exist, add them if needed
333
+ else if (!lastImportsExist) {
334
+ // Collect all lastImports
335
+ const allLastImports = [];
336
+
337
+ // Add any new lastImports from this build
338
+ if (newLastRequires.length) {
339
+ for (const req of newLastRequires) {
340
+ const modulePath = req.match(/require\('([^']+)'\)/)[1];
341
+ allLastImports.push(`import '${modulePath}';`);
342
+ }
343
+ }
344
+
345
+ // Add any existing lastImports from the configuration
346
+ if (this._lastImports && Array.isArray(this._lastImports)) {
347
+ for (const pkg of this._lastImports) {
348
+ if (!allLastImports.some(imp => imp === `import '${pkg}';`) && !existing.has(pkg)) {
349
+ allLastImports.push(`import '${pkg}';`);
350
+ }
351
+ }
352
+ }
353
+
354
+ // Add the lastImports to the end of the file
355
+ if (allLastImports.length > 0) {
356
+ const body = allLastImports.join('\n');
357
+ const fnCode = `\n// (function lastImports() {\n${body}\n// })\n`;
358
+ try {
359
+ fs.appendFileSync(this.filePath, fnCode);
360
+ } catch (err) {
361
+ console.error(`Failed to append last imports to ${this.filePath}:`, err);
362
+ }
363
+ }
364
+ }
365
+ // If lastImports exist and are already at the end, add any new ones
366
+ else if (lastImportsExist && lastImportsAtEnd && newLastRequires.length) {
367
+ // Extract the existing lastImports
368
+ const lastImportsRe = /\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n([\s\S]*?)\/\/\s*\}\)/;
369
+ const match = lastImportsRe.exec(content);
370
+
371
+ if (match) {
372
+ const existingBody = match[1];
373
+ const existingImports = new Set();
374
+
375
+ // Extract the imports from the existing lastImports
376
+ const importRe = /import\s+'([^']+)'/g;
377
+ let importMatch;
378
+
379
+ while ((importMatch = importRe.exec(existingBody)) !== null) {
380
+ existingImports.add(importMatch[1]);
381
+ }
382
+
383
+ // Add any new lastImports
384
+ let newBody = existingBody;
385
+ for (const req of newLastRequires) {
386
+ const modulePath = req.match(/require\('([^']+)'\)/)[1];
387
+ if (!existingImports.has(modulePath)) {
388
+ newBody += `import '${modulePath}';\n`;
389
+ }
390
+ }
391
+
392
+ // Replace the existing lastImports with the updated ones
393
+ const updatedContent = content.replace(
394
+ lastImportsRe,
395
+ `// (function lastImports() {\n${newBody}// })`
396
+ );
397
+
398
+ fs.writeFileSync(this.filePath, updatedContent);
202
399
  }
203
400
  }
204
401
  });
@@ -209,12 +406,33 @@ export class RequireExternalsPlugin {
209
406
  if (fs.existsSync(this.filePath)) {
210
407
  try {
211
408
  const content = fs.readFileSync(this.filePath, 'utf-8');
212
- const fnRe = /function\s+lazyExternalImports(\d+)\s*\(\)/g;
409
+ // Check for lazy, eager, and last external imports functions
410
+ const lazyFnRe = /function\s+lazyExternalImports(\d+)\s*\(\)/g;
411
+ // Only match the new commented format
412
+ const eagerFnRe = /\/\/\s*\(function\s+eagerExternalImports(\d+)\s*\(\)/g;
413
+ // Match the lastImports format
414
+ const lastFnRe = /\/\/\s*\(function\s+lastImports(\d+)?\s*\(\)/g;
415
+
213
416
  let match;
214
- while ((match = fnRe.exec(content)) !== null) {
417
+ // Check lazy imports
418
+ while ((match = lazyFnRe.exec(content)) !== null) {
419
+ const n = parseInt(match[1], 10);
420
+ if (n > max) max = n;
421
+ }
422
+
423
+ // Check eager imports
424
+ while ((match = eagerFnRe.exec(content)) !== null) {
215
425
  const n = parseInt(match[1], 10);
216
426
  if (n > max) max = n;
217
427
  }
428
+
429
+ // Check last imports
430
+ while ((match = lastFnRe.exec(content)) !== null) {
431
+ if (match[1]) {
432
+ const n = parseInt(match[1], 10);
433
+ if (n > max) max = n;
434
+ }
435
+ }
218
436
  } catch {
219
437
  // ignore read errors
220
438
  }
@@ -251,11 +469,18 @@ export class RequireExternalsPlugin {
251
469
  const existing = new Set();
252
470
  try {
253
471
  const content = fs.readFileSync(this.filePath, 'utf-8');
472
+ // Check for require statements
254
473
  const requireRegex = /require\('([^']+)'\)/g;
255
474
  let match;
256
475
  while ((match = requireRegex.exec(content)) !== null) {
257
476
  existing.add(match[1]);
258
477
  }
478
+
479
+ // Also check for import statements (used in the new format)
480
+ const importRegex = /import\s+'([^']+)'/g;
481
+ while ((match = importRegex.exec(content)) !== null) {
482
+ existing.add(match[1]);
483
+ }
259
484
  } catch {
260
485
  // ignore if file missing or unreadable
261
486
  }
package/rspack.config.js CHANGED
@@ -43,7 +43,7 @@ function createCacheStrategy(mode) {
43
43
  }
44
44
 
45
45
  // SWC loader rule (JSX/JS)
46
- function createSwcConfig({ isRun, isTypescriptEnabled, isJsxEnabled, isTsxEnabled, externalHelpers }) {
46
+ function createSwcConfig({ isTypescriptEnabled, isJsxEnabled, isTsxEnabled, externalHelpers, isDevEnvironment }) {
47
47
  const defaultConfig = {
48
48
  jsc: {
49
49
  baseUrl: process.cwd(),
@@ -56,8 +56,8 @@ function createSwcConfig({ isRun, isTypescriptEnabled, isJsxEnabled, isTsxEnable
56
56
  target: 'es2015',
57
57
  transform: {
58
58
  react: {
59
- development: isRun,
60
- refresh: isRun,
59
+ development: isDevEnvironment,
60
+ refresh: isDevEnvironment,
61
61
  },
62
62
  },
63
63
  externalHelpers,
@@ -156,12 +156,13 @@ export default function (inMeteor = {}, argv = {}) {
156
156
  console.log('[i] Meteor flags:', Meteor);
157
157
  }
158
158
 
159
+ const isDevEnvironment = isRun && isDev && !isTest;
159
160
  const swcConfigRule = createSwcConfig({
160
- isRun,
161
161
  isTypescriptEnabled,
162
162
  isJsxEnabled,
163
163
  isTsxEnabled,
164
164
  externalHelpers: swcExternalHelpers,
165
+ isDevEnvironment,
165
166
  });
166
167
  const externals = [
167
168
  /^meteor.*/,
@@ -193,16 +194,12 @@ export default function (inMeteor = {}, argv = {}) {
193
194
  filePath: path.join(buildContext, runPath),
194
195
  ...(Meteor.isBlazeEnabled && {
195
196
  externals: /\.html$/,
196
- externalMap: (module) => {
197
- const { request, context } = module;
198
- if (request.endsWith('.html')) {
199
- const relContext = path.relative(process.cwd(), context);
200
- const { name } = path.parse(request);
201
- return `./${relContext}/template.${name}.js`;
202
- }
203
- return request;
197
+ isEagerImport: (module) => module.endsWith('.html'),
198
+ ...isProd && {
199
+ lastImports: [`./${outputFilename}`],
204
200
  },
205
201
  }),
202
+ enableGlobalPolyfill: isDevEnvironment,
206
203
  });
207
204
 
208
205
  const clientNameConfig = `[${isTest && 'test-' || ''}${isTestModule && 'module' || 'client'}-rspack]`;
@@ -210,12 +207,12 @@ export default function (inMeteor = {}, argv = {}) {
210
207
  let clientConfig = {
211
208
  name: clientNameConfig,
212
209
  target: 'web',
213
- mode,
210
+ mode: 'development',
214
211
  entry: path.resolve(process.cwd(), buildContext, entryPath),
215
212
  output: {
216
213
  path: clientOutputDir,
217
214
  filename: () =>
218
- isRun && !isTest ? outputFilename : `../${buildContext}/${outputPath}`,
215
+ isDevEnvironment ? outputFilename : `../${buildContext}/${outputPath}`,
219
216
  libraryTarget: 'commonjs',
220
217
  publicPath: '/',
221
218
  chunkFilename: `${bundlesContext}/[id].[chunkhash].js`,
@@ -243,7 +240,7 @@ export default function (inMeteor = {}, argv = {}) {
243
240
  externals,
244
241
  plugins: [
245
242
  ...[
246
- ...(isReactEnabled && reactRefreshModule
243
+ ...(isReactEnabled && reactRefreshModule && isDevEnvironment
247
244
  ? [new reactRefreshModule()]
248
245
  : []),
249
246
  requireExternalsPlugin,
@@ -261,8 +258,8 @@ export default function (inMeteor = {}, argv = {}) {
261
258
  }),
262
259
  ],
263
260
  watchOptions,
264
- devtool: isDev || isTest ? 'source-map' : 'hidden-source-map',
265
- ...(isRun && !isTest && {
261
+ devtool: isDevEnvironment || isTest ? 'source-map' : 'hidden-source-map',
262
+ ...(isDevEnvironment && {
266
263
  devServer: {
267
264
  static: { directory: clientOutputDir, publicPath: '/__rspack__/' },
268
265
  hot: true,
@@ -322,8 +319,8 @@ export default function (inMeteor = {}, argv = {}) {
322
319
  isTestModule && requireExternalsPlugin,
323
320
  ],
324
321
  watchOptions,
325
- devtool: isRun ? 'source-map' : 'hidden-source-map',
326
- ...((isRun || isTest) &&
322
+ devtool: isDevEnvironment || isTest ? 'source-map' : 'hidden-source-map',
323
+ ...((isDevEnvironment || isTest) &&
327
324
  createCacheStrategy(mode)
328
325
  ),
329
326
  };