fix(admin): admin build errors (#18764) (#18770)

* fix(admin): import & pass user customisations for admin panel

* fix(admin): modules should be resolved with module paths not system paths

* fix(admin): ensure webpack config is a type of function before calling, warn otherwise

* fix: convert the pluginPath to a system path before trying to resolve relative

* chore: update documentation
This commit is contained in:
Josh 2023-11-13 12:07:27 +00:00 committed by GitHub
parent da634b0951
commit 2f026ea9b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 95 deletions

View File

@ -44,6 +44,10 @@ interface BuildContext {
* this path so all asset paths will be rewritten accordingly
*/
basePath: string;
/**
* The customisations defined by the user in their app.js file
*/
customisations?: AppFile;
/**
* The current working directory
*/
@ -64,9 +68,9 @@ interface BuildContext {
* The environment variables to be included in the JS bundle
*/
env: Record<string, string>;
logger: Logger;
logger: CLIContext['logger'];
/**
* The build or develop options
* The build options
*/
options: Pick<BuildOptions, 'minify' | 'sourcemaps' | 'stats'> & Pick<DevelopOptions, 'open'>;
/**
@ -90,7 +94,7 @@ interface BuildContext {
* The browserslist target either loaded from the user's workspace or falling back to the default
*/
target: string[];
tsconfig?: TSConfig;
tsconfig?: CLIContext['tsconfig'];
}
```

View File

@ -24,7 +24,7 @@ Start your Strapi application in development mode
Options:
--polling Watch for file changes (default: false) Whether to use fs.watchFile (backed by polling), or fs.watch, this is passed directly to chokidar
--no-build [deprecated]: there is middleware for the server, it is no longer a separate process
--watch-admin [deprecated]: there is now middleware for watching, it is no longer a separate process
--watch-admin Watch the admin for changes (default: false)
--browser <name> [deprecated]: use open instead
--open Open the admin in your browser (default: true)
-h, --help Display help for command
@ -66,6 +66,10 @@ interface DevelopOptions extends CLIContext {
* The tsconfig to use for the build. If undefined, this is not a TS project.
*/
tsconfig?: TsConfig;
/**
* Watch the admin for changes
*/
watchAdmin?: boolean;
}
interface Logger {

View File

@ -1,9 +0,0 @@
const config = {
locales: ['fr'],
};
const bootstrap = () => {};
export default {
config,
bootstrap,
};

View File

@ -0,0 +1,11 @@
const config = {
locales: ['it', 'es', 'en'],
};
const bootstrap = () => {
console.log('I AM BOOTSTRAPPED');
};
export default {
config,
bootstrap,
};

View File

@ -0,0 +1,32 @@
import path from 'node:path';
import { loadFile } from './files';
const ADMIN_APP_FILES = ['app.js', 'app.mjs', 'app.ts', 'app.jsx', 'app.tsx'];
interface AdminCustomisations {
config?: {
locales?: string[];
};
bootstrap?: Function;
}
interface AppFile {
path: string;
config: AdminCustomisations['config'];
}
const loadUserAppFile = async (appDir: string): Promise<AppFile | undefined> => {
for (const file of ADMIN_APP_FILES) {
const filePath = path.join(appDir, 'src', 'admin', file);
const configFile = await loadFile(filePath);
if (configFile) {
return { path: filePath, config: configFile };
}
}
return undefined;
};
export { loadUserAppFile };
export type { AdminCustomisations, AppFile };

View File

@ -1,9 +1,12 @@
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import camelCase from 'lodash/camelCase';
import { env } from '@strapi/utils';
import { getModule, PackageJson } from './dependencies';
import { loadFile } from './files';
import { BuildContext, CreateBuildContextArgs } from '../createBuildContext';
import { BuildContext } from '../createBuildContext';
import { isError } from './errors';
interface PluginMeta {
name: string;
@ -32,11 +35,11 @@ const validatePackageHasStrapi = (
const validatePackageIsPlugin = (pkg: PackageJson): pkg is StrapiPlugin =>
validatePackageHasStrapi(pkg) && pkg.strapi.kind === 'plugin';
export const getEnabledPlugins = async ({
const getEnabledPlugins = async ({
strapi,
cwd,
logger,
}: Pick<BuildContext, 'cwd' | 'logger' | 'strapi'>) => {
}: Pick<BuildContext, 'cwd' | 'logger' | 'strapi'>): Promise<Record<string, PluginMeta>> => {
const plugins: Record<string, PluginMeta> = {};
/**
@ -110,3 +113,73 @@ const loadUserPluginsFile = async (root: string): Promise<UserPluginConfigFile>
return {};
};
const getMapOfPluginsWithAdmin = (
plugins: Record<string, PluginMeta>,
{ runtimeDir }: { runtimeDir: string }
) =>
Object.values(plugins)
.filter((plugin) => {
if (!plugin) {
return false;
}
/**
* There are two ways a plugin should be imported, either it's local to the strapi app,
* or it's an actual npm module that's installed and resolved via node_modules.
*
* We first check if the plugin is local to the strapi app, using a regular `resolve` because
* the pathToPlugin will be relative i.e. `/Users/my-name/strapi-app/src/plugins/my-plugin`.
*
* If the file doesn't exist well then it's probably a node_module, so instead we use `require.resolve`
* which will resolve the path to the module in node_modules. If it fails with the specific code `MODULE_NOT_FOUND`
* then it doesn't have an admin part to the package.
*/
try {
const isLocalPluginWithLegacyAdminFile = fs.existsSync(
//@ts-ignore
path.resolve(`${plugin.pathToPlugin}/strapi-admin.js`)
);
if (!isLocalPluginWithLegacyAdminFile) {
//@ts-ignore
let pathToPlugin = plugin.pathToPlugin;
if (process.platform === 'win32') {
pathToPlugin = pathToPlugin.split(path.sep).join(path.posix.sep);
}
const isModuleWithFE = require.resolve(`${pathToPlugin}/strapi-admin`);
return isModuleWithFE;
}
return isLocalPluginWithLegacyAdminFile;
} catch (err) {
if (isError(err) && 'code' in err && err.code === 'MODULE_NOT_FOUND') {
/**
* the plugin does not contain FE code, so we
* don't want to import it anyway
*/
return false;
}
throw err;
}
})
.map((plugin) => {
const systemPath = plugin.isLocal
? path.relative(runtimeDir, plugin.pathToPlugin.split('/').join(path.sep))
: undefined;
const modulePath = systemPath ? systemPath.split(path.sep).join('/') : undefined;
return {
path: !plugin.isLocal
? `${plugin.pathToPlugin}/strapi-admin`
: `${modulePath}/strapi-admin`,
name: plugin.name,
importName: camelCase(plugin.name),
};
});
export { getEnabledPlugins, getMapOfPluginsWithAdmin };

View File

@ -1,19 +1,17 @@
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs/promises';
import syncFs from 'node:fs';
import camelCase from 'lodash/camelCase';
import browserslist from 'browserslist';
import strapiFactory, { CLIContext } from '@strapi/strapi';
import { getConfigUrls } from '@strapi/utils';
import { getStrapiAdminEnvVars, loadEnv } from './core/env';
import { isError } from './core/errors';
import type { BuildOptions } from './build';
import { DevelopOptions } from './develop';
import { getEnabledPlugins } from './core/plugins';
import { getEnabledPlugins, getMapOfPluginsWithAdmin } from './core/plugins';
import { Strapi } from '@strapi/types';
import { AppFile, loadUserAppFile } from './core/admin-customisations';
interface BuildContext {
/**
@ -25,6 +23,10 @@ interface BuildContext {
* this path so all asset paths will be rewritten accordingly
*/
basePath: string;
/**
* The customisations defined by the user in their app.js file
*/
customisations?: AppFile;
/**
* The current working directory
*/
@ -152,23 +154,18 @@ const createBuildContext = async ({
logger.debug('Enabled plugins', os.EOL, plugins);
const pluginsWithFront = Object.values(plugins)
.filter(filterPluginsByAdminEntry)
.map((plugin) => ({
path: !plugin.isLocal
? `${plugin.pathToPlugin}/strapi-admin`
: `${path.relative(runtimeDir, plugin.pathToPlugin)}/strapi-admin`,
name: plugin.name,
importName: camelCase(plugin.name),
}));
const pluginsWithFront = getMapOfPluginsWithAdmin(plugins, { runtimeDir });
logger.debug('Enabled plugins with FE', os.EOL, plugins);
const target = browserslist.loadConfig({ path: cwd }) ?? DEFAULT_BROWSERSLIST;
const customisations = await loadUserAppFile(strapiInstance.dirs.app.root);
const buildContext = {
appDir: strapiInstance.dirs.app.root,
basePath: `${adminPath}/`,
customisations,
cwd,
distDir,
distPath,
@ -186,58 +183,5 @@ const createBuildContext = async ({
return buildContext;
};
interface Plugin extends Required<{}> {
name: string;
}
const filterPluginsByAdminEntry = (plugin: Plugin) => {
if (!plugin) {
return false;
}
/**
* There are two ways a plugin should be imported, either it's local to the strapi app,
* or it's an actual npm module that's installed and resolved via node_modules.
*
* We first check if the plugin is local to the strapi app, using a regular `resolve` because
* the pathToPlugin will be relative i.e. `/Users/my-name/strapi-app/src/plugins/my-plugin`.
*
* If the file doesn't exist well then it's probably a node_module, so instead we use `require.resolve`
* which will resolve the path to the module in node_modules. If it fails with the specific code `MODULE_NOT_FOUND`
* then it doesn't have an admin part to the package.
*/
try {
const isLocalPluginWithLegacyAdminFile = syncFs.existsSync(
//@ts-ignore
path.resolve(`${plugin.pathToPlugin}/strapi-admin.js`)
);
if (!isLocalPluginWithLegacyAdminFile) {
//@ts-ignore
let pathToPlugin = plugin.pathToPlugin;
if (process.platform === 'win32') {
pathToPlugin = pathToPlugin.split(path.sep).join(path.posix.sep);
}
const isModuleWithFE = require.resolve(`${pathToPlugin}/strapi-admin`);
return isModuleWithFE;
}
return isLocalPluginWithLegacyAdminFile;
} catch (err) {
if (isError(err) && 'code' in err && err.code === 'MODULE_NOT_FOUND') {
/**
* the plugin does not contain FE code, so we
* don't want to import it anyway
*/
return false;
}
throw err;
}
};
export { createBuildContext };
export type { BuildContext, CreateBuildContextArgs };

View File

@ -8,16 +8,12 @@ import { DefaultDocument as Document } from '../../admin/src/components/DefaultD
import type { BuildContext } from './createBuildContext';
interface EntryModuleArgs {
plugins: BuildContext['plugins'];
}
const getEntryModule = ({ plugins }: EntryModuleArgs): string => {
const pluginsObject = plugins
const getEntryModule = (ctx: BuildContext): string => {
const pluginsObject = ctx.plugins
.map(({ name, importName }) => `'${name}': ${importName}`)
.join(',\n');
const pluginsImport = plugins
const pluginsImport = ctx.plugins
.map(({ importName, path }) => `import ${importName} from '${path}';`)
.join('\n');
@ -29,9 +25,19 @@ const getEntryModule = ({ plugins }: EntryModuleArgs): string => {
${pluginsImport}
import { renderAdmin } from "@strapi/strapi/admin"
${
ctx.customisations?.path
? `import customisations from '${path.relative(
ctx.runtimeDir,
ctx.customisations.path
)}'`
: ''
}
renderAdmin(
document.getElementById("strapi"),
{
${ctx.customisations?.path ? 'customisations,' : ''}
plugins: {
${pluginsObject}
}
@ -88,7 +94,7 @@ const writeStaticClientFiles = async (ctx: BuildContext) => {
ctx.logger.debug('Wrote the index.html file');
await fs.writeFile(
path.join(ctx.runtimeDir, 'app.js'),
format(getEntryModule({ plugins: ctx.plugins }), {
format(getEntryModule(ctx), {
parser: 'babel',
})
);

View File

@ -218,8 +218,19 @@ const mergeConfigWithUserConfig = async (config: Configuration, ctx: BuildContex
const userConfig = await getUserConfig(ctx);
if (userConfig) {
const webpack = await import('webpack');
return userConfig(config, webpack);
if (typeof userConfig === 'function') {
const webpack = await import('webpack');
return userConfig(config, webpack);
} else {
ctx.logger.warn(
`You've exported something other than a function from ${path.join(
ctx.appDir,
'src',
'admin',
'webpack.config'
)}, this will ignored.`
);
}
}
return config;

View File

@ -5,10 +5,14 @@ import { createRoot } from 'react-dom/client';
import { StrapiApp, StrapiAppConstructorArgs } from './StrapiApp';
interface RenderAdminArgs {
customisations: StrapiAppConstructorArgs['adminConfig'];
plugins: StrapiAppConstructorArgs['appPlugins'];
}
const renderAdmin = async (mountNode: HTMLElement | null, { plugins }: RenderAdminArgs) => {
const renderAdmin = async (
mountNode: HTMLElement | null,
{ plugins, customisations }: RenderAdminArgs
) => {
if (!mountNode) {
throw new Error('[@strapi/admin]: Could not find the root element to mount the admin app');
}
@ -73,7 +77,7 @@ const renderAdmin = async (mountNode: HTMLElement | null, { plugins }: RenderAdm
}
const app = new StrapiApp({
adminConfig: {},
adminConfig: customisations,
appPlugins: plugins,
});

View File

@ -5,8 +5,9 @@ import email from '@strapi/plugin-email/strapi-admin';
// @ts-expect-error No types, yet.
import upload from '@strapi/plugin-upload/strapi-admin';
const render = (mountNode: HTMLElement | null, { plugins }: RenderAdminArgs) => {
const render = (mountNode: HTMLElement | null, { plugins, ...restArgs }: RenderAdminArgs) => {
return renderAdmin(mountNode, {
...restArgs,
plugins: {
'content-type-builder': contentTypeBuilder,
// @ts-expect-error TODO: fix this