From 8893730a2fb9985151a6494e8997ccee2fbfb218 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 24 Sep 2024 14:37:34 +0200 Subject: [PATCH] fix(create-strapi-app): yarn 4 support (#21329) --- .../create-strapi-app/src/create-strapi.ts | 25 ++- .../src/utils/get-package-manager-args.ts | 144 ++++++++++++++++++ 2 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 packages/cli/create-strapi-app/src/utils/get-package-manager-args.ts diff --git a/packages/cli/create-strapi-app/src/create-strapi.ts b/packages/cli/create-strapi-app/src/create-strapi.ts index 94bd8e54b0..263a63a461 100644 --- a/packages/cli/create-strapi-app/src/create-strapi.ts +++ b/packages/cli/create-strapi-app/src/create-strapi.ts @@ -14,6 +14,7 @@ import { isStderrError } from './types'; import type { Scope } from './types'; import { logger } from './utils/logger'; import { gitIgnore } from './utils/gitignore'; +import { getInstallArgs } from './utils/get-package-manager-args'; async function createStrapi(scope: Scope) { const { rootPath } = scope; @@ -239,29 +240,27 @@ async function createApp(scope: Scope) { } } -const installArguments = ['install']; +async function runInstall({ rootPath, packageManager }: Scope) { + // include same cwd and env to ensure version check returns same version we use below + const { envArgs, cmdArgs } = await getInstallArgs(packageManager, { + cwd: rootPath, + env: { + ...process.env, + NODE_ENV: 'development', + }, + }); -const installArgumentsMap = { - npm: ['--legacy-peer-deps'], - yarn: ['--network-timeout 1000000'], - pnpm: [], -}; - -function runInstall({ rootPath, packageManager }: Scope) { const options: execa.Options = { cwd: rootPath, stdio: 'inherit', env: { ...process.env, + ...envArgs, NODE_ENV: 'development', }, }; - if (packageManager in installArgumentsMap) { - installArguments.push(...(installArgumentsMap[packageManager] ?? [])); - } - - const proc = execa(packageManager, installArguments, options); + const proc = execa(packageManager, cmdArgs, options); return proc; } diff --git a/packages/cli/create-strapi-app/src/utils/get-package-manager-args.ts b/packages/cli/create-strapi-app/src/utils/get-package-manager-args.ts new file mode 100644 index 0000000000..f1e21eef99 --- /dev/null +++ b/packages/cli/create-strapi-app/src/utils/get-package-manager-args.ts @@ -0,0 +1,144 @@ +import execa from 'execa'; +import semver from 'semver'; + +const installArguments = ['install']; + +type VersionedArgumentsMap = { + [key: string]: string[]; // Maps semver ranges to argument arrays +}; + +type VersionedEnvMap = { + [key: string]: Record; // Maps semver ranges to environment variables +}; + +// Set command line options for specific package managers, with full semver ranges +const installArgumentsMap: { + [key: string]: VersionedArgumentsMap; +} = { + npm: { + '*': ['--legacy-peer-deps'], + }, + yarn: { + '<4': ['--network-timeout', '1000000'], + '*': [], + }, + pnpm: { + '*': [], + }, +}; + +// Set environment variables for specific package managers, with full semver ranges +const installEnvMap: { + [key: string]: VersionedEnvMap; +} = { + yarn: { + '>=4': { YARN_HTTP_TIMEOUT: '1000000' }, + '*': {}, + }, + npm: { + '*': {}, + }, + pnpm: { + '*': {}, + }, +}; + +/** + * Retrieves the version of the specified package manager. + * + * Executes the package manager's `--version` command to determine its version. + * + * @param packageManager - The name of the package manager (e.g., 'npm', 'yarn', 'pnpm'). + * @param options - Optional execution options to pass to `execa`. + * @returns A promise that resolves to the trimmed version string of the package manager. + * + * @throws Will throw an error if the package manager's version cannot be determined. + */ +export const getPackageManagerVersion = async ( + packageManager: string, + options?: execa.Options +): Promise => { + try { + const { stdout } = await execa(packageManager, ['--version'], options); + return stdout.trim(); + } catch (err) { + throw new Error(`Error detecting ${packageManager} version: ${err}`); + } +}; + +/** + * Merges all matching semver ranges using a custom merge function. + * + * Iterates over the `versionMap`, checking if the provided `version` satisfies each semver range. + * If it does, the corresponding value is merged using the provided `mergeFn`. + * The merging starts with the value associated with the wildcard '*' key. + * + * @param version - The package manager version to check against the ranges. + * @param versionMap - A map of semver ranges to corresponding values (arguments or environment variables). + * @param mergeFn - A function that defines how to merge two values (accumulated and current). + * @returns The merged result of all matching ranges. + */ +function mergeMatchingVersionRanges( + version: string, + versionMap: { [key: string]: T }, + mergeFn: (acc: T, curr: T) => T +): T { + return Object.keys(versionMap).reduce((acc, range) => { + if (semver.satisfies(version, range) || range === '*') { + return mergeFn(acc, versionMap[range]); + } + return acc; + }, versionMap['*']); // Start with the wildcard entry +} + +function mergeArguments(acc: string[], curr: string[]): string[] { + return [...acc, ...curr]; +} + +function mergeEnvVars( + acc: Record, + curr: Record +): Record { + return { ...acc, ...curr }; +} + +/** + * Retrieves the install arguments and environment variables for a given package manager. + * + * This function determines the correct command line arguments and environment variables + * based on the package manager's version. It uses predefined semver ranges to match + * the package manager's version and merges all applicable settings. + * + * The arguments and environment variables are sourced from: + * - `installArgumentsMap` for command line arguments. + * - `installEnvMap` for environment variables. + * + * The function ensures that all matching semver ranges are considered and merged appropriately. + * It always includes the base `installArguments` (e.g., `['install']`) and applies any additional + * arguments or environment variables as defined by the matched version ranges. + * + * @param packageManager - The name of the package manager (e.g., 'npm', 'yarn', 'pnpm'). + * @param options - Optional execution options to pass to `execa`. + * @returns An object containing: + * - `cmdArgs`: The full array of install arguments for the given package manager and version. + * - `envArgs`: The merged environment variables applicable to the package manager and version. + * + * @throws Will throw an error if the package manager version cannot be determined. + */ +export const getInstallArgs = async (packageManager: string, options?: execa.Options) => { + const packageManagerVersion = await getPackageManagerVersion(packageManager, options); + + // Get environment variables + const envMap = installEnvMap[packageManager]; + const envArgs = packageManagerVersion + ? mergeMatchingVersionRanges(packageManagerVersion, envMap, mergeEnvVars) + : envMap['*']; + + // Get install arguments + const argsMap = installArgumentsMap[packageManager]; + const cmdArgs = packageManagerVersion + ? mergeMatchingVersionRanges(packageManagerVersion, argsMap, mergeArguments) + : argsMap['*']; + + return { envArgs, cmdArgs: [...installArguments, ...cmdArgs], version: packageManagerVersion }; +};