// Core definitions for the configuration system import * as classes from "./classes.js"; import * as validators from "./validators.js"; import type { PluginNodeEnvInfo, PluginWebEnvInfo } from "../plugin.js"; import { Static } from "@sinclair/typebox"; import { isTargetVersion } from "../data/version.js"; import { SyncHook } from "../SyncHook.js"; type configFromJSON = (...args: Parameters) => T; type configFromFile = (...args: Parameters) => Promise; export interface ControllerConfigFields { "controller.version": string; "controller.name": string; "controller.mods_directory": string; "controller.database_directory": string; "controller.http_port": number | null; "controller.https_port": number | null; "controller.bind_address": string | null; "controller.trusted_proxies": string | null; "controller.public_url": string | null; "controller.static_url": string | null; "controller.grafana_url": string | null; "controller.tls_certificate": string | null; "controller.tls_private_key": string | null; "controller.auth_secret": string | null; "controller.heartbeat_interval": number; "controller.session_timeout": number; "controller.max_send_buffer_size": number; "controller.metrics_timeout": number; "controller.max_subscription_filters": number; "controller.system_metrics_interval": number; "controller.proxy_stream_timeout": number; "controller.factorio_username": string | null; "controller.factorio_token": string | null; "controller.share_factorio_credentials_with_hosts": boolean; "controller.default_mod_pack_id": number | null; "controller.default_role_id": number | null; "controller.autosave_interval": number; "controller.host_pairing_timeout": number; "controller.host_pairing_max_pending": number; "controller.mod_portal_cache_duration_minutes": number; "controller.mod_portal_page_size": number; "controller.allow_remote_updates": boolean; "controller.allow_plugin_updates": boolean; "controller.allow_plugin_install": boolean; [key: `${string}.load_plugin`]: boolean; [key: `${string}.version`]: string; } /** * Controller Config class * @extends classes.Config */ export class ControllerConfig extends classes.Config { declare static fromJSON: configFromJSON; declare static fromFile: configFromFile; static pluginMigrations = new SyncHook(); static migrations(config: Static) { if (config.hasOwnProperty("controller.external_address")) { config["controller.public_url"] = config["controller.external_address"]; delete config["controller.external_address"]; } return config; } static fieldDefinitions: classes.ConfigDefs = { "controller.version": { type: "string", initialValue: "", // Set on start readonly: ["controller"], hidden: true, }, "controller.name": { title: "Name", description: "Name of the cluster.", type: "string", initialValue: "Your Cluster", }, "controller.mods_directory": { title: "Mods Directory", description: "Path to directory where mods shared with the cluster are stored.", restartRequired: true, type: "string", initialValue: "mods", validator: validators.filePath, }, "controller.database_directory": { title: "Database directory", description: "Directory where item and configuration data is stored.", type: "string", initialValue: "database", validator: validators.filePath, }, "controller.autosave_interval": { title: "Autosave Interval", description: "Interval in seconds to autosave data in memory to disk.", type: "number", initialValue: 60, validator: validators.greaterThanZero, }, "controller.host_pairing_timeout": { title: "Host Pairing Timeout", description: "Time in seconds a host can wait for approval before it has to request again, 0 for no limit.", type: "number", initialValue: 3600, validator: validators.greaterThanEqualZero, }, "controller.host_pairing_max_pending": { title: "Host Pairing Max Pending", description: "Maximum number of hosts that can wait for approval at the same time, 0 for no limit.", type: "number", initialValue: 50, validator: validators.all(validators.integer, validators.greaterThanEqualZero), }, "controller.http_port": { title: "HTTP Port", description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.", restartRequired: true, type: "number", optional: true, initialValue: 8080, validator: validators.optional(validators.all( validators.integer, validators.greaterThanZero )), }, "controller.https_port": { title: "HTTPS Port", description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.", restartRequired: true, type: "number", optional: true, validator: validators.optional(validators.all( validators.integer, validators.greaterThanZero )), }, "controller.bind_address": { title: "Bind Address", description: "IP address to bind the HTTP and HTTPS ports on.", restartRequired: true, type: "string", optional: true, // TODO extendedValidation, valid addresses including IPV4, IPV6, and localhost }, "controller.trusted_proxies": { title: "Trusted Proxies", description: "Comma separated list of IP addresses and/or CIDR blocks to trust the X-Forwarded-For header on", type: "string", optional: true, // TODO extendedValidation, see parseTrustedProxies }, "controller.public_url": { title: "Public URL", description: "Public facing URL the controller is hosted on, including the protocol.", type: "string", optional: true, // TODO extendedValidation, valid url including the protocol }, "controller.static_url": { title: "Static URL", description: "Override the URL to where static assets are hosted (must have a path consisting of /static/)", type: "string", optional: true, validator: (value) => { if (value) { // This is unfortunately a limitiation of how the web build is set up right now. const pathname = new URL(value, "http://localhost/").pathname; if (pathname !== "/static/") { throw new Error(`Path component of static_url must be /static/ not ${pathname}.`); } } }, }, "controller.grafana_url": { title: "Grafana URL", description: "URL of a Grafana dashboard to link from the overview page, including the protocol.", type: "string", optional: true, }, "controller.tls_certificate": { title: "TLS Certificate", description: "Path to the certificate to use for HTTPS.", restartRequired: true, type: "string", optional: true, validator: validators.optional(validators.filePath), }, "controller.tls_private_key": { title: "TLS Private Key", description: "Path to the private key to use for HTTPS.", restartRequired: true, type: "string", optional: true, validator: validators.optional(validators.filePath), }, "controller.auth_secret": { access: ["controller"], title: "Controller Authentication Secret", description: "Secret used to generate and verify authentication tokens. " + "Should be a long string of random letters and numbers. " + "Do not share this.", restartRequired: true, type: "string", optional: true, }, "controller.heartbeat_interval": { title: "Heartbeat Interval", description: "Interval heartbeats are sent out on WebSocket connections.", type: "number", initialValue: 15, validator: validators.greaterThanZero, }, "controller.session_timeout": { title: "Session Timeout", description: "Time in seconds before giving up resuming a dropped WebSocket session.", type: "number", initialValue: 60, validator: validators.greaterThanZero, }, "controller.max_send_buffer_size": { title: "Max Send Buffer Size", description: "MiB of unacknowledged messages to keep for a connection before ending its session, 0 for no limit.", type: "number", initialValue: 64, validator: validators.greaterThanEqualZero, }, "controller.metrics_timeout": { title: "Metrics Timeout", description: "Timeout in seconds for metrics gathering from hosts.", type: "number", initialValue: 8, validator: validators.greaterThanZero, }, "controller.max_subscription_filters": { title: "Max Subscription Filters", description: "Maximum number of filters a connection can hold for one subscribed event, 0 for no limit.", type: "number", initialValue: 10000, restartRequired: true, validator: validators.all(validators.integer, validators.greaterThanEqualZero), }, "controller.system_metrics_interval": { title: "System Metrics Interval", description: "Interval in seconds to collect and update system metrics for the Web UI", type: "number", initialValue: 10, validator: validators.greaterThanEqualZero, }, "controller.proxy_stream_timeout": { title: "Proxy Stream Timeout", description: "Timeout in seconds for proxy streams to start flowing.", type: "number", initialValue: 15, validator: validators.greaterThanZero, }, "controller.factorio_username": { title: "Factorio Username", description: "Username to authenticate with Factorio API with.", autoComplete: "section-factorio username", type: "string", optional: true, }, "controller.factorio_token": { title: "Factorio Token", description: "Token to authenticate with Factorio API with.", autoComplete: "section-factorio new-password", type: "string", credential: ["controller"], optional: true, }, "controller.share_factorio_credentials_with_hosts": { title: "Share Factorio credentials with Hosts", description: "If enabled, the Factorio Username and Token will be shared with hosts and " + "used in the server settings of instances in this cluster.", type: "boolean", initialValue: true, }, "controller.default_mod_pack_id": { title: "Default Mod Pack", description: "Mod pack used by default for instances.", inputComponent: "mod_pack", type: "number", optional: true, }, "controller.default_role_id": { title: "Default role", description: "ID of role assigned by default to new users.", inputComponent: "role", type: "number", optional: true, initialValue: 1, }, "controller.mod_portal_cache_duration_minutes": { title: "Mod Portal Cache Duration", description: "Duration in minutes to cache mod portal API responses.", type: "number", initialValue: 30, validator: validators.greaterThanEqualZero, // Zero is allowed here to disable cache }, "controller.mod_portal_page_size": { title: "Mod Portal Page Size", description: "Maximum number of results per page when querying the Factorio mod portal API.", type: "number", initialValue: 1000, validator: validators.greaterThanZero, }, "controller.allow_remote_updates": { description: "When true, allows a remote event to trigger a clusterio update via npm", type: "boolean", initialValue: true, readonly: ["controller"], hidden: true, }, "controller.allow_plugin_updates": { description: "When true, allows a remote event to trigger a plugin update via npm", type: "boolean", initialValue: true, readonly: ["controller"], hidden: true, }, "controller.allow_plugin_install": { description: "When true, allows a remote event to trigger a plugin install via npm", type: "boolean", initialValue: false, readonly: ["controller"], hidden: true, }, }; } export interface HostConfigFields { "host.version": string; "host.name": string; "host.id": number; "host.factorio_directory": string; "host.mods_directory": string; "host.instances_directory": string; "host.controller_url": string; "host.controller_token": string; "host.public_address": string; "host.factorio_port_range": string; "host.factorio_username": string | null, "host.factorio_token": string | null, "host.max_reconnect_delay": number; "host.allow_remote_updates": boolean; "host.allow_plugin_updates": boolean; "host.allow_plugin_install": boolean; [key: `${string}.load_plugin`]: boolean; [key: `${string}.version`]: string; } /** * Host Config class * @extends classes.Config */ export class HostConfig extends classes.Config { declare static fromJSON: configFromJSON; declare static fromFile: configFromFile; static pluginMigrations = new SyncHook(); static fieldDefinitions: classes.ConfigDefs = { "host.version": { type: "string", initialValue: "", // Set on start readonly: ["host"], hidden: true, }, "host.name": { description: "Name of the host", type: "string", initialValue: "New Host", }, "host.id": { description: "ID of the host", type: "number", initialValue: () => Math.random() * 2**31 | 0, hidden: true, }, "host.factorio_directory": { description: "Path to directory to look for factorio installs", type: "string", initialValue: "factorio", validator: validators.filePath, }, "host.mods_directory": { title: "Mods Directory", description: "Path to directory where mods for instances are cached.", restartRequired: true, type: "string", initialValue: "mods", validator: validators.filePath, }, "host.instances_directory": { description: "Path to directory to store instances in.", restartRequired: true, type: "string", initialValue: "instances", validator: validators.filePath, }, "host.controller_url": { description: "URL to connect to the controller at", restartRequired: true, type: "string", initialValue: "http://localhost:8080/", // TODO extendedValidation, valid url including the protocol }, "host.controller_token": { access: ["host"], description: "Token to authenticate to controller with.", restartRequired: true, type: "string", initialValue: "enter token here", // TODO extendedValidation, valid JWT with matching host id }, "host.public_address": { description: "Public facing address players should connect to in order to join instances on this host", type: "string", initialValue: "localhost", }, "host.factorio_port_range": { title: "Factorio port range", description: "Range of UDP ports to use for game connections. Supports both comma separated values and " + "ranges separated with a dash.", type: "string", initialValue: "34100-34199", // TODO extendedValidation, port range }, "host.factorio_username": { title: "Factorio Username", description: "Username to authenticate with Factorio API with. If set this will be used in the server settings " + "of all instances on this host.", autoComplete: "section-factorio username", type: "string", optional: true, }, "host.factorio_token": { title: "Factorio Token", description: "Token to authenticate with Factorio API with. If set this will be used in the server settings " + "of all instances on this host.", autoComplete: "section-factorio new-password", type: "string", credential: ["host"], optional: true, }, "host.max_reconnect_delay": { title: "Max Reconnect Delay", description: "Maximum delay to wait before attempting to reconnect WebSocket", type: "number", initialValue: 60, validator: validators.greaterThanZero, }, "host.allow_remote_updates": { description: "When true, allows a remote event to trigger a clusterio update via npm", type: "boolean", initialValue: true, readonly: ["host"], hidden: true, }, "host.allow_plugin_updates": { description: "When true, allows a remote event to trigger a plugin update via npm", type: "boolean", initialValue: true, readonly: ["host"], hidden: true, }, "host.allow_plugin_install": { description: "When true, allows a remote event to trigger a plugin install via npm", type: "boolean", initialValue: false, readonly: ["host"], hidden: true, }, }; } export interface InstanceConfigFields { "instance.name": string; "instance.id": number; "instance.assigned_host": number | null; "instance.auto_start": boolean; "instance.exclude_from_start_all": boolean; "instance.is_template": boolean; "instance.template_id": number | null; "instance.inherited_fields": Record; [key: `${string}.load_plugin`]: boolean; [key: `${string}.version`]: string; "factorio.version": string; "factorio.executable_path": string | null; "factorio.shutdown_timeout": number; "factorio.game_port": number | null; "factorio.host_assigned_game_port": number | null; "factorio.rcon_port": number | null; "factorio.rcon_password": string | null; "factorio.enable_lua_udp": boolean; "factorio.lua_udp_port": number | null; "factorio.player_online_autosave_slots": number; "factorio.mod_pack_id": number | null; "factorio.enable_save_patching": boolean; "factorio.enable_script_commands": boolean; "factorio.enable_whitelist": boolean; "factorio.enable_authserver_bans": boolean; "factorio.settings": Record; "factorio.verbose_logging": boolean; "factorio.console_logging": boolean; "factorio.strip_paths": boolean; "factorio.sync_adminlist": "enabled" | "disabled" | "bidirectional"; "factorio.sync_whitelist": "enabled" | "disabled" | "bidirectional"; "factorio.sync_banlist": "enabled" | "disabled" | "bidirectional"; "factorio.max_concurrent_commands": number; } /** * Instance config class * @extends classes.Config */ export class InstanceConfig extends classes.Config { declare static fromJSON: configFromJSON; declare static fromFile: configFromFile; static pluginMigrations = new SyncHook(); static migrations(config: Static) { function boolToEnableDisable(name: string) { if (config.hasOwnProperty(name) && typeof config[name] === "boolean") { config[name] = config[name] ? "enabled" : "disabled"; } } boolToEnableDisable("factorio.sync_adminlist"); boolToEnableDisable("factorio.sync_whitelist"); boolToEnableDisable("factorio.sync_banlist"); return config; } /** Fields that are never inherited from a template, ports clash on a shared host and secrets stay per instance */ static uninheritableFields = [ "instance.name", "instance.is_template", "instance.template_id", "factorio.game_port", "factorio.rcon_port", "factorio.rcon_password", "factorio.lua_udp_port", ]; /** True if the field can take its value from a template */ static isInheritable(name: string) { const def = this.fieldDefinitions[name as keyof InstanceConfigFields]; return Boolean(def) && !def.hidden && (def.access ?? this.defaultAccess).includes("controller") && !this.uninheritableFields.includes(name); } /** * True if the field, or the given property of an object field, takes its value from the template * * @param name - Name of field to check. * @param prop - Property of an object field to check. */ isInherited(name: string, prop?: string) { if (this.get("instance.template_id") === null) { return false; } const entry = this.get("instance.inherited_fields")[name]; if (entry instanceof Array) { return prop === undefined || !entry.includes(prop); } return Boolean(entry); } static fieldDefinitions: classes.ConfigDefs = { "instance.name": { type: "string", initialValue: "New Instance", }, "instance.id": { description: "ID of the instance", type: "number", initialValue: () => Math.random() * 2**31 | 0, hidden: true, }, "instance.assigned_host": { type: "number", optional: true, hidden: true, // Internal value, extendedValidation not used }, "instance.auto_start": { description: "Automatically start this instance when the host hosting it is started up", type: "boolean", initialValue: false, }, "instance.exclude_from_start_all": { description: "Exclude this instance from the 'Start all' button operation", type: "boolean", initialValue: false, }, "instance.is_template": { description: "Mark this instance as a template other instances can inherit config from. " + "Templates cannot be assigned to a host or started.", access: ["controller", "control"], type: "boolean", initialValue: false, hidden: true, }, "instance.template_id": { title: "Template", description: "ID of the template to inherit config values from", access: ["controller", "control"], inputComponent: "instance_template", type: "number", optional: true, // Checked by the controller }, "instance.inherited_fields": { description: "Fields whose value comes from the template, for object fields the properties set on this instance", access: ["controller", "control"], readonly: ["controller"], type: "object", initialValue: {}, hidden: true, }, "factorio.version": { description: "Version of the game to run. Use 'latest' for the latest installed version, a release " + "channel such as 'stable' or 'experimental', or a specific X.Y or X.Y.Z version.", restartRequired: true, type: "string", initialValue: "latest", inputComponent: "target_version", validator: function(value) { if (!isTargetVersion(value)) { throw new Error("Value must be 'latest', a release channel, or match X.Y or X.Y.Z"); } }, }, "factorio.executable_path": { description: "Relative path from the Factorio installation directory to the executable to run. " + "Defaults to auto detect the path, only needed in special setups.", restartRequired: true, type: "string", optional: true, validator: validators.optional(validators.filePath), }, "factorio.shutdown_timeout": { description: "Timeout in seconds to wait after requesting the server to stop before killing " + "the process. Set to 0 to disable.", type: "number", initialValue: 300, validator: validators.greaterThanEqualZero, }, "factorio.game_port": { description: "UDP port to run game on, uses a port in host.factorio_port_range if null", restartRequired: true, type: "number", optional: true, validator: validators.optional(validators.greaterThanEqualZero), }, "factorio.host_assigned_game_port": { access: ["host"], type: "number", optional: true, // Internal value, extendedValidation not used }, "factorio.rcon_port": { description: "TCP port to run RCON on, uses a random port if null", restartRequired: true, type: "number", optional: true, validator: validators.optional(validators.greaterThanEqualZero), }, "factorio.rcon_password": { credential: ["host", "controller"], description: "Password for RCON, randomly generated if null.", restartRequired: true, type: "string", optional: true, }, "factorio.enable_lua_udp": { description: "Enable UDP messaging between the Factorio server and the host. " + "Required by plugins using UDP. Requires Factorio 2.1.10 or later.", restartRequired: true, // Because of the cli option "--enable-lua-udp" type: "boolean", initialValue: false, }, "factorio.lua_udp_port": { description: "UDP port for the Factorio server to receive Lua UDP messages on, uses a random port if null", restartRequired: true, type: "number", optional: true, validator: validators.optional(validators.greaterThanEqualZero), }, "factorio.player_online_autosave_slots": { description: "Rename autosaves where players have been online since the previous autosave into a separate " + "autosave pool with this many slots. Requires autosaves to be enabled to work. Set to 0 to disable.", type: "number", initialValue: 5, dependsOn: ["factorio.settings", "factorio.settings.autosave_slots"], validator: function(value, config) { const factorioSettings = config.get("factorio.settings"); const autosaveSlots = factorioSettings.autosave_slots; if (value !== 0 && typeof autosaveSlots === "number" && value < autosaveSlots) { throw new Error("Value cannot be less than the number of autosave slots"); } }, }, "factorio.mod_pack_id": { title: "Mod Pack", description: "Mod pack to use on this server, if not set the default configured on the controller will be used", inputComponent: "mod_pack", restartRequired: true, type: "number", optional: true, }, "factorio.enable_save_patching": { description: "Patch saves with Lua code. Required for Clusterio integrations, lua modules, and most plugins.", restartRequired: true, type: "boolean", initialValue: true, }, "factorio.enable_script_commands": { description: "Allows achievement breaking commands to be executed over rcon. " + "Required for Clusterio integrations and most plugins. " + "This does not prevent players using script commands.", type: "boolean", initialValue: true, }, "factorio.enable_whitelist": { description: "Turn on whitelist for joining the server.", restartRequired: true, // Because of the cli option "--use-server-whitelist" type: "boolean", initialValue: false, }, "factorio.enable_authserver_bans": { description: "Turn on Factorio.com based multiplayer bans.", restartRequired: true, // Because of the cli option "--use-authserver-bans" type: "boolean", initialValue: false, }, "factorio.settings": { description: "Settings overridden in server-settings.json", restartRequired: true, restartRequiredProps: [ "afk_autokick_interval", "allow_commands", "autosave_interval", "autosave_only_on_server", "description", "ignore_player_limit_for_returning_players", "max_players", "max_upload_slots", "max_upload_in_kilobytes_per_second", "name", "only_admins_can_pause_the_game", "game_password", "require_user_verification", "tags", "visibility", ], type: "object", initialValue: {}, // See create instance handler in controller. // TODO extendedValidation, we could check keys and types here if we wanted }, "factorio.verbose_logging": { description: "Enable verbose logging on the Factorio server", restartRequired: true, // Because of the cli option "--verbose" type: "boolean", initialValue: false, }, "factorio.console_logging": { description: "Enable console logging to a separate file, useful for 3rd party integrations", restartRequired: true, // Because of the cli option "--console-log" type: "boolean", initialValue: false, }, "factorio.strip_paths": { description: "Strip down instance paths in the log", restartRequired: true, type: "boolean", initialValue: true, }, "factorio.sync_adminlist": { description: "Synchronize adminlist with the controller", type: "string", enum: ["disabled", "enabled", "bidirectional"], initialValue: "bidirectional", }, "factorio.sync_whitelist": { description: "Synchronize whitelist with the controller (whitelist must be enabled)", type: "string", enum: ["disabled", "enabled", "bidirectional"], initialValue: "bidirectional", }, "factorio.sync_banlist": { description: "Synchronize banlist with the controller", type: "string", enum: ["disabled", "enabled", "bidirectional"], initialValue: "bidirectional", }, "factorio.max_concurrent_commands": { description: "Maximum number of RCON commands trasmitted in parallel", restartRequired: true, type: "number", initialValue: 5, validator: validators.greaterThanZero, }, }; } export interface CtlConfigFields { "ctl.controller_url": string | null; "ctl.controller_token": string | null; "ctl.max_reconnect_delay": number; } /** * Ctl config class * @extends classes.Config */ export class CtlConfig extends classes.Config { declare static fromJSON: configFromJSON; declare static fromFile: configFromFile; static fieldDefinitions: classes.ConfigDefs = { "ctl.controller_url": { description: "URL to connect to the controller at", type: "string", optional: true, }, "ctl.controller_token": { access: ["control"], description: "Token to authenticate to controller with.", type: "string", optional: true, }, "ctl.max_reconnect_delay": { title: "Max Reconnect Delay", description: "Maximum delay to wait before attempting to reconnect WebSocket", type: "number", initialValue: 60, validator: validators.greaterThanEqualZero, }, }; } function validateFieldNames( pluginName: string, fields: Record, ) { for (const [name, field] of Object.entries(fields)) { if (!name.startsWith(`${pluginName}.`)) { throw new Error( `Expected name of config field '${name}' for ${pluginName} to start with '${pluginName}.'` ); } } } /** * Add the config fields defined by the by a specific plugin infos */ export function addPluginFieldDefinitions( pluginInfo: PluginNodeEnvInfo | PluginWebEnvInfo, kind: "controllerConfigFields" | "hostConfigFields" | "instanceConfigFields", Config: typeof classes.Config ) { // Define plugin.load_plugin const fieldDef: classes.FieldDefinition = { title: "Load Plugin", type: "boolean", initialValue: true, restartRequired: true, }; // The validator is dynamic based on the feature requirements of the plugin if (kind === "instanceConfigFields" && pluginInfo.features) { const requirements: [string, string][] = []; if (pluginInfo.features.includes("SavePatching")) { requirements.push(["factorio.enable_save_patching", "save patching"]); } if (pluginInfo.features.includes("ScriptCommands")) { requirements.push(["factorio.enable_script_commands", "script commands"]); } if (pluginInfo.features.includes("LuaUdp")) { requirements.push(["factorio.enable_lua_udp", "Lua UDP"]); } if (requirements.length) { fieldDef.dependsOn = requirements.map(([field]) => field); // Default to not loaded when the instance does not meet the requirements fieldDef.initialValue = (config: classes.Config) => requirements.every( ([field]) => config.get(field) ); fieldDef.validator = (enabled, config) => { if (!enabled) { return; } for (const [field, description] of requirements) { if (!config.get(field)) { throw new Error(`Plugin ${pluginInfo.name} requires ${description}`); } } }; } } Config.fieldDefinitions[`${pluginInfo.name}.load_plugin`] = fieldDef; // Version of the plugin the config was last loaded with, see Config.pluginVersions Config.fieldDefinitions[`${pluginInfo.name}.version`] = { type: "string", initialValue: "", readonly: ["controller", "host"], hidden: true, }; // Merge in other fields from the plugin const fields = pluginInfo[kind]; if (fields) { validateFieldNames(pluginInfo.name, fields); Object.assign(Config.fieldDefinitions, fields); } } /** * Add all config fields defined by the provided plugin infos * * @param {Array} pluginInfos - Array of plugin info objects. */ export function addPluginConfigFields(pluginInfos: PluginNodeEnvInfo[] | PluginWebEnvInfo[]) { for (let pluginInfo of pluginInfos) { addPluginFieldDefinitions(pluginInfo, "controllerConfigFields", ControllerConfig); if (pluginInfo.hostEntrypoint || pluginInfo.instanceEntrypoint) { addPluginFieldDefinitions(pluginInfo, "hostConfigFields", HostConfig); } if (pluginInfo.instanceEntrypoint) { addPluginFieldDefinitions(pluginInfo, "instanceConfigFields", InstanceConfig); } if (pluginInfo.ctlConfigFields) { validateFieldNames(pluginInfo.name, pluginInfo.ctlConfigFields); Object.assign(CtlConfig.fieldDefinitions, pluginInfo.ctlConfigFields); } } }