From b41091ac7f6cc349338f121b558acf10ee7913f2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 11 Apr 2026 21:21:22 +0100 Subject: [PATCH] fix: quiet extension unresolved import warnings --- extensions/msteams/src/sdk.ts | 35 ++++++++++++++++----- src/infra/tsdown-config.test.ts | 56 +++++++++++++++++++++++++++++++-- tsdown.config.ts | 9 +++++- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/extensions/msteams/src/sdk.ts b/extensions/msteams/src/sdk.ts index 44bdec09835..6612498c38a 100644 --- a/extensions/msteams/src/sdk.ts +++ b/extensions/msteams/src/sdk.ts @@ -48,6 +48,29 @@ type MSTeamsProcessContext = MSTeamsSendContext & { ) => Promise; }; +type AzureAccessToken = { + token?: string; +} | null; + +type AzureTokenCredential = { + getToken: (scope: string | string[]) => Promise; +}; + +type AzureIdentityModule = { + ClientCertificateCredential: new ( + tenantId: string, + clientId: string, + options: { certificate: string }, + ) => AzureTokenCredential; + ManagedIdentityCredential: new (clientId?: string) => AzureTokenCredential; +}; + +const AZURE_IDENTITY_MODULE = "@azure/identity"; + +async function loadAzureIdentity(): Promise { + return (await import(AZURE_IDENTITY_MODULE)) as AzureIdentityModule; +} + export async function loadMSTeamsSdk(): Promise { const [appsModule, apiModule] = await Promise.all([ import("@microsoft/teams.apps"), @@ -129,13 +152,11 @@ function createCertificateApp( sdk: MSTeamsTeamsSdk, ): MSTeamsApp { // Lazily create and cache the credential so the token cache is reused. - let credentialPromise: Promise< - InstanceType - > | null = null; + let credentialPromise: Promise | null = null; const getCredential = async () => { if (!credentialPromise) { - credentialPromise = import("@azure/identity").then( + credentialPromise = loadAzureIdentity().then( (az) => new az.ClientCertificateCredential(creds.tenantId, creds.appId, { certificate: privateKey, @@ -170,13 +191,11 @@ function createManagedIdentityApp( ): MSTeamsApp { // Lazily create and cache the credential instance so the token cache is // reused across calls instead of hitting IMDS/AAD on every message. - let credentialPromise: Promise< - InstanceType - > | null = null; + let credentialPromise: Promise | null = null; const getCredential = async () => { if (!credentialPromise) { - credentialPromise = import("@azure/identity").then((az) => + credentialPromise = loadAzureIdentity().then((az) => creds.managedIdentityClientId ? new az.ManagedIdentityCredential(creds.managedIdentityClientId) : new az.ManagedIdentityCredential(), diff --git a/src/infra/tsdown-config.test.ts b/src/infra/tsdown-config.test.ts index 871965f281e..3a843ed7f82 100644 --- a/src/infra/tsdown-config.test.ts +++ b/src/infra/tsdown-config.test.ts @@ -7,9 +7,29 @@ type TsdownConfigEntry = { neverBundle?: string[] | ((id: string) => boolean); }; entry?: Record | string[]; + inputOptions?: TsdownInputOptions; outDir?: string; }; +type TsdownLog = { + code?: string; + message?: string; + id?: string; + importer?: string; +}; + +type TsdownOnLog = ( + level: string, + log: TsdownLog, + defaultHandler: (level: string, log: TsdownLog) => void, +) => void; + +type TsdownInputOptions = ( + options: { onLog?: TsdownOnLog }, + format?: unknown, + context?: unknown, +) => { onLog?: TsdownOnLog } | undefined; + function asConfigArray(config: unknown): TsdownConfigEntry[] { return Array.isArray(config) ? (config as TsdownConfigEntry[]) : [config as TsdownConfigEntry]; } @@ -25,6 +45,10 @@ function bundledEntry(pluginId: string): string { return `${bundledPluginRoot(pluginId)}/index`; } +function unifiedDistGraph(): TsdownConfigEntry | undefined { + return asConfigArray(tsdownConfig).find((config) => entryKeys(config).includes("index")); +} + describe("tsdown config", () => { it("keeps core, plugin runtime, plugin-sdk, bundled plugins, and bundled hooks in one dist graph", () => { const configs = asConfigArray(tsdownConfig); @@ -76,8 +100,7 @@ describe("tsdown config", () => { }); it("externalizes staged bundled plugin runtime dependencies", () => { - const configs = asConfigArray(tsdownConfig); - const unifiedGraph = configs.find((config) => entryKeys(config).includes("index")); + const unifiedGraph = unifiedDistGraph(); const neverBundle = unifiedGraph?.deps?.neverBundle; if (typeof neverBundle === "function") { @@ -89,4 +112,33 @@ describe("tsdown config", () => { expect(neverBundle).toEqual(expect.arrayContaining(["silk-wasm", "ws"])); } }); + + it("suppresses unresolved imports from extension source", () => { + const configured = unifiedDistGraph()?.inputOptions?.({})?.onLog; + const handled: TsdownLog[] = []; + + configured?.( + "warn", + { + code: "UNRESOLVED_IMPORT", + message: "Could not resolve '@azure/identity' in extensions/msteams/src/sdk.ts", + }, + (_level, log) => handled.push(log), + ); + + expect(handled).toEqual([]); + }); + + it("keeps unresolved imports outside extension source visible", () => { + const configured = unifiedDistGraph()?.inputOptions?.({})?.onLog; + const handled: TsdownLog[] = []; + const log = { + code: "UNRESOLVED_IMPORT", + message: "Could not resolve 'missing-dependency' in src/index.ts", + }; + + configured?.("warn", log, (_level, forwardedLog) => handled.push(forwardedLog)); + + expect(handled).toEqual([log]); + }); }); diff --git a/tsdown.config.ts b/tsdown.config.ts index 55e9cda72c3..382bbe3231b 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -34,6 +34,10 @@ const SUPPRESSED_EVAL_WARNING_PATHS = [ "bottleneck/lib/RedisConnection.js", ] as const; +function normalizedLogHaystack(log: { message?: string; id?: string; importer?: string }): string { + return [log.message, log.id, log.importer].filter(Boolean).join("\n").replaceAll("\\", "/"); +} + function buildInputOptions(options: InputOptionsArg): InputOptionsReturn { if (process.env.OPENCLAW_BUILD_VERBOSE === "1") { return undefined; @@ -50,10 +54,13 @@ function buildInputOptions(options: InputOptionsArg): InputOptionsReturn { if (log.code === "PLUGIN_TIMINGS") { return true; } + if (log.code === "UNRESOLVED_IMPORT") { + return normalizedLogHaystack(log).includes("extensions/"); + } if (log.code !== "EVAL") { return false; } - const haystack = [log.message, log.id, log.importer].filter(Boolean).join("\n"); + const haystack = normalizedLogHaystack(log); return SUPPRESSED_EVAL_WARNING_PATHS.some((path) => haystack.includes(path)); }