@meteorjs/rspack 1.1.0-beta.31 → 1.1.0-beta.33

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/README.md CHANGED
@@ -14,6 +14,7 @@ When Meteor runs with the Rspack bundler enabled, this package is what generates
14
14
  - **Asset externals and HTML generation** through custom Rspack plugins
15
15
  - **A `defineConfig` helper** that accepts a factory function receiving Meteor environment flags and build utilities
16
16
  - **Customizable config** via `rspack.config.js` in your project root, with safe merging that warns if you try to override reserved settings
17
+ - **Automatic CSS delegation** when rspack is configured with CSS, Less, or SCSS loaders, Meteor automatically detects the handled extensions after the first compilation and stops processing those files itself in the entry folder context. No `.meteorignore` entries needed.
17
18
 
18
19
  ## Installation
19
20
 
@@ -0,0 +1,184 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Extract local file dependencies from a config file by parsing require/import statements using AST
6
+ * @param {string} configFilePath - Path to the config file to parse
7
+ * @returns {string[]} - Array of absolute paths to local dependencies
8
+ */
9
+ function extractLocalDependencies(configFilePath) {
10
+ if (!configFilePath || !fs.existsSync(configFilePath)) {
11
+ return [];
12
+ }
13
+
14
+ try {
15
+ const swc = require('@swc/core');
16
+ const content = fs.readFileSync(configFilePath, 'utf-8');
17
+ const configDir = path.dirname(configFilePath);
18
+ const projectDir = process.cwd();
19
+ const dependencies = [];
20
+
21
+ // Parse the file into an AST
22
+ const ast = swc.parseSync(content, {
23
+ syntax: 'ecmascript',
24
+ dynamicImport: true,
25
+ target: 'es2020',
26
+ });
27
+
28
+ // Visit all nodes to find import/require statements
29
+ visitNode(ast, (node) => {
30
+ let modulePath = null;
31
+
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) {
37
+ const arg = node.arguments[0];
38
+ if (arg.expression?.type === 'StringLiteral') {
39
+ modulePath = arg.expression.value;
40
+ }
41
+ }
42
+
43
+ // Handle dynamic import() calls: import('./plugin')
44
+ if (node.type === 'CallExpression' &&
45
+ node.callee.type === 'Import' &&
46
+ node.arguments.length > 0) {
47
+ const arg = node.arguments[0];
48
+ if (arg.expression?.type === 'StringLiteral') {
49
+ modulePath = arg.expression.value;
50
+ }
51
+ }
52
+
53
+ // Handle static imports: import x from './plugin'
54
+ if (node.type === 'ImportDeclaration' && node.source?.type === 'StringLiteral') {
55
+ modulePath = node.source.value;
56
+ }
57
+
58
+ // Handle export re-exports: export * from './plugin'
59
+ if (node.type === 'ExportAllDeclaration' && node.source?.type === 'StringLiteral') {
60
+ modulePath = node.source.value;
61
+ }
62
+
63
+ // Handle named export re-exports: export { x } from './plugin'
64
+ if (node.type === 'ExportNamedDeclaration' && node.source?.type === 'StringLiteral') {
65
+ modulePath = node.source.value;
66
+ }
67
+
68
+ // If we found a module path, try to resolve it
69
+ if (modulePath) {
70
+ const resolvedPath = resolveLocalModule(modulePath, configDir, projectDir);
71
+ if (resolvedPath) {
72
+ dependencies.push(resolvedPath);
73
+ }
74
+ }
75
+ });
76
+
77
+ // Remove duplicates
78
+ return [...new Set(dependencies)];
79
+ } catch (error) {
80
+ console.warn('[Rspack Cache] Failed to parse config dependencies:', error.message);
81
+ return [];
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Recursively visit all nodes in an AST
87
+ * @param {Object} node - AST node
88
+ * @param {Function} callback - Function to call for each node
89
+ */
90
+ function visitNode(node, callback) {
91
+ if (!node || typeof node !== 'object') {
92
+ return;
93
+ }
94
+
95
+ callback(node);
96
+
97
+ // Visit all properties of the node
98
+ for (const key in node) {
99
+ if (Object.prototype.hasOwnProperty.call(node, key)) {
100
+ const value = node[key];
101
+ if (Array.isArray(value)) {
102
+ value.forEach(child => visitNode(child, callback));
103
+ } else if (typeof value === 'object') {
104
+ visitNode(value, callback);
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Resolve a module path to an absolute path if it's a local file
112
+ * @param {string} modulePath - Module path from require/import statement
113
+ * @param {string} configDir - Directory containing the config file
114
+ * @param {string} projectDir - Project root directory
115
+ * @returns {string|null} - Resolved absolute path or null
116
+ */
117
+ function resolveLocalModule(modulePath, configDir, projectDir) {
118
+ // Only process relative paths (starts with . or ..)
119
+ if (!modulePath.startsWith('.')) {
120
+ return null;
121
+ }
122
+
123
+ try {
124
+ let resolvedPath = path.resolve(configDir, modulePath);
125
+ const extensions = ['.js', '.mjs', '.cjs', '.ts', '.json'];
126
+
127
+ // If the path exists as-is, check if it's a directory needing index resolution
128
+ if (fs.existsSync(resolvedPath)) {
129
+ if (fs.statSync(resolvedPath).isDirectory()) {
130
+ let found = false;
131
+ for (const ext of extensions) {
132
+ const indexPath = path.join(resolvedPath, `index${ext}`);
133
+ if (fs.existsSync(indexPath)) {
134
+ resolvedPath = indexPath;
135
+ found = true;
136
+ break;
137
+ }
138
+ }
139
+ if (!found) {
140
+ return null;
141
+ }
142
+ }
143
+ } else {
144
+ // Try common extensions if file doesn't exist as-is
145
+ let found = false;
146
+
147
+ for (const ext of extensions) {
148
+ const pathWithExt = resolvedPath + ext;
149
+ if (fs.existsSync(pathWithExt)) {
150
+ resolvedPath = pathWithExt;
151
+ found = true;
152
+ break;
153
+ }
154
+ }
155
+
156
+ // If still not found, return null
157
+ if (!found) {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ // Verify file is within project (not node_modules)
163
+ const resolvedReal = fs.realpathSync(resolvedPath);
164
+ const projectReal = fs.realpathSync(projectDir);
165
+
166
+ const isWithinProject =
167
+ resolvedReal === projectReal ||
168
+ resolvedReal.startsWith(projectReal + path.sep);
169
+ const hasNodeModulesSegment = resolvedReal.split(path.sep).includes('node_modules');
170
+
171
+ if (isWithinProject && !hasNodeModulesSegment) {
172
+ return resolvedPath;
173
+ }
174
+ } catch (error) {
175
+ // Silently ignore resolution errors
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ module.exports = {
182
+ extractLocalDependencies,
183
+ resolveLocalModule,
184
+ };
package/package.json CHANGED
@@ -1,19 +1,27 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "1.1.0-beta.31",
3
+ "version": "1.1.0-beta.33",
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 };
package/rspack.config.js CHANGED
@@ -10,7 +10,7 @@ const { getMeteorAppSwcConfig } = require('./lib/swc.js');
10
10
  const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js');
11
11
  const { RequireExternalsPlugin } = require('./plugins/RequireExtenalsPlugin.js');
12
12
  const { AssetExternalsPlugin } = require('./plugins/AssetExternalsPlugin.js');
13
- const { MeteorRspackOutputPlugin } = require('./plugins/MeteorRspackOutputPlugin.js');
13
+ const { MeteorRspackOutputPlugin, extractDelegatedExtensions } = require('./plugins/MeteorRspackOutputPlugin.js');
14
14
  const { generateEagerTestFile } = require("./lib/test.js");
15
15
  const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore");
16
16
  const {
@@ -27,6 +27,8 @@ const {
27
27
  } = require('./lib/meteorRspackHelpers.js');
28
28
  const { loadUserAndOverrideConfig } = require('./lib/meteorRspackConfigHelpers.js');
29
29
  const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory");
30
+ const { extractLocalDependencies } = require('./lib/localDependenciesHelpers.js');
31
+
30
32
 
31
33
  // Safe require that doesn't throw if the module isn't found
32
34
  function safeRequire(moduleName) {
@@ -69,10 +71,16 @@ function createCacheStrategy(
69
71
  const yarnLockPath = path.join(process.cwd(), 'yarn.lock');
70
72
  const hasYarnLock = fs.existsSync(yarnLockPath);
71
73
 
74
+ // Extract local dependencies from project config (e.g., plugin files)
75
+ const localDependencies = projectConfigPath
76
+ ? extractLocalDependencies(projectConfigPath)
77
+ : [];
78
+
72
79
  // Build dependencies array
73
80
  const buildDependencies = [
74
81
  ...(projectConfigPath ? [projectConfigPath] : []),
75
82
  ...(configPath ? [configPath] : []),
83
+ ...localDependencies,
76
84
  ...(hasTsconfig ? [tsconfigPath] : []),
77
85
  ...(hasBabelRcConfig ? [babelRcConfig] : []),
78
86
  ...(hasBabelJsConfig ? [babelJsConfig] : []),
@@ -643,7 +651,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
643
651
  port: devServerPort,
644
652
  devMiddleware: {
645
653
  writeToDisk: (filePath) =>
646
- /\.(html)$/.test(filePath) && !filePath.includes(".hot-update."),
654
+ /\.(html)$/.test(filePath) || filePath.endsWith('sw.js'),
647
655
  },
648
656
  onListening(devServer) {
649
657
  if (!devServer) return;
@@ -843,7 +851,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
843
851
 
844
852
  // Add MeteorRspackOutputPlugin as the last plugin to output compilation info
845
853
  const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({
846
- getData: (stats, { isRebuild, compilationCount }) => ({
854
+ getData: (stats, { isRebuild, compilationCount, compiler }) => ({
847
855
  name: config.name,
848
856
  mode: config.mode,
849
857
  hasErrors: stats.hasErrors(),
@@ -852,6 +860,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
852
860
  statsOverrided,
853
861
  compilationCount,
854
862
  isRebuild,
863
+ ...(!isRebuild && compiler && {
864
+ delegatedExtensions: extractDelegatedExtensions(stats, compiler),
865
+ }),
855
866
  }),
856
867
  });
857
868
  config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])];