@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
187 lines (184 loc) • 7.5 kB
JavaScript
import path from 'node:path';
import fs from 'node:fs/promises';
import readPkgUp from 'read-pkg-up';
import { ADMIN_VITE_ALIAS_MODULES } from './admin-vite-alias-modules.mjs';
import { getModule } from './dependencies.mjs';
import { ADMIN_VITE_SINGLETON_MODULES } from './admin-vite-singleton-modules.mjs';
const REACT_PEER_DEPENDENCIES = new Set([
'react',
'react-dom'
]);
/**
* Packages explicitly pre-bundled or aliased for the admin singleton contract.
* Never auto-exclude these — they must stay on the optimizeDeps.include / dedupe path.
*
* The admin entry host (@strapi/strapi) must never land in optimizeDeps.exclude (#26944, #27014).
* CJS-only deps imported by @strapi/admin (e.g. invariant, lodash) belong in optimizeDeps.include
* (see vite/config.ts — #26964, #26944, #27014).
* The CodeMirror singletons (e.g. @uiw/react-codemirror — ESM with a React peer) match the
* exclude heuristic but must stay pre-bundled so the admin keeps a single instance
*/ const PINNED_OPTIMIZE_MODULES = new Set([
...ADMIN_VITE_ALIAS_MODULES,
...ADMIN_VITE_SINGLETON_MODULES,
'@strapi/strapi'
]);
/**
* Thin shared plugin UI kits that are known to break when Vite re-optimizes their pre-built
* dist (original #26944 victims). Only these names — plus packages that opt in via package.json —
* may be auto-excluded. Broad heuristic matching alone was too aggressive for large plugins
* (e.g. CKEditor) and orphaned their transitive CJS/UMD deps (#27136).
*
* @internal exported for tests
*/ const ADMIN_VITE_OPTIMIZE_DEPS_EXCLUDE_ALLOWLIST = new Set([
'strapi-design-extended'
]);
const isOfficialStrapiPackage = (name)=>name.startsWith('@strapi/');
const getRootPackageExport = (pkg)=>{
const exportsField = pkg.exports;
if (!exportsField || typeof exportsField === 'string') {
return undefined;
}
return exportsField['.'];
};
/**
* @internal exported for tests
*/ const isEsmPackage = (pkg)=>{
if (pkg.type === 'module') {
return true;
}
const rootExport = getRootPackageExport(pkg);
return typeof rootExport === 'object' && rootExport !== null && 'import' in rootExport && typeof rootExport.import === 'string';
};
/**
* @internal exported for tests
*/ const hasReactPeerDependency = (pkg)=>Object.keys(pkg.peerDependencies ?? {}).some((name)=>REACT_PEER_DEPENDENCIES.has(name));
/**
* Targets libraries that ship a pre-built dist bundle (e.g. plugin UI kits) rather than
* source packages like react-intl that still benefit from Vite's dep optimizer.
*
* @internal exported for tests
*/ const shipsPreBuiltDist = (pkg)=>{
const rootExport = getRootPackageExport(pkg);
const exportImport = typeof rootExport === 'object' && rootExport !== null && 'import' in rootExport ? rootExport.import : undefined;
const entryPaths = [
pkg.module,
pkg.main,
exportImport
].filter((entry)=>typeof entry === 'string');
if (entryPaths.some((entry)=>entry.includes('/dist/') || entry.startsWith('./dist/') || entry.startsWith('dist/'))) {
return true;
}
return Array.isArray(pkg.files) && pkg.files.some((file)=>file === 'dist' || file.startsWith('dist/'));
};
/**
* Shape check for packages that *could* be excluded (pre-built ESM + React peer + dist).
* Matching alone is not enough to exclude — see {@link isEligibleForOptimizeDepsExclude}.
*
* @internal exported for tests
*/ const shouldExcludeFromOptimizeDeps = (pkg)=>hasReactPeerDependency(pkg) && isEsmPackage(pkg) && shipsPreBuiltDist(pkg);
/**
* Packages may opt into optimizeDeps.exclude via package.json:
* `{ "strapi": { "admin": { "vite": { "optimizeDepsExclude": true } } } }`
*
* @internal exported for tests
*/ const packageOptsIntoOptimizeDepsExclude = (pkg)=>{
const strapi = pkg.strapi;
return strapi?.admin?.vite?.optimizeDepsExclude === true;
};
/**
* A candidate is excluded only when it matches the UI-kit shape AND is either on the
* known-safe allowlist or explicitly opts in. This keeps #26944 working for thin kits
* without auto-excluding large editor/chart plugins (#27136).
*
* @internal exported for tests
*/ const isEligibleForOptimizeDepsExclude = (name, pkg)=>{
if (!shouldExcludeFromOptimizeDeps(pkg)) {
return false;
}
return ADMIN_VITE_OPTIMIZE_DEPS_EXCLUDE_ALLOWLIST.has(name) || packageOptsIntoOptimizeDepsExclude(pkg);
};
/**
* @internal exported for tests
*/ const getPluginPackageName = (modulePath)=>{
if (modulePath.startsWith('@')) {
const [scope, name] = modulePath.split('/');
return `${scope}/${name}`;
}
return modulePath.split('/')[0] ?? modulePath;
};
const collectDependencyNames = (pkg)=>{
const names = new Set();
for (const section of [
pkg.dependencies,
pkg.devDependencies
]){
if (section) {
for (const name of Object.keys(section)){
names.add(name);
}
}
}
return [
...names
];
};
const getPluginPackageJson = async (plugin, cwd)=>{
if (plugin.type === 'local' && plugin.path) {
try {
const content = await fs.readFile(path.join(plugin.path, 'package.json'), 'utf8');
return JSON.parse(content);
} catch {
return null;
}
}
return getModule(getPluginPackageName(plugin.modulePath), cwd);
};
const loadAppPackageJson = async (cwd)=>{
const result = await readPkgUp({
cwd
});
return result?.packageJson ?? null;
};
/**
* Pre-built ESM libraries with React peers (shared plugin UI kits) are incompatible with
* Strapi's React/design-system pre-bundling. Skip dep optimization so they resolve through
* the admin resolve aliases instead of being re-bundled by Vite.
*
* Scans app and plugin dependency trees (#26944). Only allowlisted or opt-in packages that
* also match the UI-kit shape are excluded (#27136). Official @strapi/* packages and pinned
* singletons are never auto-excluded — @strapi/strapi matches the heuristic but must stay on
* the optimizeDeps.include path (#26944, #27014).
*
* Apps can still exclude additional packages via `src/admin/vite.config` optimizeDeps.exclude.
*
* @internal
*/ const collectAdminOptimizeDepsExclude = async (cwd, plugins)=>{
const candidateNames = new Set();
const appPkg = await loadAppPackageJson(cwd);
if (appPkg) {
for (const name of collectDependencyNames(appPkg)){
candidateNames.add(name);
}
}
for (const plugin of plugins){
const pluginPkg = await getPluginPackageJson(plugin, cwd);
if (pluginPkg) {
for (const name of collectDependencyNames(pluginPkg)){
candidateNames.add(name);
}
}
}
const exclude = [];
for (const name of candidateNames){
if (PINNED_OPTIMIZE_MODULES.has(name) || isOfficialStrapiPackage(name)) {
continue;
}
const pkg = await getModule(name, cwd);
if (pkg && isEligibleForOptimizeDepsExclude(name, pkg)) {
exclude.push(name);
}
}
return exclude.sort();
};
export { ADMIN_VITE_OPTIMIZE_DEPS_EXCLUDE_ALLOWLIST, collectAdminOptimizeDepsExclude, getPluginPackageName, hasReactPeerDependency, isEligibleForOptimizeDepsExclude, isEsmPackage, packageOptsIntoOptimizeDepsExclude, shipsPreBuiltDist, shouldExcludeFromOptimizeDeps };
//# sourceMappingURL=admin-vite-optimize-exclude.mjs.map