Files
Termix/scripts/check-sdk-ui-exports.cjs
T

800 lines
19 KiB
JavaScript

#!/usr/bin/env node
/**
* @termix-ssh/plugin-sdk/ui is public API, implemented by
* src/ui/plugin-host/sdk-ui.ts. Adding to it is a contract change and
* removing from it is a breaking one, so neither should happen by accident.
*
* Resolves every name the module exports (through its `export *` lines too)
* and fails when that differs from ALLOWED. A new export goes on the list in
* the same change, which is what makes it a deliberate one. Only generic
* shell pieces belong here: anything one feature needs lives in its plugin.
*
* --list prints what the module exports now.
*/
const path = require("node:path");
const ROOT = path.resolve(__dirname, "..");
const SDK_UI = path.join(ROOT, "src", "ui", "plugin-host", "sdk-ui.ts");
const TSCONFIG = path.join(ROOT, "tsconfig.app.json");
const ALLOWED = [
"ACCENT_PRESET_COLORS",
"APP_VERSION",
"AccessRecord",
"ActionSlot",
"ActiveSessionInfo",
"AddButton",
"AdminSection",
"Alert",
"AlertDescription",
"AlertDialog",
"AlertDialogAction",
"AlertDialogCancel",
"AlertDialogContent",
"AlertDialogDescription",
"AlertDialogFooter",
"AlertDialogHeader",
"AlertDialogOverlay",
"AlertDialogPortal",
"AlertDialogTitle",
"AlertDialogTrigger",
"AlertTitle",
"ApiKey",
"AuditLog",
"AuditLogFilters",
"AuthEnrollmentSections",
"AuthOverrideProtocol",
"BackButton",
"Badge",
"BarSeries",
"BarSeriesItem",
"BarSeriesProps",
"BrandingSettings",
"BrowserSignInDialog",
"Button",
"ButtonProps",
"CORE_HOST_DEFAULTS",
"CORE_NAMESPACE",
"CREDENTIAL_ROW_FIELD_KEYS",
"Card",
"CardAction",
"CardContent",
"CardDescription",
"CardFooter",
"CardGridCanvas",
"CardHeader",
"CardMasonry",
"CardTitle",
"Checkbox",
"ColumnCountStepper",
"Command",
"CommandEmpty",
"CommandGroup",
"CommandItem",
"CommandList",
"CommandPrimitive",
"ComponentSlot",
"ConfirmOptions",
"ConnectionGate",
"ConnectionGateController",
"ConnectionGateProps",
"ConnectionLogPanel",
"ConnectionLogProvider",
"ConnectionOrigin",
"ConnectionScreen",
"ConnectionStage",
"ConnectionStatus",
"ConnectionUnavailable",
"CreatedApiKey",
"Credential",
"CredentialDensity",
"CredentialPermissionLevel",
"CredentialRowFields",
"CredentialSidebarFilterState",
"CredentialSidebarPreferences",
"CredentialSortKey",
"CredentialTrayTrigger",
"CustomKeybinding",
"DASHBOARD_CARDS",
"DEFAULT_FOLDER_COLOR",
"DEFAULT_FOLDER_ICON",
"DashboardCardId",
"DataColumn",
"DataView",
"DefaultOverrides",
"DefaultsOnly",
"DesktopUpdateMode",
"DesktopUpdateState",
"Dialog",
"DialogClose",
"DialogContent",
"DialogDescription",
"DialogFooter",
"DialogHeader",
"DialogOverlay",
"DialogPortal",
"DialogTitle",
"DialogTrigger",
"DocsLink",
"DocsLinkProps",
"DocsPage",
"DropdownMenu",
"DropdownMenuCheckboxItem",
"DropdownMenuContent",
"DropdownMenuGroup",
"DropdownMenuItem",
"DropdownMenuLabel",
"DropdownMenuPortal",
"DropdownMenuRadioGroup",
"DropdownMenuRadioItem",
"DropdownMenuSeparator",
"DropdownMenuShortcut",
"DropdownMenuSub",
"DropdownMenuSubContent",
"DropdownMenuSubTrigger",
"DropdownMenuTrigger",
"EditingWidth",
"EmptyState",
"FOLDER_COLORS",
"FONT_SIZES",
"Facts",
"FakeSwitch",
"Field",
"FieldPair",
"FolderAccessRule",
"FolderIconEl",
"FontSizeId",
"FormFooter",
"FullScreenAppPhase",
"FullScreenAppWrapper",
"GridCardCatalogEntry",
"GridColSpan",
"GridColumns",
"GridLayout",
"GridSlot",
"GroupHeading",
"HexColorPicker",
"Host",
"HostActionDef",
"HostClickBehavior",
"HostData",
"HostDefaultBadge",
"HostDefaultField",
"HostDefaultsChange",
"HostDefaultsContext",
"HostDefaultsContextValue",
"HostDefaultsLevel",
"HostDefaultsTarget",
"HostDensity",
"HostFeatureFields",
"HostFeatureFieldsProps",
"HostFolder",
"HostGroupKey",
"HostKeyVerificationDialog",
"HostOnly",
"HostProtocolAuthInput",
"HostProtocolDef",
"HostProtocols",
"HostRowFields",
"HostSidebarFilterState",
"HostSidebarPreferences",
"HostTrayTrigger",
"IconPicker",
"InlineView",
"Input",
"Kbd",
"KbdKey",
"KbdSeparator",
"KeyCombo",
"KeybindingAction",
"KeybindingActionDef",
"KeybindingActionType",
"LANGUAGES",
"Label",
"LineChart",
"LineChartSeries",
"LinkServerDialog",
"LinkedAccountInfo",
"ListBadge",
"ListRow",
"ListRowAction",
"ListRowFolder",
"ListRowTone",
"LogContext",
"LogEntry",
"LookSettings",
"MANAGE_REQUEST_EVENT",
"MAX_PANES",
"MFAPromptMode",
"ManageMode",
"ManageRequest",
"ManagerCardError",
"ManagerCardShell",
"ManagerSearch",
"Meter",
"MetricCard",
"MiniStat",
"NumberField",
"OPEN_PLUGINS_EVENT",
"OpenTabRecord",
"PANEL",
"PROMPT_BUTTON",
"PROMPT_DESTRUCTIVE_BUTTON",
"PROMPT_PRIMARY_BUTTON",
"PaletteItemDef",
"PanePrompt",
"PanelDensity",
"PanelList",
"PanelSearch",
"PanelShell",
"PanelViewMode",
"PassphraseDialog",
"PasswordInput",
"PermissionCatalogEntry",
"PluginComponent",
"PRESETS",
"PluginIcon",
"PluginSettingsErrors",
"PluginSettingsField",
"PluginSettingsValidationError",
"PluginSummary",
"PluginViewPlaceholder",
"Popover",
"PopoverAnchor",
"PopoverContent",
"PopoverTrigger",
"ProxyNode",
"ProxySettingsFields",
"RadialGauge",
"RadialGaugeProps",
"RailGroup",
"RailItemDef",
"RecentActivityItem",
"ReleaseInfo",
"Repeater",
"ResolvedHostDefault",
"RobustClipboardProvider",
"Role",
"SSHAuthDialog",
"SSHHost",
"SSHHostData",
"SSHHostWithStatus",
"SSH_AUTH_PROTOCOL",
"ScrollArea",
"ScrollBar",
"SecretField",
"SectionCard",
"Segmented",
"Select",
"Select2",
"SelectContent",
"SelectField",
"SelectGroup",
"SelectItem",
"SelectLabel",
"SelectScrollDownButton",
"SelectScrollUpButton",
"SelectSeparator",
"SelectTrigger",
"SelectValue",
"Separator",
"SettingRow",
"SettingsFieldRow",
"SharePermissionLevel",
"ShareTarget",
"Sheet",
"SheetClose",
"SheetContent",
"SheetDescription",
"SheetFooter",
"SheetHeader",
"SheetTitle",
"SheetTrigger",
"ShellConfirmCapture",
"Skeleton",
"Slider",
"SortKey",
"Sparkline",
"SparklineProps",
"SplitLayoutMenu",
"SplitOpenTarget",
"SplitSummary",
"StatRow",
"StatusValue",
"SurfaceKind",
"SurfaceScope",
"Switch",
"SwitchRow",
"SyncBasicAuth",
"SyncConflictItem",
"SyncEntityInfo",
"SyncErrorItem",
"SyncProxyHeader",
"SyncStatus",
"TOTPDialog",
"Tab",
"TabShellCallbacks",
"TabSplitAction",
"TabStage",
"TabStrip",
"TabStripItem",
"TabType",
"TagInput",
"TextAreaField",
"TextField",
"Textarea",
"TextareaProps",
"ThemeId",
"TlsStatus",
"ToolsTab",
"Tooltip",
"TooltipContent",
"TooltipProvider",
"TooltipTrigger",
"UI_AREA_KEYS",
"UI_FONTS",
"UiAreaKey",
"UiFontId",
"UiPreset",
"UpdateChannelState",
"UptimeInfo",
"UsagePair",
"UserPreferences",
"UserRole",
"UserSecondFactorSummary",
"VersionBadge",
"VersionInfo",
"ViewToggle",
"WebSocketConnectionTarget",
"WidgetTitle",
"actionOrder",
"activityTarget",
"adminCreateUser",
"adminCreateUserCredential",
"adminCreateUserHost",
"adminDeleteUserCredential",
"adminDeleteUserHost",
"adminExportUserData",
"adminGetHostPassword",
"adminGetUserCredentials",
"adminGetUserHosts",
"adminResetUserPassword",
"adminUpdateUserCredential",
"adminUpdateUserHost",
"applyAccentColor",
"applyFontSize",
"applyPresetSideEffects",
"applyUiFont",
"assignRoleToUser",
"beginSplitDrag",
"buildOriginWsUrl",
"bulkUpdateSSHHosts",
"canConfigureUpdates",
"canJoinSplit",
"changeAppLanguage",
"changePassword",
"clearLocalAdaptivePreferences",
"cn",
"copyToClipboard",
"createApiKey",
"createCredential",
"createFrontendLogger",
"createQuickConnectHost",
"createRole",
"createSSHHost",
"defaultConnectAction",
"defaultCredentialRowFields",
"defaultCredentialSidebarPreferences",
"defaultHostRowFields",
"defaultHostSidebarPreferences",
"defaultValuesEqual",
"deleteAccount",
"deleteAllHostsInFolder",
"deleteApiKey",
"deleteCredential",
"deleteOpenTab",
"deleteRole",
"deleteSSHHost",
"deleteUser",
"desktopUpdates",
"docsUrl",
"duplicateCredential",
"enabledHostProtocols",
"endSplitDrag",
"filterPaletteItems",
"findCoreHostDefault",
"findMatchingKeybinding",
"getActiveSessions",
"getAdaptiveResourceBudget",
"getApiKeys",
"getAuditLogActions",
"getAuditLogs",
"getBasePath",
"getBranding",
"getChannelReleases",
"getConnectedRemoteApi",
"getCredentialAccess",
"getCredentialDetails",
"getCredentialSidebarPreferences",
"getCredentials",
"getDatabaseHealth",
"getDatabaseTransferUrl",
"getDefaultConnectionTab",
"getDeveloperMode",
"getDeviceId",
"getErrorMessage",
"getExternalAutoProvision",
"getFolderAccess",
"getFolderDefaultsId",
"getHostAccess",
"getHostAuthOverride",
"getHostDefaultsLevel",
"getHostEditorSection",
"getHostPassword",
"getHostSidebarPreferences",
"getHostTags",
"getLiveHostStatus",
"getLogLevel",
"getNotificationPrivateEndpoints",
"getPanel",
"getPasswordLoginAllowed",
"getPasswordResetAllowed",
"getPermissionsCatalog",
"getPluginAdminSettings",
"getPluginHostSettings",
"getPluginUserSettings",
"getPlugins",
"getPollingEnvironmentMultiplier",
"getPreferredHostAction",
"getRecentActivity",
"getRegisteredDashboardCard",
"getRegistrationAllowed",
"getRoles",
"getSSHFolders",
"getSSHHosts",
"getSecondFactorAfterExternalLogin",
"getSessionTimeout",
"getSessionTimeoutMinutes",
"getSessions",
"getStatusCheckSettings",
"getStatusClasses",
"getSyncConflicts",
"getSyncErrors",
"getTabType",
"getTlsStatus",
"getUpdateChannel",
"getUptime",
"getUserInfo",
"getUserList",
"getUserPreferences",
"getUserRoles",
"getUserSecondFactors",
"getVersionInfo",
"globalShortcutHandler",
"groupRailItems",
"hasVisibleFields",
"hostActionsFor",
"hostBadgesFor",
"hostEditorToolbarItemsFor",
"hostMenuItemsFor",
"hostProtocolFlags",
"hostStatusIndicatorsFor",
"hydrateLocalSharedHostAuth",
"invalidateHostsAndStatusCaches",
"invalidateServerStatusCache",
"isCoreRailView",
"isElectron",
"isFieldShown",
"isMacPlatform",
"isPersistentTabType",
"isPrerelease",
"isSessionTabType",
"isSplitDragging",
"isSplitTab",
"isTabJumpHotkey",
"linkExternalToPasswordAccount",
"linkedServerUrl",
"listHostActions",
"listHostProtocols",
"loadPaletteGroup",
"logoutUser",
"makeHostSshSubTabs",
"makeHostTabs",
"makeUserAdmin",
"mapCredentials",
"markAdaptiveResourceUsed",
"markTabSurfaceUsed",
"moveRailItem",
"moveSplitDrag",
"normalizeLanguageCode",
"notifySyncChanged",
"openInSplit",
"paletteEntriesFor",
"parseCustomKeybindings",
"parseServerTime",
"pluginKey",
"pluginName",
"pluginWsUrl",
"preloadTabSurface",
"previewHostDefaultsLevel",
"promotableIds",
"protocolEnabled",
"protocolPort",
"quickConnectTargets",
"railItemLabel",
"readFromClipboard",
"recordHostActionPreference",
"refreshSyncStatus",
"registerLayout",
"registerTabCloseGuard",
"releaseUrlFrom",
"rem",
"removeAdminStatus",
"removeRoleFromUser",
"renameCredentialFolder",
"renameFolder",
"reorderCredentials",
"reorderFolders",
"reorderSSHHosts",
"reportBetaFeedbackUrl",
"reportCoreIssueUrl",
"requestFromLegacyEvent",
"requestManage",
"resetHostToDefaults",
"resetRecentActivity",
"resetUserSecondFactors",
"resolveConnectionOrigin",
"resolveHostDefaults",
"resolveHostTabType",
"resolveRemoteHostId",
"retrySyncErrors",
"revokeAllUserSessions",
"revokeCredentialAccess",
"revokeFolderAccess",
"revokeHostAccess",
"revokeSession",
"rightDockableIds",
"roleLabel",
"runAdaptiveBackgroundTask",
"runAdaptivePolling",
"runHostAction",
"runVisibleInterval",
"sanitizeCredentialSidebarPreferences",
"sanitizeHostSidebarPreferences",
"saveCredentialSidebarPreferences",
"saveHostDefaultsLevel",
"saveHostSidebarPreferences",
"saveHostTags",
"saveUserPreferences",
"setAllPluginChannels",
"setDeveloperMode",
"setHostAuthOverride",
"setNotificationPrivateEndpoints",
"setPluginOptions",
"setSyncStatus",
"setUpdateChannel",
"settleSyncConflict",
"shareCredential",
"shareFolder",
"shareHost",
"shell",
"splitDefaultKey",
"sshHostToHost",
"syncNow",
"tabIcon",
"tabJumpHotkeyKeys",
"takePendingManageRequest",
"unlinkExternalFromPasswordAccount",
"unlinkServer",
"updateCredential",
"updateExternalAutoProvision",
"updateFolderMetadata",
"updateHostAccess",
"updateLogLevel",
"updatePasswordLoginAllowed",
"updatePasswordResetAllowed",
"updatePluginAdminSettings",
"updatePluginHostSettings",
"updatePluginUserSettings",
"updateRegistrationAllowed",
"updateRole",
"updateSSHHost",
"updateSecondFactorAfterExternalLogin",
"updateSessionTimeout",
"updateStatusCheckSettings",
"updateSyncSettings",
"uploadTlsCertificate",
"usageBarColor",
"usageColor",
"useActionSlot",
"useAdaptivePolling",
"useAppTheme",
"useAreaPreferences",
"useBranding",
"useChooseTheme",
"useConfirm",
"useConnectionGate",
"useConnectionLog",
"useDesktopUpdate",
"useEditorDefaultKeys",
"useHostActions",
"useHostBadges",
"useHostContextMenuItems",
"useHostEditorToolbarItems",
"useHostStatusIndicators",
"useHostEditorSections",
"useHostProtocols",
"useHostStatus",
"useInstalledPlugins",
"useIsDefaultsEditor",
"useIsMobile",
"useKeybindingActions",
"useKeybindingDefaults",
"useOptionalConnectionLog",
"usePageVisibleInterval",
"usePaletteEntries",
"usePaletteGroups",
"usePanels",
"usePermissions",
"usePluginDocsUrl",
"usePluginHostSections",
"usePluginStore",
"useRailItems",
"useRegisteredDashboardCards",
"useServerStatus",
"useServerStatusMeta",
"useShellController",
"useSplitTargets",
"useSshAuthEditors",
"useSshAuthProviders",
"useStatusColorScheme",
"useSurfaceKind",
"useSyncStatus",
"useTabHandlesVersion",
"useTabs",
"useTabsSafe",
"useTheme",
"useThemeOptions",
"useUiPreference",
"useUiPreferencesContext",
"waitForDefaultsJob",
"withProtocolSettings",
];
/** Every name a module exports, resolved the way the compiler sees it. */
function exportedNames(file = SDK_UI, tsconfig = TSCONFIG) {
const ts = require("typescript");
const parsed = ts.getParsedCommandLineOfConfigFile(
tsconfig,
{},
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
);
const program = ts.createProgram([file], {
...(parsed?.options ?? {}),
noEmit: true,
});
const checker = program.getTypeChecker();
const source = program.getSourceFile(file);
const symbol = source && checker.getSymbolAtLocation(source);
if (!symbol) throw new Error(`Cannot read the exports of ${file}`);
return checker
.getExportsOfModule(symbol)
.map((entry) => entry.getName())
.sort();
}
const SNAPSHOT = path.join(ROOT, "packages", "plugin-sdk", "ui.api.txt");
/**
* One line per export with its type as the compiler prints it. Committed as
* packages/plugin-sdk/ui.api.txt, so a changed prop or signature shows up as
* a diff instead of reaching plugins unnoticed.
*/
function exportSignatures(file = SDK_UI, tsconfig = TSCONFIG) {
const ts = require("typescript");
const parsed = ts.getParsedCommandLineOfConfigFile(
tsconfig,
{},
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
);
const program = ts.createProgram([file], {
...(parsed?.options ?? {}),
noEmit: true,
});
const checker = program.getTypeChecker();
const source = program.getSourceFile(file);
const symbol = source && checker.getSymbolAtLocation(source);
const flags =
ts.TypeFormatFlags.NoTruncation |
ts.TypeFormatFlags.WriteArrowStyleSignature;
return checker
.getExportsOfModule(symbol)
.map((entry) => {
const target =
entry.flags & ts.SymbolFlags.Alias
? checker.getAliasedSymbol(entry)
: entry;
const isValue = !!(target.flags & ts.SymbolFlags.Value);
const declaration = target.declarations?.[0] ?? source;
const type = isValue
? checker.getTypeOfSymbolAtLocation(target, declaration)
: checker.getDeclaredTypeOfSymbol(target);
const text = checker
.typeToString(type, undefined, flags)
.replace(/import\("[^"]*"\)\./g, "")
.replace(/\s+/g, " ");
return `${entry.getName()} (${isValue ? "value" : "type"}): ${text}`;
})
.sort()
.join("\n")
.concat("\n");
}
/** What the module adds to, and drops from, the allowlist. */
function compare(names, allowed = ALLOWED) {
const allowedSet = new Set(allowed);
const nameSet = new Set(names);
return {
added: names.filter((name) => !allowedSet.has(name)),
removed: allowed.filter((name) => !nameSet.has(name)),
};
}
function main() {
const fs = require("node:fs");
if (/^export (type )?\*/m.test(fs.readFileSync(SDK_UI, "utf8"))) {
console.error(
"sdk-ui.ts uses export *; name each export so nothing becomes public API by accident.",
);
process.exit(1);
}
if (process.argv.includes("--update-snapshot")) {
fs.writeFileSync(SNAPSHOT, exportSignatures());
console.log(`Wrote ${path.relative(ROOT, SNAPSHOT)}`);
return;
}
const saved = fs.existsSync(SNAPSHOT)
? fs.readFileSync(SNAPSHOT, "utf8")
: "";
const current = exportSignatures();
if (current !== saved) {
const before = new Set(saved.split("\n"));
const after = new Set(current.split("\n"));
console.error(
"@termix-ssh/plugin-sdk/ui changed shape. If that is deliberate, run: node scripts/check-sdk-ui-exports.cjs --update-snapshot",
);
for (const line of saved.split("\n")) {
if (line && !after.has(line)) console.error(` - ${line.slice(0, 200)}`);
}
for (const line of current.split("\n")) {
if (line && !before.has(line)) console.error(` + ${line.slice(0, 200)}`);
}
process.exitCode = 1;
}
const names = exportedNames();
if (process.argv.includes("--list")) {
console.log(names.join("\n"));
return;
}
const { added, removed } = compare(names);
if (added.length === 0 && removed.length === 0) return;
if (added.length > 0) {
console.error(
"@termix-ssh/plugin-sdk/ui exports names that are not on the allowlist in scripts/check-sdk-ui-exports.cjs:",
);
for (const name of added) console.error(` + ${name}`);
}
if (removed.length > 0) {
console.error(
"@termix-ssh/plugin-sdk/ui no longer exports these allowlisted names (a breaking change for plugins):",
);
for (const name of removed) console.error(` - ${name}`);
}
process.exit(1);
}
if (require.main === module) main();
module.exports = { ALLOWED, compare, exportedNames, exportSignatures };