Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/gui/AIAssistantProvidersModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { dedupeModels } from "src/ai/modelsDirectory";
import { discoverProviderModels } from "src/ai/modelDiscoveryService";
import { ModelDirectoryModal } from "./ModelDirectoryModal";
import { setPasswordOnBlur } from "src/utils/setPasswordOnBlur";
import { deepClone } from "src/utils/deepClone";
import GenericInputPrompt from "./GenericInputPrompt/GenericInputPrompt";
import { ProviderPickerModal } from "./ProviderPickerModal";
import GenericYesNoPrompt from "./GenericYesNoPrompt/GenericYesNoPrompt";
Expand Down Expand Up @@ -101,14 +102,14 @@ export class AIAssistantProvidersModal extends Modal {
button.setWarning();
button.setIcon("trash" as IconType);
})
.addButton((button) => {
button.setButtonText("Edit").onClick(() => {
this.selectedProvider = provider;
this._selectedProviderClone = structuredClone(provider);
.addButton((button) => {
button.setButtonText("Edit").onClick(() => {
this.selectedProvider = provider;
this._selectedProviderClone = deepClone(provider);

this.reload();
this.reload();
});
});
});
});
}

Expand Down
11 changes: 2 additions & 9 deletions src/gui/MacroGUIs/ConditionalBranchEditorModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,12 @@ import type { App } from "obsidian";
import type QuickAdd from "../../main";
import type IChoice from "../../types/choices/IChoice";
import type { ICommand } from "../../types/macros/ICommand";
import { deepClone } from "../../utils/deepClone";
import {
CommandSequenceEditor,
type CommandSequenceEditorConditionalHandlers,
} from "./CommandSequenceEditor";

function cloneCommands(commands: ICommand[]): ICommand[] {
if (typeof structuredClone === "function") {
return structuredClone(commands);
}

return JSON.parse(JSON.stringify(commands)) as ICommand[];
}

interface ConditionalBranchEditorModalOptions {
app: App;
plugin: QuickAdd;
Expand All @@ -40,7 +33,7 @@ export class ConditionalBranchEditorModal extends Modal {
this.plugin = options.plugin;
this.choices = options.choices;
this.conditionalHandlers = options.conditionalHandlers;
this.workingCommands = cloneCommands(options.commands);
this.workingCommands = deepClone(options.commands);

this.waitForClose = new Promise<ICommand[] | null>((resolve) => {
this.resolvePromise = resolve;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type QuickAdd from "src/main";
import type IChoice from "src/types/choices/IChoice";
import type { IMacro } from "src/types/macros/IMacro";
import { deepClone } from "src/utils/deepClone";
import { isMultiChoice } from "./helpers/isMultiChoice";
import { isNestedChoiceCommand } from "./helpers/isNestedChoiceCommand";
import { isOldTemplateChoice } from "./helpers/isOldTemplateChoice";
Expand Down Expand Up @@ -48,13 +49,13 @@ const incrementFileNameSettingMoveToDefaultBehavior: Migration = {
"'Increment file name' setting moved to 'Set default behavior if file already exists' setting",

migrate: async (plugin: QuickAdd): Promise<void> => {
const choicesCopy = structuredClone(plugin.settings.choices);
const choicesCopy = deepClone(plugin.settings.choices);
const choices = recursiveRemoveIncrementFileName(choicesCopy);

const macrosCopy = structuredClone((plugin.settings as any).macros || []);
const macrosCopy = deepClone((plugin.settings as any).macros || []);
const macros = removeIncrementFileName(macrosCopy);

plugin.settings.choices = structuredClone(choices);
plugin.settings.choices = deepClone(choices);

// Save the migrated macros back to settings - later migrations still need it
(plugin.settings as any).macros = macros;
Expand Down
3 changes: 2 additions & 1 deletion src/migrations/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import addDefaultAIProviders from "./addDefaultAIProviders";
import removeMacroIndirection from "./removeMacroIndirection";
import migrateFileOpeningSettings from "./migrateFileOpeningSettings";
import setProviderModelDiscoveryMode from "./setProviderModelDiscoveryMode";
import { deepClone } from "src/utils/deepClone";

const migrations: Migrations = {
useQuickAddTemplateFolder,
Expand Down Expand Up @@ -38,7 +39,7 @@ async function migrate(plugin: QuickAdd): Promise<void> {
`Running migration ${migration}: ${migrations[migration].description}`
);

const backup = structuredClone(plugin.settings);
const backup = deepClone(plugin.settings);

try {
await migrations[migration].migrate(plugin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isCaptureChoice } from "./helpers/isCaptureChoice";
import { isMultiChoice } from "./helpers/isMultiChoice";
import { isNestedChoiceCommand } from "./helpers/isNestedChoiceCommand";
import type { Migration } from "./Migrations";
import { deepClone } from "src/utils/deepClone";

function recursiveMigrateSettingInChoices(choices: IChoice[]): IChoice[] {
for (const choice of choices) {
Expand Down Expand Up @@ -48,10 +49,10 @@ const mutualExclusionInsertAfterAndWriteToBottomOfFile: Migration = {
"Mutual exclusion of insertAfter and writeToBottomOfFile settings. If insertAfter is enabled, writeToBottomOfFile is disabled. To support changes in settings UI.",

migrate: async (plugin) => {
const choicesCopy = structuredClone(plugin.settings.choices);
const choicesCopy = deepClone(plugin.settings.choices);
const choices = recursiveMigrateSettingInChoices(choicesCopy);

const macrosCopy = structuredClone((plugin.settings as any).macros || []);
const macrosCopy = deepClone((plugin.settings as any).macros || []);
const macros = migrateSettingsInMacros(macrosCopy);

plugin.settings.choices = choices;
Expand Down
3 changes: 2 additions & 1 deletion src/migrations/setProviderModelDiscoveryMode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type QuickAdd from "src/main";
import { settingsStore } from "src/settingsStore";
import type { Migration } from "./Migrations";
import { deepClone } from "src/utils/deepClone";

const setProviderModelDiscoveryMode: Migration = {
description:
Expand All @@ -25,7 +26,7 @@ const setProviderModelDiscoveryMode: Migration = {
...state,
ai: {
...state.ai,
providers: structuredClone(providers),
providers: deepClone(providers),
},
}));
},
Expand Down
5 changes: 2 additions & 3 deletions src/services/choiceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MultiChoice } from "../types/choices/MultiChoice";
import { TemplateChoice } from "../types/choices/TemplateChoice";
import { excludeKeys } from "../utilityObsidian";
import { regenerateIds } from "../utils/macroUtils";
import { deepClone } from "../utils/deepClone";

const choiceConstructors: Record<ChoiceType, new (name: string) => IChoice> = {
Template: TemplateChoice,
Expand Down Expand Up @@ -49,9 +50,7 @@ export function duplicateChoice(choice: IChoice): IChoice {
Object.assign(newChoice, excludeKeys(choice, ["id", "name"]));

if (choice.type === "Macro") {
(newChoice as IMacroChoice).macro = structuredClone(
(choice as IMacroChoice).macro,
);
(newChoice as IMacroChoice).macro = deepClone((choice as IMacroChoice).macro);
regenerateIds((newChoice as IMacroChoice).macro);
}

Expand Down
25 changes: 13 additions & 12 deletions src/services/packageExportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import type {
} from "../types/packages/QuickAddPackage";
import { QUICKADD_PACKAGE_SCHEMA_VERSION } from "../types/packages/QuickAddPackage";
import {
collectChoiceClosure,
collectScriptDependencies,
collectFileDependencies,
collectChoiceClosure,
collectScriptDependencies,
collectFileDependencies,
} from "../utils/packageTraversal";
import { log } from "../logger/logManager";
import { encodeToBase64 } from "../utils/base64";
import { deepClone } from "../utils/deepClone";

export interface BuildPackageOptions {
choices: IChoice[];
Expand Down Expand Up @@ -61,15 +62,15 @@ export async function buildPackage(
const assets = await encodeAssets(app, assetDescriptors);

const packageChoices: QuickAddPackageChoice[] = closure.choiceIds.map(
(choiceId) => {
const entry = closure.catalog.get(choiceId);
if (!entry) throw new Error(`Choice '${choiceId}' missing from catalog.`);
const clonedChoice = structuredClone(entry.choice);
pruneChoiceTree(clonedChoice, includedChoiceIds);
return {
choice: clonedChoice,
pathHint: [...entry.path],
parentChoiceId: entry.parentId,
(choiceId) => {
const entry = closure.catalog.get(choiceId);
if (!entry) throw new Error(`Choice '${choiceId}' missing from catalog.`);
const clonedChoice = deepClone(entry.choice);
pruneChoiceTree(clonedChoice, includedChoiceIds);
return {
choice: clonedChoice,
pathHint: [...entry.path],
parentChoiceId: entry.parentId,
};
},
);
Expand Down
11 changes: 6 additions & 5 deletions src/services/packageImportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { IUserScript } from "../types/macros/IUserScript";
import { CommandType } from "../types/macros/CommandType";
import { log } from "../logger/logManager";
import { decodeFromBase64 } from "../utils/base64";
import { deepClone } from "../utils/deepClone";

export interface LoadedQuickAddPackage {
pkg: QuickAddPackage;
Expand Down Expand Up @@ -259,7 +260,7 @@ export async function applyPackageImport(
idMap.set(entry.choice.id, newId);
}

const updatedChoices = structuredClone(existingChoices);
const updatedChoices = deepClone(existingChoices);
const addedChoiceIds: string[] = [];
const overwrittenChoiceIds: string[] = [];
const skippedChoiceIds: string[] = [];
Expand All @@ -272,10 +273,10 @@ export async function applyPackageImport(
continue;
}

const clone = structuredClone(entry.choice);
const remapped = remapChoiceTree(clone, idMap, importableChoiceIds);
preparedChoices.set(entry.choice.id, remapped);
}
const clone = deepClone(entry.choice);
const remapped = remapChoiceTree(clone, idMap, importableChoiceIds);
preparedChoices.set(entry.choice.id, remapped);
}

const handledChoices = new Set<string>();

Expand Down
3 changes: 2 additions & 1 deletion src/settingsStore.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { createStore } from "zustand/vanilla";
import type { QuickAddSettings } from "./quickAddSettingsTab";
import { DEFAULT_SETTINGS } from "./quickAddSettingsTab";
import { deepClone } from "./utils/deepClone";

type SettingsState = QuickAddSettings;

export const settingsStore = (() => {
const useSettingsStore = createStore<SettingsState>((set, _get) => ({
...structuredClone(DEFAULT_SETTINGS),
...deepClone(DEFAULT_SETTINGS),
}));

const { getState, setState, subscribe } = useSettingsStore;
Expand Down
3 changes: 2 additions & 1 deletion src/utilityObsidian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { AppendLinkOptions, LinkPlacement } from "./types/linkPlacement";
import { placementSupportsEmbed } from "./types/linkPlacement";
import type { IUserScript } from "./types/macros/IUserScript";
import { reportError } from "./utils/errorUtils";
import { deepClone } from "./utils/deepClone";

export type TemplaterPluginLike = {
settings?: {
Expand Down Expand Up @@ -945,7 +946,7 @@ export function excludeKeys<T extends object, K extends keyof T>(
sourceObj: T,
except: K[],
): Omit<T, K> {
const obj = structuredClone(sourceObj);
const obj = deepClone(sourceObj);

for (const key of except) {
delete obj[key];
Expand Down
67 changes: 67 additions & 0 deletions src/utils/deepClone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it, afterEach } from "vitest";
import { deepClone } from "./deepClone";

describe("deepClone", () => {
const originalStructuredClone = (globalThis as any).structuredClone;

afterEach(() => {
(globalThis as any).structuredClone = originalStructuredClone;
});

it("deep clones plain objects when structuredClone is missing", () => {
(globalThis as any).structuredClone = undefined;

const value = { a: 1, b: { c: 2 }, d: [1, { e: 3 }] };
const cloned = deepClone(value);

expect(cloned).toEqual(value);
expect(cloned).not.toBe(value);
expect(cloned.b).not.toBe(value.b);
expect(cloned.d).not.toBe(value.d);
expect(cloned.d[1]).not.toBe(value.d[1]);
});

it("handles circular references in the fallback clone", () => {
(globalThis as any).structuredClone = undefined;

const value: { self?: unknown } = {};
value.self = value;

const cloned = deepClone(value) as typeof value;
expect(cloned).not.toBe(value);
expect(cloned.self).toBe(cloned);
});

it("falls back if structuredClone throws", () => {
(globalThis as any).structuredClone = () => {
throw new Error("boom");
};

const value = { a: 1, b: { c: 2 } };
const cloned = deepClone(value);

expect(cloned).toEqual(value);
expect(cloned).not.toBe(value);
expect(cloned.b).not.toBe(value.b);
});

it("clones class instances without mutating the original", () => {
(globalThis as any).structuredClone = undefined;

class Example {
public nested: { value: number };

constructor(value: number) {
this.nested = { value };
}
}

const instance = new Example(123);
const cloned = deepClone(instance);

expect(cloned).toBeInstanceOf(Example);
expect(cloned).not.toBe(instance);
expect(cloned.nested).toEqual(instance.nested);
expect(cloned.nested).not.toBe(instance.nested);
});
});
Loading