From 8635fcf2eb708b6610e01062db24bdb2be05c57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 4 Sep 2025 17:24:15 +0200 Subject: [PATCH] support modules config to allow load in Meteor specific files or folders for plugins to act --- packages/rspack/lib/config.js | 9 +- packages/tools-core/lib/ignore.js | 87 +++++++++++++++++++ packages/tools-core/tools-core_server.js | 1 + .../apps/react-router/package.json | 1 + .../apps/react-router/styles/module.css | 3 + tools/modern-tests/assertions.js | 1 + tools/modern-tests/react-router.test.js | 8 ++ .../rspack-bundler-integration.md | 14 +++ 8 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/tools-core/lib/ignore.js create mode 100644 tools/modern-tests/apps/react-router/styles/module.css diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js index 24fc73a6d9..6855a291cf 100644 --- a/packages/rspack/lib/config.js +++ b/packages/rspack/lib/config.js @@ -21,7 +21,9 @@ const { isMeteorLessProject, isMeteorScssProject, getMeteorEnvPackageDirs, + getMeteorAppConfig, } = require('meteor/tools-core/lib/meteor'); +const { buildUnignorePatterns } = require('meteor/tools-core/lib/ignore'); import { getInitialEntrypoints } from './build-context'; @@ -92,6 +94,7 @@ function getFileExtensionsToIgnore() { * @returns {void} */ export function configureMeteorForRspack() { + const meteorAppConfig = getMeteorAppConfig(); const initialEntrypoints = getInitialEntrypoints(); // Ignore node_modules to prevent Meteor from processing them @@ -180,9 +183,13 @@ export function configureMeteorForRspack() { ), ]; const filesToIgnore = [...rootFilesToIgnore, ...extraFilesToIgnore]; + const unignoredFilesAndFolders = buildUnignorePatterns( + meteorAppConfig?.modules || [], + { skipLevel: 1 }, + ); const meteorAppIgnores = `${foldersToIgnore.join(' ')} ${filesToIgnore.join( ' ', - )}`; + )} ${unignoredFilesAndFolders.join(' ')}`.trim(); setMeteorAppIgnore(meteorAppIgnores); if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { diff --git a/packages/tools-core/lib/ignore.js b/packages/tools-core/lib/ignore.js new file mode 100644 index 0000000000..611cb3344b --- /dev/null +++ b/packages/tools-core/lib/ignore.js @@ -0,0 +1,87 @@ +/** + * Build gitignore-style "unignore" patterns for specific files/folders. + * + * Rules: + * - Files: !a/ !a/b/ !a/b/c.txt + * - Folders (must end with '/'): + * !a/ !a/b/ !a/b/c/ !a/b/c/** + * + * @param {string[]} inputPaths Paths to keep. Use '/' for dirs (e.g. 'assets/public/'). + * @param {Object} [options] + * @param {boolean} [options.includeAllAncestors=true] If false, only include the immediate parent dir. + * @param {boolean} [options.includeGlobForDirs=true] Emit '**' for directories. + * @param {number} [options.skipLevel=0] Skip this many levels from the beginning. + * @returns {string[]} Negation patterns, in correct order. + */ +export function buildUnignorePatterns(inputPaths, { + includeAllAncestors = true, + includeGlobForDirs = true, + skipLevel = 0, +} = {}) { + const out = []; + const seen = new Set(); + + const push = (p) => { + if (!seen.has(p)) { + seen.add(p); + out.push(p); + } + }; + + for (let raw of inputPaths) { + if (!raw || typeof raw !== 'string') continue; + + // Normalize: forward slashes, drop leading './', collapse double slashes + let anchored = raw.startsWith('/'); + let p = raw.replace(/\\/g, '/') + .replace(/^\.\/+/, '') + .replace(/\/{2,}/g, '/'); + + // detect dir by trailing slash + const isDir = p.endsWith('/'); + // strip leading + trailing slashes for splitting, but remember anchoring + const core = p.replace(/^\/+/, '').replace(/\/+$/, ''); + if (!core) continue; + + const parts = core.split('/'); + + // Process based on skipLevel + if (skipLevel >= parts.length) { + // Skip everything if skipLevel is greater than or equal to the number of parts + continue; + } + + // Ancestors (top-down) + if (includeAllAncestors) { + // Start from skipLevel + 1 to skip the specified number of levels + const startLevel = Math.max(1, skipLevel + 1); + for (let i = startLevel; i <= parts.length - 1; i++) { + const anc = (anchored ? '/' : '') + parts.slice(0, i).join('/') + '/'; + push('!' + anc); + } + } else if (parts.length > 1) { + // Only immediate parent + // For minimal mode with skipLevel, we need to check if the parent is at a level we should skip + if (skipLevel < parts.length - 1) { + // Check if the parent's level is greater than skipLevel + const parentLevel = parts.length - 1; + if (parentLevel > skipLevel) { + const parent = (anchored ? '/' : '') + parts.slice(0, parts.length - 1).join('/') + '/'; + push('!' + parent); + } + } + } + + // Add the file/directory pattern + if (isDir) { + const dir = (anchored ? '/' : '') + parts.join('/') + '/'; + push('!' + dir); + if (includeGlobForDirs) push('!' + dir + '**'); + } else { + const file = (anchored ? '/' : '') + parts.join('/'); + push('!' + file); + } + } + + return out; +} diff --git a/packages/tools-core/tools-core_server.js b/packages/tools-core/tools-core_server.js index 313cc1674c..108d4e35c5 100644 --- a/packages/tools-core/tools-core_server.js +++ b/packages/tools-core/tools-core_server.js @@ -5,3 +5,4 @@ export * from './lib/process'; export * from './lib/global-state'; export * from './lib/git'; export * from './lib/string'; +export * from './lib/ignore'; diff --git a/tools/modern-tests/apps/react-router/package.json b/tools/modern-tests/apps/react-router/package.json index dbbf46f446..9e3c78d80b 100644 --- a/tools/modern-tests/apps/react-router/package.json +++ b/tools/modern-tests/apps/react-router/package.json @@ -33,6 +33,7 @@ "client": "client/main.jsx", "server": "server/main.js" }, + "modules": ["styles/module.css"], "modern": true } } diff --git a/tools/modern-tests/apps/react-router/styles/module.css b/tools/modern-tests/apps/react-router/styles/module.css new file mode 100644 index 0000000000..2b1f0dafba --- /dev/null +++ b/tools/modern-tests/apps/react-router/styles/module.css @@ -0,0 +1,3 @@ +body { + align-content: center; +} diff --git a/tools/modern-tests/assertions.js b/tools/modern-tests/assertions.js index cf31f15aaa..5fe8813962 100644 --- a/tools/modern-tests/assertions.js +++ b/tools/modern-tests/assertions.js @@ -167,6 +167,7 @@ export async function assertConsoleEval(code, expectedResult, options = {}) { try { // Evaluate the code in the browser context const result = await page.evaluate(code); + console.log("--> (assertions.js-Line: 170)\n result: ", result); if (exactMatch) { // Check for exact match diff --git a/tools/modern-tests/react-router.test.js b/tools/modern-tests/react-router.test.js index 0c811ae9a0..1941fa7b85 100644 --- a/tools/modern-tests/react-router.test.js +++ b/tools/modern-tests/react-router.test.js @@ -27,6 +27,10 @@ describe('ReactRouter App Bundling /', () => { await assertBodyStyles({ 'white-space': 'break-spaces', }); + // Meteor modules config + await assertBodyStyles({ + 'align-content': 'center', + }); // Custom html rspack plugin options await assertMetaTags({ 'theme-color': '#4285f4', @@ -50,6 +54,10 @@ describe('ReactRouter App Bundling /', () => { await assertBodyStyles({ 'white-space': 'break-spaces', }); + // Meteor modules config + await assertBodyStyles({ + 'align-content': 'center', + }); // Custom html rspack plugin options await assertMetaTags({ 'theme-color': '#4285f4', diff --git a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md index 056ce5cc93..434cd777eb 100644 --- a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md +++ b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md @@ -174,6 +174,20 @@ Ensure your app defines these entry files with the correct paths where each modu Defining entry points improves performance even with the Meteor bundler, as Meteor stops scanning and eagerly loading unnecessary files. For Meteor-Rspack integration, this is required, since it does not support automatic code discovery for efficiency. +In Meteor-Rspack integration, all app code is ignored by Meteor and handled by Rspack. By default, Meteor still processes eagerly CSS and HTML files in the entry folder (e.g. `client/`). + +If you need Meteor to handle CSS or HTML files outside the main entry folder, add them to the `modules` field. This field accepts an array of strings, each pointing to a file or folder, except those inside the reserved `imports` folder for scripts. + +``` json +{ + "meteor": { + "modules": ["styles/main.css"] + } +} +``` + +With this, Meteor will process these files, merge stylesheets, generate the final HTML, and support files a Meteor plugin may use, except for JS or script code now handled by Rspack. You can also process CSS and HTML files directly with Rspack using loaders from imports in your app code, as mentioned in ["CSS, Less and SCSS"](#css-less-and-scss) or ["HtmlRspackPlugin"](#htmlrspackplugin). If you prefer Meteor's loading approach, you can still rely on it. + ### Nested Imports Nested imports are a feature of Meteor’s bundler, not supported in standard bundlers. Meteor introduced them during a time when bundling standards were still evolving and experimented with its own approach. This feature comes from the [`reify` module](https://github.com/benjamn/reify/tree/main) and works with Babel transpilation. SWC doesn't support them since they were never standardized.