@jchat/create-addon 1.0.0-alpha.3 → 1.0.0-alpha.30
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.
- package/CHANGELOG.md +180 -0
- package/LICENSE +18 -196
- package/README.md +193 -21
- package/dist/cli.d.mts +12 -5
- package/dist/cli.mjs +71 -31
- package/dist/contracts2.d.mts +45 -0
- package/dist/generator.d.mts +38 -0
- package/dist/generator.mjs +538 -244
- package/dist/index.d.mts +3 -12
- package/dist/index.mjs +1 -2
- package/package.json +29 -23
- package/src/cli.test.ts +0 -30
- package/src/cli.ts +0 -55
- package/src/generator.ts +0 -428
- package/src/index.ts +0 -5
package/dist/generator.mjs
CHANGED
|
@@ -1,83 +1,260 @@
|
|
|
1
|
-
import *
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import{n as e,t}from"./contracts2.d.mts";import*as n from"node:fs";import*as r from"node:path";var[i]=[8,()=>[],[``]];function a(e){return`import type { ColumnType, Selectable } from "@jchat/addon-sdk"
|
|
2
|
+
|
|
3
|
+
export interface AddonItemTable {
|
|
4
|
+
id: string
|
|
5
|
+
siteId: string
|
|
6
|
+
roomId: string
|
|
7
|
+
userId: string
|
|
8
|
+
title: string
|
|
9
|
+
createdAt: ColumnType<Date, Date | string, Date | string>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AddonDatabase {
|
|
13
|
+
${`${e.replace(/-/g,`_`)}_items`}: AddonItemTable
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
15
|
+
|
|
16
|
+
export type AddonItem = Selectable<AddonItemTable>
|
|
17
|
+
`}function o(e){let t=`${e.replace(/-/g,`_`)}_items`;return`CREATE TABLE IF NOT EXISTS "addons"."${t}" (
|
|
18
|
+
"id" VARCHAR(36) PRIMARY KEY,
|
|
19
|
+
"site_id" VARCHAR(36) NOT NULL,
|
|
20
|
+
"room_id" VARCHAR(36) NOT NULL,
|
|
21
|
+
"user_id" VARCHAR(36) NOT NULL,
|
|
22
|
+
"title" VARCHAR(255) NOT NULL,
|
|
23
|
+
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS "${`idx_${t}_site_room`}"
|
|
27
|
+
ON "addons"."${t}" ("site_id", "room_id", "created_at" DESC);
|
|
28
|
+
`}function s(e){let t=`${e.replace(/-/g,`_`)}_items`;return`import type { AddonApp, AddonServerContext } from "@jchat/addon-sdk"
|
|
29
|
+
import type { AddonDatabase } from "./db"
|
|
30
|
+
|
|
31
|
+
export default (
|
|
32
|
+
{
|
|
33
|
+
db,
|
|
34
|
+
t,
|
|
35
|
+
cache,
|
|
36
|
+
emit,
|
|
37
|
+
getUser,
|
|
38
|
+
hasPermission
|
|
39
|
+
}: AddonServerContext<AddonDatabase>,
|
|
40
|
+
app: AddonApp
|
|
41
|
+
) => {
|
|
42
|
+
return app
|
|
43
|
+
.get(
|
|
44
|
+
"/items",
|
|
45
|
+
async ({ query, user }) => {
|
|
46
|
+
const siteId = user?.siteId ?? ""
|
|
47
|
+
const roomId = query.roomId ?? ""
|
|
48
|
+
const items = await db
|
|
49
|
+
.selectFrom("${t}")
|
|
50
|
+
.where("siteId", "=", siteId)
|
|
51
|
+
.where("roomId", "=", roomId)
|
|
52
|
+
.orderBy("createdAt", "desc")
|
|
53
|
+
.limit(20)
|
|
54
|
+
.selectAll()
|
|
55
|
+
.execute()
|
|
56
|
+
|
|
57
|
+
return { items }
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
query: t.Object({
|
|
61
|
+
roomId: t.Optional(t.String())
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
.post(
|
|
66
|
+
"/items",
|
|
67
|
+
async ({ body, user, set }) => {
|
|
68
|
+
if (!user) {
|
|
69
|
+
set.status = 401
|
|
70
|
+
return { error: "Unauthorized" }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const id = Bun.randomUUIDv7()
|
|
74
|
+
const siteId = user.siteId
|
|
75
|
+
const roomId = body.roomId
|
|
76
|
+
const title = body.title
|
|
77
|
+
|
|
78
|
+
await db
|
|
79
|
+
.insertInto("${t}")
|
|
80
|
+
.values({
|
|
81
|
+
id,
|
|
82
|
+
siteId,
|
|
83
|
+
roomId,
|
|
84
|
+
userId: user.id,
|
|
85
|
+
title,
|
|
86
|
+
createdAt: new Date()
|
|
87
|
+
})
|
|
88
|
+
.execute()
|
|
89
|
+
|
|
90
|
+
await emit("addon:${e}:item-created", { id, title, userId: user.id })
|
|
91
|
+
return { id, title }
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
body: t.Object({
|
|
95
|
+
roomId: t.String(),
|
|
96
|
+
title: t.String({ minLength: 1, maxLength: 255 })
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
)
|
|
58
100
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
`}function c(e,t,n){return n?`import type { AddonProps } from "@jchat/addon-sdk"
|
|
102
|
+
import { useAddonMutation, useAddonQuery, useAddonSDK, useTranslation } from "@jchat/addon-sdk"
|
|
103
|
+
import Icons from "@jchat/addon-sdk/icon"
|
|
104
|
+
import { useState } from "react"
|
|
105
|
+
import locales from "./locales"
|
|
106
|
+
import "./tailwind.css"
|
|
107
|
+
|
|
108
|
+
interface ItemRecord {
|
|
109
|
+
id: string
|
|
110
|
+
title: string
|
|
111
|
+
createdAt: string
|
|
67
112
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
113
|
+
|
|
114
|
+
export default function Addon({ mountPoint, roomId }: AddonProps) {
|
|
115
|
+
const sdk = useAddonSDK()
|
|
116
|
+
const { t, dir } = useTranslation(locales)
|
|
117
|
+
const [title, setTitle] = useState("")
|
|
118
|
+
|
|
119
|
+
const { data, refetch, isLoading } = useAddonQuery<{ items: ItemRecord[] }>({
|
|
120
|
+
path: \`/items?roomId=\${roomId ?? ""}\`
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const { mutate: createItem, isPending } = useAddonMutation<
|
|
124
|
+
{ id: string; title: string },
|
|
125
|
+
{ roomId: string; title: string }
|
|
126
|
+
>({
|
|
127
|
+
path: "/items",
|
|
128
|
+
onSuccess: () => {
|
|
129
|
+
setTitle("")
|
|
130
|
+
refetch()
|
|
131
|
+
sdk.toast(t("toast_message", { name: "${e}" }))
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (mountPoint === "chat-sidebar") {
|
|
136
|
+
return (
|
|
137
|
+
<aside dir={dir} className="flex h-full flex-col border-s border-border bg-card p-4 text-card-foreground">
|
|
138
|
+
<header className="mb-4 flex items-center gap-2 border-b border-border pb-2">
|
|
139
|
+
<Icons.Plus aria-hidden="true" className="size-5 text-primary" />
|
|
140
|
+
<h2 className="font-semibold">${e}</h2>
|
|
141
|
+
</header>
|
|
142
|
+
<p className="mb-3 text-xs text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
|
|
143
|
+
<form
|
|
144
|
+
className="mb-4 flex gap-2"
|
|
145
|
+
onSubmit={(e) => {
|
|
146
|
+
e.preventDefault()
|
|
147
|
+
if (!title.trim() || !roomId) return
|
|
148
|
+
createItem({ roomId, title: title.trim() })
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<input
|
|
152
|
+
type="text"
|
|
153
|
+
value={title}
|
|
154
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
155
|
+
placeholder="Add new item..."
|
|
156
|
+
className="flex-1 rounded border border-input bg-background px-2 py-1 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
157
|
+
/>
|
|
158
|
+
<button
|
|
159
|
+
type="submit"
|
|
160
|
+
disabled={isPending || !title.trim()}
|
|
161
|
+
className="rounded bg-primary px-3 py-1 text-sm text-primary-foreground disabled:opacity-50"
|
|
162
|
+
>
|
|
163
|
+
{isPending ? "..." : t("test_button")}
|
|
164
|
+
</button>
|
|
165
|
+
</form>
|
|
166
|
+
<div className="flex-1 space-y-1 overflow-y-auto">
|
|
167
|
+
{isLoading && <p className="text-xs text-muted-foreground">Loading...</p>}
|
|
168
|
+
{data?.items?.map((item) => (
|
|
169
|
+
<div key={item.id} className="rounded border border-border/50 bg-background/50 p-2 text-xs">
|
|
170
|
+
{item.title}
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
</aside>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div dir={dir} className="rounded border border-border bg-card p-2 text-card-foreground">
|
|
180
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
181
|
+
<div className="flex items-center gap-2">
|
|
182
|
+
<Icons.Plus aria-hidden="true" className="size-4 text-primary" />
|
|
183
|
+
<strong>${e}</strong>
|
|
184
|
+
</div>
|
|
185
|
+
<span className="text-xs text-muted-foreground">{t("mounted_at", { mountPoint })}</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="flex gap-2">
|
|
188
|
+
<input
|
|
189
|
+
type="text"
|
|
190
|
+
value={title}
|
|
191
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
192
|
+
placeholder="New item..."
|
|
193
|
+
className="flex-1 rounded border border-input bg-background px-2 py-1 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
194
|
+
/>
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
disabled={isPending || !title.trim()}
|
|
198
|
+
className="rounded bg-primary px-3 py-1 text-xs text-primary-foreground disabled:opacity-50"
|
|
199
|
+
onClick={() => {
|
|
200
|
+
if (!title.trim()) return
|
|
201
|
+
createItem({ roomId: roomId ?? "general", title: title.trim() })
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
{isPending ? "..." : t("test_button")}
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)
|
|
78
209
|
}
|
|
79
|
-
|
|
80
|
-
|
|
210
|
+
`:`import type { AddonProps } from "@jchat/addon-sdk"
|
|
211
|
+
import { useAddonSDK, useTranslation } from "@jchat/addon-sdk"
|
|
212
|
+
import Icons from "@jchat/addon-sdk/icon"
|
|
213
|
+
import locales from "./locales"
|
|
214
|
+
import "./tailwind.css"
|
|
215
|
+
|
|
216
|
+
export default function Addon({ mountPoint }: AddonProps) {
|
|
217
|
+
const sdk = useAddonSDK()
|
|
218
|
+
const { t, dir } = useTranslation(locales)
|
|
219
|
+
|
|
220
|
+
if (mountPoint === "chat-sidebar") {
|
|
221
|
+
return (
|
|
222
|
+
<aside dir={dir} className="flex h-full flex-col border-s border-border bg-card p-4 text-card-foreground">
|
|
223
|
+
<header className="mb-4 flex items-center gap-2 border-b border-border pb-2">
|
|
224
|
+
<Icons.Plus aria-hidden="true" className="size-5 text-primary" />
|
|
225
|
+
<h2 className="font-semibold">${e}</h2>
|
|
226
|
+
</header>
|
|
227
|
+
<p className="mb-2 text-sm text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
className="mt-4 rounded bg-primary px-3 py-2 text-primary-foreground"
|
|
231
|
+
onClick={() => sdk.toast(t("toast_message", { name: "${e}" }))}
|
|
232
|
+
>
|
|
233
|
+
{t("test_button")}
|
|
234
|
+
</button>
|
|
235
|
+
</aside>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div dir={dir} className="rounded border border-border bg-card p-2 text-card-foreground">
|
|
241
|
+
<div className="mb-2 flex items-center gap-2">
|
|
242
|
+
<Icons.Plus aria-hidden="true" className="size-4 text-primary" />
|
|
243
|
+
<strong>${e}</strong>
|
|
244
|
+
</div>
|
|
245
|
+
<p className="mb-2 text-sm text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
className="rounded bg-primary px-3 py-2 text-primary-foreground"
|
|
249
|
+
onClick={() => sdk.toast(t("toast_message", { name: "${e}" }))}
|
|
250
|
+
>
|
|
251
|
+
{t("test_button")}
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
`}function l(e,t){return[`JCHAT_SANDBOX_IMAGE=${t}`,`JCHAT_SANDBOX_ADDON_ID=${e}`,`JCHAT_SANDBOX_PORT=4317`,`JCHAT_SANDBOX_HTTP_MOCK_PORT=4318`,`JCHAT_SANDBOX_HTTP_MOCK_OFFLINE=false`,`JCHAT_SANDBOX_ADDON_HTTP_MOCK_URL=http://http-mock:4318`,``].join(`
|
|
257
|
+
`)}function u(){return`services:
|
|
81
258
|
sandbox:
|
|
82
259
|
image: \${JCHAT_SANDBOX_IMAGE:?Copy .env.example to .env}
|
|
83
260
|
restart: unless-stopped
|
|
@@ -87,9 +264,6 @@ function createComposeFile() {
|
|
|
87
264
|
JCHAT_COOKIE_SECRET: addon-sandbox-cookie-secret
|
|
88
265
|
JCHAT_DATABASE_URL: postgresql://jchat:jchat@db:5432/jchat
|
|
89
266
|
JCHAT_REDIS_DATABASE_URL: redis://redis:6379
|
|
90
|
-
JCHAT_STORAGE_TYPE: LOCAL
|
|
91
|
-
JCHAT_BOT_NAME: Sandbox Bot
|
|
92
|
-
JCHAT_BOT_EMAIL: bot@sandbox.local
|
|
93
267
|
JCHAT_DOMAIN: localhost
|
|
94
268
|
JCHAT_SITE_URL: http://localhost:\${JCHAT_SANDBOX_PORT:-4317}
|
|
95
269
|
JCHAT_ROUTING_MODE: path
|
|
@@ -98,7 +272,7 @@ function createComposeFile() {
|
|
|
98
272
|
ports:
|
|
99
273
|
- "127.0.0.1:\${JCHAT_SANDBOX_PORT:-4317}:4317"
|
|
100
274
|
volumes:
|
|
101
|
-
- ./addon:/app/addons
|
|
275
|
+
- ./addon:/app/addons/\${JCHAT_SANDBOX_ADDON_ID:?Set addon id}:ro
|
|
102
276
|
- sandbox_uploads:/app/uploads
|
|
103
277
|
depends_on:
|
|
104
278
|
db:
|
|
@@ -150,159 +324,7 @@ volumes:
|
|
|
150
324
|
sandbox_postgres:
|
|
151
325
|
sandbox_redis:
|
|
152
326
|
sandbox_uploads:
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
function createRootPackage(addonId) {
|
|
156
|
-
return {
|
|
157
|
-
name: `${addonId}-development-kit`,
|
|
158
|
-
private: true,
|
|
159
|
-
type: "module",
|
|
160
|
-
workspaces: ["addon"],
|
|
161
|
-
scripts: {
|
|
162
|
-
sandbox: "bun run ./scripts/devkit.ts run",
|
|
163
|
-
"sandbox:test": "bun run ./scripts/devkit.ts test",
|
|
164
|
-
"sandbox:reset": "bun run ./scripts/devkit.ts reset",
|
|
165
|
-
"sandbox:package": "bun run ./scripts/devkit.ts package",
|
|
166
|
-
"sandbox:cleanup": "bun run ./scripts/devkit.ts cleanup"
|
|
167
|
-
},
|
|
168
|
-
devDependencies: { "bun-types": "^1.3.14" },
|
|
169
|
-
engines: { bun: ">=1.3.11" }
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
function createAddonPackage(addonId, addonSdkVersion) {
|
|
173
|
-
return {
|
|
174
|
-
name: `@jchat/${addonId}`,
|
|
175
|
-
version: "1.0.0",
|
|
176
|
-
private: true,
|
|
177
|
-
type: "module",
|
|
178
|
-
scripts: {
|
|
179
|
-
dev: "vite build --watch --mode=development",
|
|
180
|
-
build: "vite build --mode=production",
|
|
181
|
-
test: "bun test",
|
|
182
|
-
typecheck: "tsc --noEmit"
|
|
183
|
-
},
|
|
184
|
-
dependencies: {
|
|
185
|
-
"@jchat/addon-sdk": `^${addonSdkVersion}`,
|
|
186
|
-
"@tanstack/react-query": "^5.101.4",
|
|
187
|
-
react: "^19.2.8",
|
|
188
|
-
"react-dom": "^19.2.8"
|
|
189
|
-
},
|
|
190
|
-
devDependencies: {
|
|
191
|
-
"@tailwindcss/vite": "^4.3.3",
|
|
192
|
-
"@types/node": "^26.1.1",
|
|
193
|
-
"@types/react": "^19.2.17",
|
|
194
|
-
"@types/react-dom": "^19.2.3",
|
|
195
|
-
"bun-types": "^1.3.14",
|
|
196
|
-
tailwindcss: "^4.3.3",
|
|
197
|
-
typescript: "^7.0.2",
|
|
198
|
-
vite: "^8.1.5"
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
function createTsConfig() {
|
|
203
|
-
return {
|
|
204
|
-
compilerOptions: {
|
|
205
|
-
strict: true,
|
|
206
|
-
target: "ESNext",
|
|
207
|
-
module: "ESNext",
|
|
208
|
-
moduleResolution: "bundler",
|
|
209
|
-
jsx: "react-jsx",
|
|
210
|
-
noEmit: true,
|
|
211
|
-
types: ["bun-types", "node"]
|
|
212
|
-
},
|
|
213
|
-
include: [
|
|
214
|
-
"src/**/*.ts",
|
|
215
|
-
"src/**/*.tsx",
|
|
216
|
-
"vite.config.ts"
|
|
217
|
-
]
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
function createManifestSource(manifest) {
|
|
221
|
-
return `import type { AddonManifest } from "@jchat/addon-sdk"
|
|
222
|
-
|
|
223
|
-
const manifest: AddonManifest = ${JSON.stringify(manifest, null, 2)}
|
|
224
|
-
|
|
225
|
-
export default manifest
|
|
226
|
-
`;
|
|
227
|
-
}
|
|
228
|
-
function createViteConfig() {
|
|
229
|
-
return `import { jchatAddon } from "@jchat/addon-sdk/vite"
|
|
230
|
-
import tailwindcss from "@tailwindcss/vite"
|
|
231
|
-
import path from "node:path"
|
|
232
|
-
import { defineConfig } from "vite"
|
|
233
|
-
import manifest from "./src/manifest"
|
|
234
|
-
|
|
235
|
-
export default defineConfig({
|
|
236
|
-
plugins: [tailwindcss(), jchatAddon({ manifest })],
|
|
237
|
-
build: {
|
|
238
|
-
outDir: "build",
|
|
239
|
-
emptyOutDir: true,
|
|
240
|
-
lib: {
|
|
241
|
-
entry: path.resolve(import.meta.dirname, "src/ui.tsx"),
|
|
242
|
-
formats: ["es"],
|
|
243
|
-
fileName: () => "ui.[hash].js"
|
|
244
|
-
},
|
|
245
|
-
rolldownOptions: {
|
|
246
|
-
external: [
|
|
247
|
-
"react",
|
|
248
|
-
"react-dom",
|
|
249
|
-
"react/jsx-runtime",
|
|
250
|
-
"@tanstack/react-query",
|
|
251
|
-
"@jchat/addon-sdk"
|
|
252
|
-
],
|
|
253
|
-
output: {
|
|
254
|
-
format: "esm"
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
})
|
|
259
|
-
`;
|
|
260
|
-
}
|
|
261
|
-
function createUiSource(name) {
|
|
262
|
-
return `import type { AddonProps } from "@jchat/addon-sdk"
|
|
263
|
-
import { useAddonSDK, useIcons } from "@jchat/addon-sdk"
|
|
264
|
-
import "./tailwind.css"
|
|
265
|
-
|
|
266
|
-
export default function Addon({ mountPoint }: AddonProps) {
|
|
267
|
-
const sdk = useAddonSDK()
|
|
268
|
-
const { Icons } = useIcons()
|
|
269
|
-
|
|
270
|
-
return (
|
|
271
|
-
<div className="rounded border border-border bg-card p-2 text-card-foreground">
|
|
272
|
-
<div className="mb-2 flex items-center gap-2">
|
|
273
|
-
<Icons.Plus aria-hidden="true" className="size-4 text-primary" />
|
|
274
|
-
<strong>${name}</strong>
|
|
275
|
-
</div>
|
|
276
|
-
<p className="mb-2 text-sm text-muted-foreground">Mounted at {mountPoint}</p>
|
|
277
|
-
<button
|
|
278
|
-
type="button"
|
|
279
|
-
className="rounded bg-primary px-3 py-2 text-primary-foreground"
|
|
280
|
-
onClick={() => sdk.toast("Hello from ${name}")}
|
|
281
|
-
>
|
|
282
|
-
Test addon
|
|
283
|
-
</button>
|
|
284
|
-
</div>
|
|
285
|
-
)
|
|
286
|
-
}
|
|
287
|
-
`;
|
|
288
|
-
}
|
|
289
|
-
function createTailwindSource() {
|
|
290
|
-
return `@import "tailwindcss";
|
|
291
|
-
|
|
292
|
-
@theme inline {
|
|
293
|
-
--color-background: var(--background);
|
|
294
|
-
--color-foreground: var(--foreground);
|
|
295
|
-
--color-card: var(--card);
|
|
296
|
-
--color-card-foreground: var(--card-foreground);
|
|
297
|
-
--color-border: var(--border);
|
|
298
|
-
--color-primary: var(--primary);
|
|
299
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
300
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
301
|
-
}
|
|
302
|
-
`;
|
|
303
|
-
}
|
|
304
|
-
function createLifecycleScript() {
|
|
305
|
-
return `import * as fs from "node:fs"
|
|
327
|
+
`}function d(){return`import * as fs from "node:fs"
|
|
306
328
|
import * as path from "node:path"
|
|
307
329
|
|
|
308
330
|
type Command = "run" | "test" | "reset" | "package" | "cleanup"
|
|
@@ -314,7 +336,7 @@ if (!["run", "test", "reset", "package", "cleanup"].includes(command)) {
|
|
|
314
336
|
|
|
315
337
|
const root = path.resolve(import.meta.dir, "..")
|
|
316
338
|
const environment = {
|
|
317
|
-
...
|
|
339
|
+
...Bun.env,
|
|
318
340
|
...readEnvironment(path.join(root, ".env"))
|
|
319
341
|
}
|
|
320
342
|
const addonId = environment.JCHAT_SANDBOX_ADDON_ID
|
|
@@ -371,12 +393,9 @@ function readEnvironment(file: string): Record<string, string> {
|
|
|
371
393
|
})
|
|
372
394
|
)
|
|
373
395
|
}
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
function createReadme(addonId) {
|
|
377
|
-
return `# ${toDisplayName(addonId)} Development Kit
|
|
396
|
+
`}function f(e,t){return`# ${t} Development Kit
|
|
378
397
|
|
|
379
|
-
This workspace develops \`${
|
|
398
|
+
This workspace develops \`${e}\` against the license-free JChat developer sandbox without
|
|
380
399
|
including JChat Core source.
|
|
381
400
|
|
|
382
401
|
## Start
|
|
@@ -400,10 +419,285 @@ Open \`http://127.0.0.1:4317\`. The HTTP fixture recorder is available at
|
|
|
400
419
|
|
|
401
420
|
The referenced JChat packages and sandbox image must be published before this standalone kit can
|
|
402
421
|
install or start outside the JChat repository.
|
|
403
|
-
|
|
422
|
+
`}const p={ar:e=>({welcome:`مرحبًا بك في ${e}`,mounted_at:`تم التثبيت في {{mountPoint}}`,test_button:`اختبار الإضافة`,toast_message:`مرحبًا من ${e}`}),bg:e=>({welcome:`Добре дошли в ${e}`,mounted_at:`Монтирано на {{mountPoint}}`,test_button:`Тестване на добавката`,toast_message:`Здравейте от ${e}`}),de:e=>({welcome:`Willkommen bei ${e}`,mounted_at:`Eingebunden an {{mountPoint}}`,test_button:`Addon testen`,toast_message:`Hallo von ${e}`}),el:e=>({welcome:`Καλώς ήρθατε στο ${e}`,mounted_at:`Προσαρτήθηκε στο {{mountPoint}}`,test_button:`Δοκιμή πρόσθετου`,toast_message:`Γεια σας από το ${e}`}),en:e=>({welcome:`Welcome to ${e}`,mounted_at:`Mounted at {{mountPoint}}`,test_button:`Test addon`,toast_message:`Hello from ${e}`}),es:e=>({welcome:`Bienvenido a ${e}`,mounted_at:`Montado en {{mountPoint}}`,test_button:`Probar complemento`,toast_message:`Hola desde ${e}`}),fr:e=>({welcome:`Bienvenue sur ${e}`,mounted_at:`Monté sur {{mountPoint}}`,test_button:`Tester le module`,toast_message:`Bonjour de ${e}`}),he:e=>({welcome:`ברוכים הבאים אל ${e}`,mounted_at:`מותקן ב-{{mountPoint}}`,test_button:`בדוק תוסף`,toast_message:`שלום מ-${e}`}),hi:e=>({welcome:`${e} में आपका स्वागत है`,mounted_at:`{{mountPoint}} पर स्थापित`,test_button:`ऐडऑन का परीक्षण करें`,toast_message:`${e} से नमस्ते`}),hr:e=>({welcome:`Dobrodošli u ${e}`,mounted_at:`Postavljeno na {{mountPoint}}`,test_button:`Testiraj dodatak`,toast_message:`Pozdrav iz ${e}`}),kn:e=>({welcome:`${e} ಗೆ ಸುಸ್ವಾಗತ`,mounted_at:`{{mountPoint}} ನಲ್ಲಿ ಸ್ಥಾಪಿಸಲಾಗಿದೆ`,test_button:`ಆಡ್ಆನ್ ಪರೀಕ್ಷಿಸಿ`,toast_message:`${e} ಇಂದ ನಮಸ್ಕಾರ`}),ml:e=>({welcome:`${e}-ലേക്ക് സ്വാഗതം`,mounted_at:`{{mountPoint}}-ൽ ഘടിപ്പിച്ചു`,test_button:`ആഡ്ഓൺ പരിശോധിക്കുക`,toast_message:`${e}-ൽ നിന്നുള്ള ആശംസകൾ`}),nl:e=>({welcome:`Welkom bij ${e}`,mounted_at:`Gekoppeld op {{mountPoint}}`,test_button:`Addon testen`,toast_message:`Hallo vanuit ${e}`}),pt:e=>({welcome:`Bem-vindo ao ${e}`,mounted_at:`Montado em {{mountPoint}}`,test_button:`Testar extensão`,toast_message:`Olá de ${e}`}),ro:e=>({welcome:`Bun venit la ${e}`,mounted_at:`Montat la {{mountPoint}}`,test_button:`Testează extensia`,toast_message:`Bună din ${e}`}),ru:e=>({welcome:`Добро пожаловать в ${e}`,mounted_at:`Подключено в {{mountPoint}}`,test_button:`Проверить дополнение`,toast_message:`Привет от ${e}`}),ta:e=>({welcome:`${e}-க்கு வரவேற்கிறோம்`,mounted_at:`{{mountPoint}}-ல் பொருத்தப்பட்டுள்ளது`,test_button:`செருகுநிரலை சோதிக்கவும்`,toast_message:`${e}-லிருந்து வணக்கம்`}),te:e=>({welcome:`${e}కి స్వాగతం`,mounted_at:`{{mountPoint}} వద్ద మౌంట్ చేయబడింది`,test_button:`యాడ్ఆన్ని పరీక్షించండి`,toast_message:`${e} నుండి నమస్కారం`}),tr:e=>({welcome:`${e}'e hoş geldiniz`,mounted_at:`{{mountPoint}} konumuna yerleştirildi`,test_button:`Eklentiyi test et`,toast_message:`${e}'den merhaba`}),ur:e=>({welcome:`${e} میں خوش آمدید`,mounted_at:`{{mountPoint}} پر نصب ہے`,test_button:`ایڈ آن کا تجربہ کریں`,toast_message:`${e} کی طرف سے ہیلو`})};function m(){return`${t.map(e=>`import ${e} from "./${e}.json"`).join(`
|
|
423
|
+
`)}
|
|
424
|
+
|
|
425
|
+
export type TranslationKey = keyof typeof en
|
|
426
|
+
|
|
427
|
+
export const locales = {
|
|
428
|
+
${t.map(e=>` ${e}`).join(`,
|
|
429
|
+
`)}
|
|
430
|
+
} as const
|
|
431
|
+
|
|
432
|
+
export default locales
|
|
433
|
+
`}function h(){return`import { describe, expect, test } from "bun:test"
|
|
434
|
+
import { validateAddonLocales } from "@jchat/addon-sdk/i18n-validator"
|
|
435
|
+
import * as path from "node:path"
|
|
436
|
+
|
|
437
|
+
describe("addon locales", () => {
|
|
438
|
+
test("maintains key parity across all 20 host languages", () => {
|
|
439
|
+
const localesDir = path.join(import.meta.dir, "locales")
|
|
440
|
+
const result = validateAddonLocales(localesDir)
|
|
441
|
+
if (!result.valid) {
|
|
442
|
+
console.error(result.errors.join("\\n"))
|
|
443
|
+
}
|
|
444
|
+
expect(result.valid).toBe(true)
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
`}function g(e){let n={"src/locales/index.ts":m(),"src/locales.test.ts":h()};for(let r of t){let t=p[r];n[`src/locales/${r}.json`]=`${JSON.stringify(t(e),null,2)}\n`}return n}function _(e){return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
|
448
|
+
<defs>
|
|
449
|
+
<linearGradient id="addon-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
450
|
+
<stop offset="0%" stop-color="#6366f1" />
|
|
451
|
+
<stop offset="100%" stop-color="#a855f7" />
|
|
452
|
+
</linearGradient>
|
|
453
|
+
</defs>
|
|
454
|
+
<rect width="128" height="128" rx="28" fill="url(#addon-grad)" />
|
|
455
|
+
<circle cx="64" cy="64" r="42" fill="white" fill-opacity="0.15" />
|
|
456
|
+
<text x="64" y="76" font-family="system-ui, -apple-system, sans-serif" font-size="44" font-weight="800" fill="white" text-anchor="middle" dominant-baseline="middle">${e.charAt(0).toUpperCase()||`A`}</text>
|
|
457
|
+
</svg>
|
|
458
|
+
`}function v(e){return`import type { AddonManifest } from "@jchat/addon-sdk"
|
|
459
|
+
|
|
460
|
+
const manifest: AddonManifest = ${JSON.stringify(e,null,2)}
|
|
461
|
+
|
|
462
|
+
export default manifest
|
|
463
|
+
`}function y(){return`@import "@jchat/addon-sdk/ui/tailwind.css";`}var b={name:`jchat`,version:`0.0.0`,private:!0,author:{name:`Jchat Cloud`,email:`support@jchatcloud.com`,url:`https://github.com/jchatcloud`},workspaces:[`apps/*`,`!apps/android`,`packages/*`,`addons/*`],scripts:{dev:`bun run --parallel chat:dev addons:dev`,"chat:dev":`bun --watch apps/chat/server.ts`,build:`bun run --filter @jchat/chat build`,start:`bun apps/chat/server.ts`,"db:generate":`bun --env-file=.env --filter="@jchat/db" run db:generate`,"db:migrate":`bun --env-file=.env --filter="@jchat/db" run db:migrate`,"db:seed":`bun --env-file=.env --filter="@jchat/db" run db:seed`,"db:studio":`bun --env-file=.env --filter="@jchat/db" run db:studio`,"db:push":`bun --env-file=.env --filter="@jchat/db" run db:push`,"db:pull":`bun --env-file=.env --filter="@jchat/db" run db:pull`,"icon:build":`bun run --filter @jchat/icon build`,"ui:build":`bun run --filter @jchat/ui build`,"addon-sdk:build":`bun run icon:build && bun run ui:build && bun run --filter @jchat/addon-sdk build`,"create-addon:build":`bun run --filter @jchat/create-addon build`,"create-addon:test":`bun run --filter @jchat/create-addon test`,"create-addon":`bun run ./packages/create-addon/src/cli.ts`,"runtime:build":`bun run --filter @jchat/runtime build`,"addons:dev":`bun --filter='./addons/*' --parallel run dev`,"addons:init":`bun run ./packages/create-addon/src/cli.ts --workspace`,"addons:sync":`bun run ./scripts/sync-local-addons.ts`,"addons:sandbox":`bun run ./scripts/developer-sandbox.ts run`,"addons:sandbox:init":`bun run ./scripts/developer-sandbox.ts init`,"addons:sandbox:test":`bun run ./scripts/developer-sandbox.ts test`,"addons:sandbox:reset":`bun run ./scripts/developer-sandbox.ts reset`,"addons:sandbox:package":`bun run ./scripts/developer-sandbox.ts package`,"addons:sandbox:cleanup":`bun run ./scripts/developer-sandbox.ts cleanup`,"addons:package":`bun run ./scripts/package-addons.ts`,"addon:release":`bun run ./scripts/release-addon.ts`,"addons:devkit:init":`bun run ./scripts/create-addon-development-kit.ts`,"addons:devkit:artifacts:check":`bun run addon-sdk:build && bun run create-addon:build && bun run ./scripts/validate-addon-development-artifacts.ts`,"store:build":`bun run --filter @jchat/store build`,"store:dev":`bun --watch apps/store/server.ts`,"store:db:generate":`bun --filter @jchat/store db:generate`,"store:db:migrate":`bun --env-file=apps/store/.env --filter @jchat/store db:migrate`,"store:db:seed":`bun --env-file=apps/store/.env --filter @jchat/store db:seed`,"store:test":`bun run --filter @jchat/store test`,"store:contracts:test":`bun run --filter @jchat/store-contracts test`,"license:build":`bun run --filter @jchat/license build`,"license:dev":`bun --watch apps/license/server.ts`,"license:start":`bun run --filter @jchat/license start`,"license:db:generate":`bun --filter @jchat/license db:generate`,"license:db:push":`bun --filter @jchat/license db:push`,"license:test":`bun --env-file=apps/license/.env.test test apps/license`,prepare:`git config core.hooksPath .githooks && bun run addon-sdk:build && bun run runtime:build`,predev:`bun run addon-sdk:build && bun run runtime:build`,release:`bun run ./scripts/release.ts`,"version:addon-sdk":`bun run ./scripts/set-addon-artifact-version.ts sdk`,"version:create-addon":`bun run ./scripts/set-addon-artifact-version.ts creator`,"check:translations":`bun ./scripts/check-translations.ts`,"audit:changes":`bun ./scripts/audit-changes.ts`,"update:geoip":`bun ./scripts/update-geoip.ts`,test:`bun --filter="@jchat/chat" run test:api`,"test:api":`bun --env-file=.env.test --filter="@jchat/chat" run test:api`,"test:unit":`bun --filter="@jchat/chat" run test:unit`,"test:packages":`bun test packages/`,"test:store":`bun run --filter @jchat/store test`,"test:license":`bun --env-file=apps/license/.env.test test apps/license`,"test:watch":`bun --filter="@jchat/chat" run test:api:watch`,"test:coverage":`bun --filter="@jchat/chat" run test:api:coverage`,"test:e2e":`playwright test`,"test:e2e:ui":`playwright test --ui`,"benchmark:chat":`bun run ./scripts/benchmark-chat.ts`,pretypecheck:`bun run addon-sdk:build && bun run runtime:build`,typecheck:`bun --filter='*' run typecheck`,lint:`bun --filter='*' run lint`,format:`bun --filter='*' run format`,change:`bun run ./scripts/release-engine/cli.ts`,"version-packages":`bun run ./scripts/release-engine/version.ts`,"release-packages":`bun run ./scripts/release-engine/publish.ts`,"sync-changelogs":`bun run ./scripts/release-engine/sync-changelog.ts`},devDependencies:{"@clack/prompts":`catalog:`,"@playwright/test":`^1.63.0`,"@types/bun":`catalog:`,elysia:`catalog:`,oxfmt:`catalog:`,oxlint:`catalog:`,typescript:`catalog:`},engines:{node:`>=22.22.0`},packageManager:`bun@1.4.0`,catalog:{"@better-auth/api-key":`^1.7.7`,"@better-auth/passkey":`^1.7.7`,"@casl/ability":`^7.0.1`,"@casl/react":`^7.0.1`,"@clack/prompts":`^1.8.1`,"@daypicker/react":`^10.0.2`,"@elysiajs/cors":`^1.4.1`,"@elysiajs/cron":`^1.4.2`,"@elysiajs/eden":`^1.4.9`,"@elysiajs/static":`^1.4.10`,"@react-router/dev":`^8.4.0`,"@react-router/node":`^8.4.0`,"@react-router/serve":`^8.4.0`,"@tailwindcss/vite":`^4.3.3`,"@tanstack/react-query":`^5.104.0`,"@tanstack/react-virtual":`^3.14.13`,"@types/bun":`^1.4.2`,"@types/nodemailer":`^8.0.2`,"@types/react":`^19.3.0`,"@types/react-dom":`^19.3.0`,"better-auth":`^1.7.7`,cn:`^0.4.0`,"date-fns":`4.4.0`,"drizzle-orm":`^0.45.3`,"drizzle-kit":`^0.31.11`,effect:`^4.0.0`,elysia:`^1.4.30`,"elysia-helmet":`^3.0.0`,"elysia-ip":`^1.1.1`,"elysia-rate-limit":`^4.6.3`,motion:`^13.5.0`,nodemailer:`^10.0.13`,i18next:`^26.4.2`,"i18next-browser-languagedetector":`^8.2.1`,isbot:`^5.2.2`,jotai:`^3.0.1`,kysely:`^0.29.6`,"lucide-react":`1.49.0`,oxfmt:`^0.71.0`,oxlint:`^1.86.0`,react:`^19.3.0`,"react-dom":`^19.3.0`,"react-hook-form":`7.54.2`,"react-hot-toast":`^2.6.1`,"react-i18next":`^17.0.15`,"react-router":`^8.4.0`,"tailwind-scrollbar":`^4.0.2`,tailwindcss:`^4.3.3`,tsdown:`0.23.0`,typescript:`^7.0.2`,vite:`^8.3.2`,zod:`^4.6.5`},trustedDependencies:[`@tailwindcss/oxide`,`esbuild`,`unrs-resolver`]};function x(){return b&&`catalog`in b&&typeof b.catalog==`object`&&b.catalog!==null?b.catalog:{}}function S(e){return{name:`${e}-development-kit`,private:!0,type:`module`,workspaces:[`addon`],scripts:{sandbox:`bun run ./scripts/devkit.ts run`,"sandbox:test":`bun run ./scripts/devkit.ts test`,"sandbox:reset":`bun run ./scripts/devkit.ts reset`,"sandbox:package":`bun run ./scripts/devkit.ts package`,"sandbox:cleanup":`bun run ./scripts/devkit.ts cleanup`},devDependencies:{"@types/bun":`^1.4.0`},engines:{bun:`>=1.4.0`}}}function C(e,t){return{name:`@jchat/${e}`,version:`1.0.0-alpha.0`,private:!0,description:`${t} addon for JChat`,repository:{type:`git`,url:`git+https://github.com/jchatcloud/jchat.git`,directory:`addons/${e}`},type:`module`,scripts:{dev:`jchat-addon dev`,build:`tsc --noEmit && jchat-addon build`,test:`bun test`,lint:`oxlint .`,format:`oxfmt . --write`,typecheck:`tsc --noEmit`},dependencies:{},devDependencies:{"@tailwindcss/vite":`catalog:`,"@types/bun":`catalog:`,"@types/react":`catalog:`,"@types/react-dom":`catalog:`,elysia:`catalog:`,vite:`catalog:`},peerDependencies:{"@jchat/addon-sdk":`workspace:*`,"@tanstack/react-query":`catalog:`,react:`catalog:`,"react-dom":`catalog:`}}}function w(e,t){let n=x();return{name:`@jchat/${e}`,version:`1.0.0`,private:!0,type:`module`,scripts:{dev:`jchat-addon dev`,build:`tsc --noEmit && jchat-addon build`,test:`bun test`,typecheck:`tsc --noEmit`},dependencies:{},devDependencies:{"@tailwindcss/vite":n[`@tailwindcss/vite`]??`^4.3.3`,"@types/bun":n[`@types/bun`]??`^1.4.0`,"@types/react":n[`@types/react`]??`^19.2.18`,"@types/react-dom":n[`@types/react-dom`]??`^19.2.4`,elysia:n.elysia??`^1.4.29`,tailwindcss:n.tailwindcss??`^4.3.3`,vite:n.vite??`^8.2.1`},peerDependencies:{"@jchat/addon-sdk":`^${t}`,"@tanstack/react-query":n[`@tanstack/react-query`]??`^5.101.4`,react:n.react??`^19.2.8`,"react-dom":n[`react-dom`]??`^19.2.8`}}}function T(){return{extends:`@jchat/addon-sdk/tsconfig.json`,include:[`src/**/*`]}}function E(){return{extends:`@jchat/addon-sdk/tsconfig.json`,include:[`src/**/*.ts`,`src/**/*.tsx`]}}function D(){return`---
|
|
464
|
+
name: jchat-addon-development
|
|
465
|
+
description: Comprehensive guide for developing JChat addons using @jchat/addon-sdk, including manifests, UI widgets, direct icon imports, Elysia backend, isolated DB schemas, and i18n.
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
# JChat Addon Development Skill
|
|
469
|
+
|
|
470
|
+
Use this skill when developing, updating, or debugging JChat addons using \`@jchat/addon-sdk\`.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## 1. Architectural Guardrails (Non-Negotiable)
|
|
475
|
+
|
|
476
|
+
1. **Import Boundaries**:
|
|
477
|
+
- Addons MUST ONLY import \`@jchat/addon-sdk\`, \`@jchat/addon-sdk/ui\` (components & toast), \`@jchat/addon-sdk/state\` (atomic state), \`@jchat/addon-sdk/icon\`, and \`@jchat/addon-sdk/server\`.
|
|
478
|
+
- NEVER import private internal monorepo packages (\`@jchat/icon\`, \`@jchat/ui\`, \`@jchat/db\`, \`@jchat/shared\`, etc.).
|
|
479
|
+
- Host externals (\`react\`, \`react-dom\`, \`@tanstack/react-query\`) are provided at runtime by the JChat host.
|
|
480
|
+
2. **Icons**:
|
|
481
|
+
- Use direct imports: \`import Icons from "@jchat/addon-sdk/icon"\`.
|
|
482
|
+
- Never call \`useIcons()\` or install \`lucide-react\` / \`react-icons\` directly in the addon.
|
|
483
|
+
3. **Database Isolation**:
|
|
484
|
+
- All addon tables MUST reside in the isolated \`addons\` PostgreSQL schema (e.g. \`addons.GlimpseStories\`).
|
|
485
|
+
- Never write to or alter core tables (\`Users\`, \`Rooms\`, \`Messages\`, \`Sites\`).
|
|
486
|
+
4. **Real-Time Event Namespacing**:
|
|
487
|
+
- All WebSocket/Redis events MUST follow the canonical format: \`addon:<addonId>:<eventName>\` (e.g. \`addon:pollwise:voted\`).
|
|
488
|
+
5. **Localization (i18n)**:
|
|
489
|
+
- All addons must support all 20 host languages:
|
|
490
|
+
\`ar\`, \`bg\`, \`de\`, \`el\`, \`en\`, \`es\`, \`fr\`, \`he\`, \`hi\`, \`hr\`, \`kn\`, \`ml\`, \`nl\`, \`pt\`, \`ro\`, \`ru\`, \`ta\`, \`te\`, \`tr\`, \`ur\`.
|
|
491
|
+
- All locale dictionaries in \`src/locales/*.json\` must maintain 100% key parity.
|
|
492
|
+
6. **Directional Styling (RTL/LTR)**:
|
|
493
|
+
- Use Tailwind CSS logical properties (\`ps-*\`, \`pe-*\`, \`ms-*\`, \`me-*\`, \`text-start\`, \`text-end\`, \`start-*\`, \`end-*\`, \`rounded-s-*\`, \`rounded-e-*\`).
|
|
494
|
+
- Never use physical \`pl-*\`, \`pr-*\`, \`ml-*\`, \`mr-*\`, \`text-left\`, or \`text-right\`.
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 2. Manifest Contract (\`src/manifest.ts\`)
|
|
499
|
+
|
|
500
|
+
Define addon metadata and capabilities with \`defineAddonManifest\`:
|
|
501
|
+
|
|
502
|
+
\`\`\`typescript
|
|
503
|
+
import { defineAddonManifest } from "@jchat/addon-sdk"
|
|
504
|
+
|
|
505
|
+
export default defineAddonManifest({
|
|
506
|
+
id: "pollwise",
|
|
507
|
+
name: "Pollwise Polls",
|
|
508
|
+
version: "1.0.0",
|
|
509
|
+
description: "Create and vote on live interactive polls in chat rooms.",
|
|
510
|
+
author: "JChat Ecosystem Team",
|
|
511
|
+
compatibility: {
|
|
512
|
+
jchat: ">=1.0.0-alpha.0 <2.0.0",
|
|
513
|
+
addonSdk: ">=1.0.0-alpha.0 <2.0.0"
|
|
514
|
+
},
|
|
515
|
+
locales: [
|
|
516
|
+
"ar", "bg", "de", "el", "en", "es", "fr", "he", "hi", "hr",
|
|
517
|
+
"kn", "ml", "nl", "pt", "ro", "ru", "ta", "te", "tr", "ur"
|
|
518
|
+
],
|
|
519
|
+
defaultLocale: "en",
|
|
520
|
+
frontend: {
|
|
521
|
+
bundle: "./ui.js",
|
|
522
|
+
mountPoints: ["chat-toolbar", "room-settings"]
|
|
523
|
+
},
|
|
524
|
+
backend: {
|
|
525
|
+
bundle: "./server.js"
|
|
526
|
+
},
|
|
527
|
+
permissions: ["network", "storage"]
|
|
528
|
+
})
|
|
529
|
+
\`\`\`
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## 3. Frontend UI Development (\`src/ui.tsx\`)
|
|
534
|
+
|
|
535
|
+
Addon UI components receive \`AddonProps\` and use hooks from \`@jchat/addon-sdk\`:
|
|
536
|
+
|
|
537
|
+
\`\`\`tsx
|
|
538
|
+
import {
|
|
539
|
+
type AddonProps,
|
|
540
|
+
useAddonSDK,
|
|
541
|
+
useAddonQuery,
|
|
542
|
+
useAddonMutation,
|
|
543
|
+
useAddonTranslation
|
|
544
|
+
} from "@jchat/addon-sdk"
|
|
545
|
+
import Icons from "@jchat/addon-sdk/icon"
|
|
546
|
+
|
|
547
|
+
export default function PollwiseAddon({ mountPoint, roomId }: AddonProps) {
|
|
548
|
+
const sdk = useAddonSDK()
|
|
549
|
+
const { t } = useAddonTranslation()
|
|
550
|
+
|
|
551
|
+
// 1. Data fetching via TanStack Query bridge
|
|
552
|
+
const { data: poll, isLoading } = useAddonQuery({
|
|
553
|
+
queryKey: ["poll", roomId],
|
|
554
|
+
queryFn: () => sdk.api.get<{ id: string; question: string }>(\`/rooms/\${roomId}/poll\`)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
// 2. Mutations
|
|
558
|
+
const voteMutation = useAddonMutation({
|
|
559
|
+
mutationFn: (optionId: string) => sdk.api.post(\`/rooms/\${roomId}/vote\`, { optionId }),
|
|
560
|
+
onSuccess: () => {
|
|
561
|
+
sdk.ui.toast({
|
|
562
|
+
title: t("vote_success_title"),
|
|
563
|
+
description: t("vote_success_desc"),
|
|
564
|
+
variant: "success"
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
// 3. Real-time event listener
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
const unsubscribe = sdk.events.on("addon:pollwise:updated", (updatedPoll) => {
|
|
572
|
+
// Handle incoming real-time payload
|
|
573
|
+
})
|
|
574
|
+
return () => unsubscribe()
|
|
575
|
+
}, [sdk])
|
|
576
|
+
|
|
577
|
+
if (isLoading) return <div>{t("loading")}</div>
|
|
578
|
+
|
|
579
|
+
return (
|
|
580
|
+
<div className="p-4 rounded-xl bg-card border border-border">
|
|
581
|
+
<div className="flex items-center gap-2 mb-3">
|
|
582
|
+
<Icons.BarChart3 className="size-5 text-primary" />
|
|
583
|
+
<h3 className="font-semibold text-foreground">{poll?.question}</h3>
|
|
584
|
+
</div>
|
|
585
|
+
<button
|
|
586
|
+
onClick={() => voteMutation.mutate("option-1")}
|
|
587
|
+
className="px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
588
|
+
>
|
|
589
|
+
{t("vote_button")}
|
|
590
|
+
</button>
|
|
591
|
+
</div>
|
|
592
|
+
)
|
|
404
593
|
}
|
|
405
|
-
|
|
406
|
-
|
|
594
|
+
\`\`\`
|
|
595
|
+
|
|
596
|
+
### Direct Icon Usage
|
|
597
|
+
Icons are rendered directly:
|
|
598
|
+
\`\`\`tsx
|
|
599
|
+
import Icons, { resolveIcon } from "@jchat/addon-sdk/icon"
|
|
600
|
+
|
|
601
|
+
// Direct JSX:
|
|
602
|
+
<Icons.Sparkles className="size-4 text-primary" />
|
|
603
|
+
<Icons.Send className="size-5" />
|
|
604
|
+
|
|
605
|
+
// Dynamic resolution:
|
|
606
|
+
const DynamicIcon = resolveIcon("Smile", Icons.HelpCircle)
|
|
607
|
+
<DynamicIcon className="size-4" />
|
|
608
|
+
\`\`\`
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## 4. Backend Server Development (\`src/server.ts\`)
|
|
613
|
+
|
|
614
|
+
Fullstack addons expose ElysiaJS routes running within the JChat server runtime:
|
|
615
|
+
|
|
616
|
+
\`\`\`typescript
|
|
617
|
+
import { createAddonServer } from "@jchat/addon-sdk/server"
|
|
618
|
+
import { t } from "elysia"
|
|
619
|
+
|
|
620
|
+
export default createAddonServer("pollwise", ({ app, db, events }) => {
|
|
621
|
+
return app
|
|
622
|
+
.get("/rooms/:roomId/poll", async ({ params: { roomId } }) => {
|
|
623
|
+
// Query isolated addons schema using Kysely
|
|
624
|
+
const poll = await db
|
|
625
|
+
.selectFrom("addons.Polls")
|
|
626
|
+
.selectAll()
|
|
627
|
+
.where("roomId", "=", roomId)
|
|
628
|
+
.executeTakeFirst()
|
|
629
|
+
|
|
630
|
+
return poll ?? { message: "No active poll" }
|
|
631
|
+
})
|
|
632
|
+
.post(
|
|
633
|
+
"/rooms/:roomId/vote",
|
|
634
|
+
async ({ params: { roomId }, body, user }) => {
|
|
635
|
+
// Record vote in DB
|
|
636
|
+
await db
|
|
637
|
+
.insertInto("addons.PollVotes")
|
|
638
|
+
.values({
|
|
639
|
+
pollId: body.pollId,
|
|
640
|
+
userId: user.id,
|
|
641
|
+
optionId: body.optionId
|
|
642
|
+
})
|
|
643
|
+
.execute()
|
|
644
|
+
|
|
645
|
+
// Broadcast real-time event to room subscribers
|
|
646
|
+
await events.publish(\`addon:pollwise:updated\`, {
|
|
647
|
+
roomId,
|
|
648
|
+
pollId: body.pollId
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
return { success: true }
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
body: t.Object({
|
|
655
|
+
pollId: t.String(),
|
|
656
|
+
optionId: t.String()
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
)
|
|
660
|
+
})
|
|
661
|
+
\`\`\`
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## 5. Addon Localization (\`src/locales/\`)
|
|
666
|
+
|
|
667
|
+
Every translatable string in the UI must use \`t("key")\`.
|
|
668
|
+
Translations reside in \`src/locales/<lang>.json\` for all 20 supported languages:
|
|
669
|
+
|
|
670
|
+
\`\`\`json
|
|
671
|
+
// src/locales/en.json
|
|
672
|
+
{
|
|
673
|
+
"welcome": "Welcome to Pollwise",
|
|
674
|
+
"vote_button": "Submit Vote",
|
|
675
|
+
"vote_success_title": "Vote Recorded",
|
|
676
|
+
"vote_success_desc": "Thank you for participating."
|
|
407
677
|
}
|
|
408
|
-
|
|
409
|
-
|
|
678
|
+
\`\`\`
|
|
679
|
+
|
|
680
|
+
Validate key parity across all 20 languages:
|
|
681
|
+
\`\`\`bash
|
|
682
|
+
bun check:translations
|
|
683
|
+
\`\`\`
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## 6. Build and Verification Workflow
|
|
688
|
+
|
|
689
|
+
\`\`\`bash
|
|
690
|
+
# 1. Typecheck the addon
|
|
691
|
+
bun run typecheck
|
|
692
|
+
|
|
693
|
+
# 2. Bundle the addon artifacts (frontend bundle into ui.js, backend into server.js)
|
|
694
|
+
bun run build
|
|
695
|
+
|
|
696
|
+
# 3. In the standalone devkit: start the disposable sandbox environment
|
|
697
|
+
bun run devkit:start
|
|
698
|
+
|
|
699
|
+
# 4. View sandbox logs
|
|
700
|
+
bun run devkit:logs
|
|
701
|
+
\`\`\`
|
|
702
|
+
`}async function O(e,t,i=`Target already exists`){let a=r.resolve(e);if(n.existsSync(a))throw Error(`${i}: ${a}`);let o=Object.entries(t);for(let[e]of o){let t=r.join(a,...e.split(`/`));n.mkdirSync(r.dirname(t),{recursive:!0})}await Promise.all(o.map(([e,t])=>{let n=r.join(a,...e.split(`/`));return Bun.write(n,t)}))}const k=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;function A(e){return e.split(`-`).map(e=>`${e[0]?.toUpperCase()??``}${e.slice(1)}`).join(` `)}function j(n){if(!k.test(n.addonId))throw Error(`Addon id must contain lowercase letters, numbers, and hyphens`);let r=(n.template??`ui`)===`fullstack`,l=n.displayName?.trim()||A(n.addonId),u=n.mountPoints&&n.mountPoints.length>0?n.mountPoints:[e.CHAT_TOOLBAR,e.CHAT_PLUS_MENU],d={id:n.addonId,name:l,version:`1.0.0`,description:`${l} addon for JChat`,logo:`./assets/logo.svg`,author:`Developer`,compatibility:{jchat:i(n.jchatVersion),addonSdk:i(n.addonSdkVersion)},locales:[...t],defaultLocale:`en`,frontend:{bundle:`./ui.js`,mountPoints:u},...r?{server:{bundle:`./api.ts`,migrations:[`migrations/001_initial.sql`]},permissions:[`addon.${n.addonId}.use`,`addon.${n.addonId}.manage`],permissionDefinitions:[{name:`addon.${n.addonId}.use`,label:`Use ${l}`,description:`Access and use ${l} in chat`},{name:`addon.${n.addonId}.manage`,label:`Manage ${l}`,description:`Configure and moderate ${l} settings`}]}:{permissions:[]}},f=n.isMonorepoWorkspace?C(n.addonId,l):w(n.addonId,n.addonSdkVersion),p=n.isMonorepoWorkspace?T():E(),m={"package.json":`${JSON.stringify(f,null,2)}\n`,"tsconfig.json":`${JSON.stringify(p,null,2)}\n`,"assets/logo.svg":_(l),"src/manifest.ts":v(d),"src/tailwind.css":y(),...g(l),"src/ui.tsx":c(l,u,r),".agents/skills/jchat-addon-development/SKILL.md":D()};return r&&(m[`src/db.ts`]=a(n.addonId),m[`src/api.ts`]=s(n.addonId),m[`migrations/001_initial.sql`]=o(n.addonId)),Object.freeze(m)}async function M(e){await O(e.targetDir,j(e),`Addon target already exists`)}function N(e){if(!k.test(e.addonId))throw Error(`Addon id must contain lowercase letters, numbers, and hyphens`);let t=e.sandboxImage??`ghcr.io/jchatcloud/jchat-sandbox:${e.jchatVersion.replace(/\+/g,`-`)}`,n=e.displayName?.trim()||A(e.addonId),r=j({addonId:e.addonId,isMonorepoWorkspace:!1,jchatVersion:e.jchatVersion,addonSdkVersion:e.addonSdkVersion,template:e.template,mountPoints:e.mountPoints,displayName:e.displayName}),i={".env.example":l(e.addonId,t),".gitignore":[`node_modules/`,`addon/node_modules/`,`addon/build/`,`.env`,``].join(`
|
|
703
|
+
`),"README.md":f(e.addonId,n),"docker-compose.yml":u(),"package.json":`${JSON.stringify(S(e.addonId),null,2)}\n`,"scripts/devkit.ts":d(),".agents/skills/jchat-addon-development/SKILL.md":D()};for(let[e,t]of Object.entries(r))i[`addon/${e}`]=t;return Object.freeze(i)}async function P(e){await O(e.targetDir,N(e),`Development kit target already exists`)}export{P as a,M as i,N as n,A as r,j as t};
|