diff --git a/packages/rspack/lib/dependencies.js b/packages/rspack/lib/dependencies.js index f458457a5c..feb67ef3a3 100644 --- a/packages/rspack/lib/dependencies.js +++ b/packages/rspack/lib/dependencies.js @@ -81,6 +81,9 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package logInfo(` • ${dep}`); }); + // Check if this is a Yarn project + const isYarnProj = process.env.YARN_ENABLED === 'true'; + // Install dev dependencies const devDepsToInstall = allDepsToInstall.filter(dep => dep.dev === true || dep.dev == null); if (devDepsToInstall.length > 0) { @@ -88,6 +91,7 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package success = await installNpmDependency(devDepsStrings, { cwd: appDir, dev: true, + yarn: isYarnProj, }); } @@ -95,22 +99,30 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package const depsToInstall = allDepsToInstall.filter(dep => dep.dev === false); if (depsToInstall.length > 0) { const depsStrings = depsToInstall.map(dep => `${dep.name}@${dep.version}`); - const depsSuccess = await installNpmDependency(depsStrings, { + + let depsSuccess; + depsSuccess = await installNpmDependency(depsStrings, { cwd: appDir, dev: false, + yarn: isYarnProj, }); success = success && depsSuccess; } if (!success) { + const isYarnProj = process.env.YARN_ENABLED === 'true'; + const installCommand = isYarnProj + ? `yarn add --dev ${joinWithAnd(dependencyStrings)}` + : `meteor npm install -D ${joinWithAnd(dependencyStrings)}`; + logError(`\n┌─────────────────────────────────────────────────`); logError(`│ ❌ ${packageName} Installation Failed`); logError(`└─────────────────────────────────────────────────`); - logError(`Run: meteor npm install -D ${joinWithAnd(dependencyStrings)}`); + logError(`Run: ${installCommand}`); throw new Error( - `Failed to install ${packageName} dependencies. Please install them manually with: meteor npm install -D ${joinWithAnd(dependencyStrings)}` + `Failed to install ${packageName} dependencies. Please install them manually with: ${installCommand}` ); } diff --git a/packages/rspack/rspack_plugin.js b/packages/rspack/rspack_plugin.js index 737e590796..c76f6dbd2b 100644 --- a/packages/rspack/rspack_plugin.js +++ b/packages/rspack/rspack_plugin.js @@ -79,17 +79,29 @@ const { const { getNpxCommand, getNpmCommand, + getYarnCommand, + isYarnProject, } = require('meteor/tools-core/lib/npm'); if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) { // Get entry points from Meteor configuration setGlobalState(GLOBAL_STATE_KEYS.INITIAL_ENTRYPONTS, getMeteorAppEntrypoints()); + let isYarnProj = process.env.YARN_ENABLED === 'true'; + console.log("--> (rspack_plugin.js-Line: 91)\n process.env.YARN_ENABLED: ", process.env.YARN_ENABLED); // Main entry point - using top-level await try { + // Check if the project is a Yarn project and store the result in environment variable if not already set + if (process.env.YARN_ENABLED === undefined) { + isYarnProj = isYarnProject(); + process.env.YARN_ENABLED = isYarnProj ? 'true' : 'false'; + } if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { logInfo(`[i] Meteor Npx prefix: ${getNpxCommand([])?.prefix}`); logInfo(`[i] Meteor Npm prefix: ${getNpmCommand([])?.prefix}`); + if (isYarnProj) { + logInfo(`[i] Meteor Yarn prefix: ${getYarnCommand([])?.prefix}`); + } } // Clean build context files only if they haven't been cleaned yet diff --git a/packages/tools-core/lib/npm.js b/packages/tools-core/lib/npm.js index a07bad0b1a..315b52fb06 100644 --- a/packages/tools-core/lib/npm.js +++ b/packages/tools-core/lib/npm.js @@ -136,6 +136,37 @@ function buildNpmInstallArgs(dependencies, options = {}) { return args; } +/** + * Builds yarn install arguments based on options and dependencies + * + * @param {string|string[]} dependencies - The npm dependency or dependencies to install + * @param {Object} [options] - Options for the installation + * @param {boolean} [options.dev=false] - If true, install as a dev dependency + * @param {boolean} [options.exact=false] - If true, install with exact version + * @returns {string[]} Array of arguments for the yarn add command + */ +function buildYarnInstallArgs(dependencies, options = {}) { + const args = ['add']; + + // Add flags based on options + if (options.dev) { + args.push('--dev'); + } + + if (options.exact) { + args.push('--exact'); + } + + // Add dependencies to the command + if (Array.isArray(dependencies)) { + args.push(...dependencies); + } else { + args.push(dependencies); + } + + return args; +} + /** * Executes a command and returns a promise that resolves to true if successful * @@ -161,17 +192,26 @@ function executeCommand(command, args, options) { /** * Installs a npm dependency using direct npm binary if available, otherwise falls back to `meteor npm install`. + * If yarn option is true, uses yarn instead. * * @param {string|string[]} dependencies - The npm dependency or dependencies to install * @param {Object} [options] - Options for the installation * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) * @param {boolean} [options.dev=false] - If true, install as a dev dependency * @param {boolean} [options.exact=false] - If true, install with exact version + * @param {boolean} [options.yarn=false] - If true, use yarn instead of npm * @returns {Promise} A promise that resolves to true if installation succeeded, false otherwise */ export function installNpmDependency(dependencies, options = {}) { const cwd = options.cwd || process.cwd(); + // If yarn option is true, use yarn + if (options.yarn) { + const { command, args: baseArgs } = getYarnCommand([]); + const args = buildYarnInstallArgs(dependencies, options); + return executeCommand(command, [...baseArgs, ...args], { cwd }); + } + // Try to get the npm binary path const npmBinaryPath = getNodeBinaryPath('npm'); @@ -319,3 +359,64 @@ export function getNpxCommand(args) { prefix: `meteor npx`, }; } + +/** + * Checks if the current project is a Yarn project. + * Looks for yarn.lock file in the current working directory and checks packageManager in package.json. + * + * @param {Object} [options] - Options for the check + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @returns {boolean} True if it's a Yarn project, false otherwise + */ +export function isYarnProject(options = {}) { + const cwd = options.cwd || process.cwd(); + + // Check if yarn.lock exists + const yarnLockPath = path.join(cwd, 'yarn.lock'); + if (fs.existsSync(yarnLockPath)) { + return true; + } + + // Check packageManager field in package.json + try { + const packageJsonPath = path.join(cwd, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Check if packageManager contains "yarn" + if (packageJson.packageManager && packageJson.packageManager.includes('yarn')) { + return true; + } + } + } catch (error) { + // If there's an error reading or parsing package.json, continue + } + + return false; +} + +/** + * Gets the yarn command and arguments + * @param {string[]} args - The arguments to pass to yarn + * @returns {Object} An object with command, args, and base properties + */ +export function getYarnCommand(args) { + // Try to get the yarn binary path + const yarnBinaryPath = getNodeBinaryPath('yarn'); + + // If we have a direct path to yarn, use it + if (yarnBinaryPath && fs.existsSync(yarnBinaryPath)) { + return { + command: yarnBinaryPath, + args, + prefix: `${yarnBinaryPath}`, + }; + } + + // Fall back to using 'yarn' directly + return { + command: 'yarn', + args, + prefix: `yarn`, + }; +}