@jchat/addon-sdk 1.0.0-alpha.1 → 1.0.0-alpha.3
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 +9 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.mjs +20 -1
- package/package.json +1 -2
- package/src/contracts.d.ts +0 -127
- package/src/contracts.ts +0 -132
- package/src/diagnostics.d.ts +0 -19
- package/src/diagnostics.test.ts +0 -106
- package/src/diagnostics.ts +0 -182
- package/src/index.d.ts +0 -249
- package/src/index.test.ts +0 -35
- package/src/index.ts +0 -299
- package/src/vite.test.ts +0 -62
- package/src/vite.ts +0 -289
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @jchat/addon-sdk
|
|
2
2
|
|
|
3
|
+
## 1.0.0-alpha.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f7a80fc: - Added `getAddonSdkVersion()` utility export to allow addons and the sandbox environment to safely resolve the runtime SDK version.
|
|
8
|
+
- Updated sandbox developer scripts and Dockerfiles to correctly mount and execute the development environment.
|
|
9
|
+
|
|
10
|
+
## 1.0.0-alpha.2
|
|
11
|
+
|
|
3
12
|
## 1.0.0-alpha.1
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -253,5 +253,19 @@ declare function useAddonSDK(): AddonSDK;
|
|
|
253
253
|
declare function useIcons(): {
|
|
254
254
|
Icons: IconPack;
|
|
255
255
|
};
|
|
256
|
+
/**
|
|
257
|
+
* Get the version of the addon SDK. This is useful for checking compatibility with the host
|
|
258
|
+
* application.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ;```tsx
|
|
262
|
+
* import { getAddonSdkVersion } from "@jchat/addon-sdk"
|
|
263
|
+
*
|
|
264
|
+
* console.log(getAddonSdkVersion())
|
|
265
|
+
* ```
|
|
266
|
+
*
|
|
267
|
+
* @returns
|
|
268
|
+
*/
|
|
269
|
+
declare function getAddonSdkVersion(): string;
|
|
256
270
|
//#endregion
|
|
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 };
|
|
271
|
+
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, getAddonSdkVersion, useAddonSDK, useIcons };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
//#region package.json
|
|
2
|
+
var version = "1.0.0-alpha.3";
|
|
3
|
+
//#endregion
|
|
1
4
|
//#region src/contracts.ts
|
|
2
5
|
/** Valid locations where an addon can be rendered within the Jchat layout. */
|
|
3
6
|
const MOUNT_POINTS = {
|
|
@@ -82,5 +85,21 @@ function useAddonSDK() {
|
|
|
82
85
|
function useIcons() {
|
|
83
86
|
throw new Error("useIcons must be used within the Jchat host environment.");
|
|
84
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the version of the addon SDK. This is useful for checking compatibility with the host
|
|
90
|
+
* application.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ;```tsx
|
|
94
|
+
* import { getAddonSdkVersion } from "@jchat/addon-sdk"
|
|
95
|
+
*
|
|
96
|
+
* console.log(getAddonSdkVersion())
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @returns
|
|
100
|
+
*/
|
|
101
|
+
function getAddonSdkVersion() {
|
|
102
|
+
return version;
|
|
103
|
+
}
|
|
85
104
|
//#endregion
|
|
86
|
-
export { MOUNT_POINTS, defineAddonManifest, defineAddonServer, useAddonSDK, useIcons };
|
|
105
|
+
export { MOUNT_POINTS, defineAddonManifest, defineAddonServer, getAddonSdkVersion, useAddonSDK, useIcons };
|
package/package.json
CHANGED
package/src/contracts.d.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
export type AddonPermission = string;
|
|
2
|
-
/**
|
|
3
|
-
* Defines a single configuration field for an addon's settings panel. These are shown to site
|
|
4
|
-
* admins in the Jchat dashboard.
|
|
5
|
-
*/
|
|
6
|
-
export interface ConfigSchemaField {
|
|
7
|
-
/** Unique key used to store and retrieve the value in `getConfig` */
|
|
8
|
-
key: string;
|
|
9
|
-
/** Data type for validation and UI rendering */
|
|
10
|
-
type: "string" | "number" | "boolean";
|
|
11
|
-
/** Human-readable label shown in the settings UI */
|
|
12
|
-
label: string;
|
|
13
|
-
/** If true, the admin must provide a value before saving */
|
|
14
|
-
required?: boolean;
|
|
15
|
-
/** Whether the value is sensitive (encrypted at rest and masked in the UI) */
|
|
16
|
-
secret?: boolean;
|
|
17
|
-
/** Default value pre-filled in the settings form for new installations */
|
|
18
|
-
default?: string | number | boolean;
|
|
19
|
-
}
|
|
20
|
-
/** Definition of a custom permission introduced by an addon. */
|
|
21
|
-
export interface AddonPermissionDefinition {
|
|
22
|
-
/** Permission identifier (recommended: `addon.<id>.<action>`) */
|
|
23
|
-
name: string;
|
|
24
|
-
/** Short, human-readable name for the permission */
|
|
25
|
-
label: string;
|
|
26
|
-
/** Detailed description of what the permission grants (shown in Rank editor) */
|
|
27
|
-
description: string;
|
|
28
|
-
}
|
|
29
|
-
/** Valid locations where an addon can be rendered within the Jchat layout. */
|
|
30
|
-
export declare const MOUNT_POINTS: {
|
|
31
|
-
/** Top of the main chat column */
|
|
32
|
-
readonly CHAT_HEADER: "chat-header";
|
|
33
|
-
/** Below the main chat header, above messages */
|
|
34
|
-
readonly CHAT_TOOLBAR: "chat-toolbar";
|
|
35
|
-
/** The persistent right-side sidebar */
|
|
36
|
-
readonly CHAT_SIDEBAR: "chat-sidebar";
|
|
37
|
-
/** Top of the left sidebar (room list) */
|
|
38
|
-
readonly SIDEBAR_TOP: "sidebar-top";
|
|
39
|
-
/** Bottom of the left sidebar */
|
|
40
|
-
readonly SIDEBAR_BOTTOM: "sidebar-bottom";
|
|
41
|
-
/** Action list shown when clicking a user's avatar */
|
|
42
|
-
readonly USER_POPOVER_ACTIONS: "user-popover-actions";
|
|
43
|
-
/** Menu shown when right-clicking a message */
|
|
44
|
-
readonly MESSAGE_CONTEXT_MENU: "message-context-menu";
|
|
45
|
-
/** Floating overlay rendered on top of the message stream */
|
|
46
|
-
readonly MESSAGE_FEED_OVERLAY: "message-feed-overlay";
|
|
47
|
-
/** Main admin dashboard area */
|
|
48
|
-
readonly ADMIN_PANEL: "admin-panel";
|
|
49
|
-
/** User or site settings dashboard area */
|
|
50
|
-
readonly SETTINGS_PANEL: "settings-panel";
|
|
51
|
-
/** The 'plus' (+) menu in the message composer */
|
|
52
|
-
readonly CHAT_PLUS_MENU: "chat-plus-menu";
|
|
53
|
-
/** Custom tab in the emoji/gif selection panel */
|
|
54
|
-
readonly EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab";
|
|
55
|
-
/** Full-screen or modal dialog specifically for TubeScan */
|
|
56
|
-
readonly TUBESCAN_DIALOG: "tubescan-dialog";
|
|
57
|
-
};
|
|
58
|
-
/** Union type of all valid addon mount points. */
|
|
59
|
-
export type MountPoint = (typeof MOUNT_POINTS)[keyof typeof MOUNT_POINTS];
|
|
60
|
-
/**
|
|
61
|
-
* The `manifest.json` schema required for every Jchat addon. This file describes how the addon is
|
|
62
|
-
* identified and mounted.
|
|
63
|
-
*/
|
|
64
|
-
export interface AddonManifest {
|
|
65
|
-
/** Unique kebab-case addon identifier (must match the folder name) */
|
|
66
|
-
id: string;
|
|
67
|
-
/** Human-readable addon name */
|
|
68
|
-
name: string;
|
|
69
|
-
/** Semver version string (e.g. `1.2.0`) */
|
|
70
|
-
version: string;
|
|
71
|
-
/** Brief summary of what the addon does */
|
|
72
|
-
description?: string;
|
|
73
|
-
/** Author name, organization, or GitHub handle */
|
|
74
|
-
author?: string;
|
|
75
|
-
/** Support or documentation URL */
|
|
76
|
-
homepage?: string;
|
|
77
|
-
/** SPDX license identifier (e.g. `MIT`) */
|
|
78
|
-
license?: string;
|
|
79
|
-
/** Host and SDK versions this directly executed plugin was built and tested against */
|
|
80
|
-
compatibility: {
|
|
81
|
-
/** Supported JChat versions as a semantic-version range, for example `^1.0.0` */
|
|
82
|
-
jchat: string;
|
|
83
|
-
/** Supported `@jchat/addon-sdk` versions as a semantic-version range */
|
|
84
|
-
addonSdk: string;
|
|
85
|
-
};
|
|
86
|
-
/** Identity of the trusted publisher that signed this package. Required when migrations exist */
|
|
87
|
-
publisher?: {
|
|
88
|
-
id: string;
|
|
89
|
-
};
|
|
90
|
-
/** Signature over the canonical archive digest. Required when migrations exist */
|
|
91
|
-
signature?: {
|
|
92
|
-
algorithm: "rsa-sha256";
|
|
93
|
-
digest: string;
|
|
94
|
-
value: string;
|
|
95
|
-
};
|
|
96
|
-
/** Configuration for the React-based frontend bundle */
|
|
97
|
-
frontend?: {
|
|
98
|
-
/** Relative path to the pre-compiled ESM bundle (created by Vite) */
|
|
99
|
-
bundle: string;
|
|
100
|
-
/** List of UI locations where this addon should be active */
|
|
101
|
-
mountPoints: MountPoint[];
|
|
102
|
-
/** Optional path to a specific UI for the settings panel */
|
|
103
|
-
settings?: string;
|
|
104
|
-
/** Additional shared packages this addon imports from the host */
|
|
105
|
-
externals?: string[];
|
|
106
|
-
};
|
|
107
|
-
/** Configuration for the server-side API bundle */
|
|
108
|
-
server?: {
|
|
109
|
-
/** Relative path to the pre-compiled server bundle */
|
|
110
|
-
bundle?: string;
|
|
111
|
-
/** List of SQL files to run (in order) to set up the addon's database tables */
|
|
112
|
-
migrations?: string[];
|
|
113
|
-
/** List of declarative JSON master-data files to apply once, in order */
|
|
114
|
-
seeds?: string[];
|
|
115
|
-
};
|
|
116
|
-
/**
|
|
117
|
-
* The minimum permission required to see or use this addon. If provided, the host gates
|
|
118
|
-
* visibility in the UI and access to all server routes.
|
|
119
|
-
*/
|
|
120
|
-
accessPermission?: AddonPermission;
|
|
121
|
-
/** Full list of all permissions used by this addon for informational purposes */
|
|
122
|
-
permissions?: AddonPermission[];
|
|
123
|
-
/** Custom permissions that should be registered with the host application on installation */
|
|
124
|
-
permissionDefinitions?: AddonPermissionDefinition[];
|
|
125
|
-
/** Settings form structure shown to site admins. Values are stored encrypted per site */
|
|
126
|
-
configSchema?: ConfigSchemaField[];
|
|
127
|
-
}
|
package/src/contracts.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
export type AddonPermission = string
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Defines a single configuration field for an addon's settings panel. These are shown to site
|
|
5
|
-
* admins in the Jchat dashboard.
|
|
6
|
-
*/
|
|
7
|
-
export interface ConfigSchemaField {
|
|
8
|
-
/** Unique key used to store and retrieve the value in `getConfig` */
|
|
9
|
-
key: string
|
|
10
|
-
/** Data type for validation and UI rendering */
|
|
11
|
-
type: "string" | "number" | "boolean"
|
|
12
|
-
/** Human-readable label shown in the settings UI */
|
|
13
|
-
label: string
|
|
14
|
-
/** If true, the admin must provide a value before saving */
|
|
15
|
-
required?: boolean
|
|
16
|
-
/** Whether the value is sensitive (encrypted at rest and masked in the UI) */
|
|
17
|
-
secret?: boolean
|
|
18
|
-
/** Default value pre-filled in the settings form for new installations */
|
|
19
|
-
default?: string | number | boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Definition of a custom permission introduced by an addon. */
|
|
23
|
-
export interface AddonPermissionDefinition {
|
|
24
|
-
/** Permission identifier (recommended: `addon.<id>.<action>`) */
|
|
25
|
-
name: string
|
|
26
|
-
/** Short, human-readable name for the permission */
|
|
27
|
-
label: string
|
|
28
|
-
/** Detailed description of what the permission grants (shown in Rank editor) */
|
|
29
|
-
description: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Valid locations where an addon can be rendered within the Jchat layout. */
|
|
33
|
-
export const MOUNT_POINTS = {
|
|
34
|
-
/** Top of the main chat column */
|
|
35
|
-
CHAT_HEADER: "chat-header",
|
|
36
|
-
/** Below the main chat header, above messages */
|
|
37
|
-
CHAT_TOOLBAR: "chat-toolbar",
|
|
38
|
-
/** The persistent right-side sidebar */
|
|
39
|
-
CHAT_SIDEBAR: "chat-sidebar",
|
|
40
|
-
/** Top of the left sidebar (room list) */
|
|
41
|
-
SIDEBAR_TOP: "sidebar-top",
|
|
42
|
-
/** Bottom of the left sidebar */
|
|
43
|
-
SIDEBAR_BOTTOM: "sidebar-bottom",
|
|
44
|
-
/** Action list shown when clicking a user's avatar */
|
|
45
|
-
USER_POPOVER_ACTIONS: "user-popover-actions",
|
|
46
|
-
/** Menu shown when right-clicking a message */
|
|
47
|
-
MESSAGE_CONTEXT_MENU: "message-context-menu",
|
|
48
|
-
/** Floating overlay rendered on top of the message stream */
|
|
49
|
-
MESSAGE_FEED_OVERLAY: "message-feed-overlay",
|
|
50
|
-
/** Main admin dashboard area */
|
|
51
|
-
ADMIN_PANEL: "admin-panel",
|
|
52
|
-
/** User or site settings dashboard area */
|
|
53
|
-
SETTINGS_PANEL: "settings-panel",
|
|
54
|
-
/** The 'plus' (+) menu in the message composer */
|
|
55
|
-
CHAT_PLUS_MENU: "chat-plus-menu",
|
|
56
|
-
/** Custom tab in the emoji/gif selection panel */
|
|
57
|
-
EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab",
|
|
58
|
-
/** Full-screen or modal dialog specifically for TubeScan */
|
|
59
|
-
TUBESCAN_DIALOG: "tubescan-dialog"
|
|
60
|
-
} as const
|
|
61
|
-
|
|
62
|
-
/** Union type of all valid addon mount points. */
|
|
63
|
-
export type MountPoint = (typeof MOUNT_POINTS)[keyof typeof MOUNT_POINTS]
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The `manifest.json` schema required for every Jchat addon. This file describes how the addon is
|
|
67
|
-
* identified and mounted.
|
|
68
|
-
*/
|
|
69
|
-
export interface AddonManifest {
|
|
70
|
-
/** Unique kebab-case addon identifier (must match the folder name) */
|
|
71
|
-
id: string
|
|
72
|
-
/** Human-readable addon name */
|
|
73
|
-
name: string
|
|
74
|
-
/** Semver version string (e.g. `1.2.0`) */
|
|
75
|
-
version: string
|
|
76
|
-
/** Brief summary of what the addon does */
|
|
77
|
-
description?: string
|
|
78
|
-
/** Author name, organization, or GitHub handle */
|
|
79
|
-
author?: string
|
|
80
|
-
/** Support or documentation URL */
|
|
81
|
-
homepage?: string
|
|
82
|
-
/** SPDX license identifier (e.g. `MIT`) */
|
|
83
|
-
license?: string
|
|
84
|
-
/** Host and SDK versions this directly executed plugin was built and tested against */
|
|
85
|
-
compatibility: {
|
|
86
|
-
/** Supported JChat versions as a semantic-version range, for example `^1.0.0` */
|
|
87
|
-
jchat: string
|
|
88
|
-
/** Supported `@jchat/addon-sdk` versions as a semantic-version range */
|
|
89
|
-
addonSdk: string
|
|
90
|
-
}
|
|
91
|
-
/** Identity of the trusted publisher that signed this package. Required when migrations exist */
|
|
92
|
-
publisher?: {
|
|
93
|
-
id: string
|
|
94
|
-
}
|
|
95
|
-
/** Signature over the canonical archive digest. Required when migrations exist */
|
|
96
|
-
signature?: {
|
|
97
|
-
algorithm: "rsa-sha256"
|
|
98
|
-
digest: string
|
|
99
|
-
value: string
|
|
100
|
-
}
|
|
101
|
-
/** Configuration for the React-based frontend bundle */
|
|
102
|
-
frontend?: {
|
|
103
|
-
/** Relative path to the pre-compiled ESM bundle (created by Vite) */
|
|
104
|
-
bundle: string
|
|
105
|
-
/** List of UI locations where this addon should be active */
|
|
106
|
-
mountPoints: MountPoint[]
|
|
107
|
-
/** Optional path to a specific UI for the settings panel */
|
|
108
|
-
settings?: string
|
|
109
|
-
/** Additional shared packages this addon imports from the host */
|
|
110
|
-
externals?: string[]
|
|
111
|
-
}
|
|
112
|
-
/** Configuration for the server-side API bundle */
|
|
113
|
-
server?: {
|
|
114
|
-
/** Relative path to the pre-compiled server bundle */
|
|
115
|
-
bundle?: string
|
|
116
|
-
/** List of SQL files to run (in order) to set up the addon's database tables */
|
|
117
|
-
migrations?: string[]
|
|
118
|
-
/** List of declarative JSON master-data files to apply once, in order */
|
|
119
|
-
seeds?: string[]
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* The minimum permission required to see or use this addon. If provided, the host gates
|
|
123
|
-
* visibility in the UI and access to all server routes.
|
|
124
|
-
*/
|
|
125
|
-
accessPermission?: AddonPermission
|
|
126
|
-
/** Full list of all permissions used by this addon for informational purposes */
|
|
127
|
-
permissions?: AddonPermission[]
|
|
128
|
-
/** Custom permissions that should be registered with the host application on installation */
|
|
129
|
-
permissionDefinitions?: AddonPermissionDefinition[]
|
|
130
|
-
/** Settings form structure shown to site admins. Values are stored encrypted per site */
|
|
131
|
-
configSchema?: ConfigSchemaField[]
|
|
132
|
-
}
|
package/src/diagnostics.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { AddonManifest } from "./contracts";
|
|
2
|
-
export type AddonDiagnosticStatus = "pass" | "warning" | "error";
|
|
3
|
-
export interface AddonPackageDiagnostic {
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
status: AddonDiagnosticStatus;
|
|
7
|
-
message: string;
|
|
8
|
-
}
|
|
9
|
-
export interface AddonPackageDiagnosticOptions {
|
|
10
|
-
buildDir: string;
|
|
11
|
-
manifest: AddonManifest;
|
|
12
|
-
jchatVersion: string;
|
|
13
|
-
addonSdkVersion: string;
|
|
14
|
-
productionPackage?: boolean;
|
|
15
|
-
maxBytes?: number;
|
|
16
|
-
}
|
|
17
|
-
export declare function createCompatibleMajorRange(version: string): string;
|
|
18
|
-
export declare function diagnoseAddonPackage(options: AddonPackageDiagnosticOptions): AddonPackageDiagnostic[];
|
|
19
|
-
export declare function hasAddonDiagnosticErrors(diagnostics: readonly AddonPackageDiagnostic[]): boolean;
|
package/src/diagnostics.test.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import * as fs from "node:fs"
|
|
3
|
-
import * as os from "node:os"
|
|
4
|
-
import * as path from "node:path"
|
|
5
|
-
import {
|
|
6
|
-
createCompatibleMajorRange,
|
|
7
|
-
diagnoseAddonPackage,
|
|
8
|
-
hasAddonDiagnosticErrors
|
|
9
|
-
} from "./diagnostics"
|
|
10
|
-
|
|
11
|
-
describe("addon package diagnostics", () => {
|
|
12
|
-
test("accepts compatible builds with every declared file", () => {
|
|
13
|
-
const buildDir = fs.mkdtempSync(path.join(os.tmpdir(), "jchat-addon-diagnostics-"))
|
|
14
|
-
fs.writeFileSync(path.join(buildDir, "ui.js"), "export default function Addon() {}")
|
|
15
|
-
fs.writeFileSync(path.join(buildDir, "api.js"), "export default () => {}")
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const diagnostics = diagnoseAddonPackage({
|
|
19
|
-
buildDir,
|
|
20
|
-
manifest: {
|
|
21
|
-
id: "sample-addon",
|
|
22
|
-
name: "Sample addon",
|
|
23
|
-
version: "1.0.0",
|
|
24
|
-
compatibility: {
|
|
25
|
-
jchat: ">=1.0.0-alpha.0 <2.0.0",
|
|
26
|
-
addonSdk: "^1.0.0"
|
|
27
|
-
},
|
|
28
|
-
frontend: { bundle: "./ui.js", mountPoints: ["chat-toolbar"] },
|
|
29
|
-
server: { bundle: "./api.js" }
|
|
30
|
-
},
|
|
31
|
-
jchatVersion: "1.0.0-alpha.0",
|
|
32
|
-
addonSdkVersion: "1.0.0"
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
expect(hasAddonDiagnosticErrors(diagnostics)).toBe(false)
|
|
36
|
-
expect(diagnostics.find(({ id }) => id === "jchat")?.status).toBe("pass")
|
|
37
|
-
expect(diagnostics.find(({ id }) => id === "server-bundle")?.status).toBe("pass")
|
|
38
|
-
} finally {
|
|
39
|
-
fs.rmSync(buildDir, { recursive: true })
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test("reports incompatible versions, missing files, and unsafe references", () => {
|
|
44
|
-
const buildDir = fs.mkdtempSync(path.join(os.tmpdir(), "jchat-addon-diagnostics-"))
|
|
45
|
-
try {
|
|
46
|
-
const diagnostics = diagnoseAddonPackage({
|
|
47
|
-
buildDir,
|
|
48
|
-
manifest: {
|
|
49
|
-
id: "sample-addon",
|
|
50
|
-
name: "Sample addon",
|
|
51
|
-
version: "1.0.0",
|
|
52
|
-
compatibility: { jchat: "^2.0.0", addonSdk: "^1.0.0" },
|
|
53
|
-
frontend: { bundle: "../ui.js", mountPoints: [] },
|
|
54
|
-
server: { bundle: "./missing-api.js" }
|
|
55
|
-
},
|
|
56
|
-
jchatVersion: "1.0.0-alpha.0",
|
|
57
|
-
addonSdkVersion: "1.0.0"
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
expect(hasAddonDiagnosticErrors(diagnostics)).toBe(true)
|
|
61
|
-
expect(diagnostics.find(({ id }) => id === "jchat")?.status).toBe("error")
|
|
62
|
-
expect(diagnostics.find(({ id }) => id === "frontend-bundle")?.status).toBe("error")
|
|
63
|
-
expect(diagnostics.find(({ id }) => id === "server-bundle")?.status).toBe("error")
|
|
64
|
-
} finally {
|
|
65
|
-
fs.rmSync(buildDir, { recursive: true })
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test("creates a prerelease-compatible range for the current major", () => {
|
|
70
|
-
expect(createCompatibleMajorRange("1.0.0-alpha.0")).toBe(">=1.0.0-alpha.0 <2.0.0")
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test("rejects malformed versions using Bun semver validation", () => {
|
|
74
|
-
expect(() => createCompatibleMajorRange("1.invalid.0")).toThrow(
|
|
75
|
-
"Invalid semantic version: 1.invalid.0"
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test("measures the distributable archive for production package size", () => {
|
|
80
|
-
const buildDir = fs.mkdtempSync(path.join(os.tmpdir(), "jchat-addon-diagnostics-"))
|
|
81
|
-
fs.writeFileSync(path.join(buildDir, "ui.js"), "unpacked content is intentionally larger")
|
|
82
|
-
fs.writeFileSync(path.join(buildDir, "sample-addon-1.0.0.zip"), "zip")
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const diagnostics = diagnoseAddonPackage({
|
|
86
|
-
buildDir,
|
|
87
|
-
manifest: {
|
|
88
|
-
id: "sample-addon",
|
|
89
|
-
name: "Sample addon",
|
|
90
|
-
version: "1.0.0",
|
|
91
|
-
compatibility: { jchat: "^1.0.0", addonSdk: "^1.0.0" },
|
|
92
|
-
frontend: { bundle: "./ui.js", mountPoints: ["chat-toolbar"] }
|
|
93
|
-
},
|
|
94
|
-
jchatVersion: "1.0.0",
|
|
95
|
-
addonSdkVersion: "1.0.0",
|
|
96
|
-
productionPackage: true,
|
|
97
|
-
maxBytes: 3
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
expect(diagnostics.find(({ id }) => id === "package-size")?.status).toBe("pass")
|
|
101
|
-
expect(diagnostics.find(({ id }) => id === "production-archive")?.status).toBe("pass")
|
|
102
|
-
} finally {
|
|
103
|
-
fs.rmSync(buildDir, { recursive: true })
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
})
|
package/src/diagnostics.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs"
|
|
2
|
-
import * as path from "node:path"
|
|
3
|
-
import type { AddonManifest } from "./contracts"
|
|
4
|
-
|
|
5
|
-
export type AddonDiagnosticStatus = "pass" | "warning" | "error"
|
|
6
|
-
|
|
7
|
-
export interface AddonPackageDiagnostic {
|
|
8
|
-
id: string
|
|
9
|
-
label: string
|
|
10
|
-
status: AddonDiagnosticStatus
|
|
11
|
-
message: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface AddonPackageDiagnosticOptions {
|
|
15
|
-
buildDir: string
|
|
16
|
-
manifest: AddonManifest
|
|
17
|
-
jchatVersion: string
|
|
18
|
-
addonSdkVersion: string
|
|
19
|
-
productionPackage?: boolean
|
|
20
|
-
maxBytes?: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createCompatibleMajorRange(version: string): string {
|
|
24
|
-
try {
|
|
25
|
-
Bun.semver.order(version, version)
|
|
26
|
-
} catch {
|
|
27
|
-
throw new Error(`Invalid semantic version: ${version}`)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const majorComponent = /^(?:v|=)?(\d+)\./.exec(version)?.[1]
|
|
31
|
-
const major = majorComponent ? Number(majorComponent) : Number.NaN
|
|
32
|
-
if (!Number.isSafeInteger(major)) throw new Error(`Invalid semantic version: ${version}`)
|
|
33
|
-
|
|
34
|
-
return `>=${version} <${major + 1}.0.0`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function diagnoseAddonPackage(
|
|
38
|
-
options: AddonPackageDiagnosticOptions
|
|
39
|
-
): AddonPackageDiagnostic[] {
|
|
40
|
-
const { buildDir, manifest } = options
|
|
41
|
-
const diagnostics: AddonPackageDiagnostic[] = []
|
|
42
|
-
diagnostics.push(
|
|
43
|
-
compatibilityDiagnostic(
|
|
44
|
-
"jchat",
|
|
45
|
-
"JChat compatibility",
|
|
46
|
-
options.jchatVersion,
|
|
47
|
-
manifest.compatibility?.jchat
|
|
48
|
-
),
|
|
49
|
-
compatibilityDiagnostic(
|
|
50
|
-
"addon-sdk",
|
|
51
|
-
"Addon SDK compatibility",
|
|
52
|
-
options.addonSdkVersion,
|
|
53
|
-
manifest.compatibility?.addonSdk
|
|
54
|
-
)
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
const references = [
|
|
58
|
-
["frontend-bundle", "Frontend bundle", manifest.frontend?.bundle],
|
|
59
|
-
["server-bundle", "Server bundle", manifest.server?.bundle],
|
|
60
|
-
...(manifest.server?.migrations ?? []).map(
|
|
61
|
-
(file) => [`migration:${file}`, `Migration ${file}`, file] as const
|
|
62
|
-
),
|
|
63
|
-
...(manifest.server?.seeds ?? []).map((file) => [`seed:${file}`, `Seed ${file}`, file] as const)
|
|
64
|
-
] as const
|
|
65
|
-
|
|
66
|
-
for (const [id, label, reference] of references) {
|
|
67
|
-
if (!reference) continue
|
|
68
|
-
const file = resolveBuildFile(buildDir, reference)
|
|
69
|
-
diagnostics.push({
|
|
70
|
-
id,
|
|
71
|
-
label,
|
|
72
|
-
status: file && fs.existsSync(file) && fs.statSync(file).isFile() ? "pass" : "error",
|
|
73
|
-
message:
|
|
74
|
-
file && fs.existsSync(file) && fs.statSync(file).isFile()
|
|
75
|
-
? reference
|
|
76
|
-
: `Missing or unsafe build file: ${reference}`
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const files = listRegularFiles(buildDir)
|
|
81
|
-
const productionArchive = path.join(buildDir, `${manifest.id}-${manifest.version}.zip`)
|
|
82
|
-
const hasProductionArchive =
|
|
83
|
-
fs.existsSync(productionArchive) && fs.statSync(productionArchive).isFile()
|
|
84
|
-
const totalBytes =
|
|
85
|
-
options.productionPackage && hasProductionArchive
|
|
86
|
-
? fs.statSync(productionArchive).size
|
|
87
|
-
: files
|
|
88
|
-
.filter((file) => file !== productionArchive)
|
|
89
|
-
.reduce((total, file) => total + fs.statSync(file).size, 0)
|
|
90
|
-
const maxBytes = options.maxBytes ?? 25 * 1024 * 1024
|
|
91
|
-
diagnostics.push({
|
|
92
|
-
id: "package-size",
|
|
93
|
-
label: "Package size",
|
|
94
|
-
status: totalBytes <= maxBytes ? "pass" : "error",
|
|
95
|
-
message: `${totalBytes} of ${maxBytes} bytes`
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const sourceMaps = files.filter((file) => file.endsWith(".map"))
|
|
99
|
-
diagnostics.push({
|
|
100
|
-
id: "source-maps",
|
|
101
|
-
label: "Source maps",
|
|
102
|
-
status: options.productionPackage && sourceMaps.length ? "warning" : "pass",
|
|
103
|
-
message: sourceMaps.length ? `${sourceMaps.length} source map(s) present` : "No source maps"
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
const hasDatabaseFiles = Boolean(
|
|
107
|
-
manifest.server?.migrations?.length || manifest.server?.seeds?.length
|
|
108
|
-
)
|
|
109
|
-
diagnostics.push({
|
|
110
|
-
id: "database-signature",
|
|
111
|
-
label: "Database file signature",
|
|
112
|
-
status:
|
|
113
|
-
hasDatabaseFiles && options.productionPackage && !manifest.signature?.value
|
|
114
|
-
? "error"
|
|
115
|
-
: hasDatabaseFiles && !manifest.signature?.value
|
|
116
|
-
? "warning"
|
|
117
|
-
: "pass",
|
|
118
|
-
message:
|
|
119
|
-
hasDatabaseFiles && !manifest.signature?.value
|
|
120
|
-
? options.productionPackage
|
|
121
|
-
? "Production database files require a publisher signature"
|
|
122
|
-
: "Unsigned database files are allowed only in the developer sandbox"
|
|
123
|
-
: hasDatabaseFiles
|
|
124
|
-
? "Publisher signature present"
|
|
125
|
-
: "No database files declared"
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
if (options.productionPackage) {
|
|
129
|
-
diagnostics.push({
|
|
130
|
-
id: "production-archive",
|
|
131
|
-
label: "Production archive",
|
|
132
|
-
status: hasProductionArchive ? "pass" : "error",
|
|
133
|
-
message: hasProductionArchive
|
|
134
|
-
? path.basename(productionArchive)
|
|
135
|
-
: "Production archive is missing"
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return diagnostics
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function hasAddonDiagnosticErrors(diagnostics: readonly AddonPackageDiagnostic[]): boolean {
|
|
143
|
-
return diagnostics.some(({ status }) => status === "error")
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function compatibilityDiagnostic(
|
|
147
|
-
id: string,
|
|
148
|
-
label: string,
|
|
149
|
-
version: string,
|
|
150
|
-
range: string | undefined
|
|
151
|
-
): AddonPackageDiagnostic {
|
|
152
|
-
const compatible = Boolean(range && Bun.semver.satisfies(version, range))
|
|
153
|
-
return {
|
|
154
|
-
id,
|
|
155
|
-
label,
|
|
156
|
-
status: compatible ? "pass" : "error",
|
|
157
|
-
message: range
|
|
158
|
-
? `${version} ${compatible ? "satisfies" : "does not satisfy"} ${range}`
|
|
159
|
-
: "Compatibility range is missing"
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function resolveBuildFile(buildDir: string, reference: string): string | null {
|
|
164
|
-
if (
|
|
165
|
-
path.isAbsolute(reference) ||
|
|
166
|
-
reference.includes("\\") ||
|
|
167
|
-
reference.split("/").includes("..")
|
|
168
|
-
) {
|
|
169
|
-
return null
|
|
170
|
-
}
|
|
171
|
-
const target = path.resolve(buildDir, ...reference.replace(/^\.\//, "").split("/"))
|
|
172
|
-
const relative = path.relative(path.resolve(buildDir), target)
|
|
173
|
-
return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? target : null
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function listRegularFiles(root: string): string[] {
|
|
177
|
-
if (!fs.existsSync(root)) return []
|
|
178
|
-
return fs
|
|
179
|
-
.readdirSync(root, { recursive: true, withFileTypes: true })
|
|
180
|
-
.filter((entry) => entry.isFile())
|
|
181
|
-
.map((entry) => path.join(entry.parentPath, entry.name))
|
|
182
|
-
}
|