mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
refactor: cache provider tool runtimes
This commit is contained in:
@@ -6,6 +6,16 @@ import type {
|
|||||||
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||||
|
|
||||||
const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey";
|
const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey";
|
||||||
|
|
||||||
|
type BraveWebSearchRuntime = typeof import("./brave-web-search-provider.runtime.js");
|
||||||
|
|
||||||
|
let braveWebSearchRuntimePromise: Promise<BraveWebSearchRuntime> | undefined;
|
||||||
|
|
||||||
|
function loadBraveWebSearchRuntime(): Promise<BraveWebSearchRuntime> {
|
||||||
|
braveWebSearchRuntimePromise ??= import("./brave-web-search-provider.runtime.js");
|
||||||
|
return braveWebSearchRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const BraveSearchSchema = {
|
const BraveSearchSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -111,7 +121,7 @@ function createBraveToolDefinition(
|
|||||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
||||||
parameters: BraveSearchSchema,
|
parameters: BraveSearchSchema,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { executeBraveSearch } = await import("./brave-web-search-provider.runtime.js");
|
const { executeBraveSearch } = await loadBraveWebSearchRuntime();
|
||||||
return await executeBraveSearch(args, searchConfig);
|
return await executeBraveSearch(args, searchConfig);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
|
import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
|
||||||
import {
|
import {
|
||||||
createWebSearchProviderContractFields,
|
createWebSearchProviderContractFields,
|
||||||
type WebSearchProviderPlugin,
|
type WebSearchProviderPlugin,
|
||||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||||
|
|
||||||
|
type DuckDuckGoClientModule = typeof import("./ddg-client.js");
|
||||||
|
|
||||||
|
let duckDuckGoClientModulePromise: Promise<DuckDuckGoClientModule> | undefined;
|
||||||
|
|
||||||
|
function loadDuckDuckGoClientModule(): Promise<DuckDuckGoClientModule> {
|
||||||
|
duckDuckGoClientModulePromise ??= import("./ddg-client.js");
|
||||||
|
return duckDuckGoClientModulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const DuckDuckGoSearchSchema = {
|
const DuckDuckGoSearchSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -47,10 +57,7 @@ export function createDuckDuckGoWebSearchProvider(): WebSearchProviderPlugin {
|
|||||||
"Search the web using DuckDuckGo. Returns titles, URLs, and snippets with no API key required.",
|
"Search the web using DuckDuckGo. Returns titles, URLs, and snippets with no API key required.",
|
||||||
parameters: DuckDuckGoSearchSchema,
|
parameters: DuckDuckGoSearchSchema,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const [{ runDuckDuckGoSearch }, { readNumberParam, readStringParam }] = await Promise.all([
|
const { runDuckDuckGoSearch } = await loadDuckDuckGoClientModule();
|
||||||
import("./ddg-client.js"),
|
|
||||||
import("openclaw/plugin-sdk/provider-web-search"),
|
|
||||||
]);
|
|
||||||
return await runDuckDuckGoSearch({
|
return await runDuckDuckGoSearch({
|
||||||
config: ctx.config,
|
config: ctx.config,
|
||||||
query: readStringParam(args, "query", { required: true }),
|
query: readStringParam(args, "query", { required: true }),
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ const EXA_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "i
|
|||||||
const EXA_FRESHNESS_VALUES = ["day", "week", "month", "year"] as const;
|
const EXA_FRESHNESS_VALUES = ["day", "week", "month", "year"] as const;
|
||||||
const EXA_MAX_SEARCH_COUNT = 100;
|
const EXA_MAX_SEARCH_COUNT = 100;
|
||||||
|
|
||||||
|
type ExaWebSearchRuntime = typeof import("./exa-web-search-provider.runtime.js");
|
||||||
|
|
||||||
|
let exaWebSearchRuntimePromise: Promise<ExaWebSearchRuntime> | undefined;
|
||||||
|
|
||||||
|
function loadExaWebSearchRuntime(): Promise<ExaWebSearchRuntime> {
|
||||||
|
exaWebSearchRuntimePromise ??= import("./exa-web-search-provider.runtime.js");
|
||||||
|
return exaWebSearchRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const ExaSearchSchema = {
|
const ExaSearchSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -81,8 +90,7 @@ export function createExaWebSearchProvider(): WebSearchProviderPlugin {
|
|||||||
"Search the web using Exa AI. Supports neural or keyword search, publication date filters, and optional highlights or text extraction.",
|
"Search the web using Exa AI. Supports neural or keyword search, publication date filters, and optional highlights or text extraction.",
|
||||||
parameters: ExaSearchSchema,
|
parameters: ExaSearchSchema,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { executeExaWebSearchProviderTool } =
|
const { executeExaWebSearchProviderTool } = await loadExaWebSearchRuntime();
|
||||||
await import("./exa-web-search-provider.runtime.js");
|
|
||||||
return await executeExaWebSearchProviderTool(ctx, args);
|
return await executeExaWebSearchProviderTool(ctx, args);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ import {
|
|||||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||||
|
|
||||||
const FIRECRAWL_CREDENTIAL_PATH = "plugins.entries.firecrawl.config.webSearch.apiKey";
|
const FIRECRAWL_CREDENTIAL_PATH = "plugins.entries.firecrawl.config.webSearch.apiKey";
|
||||||
|
|
||||||
|
type FirecrawlClientModule = typeof import("./firecrawl-client.js");
|
||||||
|
|
||||||
|
let firecrawlClientModulePromise: Promise<FirecrawlClientModule> | undefined;
|
||||||
|
|
||||||
|
function loadFirecrawlClientModule(): Promise<FirecrawlClientModule> {
|
||||||
|
firecrawlClientModulePromise ??= import("./firecrawl-client.js");
|
||||||
|
return firecrawlClientModulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const GenericFirecrawlSearchSchema = {
|
const GenericFirecrawlSearchSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -42,7 +52,7 @@ export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin {
|
|||||||
"Search the web using Firecrawl. Returns structured results with snippets from Firecrawl Search. Use firecrawl_search for Firecrawl-specific knobs like sources or categories.",
|
"Search the web using Firecrawl. Returns structured results with snippets from Firecrawl Search. Use firecrawl_search for Firecrawl-specific knobs like sources or categories.",
|
||||||
parameters: GenericFirecrawlSearchSchema,
|
parameters: GenericFirecrawlSearchSchema,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { runFirecrawlSearch } = await import("./firecrawl-client.js");
|
const { runFirecrawlSearch } = await loadFirecrawlClientModule();
|
||||||
return await runFirecrawlSearch({
|
return await runFirecrawlSearch({
|
||||||
cfg: ctx.config,
|
cfg: ctx.config,
|
||||||
query: typeof args.query === "string" ? args.query : "",
|
query: typeof args.query === "string" ? args.query : "",
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ import {
|
|||||||
import { resolveGeminiApiKey, resolveGeminiModel } from "./gemini-web-search-provider.shared.js";
|
import { resolveGeminiApiKey, resolveGeminiModel } from "./gemini-web-search-provider.shared.js";
|
||||||
|
|
||||||
const GEMINI_CREDENTIAL_PATH = "plugins.entries.google.config.webSearch.apiKey";
|
const GEMINI_CREDENTIAL_PATH = "plugins.entries.google.config.webSearch.apiKey";
|
||||||
|
|
||||||
|
type GeminiWebSearchRuntime = typeof import("./gemini-web-search-provider.runtime.js");
|
||||||
|
|
||||||
|
let geminiWebSearchRuntimePromise: Promise<GeminiWebSearchRuntime> | undefined;
|
||||||
|
|
||||||
|
function loadGeminiWebSearchRuntime(): Promise<GeminiWebSearchRuntime> {
|
||||||
|
geminiWebSearchRuntimePromise ??= import("./gemini-web-search-provider.runtime.js");
|
||||||
|
return geminiWebSearchRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const GEMINI_TOOL_PARAMETERS = {
|
const GEMINI_TOOL_PARAMETERS = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -35,7 +45,7 @@ function createGeminiToolDefinition(
|
|||||||
"Search the web using Gemini with Google Search grounding. Returns AI-synthesized answers with citations from Google Search.",
|
"Search the web using Gemini with Google Search grounding. Returns AI-synthesized answers with citations from Google Search.",
|
||||||
parameters: GEMINI_TOOL_PARAMETERS,
|
parameters: GEMINI_TOOL_PARAMETERS,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { executeGeminiSearch } = await import("./gemini-web-search-provider.runtime.js");
|
const { executeGeminiSearch } = await loadGeminiWebSearchRuntime();
|
||||||
return await executeGeminiSearch(args, searchConfig);
|
return await executeGeminiSearch(args, searchConfig);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import {
|
|||||||
const MINIMAX_CREDENTIAL_PATH = "plugins.entries.minimax.config.webSearch.apiKey";
|
const MINIMAX_CREDENTIAL_PATH = "plugins.entries.minimax.config.webSearch.apiKey";
|
||||||
const MINIMAX_CODING_PLAN_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"] as const;
|
const MINIMAX_CODING_PLAN_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"] as const;
|
||||||
|
|
||||||
|
type MiniMaxWebSearchRuntime = typeof import("./minimax-web-search-provider.runtime.js");
|
||||||
|
|
||||||
|
let miniMaxWebSearchRuntimePromise: Promise<MiniMaxWebSearchRuntime> | undefined;
|
||||||
|
|
||||||
|
function loadMiniMaxWebSearchRuntime(): Promise<MiniMaxWebSearchRuntime> {
|
||||||
|
miniMaxWebSearchRuntimePromise ??= import("./minimax-web-search-provider.runtime.js");
|
||||||
|
return miniMaxWebSearchRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const MiniMaxSearchSchema = {
|
const MiniMaxSearchSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -41,8 +50,7 @@ export function createMiniMaxWebSearchProvider(): WebSearchProviderPlugin {
|
|||||||
"Search the web using MiniMax Search API. Returns titles, URLs, snippets, and related search suggestions.",
|
"Search the web using MiniMax Search API. Returns titles, URLs, snippets, and related search suggestions.",
|
||||||
parameters: MiniMaxSearchSchema,
|
parameters: MiniMaxSearchSchema,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { executeMiniMaxWebSearchProviderTool } =
|
const { executeMiniMaxWebSearchProviderTool } = await loadMiniMaxWebSearchRuntime();
|
||||||
await import("./minimax-web-search-provider.runtime.js");
|
|
||||||
return await executeMiniMaxWebSearchProviderTool(ctx, args);
|
return await executeMiniMaxWebSearchProviderTool(ctx, args);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import { resolvePerplexityRuntimeTransport } from "./perplexity-web-search-provi
|
|||||||
|
|
||||||
const PERPLEXITY_CREDENTIAL_PATH = "plugins.entries.perplexity.config.webSearch.apiKey";
|
const PERPLEXITY_CREDENTIAL_PATH = "plugins.entries.perplexity.config.webSearch.apiKey";
|
||||||
|
|
||||||
|
type PerplexityWebSearchRuntime = typeof import("./perplexity-web-search-provider.runtime.js");
|
||||||
|
|
||||||
|
let perplexityWebSearchRuntimePromise: Promise<PerplexityWebSearchRuntime> | undefined;
|
||||||
|
|
||||||
|
function loadPerplexityWebSearchRuntime(): Promise<PerplexityWebSearchRuntime> {
|
||||||
|
perplexityWebSearchRuntimePromise ??= import("./perplexity-web-search-provider.runtime.js");
|
||||||
|
return perplexityWebSearchRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
@@ -95,8 +104,7 @@ function createPerplexityToolDefinition(
|
|||||||
: "Search the web using Perplexity. Runtime routing decides between native Search API and Sonar chat-completions compatibility. Structured filters are available on the native Search API path.",
|
: "Search the web using Perplexity. Runtime routing decides between native Search API and Sonar chat-completions compatibility. Structured filters are available on the native Search API path.",
|
||||||
parameters: createPerplexityParameters(schemaTransport),
|
parameters: createPerplexityParameters(schemaTransport),
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { executePerplexitySearch } =
|
const { executePerplexitySearch } = await loadPerplexityWebSearchRuntime();
|
||||||
await import("./perplexity-web-search-provider.runtime.js");
|
|
||||||
return await executePerplexitySearch(args, searchConfig);
|
return await executePerplexitySearch(args, searchConfig);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import {
|
|||||||
|
|
||||||
const SEARXNG_CREDENTIAL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
|
const SEARXNG_CREDENTIAL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
|
||||||
|
|
||||||
|
type SearxngClientModule = typeof import("./searxng-client.js");
|
||||||
|
|
||||||
|
let searxngClientModulePromise: Promise<SearxngClientModule> | undefined;
|
||||||
|
|
||||||
|
function loadSearxngClientModule(): Promise<SearxngClientModule> {
|
||||||
|
searxngClientModulePromise ??= import("./searxng-client.js");
|
||||||
|
return searxngClientModulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const SearxngSearchSchema = {
|
const SearxngSearchSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -52,7 +61,7 @@ export function createSearxngWebSearchProvider(): WebSearchProviderPlugin {
|
|||||||
"Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.",
|
"Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.",
|
||||||
parameters: SearxngSearchSchema,
|
parameters: SearxngSearchSchema,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { runSearxngSearch } = await import("./searxng-client.js");
|
const { runSearxngSearch } = await loadSearxngClientModule();
|
||||||
return await runSearxngSearch({
|
return await runSearxngSearch({
|
||||||
config: ctx.config,
|
config: ctx.config,
|
||||||
query: readStringParam(args, "query", { required: true }),
|
query: readStringParam(args, "query", { required: true }),
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ import {
|
|||||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||||
|
|
||||||
const TAVILY_CREDENTIAL_PATH = "plugins.entries.tavily.config.webSearch.apiKey";
|
const TAVILY_CREDENTIAL_PATH = "plugins.entries.tavily.config.webSearch.apiKey";
|
||||||
|
|
||||||
|
type TavilyClientModule = typeof import("./tavily-client.js");
|
||||||
|
|
||||||
|
let tavilyClientModulePromise: Promise<TavilyClientModule> | undefined;
|
||||||
|
|
||||||
|
function loadTavilyClientModule(): Promise<TavilyClientModule> {
|
||||||
|
tavilyClientModulePromise ??= import("./tavily-client.js");
|
||||||
|
return tavilyClientModulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const GenericTavilySearchSchema = {
|
const GenericTavilySearchSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -42,7 +52,7 @@ export function createTavilyWebSearchProvider(): WebSearchProviderPlugin {
|
|||||||
"Search the web using Tavily. Returns structured results with snippets. Use tavily_search for Tavily-specific options like search depth, topic filtering, or AI answers.",
|
"Search the web using Tavily. Returns structured results with snippets. Use tavily_search for Tavily-specific options like search depth, topic filtering, or AI answers.",
|
||||||
parameters: GenericTavilySearchSchema,
|
parameters: GenericTavilySearchSchema,
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const { runTavilySearch } = await import("./tavily-client.js");
|
const { runTavilySearch } = await loadTavilyClientModule();
|
||||||
return await runTavilySearch({
|
return await runTavilySearch({
|
||||||
cfg: ctx.config,
|
cfg: ctx.config,
|
||||||
query: typeof args.query === "string" ? args.query : "",
|
query: typeof args.query === "string" ? args.query : "",
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ import {
|
|||||||
} from "./x-search-tool-shared.js";
|
} from "./x-search-tool-shared.js";
|
||||||
|
|
||||||
const PROVIDER_ID = "xai";
|
const PROVIDER_ID = "xai";
|
||||||
|
type CodeExecutionModule = typeof import("./code-execution.js");
|
||||||
|
|
||||||
|
let codeExecutionModulePromise: Promise<CodeExecutionModule> | undefined;
|
||||||
|
|
||||||
|
function loadCodeExecutionModule(): Promise<CodeExecutionModule> {
|
||||||
|
codeExecutionModulePromise ??= import("./code-execution.js");
|
||||||
|
return codeExecutionModulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
function hasResolvableXaiApiKey(config: unknown): boolean {
|
function hasResolvableXaiApiKey(config: unknown): boolean {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]),
|
resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]),
|
||||||
@@ -89,7 +98,7 @@ function createLazyCodeExecutionTool(ctx: {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
execute: async (toolCallId: string, args: Record<string, unknown>) => {
|
execute: async (toolCallId: string, args: Record<string, unknown>) => {
|
||||||
const { createCodeExecutionTool } = await import("./code-execution.js");
|
const { createCodeExecutionTool } = await loadCodeExecutionModule();
|
||||||
const tool = createCodeExecutionTool({
|
const tool = createCodeExecutionTool({
|
||||||
config: ctx.config as never,
|
config: ctx.config as never,
|
||||||
runtimeConfig: (ctx.runtimeConfig as never) ?? null,
|
runtimeConfig: (ctx.runtimeConfig as never) ?? null,
|
||||||
|
|||||||
@@ -40,6 +40,26 @@ function isTypeOnlyImportDeclaration(node) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readDeclarationName(node) {
|
||||||
|
if (
|
||||||
|
(ts.isFunctionDeclaration(node) ||
|
||||||
|
ts.isMethodDeclaration(node) ||
|
||||||
|
ts.isVariableDeclaration(node)) &&
|
||||||
|
node.name &&
|
||||||
|
ts.isIdentifier(node.name)
|
||||||
|
) {
|
||||||
|
return node.name.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts.isPropertyAssignment(node)) {
|
||||||
|
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) {
|
||||||
|
return node.name.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function isIgnoredTestHelperContent(content) {
|
function isIgnoredTestHelperContent(content) {
|
||||||
return /\bfrom\s+["']vitest["']/.test(content) || /\bfrom\s+["']@vitest\//.test(content);
|
return /\bfrom\s+["']vitest["']/.test(content) || /\bfrom\s+["']@vitest\//.test(content);
|
||||||
}
|
}
|
||||||
@@ -61,6 +81,8 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
|||||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||||
const staticRuntimeImports = new Map();
|
const staticRuntimeImports = new Map();
|
||||||
const dynamicImports = new Map();
|
const dynamicImports = new Map();
|
||||||
|
const directExecuteImports = [];
|
||||||
|
const declarationStack = [];
|
||||||
|
|
||||||
const addLine = (map, specifier, line) => {
|
const addLine = (map, specifier, line) => {
|
||||||
const lines = map.get(specifier) ?? [];
|
const lines = map.get(specifier) ?? [];
|
||||||
@@ -69,6 +91,11 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const visit = (node) => {
|
const visit = (node) => {
|
||||||
|
const declarationName = readDeclarationName(node);
|
||||||
|
if (declarationName) {
|
||||||
|
declarationStack.push(declarationName);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ts.isImportDeclaration(node) &&
|
ts.isImportDeclaration(node) &&
|
||||||
ts.isStringLiteral(node.moduleSpecifier) &&
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
||||||
@@ -84,16 +111,26 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
|
|||||||
) {
|
) {
|
||||||
const specifier = readStringLiteral(node.arguments[0]);
|
const specifier = readStringLiteral(node.arguments[0]);
|
||||||
if (specifier) {
|
if (specifier) {
|
||||||
addLine(dynamicImports, specifier, toLine(sourceFile, node));
|
const line = toLine(sourceFile, node);
|
||||||
|
addLine(dynamicImports, specifier, line);
|
||||||
|
if (declarationStack.includes("execute")) {
|
||||||
|
directExecuteImports.push({
|
||||||
|
line,
|
||||||
|
reason: `direct dynamic import of "${specifier}" inside execute path; move it behind a cached loader`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ts.forEachChild(node, visit);
|
ts.forEachChild(node, visit);
|
||||||
|
if (declarationName) {
|
||||||
|
declarationStack.pop();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
visit(sourceFile);
|
visit(sourceFile);
|
||||||
|
|
||||||
const advisories = [];
|
const advisories = [...directExecuteImports];
|
||||||
for (const [specifier, dynamicLines] of dynamicImports) {
|
for (const [specifier, dynamicLines] of dynamicImports) {
|
||||||
const staticLines = staticRuntimeImports.get(specifier);
|
const staticLines = staticRuntimeImports.get(specifier);
|
||||||
if (staticLines?.length) {
|
if (staticLines?.length) {
|
||||||
|
|||||||
@@ -54,4 +54,39 @@ describe("check-dynamic-import-warts", () => {
|
|||||||
`;
|
`;
|
||||||
expect(findDynamicImportAdvisories(source)).toEqual([]);
|
expect(findDynamicImportAdvisories(source)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("flags direct dynamic imports inside execute paths", () => {
|
||||||
|
const source = `
|
||||||
|
export function createTool() {
|
||||||
|
return {
|
||||||
|
execute: async () => {
|
||||||
|
return await import("./runtime.js");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
expect(findDynamicImportAdvisories(source)).toEqual([
|
||||||
|
{
|
||||||
|
line: 5,
|
||||||
|
reason:
|
||||||
|
'direct dynamic import of "./runtime.js" inside execute path; move it behind a cached loader',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows execute paths that call cached loaders", () => {
|
||||||
|
const source = `
|
||||||
|
let runtimePromise: Promise<typeof import("./runtime.js")> | undefined;
|
||||||
|
function loadRuntime() {
|
||||||
|
runtimePromise ??= import("./runtime.js");
|
||||||
|
return runtimePromise;
|
||||||
|
}
|
||||||
|
export function createTool() {
|
||||||
|
return {
|
||||||
|
execute: async () => await loadRuntime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
expect(findDynamicImportAdvisories(source)).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user