feat(core): explicit plugins.register API (replaces no-op autoLoad/directory)#829
Open
lane711 wants to merge 5 commits into
Open
feat(core): explicit plugins.register API (replaces no-op autoLoad/directory)#829lane711 wants to merge 5 commits into
lane711 wants to merge 5 commits into
Conversation
…rectory) The previous `plugins.directory` + `autoLoad` config fields were declared in the type but never wired up — bootstrap only iterated the hardcoded core plugin registry, so user plugins silently failed to mount and the starter app had to wrap the core app in its own Hono instance to attach plugin routes. Filesystem autoload is the wrong shape for this anyway: SonicJS targets Cloudflare Workers, which has no runtime `fs`, and explicit registration is what mature plugin ecosystems (Vite, Astro, Fastify, Nuxt modules) landed on — type-safe, tree-shakeable, configurable, and easy to debug. Changes: - `SonicJSConfig.plugins.register?: Plugin[]` — pass plugin builds directly - `mountPlugin()` helper sorts middleware by priority then mounts routes - User plugins mount before the `/admin/*` catch-alls so plugin admin pages are not shadowed - `directory` / `autoLoad` kept as `@deprecated` no-ops for one minor - Starter template, in-repo example app, and docs updated Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ister Exercises the full app pipeline (bootstrap, security headers, CSRF, admin auth wall, user-plugin mounting, /admin catch-alls). Critically verifies that a plugin route at /admin/plugins/<name>/* is NOT shadowed by the core /admin/plugins catch-all — a 404 there would mean the ordering broke. Also updates the doc site: - plugins/development/page.mdx Step 4 — explicit register array, not the old re-export pattern that didn't actually load anything - faq/page.mdx — adds the register snippet next to the plugin creation example so the FAQ no longer leaves users hanging the way the Discord question did Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… subpath exports Combines this branch's user-plugin work with the changes from furelid#1 (furelid#1). The two PRs solved different problems and are complementary; this commit takes both. From furelid #1: - mountPluginManagerRoutes(app, plugins[], options) replaces the 9 duplicated route-mount loops in createSonicJSApp. Each core plugin's routes are now gated by a per-request guard that checks `is_active` in the plugins table — flipping the toggle in the admin UI now actually affects route resolution. - 60s TTL cache (pluginStatusCache) so the gate is one Map lookup on the hot path; one D1 read per plugin per minute amortized. - isCorePlugin callback bypasses gating for plugins with is_core === true in their manifest (database-tools, seed-data, core-cache, etc.). - plugins.enabled?: string[] for explicit allowlists when DB gating is disabled. - @sonicjs-cms/core/plugins subpath now exports aiSearchPlugin, analyticsPlugin, globalVariablesPlugin, oauthProvidersPlugin, securityAuditPlugin, OAuthService, BUILT_IN_PROVIDERS, stripePlugin, shortcodesPlugin, etc. so advanced users can wrap or compose them. - Tightens a few imports that were going through the public package re-export back to the source modules. Kept from this branch: - plugins.register?: Plugin[] for user plugins, mounted via mountPlugin() (no DB gating — if the user imported and registered it, they want it on). - @deprecated marks on plugins.directory and plugins.autoLoad. - Doc + blog post fixes. User plugins still bypass the new gating system. Adding them to mountPluginManagerRoutes with isCorePlugin: () => false is a small follow-up so the admin "disable plugin" toggle applies to user plugins too. All 1486 unit tests pass (1479 mine + furelid + 2 plugin-route-mounting + 5 createSonicJSApp integration smoke tests). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- README.md: replace stale plugin example (createSonicJS-shaped, hooks object that doesn't match the real PluginHook[] type) with a real PluginBuilder + plugins.register snippet that compiles against today's exports. - guides/sonicjs-plugins-extending-your-cms.mdx: rewrite the registration section, npm-consumer example, and a few aside-claims to use the actual createSonicJSApp + plugins.register API. Drops fictional createSonicJS, authPlugin()/emailPlugin()/etc. factory imports, and the "drop a file in src/plugins and SonicJS picks it up" claim — Workers has no runtime fs and there is no filesystem auto-discovery. - Unskip 5 plugin e2e specs (15-plugins, 21-plugin-version-display, 28-plugin-filters-search, 29-email-plugin-settings, 36-easymde-plugin- visible). 29 was previously skipped for D1-timing flakiness in CI; worth running it again now that the merged plugin-mounting changes exercise that path differently. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Re-running the plugin e2e suite against this branch (after fixing the node_modules symlink so the conductor workspace's core was actually loaded) surfaced 13 false-404s caused by furelid #1's gating logic: 1. Five plugins (email, otp-login, security-audit, core-analytics, magic-link-auth) were added to mountPluginManagerRoutes in app.ts but never added to BOOTSTRAP_PLUGIN_IDS, so their plugins-table row was never created and isPluginActive(db, name) always returned false. Result: 404 on every admin/api request to those plugins. 2. ai-search-plugin and turnstile-plugin used `name: manifest.name` ("AI Search", "Cloudflare Turnstile") for the PluginBuilder, but the gate looks up by manifest.id. The mismatch denied every request even though both manifests have is_core: true. Fixes: - Add the five missing plugins to BOOTSTRAP_PLUGIN_IDS so they're installed + activated on first boot. - Switch ai-search and turnstile to use manifest.id as the plugin name. - Add bootstrap-coverage.test.ts asserting every plugin mounted via mountPluginManagerRoutes is also bootstrapped, plus the name===id invariant for the two plugins that broke it. - Add /contact root-path regression test (plugin mounted at path: '/' with sub-path /contact must resolve, mirrors contact-form plugin). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
plugins.register?: Plugin[]toSonicJSConfigso user plugins are mounted bycreateSonicJSAppdirectly — no more wrapping the core app in a customHono()instance just to attach plugin routes.plugins.directory/plugins.autoLoadas@deprecatedno-ops. They were declared in the type but never wired up; bootstrap only iterated the hardcoded core plugin registry, so user plugins silently failed to load.mountPlugin()helper (packages/core/src/plugins/mount.ts) sorts middleware by priority, then mounts routes. User plugins are registered before/admin/pluginsand the/admincatch-all so plugin admin pages aren't shadowed.Why explicit, not filesystem autoload
SonicJS runs on Cloudflare Workers — no runtime
fs, so any directory autoload would have to be a build-time Vite plugin. That's a lot of machinery for two lines of saved code, and most mature plugin ecosystems (Vite, Astro, Fastify, Nuxt modules) landed on explicitregister: [...]arrays for the same reasons: type-safety, tree-shaking, debuggability, and the ability to pass config (weatherPlugin({ apiKey })) inline.Originated from a Discord report: a user couldn't get a custom plugin to load with
autoLoad: true. The flag did nothing.Before / after (starter app)
Test plan
npx vitest runinpackages/core— 1479 passed, 328 skipped, 0 failedmount.test.tscovers route mounting, multi-route, global vs scoped middleware, priority ordering, and empty-plugin no-op (6 tests)npx tsc --noEmitinpackages/coreis cleanpackages/statsstill compiles (disableAll: truepath unchanged)🤖 Generated with Claude Code