diff --git a/npm-packages/meteor-rspack/rspack.config.js b/npm-packages/meteor-rspack/rspack.config.js index edf81190d7..4d22b79536 100644 --- a/npm-packages/meteor-rspack/rspack.config.js +++ b/npm-packages/meteor-rspack/rspack.config.js @@ -161,7 +161,7 @@ export default function (inMeteor = {}, argv = {}) { }); const externals = [ /^meteor.*/, - ...(isReactEnabled ? [/^react$/, /^react-dom$/] : []) + ...(isReactEnabled ? [/^react$/, /^react-dom$/] : []), ]; const alias = { '/': path.resolve(process.cwd()), @@ -194,7 +194,7 @@ export default function (inMeteor = {}, argv = {}) { output: { path: clientOutputDir, filename: () => - isDev ? outputFilename : `../${buildContext}/${outputPath}`, + isRun && !isTest ? outputFilename : `../${buildContext}/${outputPath}`, libraryTarget: 'commonjs', publicPath: '/', chunkFilename: `${bundlesContext}/[id].[chunkhash].js`, @@ -256,8 +256,8 @@ export default function (inMeteor = {}, argv = {}) { }), ], watchOptions, - devtool: isDev ? 'source-map' : 'hidden-source-map', - ...(isRun && { + devtool: isDev || isTest ? 'source-map' : 'hidden-source-map', + ...(isRun && !isTest && { devServer: { static: { directory: clientOutputDir, publicPath: '/__rspack__/' }, hot: true, @@ -310,7 +310,7 @@ export default function (inMeteor = {}, argv = {}) { ], watchOptions, devtool: isRun ? 'source-map' : 'hidden-source-map', - ...(isRun && + ...((isRun || isTest) && createCacheStrategy(mode) ), }; diff --git a/packages/rspack/lib/build-context.js b/packages/rspack/lib/build-context.js index f941dbffb0..b708698495 100644 --- a/packages/rspack/lib/build-context.js +++ b/packages/rspack/lib/build-context.js @@ -16,7 +16,6 @@ const { isMeteorAppDevelopment, isMeteorAppRun, isMeteorAppBuild, - isMeteorBlazeProject, } = require('meteor/tools-core/lib/meteor'); const { @@ -109,11 +108,11 @@ export function ensureModuleFilesExist() { outputFile: getBuildFilePath({ isMain: true, isServer: true, ...env, role: FILE_ROLE.output, onlyFilename: true }), }; const testClientFiles = { - entryFile: initialEntrypoints.testClient || '', + entryFile: initialEntrypoints.testClient || initialEntrypoints.testModule || '', outputFile: getBuildFilePath({ isTest: true, isClient: true, role: FILE_ROLE.output, onlyFilename: true }), }; const testServerFiles = { - entryFile: initialEntrypoints.testServer || '', + entryFile: initialEntrypoints.testServer || initialEntrypoints.testModule || '', outputFile: getBuildFilePath({ isTest: true, isServer: true, role: FILE_ROLE.output, onlyFilename: true }), }; diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js index 847863a4ad..e9bf568fd5 100644 --- a/packages/rspack/lib/config.js +++ b/packages/rspack/lib/config.js @@ -3,6 +3,7 @@ * @description Functions for configuring Meteor for RSPack */ import RSPACK_BUNDLES_CONTEXT from "./constants"; +import { getInitialEntrypoints } from "./build-context"; const { getMeteorAppFilesAndFolders, @@ -31,6 +32,8 @@ const { * @returns {void} */ export function configureMeteorForRSPack() { + const initialEntrypoints = getInitialEntrypoints(); + // Ignore node_modules to prevent Meteor from processing them const projectFilesAndFolders = getMeteorAppFilesAndFolders({ recursive: false }); const foldersToIgnore = [ @@ -66,16 +69,19 @@ export function configureMeteorForRSPack() { setMeteorAppEntrypoints({ mainClient: `${RSPACK_BUILD_CONTEXT}/${mainClientModule}`, mainServer: `${RSPACK_BUILD_CONTEXT}/${mainServerModule}`, - testClient: `${RSPACK_BUILD_CONTEXT}/${testClientModule}`, - testServer: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`, + ...(initialEntrypoints.testModule && { + testModule: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`, + } || { + testClient: `${RSPACK_BUILD_CONTEXT}/${testClientModule}`, + testServer: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`, + }), }); // Ensure module files exist ensureModuleFilesExist(); // Write content to module files - if (isMeteorAppDevelopment()) { - // writeMainClientEntryForHMR(); + if (isMeteorAppRun()) { setMeteorAppCustomScriptUrl( `/__rspack__/${getBuildFilePath({ ...env, isMain: true, isClient: true, role: FILE_ROLE.output, onlyFilename: true })}`, ); diff --git a/packages/rspack/lib/processes.js b/packages/rspack/lib/processes.js index a716fac41c..2865713465 100644 --- a/packages/rspack/lib/processes.js +++ b/packages/rspack/lib/processes.js @@ -3,6 +3,7 @@ * @description Functions for managing RSPack processes */ import { RSPACK_BUILD_CONTEXT } from "./constants"; +import { getMeteorAppEntrypoints } from "../../tools-core/lib/meteor"; const { spawnProcess, @@ -77,20 +78,12 @@ export function getRSPackEnv({ isClient, isServer }) { const initialEntrypoints = getMeteorInitialAppEntrypoints(); const entryKey = `${isMeteorAppTest() ? 'test' : 'main'}${isClient ? 'Client' : 'Server'}`; - const inputFilePath = initialEntrypoints[entryKey]; + const inputFilePath = initialEntrypoints[entryKey] || `testModule`; const isTypescriptEnabled = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); const isTsxEnabled = inputFilePath.endsWith('.tsx'); const isJsxEnabled = inputFilePath.endsWith('.jsx'); const pairs = [ - ['name', - `${RSPACK_BUILD_CONTEXT}/${getBuildFilePath({ - ...env, - ...side, - isMain: true, - role: FILE_ROLE.output, - })}`, - ], ['isDevelopment', isMeteorAppDevelopment()], ['isProduction', isMeteorAppProduction()], ['isDebug', isMeteorAppDebug()], @@ -240,10 +233,11 @@ export function startRSPackServerWatch(options = {}) { * @param {boolean} options.isClient - Whether this is a client build * @param {boolean} options.isServer - Whether this is a server build * @param {Function} options.onCompile - Callback function to be called when compilation is complete + * @param {boolean} options.watch - Whether to run RSPack in watch mode * @returns {Promise} A promise that resolves when the build is complete * @throws {Error} If the build process fails */ -export async function runRSPackBuild({ isClient, isServer, onCompile } = {}) { +export async function runRSPackBuild({ isClient, isServer, onCompile, watch } = {}) { const appDir = getMeteorAppDir(); const configFile = getConfigFileName(); @@ -252,7 +246,17 @@ export async function runRSPackBuild({ isClient, isServer, onCompile } = {}) { logProgress(`Running RSPack build for ${endpoint}...`); // Use a promise to ensure Meteor waits until RSPack finishes return new Promise((resolve, reject) => { - const buildProcess = spawnProcess('npx', ['rspack', 'build', '--config', configFile, ...getRSPackEnv({ isClient, isServer })], { + spawnProcess( + 'npx', + [ + 'rspack', + 'build', + '--config', + configFile, + ...(watch && ['--watch']) || [], + ...getRSPackEnv({ isClient, isServer }), + ].filter(Boolean), + { cwd: appDir, onStdout: (data) => { logInfo(`[RSPack Build ${endpoint}] ${data}`); diff --git a/packages/rspack/rspack_plugin.js b/packages/rspack/rspack_plugin.js index 1e70c41e93..2d4c50251c 100644 --- a/packages/rspack/rspack_plugin.js +++ b/packages/rspack/rspack_plugin.js @@ -21,7 +21,9 @@ const { const { ensureRSPackInstalled, - checkReactInstalled, ensureRSPackReactInstalled, checkCoffeescriptInstalled, + checkReactInstalled, + ensureRSPackReactInstalled, + checkCoffeescriptInstalled, } = require('./lib/dependencies'); const { @@ -52,7 +54,10 @@ const { const { isMeteorAppRun, isMeteorAppBuild, - getMeteorAppEntrypoints + getMeteorInitialAppEntrypoints, + getMeteorAppEntrypoints, + isMeteorAppTest, + isMeteorAppTestWatch, } = require('meteor/tools-core/lib/meteor'); const { @@ -110,12 +115,57 @@ try { // Wait for first compilation to complete await waitForFirstCompilation(clientFirstCompile, serverFirstCompile, clientFirstCompilePromise, serverFirstCompilePromise); + } else if (isMeteorAppTest()) { + const initialEntrypoints = getMeteorInitialAppEntrypoints(); + + // Setup compilation tracking and callbacks + const { + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + onCompileClient, + onCompileServer, + } = setupCompilationTracking(); + + if (initialEntrypoints?.testModule && isMeteorAppTestWatch()) { + runRSPackBuild({ + isServer: true, + isClient: false, + watch: isMeteorAppTestWatch(), + onCompile: onCompileServer, + }); + } else if (initialEntrypoints?.testModule && !isMeteorAppTestWatch()) { + await runRSPackBuild({ + isServer: true, + isClient: false, + onCompile: onCompileServer, + }); + } else if (initialEntrypoints?.testModule?.client || initialEntrypoints?.testModule?.server) { + runRSPackBuild({ + isClient: true, + isServer: false, + watch: isMeteorAppTestWatch(), + onCompile: onCompileClient, + }); + + runRSPackBuild({ + isServer: true, + isClient: false, + watch: isMeteorAppTestWatch(), + onCompile: onCompileServer, + }); + + // Wait for first compilation to complete + await waitForFirstCompilation(clientFirstCompile, serverFirstCompile, clientFirstCompilePromise, serverFirstCompilePromise); + } + } else if (isMeteorAppBuild()) { // For 'build' command, run RSPack build without watch mode // Run client and server builds in parallel and wait for both to complete await Promise.all([ runRSPackBuild({ isClient: true, isServer: false }), - runRSPackBuild({ isServer: true, isClient: false }) + runRSPackBuild({ isServer: true, isClient: false }), ]); } } catch (error) { diff --git a/packages/tools-core/lib/meteor.js b/packages/tools-core/lib/meteor.js index 5c17b67c9d..775f89d181 100644 --- a/packages/tools-core/lib/meteor.js +++ b/packages/tools-core/lib/meteor.js @@ -61,8 +61,15 @@ export function getMeteorInitialAppEntrypoints() { return { mainClient: meteorConfig?.mainModule?.client, mainServer: meteorConfig?.mainModule?.server, - testClient: meteorConfig?.testModule?.client || meteorConfig?.testModule, - testServer: meteorConfig?.testModule?.server || meteorConfig?.testModule, + ...meteorConfig?.testModule?.client && { + testClient: meteorConfig?.testModule?.client, + }, + ...meteorConfig?.testModule?.server && { + testServer: meteorConfig?.testModule?.server, + }, + ...!meteorConfig?.testModule?.client && !meteorConfig?.testModule?.server && { + testModule: meteorConfig?.testModule, + }, }; } @@ -71,21 +78,32 @@ export function getMeteorInitialAppEntrypoints() { * @param {Object} options - The entry points configuration object. * @param {string} [options.mainClient] - The client main module path. * @param {string} [options.mainServer] - The server main module path. + * @param {string} [options.testModule] - The test module path. * @param {string} [options.testClient] - The client test module path. * @param {string} [options.testServer] - The server test module path. */ -export function setMeteorAppEntrypoints({ mainClient, mainServer, testClient, testServer }) { +export function setMeteorAppEntrypoints({ + mainClient, + mainServer, + testModule, + testClient, + testServer, +}) { if (mainClient) { process.env.METEOR_CONFIG_CLIENT = mainClient; } if (mainServer) { process.env.METEOR_CONFIG_SERVER = mainServer; } - if (testClient) { - process.env.METEOR_CONFIG_TEST_CLIENT = testClient; - } - if (testServer) { - process.env.METEOR_CONFIG_TEST_SERVER = testServer; + if (testModule) { + process.env.METEOR_CONFIG_TEST = testModule; + } else { + if (testClient) { + process.env.METEOR_CONFIG_TEST_CLIENT = testClient; + } + if (testServer) { + process.env.METEOR_CONFIG_TEST_SERVER = testServer; + } } global.ensureMeteorConfigInitialized?.(); } @@ -124,6 +142,14 @@ export function isMeteorAppTest() { || Package?.meteor?.global?.currentCommand?.name === 'test-packages'; } +/** + * Checks if the current Meteor command is 'test' and is running in watch mode. + * @returns {boolean} True if the current command is 'test' and is running in watch mode, false otherwise. + */ +export function isMeteorAppTestWatch() { + return isMeteorAppTest() && !Package?.meteor?.global?.currentCommand?.options?.once; +} + /** * Checks if the Meteor application is running in development mode. * @returns {boolean} True if the application is in development mode, false otherwise.