mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
refactor: finish dynamic import cleanup
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
161
scripts/check-dynamic-import-warts.mjs
Normal file
161
scripts/check-dynamic-import-warts.mjs
Normal 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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
57
test/scripts/check-dynamic-import-warts.test.ts
Normal file
57
test/scripts/check-dynamic-import-warts.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user