diff --git a/bundle/lib/plugins/src/_internal/manifest.ts b/bundle/lib/plugins/src/_internal/manifest.ts index 566835b4..26e156d3 100644 --- a/bundle/lib/plugins/src/_internal/manifest.ts +++ b/bundle/lib/plugins/src/_internal/manifest.ts @@ -1,4 +1,5 @@ import type { PluginManifest, PluginVersion } from '../types' +import { formatPluginContributor } from '../utils' import type { InternalPluginManifest } from './types' /** Manifest schema version internal plugins are written against. */ @@ -39,6 +40,8 @@ export function completeInternalManifest( ? parseBundleVersion(manifest.version) : (manifest.version ?? version) manifest.format ??= ManifestFormat + manifest.author = formatPluginContributor(manifest.author) + manifest.contributors = manifest.contributors?.map(formatPluginContributor) if (!isReservedDependency(manifest.id)) { manifest.dependencies ??= {} diff --git a/bundle/lib/plugins/src/_internal/types.ts b/bundle/lib/plugins/src/_internal/types.ts index 1101b6b4..a3118c64 100644 --- a/bundle/lib/plugins/src/_internal/types.ts +++ b/bundle/lib/plugins/src/_internal/types.ts @@ -5,6 +5,7 @@ import type { PluginOptions, PluginOptionsFactory, } from '../types' +import type { PluginContributorInput } from '../utils' import type { PluginSystemErrorPayload } from './errors' import type { addPluginFlags, adoptPluginFlags } from './state' @@ -12,9 +13,12 @@ export type AnyPlugin = Plugin export type InternalPluginManifest = Omit< PluginManifest, - 'version' | 'format' | 'dependencies' + 'version' | 'format' | 'dependencies' | 'author' | 'contributors' > & Partial> & { + /** Contributor object, or legacy string. Normalized to the string form. */ + author: PluginContributorInput | string + contributors?: (PluginContributorInput | string)[] /** * Plugin version. Defaults to the bundle version. * Only stubs (see {@link InternalPluginManifestExtras.defaultSource}) may set it in `manifest.json`, as a string like `1.0.0`. diff --git a/bundle/lib/plugins/src/utils.test.ts b/bundle/lib/plugins/src/utils.test.ts index 408372ad..74cde248 100644 --- a/bundle/lib/plugins/src/utils.test.ts +++ b/bundle/lib/plugins/src/utils.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from 'vitest' -import { getPluginContributorName, parsePluginContributor } from './utils' +import { + formatPluginContributor, + getPluginContributorName, + parsePluginContributor, +} from './utils' const Id1 = '123456789012345678' const Id2 = '876543210987654321' @@ -64,3 +68,36 @@ describe('getPluginContributorName', () => { expect(getPluginContributorName('Jane (Doe)')).toBe('Jane (Doe)') }) }) + +describe('formatPluginContributor', () => { + test('passes strings through', () => { + expect(formatPluginContributor(`Jane <${Id1}>`)).toBe(`Jane <${Id1}>`) + }) + + test('formats objects into a parseable string', () => { + const formatted = formatPluginContributor({ + name: 'Jane Doe', + id: Id1, + links: [ + { url: 'https://example.com/jane', title: 'Website' }, + { url: 'mailto:jane@example.com' }, + ], + }) + + expect(formatted).toBe( + `Jane Doe <${Id1}> (https://example.com/jane "Website") (mailto:jane@example.com)`, + ) + expect(parsePluginContributor(formatted)).toEqual({ + name: 'Jane Doe', + ids: [Id1], + links: [ + { url: 'https://example.com/jane', label: 'Website' }, + { url: 'mailto:jane@example.com', label: undefined }, + ], + }) + }) + + test('formats a name only', () => { + expect(formatPluginContributor({ name: 'Valenced' })).toBe('Valenced') + }) +}) diff --git a/bundle/lib/plugins/src/utils.ts b/bundle/lib/plugins/src/utils.ts index ddb2b388..dba01318 100644 --- a/bundle/lib/plugins/src/utils.ts +++ b/bundle/lib/plugins/src/utils.ts @@ -19,6 +19,31 @@ export interface PluginContributorLink { label?: string } +/** + * Contributor as written in `manifest.json`. Preferred over the legacy `Name (LINK "TITLE")` string, + * which is still accepted. + */ +export interface PluginContributorInput { + name: string + /** Discord user ID. */ + id?: string + links?: { url: string; title?: string }[] +} + +/** Formats a contributor into the `Name (LINK "TITLE")` string form. Legacy strings pass through. */ +export function formatPluginContributor( + contributor: PluginContributorInput | string, +): string { + if (typeof contributor === 'string') return contributor + + const { name, id, links = [] } = contributor + let formatted = name.trim() + if (id !== undefined) formatted += ` <${id}>` + for (const { url, title } of links) + formatted += title === undefined ? ` (${url})` : ` (${url} "${title}")` + return formatted +} + const ContributorEntryRegex = /^(?:<(\d{17,20})>|\(((?:https?:\/\/|mailto:)[^()\s]+)(?:\s+"([^"()]+)")?\))\s*/i diff --git a/bundle/scripts/manifest.test.ts b/bundle/scripts/manifest.test.ts index f3570d91..5726e527 100644 --- a/bundle/scripts/manifest.test.ts +++ b/bundle/scripts/manifest.test.ts @@ -105,6 +105,21 @@ describe('getInternalPluginManifests', () => { }) }) + it('formats stub contributor objects into strings for native', async () => { + await plugin('start', 'settings.themes', { + id: 'valenced.settings.themes', + author: { name: 'Jane', id: '123456789012345678' }, + contributors: ['John', { name: 'Bob' }], + defaultSource: { repo: 'https://example.com' }, + }) + + const [stub] = await getInternalPluginManifests(root) + expect(stub).toMatchObject({ + author: 'Jane <123456789012345678>', + contributors: ['John', 'Bob'], + }) + }) + it('rejects a version on a plugin that is not a stub', async () => { await plugin('start', 'settings', { id: 'valenced.settings', diff --git a/bundle/scripts/manifest.ts b/bundle/scripts/manifest.ts index 20945636..04ed3268 100644 --- a/bundle/scripts/manifest.ts +++ b/bundle/scripts/manifest.ts @@ -2,6 +2,7 @@ import { readdir, readFile } from 'fs/promises' import { dirname } from 'path' import { fileURLToPath } from 'url' import { ReservedDependencyIds } from '../lib/plugins/src/_internal/manifest' +import { formatPluginContributor } from '../lib/plugins/src/utils' import { exists, stringEnv } from './_shared' import type { InternalPluginManifest } from '@valenced/plugins/_' @@ -31,8 +32,14 @@ interface BundleManifestPlugin { } interface RawBundleManifestPlugin - extends Omit, - Pick { + extends Omit< + BundleManifestPlugin, + 'enabledByDefault' | 'defaultSource' | 'author' | 'contributors' + >, + Pick< + InternalPluginManifest, + 'enabledByDefault' | 'defaultSource' | 'author' | 'contributors' + > { build?: { devOnly?: boolean } @@ -114,8 +121,12 @@ export async function getInternalPluginManifests( ...(defaultSource && { name: manifest.name, description: manifest.description, - author: manifest.author, - contributors: manifest.contributors, + author: + manifest.author && + formatPluginContributor(manifest.author), + contributors: manifest.contributors?.map( + formatPluginContributor, + ), icon: manifest.icon, }), ...(manifest.essential && { essential: true }), diff --git a/bundle/src/plugins/preinit/api.hidden/manifest.json b/bundle/src/plugins/preinit/api.hidden/manifest.json index 7dc938c1..a109542f 100644 --- a/bundle/src/plugins/preinit/api.hidden/manifest.json +++ b/bundle/src/plugins/preinit/api.hidden/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.api.hidden", "name": "Developer Mode", "description": "Exposes internal Valenced APIs for debugging.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "WrenchIcon", "enabledByDefault": "dev" } diff --git a/bundle/src/plugins/preinit/api.themes/manifest.json b/bundle/src/plugins/preinit/api.themes/manifest.json index 822df215..f0339dcf 100644 --- a/bundle/src/plugins/preinit/api.themes/manifest.json +++ b/bundle/src/plugins/preinit/api.themes/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.api.themes", "name": "Themes API", "description": "This feature requires an update to be installed.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "PaintPaletteIcon", "version": "0.0.0", "enabledByDefault": true, diff --git a/bundle/src/plugins/preinit/api/manifest.json b/bundle/src/plugins/preinit/api/manifest.json index 469299ce..8db120a2 100644 --- a/bundle/src/plugins/preinit/api/manifest.json +++ b/bundle/src/plugins/preinit/api/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.api", "name": "Plugin API", "description": "Provides the Valenced plugin API.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "PollsIcon", "essential": true, "api": true diff --git a/bundle/src/plugins/preinit/logging/manifest.json b/bundle/src/plugins/preinit/logging/manifest.json index 5e98dcc5..1f999225 100644 --- a/bundle/src/plugins/preinit/logging/manifest.json +++ b/bundle/src/plugins/preinit/logging/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.logging", "name": "Logging", "description": "Logs assisting Valenced developers.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "PaperIcon", "enabledByDefault": true, "build": { diff --git a/bundle/src/plugins/preinit/no-track/manifest.json b/bundle/src/plugins/preinit/no-track/manifest.json index b7985fed..53a8482b 100644 --- a/bundle/src/plugins/preinit/no-track/manifest.json +++ b/bundle/src/plugins/preinit/no-track/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.no-track", "name": "No Track", "description": "Disables Discord and Sentry analytics.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "AnalyticsIcon", "enabledByDefault": true } diff --git a/bundle/src/plugins/preinit/recovery/manifest.json b/bundle/src/plugins/preinit/recovery/manifest.json index 0731aaba..d3072c23 100644 --- a/bundle/src/plugins/preinit/recovery/manifest.json +++ b/bundle/src/plugins/preinit/recovery/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.recovery", "name": "Recovery", "description": "Handles errors and provides troubleshooting options for Valenced.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "ShieldIcon", "essential": true, "dependencies": { diff --git a/bundle/src/plugins/start/settings.plugins/manifest.json b/bundle/src/plugins/start/settings.plugins/manifest.json index b4955e3c..402af9a9 100644 --- a/bundle/src/plugins/start/settings.plugins/manifest.json +++ b/bundle/src/plugins/start/settings.plugins/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.settings.plugins", "name": "Plugin Settings", "description": "Plugin management UI for Valenced.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "PuzzlePieceIcon", "essential": true, "dependencies": { diff --git a/bundle/src/plugins/start/settings.themes/manifest.json b/bundle/src/plugins/start/settings.themes/manifest.json index f5e60c67..8f1bc99e 100644 --- a/bundle/src/plugins/start/settings.themes/manifest.json +++ b/bundle/src/plugins/start/settings.themes/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.settings.themes", "name": "Themes", "description": "This feature requires an update to be installed.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "PaintPaletteIcon", "version": "0.0.0", "enabledByDefault": true, diff --git a/bundle/src/plugins/start/settings/manifest.json b/bundle/src/plugins/start/settings/manifest.json index 5e5a4f95..f4c240c3 100644 --- a/bundle/src/plugins/start/settings/manifest.json +++ b/bundle/src/plugins/start/settings/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.settings", "name": "Settings", "description": "Settings UI for Valenced.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "SettingsIcon", "essential": true } diff --git a/bundle/src/plugins/start/staff-settings/manifest.json b/bundle/src/plugins/start/staff-settings/manifest.json index 242211bf..7a306637 100644 --- a/bundle/src/plugins/start/staff-settings/manifest.json +++ b/bundle/src/plugins/start/staff-settings/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.staff-settings", "name": "Staff Settings", "description": "Allows accessing Discord's Staff Settings.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "StaffBadgeIcon", "enabledByDefault": true } diff --git a/bundle/src/plugins/start/user-badges/manifest.json b/bundle/src/plugins/start/user-badges/manifest.json index e9876857..2c04704e 100644 --- a/bundle/src/plugins/start/user-badges/manifest.json +++ b/bundle/src/plugins/start/user-badges/manifest.json @@ -2,7 +2,10 @@ "id": "valenced.user-badges", "name": "User Badges", "description": "Badges for Valenced contributors and sponsors.", - "author": "Valenced", + "author": { + "name": "Valenced", + "links": [{ "url": "https://discord.com/invite/dtBTQHYSx4", "title": "Support Server" }] + }, "icon": "ShieldUserIcon", "essential": true }