fix(admin): clean up lazy component registration warnings (#25015)

* fix(admin): don't warn when Component is missing in addMenuLink/addSettingsLink

* fix: add some missing StrapiApp types

* fix(admin): clean up lazy component registrations

---------

Co-authored-by: Bassel Kanso <basselkanso82@gmail.com>
This commit is contained in:
Andrei L
2026-04-30 16:27:48 +03:00
committed by GitHub
parent fa6299100d
commit 90623baa6f
11 changed files with 121 additions and 73 deletions
@@ -157,15 +157,14 @@ class Router {
);
if (
!link.Component ||
(link.Component &&
typeof link.Component === 'function' &&
// @ts-expect-error shh
link.Component[Symbol.toStringTag] === 'AsyncFunction')
link.Component &&
typeof link.Component === 'function' &&
// @ts-expect-error shh
link.Component[Symbol.toStringTag] === 'AsyncFunction'
) {
console.warn(
`
[${link.intlLabel.defaultMessage}]: [deprecated] addMenuLink() was called with an async Component from the plugin "${link.intlLabel.defaultMessage}". This will be removed in the future. Please use: \`Component: () => import(path)\` ensuring you return a default export instead.
[${link.intlLabel.defaultMessage}]: [deprecated] addMenuLink() was called with an async Component from the plugin "${link.intlLabel.defaultMessage}". Component loaders should return a dynamic import with a default export shape, e.g. \`Component: () => import(path).then((mod) => ({ default: mod.Component }))\`. Async wrapper functions will stop being supported in a future version.
`.trim()
);
}
@@ -280,15 +279,14 @@ class Router {
);
if (
!link.Component ||
(link.Component &&
typeof link.Component === 'function' &&
// @ts-expect-error shh
link.Component[Symbol.toStringTag] === 'AsyncFunction')
link.Component &&
typeof link.Component === 'function' &&
// @ts-expect-error shh
link.Component[Symbol.toStringTag] === 'AsyncFunction'
) {
console.warn(
`
[${link.intlLabel.defaultMessage}]: [deprecated] addSettingsLink() was called with an async Component from the plugin "${link.intlLabel.defaultMessage}". This will be removed in the future. Please use: \`Component: () => import(path)\` ensuring you return a default export instead.
[${link.intlLabel.defaultMessage}]: [deprecated] addSettingsLink() was called with an async Component from the plugin "${link.intlLabel.defaultMessage}". Component loaders should return a dynamic import with a default export shape, e.g. \`Component: () => import(path).then((mod) => ({ default: mod.Component }))\`. Async wrapper functions will stop being supported in a future version.
`.trim()
);
}
@@ -216,7 +216,7 @@ describe('ADMIN | new StrapiApp', () => {
expect(consoleSpy.mock.calls).toHaveLength(1);
expect(consoleSpy.mock.calls[0][0]).toBe(
'[bar]: [deprecated] addSettingsLink() was called with an async Component from the plugin "bar". This will be removed in the future. Please use: `Component: () => import(path)` ensuring you return a default export instead.'
'[bar]: [deprecated] addSettingsLink() was called with an async Component from the plugin "bar". Component loaders should return a dynamic import with a default export shape, e.g. `Component: () => import(path).then((mod) => ({ default: mod.Component }))`. Async wrapper functions will stop being supported in a future version.'
);
console.warn = originalWarn;
@@ -467,6 +467,28 @@ describe('ADMIN | new StrapiApp', () => {
expect(typeof app.router.menu[0].icon).toBe('function');
});
it('addMenuLink should allow a menu-only link', () => {
const app = new StrapiApp();
const link = {
to: 'content-manager',
intlLabel: { id: 'content-manager.plugin.name', defaultMessage: 'Content Manager' },
permissions: [],
icon: jest.fn(),
};
app.addMenuLink(link as Parameters<typeof app.addMenuLink>[0]);
expect(app.router.menu[0]).toEqual({
icon: expect.any(Function),
intlLabel: {
defaultMessage: 'Content Manager',
id: 'content-manager.plugin.name',
},
permissions: [],
to: 'content-manager',
});
});
it('should warn if a user supplies an absolute link', () => {
const originalWarn = console.warn;
const consoleSpy = jest.fn();
@@ -513,7 +535,7 @@ describe('ADMIN | new StrapiApp', () => {
expect(consoleSpy.mock.calls).toHaveLength(1);
expect(consoleSpy.mock.calls[0][0]).toBe(
'[bar]: [deprecated] addMenuLink() was called with an async Component from the plugin "bar". This will be removed in the future. Please use: `Component: () => import(path)` ensuring you return a default export instead.'
'[bar]: [deprecated] addMenuLink() was called with an async Component from the plugin "bar". Component loaders should return a dynamic import with a default export shape, e.g. `Component: () => import(path).then((mod) => ({ default: mod.Component }))`. Async wrapper functions will stop being supported in a future version.'
);
console.warn = originalWarn;
@@ -8,7 +8,7 @@ import { previewAdmin } from './preview';
import { routes } from './router';
import { prefixPluginTranslations } from './utils/translations';
import type { WidgetArgs } from '@strapi/admin/strapi-admin';
import type { StrapiApp, WidgetArgs } from '@strapi/admin/strapi-admin';
// NOTE: we have to preload it to ensure chunks will have it available as global
import 'prismjs';
@@ -106,7 +106,7 @@ export default {
return [lastEditedWidget, lastPublishedWidget, ...widgets, entriesWidget];
});
},
bootstrap(app: any) {
bootstrap(app: StrapiApp) {
if (typeof historyAdmin.bootstrap === 'function') {
historyAdmin.bootstrap(app);
}
@@ -69,9 +69,10 @@ const admin: Plugin.Config.AdminInput = {
},
licenseOnly: true,
permissions: [],
async Component() {
const { ProtectedReleasesSettingsPage } = await import('./pages/ReleasesSettingsPage');
return { default: ProtectedReleasesSettingsPage };
Component() {
return import('./pages/ReleasesSettingsPage').then((mod) => ({
default: mod.ProtectedReleasesSettingsPage,
}));
},
});
@@ -119,15 +120,16 @@ const admin: Plugin.Config.AdminInput = {
) {
app.addSettingsLink('global', {
id: pluginId,
to: '/plugins/purchase-content-releases',
to: 'purchase-content-releases',
intlLabel: {
id: `${pluginId}.plugin.name`,
defaultMessage: 'Releases',
},
permissions: [],
async Component() {
const { PurchaseContentReleases } = await import('./pages/PurchaseContentReleases');
return { default: PurchaseContentReleases };
Component() {
return import('./pages/PurchaseContentReleases').then((mod) => ({
default: mod.PurchaseContentReleases,
}));
},
licenseOnly: true,
});
+15 -16
View File
@@ -1,33 +1,32 @@
import { PERMISSIONS } from './constants';
import { prefixPluginTranslations } from './utils/prefixPluginTranslations';
import type { StrapiApp } from '@strapi/admin/strapi-admin';
import type { Plugin } from '@strapi/types';
const admin: Plugin.Config.AdminInput = {
// TODO typing app in strapi/types as every plugin needs it
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register(app: any) {
register(app: StrapiApp) {
// Create the email settings section
app.createSettingSection(
app.addSettingsLink(
{
id: 'email',
intlLabel: { id: 'email.SettingsNav.section-label', defaultMessage: 'Email Plugin' },
},
[
{
intlLabel: {
id: 'email.Settings.email.plugin.title',
defaultMessage: 'Settings',
},
id: 'settings',
to: 'email',
Component: () =>
import('./pages/Settings').then((mod) => ({
default: mod.ProtectedSettingsPage,
})),
permissions: PERMISSIONS.settings,
{
intlLabel: {
id: 'email.Settings.email.plugin.title',
defaultMessage: 'Settings',
},
]
id: 'settings',
to: 'email',
Component: () =>
import('./pages/Settings').then((mod) => ({
default: mod.ProtectedSettingsPage,
})),
permissions: PERMISSIONS.settings,
}
);
app.registerPlugin({
id: 'email',
@@ -32,9 +32,10 @@ const admin: Plugin.Config.AdminInput = {
},
licenseOnly: true,
permissions: [],
async Component() {
const { Router } = await import('./router');
return { default: Router };
Component() {
return import('./router').then((mod) => ({
default: mod.Router,
}));
},
});
@@ -66,9 +67,10 @@ const admin: Plugin.Config.AdminInput = {
},
licenseOnly: true,
permissions: [],
async Component() {
const { PurchaseReviewWorkflows } = await import('./routes/purchase-review-workflows');
return { default: PurchaseReviewWorkflows };
Component() {
return import('./routes/purchase-review-workflows').then((mod) => ({
default: mod.PurchaseReviewWorkflows,
}));
},
});
}
+4 -3
View File
@@ -67,9 +67,10 @@ const admin: Plugin.Config.AdminInput = {
id: getTrad('plugin.name'),
defaultMessage: 'Media Library',
},
async Component() {
const { ProtectedSettingsPage } = await import('./pages/SettingsPage/SettingsPage');
return { default: ProtectedSettingsPage };
Component() {
return import('./pages/SettingsPage/SettingsPage').then((mod) => ({
default: mod.ProtectedSettingsPage,
}));
},
permissions: PERMISSIONS.settings,
});
+5 -6
View File
@@ -6,11 +6,13 @@ import { Initializer } from './components/Initializer';
import { pluginId } from './pluginId';
import { prefixPluginTranslations } from './utils/prefixPluginTranslations';
import type { StrapiApp } from '@strapi/admin/strapi-admin';
const pluginName = 'Deploy';
// eslint-disable-next-line import/no-default-export
export default {
register(app: any) {
register(app: StrapiApp) {
const { backendURL } = window.strapi;
// Only add the plugin menu link and registering it if the project is on development (localhost).
@@ -22,11 +24,8 @@ export default {
id: `${pluginId}.Plugin.name`,
defaultMessage: pluginName,
},
Component: async () => {
const { App } = await import('./pages/App');
return App;
},
Component: () => import('./pages/App').then((mod) => ({ default: mod.App })),
permissions: [],
});
const plugin = {
id: pluginId,
@@ -4,9 +4,11 @@ import { PERMISSIONS } from './constants';
import { pluginId } from './pluginId';
import { prefixPluginTranslations } from './utils/prefixPluginTranslations';
import type { StrapiApp } from '@strapi/admin/strapi-admin';
// eslint-disable-next-line import/no-default-export
export default {
register(app: any) {
register(app: StrapiApp) {
app.addMenuLink({
to: `plugins/${pluginId}`,
icon: Information,
@@ -15,10 +17,7 @@ export default {
defaultMessage: 'Documentation',
},
permissions: PERMISSIONS.main,
Component: async () => {
const { App } = await import('./pages/App');
return App;
},
Component: () => import('./pages/App').then((mod) => ({ default: mod.App })),
position: 9,
});
@@ -27,7 +26,7 @@ export default {
name: pluginId,
});
},
bootstrap(app: any) {
bootstrap(app: StrapiApp) {
app.addSettingsLink('global', {
intlLabel: {
id: `${pluginId}.plugin.name`,
@@ -35,10 +34,7 @@ export default {
},
id: 'documentation',
to: pluginId,
Component: async () => {
const { SettingsPage } = await import('./pages/Settings');
return SettingsPage;
},
Component: () => import('./pages/Settings').then((mod) => ({ default: mod.SettingsPage })),
permissions: PERMISSIONS.main,
});
},
+35 -8
View File
@@ -30,11 +30,37 @@ import { getTranslation } from './utils/getTranslation';
import { prefixPluginTranslations } from './utils/prefixPluginTranslations';
import { mutateCTBContentTypeSchema } from './utils/schemas';
import type { DocumentActionComponent } from '@strapi/content-manager/strapi-admin';
import type { StrapiApp } from '@strapi/admin/strapi-admin';
import type {
ContentManagerPlugin,
DocumentActionComponent,
HeaderActionComponent,
} from '@strapi/content-manager/strapi-admin';
type ContentTypeBuilderFormsAPI = {
addContentTypeSchemaMutation: (mutation: typeof mutateCTBContentTypeSchema) => void;
components: {
add: (component: { id: string; component: typeof CheckboxConfirmation }) => void;
};
extendContentType: (extension: {
validator: () => Record<string, unknown>;
form: {
advanced: () => Array<Record<string, unknown>>;
};
}) => void;
extendFields: (
fields: typeof LOCALIZED_FIELDS,
extension: {
form: {
advanced: (args: any) => Array<Record<string, unknown>>;
};
}
) => void;
};
// eslint-disable-next-line import/no-default-export
export default {
register(app: any) {
register(app: StrapiApp) {
app.addMiddlewares([extendCTBAttributeInitialDataMiddleware, extendCTBInitialDataMiddleware]);
app.addMiddlewares([() => i18nApi.middleware]);
app.addReducers({
@@ -46,7 +72,7 @@ export default {
name: pluginId,
});
},
bootstrap(app: any) {
bootstrap(app: StrapiApp) {
// // Hook that adds a column into the CM's LV table
app.registerHook('Admin/CM/pages/ListView/inject-column-in-table', addColumnToTableHook);
app.registerHook('Admin/CM/pages/EditView/mutate-edit-view-layout', mutateEditViewHook);
@@ -70,19 +96,20 @@ export default {
});
const contentManager = app.getPlugin('content-manager');
const contentManagerApis = contentManager.apis as ContentManagerPlugin['config']['apis'];
contentManager.apis.addDocumentHeaderAction([
contentManagerApis.addDocumentHeaderAction([
AITranslationStatusAction,
LocalePickerAction,
FillFromAnotherLocaleAction,
]);
contentManager.apis.addDocumentAction((actions: DocumentActionComponent[]) => {
] as HeaderActionComponent[]);
contentManagerApis.addDocumentAction((actions: DocumentActionComponent[]) => {
const indexOfDeleteAction = actions.findIndex((action) => action.type === 'delete');
actions.splice(indexOfDeleteAction, 0, DeleteLocaleAction);
return actions;
});
contentManager.apis.addDocumentAction((actions: DocumentActionComponent[]) => {
contentManagerApis.addDocumentAction((actions: DocumentActionComponent[]) => {
// When enabled the bulk locale publish action should be the first action
// in 'More Document Actions' and therefore the third action in the array
actions.splice(2, 0, BulkLocalePublishAction);
@@ -113,7 +140,7 @@ export default {
const ctbPlugin = app.getPlugin('content-type-builder');
if (ctbPlugin) {
const ctbFormsAPI = ctbPlugin.apis.forms;
const ctbFormsAPI = ctbPlugin.apis.forms as ContentTypeBuilderFormsAPI;
ctbFormsAPI.addContentTypeSchemaMutation(mutateCTBContentTypeSchema);
ctbFormsAPI.components.add({ id: 'checkboxConfirmation', component: CheckboxConfirmation });
+3 -1
View File
@@ -3,11 +3,13 @@ import pluginPkg from '../../package.json';
import { pluginId } from './pluginId';
import { prefixPluginTranslations } from './utils/prefixPluginTranslations';
import type { StrapiApp } from '@strapi/admin/strapi-admin';
const name = pluginPkg.strapi.name;
// eslint-disable-next-line import/no-default-export
export default {
register(app: any) {
register(app: StrapiApp) {
app.registerPlugin({
id: pluginId,
name,