refactor: finish dynamic import cleanup

This commit is contained in:
Peter Steinberger
2026-04-18 17:45:03 +01:00
parent f38727acd9
commit e7d33b4870
16 changed files with 413 additions and 41 deletions

View File

@@ -26,6 +26,21 @@ import { matrixSetupAdapter } from "./setup-core.js";
import type { CoreConfig } from "./types.js";
let matrixCliExitScheduled = false;
type MatrixActionClientModule = typeof import("./matrix/actions/client.js");
type MatrixDirectManagementModule = typeof import("./matrix/direct-management.js");
let matrixActionClientModulePromise: Promise<MatrixActionClientModule> | undefined;
let matrixDirectManagementModulePromise: Promise<MatrixDirectManagementModule> | undefined;
function loadMatrixActionClientModule(): Promise<MatrixActionClientModule> {
matrixActionClientModulePromise ??= import("./matrix/actions/client.js");
return matrixActionClientModulePromise;
}
function loadMatrixDirectManagementModule(): Promise<MatrixDirectManagementModule> {
matrixDirectManagementModulePromise ??= import("./matrix/direct-management.js");
return matrixDirectManagementModulePromise;
}
export function resetMatrixCliStateForTests(): void {
matrixCliExitScheduled = false;
@@ -332,8 +347,8 @@ async function inspectMatrixDirectRoom(params: {
userId: string;
}): Promise<MatrixCliDirectRoomInspection> {
const [{ withResolvedActionClient }, { inspectMatrixDirectRooms }] = await Promise.all([
import("./matrix/actions/client.js"),
import("./matrix/direct-management.js"),
loadMatrixActionClientModule(),
loadMatrixDirectManagementModule(),
]);
return await withResolvedActionClient(
{ accountId: params.accountId },
@@ -363,8 +378,8 @@ async function repairMatrixDirectRoom(params: {
const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig;
const account = resolveMatrixAccount({ cfg, accountId: params.accountId });
const [{ withStartedActionClient }, { repairMatrixDirectRooms }] = await Promise.all([
import("./matrix/actions/client.js"),
import("./matrix/direct-management.js"),
loadMatrixActionClientModule(),
loadMatrixDirectManagementModule(),
]);
return await withStartedActionClient({ accountId: params.accountId }, async (client) => {
const repaired = await repairMatrixDirectRooms({

View File

@@ -85,14 +85,21 @@ import { resolveTelegramToken } from "./token.js";
import { parseTelegramTopicConversation } from "./topic-conversation.js";
type TelegramSendFn = typeof import("./send.js").sendMessageTelegram;
type TelegramUpdateOffsetRuntime = typeof import("../update-offset-runtime-api.js");
let telegramSendModulePromise: Promise<typeof import("./send.js")> | undefined;
let telegramUpdateOffsetRuntimePromise: Promise<TelegramUpdateOffsetRuntime> | undefined;
async function loadTelegramSendModule() {
telegramSendModulePromise ??= import("./send.js");
return await telegramSendModulePromise;
}
async function loadTelegramUpdateOffsetRuntime() {
telegramUpdateOffsetRuntimePromise ??= import("../update-offset-runtime-api.js");
return await telegramUpdateOffsetRuntimePromise;
}
type TelegramSendOptions = NonNullable<Parameters<TelegramSendFn>[2]>;
function resolveTelegramProbe() {
@@ -734,12 +741,12 @@ export const telegramPlugin = createChatChannelPlugin({
const previousToken = resolveTelegramAccount({ cfg: prevCfg, accountId }).token.trim();
const nextToken = resolveTelegramAccount({ cfg: nextCfg, accountId }).token.trim();
if (previousToken !== nextToken) {
const { deleteTelegramUpdateOffset } = await import("../update-offset-runtime-api.js");
const { deleteTelegramUpdateOffset } = await loadTelegramUpdateOffsetRuntime();
await deleteTelegramUpdateOffset({ accountId });
}
},
onAccountRemoved: async ({ accountId }) => {
const { deleteTelegramUpdateOffset } = await import("../update-offset-runtime-api.js");
const { deleteTelegramUpdateOffset } = await loadTelegramUpdateOffsetRuntime();
await deleteTelegramUpdateOffset({ accountId });
},
},

View File

@@ -60,6 +60,7 @@ const ZALO_TYPING_TIMEOUT_MS = 5_000;
type ZaloCoreRuntime = ReturnType<typeof getZaloRuntime>;
type ZaloStatusSink = (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
type ZaloWebhookModule = typeof import("./monitor.webhook.js");
type ZaloProcessingContext = {
token: string;
account: ResolvedZaloAccount;
@@ -78,6 +79,13 @@ type ZaloUpdateProcessingParams = ZaloProcessingContext & {
update: ZaloUpdate;
mediaMaxMb: number;
};
let zaloWebhookModulePromise: Promise<ZaloWebhookModule> | undefined;
function loadZaloWebhookModule(): Promise<ZaloWebhookModule> {
zaloWebhookModulePromise ??= import("./monitor.webhook.js");
return zaloWebhookModulePromise;
}
type ZaloMessagePipelineParams = ZaloProcessingContext & {
message: ZaloMessage;
text?: string;
@@ -130,7 +138,7 @@ export async function handleZaloWebhookRequest(
res: ServerResponse,
): Promise<boolean> {
const { handleZaloWebhookRequest: handleZaloWebhookRequestInternal } =
await import("./monitor.webhook.js");
await loadZaloWebhookModule();
return await handleZaloWebhookRequestInternal(req, res, async ({ update, target }) => {
await processUpdate({
update,
@@ -657,7 +665,7 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
try {
if (useWebhook) {
const { registerZaloWebhookTarget } = await import("./monitor.webhook.js");
const { registerZaloWebhookTarget } = await loadZaloWebhookModule();
if (!webhookUrl || !webhookSecret) {
throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
}

View File

@@ -1314,6 +1314,7 @@
"lint:plugins:plugin-sdk-subpaths-exported": "node scripts/check-plugin-sdk-subpath-exports.mjs",
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
"lint:tmp:channel-agnostic-boundaries": "node scripts/check-channel-agnostic-boundaries.mjs",
"lint:tmp:dynamic-import-warts": "node scripts/check-dynamic-import-warts.mjs",
"lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs",
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
"lint:ui:no-raw-window-open": "node scripts/check-no-raw-window-open.mjs",

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import ts from "typescript";
import {
collectTypeScriptFilesFromRoots,
resolveRepoRoot,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const defaultRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
function readStringLiteral(node) {
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
return node.text;
}
return null;
}
function isTypeOnlyImportDeclaration(node) {
const clause = node.importClause;
if (!clause) {
return false;
}
if (clause.isTypeOnly) {
return true;
}
if (clause.name) {
return false;
}
const bindings = clause.namedBindings;
return (
Boolean(bindings) &&
ts.isNamedImports(bindings) &&
bindings.elements.length > 0 &&
bindings.elements.every((element) => element.isTypeOnly)
);
}
function isIgnoredTestHelperContent(content) {
return /\bfrom\s+["']vitest["']/.test(content) || /\bfrom\s+["']@vitest\//.test(content);
}
function isIgnoredTestHelperPath(filePath) {
const normalized = filePath.split(path.sep).join("/");
const base = path.basename(filePath);
return (
normalized.includes("/test/") ||
/(?:^|[./-])test(?:[./-]|$)/.test(base) ||
base.includes("test-support") ||
base.includes("test-harness") ||
base.includes("test-helper") ||
base.includes("test-mocks")
);
}
export function findDynamicImportAdvisories(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const staticRuntimeImports = new Map();
const dynamicImports = new Map();
const addLine = (map, specifier, line) => {
const lines = map.get(specifier) ?? [];
lines.push(line);
map.set(specifier, lines);
};
const visit = (node) => {
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
!isTypeOnlyImportDeclaration(node)
) {
addLine(staticRuntimeImports, node.moduleSpecifier.text, toLine(sourceFile, node));
}
if (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.length > 0
) {
const specifier = readStringLiteral(node.arguments[0]);
if (specifier) {
addLine(dynamicImports, specifier, toLine(sourceFile, node));
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
const advisories = [];
for (const [specifier, dynamicLines] of dynamicImports) {
const staticLines = staticRuntimeImports.get(specifier);
if (staticLines?.length) {
advisories.push({
line: dynamicLines[0],
reason: `runtime static + dynamic import of "${specifier}" (static line ${staticLines[0]})`,
});
}
if (dynamicLines.length > 1) {
advisories.push({
line: dynamicLines[0],
reason: `repeated direct dynamic import of "${specifier}" (${dynamicLines.length} callsites: ${dynamicLines.join(", ")})`,
});
}
}
return advisories;
}
export async function collectDynamicImportAdvisories(options = {}) {
const roots = options.roots ?? defaultRoots;
const files = await collectTypeScriptFilesFromRoots(roots, {
extraTestSuffixes: [".suite.ts"],
});
const advisories = [];
for (const filePath of files) {
if (isIgnoredTestHelperPath(filePath)) {
continue;
}
const content = await fs.readFile(filePath, "utf8");
if (isIgnoredTestHelperContent(content)) {
continue;
}
for (const advisory of findDynamicImportAdvisories(content, filePath)) {
advisories.push({
path: path.relative(repoRoot, filePath),
...advisory,
});
}
}
return advisories;
}
export async function main(argv = process.argv.slice(2)) {
const fail = argv.includes("--fail");
const json = argv.includes("--json");
const advisories = await collectDynamicImportAdvisories();
if (json) {
console.log(JSON.stringify({ advisories }, null, 2));
} else if (advisories.length === 0) {
console.log("No dynamic import advisories found.");
} else {
console.log(`Dynamic import advisories (${advisories.length}):`);
for (const advisory of advisories) {
console.log(`- ${advisory.path}:${advisory.line} ${advisory.reason}`);
}
console.log("Advisory only. Use --fail when ratcheting this into a hard check.");
}
if (fail && advisories.length > 0) {
process.exit(1);
}
}
runAsScript(import.meta.url, main);

View File

@@ -73,11 +73,20 @@ function isOpenAIProvider(provider?: string) {
const MEMORY_FLUSH_ALLOWED_TOOL_NAMES = new Set(["read", "write"]);
type BashToolsModule = typeof import("./bash-tools.js");
let bashToolsModulePromise: Promise<BashToolsModule> | undefined;
function loadBashToolsModule(): Promise<BashToolsModule> {
bashToolsModulePromise ??= import("./bash-tools.js");
return bashToolsModulePromise;
}
function createLazyExecTool(defaults?: ExecToolDefaults): AnyAgentTool {
let loadedTool: AnyAgentTool | undefined;
const loadTool = async () => {
if (!loadedTool) {
const { createExecTool } = await import("./bash-tools.js");
const { createExecTool } = await loadBashToolsModule();
loadedTool = createExecTool(defaults) as unknown as AnyAgentTool;
}
return loadedTool;
@@ -103,7 +112,7 @@ function createLazyProcessTool(defaults?: ProcessToolDefaults): AnyAgentTool {
let loadedTool: AnyAgentTool | undefined;
const loadTool = async () => {
if (!loadedTool) {
const { createProcessTool } = await import("./bash-tools.js");
const { createProcessTool } = await loadBashToolsModule();
loadedTool = createProcessTool(defaults) as unknown as AnyAgentTool;
}
return loadedTool;

View File

@@ -15,6 +15,22 @@ import type { ChannelChoice } from "../onboard-types.js";
import { applyAccountName, applyChannelAccountConfig } from "./add-mutators.js";
import { channelLabel, requireValidConfigFileSnapshot, shouldUseWizard } from "./shared.js";
type ChannelSetupPluginInstallModule = typeof import("../channel-setup/plugin-install.js");
type OnboardChannelsModule = typeof import("../onboard-channels.js");
let channelSetupPluginInstallPromise: Promise<ChannelSetupPluginInstallModule> | undefined;
let onboardChannelsPromise: Promise<OnboardChannelsModule> | undefined;
function loadChannelSetupPluginInstall(): Promise<ChannelSetupPluginInstallModule> {
channelSetupPluginInstallPromise ??= import("../channel-setup/plugin-install.js");
return channelSetupPluginInstallPromise;
}
function loadOnboardChannels(): Promise<OnboardChannelsModule> {
onboardChannelsPromise ??= import("../onboard-channels.js");
return onboardChannelsPromise;
}
export type ChannelsAddOptions = {
channel?: string;
account?: string;
@@ -57,7 +73,7 @@ export async function channelsAddCommand(
if (useWizard) {
const [{ buildAgentSummaries }, onboardChannels] = await Promise.all([
import("../agents.config.js"),
import("../onboard-channels.js"),
loadOnboardChannels(),
]);
const prompter = createClackPrompter();
const postWriteHooks = onboardChannels.createChannelOnboardingPostWriteHookCollector();
@@ -206,7 +222,7 @@ export async function channelsAddCommand(
return existing;
}
const { loadChannelSetupPluginRegistrySnapshotForChannel } =
await import("../channel-setup/plugin-install.js");
await loadChannelSetupPluginInstall();
const snapshot = loadChannelSetupPluginRegistrySnapshotForChannel({
cfg: nextConfig,
runtime,
@@ -230,8 +246,7 @@ export async function channelsAddCommand(
workspaceDir,
})
) {
const { ensureChannelSetupPluginInstalled } =
await import("../channel-setup/plugin-install.js");
const { ensureChannelSetupPluginInstalled } = await loadChannelSetupPluginInstall();
const prompter = createClackPrompter();
const result = await ensureChannelSetupPluginInstalled({
cfg: nextConfig,
@@ -360,7 +375,7 @@ export async function channelsAddCommand(
runtime.log(`Added ${channelLabel(channel)} account "${accountId}".`);
const afterAccountConfigWritten = plugin.setup?.afterAccountConfigWritten;
if (afterAccountConfigWritten) {
const { runCollectedChannelOnboardingPostWriteHooks } = await import("../onboard-channels.js");
const { runCollectedChannelOnboardingPostWriteHooks } = await loadOnboardChannels();
await runCollectedChannelOnboardingPostWriteHooks({
hooks: [
{

View File

@@ -50,6 +50,14 @@ import { promptRemoteGatewayConfig } from "./onboard-remote.js";
import { setupSkills } from "./onboard-skills.js";
type ConfigureSectionChoice = WizardSection | "__continue";
type SetupPluginConfigModule = typeof import("../wizard/setup.plugin-config.js");
let setupPluginConfigModulePromise: Promise<SetupPluginConfigModule> | undefined;
function loadSetupPluginConfigModule(): Promise<SetupPluginConfigModule> {
setupPluginConfigModulePromise ??= import("../wizard/setup.plugin-config.js");
return setupPluginConfigModulePromise;
}
function mergeWizardConfigOntoLatest(current: unknown, base: unknown, next: unknown): unknown {
if (isDeepStrictEqual(next, base)) {
@@ -617,7 +625,7 @@ export async function runConfigureWizard(
}
if (selected.includes("plugins")) {
const { configurePluginConfig } = await import("../wizard/setup.plugin-config.js");
const { configurePluginConfig } = await loadSetupPluginConfigModule();
nextConfig = await configurePluginConfig({
config: nextConfig,
prompter,
@@ -683,7 +691,7 @@ export async function runConfigureWizard(
}
if (choice === "plugins") {
const { configurePluginConfig } = await import("../wizard/setup.plugin-config.js");
const { configurePluginConfig } = await loadSetupPluginConfigModule();
nextConfig = await configurePluginConfig({
config: nextConfig,
prompter,

View File

@@ -1,5 +1,14 @@
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
type ChannelDoctorModule = typeof import("./channel-doctor.js");
let channelDoctorModulePromise: Promise<ChannelDoctorModule> | undefined;
function loadChannelDoctorModule(): Promise<ChannelDoctorModule> {
channelDoctorModulePromise ??= import("./channel-doctor.js");
return channelDoctorModulePromise;
}
function hasRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -89,7 +98,7 @@ export async function collectDoctorPreviewWarnings(params: {
}
if (hasChannelConfig) {
const { collectChannelDoctorPreviewWarnings } = await import("./channel-doctor.js");
const { collectChannelDoctorPreviewWarnings } = await loadChannelDoctorModule();
const channelDoctorWarnings = await collectChannelDoctorPreviewWarnings({
cfg: params.cfg,
doctorFixCommand: params.doctorFixCommand,
@@ -144,7 +153,7 @@ export async function collectDoctorPreviewWarnings(params: {
}
if (hasChannelConfig) {
const { collectChannelDoctorEmptyAllowlistExtraWarnings } = await import("./channel-doctor.js");
const { collectChannelDoctorEmptyAllowlistExtraWarnings } = await loadChannelDoctorModule();
const { scanEmptyAllowlistPolicyWarnings } = await import("./empty-allowlist-scan.js");
const emptyAllowlistWarnings = scanEmptyAllowlistPolicyWarnings(params.cfg, {
doctorFixCommand: params.doctorFixCommand,

View File

@@ -36,6 +36,15 @@ export type {
const DEFAULT_TIMEOUT_MS = 10_000;
type ConfigModule = typeof import("../config/config.js");
let configModulePromise: Promise<ConfigModule> | undefined;
function loadConfigModule(): Promise<ConfigModule> {
configModulePromise ??= import("../config/config.js");
return configModulePromise;
}
const debugHealth = (...args: unknown[]) => {
if (isTruthyEnvValue(process.env.OPENCLAW_DEBUG_HEALTH)) {
console.warn("[health:debug]", ...args);
@@ -208,7 +217,7 @@ export async function getHealthSnapshot(params?: {
probe?: boolean;
}): Promise<HealthSummary> {
const timeoutMs = params?.timeoutMs;
const { loadConfig } = await import("../config/config.js");
const { loadConfig } = await loadConfigModule();
const cfg = loadConfig();
const { defaultAgentId, ordered } = resolveAgentOrder(cfg);
const channelBindings = buildChannelAccountBindings(cfg);
@@ -636,6 +645,6 @@ export async function healthCommand(
}
async function readBestEffortHealthConfig(): Promise<OpenClawConfig> {
const { readBestEffortConfig } = await import("../config/config.js");
const { readBestEffortConfig } = await loadConfigModule();
return await readBestEffortConfig();
}

View File

@@ -32,8 +32,31 @@ type SetupCommandDeps = {
writeConfigFile?: (config: OpenClawConfig) => Promise<void>;
};
type AgentWorkspaceModule = typeof import("../agents/workspace.js");
type ConfigIOModule = typeof import("../config/io.js");
type ConfigLoggingModule = typeof import("../config/logging.js");
let agentWorkspaceModulePromise: Promise<AgentWorkspaceModule> | undefined;
let configIOModulePromise: Promise<ConfigIOModule> | undefined;
let configLoggingModulePromise: Promise<ConfigLoggingModule> | undefined;
function loadAgentWorkspaceModule(): Promise<AgentWorkspaceModule> {
agentWorkspaceModulePromise ??= import("../agents/workspace.js");
return agentWorkspaceModulePromise;
}
function loadConfigIOModule(): Promise<ConfigIOModule> {
configIOModulePromise ??= import("../config/io.js");
return configIOModulePromise;
}
function loadConfigLoggingModule(): Promise<ConfigLoggingModule> {
configLoggingModulePromise ??= import("../config/logging.js");
return configLoggingModulePromise;
}
async function createDefaultConfigIO(): Promise<ConfigIO> {
const { createConfigIO } = await import("../config/io.js");
const { createConfigIO } = await loadConfigIOModule();
return createConfigIO();
}
@@ -45,24 +68,24 @@ async function resolveDefaultAgentWorkspaceDir(deps: SetupCommandDeps): Promise<
if (typeof override === "function") {
return await override();
}
const { DEFAULT_AGENT_WORKSPACE_DIR } = await import("../agents/workspace.js");
const { DEFAULT_AGENT_WORKSPACE_DIR } = await loadAgentWorkspaceModule();
return DEFAULT_AGENT_WORKSPACE_DIR;
}
async function ensureDefaultAgentWorkspace(
params: Parameters<EnsureAgentWorkspace>[0],
): ReturnType<EnsureAgentWorkspace> {
const { ensureAgentWorkspace } = await import("../agents/workspace.js");
const { ensureAgentWorkspace } = await loadAgentWorkspaceModule();
return ensureAgentWorkspace(params);
}
async function writeDefaultConfigFile(config: OpenClawConfig): Promise<void> {
const { writeConfigFile } = await import("../config/io.js");
const { writeConfigFile } = await loadConfigIOModule();
await writeConfigFile(config);
}
async function formatDefaultConfigPath(configPath: string): Promise<string> {
const { formatConfigPath } = await import("../config/logging.js");
const { formatConfigPath } = await loadConfigLoggingModule();
return formatConfigPath(configPath);
}
@@ -70,7 +93,7 @@ async function logDefaultConfigUpdated(
runtime: RuntimeEnv,
opts: { path?: string; suffix?: string },
): Promise<void> {
const { logConfigUpdated } = await import("../config/logging.js");
const { logConfigUpdated } = await loadConfigLoggingModule();
logConfigUpdated(runtime, opts);
}

View File

@@ -93,6 +93,15 @@ import type {
} from "./types.js";
import { assertValidParams } from "./validation.js";
type SessionsRuntimeModule = typeof import("./sessions.runtime.js");
let sessionsRuntimeModulePromise: Promise<SessionsRuntimeModule> | undefined;
function loadSessionsRuntimeModule(): Promise<SessionsRuntimeModule> {
sessionsRuntimeModulePromise ??= import("./sessions.runtime.js");
return sessionsRuntimeModulePromise;
}
function requireSessionKey(key: unknown, respond: RespondFn): string | null {
const raw =
typeof key === "string"
@@ -1331,7 +1340,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
const reason = p.reason === "new" ? "new" : "reset";
const { performGatewaySessionReset } = await import("./sessions.runtime.js");
const { performGatewaySessionReset } = await loadSessionsRuntimeModule();
const result = await performGatewaySessionReset({
key,
reason,
@@ -1377,7 +1386,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
cleanupSessionBeforeMutation,
emitGatewaySessionEndPluginHook,
emitSessionUnboundLifecycleEvent,
} = await import("./sessions.runtime.js");
} = await loadSessionsRuntimeModule();
const { entry, legacyKey, canonicalKey } = loadSessionEntry(key);
const mutationCleanupError = await cleanupSessionBeforeMutation({

View File

@@ -49,6 +49,15 @@ type FinalizeOnboardingOptions = {
runtime: RuntimeEnv;
};
type OnboardSearchModule = typeof import("../commands/onboard-search.js");
let onboardSearchModulePromise: Promise<OnboardSearchModule> | undefined;
function loadOnboardSearchModule(): Promise<OnboardSearchModule> {
onboardSearchModulePromise ??= import("../commands/onboard-search.js");
return onboardSearchModulePromise;
}
export async function finalizeSetupWizard(
options: FinalizeOnboardingOptions,
): Promise<{ launchedTui: boolean }> {
@@ -522,8 +531,7 @@ export async function finalizeSetupWizard(
const webSearchEnabled = nextConfig.tools?.web?.search?.enabled;
const configuredSearchProviders = listConfiguredWebSearchProviders({ config: nextConfig });
if (webSearchProvider) {
const { resolveExistingKey, hasExistingKey, hasKeyInEnv } =
await import("../commands/onboard-search.js");
const { resolveExistingKey, hasExistingKey, hasKeyInEnv } = await loadOnboardSearchModule();
const entry = configuredSearchProviders.find((e) => e.id === webSearchProvider);
const label = entry?.label ?? webSearchProvider;
const storedKey = entry ? resolveExistingKey(nextConfig, webSearchProvider) : undefined;
@@ -585,7 +593,7 @@ export async function finalizeSetupWizard(
} else {
// Legacy configs may have a working key (e.g. apiKey or BRAVE_API_KEY) without
// an explicit provider. Runtime auto-detects these, so avoid saying "skipped".
const { hasExistingKey, hasKeyInEnv } = await import("../commands/onboard-search.js");
const { hasExistingKey, hasKeyInEnv } = await loadOnboardSearchModule();
const legacyDetected = configuredSearchProviders.find(
(e) => hasExistingKey(nextConfig, e.id) || hasKeyInEnv(e),
);

View File

@@ -15,6 +15,15 @@ export type ConfigurablePlugin = {
jsonSchema?: Record<string, unknown>;
};
type ManifestRegistryModule = typeof import("../plugins/manifest-registry.js");
let manifestRegistryModulePromise: Promise<ManifestRegistryModule> | undefined;
function loadManifestRegistryModule(): Promise<ManifestRegistryModule> {
manifestRegistryModulePromise ??= import("../plugins/manifest-registry.js");
return manifestRegistryModulePromise;
}
type JsonSchemaProperty = {
type?: string;
enum?: unknown[];
@@ -289,7 +298,7 @@ export async function setupPluginConfig(params: {
prompter: WizardPrompter;
workspaceDir?: string;
}): Promise<OpenClawConfig> {
const { loadPluginManifestRegistry } = await import("../plugins/manifest-registry.js");
const { loadPluginManifestRegistry } = await loadManifestRegistryModule();
const registry = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
@@ -351,7 +360,7 @@ export async function configurePluginConfig(params: {
prompter: WizardPrompter;
workspaceDir?: string;
}): Promise<OpenClawConfig> {
const { loadPluginManifestRegistry } = await import("../plugins/manifest-registry.js");
const { loadPluginManifestRegistry } = await loadManifestRegistryModule();
const registry = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,

View File

@@ -21,6 +21,29 @@ import { WizardCancelledError, type WizardPrompter } from "./prompts.js";
import { resolveSetupSecretInputString } from "./setup.secret-input.js";
import type { QuickstartGatewayDefaults, WizardFlow } from "./setup.types.js";
type AuthChoiceModule = typeof import("../commands/auth-choice.js");
type ConfigLoggingModule = typeof import("../config/logging.js");
type ModelPickerModule = typeof import("../commands/model-picker.js");
let authChoiceModulePromise: Promise<AuthChoiceModule> | undefined;
let configLoggingModulePromise: Promise<ConfigLoggingModule> | undefined;
let modelPickerModulePromise: Promise<ModelPickerModule> | undefined;
function loadAuthChoiceModule(): Promise<AuthChoiceModule> {
authChoiceModulePromise ??= import("../commands/auth-choice.js");
return authChoiceModulePromise;
}
function loadConfigLoggingModule(): Promise<ConfigLoggingModule> {
configLoggingModulePromise ??= import("../config/logging.js");
return configLoggingModulePromise;
}
function loadModelPickerModule(): Promise<ModelPickerModule> {
modelPickerModulePromise ??= import("../commands/model-picker.js");
return modelPickerModulePromise;
}
async function resolveAuthChoiceModelSelectionPolicy(params: {
authChoice: string;
config: OpenClawConfig;
@@ -465,7 +488,7 @@ export async function runSetupWizard(
if (mode === "remote") {
const { promptRemoteGatewayConfig } = await import("../commands/onboard-remote.js");
const { logConfigUpdated } = await import("../config/logging.js");
const { logConfigUpdated } = await loadConfigLoggingModule();
let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter, {
secretInputMode: opts.secretInputMode,
});
@@ -523,7 +546,7 @@ export async function runSetupWizard(
// Explicit skip should stay cold: do not bootstrap auth/profile machinery
// or run model/auth checks when the caller already chose to skip setup.
if (authChoiceFromPrompt) {
const { applyPrimaryModel, promptDefaultModel } = await import("../commands/model-picker.js");
const { applyPrimaryModel, promptDefaultModel } = await loadModelPickerModule();
const modelSelection = await promptDefaultModel({
config: nextConfig,
prompter,
@@ -540,13 +563,14 @@ export async function runSetupWizard(
nextConfig = applyPrimaryModel(nextConfig, modelSelection.model);
}
const { warnIfModelConfigLooksOff } = await import("../commands/auth-choice.js");
const { warnIfModelConfigLooksOff } = await loadAuthChoiceModule();
await warnIfModelConfigLooksOff(nextConfig, prompter);
}
} else {
const { applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff } =
await import("../commands/auth-choice.js");
const { applyPrimaryModel, promptDefaultModel } = await import("../commands/model-picker.js");
const [
{ applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff },
{ applyPrimaryModel, promptDefaultModel },
] = await Promise.all([loadAuthChoiceModule(), loadModelPickerModule()]);
const authResult = await applyAuthChoice({
authChoice,
config: nextConfig,
@@ -629,7 +653,7 @@ export async function runSetupWizard(
}
await writeConfigFile(nextConfig);
const { logConfigUpdated } = await import("../config/logging.js");
const { logConfigUpdated } = await loadConfigLoggingModule();
logConfigUpdated(runtime);
await onboardHelpers.ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),

View File

@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { findDynamicImportAdvisories } from "../../scripts/check-dynamic-import-warts.mjs";
describe("check-dynamic-import-warts", () => {
it("flags runtime static plus dynamic imports of the same module", () => {
const source = `
import { run } from "./runtime.js";
export async function start() {
return await import("./runtime.js");
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([
{
line: 4,
reason: 'runtime static + dynamic import of "./runtime.js" (static line 2)',
},
]);
});
it("ignores type-only static imports", () => {
const source = `
import { type Runtime } from "./runtime.js";
export async function start(): Promise<Runtime> {
return (await import("./runtime.js")).createRuntime();
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([]);
});
it("flags repeated direct dynamic imports", () => {
const source = `
export async function one() {
return await import("./runtime.js");
}
export async function two() {
return await import("./runtime.js");
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([
{
line: 3,
reason: 'repeated direct dynamic import of "./runtime.js" (2 callsites: 3, 6)',
},
]);
});
it("ignores cached loader patterns", () => {
const source = `
let runtimePromise: Promise<typeof import("./runtime.js")> | undefined;
function loadRuntime() {
runtimePromise ??= import("./runtime.js");
return runtimePromise;
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([]);
});
});