UNPKG

@strapi/strapi

Version:

An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite

207 lines (204 loc) 8.19 kB
import os from 'node:os'; import fs from 'node:fs/promises'; import path from 'node:path'; import crypto from 'node:crypto'; import semver from 'semver'; import resolveFrom from 'resolve-from'; import execa from 'execa'; import readPkgUp from 'read-pkg-up'; import { getPackageManager } from './managers.mjs'; const CACHE_PATH = path.join('node_modules', '.strapi', 'deps-check.hash'); const hashPackageJson = async (cwd)=>{ try { const content = await fs.readFile(path.join(cwd, 'package.json'), 'utf8'); return crypto.createHash('sha1').update(content).digest('hex'); } catch { return null; } }; const readCachedHash = (cwd)=>fs.readFile(path.join(cwd, CACHE_PATH), 'utf8').catch(()=>null); const writeCachedHash = async (cwd, hash)=>{ try { await fs.mkdir(path.dirname(path.join(cwd, CACHE_PATH)), { recursive: true }); await fs.writeFile(path.join(cwd, CACHE_PATH), hash, 'utf8'); } catch { // best-effort cache write — silently ignore } }; /** * From V5 this will be imported from the package.json of `@strapi/strapi`. */ const ADMIN_PEER_DEPS = { react: '^18.0.0', 'react-dom': '^18.0.0', 'react-router-dom': '^6.0.0', 'styled-components': '^6.0.0' }; class MissingAdminPeerDepsError extends Error { constructor(missing){ super('Missing required dependencies. Please install them and re-run this command.'); this.name = 'MissingAdminPeerDepsError'; this.missing = missing; } } const loadProjectPackageJson = async (cwd)=>{ const pkg = await readPkgUp({ cwd }); if (!pkg) { throw new Error(`Could not find package.json at path: ${cwd}`); } if (!pkg.packageJson.dependencies) { throw new Error(`Could not find dependencies in package.json at path: ${cwd}`); } return pkg; }; /** * Returns admin peer dependencies that are not declared in the project's package.json. */ const findUndeclaredAdminPeerDeps = async (cwd)=>{ const pkg = await loadProjectPackageJson(cwd); return Object.entries(ADMIN_PEER_DEPS).reduce((missing, [name, version])=>{ const declaredVersion = pkg.packageJson.dependencies?.[name] ?? pkg.packageJson.devDependencies?.[name]; if (!declaredVersion) { missing.push({ name, wantedVersion: version }); } return missing; }, []); }; const getDeclaredAdminPeerDeps = (packageJson)=>{ return Object.entries(ADMIN_PEER_DEPS).reduce((declared, [name, version])=>{ const declaredVersion = packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name]; if (declaredVersion) { declared.push({ name, wantedVersion: version, declaredVersion }); } return declared; }, []); }; const getInstallCommandHint = (missing)=>{ const packages = missing.map(({ name, wantedVersion })=>`${name}@${wantedVersion}`).join(' '); const packageManager = getPackageManager(); if (packageManager === 'yarn') { return `yarn add ${packages}`; } if (packageManager === 'pnpm') { return `pnpm add --save-prod ${packages}`; } return `npm install --legacy-peer-deps --save ${packages}`; }; const reportMissingAdminPeerDeps = (logger, missing)=>{ logger.error('The Strapi admin is missing required dependencies:', os.EOL, missing.map(({ name, wantedVersion })=>` - ${name}@${wantedVersion}`).join(os.EOL)); logger.error('Please install them manually before re-running this command:', os.EOL, ` ${getInstallCommandHint(missing)}`); }; /** * Validates declared admin peer dependencies (versions and node_modules presence). */ const validateDeclaredAdminPeerDeps = async (cwd, logger)=>{ const pkg = await loadProjectPackageJson(cwd); const declared = getDeclaredAdminPeerDeps(pkg.packageJson); if (!declared.length) { return; } logger.debug('Loaded package.json:', os.EOL, pkg.packageJson); const errors = []; for (const dep of declared){ let minDeclaredVersion = null; try { minDeclaredVersion = semver.minVersion(dep.declaredVersion); } catch (err) { // Intentional fall-through (variable will be left as null, throwing below) } if (!minDeclaredVersion) { errors.push(`The declared dependency, ${dep.name} has an invalid version in package.json: ${dep.declaredVersion}`); } else if (!semver.satisfies(minDeclaredVersion, dep.wantedVersion)) { logger.warn([ `Declared version of ${dep.name} (${minDeclaredVersion}) is not compatible with the version required by Strapi (${dep.wantedVersion}).`, 'You may experience issues, we recommend you change this.' ].join(os.EOL)); } const installedVersion = await getModuleVersion(dep.name, cwd); if (!installedVersion) { errors.push(`The declared dependency, ${dep.name} is not installed. You should install before re-running this command`); } else if (!semver.satisfies(installedVersion, dep.wantedVersion)) { logger.warn([ `Declared version of ${dep.name} (${installedVersion}) is not compatible with the version required by Strapi (${dep.wantedVersion}).`, 'You may experience issues, we recommend you change this.' ].join(os.EOL)); } } if (errors.length > 0 && process.env.NODE_ENV === 'development') { throw new Error(`${os.EOL}- ${errors.join(`${os.EOL}- `)}`); } }; const installAdminPeerDeps = async (missing, { cwd, logger })=>{ const packageManager = getPackageManager(); if (!packageManager) { logger.error('Could not find a supported package manager, please install the dependencies manually.'); process.exit(1); } const execOptions = { encoding: 'utf8', cwd, stdio: 'inherit' }; const packages = missing.map(({ name, wantedVersion })=>`${name}@${wantedVersion}`); let result; if (packageManager === 'npm') { const npmArgs = [ 'install', '--legacy-peer-deps', '--save', ...packages ]; logger.info(`Running 'npm ${npmArgs.join(' ')}'`); result = await execa('npm', npmArgs, execOptions); } else if (packageManager === 'yarn') { const yarnArgs = [ 'add', ...packages ]; logger.info(`Running 'yarn ${yarnArgs.join(' ')}'`); result = await execa('yarn', yarnArgs, execOptions); } else if (packageManager === 'pnpm') { const pnpmArgs = [ 'add', '--save-prod', ...packages ]; logger.info(`Running 'pnpm ${pnpmArgs.join(' ')}'`); result = await execa('pnpm', pnpmArgs, execOptions); } if (result?.exitCode || result?.failed) { throw new Error('Package installation failed'); } }; const reexecCurrentCommand = async (cwd)=>{ const [file, ...args] = process.argv; /** * Re-run the same command after installation e.g. strapi build because the yarn.lock might * not be the same and could break installations. It's not the best solution, but it works. */ await execa(file, args, { cwd, stdio: 'inherit' }); }; const getModule = async (name, cwd)=>{ const modulePackagePath = resolveFrom.silent(cwd, path.join(name, 'package.json')); if (!modulePackagePath) { return null; } const file = await fs.readFile(modulePackagePath, 'utf8').then((res)=>JSON.parse(res)); return file; }; const getModuleVersion = async (name, cwd)=>{ const pkg = await getModule(name, cwd); return pkg?.version || null; }; export { ADMIN_PEER_DEPS, MissingAdminPeerDepsError, findUndeclaredAdminPeerDeps, getInstallCommandHint, getModule, hashPackageJson, installAdminPeerDeps, readCachedHash, reexecCurrentCommand, reportMissingAdminPeerDeps, validateDeclaredAdminPeerDeps, writeCachedHash }; //# sourceMappingURL=dependencies.mjs.map