mirror of
https://github.com/strapi/strapi.git
synced 2025-12-24 13:43:41 +00:00
* 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:
parent
da634b0951
commit
2f026ea9b3
@ -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'];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
const config = {
|
||||
locales: ['fr'],
|
||||
};
|
||||
const bootstrap = () => {};
|
||||
|
||||
export default {
|
||||
config,
|
||||
bootstrap,
|
||||
};
|
||||
11
examples/getstarted/src/admin/app.js
Normal file
11
examples/getstarted/src/admin/app.js
Normal file
@ -0,0 +1,11 @@
|
||||
const config = {
|
||||
locales: ['it', 'es', 'en'],
|
||||
};
|
||||
const bootstrap = () => {
|
||||
console.log('I AM BOOTSTRAPPED');
|
||||
};
|
||||
|
||||
export default {
|
||||
config,
|
||||
bootstrap,
|
||||
};
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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',
|
||||
})
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user