fix: quiet extension unresolved import warnings

This commit is contained in:
Peter Steinberger
2026-04-11 21:21:22 +01:00
parent 0f6f80004f
commit b41091ac7f
3 changed files with 89 additions and 11 deletions

View File

@@ -48,6 +48,29 @@ type MSTeamsProcessContext = MSTeamsSendContext & {
) => Promise<unknown[]>;
};
type AzureAccessToken = {
token?: string;
} | null;
type AzureTokenCredential = {
getToken: (scope: string | string[]) => Promise<AzureAccessToken>;
};
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<AzureIdentityModule> {
return (await import(AZURE_IDENTITY_MODULE)) as AzureIdentityModule;
}
export async function loadMSTeamsSdk(): Promise<MSTeamsTeamsSdk> {
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<typeof import("@azure/identity").ClientCertificateCredential>
> | null = null;
let credentialPromise: Promise<AzureTokenCredential> | 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<typeof import("@azure/identity").ManagedIdentityCredential>
> | null = null;
let credentialPromise: Promise<AzureTokenCredential> | 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(),

View File

@@ -7,9 +7,29 @@ type TsdownConfigEntry = {
neverBundle?: string[] | ((id: string) => boolean);
};
entry?: Record<string, string> | 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]);
});
});

View File

@@ -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));
}