create-sonicjs 3.0.0-beta.2 → 3.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Example Plugin — Admin Routes
3
+ *
4
+ * Renders inside the shared admin layout (sidebar, nav, header) by calling
5
+ * renderAdminLayoutCatalyst from @sonicjs-cms/core. This is the DEFAULT
6
+ * pattern for plugin admin pages — provide inner `content` HTML only;
7
+ * the layout owns <html>, <head>, <body>, sidebar, and nav chrome.
8
+ *
9
+ * Pattern (copy this for your own plugin):
10
+ * 1. requireAuth() is applied by the core framework before this route fires.
11
+ * 2. Pull user + pluginMenuItems from context.
12
+ * 3. Build a `content` string — just the page body, no outer chrome.
13
+ * 4. Call renderAdminLayoutCatalyst(layoutData) and return c.html(result).
14
+ *
15
+ * Gear icon:
16
+ * Every plugin admin page should include a gear icon linking to
17
+ * /admin/plugins/<id>#settings. Admins can then jump straight to the
18
+ * settings form without navigating through the Plugins list.
19
+ *
20
+ * Escape hatch:
21
+ * If you need a fully custom page (OAuth callback, print view, embedded
22
+ * widget), return c.html(<full HTML document>) without calling
23
+ * renderAdminLayoutCatalyst. That still works — it just bypasses the
24
+ * shared chrome.
25
+ */
26
+
27
+ import { Hono } from 'hono'
28
+ import { renderAdminLayoutCatalyst, escapeHtml } from '@sonicjs-cms/core'
29
+ import type { AdminLayoutCatalystData } from '@sonicjs-cms/core'
30
+
31
+ export function createExampleAdminRoutes(options: { greeting?: string; defaultName?: string } = {}): Hono {
32
+ const router = new Hono<any>()
33
+
34
+ router.get('/', (c) => {
35
+ const greeting = options.greeting ?? 'Hello, Cruel World!'
36
+ const defaultName = options.defaultName ?? 'Stranger'
37
+ const user = c.get('user') as { email: string; role: string } | undefined
38
+ const dynamicMenuItems = c.get('pluginMenuItems') as Array<{ label: string; path: string; icon: string }> | undefined
39
+
40
+ const content = `
41
+ <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
42
+
43
+ <div class="flex items-center justify-between mb-6">
44
+ <div>
45
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Example Plugin</h1>
46
+ <p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">
47
+ A demo plugin for understanding the SonicJS v3 plugin system.
48
+ </p>
49
+ </div>
50
+ <a
51
+ href="/admin/plugins/example#settings"
52
+ title="Plugin settings"
53
+ class="inline-flex items-center gap-2 rounded-lg bg-zinc-100 dark:bg-zinc-800 px-3 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
54
+ >
55
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
57
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
59
+ </svg>
60
+ Settings
61
+ </a>
62
+ </div>
63
+
64
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6 mb-6">
65
+ <h2 class="text-lg font-semibold text-white mb-3">Current Config</h2>
66
+ <dl class="space-y-2 text-sm">
67
+ <div class="flex items-center gap-3">
68
+ <dt class="text-gray-400 w-28 shrink-0">Greeting</dt>
69
+ <dd class="text-green-400 font-mono">${escapeHtml(greeting)}</dd>
70
+ </div>
71
+ <div class="flex items-center gap-3">
72
+ <dt class="text-gray-400 w-28 shrink-0">Default name</dt>
73
+ <dd class="text-green-400 font-mono">${escapeHtml(defaultName)}</dd>
74
+ </div>
75
+ </dl>
76
+ <p class="text-xs text-gray-500 mt-3">
77
+ Try: <a href="/example" class="text-blue-400 hover:text-blue-300 font-mono">/example</a>
78
+ → greets <span class="text-green-400 font-mono">${escapeHtml(defaultName)}</span>
79
+ </p>
80
+ </div>
81
+
82
+ <!-- Moods Collection ─────────────────────────────────────────────────
83
+ Shows how a plugin contributes a collection to the document repo.
84
+ The core /admin/content/:collection route provides full CRUD for free —
85
+ the plugin just links to it. -->
86
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6 mb-6">
87
+ <div class="flex items-center justify-between mb-4">
88
+ <div>
89
+ <h2 class="text-lg font-semibold text-white">😈 Moods</h2>
90
+ <p class="text-xs text-gray-400 mt-0.5">
91
+ A random published mood is included in every API response.
92
+ Add, edit, or remove moods below.
93
+ </p>
94
+ </div>
95
+ <a
96
+ href="/admin/content?model=example&page=1"
97
+ class="inline-flex items-center gap-1.5 rounded-lg bg-gradient-to-r from-cyan-500 to-purple-600 hover:from-cyan-400 hover:to-purple-500 px-3 py-2 text-sm font-semibold text-white shadow-sm transition-all duration-200"
98
+ >
99
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
100
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
101
+ </svg>
102
+ Manage Moods
103
+ </a>
104
+ </div>
105
+ <p class="text-xs text-gray-500">
106
+ Try: <a href="/example" class="text-blue-400 hover:text-blue-300 font-mono">/example</a>
107
+ → response includes a random <code class="text-purple-400">mood</code> field from this collection.
108
+ </p>
109
+ </div>
110
+
111
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6 mb-6">
112
+ <h2 class="text-lg font-semibold text-white mb-3">How This Plugin Works</h2>
113
+ <dl class="space-y-3 text-sm">
114
+ <div>
115
+ <dt class="text-gray-400 font-medium">Public API</dt>
116
+ <dd><a href="/example" class="text-blue-400 hover:text-blue-300 font-mono">GET /example</a></dd>
117
+ <dd><a href="/example/traveller" class="text-blue-400 hover:text-blue-300 font-mono">GET /example/traveller</a></dd>
118
+ <dd class="text-gray-500 text-xs mt-1">Routes at /example/* not /api/* — user plugins mount after the core /:collection catch-all.</dd>
119
+ </div>
120
+ <div>
121
+ <dt class="text-gray-400 font-medium">Admin page</dt>
122
+ <dd class="text-gray-200 font-mono">GET /admin/example</dd>
123
+ </div>
124
+ <div>
125
+ <dt class="text-gray-400 font-medium">Hook subscriptions</dt>
126
+ <dd class="text-gray-200 font-mono">content:after:create</dd>
127
+ <dd class="text-gray-200 font-mono">auth:registration:completed</dd>
128
+ </div>
129
+ <div>
130
+ <dt class="text-gray-400 font-medium">Settings</dt>
131
+ <dd><a href="/admin/plugins/example#settings" class="text-blue-400 hover:text-blue-300 font-mono">/admin/plugins/example#settings</a></dd>
132
+ </div>
133
+ </dl>
134
+ </div>
135
+
136
+ ${user ? `
137
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 p-4 text-sm text-gray-400">
138
+ Logged in as <span class="text-white font-medium">${escapeHtml(user.email)}</span>
139
+ <span class="ml-2 text-xs text-gray-500">(${escapeHtml(user.role)})</span>
140
+ </div>
141
+ ` : ''}
142
+
143
+ </div>
144
+ `
145
+
146
+ const layoutData: AdminLayoutCatalystData = {
147
+ title: 'Example Plugin',
148
+ pageTitle: 'Example Plugin',
149
+ currentPath: '/admin/example',
150
+ content,
151
+ ...(user ? { user: { name: user.email, email: user.email, role: user.role } } : {}),
152
+ ...(dynamicMenuItems ? { dynamicMenuItems } : {}),
153
+ }
154
+
155
+ return c.html(renderAdminLayoutCatalyst(layoutData))
156
+ })
157
+
158
+ return router
159
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Example Plugin — Public API Routes
3
+ *
4
+ * Hono is SonicJS's HTTP framework. Each "route file" creates a small Hono app
5
+ * (or returns one from a factory function) and exports it. The plugin's
6
+ * `register(app)` call mounts these routes onto the root application.
7
+ *
8
+ * Route files are SYNCHRONOUS setup — they only wire up handlers, no async I/O.
9
+ * Anything that needs env/DB access goes in the handler itself (called per-request)
10
+ * or in the plugin's `onBoot` (called once per isolate warm-up).
11
+ *
12
+ * PATH NOTE: These routes are mounted at /example (NOT /api/...).
13
+ * User plugins mount after the core /api/:collection catch-all, so any
14
+ * /api/your-plugin path would be swallowed by the collection handler. See
15
+ * the plugin index.ts register() comment for the full explanation.
16
+ */
17
+
18
+ import { Hono } from 'hono'
19
+ import { DocumentRepository, PluginServiceClass as PluginService } from '@sonicjs-cms/core'
20
+
21
+ const MOODS_TYPE_ID = 'example'
22
+
23
+ async function getPluginSettings(db: any): Promise<{ greeting: string; defaultName: string }> {
24
+ const defaults = { greeting: 'Hello, Cruel World!', defaultName: 'Stranger' }
25
+ if (!db) return defaults
26
+ try {
27
+ const svc = new PluginService(db)
28
+ const plugin = await svc.getPlugin('example')
29
+ const s = (plugin?.settings ?? {}) as Record<string, unknown>
30
+ return {
31
+ greeting: typeof s.greeting === 'string' ? s.greeting : defaults.greeting,
32
+ defaultName: typeof s.defaultName === 'string' ? s.defaultName : defaults.defaultName,
33
+ }
34
+ } catch {
35
+ return defaults
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Pick a random published mood from the DB, or null if none exist yet.
41
+ * Uses DocumentRepository (the tenant-scoped read chokepoint — R4).
42
+ */
43
+ async function getRandomMood(db: any): Promise<{ name: string; emoji: string; description: string } | null> {
44
+ try {
45
+ const repo = new DocumentRepository(db, 'default')
46
+ const moods = await repo.list({ typeId: MOODS_TYPE_ID, status: 'published', limit: 100 })
47
+ if (moods.length === 0) return null
48
+ // Workers has no Math.random() restriction — safe to use here.
49
+ const pick = moods[Math.floor(Math.random() * moods.length)]
50
+ return pick.data as { name: string; emoji: string; description: string }
51
+ } catch {
52
+ return null
53
+ }
54
+ }
55
+
56
+ /**
57
+ * createExampleApiRoutes — factory that returns the public API router.
58
+ *
59
+ * Settings are read from the DB on each request so changes made via the admin UI
60
+ * take effect immediately without waiting for a Worker isolate restart.
61
+ */
62
+ export function createExampleApiRoutes(): Hono {
63
+ const router = new Hono()
64
+
65
+ // ── GET /example ────────────────────────────────────────────────────────────
66
+ // Returns a JSON greeting with a randomly-selected mood from the moods collection.
67
+ // c.env holds Cloudflare bindings (DB, KV, etc.) — accessed per-request, not at setup.
68
+ router.get('/', async (c) => {
69
+ const db = (c.env as any)?.DB
70
+ const [mood, settings] = await Promise.all([
71
+ db ? getRandomMood(db) : Promise.resolve(null),
72
+ getPluginSettings(db),
73
+ ])
74
+ const name = settings.defaultName
75
+
76
+ return c.json({
77
+ message: `${settings.greeting} I am ${name}.`,
78
+ mood: mood ? `${mood.emoji} ${mood.name}`.trim() : null,
79
+ moodDescription: mood?.description ?? null,
80
+ timestamp: new Date().toISOString(),
81
+ plugin: 'example',
82
+ })
83
+ })
84
+
85
+ // ── GET /example/moods ──────────────────────────────────────────────────────
86
+ // Lists all published moods — must be registered BEFORE /:name so Hono's
87
+ // route-registration-order matching doesn't swallow it as a name param.
88
+ router.get('/moods', async (c) => {
89
+ const db = (c.env as any)?.DB
90
+ if (!db) return c.json({ moods: [] })
91
+
92
+ try {
93
+ const repo = new DocumentRepository(db, 'default')
94
+ const docs = await repo.list({ typeId: MOODS_TYPE_ID, status: 'published', limit: 100 })
95
+ return c.json({
96
+ moods: docs.map(d => d.data),
97
+ total: docs.length,
98
+ })
99
+ } catch {
100
+ return c.json({ moods: [], total: 0 })
101
+ }
102
+ })
103
+
104
+ // ── GET /example/:name ──────────────────────────────────────────────────────
105
+ // Personalised greeting — :name from the URL, random mood from the collection.
106
+ // Registered AFTER /moods so the literal path wins.
107
+ router.get('/:name', async (c) => {
108
+ const db = (c.env as any)?.DB
109
+ const [mood, settings] = await Promise.all([
110
+ db ? getRandomMood(db) : Promise.resolve(null),
111
+ getPluginSettings(db),
112
+ ])
113
+ const name = c.req.param('name')
114
+
115
+ return c.json({
116
+ message: `${settings.greeting} I am ${name}.`,
117
+ mood: mood ? `${mood.emoji} ${mood.name}`.trim() : null,
118
+ moodDescription: mood?.description ?? null,
119
+ timestamp: new Date().toISOString(),
120
+ plugin: 'example',
121
+ })
122
+ })
123
+
124
+ return router
125
+ }
@@ -1,6 +1,6 @@
1
1
  name = "my-sonicjs-app"
2
2
  main = "src/index.ts"
3
- compatibility_date = "2024-01-01"
3
+ compatibility_date = "2024-09-23"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
 
6
6
  # Cloudflare Workers settings
@@ -21,7 +21,7 @@ bucket_name = "my-sonicjs-media"
21
21
  # Environment variables
22
22
  [vars]
23
23
  ENVIRONMENT = "development"
24
- CORS_ORIGINS = "http://localhost:8787"
24
+ CORS_ORIGINS = "http://localhost:8787,http://localhost:4321"
25
25
 
26
26
  # Production environment
27
27
  [env.production]