Repository navigation
feat: Add plugin catalog to AdminCP System page - #880
Conversation
Generate plugin metadata (title, description, version, icon) from each
plugin's package.json during `vitnode build` and `vitnode dev`, register it
with buildPlugin/buildApiPlugin, and list every configured plugin once in
AdminCP → System → Plugins with the core version shown separately.
- src/plugin-metadata.gen.ts and src/plugin-icon.gen.ts per plugin, icons
validated at build time and inlined as data URLs
- web build writes src/plugin-inventory.gen.ts, passed to the API as
buildApiConfig({ frontendPlugins }) for frontend-only plugins and
frontend/API version mismatch warnings
- GET /api/@vitnode/core/admin/plugins guarded by plugins.can_view
- Blog, Forum, Example and the plugin scaffold adopt the metadata
- docs: dev/plugins/catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqJtNifFvW63sCoYVKgJ7m
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
React Doctor found 3 new issues in 2 files · 3 warnings · score 67 / 100 (Needs work) · 0 fixed · vs 3 warnings
Reviewed by React Doctor for commit |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fb4bbb344
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| description: optionalText(manifest.description, "description", source), | ||
| id, | ||
| title: title ?? id, |
There was a problem hiding this comment.
Return translation keys for plugin catalog copy
When the AdminCP uses a non-English locale, these literal package.json title and description values are returned by GET /admin/plugins and rendered directly by PluginsContent, so plugin copy remains in the manifest language. Store locale message keys in the metadata/API response and resolve them with use-intl in the web UI, as required for plugin-registered titles and descriptions.
AGENTS.md reference: AGENTS.md:L62-L62
Useful? React with 👍 / 👎.
| } catch (error) { | ||
| onError(error); | ||
| } |
There was a problem hiding this comment.
Watch a newly declared icon after validation fails
If package.json is changed to reference an icon path before that file is created and the poller observes the manifest first, writePluginMetadata() throws before returning the new input list, and this catch leaves only the manifest and any old icon watched. Creating the new icon afterward therefore emits no event, so metadata remains stale until the manifest is touched again or vitnode dev is restarted; retain/watch the newly declared candidate path even on this missing-file error.
Useful? React with 👍 / 👎.
…ata.ts with Lucide icons package.json keeps only its standard fields. A plugin's title and Lucide icon name live in a hand-written src/plugin-metadata.ts, which the generated src/plugin-metadata.gen.ts imports next to the name, version and description read from package.json. - icons are Lucide names, validated at registration and returned by the API, so API-only plugins show their own icon too - drop plugin-icon.gen.ts, SVG assets, data URLs and icon file checks - DynamicIcon gains a `missing` fallback for unknown icon names - scaffold writes src/plugin-metadata.ts; docs and tests updated Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqJtNifFvW63sCoYVKgJ7m
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a290d82696
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const [searchParams, setSearchParams] = React.useState( | ||
| () => new URLSearchParams(), | ||
| ); |
There was a problem hiding this comment.
Synchronize the catalog table with the route query string
When the catalog has enough plugins for multiple pages, this state always starts empty and navigation only updates component-local state. Consequently, the pagination links' ?page=... URLs do not reproduce the displayed page when opened in a new tab or refreshed, and browser Back/Forward cannot restore pagination or search state. Initialize from and navigate through the route's actual search parameters rather than maintaining a disconnected URLSearchParams instance.
Useful? React with 👍 / 👎.
… for improved script management
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5095042c02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <SecuritySection /> | ||
| <DevelopersSection /> | ||
| <PricingSection /> | ||
| <FaqSection /> | ||
| <FaqSection /> */} |
There was a problem hiding this comment.
Restore the omitted homepage sections
This comments out nearly the entire production homepage—outcomes, features, plugins, showcase, community, security, pricing, FAQ, and the final CTA—leaving only the hero, notice, and integrations section. Because this plugin-catalog change does not replace or relocate those sections, every visit to the home route loses the site's primary marketing and navigation content; restore the sections or lazy-load them rather than removing them from the render tree.
Useful? React with 👍 / 👎.
| const { container } = renderPlugins({ plugins: [plugin({ icon: null })] }); | ||
|
|
||
| expect(screen.getByText("Blog")).toBeTruthy(); | ||
| expect(container.querySelector("svg.lucide-puzzle")).not.toBeNull(); |
There was a problem hiding this comment.
Test the default icon without relying on its CSS class
This assertion identifies the fallback through Lucide's decorative lucide-puzzle class, so a harmless icon-library class-name change can fail the test even when the same default icon is rendered. Assert observable DOM output or an accessible/click behavior instead of a CSS selector, as required for UI component tests.
AGENTS.md reference: AGENTS.md:L109-L110
Useful? React with 👍 / 👎.
| write("package.json", JSON.stringify({ ...BLOG, version: "2.0.0" })); | ||
| await vi.waitFor( | ||
| () => { | ||
| expect(read(PLUGIN_METADATA_PATH)).toContain('"2.0.0"'); |
There was a problem hiding this comment.
Replace generated-source substring checks with behavior checks
This test reads the generated TypeScript module and searches its source for a quoted version literal (and repeats the same pattern in the recovery case), coupling the regression test to formatting rather than the exported metadata behavior. Import/evaluate the generated module and assert its exported version so generator refactors do not break the test while behavior remains correct.
AGENTS.md reference: AGENTS.md:L113-L119
Useful? React with 👍 / 👎.
| isDataScript(tag) && typeof tag.children === "string" ? ( | ||
| <script | ||
| {...tag.attrs} | ||
| // oxlint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml | ||
| dangerouslySetInnerHTML={{ __html: tag.children }} | ||
| suppressHydrationWarning |
There was a problem hiding this comment.
Forward the router nonce to data scripts
When a host configures router.options.ssr.nonce for a nonce-based CSP, this production-only branch receives the nonce but drops it while manually rendering data scripts such as the JSON-LD emitted by apps/web/src/site/marketing/metadata.ts. Inline script elements must match the policy's nonce, so these router-managed tags can be rejected under a strict script-src; pass the nonce through just as the Asset branch does. CSP Level 3
Useful? React with 👍 / 👎.
- Introduced `write-plugin-manifest.ts` to handle the generation of plugin manifests from `package.json` and `src/plugin-info.ts`. - Implemented `bindApiPlugin` and `bindContentPlugin` to facilitate the binding of API and content modules to plugins. - Created tests for manifest generation to ensure correctness and robustness. - Added manifest and plugin info files for the core and example plugins, enhancing plugin structure and metadata. - Updated existing plugins (blog, example, forum) to utilize the new manifest generation and binding features.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6da4ed198
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for (const src of entries) { | ||
| const script = document.createElement("script"); | ||
| script.type = "module"; | ||
| script.src = src; | ||
| document.body.append(script); |
There was a problem hiding this comment.
Preserve the nonce on deferred module scripts
In production, when a host uses router.options.ssr.nonce with a nonce-only script-src policy, the router-generated module entry initially carries that nonce, but this code reduces it to its src and creates a new <script> without a nonce. Unless the module URL is separately allowed or the policy uses strict-dynamic, the browser blocks the client entry and the application never hydrates; propagate the router nonce to each dynamically created module script.
Useful? React with 👍 / 👎.
| export const LogoVitNodeBrand = () => ( | ||
| <> | ||
| <LogoVitNode | ||
| className="hidden w-34 sm:block" | ||
| idPrefix="vitnode-brand-wide" | ||
| /> | ||
| <LogoVitNode | ||
| className="size-8 sm:hidden" | ||
| idPrefix="vitnode-brand-compact" | ||
| small | ||
| /> | ||
| </> | ||
| <LogoVitNode className="w-34" idPrefix="vitnode-brand" /> | ||
| ); |
There was a problem hiding this comment.
Keep the compact logo on small screens
On narrow mobile viewports, this always renders the 136px wordmark inside a non-shrinking header link alongside the non-shrinking search, notifications, and user controls. The removed responsive pair used the 32px mark below sm; without it the header can overflow or collide at small widths, so retain the compact mobile variant and show the wordmark only from sm upward.
AGENTS.md reference: AGENTS.md:L65-L67
Useful? React with 👍 / 👎.
| export const PluginsContent = ({ data }: PluginsContentProps) => { | ||
| const t = useTranslations("admin.system.plugins"); |
There was a problem hiding this comment.
Render the catalog metadata returned by the API
The component only consumes data.plugins, leaving both data.core.version and data.frontendInventory unused. Consequently the core version is absent from the catalog, and in split/API-only deployments where frontendInventory is false the page silently presents an API-only list as the complete installed-plugin list; render the core version and the incomplete-inventory notice before the table.
Useful? React with 👍 / 👎.
…ins-catalog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
||
| const watch = (next: readonly string[]) => { | ||
| for (const file of watched) { | ||
| if (!next.includes(file)) unwatchFile(file, listener); |
There was a problem hiding this comment.
React Doctor · react-doctor/js-set-map-lookups (warning)
This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.
Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time
| if (!next.includes(file)) unwatchFile(file, listener); | ||
| } | ||
| for (const file of next) { | ||
| if (!watched.includes(file)) { |
There was a problem hiding this comment.
React Doctor · react-doctor/js-set-map-lookups (warning)
This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.
Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time
Description
What?
Adds a new AdminCP → System → Plugins page that displays all installed plugins with their metadata (title, description, version, icon) read from each plugin's
package.jsonat build time.Key additions:
vitnode buildandvitnode devnow generatesrc/plugin-metadata.gen.tsandsrc/plugin-icon.gen.tsfrom each plugin'spackage.json, reading thevitnode.title,vitnode.icon,description, andversionfields/admin/pluginsroute that lists all installed plugins with their registration status on both API and frontend sidesWhy?
Administrators need visibility into what plugins are installed and their versions without manually checking package.json files. This provides a centralized catalog view similar to package managers, making plugin management more transparent and user-friendly.
The metadata is generated at build time from
package.json, ensuring the catalog always reflects what's actually installed without requiring manual version updates.Test Plan
generate.test.ts)plugin-search.test.ts)plugins-content.test.tsx)installed-plugins.test.ts)generate.test.ts)list.route.test.ts)watch-plugin-metadata.test.ts)buildPluginandbuildApiPluginhttps://claude.ai/code/session_01SqJtNifFvW63sCoYVKgJ7m