From 674562828716b2e5565e7938542b1d2b6e7a44cc Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Fri, 9 Oct 2026 07:08:33 +0200 Subject: [PATCH 1/2] test(macro): cover a user script that exports nothing (#2208) --- src/engine/userScriptExecution.test.ts | 31 +++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/engine/userScriptExecution.test.ts b/src/engine/userScriptExecution.test.ts index 3dcf6aac2..fd7a90530 100644 --- a/src/engine/userScriptExecution.test.ts +++ b/src/engine/userScriptExecution.test.ts @@ -1,5 +1,5 @@ import { TFile, type App } from "obsidian"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import type { IChoiceExecutor } from "../IChoiceExecutor"; import type QuickAdd from "../main"; import { CommandType } from "../types/macros/CommandType"; @@ -27,14 +27,14 @@ module.exports = { }; `; -function createApp(secrets: Map): App { +function createApp(secrets: Map, source = SCRIPT_SOURCE): App { const file = new TFile(); file.path = "scripts/todoist.js"; file.extension = "js"; return { vault: { getAbstractFileByPath: vi.fn(() => file), - read: vi.fn(async () => SCRIPT_SOURCE), + read: vi.fn(async () => source), }, secretStorage: { getSecret: vi.fn((id: string) => secrets.get(id) ?? null), @@ -118,3 +118,28 @@ describe("executeUserScript settings with `Script::Export` member access", () => expect([...secrets.values()]).toEqual(["legacy-token"]); }); }); + +describe("executeUserScript with a script that exports nothing", () => { + afterEach(() => vi.unstubAllGlobals()); + + // #2208: all of the script's work happens at the top level while it loads. + it("runs the top-level code and finishes without output", async () => { + const ran: string[] = []; + vi.stubGlobal("window", { require: () => ran }); + const command: IUserScript = { + id: "side-effect-command", + name: "create_wp_folder", + type: CommandType.UserScript, + path: "scripts/todoist.js", + settings: {}, + }; + + const { result } = await run( + command, + createApp(new Map(), "require('ran').push('top level');"), + ); + + expect(ran).toEqual(["top level"]); + expect(result).toBeUndefined(); + }); +}); From 7fc12331f9ca41ade2bd22eb0ca88fe8a6a880b3 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Fri, 9 Oct 2026 07:09:49 +0200 Subject: [PATCH 2/2] fix(macro): let a user script that exports nothing finish its macro (#2208) --- src/engine/userScriptExecution.test.ts | 14 ++++++++++++++ src/engine/userScriptExecution.ts | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/engine/userScriptExecution.test.ts b/src/engine/userScriptExecution.test.ts index fd7a90530..66ad5407d 100644 --- a/src/engine/userScriptExecution.test.ts +++ b/src/engine/userScriptExecution.test.ts @@ -142,4 +142,18 @@ describe("executeUserScript with a script that exports nothing", () => { expect(ran).toEqual(["top level"]); expect(result).toBeUndefined(); }); + + it("still rejects a selected member that is an empty object", async () => { + const command: IUserScript = { + id: "empty-member-command", + name: "lib::start", + type: CommandType.UserScript, + path: "scripts/todoist.js", + settings: {}, + }; + + await expect( + run(command, createApp(new Map(), "module.exports = { start: {} };")), + ).rejects.toThrow("is an empty object"); + }); }); diff --git a/src/engine/userScriptExecution.ts b/src/engine/userScriptExecution.ts index 4b682cca0..f6212a2d3 100644 --- a/src/engine/userScriptExecution.ts +++ b/src/engine/userScriptExecution.ts @@ -4,7 +4,7 @@ import type { QuickAddApi } from "../quickAddApi"; import type QuickAdd from "../main"; import type { IChoiceExecutor } from "../IChoiceExecutor"; import type { IUserScript } from "../types/macros/IUserScript"; -import { getUserScriptPreloadKey, type LoadedUserScript, loadUserScript } from "../utils/userScript"; +import { getUserScriptMemberAccess, getUserScriptPreloadKey, type LoadedUserScript, loadUserScript } from "../utils/userScript"; import { initializeUserScriptSettings } from "../utils/userScriptSettings"; import { resolveScriptSettings } from "./userScriptSettings"; import { log } from "../logger/logManager"; @@ -120,6 +120,10 @@ export async function executeUserScript( } } + // Nothing exported: the script did its work at the top level while loading. + const drillsMember = (getUserScriptMemberAccess(command.name ?? "").memberAccess ?? []).length > 0; + if (!drillsMember && isRecord(userScript) && Object.keys(userScript).length === 0) return; + try { return await delegate(userScript); } catch (err) {