diff --git a/core/config/load.ts b/core/config/load.ts index e4c7e13c4f..97db75614f 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -822,6 +822,86 @@ async function buildConfigTsandReadConfigJs(ide: IDE, ideType: IdeType) { return readConfigJs(); } +async function loadConfigModule(configJsPath: string) { + try { + return await import(configJsPath); + } catch (e) { + console.log(e); + console.log( + "Could not load config.ts as absolute path, retrying as file url ...", + ); + try { + return await import(localPathToUri(configJsPath)); + } catch (fileUrlError) { + throw new Error("Could not load config.ts as file url either", { + cause: fileUrlError, + }); + } + } +} + +function clearConfigModuleCache(configJsPath: string) { + if (typeof require === "undefined") { + return; + } + + try { + delete require.cache[require.resolve(configJsPath)]; + } catch { + // Dynamic imports via file URLs may not populate require cache. + } +} + +async function applyConfigModule( + config: T, + configJsPath: string, +): Promise { + const module = await loadConfigModule(configJsPath); + clearConfigModuleCache(configJsPath); + + if (!module.modifyConfig) { + throw new Error("config.ts does not export a modifyConfig function."); + } + + return module.modifyConfig(config); +} + +export async function applyConfigTsAndRemoteConfig(options: { + config: T; + ide: IDE; + ideType: IdeType; + remoteConfigServerUrl?: string; +}): Promise { + const { config, ide, ideType, remoteConfigServerUrl } = options; + + let modifiedConfig = config; + + const configJsContents = await buildConfigTsandReadConfigJs(ide, ideType); + if (configJsContents) { + try { + modifiedConfig = await applyConfigModule( + modifiedConfig, + getConfigJsPath(), + ); + } catch (e) { + console.log("Error loading config.ts: ", e); + } + } + + if (remoteConfigServerUrl) { + try { + modifiedConfig = await applyConfigModule( + modifiedConfig, + getConfigJsPathForRemote(remoteConfigServerUrl), + ); + } catch (e) { + console.log("Error loading remotely set config.js: ", e); + } + } + + return modifiedConfig; +} + async function loadContinueConfigFromJson( ide: IDE, ideSettings: IdeSettings, @@ -857,63 +937,12 @@ async function loadContinueConfigFromJson( // Convert serialized to intermediate config let intermediate = await serializedToIntermediateConfig(withShared, ide); - // Apply config.ts to modify intermediate config - const configJsContents = await buildConfigTsandReadConfigJs( + intermediate = await applyConfigTsAndRemoteConfig({ + config: intermediate, ide, - ideInfo.ideType, - ); - if (configJsContents) { - try { - // Try config.ts first - const configJsPath = getConfigJsPath(); - let module: any; - - try { - module = await import(configJsPath); - } catch (e) { - console.log(e); - console.log( - "Could not load config.ts as absolute path, retrying as file url ...", - ); - try { - module = await import(localPathToUri(configJsPath)); - } catch (e) { - throw new Error("Could not load config.ts as file url either", { - cause: e, - }); - } - } - - if (typeof require !== "undefined") { - delete require.cache[require.resolve(configJsPath)]; - } - if (!module.modifyConfig) { - throw new Error("config.ts does not export a modifyConfig function."); - } - intermediate = module.modifyConfig(intermediate); - } catch (e) { - console.log("Error loading config.ts: ", e); - } - } - - // Apply remote config.js to modify intermediate config - if (ideSettings.remoteConfigServerUrl) { - try { - const configJsPathForRemote = getConfigJsPathForRemote( - ideSettings.remoteConfigServerUrl, - ); - const module = await import(configJsPathForRemote); - if (typeof require !== "undefined") { - delete require.cache[require.resolve(configJsPathForRemote)]; - } - if (!module.modifyConfig) { - throw new Error("config.ts does not export a modifyConfig function."); - } - intermediate = module.modifyConfig(intermediate); - } catch (e) { - console.log("Error loading remotely set config.js: ", e); - } - } + ideType: ideInfo.ideType, + remoteConfigServerUrl: ideSettings.remoteConfigServerUrl, + }); // Convert to final config format const { config: finalConfig, errors: finalErrors } = diff --git a/core/config/yaml/loadContinueConfigFromYaml.vitest.ts b/core/config/yaml/loadContinueConfigFromYaml.vitest.ts new file mode 100644 index 0000000000..6e29bb35d9 --- /dev/null +++ b/core/config/yaml/loadContinueConfigFromYaml.vitest.ts @@ -0,0 +1,114 @@ +import { describe, expect, it, vi } from "vitest"; + +const { mockApplyConfigTsAndRemoteConfig } = vi.hoisted(() => ({ + mockApplyConfigTsAndRemoteConfig: vi.fn(async ({ config }) => ({ + ...config, + allowAnonymousTelemetry: false, + })), +})); + +vi.mock("../load", () => ({ + applyConfigTsAndRemoteConfig: mockApplyConfigTsAndRemoteConfig, +})); + +vi.mock("../../context/mcp/MCPManagerSingleton", () => ({ + MCPManagerSingleton: { + getInstance: () => ({ + setConnections: vi.fn(), + shutdown: vi.fn(), + getStatuses: () => [], + }), + }, +})); + +vi.mock("../../promptFiles/getPromptFiles", () => ({ + getAllPromptFiles: vi.fn().mockResolvedValue([]), +})); + +vi.mock("../../context/mcp/json/loadJsonMcpConfigs", () => ({ + loadJsonMcpConfigs: vi.fn().mockResolvedValue({ errors: [], mcpServers: [] }), +})); + +vi.mock("../../tools", () => ({ + getBaseToolDefinitions: vi.fn().mockReturnValue([]), +})); + +vi.mock("../loadContextProviders", () => ({ + loadConfigContextProviders: vi.fn().mockReturnValue({ + providers: [], + errors: [], + }), +})); + +vi.mock("../loadLocalAssistants", () => ({ + getAllDotContinueDefinitionFiles: vi.fn().mockResolvedValue([]), +})); + +vi.mock("./models", () => ({ + llmsFromModelConfig: vi.fn().mockResolvedValue([]), +})); + +vi.mock("../../util/GlobalContext", () => ({ + GlobalContext: class { + getSharedConfig() { + return {}; + } + + get() { + return {}; + } + + update() {} + }, +})); + +vi.mock("../../control-plane/env", () => ({ + getControlPlaneEnvSync: vi.fn().mockReturnValue({ + CONTROL_PLANE_URL: "https://api.example.com", + }), +})); + +vi.mock("../../control-plane/PolicySingleton", () => ({ + PolicySingleton: { + getInstance: () => ({ policy: null }), + }, +})); + +import { loadContinueConfigFromYaml } from "./loadYaml"; + +describe("loadContinueConfigFromYaml", () => { + it("applies config.ts modifiers for YAML configs", async () => { + const result = await loadContinueConfigFromYaml({ + ide: {} as any, + ideSettings: { + remoteConfigServerUrl: "", + remoteConfigSyncPeriod: 60, + userToken: "", + continueTestEnvironment: "none", + pauseCodebaseIndexOnStart: false, + }, + ideInfo: { ideType: "vscode" } as any, + uniqueId: "test-unique-id", + llmLogger: {} as any, + workOsAccessToken: undefined, + overrideConfigYaml: { + name: "Local Config", + version: "1.0.0", + schema: "v1", + models: [], + }, + controlPlaneClient: { + getAccessToken: vi.fn(), + } as any, + orgScopeId: null, + packageIdentifier: { + uriType: "file", + fileUri: "/tmp/config.yaml", + content: "name: Local Config\nversion: 1.0.0\nschema: v1\n", + }, + }); + + expect(mockApplyConfigTsAndRemoteConfig).toHaveBeenCalledTimes(1); + expect(result.config?.allowAnonymousTelemetry).toBe(false); + }); +}); diff --git a/core/config/yaml/loadYaml.ts b/core/config/yaml/loadYaml.ts index f7a82635b6..18edc8d73b 100644 --- a/core/config/yaml/loadYaml.ts +++ b/core/config/yaml/loadYaml.ts @@ -27,6 +27,7 @@ import { ControlPlaneClient } from "../../control-plane/client"; import TransformersJsEmbeddingsProvider from "../../llm/llms/TransformersJsEmbeddingsProvider"; import { getAllPromptFiles } from "../../promptFiles/getPromptFiles"; import { GlobalContext } from "../../util/GlobalContext"; +import { applyConfigTsAndRemoteConfig } from "../load"; import { modifyAnyConfigWithSharedConfig } from "../sharedConfig"; import { convertPromptBlockToSlashCommand } from "../../commands/slash/promptBlockSlashCommand"; @@ -492,8 +493,15 @@ export async function loadContinueConfigFromYaml(options: { withShared.allowAnonymousTelemetry = true; } - return { + const withConfigTs = await applyConfigTsAndRemoteConfig({ config: withShared, + ide, + ideType: ideInfo.ideType, + remoteConfigServerUrl: ideSettings.remoteConfigServerUrl, + }); + + return { + config: withConfigTs, errors: [...(configYamlResult.errors ?? []), ...localErrors], configLoadInterrupted: false, configName: configYamlResult.configName,