@meteorjs/rspack 1.1.0-beta.31 → 1.1.0-beta.32
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 +1 -0
- package/lib/localDependenciesHelpers.js +184 -0
- package/package.json +3 -2
- package/plugins/MeteorRspackOutputPlugin.js +92 -1
- package/rspack.config.js +14 -3
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meteorjs/rspack",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
3
|
+
"version": "1.1.0-beta.32",
|
|
4
4
|
"description": "Configuration logic for using Rspack in Meteor projects",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"@rspack/cli": ">=1.3.0",
|
|
17
|
-
"@rspack/core": ">=1.3.0"
|
|
17
|
+
"@rspack/core": ">=1.3.0",
|
|
18
|
+
"@swc/core": ">=1.3.0"
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -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)
|
|
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 || [])];
|