@jchat/addon-sdk 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.
@@ -0,0 +1,2 @@
1
+ import { MockAddonContextResult, MockAddonServerContextOptions, createMockAddonServerContext } from "./index2.mjs";
2
+ export { MockAddonContextResult, MockAddonServerContextOptions, createMockAddonServerContext };
@@ -0,0 +1,2 @@
1
+ import { s as createMockAddonServerContext } from "./src.mjs";
2
+ export { createMockAddonServerContext };
@@ -0,0 +1,12 @@
1
+ import { ComponentType, SVGProps } from "react";
2
+ import { icons } from "lucide-react";
3
+ //#region ../icon/dist/types.d.mts
4
+ //#region src/types.d.ts
5
+ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
6
+ type LucideIconName = keyof typeof icons;
7
+ type IconName = LucideIconName | "Youtube";
8
+ declare const ICON_NAMES: readonly IconName[];
9
+ type IconPack = Record<IconName, IconComponent>;
10
+ //#endregion
11
+ //#endregion
12
+ export { IconPack as i, IconComponent as n, IconName as r, ICON_NAMES as t };
package/dist/ui.d.mts ADDED
@@ -0,0 +1,42 @@
1
+ import { _ as AddonUser, d as AddonRequestUser } from "./contracts2.mjs";
2
+ import React from "react";
3
+ import { Can } from "@casl/react";
4
+ export * from "@jchat/ui";
5
+ //#region src/ui.d.ts
6
+ export interface AddonCanProps {
7
+ /** The current user attempting the action */
8
+ actor?: (Partial<AddonRequestUser | AddonUser> & {
9
+ permissions?: string[];
10
+ isOwner?: boolean;
11
+ }) | null;
12
+ /** The owner ID of the target resource, if evaluating an ownership-restricted action */
13
+ ownerId?: string;
14
+ /** Permission required if actor is the resource owner (e.g. 'addon.pollwise.close.own') */
15
+ ownPermission?: string;
16
+ /** Permission required if actor is NOT the resource owner (e.g. 'addon.pollwise.close.any') */
17
+ anyPermission?: string;
18
+ /** Simple permission string when not evaluating ownership (e.g. 'addon.pollwise.create') */
19
+ permission?: string;
20
+ /** Fallback content rendered when permission is denied (defaults to null) */
21
+ fallback?: React.ReactNode;
22
+ /** Content rendered when access is granted */
23
+ children: React.ReactNode;
24
+ }
25
+ /**
26
+ * Declarative component for conditional UI rendering based on RBAC/ABAC permissions and resource
27
+ * ownership.
28
+ *
29
+ * @param actor The current user attempting the action
30
+ * @param ownerId The owner ID of the target resource, if evaluating an ownership-restricted action
31
+ * @param ownPermission Permission required if actor is the resource owner (e.g.
32
+ * 'addon.pollwise.close.own')
33
+ * @param anyPermission Permission required if actor is NOT the resource owner (e.g.
34
+ * 'addon.pollwise.close.any')
35
+ * @param permission Simple permission string when not evaluating ownership (e.g.
36
+ * 'addon.pollwise.create')
37
+ * @param fallback Fallback content rendered when permission is denied (defaults to null)
38
+ * @param children Content rendered when access is granted
39
+ */
40
+ export declare function AddonCan({ actor, ownerId, ownPermission, anyPermission, permission, fallback, children }: AddonCanProps): React.ReactElement | null;
41
+ //#endregion
42
+ export { Can };
package/dist/ui.mjs ADDED
@@ -0,0 +1,33 @@
1
+ import { _ as hasPermission } from "./src.mjs";
2
+ import { Can } from "@casl/react";
3
+ export * from "@jchat/ui";
4
+ //#region src/ui.ts
5
+ /**
6
+ * Declarative component for conditional UI rendering based on RBAC/ABAC permissions and resource
7
+ * ownership.
8
+ *
9
+ * @param actor The current user attempting the action
10
+ * @param ownerId The owner ID of the target resource, if evaluating an ownership-restricted action
11
+ * @param ownPermission Permission required if actor is the resource owner (e.g.
12
+ * 'addon.pollwise.close.own')
13
+ * @param anyPermission Permission required if actor is NOT the resource owner (e.g.
14
+ * 'addon.pollwise.close.any')
15
+ * @param permission Simple permission string when not evaluating ownership (e.g.
16
+ * 'addon.pollwise.create')
17
+ * @param fallback Fallback content rendered when permission is denied (defaults to null)
18
+ * @param children Content rendered when access is granted
19
+ */
20
+ function AddonCan({ actor, ownerId, ownPermission, anyPermission, permission, fallback = null, children }) {
21
+ if (!actor) return fallback;
22
+ const perms = actor.permissions ?? [];
23
+ if (actor.isOwner || hasPermission(perms, "*")) return children;
24
+ if (permission && !hasPermission(perms, permission)) return fallback;
25
+ if (ownPermission || anyPermission) {
26
+ if (Boolean(actor.id && ownerId && actor.id === ownerId) && ownPermission && hasPermission(perms, ownPermission)) return children;
27
+ if (anyPermission && hasPermission(perms, anyPermission)) return children;
28
+ return fallback;
29
+ }
30
+ return children;
31
+ }
32
+ //#endregion
33
+ export { AddonCan, Can };
package/dist/vite.d.mts CHANGED
@@ -1,14 +1,44 @@
1
- import { t as AddonManifest } from "./contracts.mjs";
1
+ import { l as AddonManifest } from "./contracts2.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/vite.d.ts
4
4
  /**
5
- * Vite plugin to configure Jchat addon manifest generation, bundle backend scripts, copy
6
- * migrations, declarative seed data, and zip production build artifacts.
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.
7
25
  */
8
- declare function jchatAddon(options: JchatAddonPluginOptions): Plugin;
9
- interface JchatAddonPluginOptions {
26
+ export declare function jchatAddon(options: JchatAddonPluginOptions): Plugin;
27
+ /** Configuration options for the {@link jchatAddon} Vite plugin. */
28
+ export interface JchatAddonPluginOptions {
29
+ /** The base manifest specification for the addon. */
10
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
+ */
11
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
+ */
12
42
  signingPrivateKey?: string;
13
43
  /**
14
44
  * Embed Tailwind CSS output into the JS bundle and scope it to the addon's DOM container.
@@ -16,11 +46,35 @@ interface JchatAddonPluginOptions {
16
46
  */
17
47
  css?: boolean;
18
48
  }
19
- declare function createAddonServerBuildArguments(entryPath: string, outputPath: string, developmentBuild: boolean): string[];
20
- declare function signAddonManifest(manifest: AddonManifest, files: readonly {
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
+ */
58
+ export 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
+ */
67
+ export declare function signAddonManifest(manifest: AddonManifest, files: readonly {
21
68
  path: string;
22
69
  contents: Buffer;
23
70
  }[], privateKey: string): AddonManifest;
24
- declare function requireAddonSigningKey(signingPrivateKey: string | undefined, developmentBuild: boolean): string | undefined;
25
- //#endregion
26
- export { JchatAddonPluginOptions, createAddonServerBuildArguments, jchatAddon, requireAddonSigningKey, signAddonManifest };
71
+ /**
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.
78
+ */
79
+ export declare function requireAddonSigningKey(signingPrivateKey: string | undefined, developmentBuild: boolean): string | undefined;
80
+ //#endregion
package/dist/vite.mjs CHANGED
@@ -1,11 +1,28 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { execFileSync } from "node:child_process";
4
1
  import crypto from "node:crypto";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
5
4
  //#region src/vite.ts
6
5
  /**
7
- * Vite plugin to configure Jchat addon manifest generation, bundle backend scripts, copy
8
- * migrations, declarative seed data, and zip production build artifacts.
6
+ * Vite plugin for building JChat sandboxed addon extensions.
7
+ *
8
+ * Configures automatic manifest generation (`manifest.json`), bundles server-side API scripts
9
+ * (`src/api.ts`), embeds scoped Tailwind CSS into the frontend bundle, packages database migrations
10
+ * and seed files, copies documentation (`README.md`), signs database-bearing addon manifests, and
11
+ * zips production build artifacts.
12
+ *
13
+ * @example
14
+ * ;```ts
15
+ * import { jchatAddon } from "@jchat/addon-sdk/vite"
16
+ * import manifest from "./src/manifest"
17
+ * import { defineConfig } from "vite"
18
+ *
19
+ * export default defineConfig({
20
+ * plugins: [jchatAddon({ manifest })]
21
+ * })
22
+ * ```
23
+ *
24
+ * @param options - Configuration options for the JChat addon Vite plugin.
25
+ * @returns A Vite {@link Plugin} instance configured for addon compilation.
9
26
  */
10
27
  function jchatAddon(options) {
11
28
  const { manifest: manifestData } = options;
@@ -15,10 +32,45 @@ function jchatAddon(options) {
15
32
  name: "jchat-addon",
16
33
  apply: "build",
17
34
  config(_, env) {
18
- return { define: { "process.env.NODE_ENV": JSON.stringify(env.mode || "production") } };
35
+ const rootDir = process.cwd();
36
+ const uiEntry = path.resolve(rootDir, "src/ui.tsx");
37
+ const hasUi = fs.existsSync(uiEntry);
38
+ return {
39
+ define: {
40
+ global: "window",
41
+ "process.env.NODE_ENV": JSON.stringify(env.mode || "production")
42
+ },
43
+ build: {
44
+ outDir: "build",
45
+ emptyOutDir: true,
46
+ minify: env.mode === "production",
47
+ ...hasUi ? {
48
+ lib: {
49
+ entry: uiEntry,
50
+ name: manifestData.id,
51
+ formats: ["es"],
52
+ fileName: () => "ui.[hash].js"
53
+ },
54
+ rolldownOptions: {
55
+ external: [
56
+ "react",
57
+ "react-dom",
58
+ "react/jsx-runtime",
59
+ "react/jsx-dev-runtime",
60
+ "@tanstack/react-query",
61
+ "@jchat/addon-sdk",
62
+ "@jchat/addon-sdk/state",
63
+ "@jchat/addon-sdk/ui",
64
+ "@jchat/addon-sdk/icon"
65
+ ],
66
+ output: { format: "esm" }
67
+ }
68
+ } : {}
69
+ }
70
+ };
19
71
  },
20
72
  configResolved(config) {
21
- developmentBuild = config.mode === "development";
73
+ developmentBuild = options.development ?? (config.mode === "development" || Bun.env.JCHAT_ADDON_DEV === "true" || Bun.env.NODE_ENV === "development");
22
74
  if (shouldZip === void 0) shouldZip = config.mode === "production";
23
75
  },
24
76
  async writeBundle() {
@@ -30,7 +82,7 @@ function jchatAddon(options) {
30
82
  if (!fs.existsSync(buildDir)) return;
31
83
  const bundleFile = fs.readdirSync(buildDir).find((f) => f.includes("ui") && f.endsWith(".js"));
32
84
  if (!bundleFile) return;
33
- if (options.css !== false) embedCssIntoBundle(buildDir, manifestData.id);
85
+ if (options.css !== false) embedCssIntoBundle(buildDir);
34
86
  let manifest = {
35
87
  ...manifestData,
36
88
  version: pkg.version,
@@ -41,9 +93,13 @@ function jchatAddon(options) {
41
93
  }
42
94
  };
43
95
  const serverSrcPath = path.resolve(rootDir, "src/api.ts");
44
- const hasServer = fs.existsSync(serverSrcPath);
96
+ const hasServer = await Bun.file(serverSrcPath).exists();
45
97
  if (hasServer) {
46
- execFileSync("bun", createAddonServerBuildArguments(serverSrcPath, path.resolve(buildDir, "api.js"), developmentBuild), { stdio: "inherit" });
98
+ Bun.spawnSync(["bun", ...createAddonServerBuildArguments(serverSrcPath, path.resolve(buildDir, "api.js"), developmentBuild)], { stdio: [
99
+ "inherit",
100
+ "inherit",
101
+ "inherit"
102
+ ] });
47
103
  manifest = {
48
104
  ...manifest,
49
105
  server: {
@@ -59,14 +115,17 @@ function jchatAddon(options) {
59
115
  });
60
116
  for (const relativePath of databaseFiles) {
61
117
  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
118
  const destination = path.resolve(buildDir, relativePath);
65
119
  fs.mkdirSync(path.dirname(destination), { recursive: true });
66
- fs.copyFileSync(source, destination);
67
120
  }
121
+ await Promise.all(databaseFiles.map(async (relativePath) => {
122
+ const source = path.resolve(rootDir, relativePath);
123
+ if (!await Bun.file(source).exists() || !fs.statSync(source).isFile()) throw new Error(`Missing addon database file: ${relativePath}`);
124
+ const destination = path.resolve(buildDir, relativePath);
125
+ return Bun.write(destination, Bun.file(source));
126
+ }));
68
127
  if (manifest.server?.migrations?.length || manifest.server?.seeds?.length) {
69
- const signingPrivateKey = requireAddonSigningKey(options.signingPrivateKey ?? process.env.JCHAT_ADDON_SIGNING_PRIVATE_KEY, developmentBuild);
128
+ const signingPrivateKey = requireAddonSigningKey(options.signingPrivateKey ?? Bun.env.JCHAT_ADDON_SIGNING_PRIVATE_KEY, developmentBuild);
70
129
  if (signingPrivateKey) {
71
130
  const archivePaths = [
72
131
  "manifest.json",
@@ -75,36 +134,57 @@ function jchatAddon(options) {
75
134
  ...manifest.server?.seeds ?? []
76
135
  ];
77
136
  if (hasServer) archivePaths.push("api.js");
78
- manifest = signAddonManifest(manifest, archivePaths.map((file) => ({
137
+ const resolvedFiles = await Promise.all(archivePaths.map(async (file) => ({
79
138
  path: file,
80
- contents: file === "manifest.json" ? Buffer.from("") : fs.readFileSync(path.resolve(buildDir, file))
81
- })), signingPrivateKey);
139
+ contents: file === "manifest.json" ? Buffer.from("") : Buffer.from(await Bun.file(path.resolve(buildDir, file)).arrayBuffer())
140
+ })));
141
+ manifest = signAddonManifest(manifest, resolvedFiles, signingPrivateKey);
82
142
  }
83
143
  }
144
+ const readmeSource = path.resolve(rootDir, "README.md");
145
+ if (await Bun.file(readmeSource).exists()) await Bun.write(path.resolve(buildDir, "README.md"), Bun.file(readmeSource));
146
+ const vendorLicensesContent = await generateAddonVendorLicenses(rootDir, manifest.name);
147
+ await Bun.write(path.resolve(buildDir, "vendor-licenses.txt"), vendorLicensesContent);
84
148
  const manifestJson = JSON.stringify(manifest, null, 2);
85
- fs.writeFileSync(path.resolve(buildDir, "manifest.json"), manifestJson);
149
+ await Bun.write(path.resolve(buildDir, "manifest.json"), manifestJson);
86
150
  if (shouldZip) {
87
151
  const zipName = `${manifest.id}-${pkg.version}.zip`;
88
152
  const filesToZip = ["manifest.json", bundleFile];
89
- if (fs.existsSync(path.resolve(buildDir, "api.js"))) filesToZip.push("api.js");
153
+ if (await Bun.file(path.resolve(buildDir, "api.js")).exists()) filesToZip.push("api.js");
154
+ if (await Bun.file(path.resolve(buildDir, "README.md")).exists()) filesToZip.push("README.md");
155
+ if (await Bun.file(path.resolve(buildDir, "vendor-licenses.txt")).exists()) filesToZip.push("vendor-licenses.txt");
90
156
  const migrationsDest = path.resolve(buildDir, "migrations");
91
157
  if (fs.existsSync(migrationsDest)) filesToZip.push("migrations");
92
158
  const seedsDest = path.resolve(buildDir, "seeds");
93
159
  if (fs.existsSync(seedsDest)) filesToZip.push("seeds");
94
- execFileSync("tar", [
160
+ Bun.spawnSync([
161
+ "tar",
95
162
  "-a",
96
163
  "-cf",
97
164
  zipName,
98
165
  ...filesToZip
99
166
  ], {
100
167
  cwd: buildDir,
101
- stdio: "inherit"
168
+ stdio: [
169
+ "inherit",
170
+ "inherit",
171
+ "inherit"
172
+ ]
102
173
  });
103
174
  console.log(`Build Complete: ${manifest.id} v${pkg.version} → ${zipName}`);
104
175
  }
105
176
  }
106
177
  };
107
178
  }
179
+ /**
180
+ * Generates CLI argument parameters for bundling server-side Elysia API scripts (`src/api.ts`) via
181
+ * Bun.
182
+ *
183
+ * @param entryPath - Absolute file path to the server entry script (`src/api.ts`).
184
+ * @param outputPath - Absolute destination file path for the bundled output (`build/api.js`).
185
+ * @param developmentBuild - Indicates whether to enable external sourcemaps or minification.
186
+ * @returns Array of command line arguments for running `bun build`.
187
+ */
108
188
  function createAddonServerBuildArguments(entryPath, outputPath, developmentBuild) {
109
189
  return [
110
190
  "build",
@@ -112,9 +192,17 @@ function createAddonServerBuildArguments(entryPath, outputPath, developmentBuild
112
192
  "--target=bun",
113
193
  "--format=esm",
114
194
  `--outfile=${outputPath}`,
115
- developmentBuild ? "--sourcemap=external" : "--minify"
195
+ developmentBuild ? "--sourcemap=inline" : "--minify"
116
196
  ];
117
197
  }
198
+ /**
199
+ * Cryptographically signs an addon manifest using RSA-SHA256 over all archive file contents.
200
+ *
201
+ * @param manifest - Unsigned or base addon manifest.
202
+ * @param files - List of archive file paths and raw contents to digest.
203
+ * @param privateKey - RSA private key in PEM format.
204
+ * @returns Updated {@link AddonManifest} containing an RSA signature block.
205
+ */
118
206
  function signAddonManifest(manifest, files, privateKey) {
119
207
  if (!manifest.publisher?.id) throw new Error("Signed addons require manifest.publisher.id");
120
208
  const unsigned = {
@@ -126,28 +214,37 @@ function signAddonManifest(manifest, files, privateKey) {
126
214
  }
127
215
  };
128
216
  const hash = crypto.createHash("sha256");
129
- for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
217
+ for (const file of [...files].toSorted((a, b) => a.path.localeCompare(b.path))) {
130
218
  hash.update(file.path);
131
219
  hash.update("\0");
132
220
  hash.update(file.path === "manifest.json" ? stableStringify(unsigned) : file.contents);
133
221
  hash.update("\0");
134
222
  }
135
223
  const digest = hash.digest("hex");
224
+ const normalizedKey = privateKey.replace(/\\n/g, "\n").trim();
136
225
  return {
137
226
  ...unsigned,
138
227
  signature: {
139
228
  algorithm: "rsa-sha256",
140
229
  digest,
141
- value: crypto.sign("sha256", Buffer.from(digest), privateKey).toString("base64url")
230
+ value: crypto.sign("sha256", Buffer.from(digest), normalizedKey).toString("base64url")
142
231
  }
143
232
  };
144
233
  }
234
+ /**
235
+ * Resolves the required RSA signing key for database-bearing addons.
236
+ *
237
+ * @param signingPrivateKey - Provided signing key or undefined.
238
+ * @param developmentBuild - Whether the current build is targeting development mode.
239
+ * @returns Resolved signing key string or undefined (in dev builds).
240
+ * @throws {@link Error} If building a database-bearing addon in production without a signing key.
241
+ */
145
242
  function requireAddonSigningKey(signingPrivateKey, developmentBuild) {
146
243
  if (signingPrivateKey) return signingPrivateKey;
147
244
  if (developmentBuild) return void 0;
148
245
  throw new Error("Database-bearing addon builds require JCHAT_ADDON_SIGNING_PRIVATE_KEY");
149
246
  }
150
- function embedCssIntoBundle(buildDir, addonId) {
247
+ function embedCssIntoBundle(buildDir) {
151
248
  const files = fs.readdirSync(buildDir);
152
249
  const cssFile = files.find((f) => f.endsWith(".css"));
153
250
  const jsFile = files.find((f) => f.includes("ui") && f.endsWith(".js"));
@@ -158,34 +255,131 @@ function embedCssIntoBundle(buildDir, addonId) {
158
255
  const js = fs.readFileSync(jsPath, "utf-8");
159
256
  if (css.length < 10) css = "";
160
257
  else {
258
+ css = stripLayer(css, "properties");
259
+ css = stripLayer(css, "base");
161
260
  css = css.replace(/:root\s*\{[^}]+\}/g, "");
261
+ css = css.replace(/:root:not\([^)]+\)\s*\{[^}]+\}/g, "");
162
262
  css = css.replace(/\.dark\s*\{[^}]+\}/g, "");
163
263
  css = css.replace(/\[data-theme=[^\]]+\]\s*\{[^}]+\}/g, "");
164
264
  css = css.replace(/\.dark\s*\[data-theme=[^\]]+\]\s*\{[^}]+\}/g, "");
165
- css = `@scope (#addon-${addonId}) {\n*, ::before, ::after { border-color: var(--border); }\n${css}\n}`;
265
+ css = css.replace(/\.font-(?:lato|playfair|dancing|poppins|jetbrains|quicksand|roboto-serif|audiowide|special-elite|press-start|grenze|righteous|creepster|vast-shadow|syncopate|lobster)\s*\{[^}]+\}/g, "");
266
+ css = css.replace(/--font-(?:lato|playfair|dancing|poppins|jetbrains|quicksand|roboto-serif|audiowide|special-elite|press-start|grenze|righteous|creepster|vast-shadow|syncopate|lobster):[^;]+;/g, "");
267
+ css = css.replace(/[^{}]*\[data-color[^{}]*\{[^}]+\}/g, "");
268
+ css = css.replace(/@property\s+--tw-[^{]+\{[^}]+\}/g, "");
269
+ css = css.replace(/\.loader-dots\s*\{[^}]+\}/g, "");
270
+ css = css.replace(/\.loader-dots\s+span[^{]*\{[^}]+\}/g, "");
271
+ css = css.replace(/@keyframes\s+wave-dots\s*\{[\s\S]*?\n\}/g, "");
272
+ css = css.replace(/@keyframes\s+gradient-border\s*\{[\s\S]*?\n\}/g, "");
273
+ css = css.replace(/\.animate-gradient(?:-border|-text)?\s*\{[^}]+\}/g, "");
274
+ css = css.replace(/\.me\s*\{[^}]+\}/g, "");
275
+ css = css.replace(/\.user-border\s*\{[^}]+\}/g, "");
276
+ css = css.replace(/\.scrollbar-hide[^{]*\{[^}]+\}/g, "");
277
+ css = css.replace(/@layer\s+components(?:\s*\{[^}]*\}|;)/g, "");
278
+ css = minifyCss(css);
166
279
  }
167
- const injection = `
280
+ if (css.trim().length > 0) {
281
+ const injection = `
168
282
  (function() {
169
- if (typeof document !== 'undefined' && ${JSON.stringify(css)}.length > 0) {
170
- const style = document.createElement('style');
171
- style.type = 'text/css';
172
- style.textContent = ${JSON.stringify(css)};
173
- document.head.appendChild(style);
283
+ if (typeof document !== 'undefined') {
284
+ var s = document.createElement('style');
285
+ s.type = 'text/css';
286
+ s.textContent = ${JSON.stringify(css)};
287
+ document.head.appendChild(s);
174
288
  }
175
289
  })();
176
290
  `;
177
- fs.writeFileSync(jsPath, injection + js);
291
+ fs.writeFileSync(jsPath, injection + js);
292
+ }
178
293
  try {
179
294
  fs.unlinkSync(cssPath);
180
295
  } catch {}
181
296
  }
297
+ function stripLayer(css, layerName) {
298
+ const regex = new RegExp(`@layer\\s+${layerName}\\s*\\{`);
299
+ let match = regex.exec(css);
300
+ while (match) {
301
+ const startIdx = match.index;
302
+ const openBraceIdx = startIdx + match[0].length - 1;
303
+ let braceCount = 1;
304
+ let endIdx = -1;
305
+ for (let i = openBraceIdx + 1; i < css.length; i++) if (css[i] === "{") braceCount++;
306
+ else if (css[i] === "}") {
307
+ braceCount--;
308
+ if (braceCount === 0) {
309
+ endIdx = i;
310
+ break;
311
+ }
312
+ }
313
+ if (endIdx === -1) break;
314
+ css = css.slice(0, startIdx) + css.slice(endIdx + 1);
315
+ match = regex.exec(css);
316
+ }
317
+ return css;
318
+ }
319
+ function minifyCss(css) {
320
+ return css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").replace(/\s*([{};:,>])\s*/g, "$1").replace(/;}/g, "}").trim();
321
+ }
322
+ function sortValue(child) {
323
+ if (Array.isArray(child)) return child.map(sortValue);
324
+ if (child && typeof child === "object") return Object.fromEntries(Object.entries(child).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortValue(nested)]));
325
+ return child;
326
+ }
182
327
  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
328
  return JSON.stringify(sortValue(value));
189
329
  }
330
+ async function generateAddonVendorLicenses(addonDir, addonName) {
331
+ const title = addonName ? `${addonName} Addon` : "JChat Addon";
332
+ try {
333
+ const proc = Bun.spawn([
334
+ "bun",
335
+ "pm",
336
+ "licenses",
337
+ "--json",
338
+ "--prod"
339
+ ], {
340
+ cwd: addonDir,
341
+ stdout: "pipe",
342
+ stderr: "pipe"
343
+ });
344
+ const output = await new Response(proc.stdout).text();
345
+ await proc.exited;
346
+ const data = JSON.parse(output);
347
+ const lines = [
348
+ "================================================================================",
349
+ `${title} - Third-Party Vendor Licenses & Open Source Attribution`,
350
+ "================================================================================",
351
+ "This distribution incorporates open-source libraries and software components.",
352
+ "Their licenses and attributions are listed below.",
353
+ "================================================================================",
354
+ ""
355
+ ];
356
+ let totalPackages = 0;
357
+ const sortedCategories = Object.keys(data).toSorted((a, b) => a.localeCompare(b));
358
+ for (const licenseCategory of sortedCategories) {
359
+ const packages = data[licenseCategory];
360
+ if (!packages || packages.length === 0) continue;
361
+ lines.push(`\n## License: ${licenseCategory} (${packages.length} packages)\n`);
362
+ for (const pkg of packages) {
363
+ totalPackages++;
364
+ lines.push(`- Package: ${pkg.name}@${pkg.versions.join(", ")}`);
365
+ if (pkg.description) lines.push(` Description: ${pkg.description}`);
366
+ if (pkg.author) lines.push(` Author: ${pkg.author}`);
367
+ if (pkg.homepage) lines.push(` Homepage: ${pkg.homepage}`);
368
+ }
369
+ }
370
+ lines.splice(5, 0, `Total Open-Source Dependencies: ${totalPackages}\n`);
371
+ return lines.join("\n") + "\n";
372
+ } catch {
373
+ return [
374
+ "================================================================================",
375
+ `${title} - Third-Party Vendor Licenses & Open Source Attribution`,
376
+ "================================================================================",
377
+ "This distribution incorporates open-source libraries and software components.",
378
+ "See individual package.json files and dependencies for specific license details.",
379
+ "================================================================================",
380
+ ""
381
+ ].join("\n");
382
+ }
383
+ }
190
384
  //#endregion
191
385
  export { createAddonServerBuildArguments, jchatAddon, requireAddonSigningKey, signAddonManifest };