@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
184 lines (181 loc) • 7.34 kB
JavaScript
import os from 'node:os';
import fs from 'node:fs/promises';
import path from 'node:path';
import semver from 'semver';
import resolveFrom from 'resolve-from';
import execa from 'execa';
import readPkgUp from 'read-pkg-up';
import { getPackageManager } from './managers.mjs';
/**
* From V5 this will be imported from the package.json of `@strapi/strapi`.
*/ const PEER_DEPS = {
react: '^18.0.0',
'react-dom': '^18.0.0',
'react-router-dom': '^6.0.0',
'styled-components': '^6.0.0'
};
/**
* Checks the user's project that it has declared and installed the required dependencies
* needed by the Strapi admin project. Whilst generally speaking most modules will be
* declared by the actual packages there are some packages where you only really want one of
* and thus they are declared as peer dependencies – react / styled-components / etc.
*
* If these deps are not installed or declared, then we prompt the user to correct this. In
* V4 this is not a hard requirement, but in V5 it will be. Might as well get people started now.
*/ const checkRequiredDependencies = async ({ cwd, logger })=>{
/**
* This enables us to use experimental deps for libraries like
* react or styled-components. This is useful for testing against.
*/ if (process.env.USE_EXPERIMENTAL_DEPENDENCIES === 'true') {
logger.warn('You are using experimental dependencies that may not be compatible with Strapi.');
return {
didInstall: false
};
}
const pkg = await readPkgUp({
cwd
});
if (!pkg) {
throw new Error(`Could not find package.json at path: ${cwd}`);
}
logger.debug('Loaded package.json:', os.EOL, pkg.packageJson);
/**
* Run through each of the peer deps and figure out if they need to be
* installed or they need their version checked against.
*/ const { install, review } = Object.entries(PEER_DEPS).reduce((acc, [name, version])=>{
if (!pkg.packageJson.dependencies) {
throw new Error(`Could not find dependencies in package.json at path: ${cwd}`);
}
const declaredVersion = pkg.packageJson.dependencies[name];
if (!declaredVersion) {
acc.install.push({
name,
wantedVersion: version
});
} else {
acc.review.push({
name,
wantedVersion: version,
declaredVersion
});
}
return acc;
}, {
install: [],
review: []
});
if (install.length > 0) {
logger.info('The Strapi admin needs to install the following dependencies:', os.EOL, install.map(({ name, wantedVersion })=>` - ${name}@${wantedVersion}`).join(os.EOL));
await installDependencies(install, {
cwd,
logger
});
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'
});
return {
didInstall: true
};
}
if (review.length) {
const errors = [];
for (const dep of review){
// The version specified in package.json could be incorrect, eg `foo`
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)) {
/**
* The delcared version should be semver compatible with our required version
* of the dependency. If it's not, we should advise the user to change it.
*/ 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) {
/**
* TODO: when we know the packageManager we can advise the actual install command.
*/ 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}- `)}`);
}
}
return {
didInstall: false
};
};
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;
};
const installDependencies = async (install, { 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 = install.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');
}
};
export { checkRequiredDependencies, getModule };
//# sourceMappingURL=dependencies.mjs.map