Skip to content

fix: init admin typings and scaffold regressions#107

Merged
innerdvations merged 8 commits intostrapi:mainfrom
Link2Twenty:register-types
May 5, 2026
Merged

fix: init admin typings and scaffold regressions#107
innerdvations merged 8 commits intostrapi:mainfrom
Link2Twenty:register-types

Conversation

@Link2Twenty
Copy link
Copy Markdown
Contributor

@Link2Twenty Link2Twenty commented Jan 29, 2026

What does it do?

Remove the any from register and update the function so it matches the existing type.
One of the changes is to make App a default export, this is simply to make the linter happy and changing the type might be the correct way to go.

Why is it needed?

Types are good

How to test it?

Load the types in and see the linter is happy.

Related issue(s)/PR(s)

N/A


Edit:
I noticed there was already a type the describes this whole export so I've set it up to use that instead.
While I was in there I realised the mapping of translation keys wasn't happening so I've added that in.
Let me know if yo need that to be a send PR.

Signed-off-by: Andrew Bone <AndrewB05@gmail.com>
addMenuLink['Component'] expects 

```tsx
() => Promise<{
  default: React.ComponentType;
}>;
```

Meaning the component must be the default export

Signed-off-by: Andrew Bone <AndrewB05@gmail.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 29, 2026

🦋 Changeset detected

Latest commit: ebcae7a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@strapi/sdk-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dosubot
Copy link
Copy Markdown

dosubot Bot commented Jan 29, 2026

Documentation Updates

1 document(s) were updated by changes in this PR:

admin-permissions-for-plugins
View Changes
@@ -152,11 +152,7 @@
         id: `${PLUGIN_ID}.plugin.name`,
         defaultMessage: PLUGIN_ID,
       },
-      Component: async () => {
-        const { App } = await import('./pages/App');
-
-        return App;
-      },
+      Component: () => import('./pages/App'),
       permissions: [
         pluginPermissions.accessOverview[0],
       ],

How did I do? Any feedback?  Join Discord

- Use the `StrapiAppPlugin` type rather than remaking it
- fix registerTrads so it actually transforms languages. 

Signed-off-by: Andrew Bone <AndrewB05@gmail.com>
@sonarqubecloud
Copy link
Copy Markdown

@innerdvations
Copy link
Copy Markdown
Collaborator

Thanks for this! I pushed a change that:

  • adds regression tests around generated admin templates
  • fixes a TS template formatting issue that was producing malformed generated output
  • keeps the JS/TS registerTrads behavior aligned
  • reverts the App default export change for now to stay consistent with current Strapi docs/examples -- I agree it's probably slightly better but I don't want to add unneccessary friction

If this looks good to you still, I’m happy to merge!

@innerdvations innerdvations changed the title Fix types in Register Improve init admin typings and scaffold regressions Apr 23, 2026
@innerdvations innerdvations changed the title Improve init admin typings and scaffold regressions fix: init admin typings and scaffold regressions Apr 23, 2026
@Link2Twenty
Copy link
Copy Markdown
Contributor Author

The reason I changed the export to default was because the addMenuLink type expects it to be a default export. As it stands now it will throw and type error.

addMenuLink: (link: Omit<MenuItem, 'Component'> & {
  Component: () => Promise<{
    default: React.ComponentType;
  }>;
}) => void;

We could fake the import in admin/src/index.ts by wrapping it in an object but it feels a little hoop jumpy

Component: async () => {
  const { App } = await import('./pages/App');

  return {default: App};
},

But other than that everything looks great.

@jhoward1994
Copy link
Copy Markdown
Contributor

Thanks for this!

I think this has highlighted a discrepancy in our docs more than anything else (see PR mentioned above). We seem to be documenting a deprecated import syntax. wdyt @innerdvations ?

If we go ahead with those docs changes, I think we can move back to something closer to the first diff you had. Using a diff like the one below.

I've marked this as dont merge for now so we can discuss the docs changes first.

diff --git a/src/__tests__/unit/init-file-generator.test.ts b/src/__tests__/unit/init-file-generator.test.ts
index a754e08..06ddb89 100644
--- a/src/__tests__/unit/init-file-generator.test.ts
+++ b/src/__tests__/unit/init-file-generator.test.ts
@@ -186,6 +186,12 @@ describe('init file generation', () => {
     const adminIndexFile = getFile(files, 'admin/src/index.ts');
     expect(adminIndexFile.contents).toContain("const plugin: StrapiApp['appPlugins'][string] = {");
     expect(adminIndexFile.contents).toContain('};\n\nexport default plugin;');
+    expect(adminIndexFile.contents).toContain("Component: () => import('./pages/App'),");
+    expect(adminIndexFile.contents).not.toContain('Component: async () =>');
+
+    const appPageFile = getFile(files, 'admin/src/pages/App.tsx');
+    expect(appPageFile.contents).toContain('export default App;');
+    expect(appPageFile.contents).not.toContain('export { App };');
   });
 
   it('should generate JS admin template with docs-aligned App loader and translated trad keys', async () => {
@@ -212,12 +218,13 @@ describe('init file generation', () => {
     );
 
     const adminIndexFile = getFile(files, 'admin/src/index.js');
-    expect(adminIndexFile.contents).toContain("const { App } = await import('./pages/App');");
-    expect(adminIndexFile.contents).not.toContain("const App = await import('./pages/App');");
+    expect(adminIndexFile.contents).toContain("Component: () => import('./pages/App'),");
+    expect(adminIndexFile.contents).not.toContain('Component: async () =>');
+    expect(adminIndexFile.contents).not.toContain("const { App } = await import('./pages/App');");
     expect(adminIndexFile.contents).toContain('newData[getTranslation(key)] = data[key];');
 
     const appPageFile = getFile(files, 'admin/src/pages/App.jsx');
-    expect(appPageFile.contents).toContain('export { App };');
-    expect(appPageFile.contents).not.toContain('export default App;');
+    expect(appPageFile.contents).toContain('export default App;');
+    expect(appPageFile.contents).not.toContain('export { App };');
   });
 });
diff --git a/src/cli/commands/plugin/init/files/admin.ts b/src/cli/commands/plugin/init/files/admin.ts
index 8f17fc2..7a87eb3 100644
--- a/src/cli/commands/plugin/init/files/admin.ts
+++ b/src/cli/commands/plugin/init/files/admin.ts
@@ -25,7 +25,7 @@ const App = () => {
   );
 };
 
-export { App };
+export default App;
 `;
 
 const HOMEPAGE_CODE = outdent`
@@ -56,56 +56,51 @@ const TYPESCRIPT: TemplateFile[] = [
         import { Initializer } from './components/Initializer';
         import { PluginIcon } from './components/PluginIcon';
 
-        // Types
         import type { StrapiApp } from '@strapi/strapi/admin';
-        
+
         const plugin: StrapiApp['appPlugins'][string] = {
-        register(app) {
-          app.addMenuLink({
-            to: \`plugins/\${PLUGIN_ID}\`,
-            icon: PluginIcon,
-            intlLabel: {
-              id: \`\${PLUGIN_ID}.plugin.name\`,
-              defaultMessage: PLUGIN_ID,
-            },
-            Component: async () => {
-              const { App } = await import('./pages/App');
+          register(app) {
+            app.addMenuLink({
+              to: \`plugins/\${PLUGIN_ID}\`,
+              icon: PluginIcon,
+              intlLabel: {
+                id: \`\${PLUGIN_ID}.plugin.name\`,
+                defaultMessage: PLUGIN_ID,
+              },
+              Component: () => import('./pages/App'),
+              permissions: [],
+            });
 
-              return App;
-            },
-            permissions: [],
-          });
+            app.registerPlugin({
+              id: PLUGIN_ID,
+              initializer: Initializer,
+              isReady: false,
+              name: PLUGIN_ID,
+            });
+          },
 
-          app.registerPlugin({
-            id: PLUGIN_ID,
-            initializer: Initializer,
-            isReady: false,
-            name: PLUGIN_ID,
-          });
-        },
+          async registerTrads({ locales }) {
+            return Promise.all(
+              locales.map(async (locale) => {
+                try {
+                  const { default: data } = (await import(\`./translations/\${locale}.json\`)) as {
+                    default: Record<string, string>;
+                  };
 
-        registerTrads({ locales }) {
-          return Promise.all(
-            locales.map(async (locale) => {
-              try {
-                const { default: data } = (await import(\`./translations/\${locale}.json\`)) as {
-                  default: Record<string, string>;
-                };
+                  const newData: Record<string, string> = {};
+                  const keys = Object.keys(data);
 
-                const newData: Record<string, string> = {};
-                const keys = Object.keys(data);
+                  for (const key of keys) {
+                    newData[getTranslation(key)] = data[key];
+                  }
 
-                for (const key of keys) {
-                  newData[getTranslation(key)] = data[key];
+                  return { data: newData, locale };
+                } catch {
+                  return { data: {}, locale };
                 }
-
-                return { data: newData, locale };
-              } catch {
-                return { data: {}, locale };
-              }
-            })
-          );
-        },
+              })
+            );
+          },
         };
 
         export default plugin;
@@ -193,11 +188,8 @@ const JAVASCRIPT: TemplateFile[] = [
                     id: \`\${PLUGIN_ID}.plugin.name\`,
                     defaultMessage: PLUGIN_ID,
                   },
-                  Component: async () => {
-                    const { App } = await import('./pages/App');
-            
-                    return App;
-                  },
+                  Component: () => import('./pages/App'),
+                  permissions: [],
                 });
             
                 app.registerPlugin({


@jhoward1994 jhoward1994 self-assigned this May 1, 2026
@innerdvations
Copy link
Copy Markdown
Collaborator

Yeah, let's just move forward with this improvement. I just reverted my change back to @Link2Twenty 's default export, and we'll consider it a documentation problem. Does that seem right to you too @jhoward1994 ?

@jhoward1994
Copy link
Copy Markdown
Contributor

Thanks @innerdvations yeah that seems right to me

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
49.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@innerdvations innerdvations merged commit bd4d5a4 into strapi:main May 5, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community flag: don't merge This PR should not be merged at the moment pr: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants