@jchat/addon-sdk 0.0.1 → 1.0.0-alpha.1

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 ADDED
@@ -0,0 +1,11 @@
1
+ # @jchat/addon-sdk
2
+
3
+ ## 1.0.0-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 70537c0: Cleanup folder structure.
8
+
9
+ ## 1.0.0-alpha.0
10
+
11
+ - Initial public alpha release.
package/README.md CHANGED
@@ -26,7 +26,7 @@ import type {
26
26
  AddonManifest,
27
27
  AddonServerContext,
28
28
  AddonClientContext,
29
- AddonProps,
29
+ AddonProps
30
30
  } from "@jchat/addon-sdk"
31
31
  ```
32
32
 
@@ -36,7 +36,7 @@ For the Vite build plugin:
36
36
  import { jchatAddon } from "@jchat/addon-sdk/vite"
37
37
 
38
38
  export default {
39
- plugins: [jchatAddon()],
39
+ plugins: [jchatAddon()]
40
40
  }
41
41
  ```
42
42
 
@@ -1 +1,110 @@
1
- import*as e from"node:fs";import*as t from"node:path";function n(e){try{Bun.semver.order(e,e)}catch{throw Error(`Invalid semantic version: ${e}`)}let t=/^(?:v|=)?(\d+)\./.exec(e)?.[1],n=t?Number(t):NaN;if(!Number.isSafeInteger(n))throw Error(`Invalid semantic version: ${e}`);return`>=${e} <${n+1}.0.0`}function r(n){let{buildDir:r,manifest:i}=n,c=[];c.push(a(`jchat`,`JChat compatibility`,n.jchatVersion,i.compatibility?.jchat),a(`addon-sdk`,`Addon SDK compatibility`,n.addonSdkVersion,i.compatibility?.addonSdk));let l=[[`frontend-bundle`,`Frontend bundle`,i.frontend?.bundle],[`server-bundle`,`Server bundle`,i.server?.bundle],...(i.server?.migrations??[]).map(e=>[`migration:${e}`,`Migration ${e}`,e]),...(i.server?.seeds??[]).map(e=>[`seed:${e}`,`Seed ${e}`,e])];for(let[t,n,i]of l){if(!i)continue;let a=o(r,i);c.push({id:t,label:n,status:a&&e.existsSync(a)&&e.statSync(a).isFile()?`pass`:`error`,message:a&&e.existsSync(a)&&e.statSync(a).isFile()?i:`Missing or unsafe build file: ${i}`})}let u=s(r),d=t.join(r,`${i.id}-${i.version}.zip`),f=e.existsSync(d)&&e.statSync(d).isFile(),p=n.productionPackage&&f?e.statSync(d).size:u.filter(e=>e!==d).reduce((t,n)=>t+e.statSync(n).size,0),m=n.maxBytes??25*1024*1024;c.push({id:`package-size`,label:`Package size`,status:p<=m?`pass`:`error`,message:`${p} of ${m} bytes`});let h=u.filter(e=>e.endsWith(`.map`));c.push({id:`source-maps`,label:`Source maps`,status:n.productionPackage&&h.length?`warning`:`pass`,message:h.length?`${h.length} source map(s) present`:`No source maps`});let g=!!(i.server?.migrations?.length||i.server?.seeds?.length);return c.push({id:`database-signature`,label:`Database file signature`,status:g&&n.productionPackage&&!i.signature?.value?`error`:g&&!i.signature?.value?`warning`:`pass`,message:g&&!i.signature?.value?n.productionPackage?`Production database files require a publisher signature`:`Unsigned database files are allowed only in the developer sandbox`:g?`Publisher signature present`:`No database files declared`}),n.productionPackage&&c.push({id:`production-archive`,label:`Production archive`,status:f?`pass`:`error`,message:f?t.basename(d):`Production archive is missing`}),c}function i(e){return e.some(({status:e})=>e===`error`)}function a(e,t,n,r){let i=!!(r&&Bun.semver.satisfies(n,r));return{id:e,label:t,status:i?`pass`:`error`,message:r?`${n} ${i?`satisfies`:`does not satisfy`} ${r}`:`Compatibility range is missing`}}function o(e,n){if(t.isAbsolute(n)||n.includes(`\\`)||n.split(`/`).includes(`..`))return null;let r=t.resolve(e,...n.replace(/^\.\//,``).split(`/`)),i=t.relative(t.resolve(e),r);return i&&!i.startsWith(`..`)&&!t.isAbsolute(i)?r:null}function s(n){return e.existsSync(n)?e.readdirSync(n,{recursive:!0,withFileTypes:!0}).filter(e=>e.isFile()).map(e=>t.join(e.parentPath,e.name)):[]}export{n as createCompatibleMajorRange,r as diagnoseAddonPackage,i as hasAddonDiagnosticErrors};
1
+ import * as fs$1 from "node:fs";
2
+ import * as path$1 from "node:path";
3
+ //#region src/diagnostics.ts
4
+ function createCompatibleMajorRange(version) {
5
+ try {
6
+ Bun.semver.order(version, version);
7
+ } catch {
8
+ throw new Error(`Invalid semantic version: ${version}`);
9
+ }
10
+ const majorComponent = /^(?:v|=)?(\d+)\./.exec(version)?.[1];
11
+ const major = majorComponent ? Number(majorComponent) : NaN;
12
+ if (!Number.isSafeInteger(major)) throw new Error(`Invalid semantic version: ${version}`);
13
+ return `>=${version} <${major + 1}.0.0`;
14
+ }
15
+ function diagnoseAddonPackage(options) {
16
+ const { buildDir, manifest } = options;
17
+ const diagnostics = [];
18
+ diagnostics.push(compatibilityDiagnostic("jchat", "JChat compatibility", options.jchatVersion, manifest.compatibility?.jchat), compatibilityDiagnostic("addon-sdk", "Addon SDK compatibility", options.addonSdkVersion, manifest.compatibility?.addonSdk));
19
+ const references = [
20
+ [
21
+ "frontend-bundle",
22
+ "Frontend bundle",
23
+ manifest.frontend?.bundle
24
+ ],
25
+ [
26
+ "server-bundle",
27
+ "Server bundle",
28
+ manifest.server?.bundle
29
+ ],
30
+ ...(manifest.server?.migrations ?? []).map((file) => [
31
+ `migration:${file}`,
32
+ `Migration ${file}`,
33
+ file
34
+ ]),
35
+ ...(manifest.server?.seeds ?? []).map((file) => [
36
+ `seed:${file}`,
37
+ `Seed ${file}`,
38
+ file
39
+ ])
40
+ ];
41
+ for (const [id, label, reference] of references) {
42
+ if (!reference) continue;
43
+ const file = resolveBuildFile(buildDir, reference);
44
+ diagnostics.push({
45
+ id,
46
+ label,
47
+ status: file && fs$1.existsSync(file) && fs$1.statSync(file).isFile() ? "pass" : "error",
48
+ message: file && fs$1.existsSync(file) && fs$1.statSync(file).isFile() ? reference : `Missing or unsafe build file: ${reference}`
49
+ });
50
+ }
51
+ const files = listRegularFiles(buildDir);
52
+ const productionArchive = path$1.join(buildDir, `${manifest.id}-${manifest.version}.zip`);
53
+ const hasProductionArchive = fs$1.existsSync(productionArchive) && fs$1.statSync(productionArchive).isFile();
54
+ const totalBytes = options.productionPackage && hasProductionArchive ? fs$1.statSync(productionArchive).size : files.filter((file) => file !== productionArchive).reduce((total, file) => total + fs$1.statSync(file).size, 0);
55
+ const maxBytes = options.maxBytes ?? 25 * 1024 * 1024;
56
+ diagnostics.push({
57
+ id: "package-size",
58
+ label: "Package size",
59
+ status: totalBytes <= maxBytes ? "pass" : "error",
60
+ message: `${totalBytes} of ${maxBytes} bytes`
61
+ });
62
+ const sourceMaps = files.filter((file) => file.endsWith(".map"));
63
+ diagnostics.push({
64
+ id: "source-maps",
65
+ label: "Source maps",
66
+ status: options.productionPackage && sourceMaps.length ? "warning" : "pass",
67
+ message: sourceMaps.length ? `${sourceMaps.length} source map(s) present` : "No source maps"
68
+ });
69
+ const hasDatabaseFiles = Boolean(manifest.server?.migrations?.length || manifest.server?.seeds?.length);
70
+ diagnostics.push({
71
+ id: "database-signature",
72
+ label: "Database file signature",
73
+ status: hasDatabaseFiles && options.productionPackage && !manifest.signature?.value ? "error" : hasDatabaseFiles && !manifest.signature?.value ? "warning" : "pass",
74
+ message: hasDatabaseFiles && !manifest.signature?.value ? options.productionPackage ? "Production database files require a publisher signature" : "Unsigned database files are allowed only in the developer sandbox" : hasDatabaseFiles ? "Publisher signature present" : "No database files declared"
75
+ });
76
+ if (options.productionPackage) diagnostics.push({
77
+ id: "production-archive",
78
+ label: "Production archive",
79
+ status: hasProductionArchive ? "pass" : "error",
80
+ message: hasProductionArchive ? path$1.basename(productionArchive) : "Production archive is missing"
81
+ });
82
+ return diagnostics;
83
+ }
84
+ function hasAddonDiagnosticErrors(diagnostics) {
85
+ return diagnostics.some(({ status }) => status === "error");
86
+ }
87
+ function compatibilityDiagnostic(id, label, version, range) {
88
+ const compatible = Boolean(range && Bun.semver.satisfies(version, range));
89
+ return {
90
+ id,
91
+ label,
92
+ status: compatible ? "pass" : "error",
93
+ message: range ? `${version} ${compatible ? "satisfies" : "does not satisfy"} ${range}` : "Compatibility range is missing"
94
+ };
95
+ }
96
+ function resolveBuildFile(buildDir, reference) {
97
+ if (path$1.isAbsolute(reference) || reference.includes("\\") || reference.split("/").includes("..")) return null;
98
+ const target = path$1.resolve(buildDir, ...reference.replace(/^\.\//, "").split("/"));
99
+ const relative = path$1.relative(path$1.resolve(buildDir), target);
100
+ return relative && !relative.startsWith("..") && !path$1.isAbsolute(relative) ? target : null;
101
+ }
102
+ function listRegularFiles(root) {
103
+ if (!fs$1.existsSync(root)) return [];
104
+ return fs$1.readdirSync(root, {
105
+ recursive: true,
106
+ withFileTypes: true
107
+ }).filter((entry) => entry.isFile()).map((entry) => path$1.join(entry.parentPath, entry.name));
108
+ }
109
+ //#endregion
110
+ export { createCompatibleMajorRange, diagnoseAddonPackage, hasAddonDiagnosticErrors };
package/dist/index.d.mts CHANGED
@@ -2,8 +2,7 @@ import { a as MOUNT_POINTS, i as ConfigSchemaField, n as AddonPermission, o as M
2
2
  import { ComponentType, SVGProps } from "react";
3
3
  import { Elysia } from "elysia";
4
4
  import { Kysely } from "kysely";
5
- //#region ../icon/dist/index.d.mts
6
- //#region src/types.d.ts
5
+ //#region ../icon/src/types.d.ts
7
6
  type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
8
7
  declare const ICON_NAMES: readonly ["ProfileMen", "ProfileWomen", "Stories", "Search", "AdminPanel", "Investigation", "Logout", "Users", "Settings", "RoomList", "AdminShip", "Announcement", "NewsFeed", "Ranks", "TopPoints", "Like", "DisLike", "Love", "Funny", "Comment", "Chat", "Brush", "Image", "Mic", "More", "Palette", "Smiley", "Send", "Private", "Action", "View", "Female", "Male", "LuSend", "Heart", "Trash", "HorizontalOption", "Menu", "UserGroup", "Globe", "Plus", "Loader", "X", "UserRoundPlus", "Footprints", "Calendar", "Volume", "Youtube", "Upload", "Play", "Pause", "Stop", "Ok", "ImageFill", "MicFill", "YoutubeFill", "PlusFill", "Music", "Gift", "Camera", "Envelope", "Key", "Signature", "UserPen", "Edit", "AddressCard", "Star", "StatusOnline", "StatusAway", "StatusBusy", "StatusEating", "StatusGaming", "StatusSinging", "StatusListening", "StatusStay"];
9
8
  type IconName = (typeof ICON_NAMES)[number];
@@ -108,6 +107,19 @@ interface AddonApi {
108
107
  delete: <TResponse = unknown>(path: string) => Promise<TResponse>;
109
108
  }
110
109
  type AddonEventListener = (data: unknown) => void;
110
+ /** A font option provided by the JChat host. */
111
+ interface AddonFont {
112
+ /** Display label shown to users (e.g. 'Modern', 'Elegant') */
113
+ label: string;
114
+ /** Font identifier stored in the database (e.g. 'poppins', 'lato') */
115
+ value: string;
116
+ /** CSS font-family string ready for use in inline styles */
117
+ family: string;
118
+ /** Google Fonts name for loading the font via a <link> tag */
119
+ googleName: string;
120
+ /** Relative size multiplier — apply this to base font sizes to match the host's rendering */
121
+ scale: number;
122
+ }
111
123
  /**
112
124
  * The bridge between your addon and the Jchat host application. Use the `useAddonSDK()` hook to
113
125
  * access this interface in your components.
@@ -172,6 +184,11 @@ interface AddonSDK {
172
184
  /** Unregister a previously attached listener */
173
185
  off: (event: string, callback: AddonEventListener) => void;
174
186
  };
187
+ /**
188
+ * The list of fonts available in this JChat deployment, as configured by the site administrator.
189
+ * Use this to build a font picker or apply a matching font family in your addon UI.
190
+ */
191
+ supportedFonts?: AddonFont[];
175
192
  }
176
193
  /**
177
194
  * Seed data for your addon's tables, provided inside the addon's isolated schema.
@@ -237,4 +254,4 @@ declare function useIcons(): {
237
254
  Icons: IconPack;
238
255
  };
239
256
  //#endregion
240
- export { AddonApi, AddonDatabase, AddonEventListener, AddonJsonBody, type AddonManifest, type AddonPermission, type AddonPermissionDefinition, AddonProps, AddonRequestUser, AddonSDK, AddonSeedDocument, AddonServerContext, AddonServerFactory, AddonUser, type ConfigSchemaField, MOUNT_POINTS, type MountPoint, defineAddonManifest, defineAddonServer, useAddonSDK, useIcons };
257
+ export { AddonApi, AddonDatabase, AddonEventListener, AddonFont, AddonJsonBody, type AddonManifest, type AddonPermission, type AddonPermissionDefinition, AddonProps, AddonRequestUser, AddonSDK, AddonSeedDocument, AddonServerContext, AddonServerFactory, AddonUser, type ConfigSchemaField, MOUNT_POINTS, type MountPoint, defineAddonManifest, defineAddonServer, useAddonSDK, useIcons };
package/dist/index.mjs CHANGED
@@ -1 +1,86 @@
1
- const e={CHAT_HEADER:`chat-header`,CHAT_TOOLBAR:`chat-toolbar`,CHAT_SIDEBAR:`chat-sidebar`,SIDEBAR_TOP:`sidebar-top`,SIDEBAR_BOTTOM:`sidebar-bottom`,USER_POPOVER_ACTIONS:`user-popover-actions`,MESSAGE_CONTEXT_MENU:`message-context-menu`,MESSAGE_FEED_OVERLAY:`message-feed-overlay`,ADMIN_PANEL:`admin-panel`,SETTINGS_PANEL:`settings-panel`,CHAT_PLUS_MENU:`chat-plus-menu`,EMOJI_PICKER_GIF_TAB:`emoji-picker-gif-tab`,TUBESCAN_DIALOG:`tubescan-dialog`};function t(e){return e}function n(e){return e}function r(){throw Error(`AddonSDK must be used within the Jchat host environment.`)}function i(){throw Error(`useIcons must be used within the Jchat host environment.`)}export{e as MOUNT_POINTS,t as defineAddonManifest,n as defineAddonServer,r as useAddonSDK,i as useIcons};
1
+ //#region src/contracts.ts
2
+ /** Valid locations where an addon can be rendered within the Jchat layout. */
3
+ const MOUNT_POINTS = {
4
+ /** Top of the main chat column */
5
+ CHAT_HEADER: "chat-header",
6
+ /** Below the main chat header, above messages */
7
+ CHAT_TOOLBAR: "chat-toolbar",
8
+ /** The persistent right-side sidebar */
9
+ CHAT_SIDEBAR: "chat-sidebar",
10
+ /** Top of the left sidebar (room list) */
11
+ SIDEBAR_TOP: "sidebar-top",
12
+ /** Bottom of the left sidebar */
13
+ SIDEBAR_BOTTOM: "sidebar-bottom",
14
+ /** Action list shown when clicking a user's avatar */
15
+ USER_POPOVER_ACTIONS: "user-popover-actions",
16
+ /** Menu shown when right-clicking a message */
17
+ MESSAGE_CONTEXT_MENU: "message-context-menu",
18
+ /** Floating overlay rendered on top of the message stream */
19
+ MESSAGE_FEED_OVERLAY: "message-feed-overlay",
20
+ /** Main admin dashboard area */
21
+ ADMIN_PANEL: "admin-panel",
22
+ /** User or site settings dashboard area */
23
+ SETTINGS_PANEL: "settings-panel",
24
+ /** The 'plus' (+) menu in the message composer */
25
+ CHAT_PLUS_MENU: "chat-plus-menu",
26
+ /** Custom tab in the emoji/gif selection panel */
27
+ EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab",
28
+ /** Full-screen or modal dialog specifically for TubeScan */
29
+ TUBESCAN_DIALOG: "tubescan-dialog"
30
+ };
31
+ //#endregion
32
+ //#region src/index.ts
33
+ /** Define a manifest with full validation-friendly typing while preserving literal values. */
34
+ function defineAddonManifest(manifest) {
35
+ return manifest;
36
+ }
37
+ /** Define a typed server factory without widening its addon database interface. */
38
+ function defineAddonServer(factory) {
39
+ return factory;
40
+ }
41
+ /**
42
+ * Access the host application's SDK. Provides authenticated API access, toast notifications, and
43
+ * navigation.
44
+ *
45
+ * This function is a shell; the real implementation is provided by the Jchat host environment at
46
+ * runtime.
47
+ *
48
+ * @example
49
+ * ;```tsx
50
+ * import { useAddonSDK } from "@jchat/addon-sdk"
51
+ *
52
+ * export default function MyAddon({ mountPoint }: AddonProps) {
53
+ * const sdk = useAddonSDK()
54
+ *
55
+ * const handleClick = async () => {
56
+ * const data = await sdk.api.get("/trending")
57
+ * sdk.toast("Fetched trending posts!", "success")
58
+ * }
59
+ * }
60
+ * ```
61
+ */
62
+ function useAddonSDK() {
63
+ throw new Error("AddonSDK must be used within the Jchat host environment.");
64
+ }
65
+ /**
66
+ * Access the host application's active icon pack. Returns the icon set matching the site's
67
+ * configured icon pack preference.
68
+ *
69
+ * This function is a shell; the real implementation is provided by the Jchat host environment at
70
+ * runtime.
71
+ *
72
+ * @example
73
+ * ;```tsx
74
+ * import { useIcons } from "@jchat/addon-sdk"
75
+ *
76
+ * export default function MyAddon() {
77
+ * const { Icons } = useIcons()
78
+ * return <Icons.Send className="size-5" />
79
+ * }
80
+ * ```
81
+ */
82
+ function useIcons() {
83
+ throw new Error("useIcons must be used within the Jchat host environment.");
84
+ }
85
+ //#endregion
86
+ export { MOUNT_POINTS, defineAddonManifest, defineAddonServer, useAddonSDK, useIcons };
package/dist/vite.d.mts CHANGED
@@ -1,18 +1,20 @@
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 that gathers CSS styles generated by Tailwind CSS and embeds them directly inside the
9
- * main UI JavaScript bundle at the top, avoiding the need to load external stylesheet files.
5
+ * Vite plugin to configure Jchat addon manifest generation, bundle backend scripts, copy
6
+ * migrations, declarative seed data, and zip production build artifacts.
10
7
  */
11
- declare function jchatEmbedCss(options?: EmbedCssOptions): Plugin;
8
+ declare function jchatAddon(options: JchatAddonPluginOptions): Plugin;
12
9
  interface JchatAddonPluginOptions {
13
10
  manifest: AddonManifest;
14
11
  zip?: boolean;
15
12
  signingPrivateKey?: string;
13
+ /**
14
+ * Embed Tailwind CSS output into the JS bundle and scope it to the addon's DOM container.
15
+ * Defaults to `true`. Set to `false` only if the addon has no CSS.
16
+ */
17
+ css?: boolean;
16
18
  }
17
19
  declare function createAddonServerBuildArguments(entryPath: string, outputPath: string, developmentBuild: boolean): string[];
18
20
  declare function signAddonManifest(manifest: AddonManifest, files: readonly {
@@ -20,10 +22,5 @@ declare function signAddonManifest(manifest: AddonManifest, files: readonly {
20
22
  contents: Buffer;
21
23
  }[], privateKey: string): AddonManifest;
22
24
  declare function requireAddonSigningKey(signingPrivateKey: string | undefined, developmentBuild: boolean): string | undefined;
23
- /**
24
- * Vite plugin to configure Jchat addon manifest generation, bundle backend scripts, copy
25
- * migrations, declarative seed data, and zip production build artifacts.
26
- */
27
- declare function jchatAddon(options: JchatAddonPluginOptions): Plugin;
28
25
  //#endregion
29
- export { EmbedCssOptions, JchatAddonPluginOptions, createAddonServerBuildArguments, jchatAddon, jchatEmbedCss, requireAddonSigningKey, signAddonManifest };
26
+ export { JchatAddonPluginOptions, createAddonServerBuildArguments, jchatAddon, requireAddonSigningKey, signAddonManifest };
package/dist/vite.mjs CHANGED
@@ -1,10 +1,191 @@
1
- import e from"node:fs";import t from"node:path";import{execFileSync as n}from"node:child_process";import r from"node:crypto";function i(n){return{name:`jchat-embed-css`,apply:`build`,writeBundle(){let r=t.resolve(process.cwd(),`build`);if(!e.existsSync(r))return;let i=e.readdirSync(r),a=i.find(e=>e.endsWith(`.css`)),o=i.find(e=>e.includes(`ui`)&&e.endsWith(`.js`));if(!a||!o)return;let s=t.join(r,a),c=t.join(r,o),l=e.readFileSync(s,`utf-8`),u=e.readFileSync(c,`utf-8`);l.length<10?l=``:n?.id&&(l=l.replace(/:root\s*\{[^}]+\}/g,``),l=l.replace(/\.dark\s*\{[^}]+\}/g,``),l=l.replace(/\[data-theme=[^\]]+\]\s*\{[^}]+\}/g,``),l=l.replace(/\.dark\s*\[data-theme=[^\]]+\]\s*\{[^}]+\}/g,``),l=`@scope (#addon-${n.id}) {\n*, ::before, ::after { border-color: var(--border); }\n${l}\n}`),u=`
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import crypto from "node:crypto";
5
+ //#region src/vite.ts
6
+ /**
7
+ * Vite plugin to configure Jchat addon manifest generation, bundle backend scripts, copy
8
+ * migrations, declarative seed data, and zip production build artifacts.
9
+ */
10
+ function jchatAddon(options) {
11
+ const { manifest: manifestData } = options;
12
+ let shouldZip = options.zip;
13
+ let developmentBuild = false;
14
+ return {
15
+ name: "jchat-addon",
16
+ apply: "build",
17
+ config(_, env) {
18
+ return { define: { "process.env.NODE_ENV": JSON.stringify(env.mode || "production") } };
19
+ },
20
+ configResolved(config) {
21
+ developmentBuild = config.mode === "development";
22
+ if (shouldZip === void 0) shouldZip = config.mode === "production";
23
+ },
24
+ async writeBundle() {
25
+ const rootDir = process.cwd();
26
+ const pkgPath = path.resolve(rootDir, "package.json");
27
+ if (!fs.existsSync(pkgPath)) return;
28
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
29
+ const buildDir = path.resolve(rootDir, "build");
30
+ if (!fs.existsSync(buildDir)) return;
31
+ const bundleFile = fs.readdirSync(buildDir).find((f) => f.includes("ui") && f.endsWith(".js"));
32
+ if (!bundleFile) return;
33
+ if (options.css !== false) embedCssIntoBundle(buildDir, manifestData.id);
34
+ let manifest = {
35
+ ...manifestData,
36
+ version: pkg.version,
37
+ frontend: {
38
+ ...manifestData.frontend,
39
+ bundle: `./${bundleFile}`,
40
+ mountPoints: manifestData.frontend?.mountPoints ?? []
41
+ }
42
+ };
43
+ const serverSrcPath = path.resolve(rootDir, "src/api.ts");
44
+ const hasServer = fs.existsSync(serverSrcPath);
45
+ if (hasServer) {
46
+ execFileSync("bun", createAddonServerBuildArguments(serverSrcPath, path.resolve(buildDir, "api.js"), developmentBuild), { stdio: "inherit" });
47
+ manifest = {
48
+ ...manifest,
49
+ server: {
50
+ ...manifest.server,
51
+ bundle: "./api.js"
52
+ }
53
+ };
54
+ }
55
+ const databaseFiles = [...manifest.server?.migrations ?? [], ...manifest.server?.seeds ?? []];
56
+ for (const directory of ["migrations", "seeds"]) fs.rmSync(path.resolve(buildDir, directory), {
57
+ recursive: true,
58
+ force: true
59
+ });
60
+ for (const relativePath of databaseFiles) {
61
+ if (path.isAbsolute(relativePath) || relativePath.includes("\\") || relativePath.split("/").includes("..")) throw new Error(`Unsafe addon database file path: ${relativePath}`);
62
+ const source = path.resolve(rootDir, relativePath);
63
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) throw new Error(`Missing addon database file: ${relativePath}`);
64
+ const destination = path.resolve(buildDir, relativePath);
65
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
66
+ fs.copyFileSync(source, destination);
67
+ }
68
+ if (manifest.server?.migrations?.length || manifest.server?.seeds?.length) {
69
+ const signingPrivateKey = requireAddonSigningKey(options.signingPrivateKey ?? process.env.JCHAT_ADDON_SIGNING_PRIVATE_KEY, developmentBuild);
70
+ if (signingPrivateKey) {
71
+ const archivePaths = [
72
+ "manifest.json",
73
+ bundleFile,
74
+ ...manifest.server?.migrations ?? [],
75
+ ...manifest.server?.seeds ?? []
76
+ ];
77
+ if (hasServer) archivePaths.push("api.js");
78
+ manifest = signAddonManifest(manifest, archivePaths.map((file) => ({
79
+ path: file,
80
+ contents: file === "manifest.json" ? Buffer.from("") : fs.readFileSync(path.resolve(buildDir, file))
81
+ })), signingPrivateKey);
82
+ }
83
+ }
84
+ const manifestJson = JSON.stringify(manifest, null, 2);
85
+ fs.writeFileSync(path.resolve(buildDir, "manifest.json"), manifestJson);
86
+ if (shouldZip) {
87
+ const zipName = `${manifest.id}-${pkg.version}.zip`;
88
+ const filesToZip = ["manifest.json", bundleFile];
89
+ if (fs.existsSync(path.resolve(buildDir, "api.js"))) filesToZip.push("api.js");
90
+ const migrationsDest = path.resolve(buildDir, "migrations");
91
+ if (fs.existsSync(migrationsDest)) filesToZip.push("migrations");
92
+ const seedsDest = path.resolve(buildDir, "seeds");
93
+ if (fs.existsSync(seedsDest)) filesToZip.push("seeds");
94
+ execFileSync("tar", [
95
+ "-a",
96
+ "-cf",
97
+ zipName,
98
+ ...filesToZip
99
+ ], {
100
+ cwd: buildDir,
101
+ stdio: "inherit"
102
+ });
103
+ console.log(`Build Complete: ${manifest.id} v${pkg.version} → ${zipName}`);
104
+ }
105
+ }
106
+ };
107
+ }
108
+ function createAddonServerBuildArguments(entryPath, outputPath, developmentBuild) {
109
+ return [
110
+ "build",
111
+ entryPath,
112
+ "--target=bun",
113
+ "--format=esm",
114
+ `--outfile=${outputPath}`,
115
+ developmentBuild ? "--sourcemap=external" : "--minify"
116
+ ];
117
+ }
118
+ function signAddonManifest(manifest, files, privateKey) {
119
+ if (!manifest.publisher?.id) throw new Error("Signed addons require manifest.publisher.id");
120
+ const unsigned = {
121
+ ...manifest,
122
+ signature: {
123
+ algorithm: "rsa-sha256",
124
+ digest: "",
125
+ value: ""
126
+ }
127
+ };
128
+ const hash = crypto.createHash("sha256");
129
+ for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
130
+ hash.update(file.path);
131
+ hash.update("\0");
132
+ hash.update(file.path === "manifest.json" ? stableStringify(unsigned) : file.contents);
133
+ hash.update("\0");
134
+ }
135
+ const digest = hash.digest("hex");
136
+ return {
137
+ ...unsigned,
138
+ signature: {
139
+ algorithm: "rsa-sha256",
140
+ digest,
141
+ value: crypto.sign("sha256", Buffer.from(digest), privateKey).toString("base64url")
142
+ }
143
+ };
144
+ }
145
+ function requireAddonSigningKey(signingPrivateKey, developmentBuild) {
146
+ if (signingPrivateKey) return signingPrivateKey;
147
+ if (developmentBuild) return void 0;
148
+ throw new Error("Database-bearing addon builds require JCHAT_ADDON_SIGNING_PRIVATE_KEY");
149
+ }
150
+ function embedCssIntoBundle(buildDir, addonId) {
151
+ const files = fs.readdirSync(buildDir);
152
+ const cssFile = files.find((f) => f.endsWith(".css"));
153
+ const jsFile = files.find((f) => f.includes("ui") && f.endsWith(".js"));
154
+ if (!cssFile || !jsFile) return;
155
+ const cssPath = path.join(buildDir, cssFile);
156
+ const jsPath = path.join(buildDir, jsFile);
157
+ let css = fs.readFileSync(cssPath, "utf-8");
158
+ const js = fs.readFileSync(jsPath, "utf-8");
159
+ if (css.length < 10) css = "";
160
+ else {
161
+ css = css.replace(/:root\s*\{[^}]+\}/g, "");
162
+ css = css.replace(/\.dark\s*\{[^}]+\}/g, "");
163
+ css = css.replace(/\[data-theme=[^\]]+\]\s*\{[^}]+\}/g, "");
164
+ css = css.replace(/\.dark\s*\[data-theme=[^\]]+\]\s*\{[^}]+\}/g, "");
165
+ css = `@scope (#addon-${addonId}) {\n*, ::before, ::after { border-color: var(--border); }\n${css}\n}`;
166
+ }
167
+ const injection = `
2
168
  (function() {
3
- if (typeof document !== 'undefined' && ${JSON.stringify(l)}.length > 0) {
169
+ if (typeof document !== 'undefined' && ${JSON.stringify(css)}.length > 0) {
4
170
  const style = document.createElement('style');
5
171
  style.type = 'text/css';
6
- style.textContent = ${JSON.stringify(l)};
172
+ style.textContent = ${JSON.stringify(css)};
7
173
  document.head.appendChild(style);
8
174
  }
9
175
  })();
10
- `+u,e.writeFileSync(c,u);try{e.unlinkSync(s)}catch{}}}}function a(e,t,n){return[`build`,e,`--target=bun`,`--format=esm`,`--outfile=${t}`,n?`--sourcemap=external`:`--minify`]}function o(e){let t=e=>Array.isArray(e)?e.map(t):e&&typeof e==`object`?Object.fromEntries(Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,n])=>[e,t(n)])):e;return JSON.stringify(t(e))}function s(e,t,n){if(!e.publisher?.id)throw Error(`Signed addons require manifest.publisher.id`);let i={...e,signature:{algorithm:`rsa-sha256`,digest:``,value:``}},a=r.createHash(`sha256`);for(let e of[...t].sort((e,t)=>e.path.localeCompare(t.path)))a.update(e.path),a.update(`\0`),a.update(e.path===`manifest.json`?o(i):e.contents),a.update(`\0`);let s=a.digest(`hex`);return{...i,signature:{algorithm:`rsa-sha256`,digest:s,value:r.sign(`sha256`,Buffer.from(s),n).toString(`base64url`)}}}function c(e,t){if(e)return e;if(!t)throw Error(`Database-bearing addon builds require JCHAT_ADDON_SIGNING_PRIVATE_KEY`)}function l(r){let{manifest:i}=r,o=r.zip,l=!1;return{name:`jchat-addon`,apply:`build`,config(e,t){return{define:{"process.env.NODE_ENV":JSON.stringify(t.mode||`production`)}}},configResolved(e){l=e.mode===`development`,o===void 0&&(o=e.mode===`production`)},async writeBundle(){let u=process.cwd(),d=t.resolve(u,`package.json`);if(!e.existsSync(d))return;let f=JSON.parse(e.readFileSync(d,`utf-8`)),p=t.resolve(u,`build`);if(!e.existsSync(p))return;let m=e.readdirSync(p).find(e=>e.includes(`ui`)&&e.endsWith(`.js`));if(!m)return;let h={...i,version:f.version,frontend:{...i.frontend,bundle:`./${m}`,mountPoints:i.frontend?.mountPoints??[]}},g=t.resolve(u,`src/api.ts`),_=e.existsSync(g);_&&(n(`bun`,a(g,t.resolve(p,`api.js`),l),{stdio:`inherit`}),h={...h,server:{...h.server,bundle:`./api.js`}});let v=[...h.server?.migrations??[],...h.server?.seeds??[]];for(let n of[`migrations`,`seeds`])e.rmSync(t.resolve(p,n),{recursive:!0,force:!0});for(let n of v){if(t.isAbsolute(n)||n.includes(`\\`)||n.split(`/`).includes(`..`))throw Error(`Unsafe addon database file path: ${n}`);let r=t.resolve(u,n);if(!e.existsSync(r)||!e.statSync(r).isFile())throw Error(`Missing addon database file: ${n}`);let i=t.resolve(p,n);e.mkdirSync(t.dirname(i),{recursive:!0}),e.copyFileSync(r,i)}if(h.server?.migrations?.length||h.server?.seeds?.length){let n=c(r.signingPrivateKey??process.env.JCHAT_ADDON_SIGNING_PRIVATE_KEY,l);if(n){let r=[`manifest.json`,m,...h.server?.migrations??[],...h.server?.seeds??[]];_&&r.push(`api.js`),h=s(h,r.map(n=>({path:n,contents:n===`manifest.json`?Buffer.from(``):e.readFileSync(t.resolve(p,n))})),n)}}let y=JSON.stringify(h,null,2);if(e.writeFileSync(t.resolve(p,`manifest.json`),y),o){let r=`${h.id}-${f.version}.zip`,i=[`manifest.json`,m];e.existsSync(t.resolve(p,`api.js`))&&i.push(`api.js`);let a=t.resolve(p,`migrations`);e.existsSync(a)&&i.push(`migrations`);let o=t.resolve(p,`seeds`);e.existsSync(o)&&i.push(`seeds`),n(`tar`,[`-a`,`-cf`,r,...i],{cwd:p,stdio:`inherit`}),console.log(`Build Complete: ${h.id} v${f.version} → ${r}`)}}}}export{a as createAddonServerBuildArguments,l as jchatAddon,i as jchatEmbedCss,c as requireAddonSigningKey,s as signAddonManifest};
176
+ `;
177
+ fs.writeFileSync(jsPath, injection + js);
178
+ try {
179
+ fs.unlinkSync(cssPath);
180
+ } catch {}
181
+ }
182
+ function stableStringify(value) {
183
+ const sortValue = (child) => {
184
+ if (Array.isArray(child)) return child.map(sortValue);
185
+ if (child && typeof child === "object") return Object.fromEntries(Object.entries(child).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortValue(nested)]));
186
+ return child;
187
+ };
188
+ return JSON.stringify(sortValue(value));
189
+ }
190
+ //#endregion
191
+ export { createAddonServerBuildArguments, jchatAddon, requireAddonSigningKey, signAddonManifest };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jchat/addon-sdk",
3
- "version": "0.0.1",
3
+ "version": "1.0.0-alpha.1",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "addon",
@@ -12,9 +12,16 @@
12
12
  "vite"
13
13
  ],
14
14
  "license": "Apache-2.0",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/mjjabarullah/jchat.git",
18
+ "directory": "packages/addon-sdk"
19
+ },
15
20
  "files": [
16
21
  "dist",
17
- "README.md"
22
+ "src",
23
+ "README.md",
24
+ "CHANGELOG.md"
18
25
  ],
19
26
  "type": "module",
20
27
  "main": "./dist/index.mjs",
@@ -44,17 +51,17 @@
44
51
  "prepack": "bun run build"
45
52
  },
46
53
  "devDependencies": {
47
- "@jchat/icon": "1.0.0-alpha.1",
48
- "@types/react": "^19.2.17",
49
- "elysia": "^1.4.29",
54
+ "@jchat/icon": "workspace:*",
55
+ "@types/react": "catalog:",
56
+ "elysia": "catalog:",
50
57
  "kysely": "^catalog:",
51
58
  "tsdown": "^0.22.14",
52
- "vite": "^8.1.5"
59
+ "vite": "catalog:"
53
60
  },
54
61
  "peerDependencies": {
55
- "elysia": "^1.4.29",
62
+ "elysia": "catalog:",
56
63
  "kysely": "^catalog:",
57
- "react": "^19.2.8",
58
- "vite": "^8.1.5"
64
+ "react": "catalog:",
65
+ "vite": "catalog:"
59
66
  }
60
67
  }