diff --git a/companion/lib/Instance/Controller.ts b/companion/lib/Instance/Controller.ts index 82b049186f..c320ab9184 100644 --- a/companion/lib/Instance/Controller.ts +++ b/companion/lib/Instance/Controller.ts @@ -43,7 +43,10 @@ import type { PageController } from '../Page/Controller.js' import express from 'express' import { InstanceInstalledModulesManager } from './InstalledModulesManager.js' import type { ModuleVersionInfo } from '@companion-app/shared/Model/ModuleInfo.js' +import type { ModuleDirs } from './Types.js' import path from 'path' +import { isPackaged } from '../Resources/Util.js' +import { fileURLToPath } from 'url' import { ModuleStoreService } from './ModuleStore.js' import type { AppInfo } from '../Registry.js' import type { DataCache } from '../Data/Cache.js' @@ -100,14 +103,26 @@ export class InstanceController extends EventEmitter { this.#variablesController = variables this.#controlsController = controls - const installedModulesDir = path.join(appInfo.modulesDir, 'store') + function generatePath(subpath: string): string { + if (isPackaged()) { + return path.join(__dirname, subpath) + } else { + return fileURLToPath(new URL(path.join('../../..', subpath), import.meta.url)) + } + } + + const moduleDirs: ModuleDirs = { + bundledLegacyModulesDir: path.resolve(generatePath('modules')), + bundledModulesDir: path.resolve(generatePath('bundled-modules')), + installedModulesDir: path.join(appInfo.modulesDir, 'store'), + } this.#configStore = new ConnectionConfigStore(db, this.broadcastChanges.bind(this)) this.sharedUdpManager = new InstanceSharedUdpManager() this.definitions = new InstanceDefinitions(io, controls, graphics, variables.values) this.status = new InstanceStatus(io, controls) - this.modules = new InstanceModules(io, this, apiRouter, installedModulesDir) + this.modules = new InstanceModules(io, this, apiRouter, moduleDirs) this.moduleHost = new ModuleHost( { controls: controls, @@ -127,12 +142,7 @@ export class InstanceController extends EventEmitter { this.#configStore ) this.modulesStore = new ModuleStoreService(io, cache) - this.userModulesManager = new InstanceInstalledModulesManager( - appInfo, - this.modules, - this.modulesStore, - installedModulesDir - ) + this.userModulesManager = new InstanceInstalledModulesManager(appInfo, this.modules, this.modulesStore, moduleDirs) graphics.on('resubscribeFeedbacks', () => this.moduleHost.resubscribeAllFeedbacks()) diff --git a/companion/lib/Instance/InstalledModulesManager.ts b/companion/lib/Instance/InstalledModulesManager.ts index 839b8248a4..0c8fd6d0ec 100644 --- a/companion/lib/Instance/InstalledModulesManager.ts +++ b/companion/lib/Instance/InstalledModulesManager.ts @@ -8,6 +8,7 @@ import * as ts from 'tar-stream' import { Readable } from 'node:stream' import { ModuleManifest } from '@companion-module/base' import * as tarfs from 'tar-fs' +import type { ModuleDirs } from './Types.js' import type { ModuleStoreService } from './ModuleStore.js' import type { AppInfo } from '../Registry.js' import { promisify } from 'util' @@ -41,16 +42,11 @@ export class InstanceInstalledModulesManager { */ readonly #modulesDir: string - constructor( - appInfo: AppInfo, - modulesManager: InstanceModules, - modulesStore: ModuleStoreService, - installedModulesDir: string - ) { + constructor(appInfo: AppInfo, modulesManager: InstanceModules, modulesStore: ModuleStoreService, dirs: ModuleDirs) { this.#appInfo = appInfo this.#modulesManager = modulesManager this.#modulesStore = modulesStore - this.#modulesDir = installedModulesDir + this.#modulesDir = dirs.installedModulesDir } /** diff --git a/companion/lib/Instance/ModuleInfo.ts b/companion/lib/Instance/ModuleInfo.ts index 1940496f12..1980eed327 100644 --- a/companion/lib/Instance/ModuleInfo.ts +++ b/companion/lib/Instance/ModuleInfo.ts @@ -86,6 +86,7 @@ function translateStableVersion(version: SomeModuleVersionInfo | null): NewClien displayName: 'Latest Stable (Dev)', isLegacy: false, isDev: true, + isBuiltin: false, hasHelp: version.helpPath !== null, version: { mode: 'stable', @@ -98,6 +99,7 @@ function translateStableVersion(version: SomeModuleVersionInfo | null): NewClien displayName: `Latest Stable (v${version.versionId})`, isLegacy: version.display.isLegacy ?? false, isDev: false, + isBuiltin: version.isBuiltin, hasHelp: version.helpPath !== null, version: { mode: 'stable', @@ -116,6 +118,7 @@ function translatePrereleaseVersion(version: SomeModuleVersionInfo | null): NewC displayName: 'Latest Prerelease (Dev)', isLegacy: false, isDev: true, + isBuiltin: false, hasHelp: version.helpPath !== null, version: { mode: 'prerelease', @@ -128,6 +131,7 @@ function translatePrereleaseVersion(version: SomeModuleVersionInfo | null): NewC displayName: `Latest Prerelease (v${version.versionId})`, isLegacy: version.display.isLegacy ?? false, isDev: false, + isBuiltin: version.isBuiltin, hasHelp: version.helpPath !== null, version: { mode: 'prerelease', @@ -144,6 +148,7 @@ function translateReleaseVersion(version: ReleaseModuleVersionInfo): NewClientMo displayName: `v${version.versionId}`, isLegacy: version.display.isLegacy ?? false, isDev: false, + isBuiltin: version.isBuiltin, hasHelp: version.helpPath !== null, version: { mode: 'specific-version', diff --git a/companion/lib/Instance/Modules.ts b/companion/lib/Instance/Modules.ts index 5e51949b0a..add4cbe4df 100644 --- a/companion/lib/Instance/Modules.ts +++ b/companion/lib/Instance/Modules.ts @@ -27,7 +27,7 @@ import type { HelpDescription } from '@companion-app/shared/Model/Common.js' import LogController from '../Log/Controller.js' import type { InstanceController } from './Controller.js' import jsonPatch from 'fast-json-patch' -import type { SomeModuleVersionInfo } from './Types.js' +import type { ModuleDirs, SomeModuleVersionInfo } from './Types.js' import { InstanceModuleInfo } from './ModuleInfo.js' const ModulesRoom = 'modules' @@ -65,12 +65,12 @@ export class InstanceModules { */ readonly #moduleScanner = new InstanceModuleScanner() - readonly #installedModulesDir: string + readonly #moduleDirs: ModuleDirs - constructor(io: UIHandler, instance: InstanceController, apiRouter: express.Router, installedModulesDir: string) { + constructor(io: UIHandler, instance: InstanceController, apiRouter: express.Router, moduleDirs: ModuleDirs) { this.#io = io this.#instanceController = instance - this.#installedModulesDir = installedModulesDir + this.#moduleDirs = moduleDirs apiRouter.get('/help/module/:moduleId/:versionMode/:versionId/*path', this.#getHelpAsset) } @@ -96,6 +96,7 @@ export class InstanceModules { type: 'release', versionId: loadedModuleInfo.display.version, releaseType: loadedModuleInfo.isPrerelease ? 'prerelease' : 'stable', + isBuiltin: false, } // Notify clients @@ -141,8 +142,39 @@ export class InstanceModules { * @param extraModulePath - extra directory to search for modules */ async initInstances(extraModulePath: string): Promise { - // And modules from the installed modules dir - const storeModules = await this.#moduleScanner.loadInfoForModulesInDir(this.#installedModulesDir, true) + const legacyCandidates = await this.#moduleScanner.loadInfoForModulesInDir( + this.#moduleDirs.bundledLegacyModulesDir, + false + ) + + // Start with 'legacy' candidates + for (const candidate of legacyCandidates) { + candidate.display.isLegacy = true + const moduleInfo = this.#getOrCreateModuleEntry(candidate.manifest.id) + moduleInfo.installedVersions[candidate.display.version] = { + ...candidate, + type: 'release', + releaseType: 'stable', + versionId: candidate.display.version, + isBuiltin: true, + } + } + + // Load bundled modules + const bundledModules = await this.#moduleScanner.loadInfoForModulesInDir(this.#moduleDirs.bundledModulesDir, false) + for (const candidate of bundledModules) { + const moduleInfo = this.#getOrCreateModuleEntry(candidate.manifest.id) + moduleInfo.installedVersions[candidate.display.version] = { + ...candidate, + type: 'release', + releaseType: 'stable', + versionId: candidate.display.version, + isBuiltin: true, + } + } + + // And modules from the store + const storeModules = await this.#moduleScanner.loadInfoForModulesInDir(this.#moduleDirs.installedModulesDir, true) for (const candidate of storeModules) { const moduleInfo = this.#getOrCreateModuleEntry(candidate.manifest.id) moduleInfo.installedVersions[candidate.display.version] = { @@ -150,6 +182,7 @@ export class InstanceModules { type: 'release', releaseType: candidate.isPrerelease ? 'prerelease' : 'stable', versionId: candidate.display.version, + isBuiltin: false, } } @@ -190,7 +223,7 @@ export class InstanceModules { if (moduleInfo.devModule) { this.#logger.info( - `${moduleInfo.devModule.display.id}: ${moduleInfo.devModule.display.name} (Dev${ + `${moduleInfo.devModule.display.id}: ${moduleInfo.devModule.display.name} (Overridden${ moduleInfo.devModule.isPackaged ? ' & Packaged' : '' })` ) @@ -198,7 +231,16 @@ export class InstanceModules { for (const moduleVersion of Object.values(moduleInfo.installedVersions)) { if (!moduleVersion) continue - this.#logger.info(`${moduleVersion.display.id}@${moduleVersion.display.version}: ${moduleVersion.display.name}`) + this.#logger.info( + `${moduleVersion.display.id}@${moduleVersion.display.version}: ${moduleVersion.display.name}${moduleVersion.isBuiltin ? ' (Builtin)' : ''}` + ) + } + + for (const moduleVersion of Object.values(moduleInfo.installedVersions)) { + if (!moduleVersion) continue + this.#logger.info( + `${moduleVersion.display.id}@${moduleVersion.display.version}: ${moduleVersion.display.name} (Custom)` + ) } } } diff --git a/companion/lib/Instance/Types.ts b/companion/lib/Instance/Types.ts index 91f548fd9c..e6bf331eb3 100644 --- a/companion/lib/Instance/Types.ts +++ b/companion/lib/Instance/Types.ts @@ -1,6 +1,12 @@ import type { ModuleDisplayInfo } from '@companion-app/shared/Model/ModuleInfo.js' import type { ModuleManifest } from '@companion-module/base' +export interface ModuleDirs { + readonly bundledLegacyModulesDir: string + readonly bundledModulesDir: string + readonly installedModulesDir: string +} + export interface ModuleVersionInfoBase { basePath: string helpPath: string | null @@ -14,6 +20,7 @@ export interface ReleaseModuleVersionInfo extends ModuleVersionInfoBase { type: 'release' releaseType: 'stable' | 'prerelease' versionId: string + isBuiltin: boolean } export interface DevModuleVersionInfo extends ModuleVersionInfoBase { type: 'dev' diff --git a/shared-lib/lib/Model/ModuleInfo.ts b/shared-lib/lib/Model/ModuleInfo.ts index 58af1e299d..8a4d728690 100644 --- a/shared-lib/lib/Model/ModuleInfo.ts +++ b/shared-lib/lib/Model/ModuleInfo.ts @@ -34,6 +34,7 @@ export interface NewClientModuleVersionInfo2 { displayName: string isLegacy: boolean isDev: boolean + isBuiltin: boolean version: ModuleVersionInfo hasHelp: boolean } diff --git a/webui/src/Modules/ModuleVersionsTable.tsx b/webui/src/Modules/ModuleVersionsTable.tsx index 062f1d52e1..a8da7b0ca1 100644 --- a/webui/src/Modules/ModuleVersionsTable.tsx +++ b/webui/src/Modules/ModuleVersionsTable.tsx @@ -3,6 +3,7 @@ import { socketEmitPromise } from '../util.js' import { CButton, CButtonGroup } from '@coreui/react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { + faLock, faPlus, faQuestion, faStar, @@ -147,7 +148,7 @@ const ModuleVersionRow = observer(function ModuleVersionRow({ {installedInfo ? ( - + ) : ( {versionId} - {isLatestStable && } - {isLatestPrerelease && } - {storeInfo?.isPrerelease && } {storeInfo?.deprecationReason && } @@ -173,6 +171,9 @@ const ModuleVersionRow = observer(function ModuleVersionRow({ )} + {isLatestStable && } + {isLatestPrerelease && } + + } + return ( {isRunningInstallOrUninstall ? ( diff --git a/webui/src/Modules/ModulesList.tsx b/webui/src/Modules/ModulesList.tsx index 341a81b2d5..b8c0d670ed 100644 --- a/webui/src/Modules/ModulesList.tsx +++ b/webui/src/Modules/ModulesList.tsx @@ -18,7 +18,8 @@ import { useTableVisibilityHelper, VisibilityButton } from '../Components/TableV interface VisibleModulesState { dev: boolean - installed: boolean + builtin: boolean + store: boolean } interface ModulesListProps { @@ -42,7 +43,8 @@ export const ModulesList = observer(function ModulesList({ const visibleModules = useTableVisibilityHelper('modules_visible', { dev: true, - installed: true, + builtin: true, + store: true, }) const [filter, setFilter] = useState('') @@ -55,7 +57,17 @@ export const ModulesList = observer(function ModulesList({ for (const moduleInfo of searchResults) { let isVisible = false if (moduleInfo.hasDevVersion && visibleModules.visiblity.dev) isVisible = true - if (moduleInfo.installedVersions.length > 0 && visibleModules.visiblity.installed) isVisible = true + + const [hasBuiltin, hasStore] = moduleInfo.installedVersions.reduce( + ([builtin, release], v) => { + if (v.isBuiltin) return [true, release] + if (!v.isBuiltin) return [builtin, true] + return [builtin, release] + }, + [false, false] + ) + if (hasBuiltin && visibleModules.visiblity.builtin) isVisible = true + if (hasStore && visibleModules.visiblity.store) isVisible = true if (!isVisible) continue @@ -116,7 +128,8 @@ export const ModulesList = observer(function ModulesList({ - + +