@jchat/addon-sdk 0.0.1 → 1.0.0-alpha.10
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 +64 -0
- package/LICENSE +18 -196
- package/README.md +26 -2
- package/dist/cli.d.mts +36 -0
- package/dist/cli.mjs +135 -0
- package/dist/contracts.d.mts +16 -24
- package/dist/diagnostics.d.mts +47 -0
- package/dist/diagnostics.mjs +175 -1
- package/dist/index.d.mts +282 -8
- package/dist/index.mjs +481 -1
- package/dist/ui.d.mts +1 -0
- package/dist/ui.mjs +2 -0
- package/dist/vite.d.mts +63 -11
- package/dist/vite.mjs +335 -4
- package/package.json +45 -12
- package/tsconfig.addon.json +20 -0
package/dist/index.mjs
CHANGED
|
@@ -1 +1,481 @@
|
|
|
1
|
-
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import * as fs$1 from "node:fs";
|
|
3
|
+
import * as path$1 from "node:path";
|
|
4
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
//#region package.json
|
|
6
|
+
var version = "1.0.0-alpha.10";
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region ../icon/dist/types.mjs
|
|
9
|
+
const e = `Activity.ArrowLeft.ArrowRight.Ban.Bell.BookOpen.Brush.Calendar.Camera.ChartBar.Check.CheckCheck.CheckCircle2.ChevronDown.ChevronLeft.ChevronRight.ChevronUp.CircleAlert.CircleCheck.CircleOff.CirclePlay.CirclePlus.Clock.Coffee.Contact.Cpu.Database.Dices.Ellipsis.Eraser.ExternalLink.Eye.EyeOff.Flag.Flame.FolderPlus.Footprints.Gamepad2.Gift.Globe.HardDrive.Headphones.Heart.Image.Info.Key.KeyRound.Layers.LayoutDashboard.LayoutList.LayoutTemplate.Link2.ListOrdered.Loader.LoaderCircle.Lock.LogOut.Mail.Maximize.Megaphone.MemoryStick.Menu.MessageCircle.MessageSquare.Mic.Minimize.Minus.Monitor.Moon.MoreHorizontal.Music.Music2.Newspaper.Palette.Paperclip.PartyPopper.Pause.PenLine.Play.Plus.Radio.Reply.RotateCcw.RotateCw.Search.Send.Server.Settings.Shield.ShieldAlert.ShieldCheck.SkipForward.SlidersHorizontal.Smartphone.Smile.Sparkles.Square.SquarePen.Star.Sun.ThumbsDown.ThumbsUp.Timer.Trash.Trash2.Trophy.Upload.User.UserCheck.UserCog.UserMinus.UserPen.UserPlus.UserRoundPlus.Users.UserX.Utensils.Video.VideoOff.Volume.Volume2.VolumeX.X.Youtube.Zap`.split(`.`);
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/contracts.ts
|
|
12
|
+
/** Valid locations where an addon can be rendered within the Jchat layout. */
|
|
13
|
+
const MOUNT_POINTS = {
|
|
14
|
+
/** Top of the main chat column */
|
|
15
|
+
CHAT_HEADER: "chat-header",
|
|
16
|
+
/** Below the main chat header, above messages */
|
|
17
|
+
CHAT_TOOLBAR: "chat-toolbar",
|
|
18
|
+
/** The persistent right-side sidebar */
|
|
19
|
+
CHAT_SIDEBAR: "chat-sidebar",
|
|
20
|
+
/** Top of the left sidebar (room list) */
|
|
21
|
+
SIDEBAR_TOP: "sidebar-top",
|
|
22
|
+
/** Bottom of the left sidebar */
|
|
23
|
+
SIDEBAR_BOTTOM: "sidebar-bottom",
|
|
24
|
+
/** Action list shown when clicking a user's avatar */
|
|
25
|
+
USER_POPOVER_ACTIONS: "user-popover-actions",
|
|
26
|
+
/** Menu shown when right-clicking a message */
|
|
27
|
+
MESSAGE_CONTEXT_MENU: "message-context-menu",
|
|
28
|
+
/** Floating overlay rendered on top of the message stream */
|
|
29
|
+
MESSAGE_FEED_OVERLAY: "message-feed-overlay",
|
|
30
|
+
/** Main admin dashboard area */
|
|
31
|
+
ADMIN_PANEL: "admin-panel",
|
|
32
|
+
/** User or site settings dashboard area */
|
|
33
|
+
SETTINGS_PANEL: "settings-panel",
|
|
34
|
+
/** The 'plus' (+) menu in the message composer */
|
|
35
|
+
CHAT_PLUS_MENU: "chat-plus-menu",
|
|
36
|
+
/** Custom tab in the emoji/gif selection panel */
|
|
37
|
+
EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab",
|
|
38
|
+
/** Custom tab in the emoji/sticker selection panel */
|
|
39
|
+
EMOJI_PICKER_STICKER_TAB: "emoji-picker-sticker-tab",
|
|
40
|
+
/** Full-screen or modal dialog specifically for TubeScan */
|
|
41
|
+
TUBESCAN_DIALOG: "tubescan-dialog",
|
|
42
|
+
/** Dedicated settings panel rendered in the Owner Dashboard */
|
|
43
|
+
OWNER_SETTINGS: "owner-settings",
|
|
44
|
+
/** Dedicated settings panel rendered in the Site Admin Dashboard */
|
|
45
|
+
ADDON_SETTINGS: "addon-settings"
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/i18n.ts
|
|
49
|
+
function checkIsRtl(code) {
|
|
50
|
+
if (!code) return false;
|
|
51
|
+
const normalized = code.toLowerCase().split("-")[0];
|
|
52
|
+
return normalized === "ar" || normalized === "he" || normalized === "ur";
|
|
53
|
+
}
|
|
54
|
+
function getLanguageDirection(code) {
|
|
55
|
+
return checkIsRtl(code) ? "rtl" : "ltr";
|
|
56
|
+
}
|
|
57
|
+
function interpolate(text, params) {
|
|
58
|
+
if (!params) return text;
|
|
59
|
+
return text.replace(/\{\{\s*(\w+)\s*\}\}|\{\s*(\w+)\s*\}/g, (match, key1, key2) => {
|
|
60
|
+
const key = key1 || key2;
|
|
61
|
+
return key in params ? String(params[key]) : match;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const HOST_LANGUAGES = [
|
|
65
|
+
"ar",
|
|
66
|
+
"bg",
|
|
67
|
+
"hr",
|
|
68
|
+
"de",
|
|
69
|
+
"el",
|
|
70
|
+
"en",
|
|
71
|
+
"es",
|
|
72
|
+
"fr",
|
|
73
|
+
"he",
|
|
74
|
+
"hi",
|
|
75
|
+
"kn",
|
|
76
|
+
"ml",
|
|
77
|
+
"nl",
|
|
78
|
+
"pt",
|
|
79
|
+
"ro",
|
|
80
|
+
"ru",
|
|
81
|
+
"ta",
|
|
82
|
+
"te",
|
|
83
|
+
"tr",
|
|
84
|
+
"ur"
|
|
85
|
+
];
|
|
86
|
+
function extractNestedKeys(obj, prefix = "") {
|
|
87
|
+
if (!obj || typeof obj !== "object") return [];
|
|
88
|
+
const keys = [];
|
|
89
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
90
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
91
|
+
if (value && typeof value === "object" && !Array.isArray(value)) keys.push(...extractNestedKeys(value, fullKey));
|
|
92
|
+
else keys.push(fullKey);
|
|
93
|
+
}
|
|
94
|
+
return keys;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validates that an addon's `locales/` directory contains valid translation files for all 11 host
|
|
98
|
+
* languages with 100% key parity against `en.json`.
|
|
99
|
+
*
|
|
100
|
+
* @param dirPath - Path to the addon's locales directory.
|
|
101
|
+
* @param domainName - Optional label for error messages (defaults to "Addon").
|
|
102
|
+
*/
|
|
103
|
+
function validateAddonLocales(dirPath, domainName = "Addon") {
|
|
104
|
+
const errors = [];
|
|
105
|
+
const missingFiles = [];
|
|
106
|
+
const missingKeys = {};
|
|
107
|
+
const extraKeys = {};
|
|
108
|
+
const enPath = path$1.join(dirPath, "en.json");
|
|
109
|
+
if (!fs$1.existsSync(enPath)) {
|
|
110
|
+
const errorMsg = `[${domainName}] Missing base locale file: en.json`;
|
|
111
|
+
errors.push(errorMsg);
|
|
112
|
+
missingFiles.push("en.json");
|
|
113
|
+
return {
|
|
114
|
+
valid: false,
|
|
115
|
+
errors,
|
|
116
|
+
missingFiles,
|
|
117
|
+
missingKeys,
|
|
118
|
+
extraKeys
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
let enContent;
|
|
122
|
+
try {
|
|
123
|
+
enContent = JSON.parse(fs$1.readFileSync(enPath, "utf-8"));
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const errorMsg = `[${domainName}] Invalid JSON in en.json: ${err instanceof Error ? err.message : String(err)}`;
|
|
126
|
+
errors.push(errorMsg);
|
|
127
|
+
return {
|
|
128
|
+
valid: false,
|
|
129
|
+
errors,
|
|
130
|
+
missingFiles,
|
|
131
|
+
missingKeys,
|
|
132
|
+
extraKeys
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const enKeys = new Set(extractNestedKeys(enContent));
|
|
136
|
+
for (const lang of HOST_LANGUAGES) {
|
|
137
|
+
if (lang === "en") continue;
|
|
138
|
+
const langFileName = `${lang}.json`;
|
|
139
|
+
const langPath = path$1.join(dirPath, langFileName);
|
|
140
|
+
if (!fs$1.existsSync(langPath)) {
|
|
141
|
+
errors.push(`[${domainName}] Missing locale file: ${langFileName}`);
|
|
142
|
+
missingFiles.push(langFileName);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const langContent = JSON.parse(fs$1.readFileSync(langPath, "utf-8"));
|
|
147
|
+
const langKeys = new Set(extractNestedKeys(langContent));
|
|
148
|
+
for (const key of enKeys) if (!langKeys.has(key)) {
|
|
149
|
+
errors.push(`[${domainName}] Missing key in ${langFileName}: "${key}"`);
|
|
150
|
+
if (!missingKeys[lang]) missingKeys[lang] = [];
|
|
151
|
+
missingKeys[lang].push(key);
|
|
152
|
+
}
|
|
153
|
+
for (const key of langKeys) if (!enKeys.has(key)) {
|
|
154
|
+
errors.push(`[${domainName}] Extra key in ${langFileName} (not in en.json): "${key}"`);
|
|
155
|
+
if (!extraKeys[lang]) extraKeys[lang] = [];
|
|
156
|
+
extraKeys[lang].push(key);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
errors.push(`[${domainName}] Invalid JSON in ${langFileName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
valid: errors.length === 0,
|
|
164
|
+
errors,
|
|
165
|
+
missingFiles,
|
|
166
|
+
missingKeys,
|
|
167
|
+
extraKeys
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const LocaleContext = createContext({
|
|
171
|
+
locale: "en",
|
|
172
|
+
dir: "ltr"
|
|
173
|
+
});
|
|
174
|
+
/** Resolves a nested key in an object dictionary using dot-notation (e.g. "buttons.submit") */
|
|
175
|
+
function resolveNestedKey(obj, keyPath) {
|
|
176
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
177
|
+
const segments = keyPath.split(".");
|
|
178
|
+
let current = obj;
|
|
179
|
+
for (const seg of segments) if (current && typeof current === "object" && seg in current) current = current[seg];
|
|
180
|
+
else return;
|
|
181
|
+
return current;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Creates a standalone translation function for backend server routes (`src/api.ts`) or utilities.
|
|
185
|
+
*
|
|
186
|
+
* @param resources - Dictionary of translation resources (e.g., `{ en: enJson, ta: taJson }`).
|
|
187
|
+
* @param defaultLocale - Default locale to use if not specified per call (defaults to "en").
|
|
188
|
+
* @param fallbackLocale - Fallback locale if a key is missing in the target language (defaults to
|
|
189
|
+
* "en").
|
|
190
|
+
*/
|
|
191
|
+
function createTranslator(resources = {}, defaultLocale = "en", fallbackLocale = "en") {
|
|
192
|
+
return (key, paramsOrLang, maybeLang) => {
|
|
193
|
+
let params;
|
|
194
|
+
let lang = defaultLocale;
|
|
195
|
+
if (typeof paramsOrLang === "string") lang = paramsOrLang;
|
|
196
|
+
else if (paramsOrLang && typeof paramsOrLang === "object") {
|
|
197
|
+
params = paramsOrLang;
|
|
198
|
+
if (typeof maybeLang === "string") lang = maybeLang;
|
|
199
|
+
}
|
|
200
|
+
const normalizedLang = (lang || defaultLocale).toLowerCase().split("-")[0];
|
|
201
|
+
const normalizedFallback = fallbackLocale.toLowerCase().split("-")[0];
|
|
202
|
+
const langDict = resources[normalizedLang];
|
|
203
|
+
let resolved = resolveNestedKey(langDict, key);
|
|
204
|
+
if (resolved === void 0 && normalizedLang !== normalizedFallback) {
|
|
205
|
+
const fallbackDict = resources[normalizedFallback];
|
|
206
|
+
resolved = resolveNestedKey(fallbackDict, key);
|
|
207
|
+
}
|
|
208
|
+
if (typeof resolved === "string") return interpolate(resolved, params);
|
|
209
|
+
return typeof resolved === "number" || typeof resolved === "boolean" ? String(resolved) : interpolate(key, params);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Hook to access the current active host locale and text direction in an addon component.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ;```tsx
|
|
217
|
+
* import { useLocale } from "@jchat/addon-sdk"
|
|
218
|
+
*
|
|
219
|
+
* export default function MyAddon() {
|
|
220
|
+
* const { locale, dir, isRtl } = useLocale()
|
|
221
|
+
* return <div dir={dir}>Current language: {locale}</div>
|
|
222
|
+
* }
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
function useLocale() {
|
|
226
|
+
const context = useContext(LocaleContext);
|
|
227
|
+
const locale = context?.locale || "en";
|
|
228
|
+
const dir = context?.dir || getLanguageDirection(locale);
|
|
229
|
+
return {
|
|
230
|
+
locale,
|
|
231
|
+
dir,
|
|
232
|
+
isRtl: dir === "rtl" || checkIsRtl(locale)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const loadedChunkCache = {};
|
|
236
|
+
/**
|
|
237
|
+
* Hook to access translation function and locale metadata inside addon components. Automatically
|
|
238
|
+
* synchronizes with host language switches and falls back to English. Supports static dictionary
|
|
239
|
+
* resources or dynamic async locale loaders.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ;```tsx
|
|
243
|
+
* import { useTranslation } from "@jchat/addon-sdk"
|
|
244
|
+
* import locales from "./locales"
|
|
245
|
+
*
|
|
246
|
+
* export default function MyAddon() {
|
|
247
|
+
* const { t, locale, dir } = useTranslation(locales)
|
|
248
|
+
* return (
|
|
249
|
+
* <div dir={dir}>
|
|
250
|
+
* <h1>{t("welcome", { name: "Guest" })}</h1>
|
|
251
|
+
* </div>
|
|
252
|
+
* )
|
|
253
|
+
* }
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* @param resourcesOrLoader - Addon-scoped translation dictionaries or async chunk loader.
|
|
257
|
+
* @param fallbackLocale - Fallback language code when a string is missing (default: "en").
|
|
258
|
+
*/
|
|
259
|
+
function useTranslation(resourcesOrLoader = {}, fallbackLocale = "en") {
|
|
260
|
+
const { locale, dir, isRtl } = useLocale();
|
|
261
|
+
const isLoader = typeof resourcesOrLoader === "function";
|
|
262
|
+
const [dynamicResources, setDynamicResources] = useState(() => {
|
|
263
|
+
return isLoader ? loadedChunkCache : resourcesOrLoader;
|
|
264
|
+
});
|
|
265
|
+
const [isReady, setIsReady] = useState(!isLoader);
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (!isLoader) {
|
|
268
|
+
setDynamicResources(resourcesOrLoader);
|
|
269
|
+
setIsReady(true);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const loader = resourcesOrLoader;
|
|
273
|
+
const normLocale = locale.toLowerCase().split("-")[0];
|
|
274
|
+
if (loadedChunkCache[normLocale]) {
|
|
275
|
+
setDynamicResources((prev) => ({
|
|
276
|
+
...prev,
|
|
277
|
+
[normLocale]: loadedChunkCache[normLocale]
|
|
278
|
+
}));
|
|
279
|
+
setIsReady(true);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
let isMounted = true;
|
|
283
|
+
Promise.resolve(loader(normLocale)).then((dict) => {
|
|
284
|
+
if (!isMounted || !dict) return;
|
|
285
|
+
loadedChunkCache[normLocale] = dict;
|
|
286
|
+
setDynamicResources((prev) => ({
|
|
287
|
+
...prev,
|
|
288
|
+
[normLocale]: dict
|
|
289
|
+
}));
|
|
290
|
+
setIsReady(true);
|
|
291
|
+
}).catch((err) => {
|
|
292
|
+
console.warn(`[addon-sdk] Failed to dynamically load locale chunk "${normLocale}":`, err);
|
|
293
|
+
if (isMounted) setIsReady(true);
|
|
294
|
+
});
|
|
295
|
+
return () => {
|
|
296
|
+
isMounted = false;
|
|
297
|
+
};
|
|
298
|
+
}, [
|
|
299
|
+
isLoader,
|
|
300
|
+
resourcesOrLoader,
|
|
301
|
+
locale
|
|
302
|
+
]);
|
|
303
|
+
const translator = useMemo(() => {
|
|
304
|
+
return createTranslator(dynamicResources, locale, fallbackLocale);
|
|
305
|
+
}, [
|
|
306
|
+
dynamicResources,
|
|
307
|
+
locale,
|
|
308
|
+
fallbackLocale
|
|
309
|
+
]);
|
|
310
|
+
return {
|
|
311
|
+
t: useMemo(() => {
|
|
312
|
+
return (key, params) => translator(key, params, locale);
|
|
313
|
+
}, [translator, locale]),
|
|
314
|
+
locale,
|
|
315
|
+
dir,
|
|
316
|
+
isRtl,
|
|
317
|
+
isReady
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/query.ts
|
|
322
|
+
/**
|
|
323
|
+
* Perform a declarative, cached GET request using the host's shared React Query instance.
|
|
324
|
+
* Automatically injects authentication headers and prefixes addon paths.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ;```tsx
|
|
328
|
+
* const { data, isLoading } = useAddonQuery<{ polls: Poll[] }>({
|
|
329
|
+
* path: "/polls",
|
|
330
|
+
* queryKey: [siteId]
|
|
331
|
+
* })
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
function useAddonQuery(options) {
|
|
335
|
+
const sdk = useAddonSDK();
|
|
336
|
+
const { path, queryKey = [], queryFn, ...queryOptions } = options;
|
|
337
|
+
const effectiveKey = path !== void 0 ? [path, ...queryKey] : queryKey;
|
|
338
|
+
const effectiveFn = queryFn ?? (() => {
|
|
339
|
+
if (!path) throw new Error("useAddonQuery requires either a `path` or a custom `queryFn`");
|
|
340
|
+
return sdk.api.get(path);
|
|
341
|
+
});
|
|
342
|
+
return useQuery({
|
|
343
|
+
...queryOptions,
|
|
344
|
+
queryKey: effectiveKey,
|
|
345
|
+
queryFn: effectiveFn
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Perform a paginated or infinite scrolling request using the host's shared React Query instance.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ;```tsx
|
|
353
|
+
* const { data, fetchNextPage, hasNextPage } = useAddonInfiniteQuery<GifResponse, Error, number>({
|
|
354
|
+
* path: (page) => `/trending?offset=${page * 20}`,
|
|
355
|
+
* initialPageParam: 0,
|
|
356
|
+
* getNextPageParam: (lastPage, allPages) => (lastPage.hasMore ? allPages.length : undefined)
|
|
357
|
+
* })
|
|
358
|
+
* ```
|
|
359
|
+
*/
|
|
360
|
+
function useAddonInfiniteQuery(options) {
|
|
361
|
+
const sdk = useAddonSDK();
|
|
362
|
+
const { path, queryKey = [], queryFn, ...infiniteOptions } = options;
|
|
363
|
+
const effectiveKey = [typeof path === "string" ? path : "infinite", ...queryKey];
|
|
364
|
+
const effectiveFn = queryFn ?? (async (context) => {
|
|
365
|
+
const pageParam = context.pageParam;
|
|
366
|
+
const resolvedPath = typeof path === "function" ? path(pageParam) : path;
|
|
367
|
+
if (!resolvedPath) throw new Error("useAddonInfiniteQuery requires either a `path` or a custom `queryFn`");
|
|
368
|
+
return sdk.api.get(resolvedPath);
|
|
369
|
+
});
|
|
370
|
+
return useInfiniteQuery({
|
|
371
|
+
...infiniteOptions,
|
|
372
|
+
queryKey: effectiveKey,
|
|
373
|
+
queryFn: effectiveFn
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Perform a declarative mutation using the host's shared React Query instance. Automatically
|
|
378
|
+
* executes the request and can invalidate specified query keys upon success.
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ;```tsx
|
|
382
|
+
* const { mutate, isPending } = useAddonMutation<Poll, CreatePollInput>({
|
|
383
|
+
* path: "/polls",
|
|
384
|
+
* method: "POST",
|
|
385
|
+
* invalidateQueries: [["/polls"]],
|
|
386
|
+
* onSuccess: () => sdk.toast("Poll created!", "success")
|
|
387
|
+
* })
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
function useAddonMutation(options) {
|
|
391
|
+
const sdk = useAddonSDK();
|
|
392
|
+
const queryClient = useQueryClient();
|
|
393
|
+
const { path, method = "POST", invalidateQueries = [], mutationFn, onSuccess, ...mutationOptions } = options;
|
|
394
|
+
const effectiveFn = mutationFn ?? (async (variables) => {
|
|
395
|
+
const resolvedPath = typeof path === "function" ? path(variables) : path ?? "";
|
|
396
|
+
if (!resolvedPath) throw new Error("useAddonMutation requires a valid 'path' or 'mutationFn'.");
|
|
397
|
+
if (method === "PUT") return sdk.api.put(resolvedPath, variables);
|
|
398
|
+
if (method === "DELETE") return sdk.api.delete(resolvedPath);
|
|
399
|
+
return sdk.api.post(resolvedPath, variables);
|
|
400
|
+
});
|
|
401
|
+
return useMutation({
|
|
402
|
+
...mutationOptions,
|
|
403
|
+
mutationFn: effectiveFn,
|
|
404
|
+
onSuccess: async (...args) => {
|
|
405
|
+
if (invalidateQueries.length > 0) await Promise.all(invalidateQueries.map((key) => queryClient.invalidateQueries({ queryKey: key })));
|
|
406
|
+
if (onSuccess) await onSuccess(...args);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/index.ts
|
|
412
|
+
/** Define a manifest with full validation-friendly typing while preserving literal values. */
|
|
413
|
+
function defineAddonManifest(manifest) {
|
|
414
|
+
return manifest;
|
|
415
|
+
}
|
|
416
|
+
/** Define a typed server factory without widening its addon database interface. */
|
|
417
|
+
function defineAddonServer(factory) {
|
|
418
|
+
return factory;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Access the host application's SDK. Provides authenticated API access, toast notifications, and
|
|
422
|
+
* navigation.
|
|
423
|
+
*
|
|
424
|
+
* This function is a shell; the real implementation is provided by the Jchat host environment at
|
|
425
|
+
* runtime.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ;```tsx
|
|
429
|
+
* import { useAddonSDK } from "@jchat/addon-sdk"
|
|
430
|
+
*
|
|
431
|
+
* export default function MyAddon({ mountPoint }: AddonProps) {
|
|
432
|
+
* const sdk = useAddonSDK()
|
|
433
|
+
*
|
|
434
|
+
* const handleClick = async () => {
|
|
435
|
+
* const data = await sdk.api.get("/trending")
|
|
436
|
+
* sdk.toast("Fetched trending posts!", "success")
|
|
437
|
+
* }
|
|
438
|
+
* }
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
function useAddonSDK() {
|
|
442
|
+
throw new Error("AddonSDK must be used within the Jchat host environment.");
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Access the host application's active icon pack. Returns the icon set matching the site's
|
|
446
|
+
* configured icon pack preference.
|
|
447
|
+
*
|
|
448
|
+
* This function is a shell; the real implementation is provided by the Jchat host environment at
|
|
449
|
+
* runtime.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ;```tsx
|
|
453
|
+
* import { useIcons } from "@jchat/addon-sdk"
|
|
454
|
+
*
|
|
455
|
+
* export default function MyAddon() {
|
|
456
|
+
* const { Icons } = useIcons()
|
|
457
|
+
* return <Icons.Send className="size-5" />
|
|
458
|
+
* }
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
function useIcons() {
|
|
462
|
+
throw new Error("useIcons must be used within the Jchat host environment.");
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get the version of the addon SDK. This is useful for checking compatibility with the host
|
|
466
|
+
* application.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ;```tsx
|
|
470
|
+
* import { getAddonSdkVersion } from "@jchat/addon-sdk"
|
|
471
|
+
*
|
|
472
|
+
* console.log(getAddonSdkVersion())
|
|
473
|
+
* ```
|
|
474
|
+
*
|
|
475
|
+
* @returns
|
|
476
|
+
*/
|
|
477
|
+
function getAddonSdkVersion() {
|
|
478
|
+
return version;
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
export { HOST_LANGUAGES, e as ICON_NAMES, LocaleContext, MOUNT_POINTS, createTranslator, defineAddonManifest, defineAddonServer, getAddonSdkVersion, useAddonInfiniteQuery, useAddonMutation, useAddonQuery, useAddonSDK, useIcons, useLocale, useQueryClient, useTranslation, validateAddonLocales };
|
package/dist/ui.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@jchat/ui";
|
package/dist/ui.mjs
ADDED
package/dist/vite.d.mts
CHANGED
|
@@ -1,29 +1,81 @@
|
|
|
1
1
|
import { t as AddonManifest } from "./contracts.mjs";
|
|
2
2
|
import { Plugin } from "vite";
|
|
3
3
|
//#region src/vite.d.ts
|
|
4
|
-
interface EmbedCssOptions {
|
|
5
|
-
id?: string;
|
|
6
|
-
}
|
|
7
4
|
/**
|
|
8
|
-
* Vite plugin
|
|
9
|
-
*
|
|
5
|
+
* Vite plugin for building JChat sandboxed addon extensions.
|
|
6
|
+
*
|
|
7
|
+
* Configures automatic manifest generation (`manifest.json`), bundles server-side API scripts
|
|
8
|
+
* (`src/api.ts`), embeds scoped Tailwind CSS into the frontend bundle, packages database migrations
|
|
9
|
+
* and seed files, copies documentation (`README.md`), signs database-bearing addon manifests, and
|
|
10
|
+
* zips production build artifacts.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ;```ts
|
|
14
|
+
* import { jchatAddon } from "@jchat/addon-sdk/vite"
|
|
15
|
+
* import manifest from "./src/manifest"
|
|
16
|
+
* import { defineConfig } from "vite"
|
|
17
|
+
*
|
|
18
|
+
* export default defineConfig({
|
|
19
|
+
* plugins: [jchatAddon({ manifest })]
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @param options - Configuration options for the JChat addon Vite plugin.
|
|
24
|
+
* @returns A Vite {@link Plugin} instance configured for addon compilation.
|
|
10
25
|
*/
|
|
11
|
-
declare function
|
|
26
|
+
declare function jchatAddon(options: JchatAddonPluginOptions): Plugin;
|
|
27
|
+
/** Configuration options for the {@link jchatAddon} Vite plugin. */
|
|
12
28
|
interface JchatAddonPluginOptions {
|
|
29
|
+
/** The base manifest specification for the addon. */
|
|
13
30
|
manifest: AddonManifest;
|
|
31
|
+
/** Explicitly declare development mode to permit unsigned database-bearing builds. */
|
|
32
|
+
development?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to package the build output into a `.zip` archive upon build completion. Defaults to
|
|
35
|
+
* `true` during production builds and `false` during development builds.
|
|
36
|
+
*/
|
|
14
37
|
zip?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Optional RSA private key string used to cryptographically sign database-bearing addons. If
|
|
40
|
+
* omitted, falls back to the `JCHAT_ADDON_SIGNING_PRIVATE_KEY` environment variable.
|
|
41
|
+
*/
|
|
15
42
|
signingPrivateKey?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Embed Tailwind CSS output into the JS bundle and scope it to the addon's DOM container.
|
|
45
|
+
* Defaults to `true`. Set to `false` only if the addon has no CSS.
|
|
46
|
+
*/
|
|
47
|
+
css?: boolean;
|
|
16
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Generates CLI argument parameters for bundling server-side Elysia API scripts (`src/api.ts`) via
|
|
51
|
+
* Bun.
|
|
52
|
+
*
|
|
53
|
+
* @param entryPath - Absolute file path to the server entry script (`src/api.ts`).
|
|
54
|
+
* @param outputPath - Absolute destination file path for the bundled output (`build/api.js`).
|
|
55
|
+
* @param developmentBuild - Indicates whether to enable external sourcemaps or minification.
|
|
56
|
+
* @returns Array of command line arguments for running `bun build`.
|
|
57
|
+
*/
|
|
17
58
|
declare function createAddonServerBuildArguments(entryPath: string, outputPath: string, developmentBuild: boolean): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Cryptographically signs an addon manifest using RSA-SHA256 over all archive file contents.
|
|
61
|
+
*
|
|
62
|
+
* @param manifest - Unsigned or base addon manifest.
|
|
63
|
+
* @param files - List of archive file paths and raw contents to digest.
|
|
64
|
+
* @param privateKey - RSA private key in PEM format.
|
|
65
|
+
* @returns Updated {@link AddonManifest} containing an RSA signature block.
|
|
66
|
+
*/
|
|
18
67
|
declare function signAddonManifest(manifest: AddonManifest, files: readonly {
|
|
19
68
|
path: string;
|
|
20
69
|
contents: Buffer;
|
|
21
70
|
}[], privateKey: string): AddonManifest;
|
|
22
|
-
declare function requireAddonSigningKey(signingPrivateKey: string | undefined, developmentBuild: boolean): string | undefined;
|
|
23
71
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
72
|
+
* Resolves the required RSA signing key for database-bearing addons.
|
|
73
|
+
*
|
|
74
|
+
* @param signingPrivateKey - Provided signing key or undefined.
|
|
75
|
+
* @param developmentBuild - Whether the current build is targeting development mode.
|
|
76
|
+
* @returns Resolved signing key string or undefined (in dev builds).
|
|
77
|
+
* @throws {@link Error} If building a database-bearing addon in production without a signing key.
|
|
26
78
|
*/
|
|
27
|
-
declare function
|
|
79
|
+
declare function requireAddonSigningKey(signingPrivateKey: string | undefined, developmentBuild: boolean): string | undefined;
|
|
28
80
|
//#endregion
|
|
29
|
-
export {
|
|
81
|
+
export { JchatAddonPluginOptions, createAddonServerBuildArguments, jchatAddon, requireAddonSigningKey, signAddonManifest };
|