Compare commits

...
Author SHA1 Message Date
dependabot[bot] 9ed472502a chore(deps): bump getrandom from 0.3.4 to 0.4.3
Bumps [getrandom](https://github.com/rust-random/getrandom) from 0.3.4 to 0.4.3.
- [Changelog](https://github.com/rust-random/getrandom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/getrandom/compare/v0.3.4...v0.4.3)

---
updated-dependencies:
- dependency-name: getrandom
  dependency-version: 0.4.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 06:09:39 +00:00
Gregory Schier 87a30200f9 Debug nested template arguments (#788) 2026-11-01 21:46:14 -07:00
Gregory SchierandClaude Opus 5.5 c9ad753672 Bump zip from 7 to 9.0.5 (#802)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:07:04 -07:00
4e7c0095d8 chore(deps-dev): bump vite-plus from 0.3.3 to 1.4.0 (#768)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:30:19 -08:00
dependabot[bot]andGregory Schier 07ed9d75fa chore(deps): bump the npm-production group across 1 directory with 23 updates (#804)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2026-11-01 18:18:37 -08:00
dependabot[bot]andGregory Schier 8c0c76e812 chore(deps): bump seroval from 1.6.7 to 1.7.6 (#794)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2026-11-01 18:03:49 -08:00
60ff38cbdc chore(deps): bump nanoid from 0.4.0 to 0.5.0 (#771)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:57:17 -08:00
dependabot[bot] 405b8b2567 chore(deps): bump md5 from 0.7.0 to 0.8.0 (#770)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 17:44:11 -08:00
dependabot[bot] 489955a913 chore(deps-dev): bump follow-redirects from 1.16.0 to 1.16.2 (#801)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 17:29:14 -08:00
11cbb96746 chore(deps): bump fancy-regex from 0.17.0 to 0.19.2 (#773)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:16:13 -08:00
dependabot[bot] f1ae62b2dc chore(deps): bump fast-uri from 3.1.8 to 3.1.9 (#780)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 17:09:26 -08:00
dependabot[bot] 4bfb1a106a chore(deps): bump the cargo group across 1 directory with 15 updates (#805)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 16:57:32 -08:00
dependabot[bot] bf47095cc0 chore(deps-dev): bump the npm-development group across 1 directory with 7 updates (#803)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 16:43:30 -08:00
0489b67f6c chore(deps): bump rand from 0.8.7 to 0.10.2 (#772)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 16:30:49 -08:00
dependabot[bot] 143acdbd7e chore(deps): bump rustls from 0.23.45 to 0.23.46 (#790)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 16:30:28 -08:00
dependabot[bot] 27698b15bb chore(deps-dev): bump @vitest/mocker from 4.1.11 to 4.1.12 (#778)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 16:20:33 -08:00
dependabot[bot] 48a9c7e81f chore(deps): bump ip-address from 10.7.3 to 10.7.5 (#782)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 16:18:30 -08:00
dependabot[bot] 3c5429897e chore(deps): bump body-parser from 2.3.0 to 2.4.0 (#781)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 16:18:06 -08:00
David Leung 53e766b2a9 Pin the wasm target in the build scripts (#799) 2026-11-01 16:46:55 -07:00
34 changed files with 2145 additions and 1593 deletions

No files matched your search

Generated
+276 -229
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,152 @@
// @vitest-environment happy-dom
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { Workspace } from "@yaakapp-internal/models";
import type { TemplateFunction } from "@yaakapp-internal/plugins";
import type { Tokens } from "@yaakapp-internal/templates";
import { act, type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vite-plus/test";
import { TemplateFunctionDialog } from "./TemplateFunctionDialog";
interface FormProps {
data: Record<string, string | boolean>;
onChange: (values: Record<string, string | boolean>) => void;
}
const mocks = vi.hoisted(() => ({
parse: vi.fn<(text: string) => Tokens>(),
print: vi.fn<(tokens: Tokens) => Promise<string>>(),
serialize: vi.fn<(tokens: Tokens) => { data: string }>(),
preview: vi.fn<({ template }: { template: string }) => { data: string }>(),
form: null as FormProps | null,
config: {
name: "timestamp.format",
args: [
{ type: "text", name: "date" },
{ type: "text", name: "format" },
],
} as TemplateFunction,
}));
vi.mock("@yaakapp-internal/templates", () => ({ parseTemplate: mocks.parse }));
vi.mock("@yaakapp-internal/ui", () => ({
HStack: ({ children }: { children: ReactNode }) => <div>{children}</div>,
InlineCode: "code",
LoadingIcon: () => null,
useDebouncedValue: (value: string) => value,
}));
vi.mock("../hooks/useActiveWorkspace", () => ({ activeWorkspaceAtom: null }));
vi.mock("../hooks/useRenderTemplate", () => ({ useRenderTemplate: mocks.preview }));
vi.mock("../hooks/useTemplateFunctionConfig", () => ({
useTemplateFunctionConfig: () => ({ data: mocks.config }),
}));
vi.mock("../hooks/useTemplateTokensToString", () => ({
templateTokensToString: mocks.print,
useTemplateTokensToString: mocks.serialize,
}));
vi.mock("../lib/dialog", () => ({ showDialog: vi.fn() }));
vi.mock("../lib/encryption", () => ({ convertTemplateToInsecure: vi.fn() }));
vi.mock("../lib/jotai", () => ({ jotaiStore: { get: vi.fn() } }));
vi.mock("../lib/setupOrConfigureEncryption", () => ({ setupOrConfigureEncryption: vi.fn() }));
vi.mock("./core/Dialog", () => ({ DialogFooter: () => <button type="submit">Save</button> }));
vi.mock("./core/IconButton", () => ({ IconButton: () => <button type="button" /> }));
vi.mock("./core/PlainInput", () => ({ PlainInput: () => null }));
vi.mock("./DynamicForm", () => ({
DYNAMIC_FORM_NULL_ARG: "__NULL__",
DynamicForm: (props: FormProps) => {
mocks.form = props;
return <input aria-label="Timestamp" readOnly value={String(props.data.date ?? "")} />;
},
}));
beforeAll(async () => {
const base = pathToFileURL(
resolve(import.meta.dirname, "../../../crates/yaak-templates/pkg") + "/",
);
const bindings = await import(new URL("yaak_templates_bg.js", base).href);
const { instance } = await WebAssembly.instantiate(
await readFile(new URL("yaak_templates_bg.wasm", base)),
{ "./yaak_templates_bg.js": bindings },
);
bindings.__wbg_set_wasm(instance.exports);
(instance.exports.__wbindgen_start as () => void)();
mocks.parse.mockImplementation(bindings.parse_template);
});
let root: Root;
let host: HTMLDivElement;
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
mocks.form = null;
// The transport returns an opaque string. Carry the AST as JSON so the assertions can
// inspect what Preview and Save receive, without duplicating the backend's printer.
mocks.serialize.mockImplementation((tokens) => ({ data: JSON.stringify(tokens) }));
mocks.preview.mockImplementation(({ template }) => ({ data: template }));
host = document.createElement("div");
document.body.append(host);
root = createRoot(host);
});
afterEach(async () => {
await act(async () => root.unmount());
host.remove();
vi.unstubAllGlobals();
});
function functionValue(tokens: Tokens) {
const first = tokens.tokens[0];
if (first?.type !== "tag" || first.val.type !== "fn") throw new Error("Expected function");
return first.val;
}
test.each([
["timestamp.offset(expression='-1h')", "${[ timestamp.offset(expression='-1h') ]}"],
["date_var", "${[ date_var ]}"],
["dates.previous", "${[ dates.previous ]}"],
["'${[ date_var ]}'", "${[ date_var ]}"],
])("Preview and Save preserve %s until that input changes", async (expression, display) => {
const initialTokens = mocks.parse(`\${[ timestamp.format(date=${expression}, format='HH:mm') ]}`);
const originalDate = functionValue(initialTokens).args[0];
const saved = vi.fn<(value: string) => void>();
mocks.print.mockResolvedValue(display);
await act(async () =>
root.render(
<TemplateFunctionDialog
templateFunction={mocks.config}
initialTokens={initialTokens}
model={{ id: "wk_test" } as Workspace}
onChange={saved}
hide={() => {}}
/>,
),
);
expect(host.querySelector<HTMLInputElement>("input")?.value).toBe(display);
async function submit() {
const form = host.querySelector("form");
if (form == null) throw new Error("Dialog did not initialize");
await act(async () => {
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
});
const value = saved.mock.calls.at(-1)?.[0];
expect(value).toBe(mocks.preview.mock.calls.at(-1)?.[0].template);
return functionValue(JSON.parse(value ?? "null") as Tokens).args;
}
expect((await submit())[0]).toEqual(originalDate);
await act(async () => mocks.form?.onChange({ ...mocks.form.data, format: "yyyy" }));
expect(await submit()).toEqual([
originalDate,
{ name: "format", value: { type: "str", text: "yyyy" } },
]);
const edited = "${[ timestamp.offset(expression='-2h') ]}";
await act(async () => mocks.form?.onChange({ ...mocks.form.data, date: edited }));
expect((await submit())[0]).toEqual({ name: "date", value: { type: "str", text: edited } });
await act(async () => mocks.form?.onChange({ ...mocks.form.data, date: "" }));
expect((await submit())[0]).toEqual({ name: "date", value: { type: "str", text: "" } });
});
@@ -24,8 +24,11 @@ import { showDialog } from "../lib/dialog";
import { convertTemplateToInsecure } from "../lib/encryption";
import { jotaiStore } from "../lib/jotai";
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
import {
collectEditableArgumentValues,
preserveUnchangedArgumentExpressions,
} from "../lib/templateFunctionArguments";
import { DialogFooter } from "./core/Dialog";
import { collectArgumentValues } from "./core/Editor/twig/util";
import { IconButton } from "./core/IconButton";
import { PlainInput } from "./core/PlainInput";
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from "./DynamicForm";
@@ -48,7 +51,11 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
}
(async () => {
const initial = collectArgumentValues(initialTokens, templateFunction);
const initial = await collectEditableArgumentValues(
initialTokens,
templateFunction,
templateTokensToString,
);
// HACK: Replace the secure() function's encrypted `value` arg with the decrypted version so
// we can display it in the editor input.
@@ -74,6 +81,7 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
<InitializedTemplateFunctionDialog
{...props}
templateFunction={templateFunction}
initialTokens={initialTokens}
initialArgValues={initialArgValues}
/>
);
@@ -82,10 +90,11 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
function InitializedTemplateFunctionDialog({
templateFunction: { name, previewType: ogPreviewType },
initialArgValues,
initialTokens,
hide,
onChange,
model,
}: Omit<Props, "initialTokens"> & {
}: Props & {
initialArgValues: Record<string, string | boolean>;
}) {
const previewType = ogPreviewType == null ? "live" : ogPreviewType;
@@ -111,12 +120,17 @@ function InitializedTemplateFunctionDialog({
val: {
type: "fn",
name,
args: argTokens,
args: preserveUnchangedArgumentExpressions(
argTokens,
initialTokens,
initialArgValues,
argValues,
),
},
},
],
};
}, [argValues, name]);
}, [argValues, initialArgValues, initialTokens, name]);
const tagText = useTemplateTokensToString(tokens);
const templateFunction = useTemplateFunctionConfig(name, argValues, model);
@@ -0,0 +1,263 @@
import { readFile } from "node:fs/promises";
import type { TemplateFunction } from "@yaakapp-internal/plugins";
import type { FnArg, Tokens } from "@yaakapp-internal/templates";
import { beforeAll, describe, expect, test, vi } from "vite-plus/test";
import {
collectEditableArgumentValues,
preserveUnchangedArgumentExpressions,
} from "./templateFunctionArguments";
// Use the shipped WASM parser without the browser-only `?init` loader.
let parse: (text: string) => Tokens;
beforeAll(async () => {
const base = new URL("../../../crates/yaak-templates/pkg/", import.meta.url);
const bindings = await import(new URL("yaak_templates_bg.js", base).href);
const { instance } = await WebAssembly.instantiate(
await readFile(new URL("yaak_templates_bg.wasm", base)),
{ "./yaak_templates_bg.js": bindings },
);
bindings.__wbg_set_wasm(instance.exports);
(instance.exports.__wbindgen_start as () => void)();
parse = bindings.parse_template;
});
const definition: TemplateFunction = {
name: "timestamp.iso8601",
args: [{ type: "text", name: "date", optional: true }],
};
function args(tokens: Tokens): FnArg[] {
const first = tokens.tokens[0];
if (first?.type !== "tag" || first.val.type !== "fn") throw new Error("Expected a function");
return first.val.args;
}
function stringArg(name: string, text: string): FnArg {
return { name, value: { type: "str", text } };
}
// Printing is an existing async RPC. Assert the exact expression handed to it and return
// its expected text, including real parser round-tripping of quoted/escaped fixtures.
function printer(text: string) {
return vi.fn(async (tokens: Tokens) => {
expect(tokens.tokens).toEqual(parse(text).tokens.filter((t) => t.type !== "eof"));
return text;
});
}
describe("editable template function arguments", () => {
test.each([
[
"reported call",
"${[timestamp.iso8601(date=timestamp.offset(expression='-1h')) ]}",
"${[ timestamp.offset(expression='-1h') ]}",
],
["variable", "${[ timestamp.iso8601(date=date_var) ]}", "${[ date_var ]}"],
["dotted variable", "${[ timestamp.iso8601(date=dates.previous) ]}", "${[ dates.previous ]}"],
[
"nested escaping",
String.raw`${"${["} timestamp.iso8601(date=outer(value=inner(text='it\'s \\ nested'))) ]}`,
String.raw`${"${["} outer(value=inner(text='it\'s \\ nested')) ]}`,
],
])("loads and preserves %s on unchanged Save", async (_, source, display) => {
const initialTokens = parse(source);
const snapshot = structuredClone(initialTokens);
const print = printer(display);
const initial = await collectEditableArgumentValues(initialTokens, definition, print);
expect(initial).toEqual({ date: display });
expect(print).toHaveBeenCalledOnce();
const saved = preserveUnchangedArgumentExpressions(
[stringArg("date", display)],
initialTokens,
initial,
{ ...initial },
);
expect(saved).toEqual(args(initialTokens));
expect(initialTokens).toEqual(snapshot);
});
test.each([
String.raw`${"${["} timestamp.iso8601(date='${"${["} timestamp.offset(expression=\'-1h\') ]}') ]}`,
"${[ timestamp.iso8601(date='${[ date_var ]}') ]}",
"${[ timestamp.iso8601(date='${[ dates.previous ]}') ]}",
"${[ timestamp.iso8601(date='2026-10-27T11:00:00.000Z') ]}",
])("leaves existing quoted arguments unchanged: %s", async (source) => {
const initialTokens = parse(source);
const print = vi.fn<(_: Tokens) => Promise<string>>();
const initial = await collectEditableArgumentValues(initialTokens, definition, print);
const value = args(initialTokens)[0]?.value;
if (value?.type !== "str") throw new Error("Expected a string fixture");
expect(initial).toEqual({ date: value.text });
expect(print).not.toHaveBeenCalled();
expect(
preserveUnchangedArgumentExpressions(
[stringArg("date", value.text)],
initialTokens,
initial,
initial,
),
).toEqual(args(initialTokens));
});
test("editing a sibling preserves the original expression", async () => {
const initialTokens = parse(
"${[ timestamp.format(date=timestamp.offset(expression='-1h'), format='HH:mm') ]}",
);
const initial = await collectEditableArgumentValues(
initialTokens,
{
name: "timestamp.format",
args: [...definition.args, { type: "text", name: "format" }],
},
printer("${[ timestamp.offset(expression='-1h') ]}"),
);
const values: Record<string, string | boolean> = { ...initial, format: "yyyy-MM-dd" };
const saved = preserveUnchangedArgumentExpressions(
[stringArg("date", String(values.date)), stringArg("format", String(values.format))],
initialTokens,
initial,
values,
);
expect(saved).toEqual([args(initialTokens)[0], stringArg("format", "yyyy-MM-dd")]);
});
test.each(["${[ timestamp.offset(expression='-2h') ]}", "2026-10-26", ""])(
"intentional edits use the form's quoted-string convention: %s",
async (edited) => {
const initialTokens = parse("${[ timestamp.iso8601(date=date_var) ]}");
const initial = await collectEditableArgumentValues(
initialTokens,
definition,
printer("${[ date_var ]}"),
);
const changed = [stringArg("date", edited)];
expect(
preserveUnchangedArgumentExpressions(changed, initialTokens, initial, { date: edited }),
).toEqual(changed);
// Returning the input to its original text restores the original expression type.
expect(
preserveUnchangedArgumentExpressions(
[stringArg("date", String(initial.date))],
initialTokens,
initial,
{ ...initial },
),
).toEqual(args(initialTokens));
},
);
test("does not resurrect removed or explicitly cleared expression arguments", async () => {
const initialTokens = parse("${[ timestamp.iso8601(date=date_var) ]}");
const initial = await collectEditableArgumentValues(
initialTokens,
definition,
printer("${[ date_var ]}"),
);
expect(preserveUnchangedArgumentExpressions([], initialTokens, initial, {})).toEqual([]);
const nullArg: FnArg = { name: "date", value: { type: "null" } };
expect(
preserveUnchangedArgumentExpressions([nullArg], initialTokens, initial, { date: "__NULL__" }),
).toEqual([nullArg]);
// DynamicForm clears some inputs by leaving a key with undefined; the dialog turns it into ''.
expect(
preserveUnchangedArgumentExpressions([stringArg("date", "")], initialTokens, initial, {}),
).toEqual([stringArg("date", "")]);
expect(
preserveUnchangedArgumentExpressions([stringArg("date", "")], initialTokens, {}, {}),
).toEqual([stringArg("date", "")]);
});
test("keeps absence, defaults, explicit empty strings, and false distinct", async () => {
const fn: TemplateFunction = {
name: "test",
args: [
{ type: "text", name: "absent" },
{ type: "text", name: "defaulted", defaultValue: "fallback" },
{ type: "text", name: "empty", defaultValue: "fallback" },
{ type: "checkbox", name: "flag", defaultValue: "true" },
{ type: "checkbox", name: "defaultFlag", defaultValue: "false" },
],
};
const initialTokens = parse("${[ test(empty='', flag=false) ]}");
const print = vi.fn<(_: Tokens) => Promise<string>>();
const initial = await collectEditableArgumentValues(initialTokens, fn, print);
expect(initial).toEqual({
defaulted: "fallback",
empty: "",
flag: false,
defaultFlag: "false",
});
expect(print).not.toHaveBeenCalled();
const formArgs: FnArg[] = [
stringArg("defaulted", "fallback"),
stringArg("empty", ""),
{ name: "flag", value: { type: "bool", value: false } },
stringArg("defaultFlag", "false"),
];
expect(preserveUnchangedArgumentExpressions(formArgs, initialTokens, initial, initial)).toEqual(
formArgs,
);
});
test("an expression overrides defaults, including inputs in a group", async () => {
const initialTokens = parse("${[ test(date=dates.previous) ]}");
const initial = await collectEditableArgumentValues(
initialTokens,
{
name: "test",
args: [
{ type: "h_stack", inputs: [{ type: "text", name: "date", defaultValue: "fallback" }] },
],
},
printer("${[ dates.previous ]}"),
);
expect(initial).toEqual({ date: "${[ dates.previous ]}" });
});
test("explicit null keeps the existing default and omission behavior", async () => {
const initialTokens = parse("${[ test(defaulted=null, omitted=null) ]}");
const print = vi.fn<(_: Tokens) => Promise<string>>();
const initial = await collectEditableArgumentValues(
initialTokens,
{
name: "test",
args: [
{ type: "text", name: "defaulted", defaultValue: "fallback" },
{ type: "text", name: "omitted" },
],
},
print,
);
expect(initial).toEqual({ defaulted: "fallback" });
expect(print).not.toHaveBeenCalled();
expect(
preserveUnchangedArgumentExpressions(
[stringArg("defaulted", "fallback")],
initialTokens,
initial,
initial,
),
).toEqual([stringArg("defaulted", "fallback")]);
});
test("keeps a deeply nested expression's AST without adding string wrappers", async () => {
// This shape reaches the native renderer's depth limit. A quoted wrapper around
// the date expression would exceed it, even though shallow calls render identically.
const expression =
"timestamp.iso8601(date=".repeat(44) + "'2026-10-26T10:20:30.000Z'" + ")".repeat(44);
const initialTokens = parse(`\${[ timestamp.iso8601(date=${expression}) ]}`);
const initial = await collectEditableArgumentValues(
initialTokens,
definition,
printer(`\${[ ${expression} ]}`),
);
const saved = preserveUnchangedArgumentExpressions(
[stringArg("date", String(initial.date))],
initialTokens,
initial,
initial,
);
expect(saved).toEqual(args(initialTokens));
});
});
@@ -0,0 +1,59 @@
import type { TemplateFunction } from "@yaakapp-internal/plugins";
import type { FnArg, Tokens } from "@yaakapp-internal/templates";
import { collectArgumentValues } from "../components/core/Editor/twig/util";
type ArgumentValues = Record<string, string | boolean>;
function functionArgs(tokens: Tokens): FnArg[] {
const first = tokens.tokens[0];
return first?.type === "tag" && first.val.type === "fn" ? first.val.args : [];
}
/** Adapt expressions to the template text that the form's editors understand. */
export async function collectEditableArgumentValues(
tokens: Tokens,
templateFunction: TemplateFunction,
tokensToString: (tokens: Tokens) => Promise<string>,
): Promise<ArgumentValues> {
const first = tokens.tokens[0];
if (first?.type !== "tag" || first.val.type !== "fn") {
return collectArgumentValues(tokens, templateFunction);
}
const args = await Promise.all(
first.val.args.map(async (arg): Promise<FnArg> => {
if (arg.value.type !== "fn" && arg.value.type !== "var") return arg;
const text = await tokensToString({ tokens: [{ type: "tag", val: arg.value }] });
return { ...arg, value: { type: "str", text } };
}),
);
return collectArgumentValues(
{ tokens: [{ ...first, val: { ...first.val, args } }] },
templateFunction,
);
}
/**
* Displaying an expression as template text must not change its stored type: a string
* wrapper adds rendering depth. Only deliberately changed inputs use the form's new values.
*/
export function preserveUnchangedArgumentExpressions(
args: FnArg[],
initialTokens: Tokens,
initialValues: ArgumentValues,
values: ArgumentValues,
): FnArg[] {
const initialArgs = functionArgs(initialTokens);
return args.map((arg) => {
const initial = initialArgs.find((a) => a.name === arg.name);
if (
(initial?.value.type === "fn" || initial?.value.type === "var") &&
Object.hasOwn(initialValues, arg.name) &&
values[arg.name] === initialValues[arg.name]
) {
return initial;
}
return arg;
});
}
+21 -20
View File
@@ -16,7 +16,7 @@
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.13.1",
"@codemirror/merge": "^6.11.2",
"@codemirror/merge": "^6.12.3",
"@codemirror/search": "^6.5.11",
"@dnd-kit/core": "^6.3.1",
"@gilbarbara/deep-equal": "^0.4.1",
@@ -27,17 +27,17 @@
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@shopify/lang-jsonc": "^1.0.1",
"@tanstack/react-query": "^5.104.2",
"@tanstack/react-router": "^1.170.42",
"@tanstack/react-virtual": "^3.14.17",
"@tauri-apps/api": "^2.12.3",
"@tauri-apps/plugin-clipboard-manager": "^2.4.2",
"@tauri-apps/plugin-dialog": "^2.8.2",
"@tauri-apps/plugin-fs": "^2.7.0",
"@tanstack/react-query": "^5.105.0",
"@tanstack/react-router": "^1.170.46",
"@tanstack/react-virtual": "^3.14.24",
"@tauri-apps/api": "^2.12.4",
"@tauri-apps/plugin-clipboard-manager": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.8.4",
"@tauri-apps/plugin-fs": "^2.7.1",
"@tauri-apps/plugin-log": "^2.10.1",
"@tauri-apps/plugin-opener": "^2.7.1",
"@tauri-apps/plugin-opener": "^2.7.2",
"@tauri-apps/plugin-os": "^2.4.0",
"@tauri-apps/plugin-shell": "^2.5.0",
"@tauri-apps/plugin-shell": "^2.6.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"cm6-graphql": "^0.2.1",
@@ -54,15 +54,15 @@
"jotai-family": "^1.0.1",
"js-md5": "^0.9.2",
"jsonc-parser": "^3.3.1",
"lucide-react": "^1.55.0",
"mime": "^4.1.1",
"motion": "^14.0.0",
"lucide-react": "^1.64.0",
"mime": "^4.1.2",
"motion": "^14.8.0",
"nanoid": "^6.0.2",
"papaparse": "^5.4.1",
"parse-color": "^1.0.0",
"react": "^19.2.0",
"react": "^19.3.1",
"react-colorful": "^5.6.1",
"react-dom": "^19.2.0",
"react-dom": "^19.3.1",
"react-markdown": "^10.1.0",
"react-pdf": "^11.0.0",
"react-syntax-highlighter": "^16.1.2",
@@ -78,8 +78,8 @@
},
"devDependencies": {
"@lezer/generator": "^1.8.1",
"@rolldown/plugin-babel": "^0.2.3",
"@tanstack/router-plugin": "^1.168.42",
"@rolldown/plugin-babel": "^0.3.0",
"@tanstack/router-plugin": "^1.168.48",
"@types/babel__core": "^7.20.5",
"@types/node": "^22.20.5",
"@types/papaparse": "^5.3.16",
@@ -93,10 +93,11 @@
"@yaakapp-internal/theme": "^1.0.0",
"@yaakapp-internal/ui": "^1.0.0",
"babel-plugin-react-compiler": "^1.0.0",
"rollup": "^4.64.3",
"vite": "npm:@voidzero-dev/vite-plus-core@^0.3.3",
"happy-dom": "^21.0.4",
"rollup": "^4.66.0",
"vite": "npm:@voidzero-dev/vite-plus-core@^1.4.0",
"vite-plugin-static-copy": "^4.1.1",
"vite-plugin-wasm": "^3.5.0",
"vite-plus": "^0.3.3"
"vite-plus": "^1.4.0"
}
}
+11 -11
View File
@@ -9,31 +9,31 @@
"lint": "tsc --noEmit"
},
"dependencies": {
"@tanstack/react-query": "^5.104.2",
"@tanstack/react-virtual": "^3.14.17",
"@tauri-apps/api": "^2.12.3",
"@tauri-apps/plugin-dialog": "^2.8.2",
"@tauri-apps/plugin-opener": "^2.7.1",
"@tanstack/react-query": "^5.105.0",
"@tanstack/react-virtual": "^3.14.24",
"@tauri-apps/api": "^2.12.4",
"@tauri-apps/plugin-dialog": "^2.8.4",
"@tauri-apps/plugin-opener": "^2.7.2",
"@tauri-apps/plugin-os": "^2.4.0",
"@yaakapp-internal/proxy-lib": "^1.0.0",
"@yaakapp-internal/theme": "^1.0.0",
"@yaakapp-internal/ui": "^1.0.0",
"classnames": "^2.5.1",
"jotai": "^2.18.0",
"motion": "^14.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"motion": "^14.8.0",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"vkbeautify": "^0.99.3"
},
"devDependencies": {
"@rolldown/plugin-babel": "^0.2.3",
"@rolldown/plugin-babel": "^0.3.0",
"@types/babel__core": "^7.20.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^6.1.2",
"babel-plugin-react-compiler": "^1.0.0",
"typescript": "^5.8.3",
"vite": "npm:@voidzero-dev/vite-plus-core@^0.3.3",
"vite-plus": "^0.3.3"
"vite": "npm:@voidzero-dev/vite-plus-core@^1.4.0",
"vite-plus": "^1.4.0"
}
}
+2 -2
View File
@@ -21,7 +21,7 @@ hex = { workspace = true }
include_dir = "0.7"
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
log = { workspace = true }
rand = "0.8"
rand = "0.10"
reqwest = { workspace = true, features = ["form"] }
rolldown = "0.1.0"
# Pinned exactly: rolldown_resolver takes `oxc_resolver = "^11"`, so cargo
@@ -42,7 +42,7 @@ tokio = { workspace = true, features = [
] }
walkdir = "2"
webbrowser = "1"
zip = "7"
zip = "9.0.5"
yaak = { workspace = true }
yaak-api = { workspace = true }
yaak-common = { workspace = true }
+3 -3
View File
@@ -3,8 +3,8 @@ use crate::ui;
use crate::utils::http;
use base64::Engine as _;
use keyring::Entry;
use rand::RngCore;
use rand::rngs::OsRng;
use rand::TryRng;
use rand::rngs::SysRng;
use reqwest::Url;
use serde_json::Value;
use sha2::{Digest, Sha256};
@@ -443,7 +443,7 @@ fn delete_auth_token(environment: Environment) -> CommandResult {
fn random_hex(bytes: usize) -> String {
let mut data = vec![0_u8; bytes];
OsRng.fill_bytes(&mut data);
SysRng.try_fill_bytes(&mut data).expect("OS random source failed");
hex::encode(data)
}
+2 -2
View File
@@ -1,7 +1,7 @@
use crate::cli::McpArgs;
use crate::context::{CliContext, CliExecutionContext};
use crate::plugin_events::SendContexts;
use rand::Rng;
use rand::RngExt;
use std::net::{Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use std::sync::Arc;
@@ -76,7 +76,7 @@ async fn serve_http(host: CliMcpHost, port: u16, token: Option<String>) -> Resul
}
fn generate_token() -> String {
hex::encode(rand::thread_rng().r#gen::<[u8; 32]>())
hex::encode(rand::rng().random::<[u8; 32]>())
}
/// Every workspace in the database is "open", and the global `--environment`
+5 -5
View File
@@ -3,7 +3,7 @@ use crate::context::CliContext;
use crate::ui;
use crate::utils::http;
use keyring::Entry;
use rand::Rng;
use rand::RngExt;
use rolldown::{
BundleEvent, Bundler, BundlerOptions, ExperimentalOptions, InputItem, LogLevel, OutputFormat,
Platform, WatchOption, Watcher, WatcherEvent,
@@ -724,9 +724,9 @@ fn random_name() -> String {
"yak", "yarn", "year", "yell", "yoke", "yoga", "yam", "yacht", "yodel",
];
let mut rng = rand::thread_rng();
let adjective = ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
let noun = NOUNS[rng.gen_range(0..NOUNS.len())];
let mut rng = rand::rng();
let adjective = ADJECTIVES[rng.random_range(0..ADJECTIVES.len())];
let noun = NOUNS[rng.random_range(0..NOUNS.len())];
format!("{adjective}-{noun}")
}
@@ -850,7 +850,7 @@ mod tests {
let mut names = HashSet::new();
for i in 0..zip.len() {
let file = zip.by_index(i).expect("zip entry");
names.insert(file.name().to_string());
names.insert(file.name().expect("zip entry name").to_string());
}
assert!(names.contains("README.md"));
+1 -1
View File
@@ -41,7 +41,7 @@ pretty_graphql = "0.2"
r2d2 = "0.8.10"
r2d2_sqlite = "0.32"
mime_guess = "2.0.5"
rand = "0.9.0"
rand = "0.10.2"
reqwest = { workspace = true, features = [
"multipart",
"gzip",
+1 -1
View File
@@ -15,7 +15,7 @@ tauri-plugin = { workspace = true, features = ["build"] }
cocoa = "0.27.0"
log = { workspace = true }
objc = "0.2.7"
rand = "0.9.0"
rand = "0.10.2"
csscolorparser = "0.9.0"
[dependencies]
+1 -1
View File
@@ -147,7 +147,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: &Window<R>) {
use cocoa::delegate;
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
use rand::Rng;
use rand::RngExt;
use rand::distr::Alphanumeric;
use std::ffi::c_void;
+1 -1
View File
@@ -7,7 +7,7 @@ publish = false
[dependencies]
log = { workspace = true }
md5 = "0.8.0"
rand = "0.9.0"
rand = "0.10.2"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tauri = { workspace = true }
+2 -2
View File
@@ -8,7 +8,7 @@ publish = false
chrono = { version = "0.4.38", features = ["serde"] }
include_dir = "0.7"
log = { workspace = true }
nanoid = "0.4.0"
nanoid = "0.5.0"
rusqlite = { version = "0.38", features = ["bundled", "chrono"] }
sea-query-rusqlite = { version = "0.8.0", features = ["with-chrono"] }
sea-query = { version = "1.0", features = ["with-chrono", "attr"] }
@@ -24,5 +24,5 @@ r2d2_sqlite = { version = "0.32" }
# nanoid pulls getrandom, which needs to be told how to reach the browser's
# CSPRNG on wasm32-unknown-unknown. Native targets are unaffected.
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
getrandom = { version = "0.4.3", features = ["wasm_js"] }
uuid = { version = "1", features = ["js"] }
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2024"
publish = false
[dependencies]
fancy-regex = "0.17"
fancy-regex = "0.19"
indexmap = "2"
sxd-document = "0.3.2"
sxd-xpath = "0.4.2"
+1 -1
View File
@@ -46,7 +46,7 @@ fn regex(pattern: &str) -> Regex {
fn replace(
re: &Regex,
text: &str,
rep: impl FnMut(&Captures<'_>) -> String,
rep: impl FnMut(&Captures<'_, str>) -> String,
) -> Result<String, String> {
re.try_replacen(text, 0, rep)
.map(|s| s.into_owned())
+1 -1
View File
@@ -158,7 +158,7 @@ impl JsRegex {
}
}
fn capture_values<'a>(captures: &Captures<'_>, from: usize) -> Vec<JsValue<'a>> {
fn capture_values<'a>(captures: &Captures<'_, str>, from: usize) -> Vec<JsValue<'a>> {
(from..captures.len())
.map(|i| captures.get(i).map_or(JsValue::Undefined, |m| JsValue::string(m.as_str())))
.collect()
+1 -1
View File
@@ -12,7 +12,7 @@ dunce = "1.0.4"
hyper-rustls = { version = "0.27.7", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.13", default-features = false, features = ["client-legacy"] }
log = { workspace = true }
md5 = "0.7.0"
md5 = "0.8.0"
prost = "0.14.4"
prost-reflect = { version = "0.16.5", default-features = false, features = ["serde", "derive"] }
prost-types = "0.14.4"
+1 -1
View File
@@ -10,7 +10,7 @@ yaak-database = { workspace = true }
hex = { workspace = true }
include_dir = "0.7"
log = { workspace = true }
nanoid = "0.4.0"
nanoid = "0.5.0"
rusqlite = { version = "0.38", features = ["bundled", "chrono"] }
sea-query = { version = "1.0", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.8.0", features = ["with-chrono"] }
+3 -3
View File
@@ -12,9 +12,9 @@ futures-util = "0.3.30"
hex = { workspace = true }
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
log = { workspace = true }
md5 = "0.7.0"
md5 = "0.8.0"
path-slash = "0.2.1"
rand = "0.9.0"
rand = "0.10.2"
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -27,4 +27,4 @@ yaak-common = { workspace = true }
yaak-crypto = { workspace = true }
yaak-models = { workspace = true }
yaak-templates = { workspace = true }
zip = "7"
zip = "9.0.5"
+1 -1
View File
@@ -1,4 +1,4 @@
use rand::Rng;
use rand::RngExt;
use rand::distr::Alphanumeric;
pub fn gen_id() -> String {
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"jsonpath-plus": "^11.2.0"
"jsonpath-plus": "^11.2.2"
},
"main": "index.ts",
"scripts": {
+1
View File
@@ -17,6 +17,7 @@ execSync("wasm-pack build --target bundler", {
stdio: "inherit",
env: {
...process.env,
CARGO_BUILD_TARGET: "wasm32-unknown-unknown",
RUSTFLAGS: `--remap-path-prefix=${cargoHome}=/cargo --remap-path-prefix=${sysroot}=/rustc`,
},
});
+1
View File
@@ -55,6 +55,7 @@ execSync("wasm-pack build --target bundler", {
cwd: __dirname,
env: {
...process.env,
CARGO_BUILD_TARGET: "wasm32-unknown-unknown",
CC_wasm32_unknown_unknown: clang,
AR_wasm32_unknown_unknown: fs.existsSync(ar)
? ar
+1291 -1277
View File
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -123,30 +123,30 @@
"@codemirror/legacy-modes": "^6.5.5"
},
"devDependencies": {
"@rolldown/plugin-babel": "^0.2.3",
"@rolldown/plugin-babel": "^0.3.0",
"@tailwindcss/postcss": "^4.3.2",
"@tauri-apps/cli": "npm:@tauri-apps/cli-cef@3.0.0-alpha.6",
"@types/babel__core": "^7.20.5",
"@vitejs/plugin-react": "^6.1.2",
"@yaakapp/cli": "2026.8.1",
"@yaakapp/cli": "2026.9.0",
"babel-plugin-react-compiler": "^1.0.0",
"dotenv-cli": "^11.0.0",
"dotenv-cli": "^11.1.0",
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.5.29",
"postcss": "^8.5.32",
"tailwindcss": "^4.3.2",
"tar": "^7.5.22",
"typescript": "^5.8.3",
"vite": "npm:@voidzero-dev/vite-plus-core@^0.3.3",
"vite-plus": "^0.3.3",
"vitest": "^4.1.10",
"vite": "npm:@voidzero-dev/vite-plus-core@^1.4.0",
"vite-plus": "^1.4.0",
"vitest": "^4.1.12",
"yauzl": "^3.4.0"
},
"overrides": {
"@vitest/mocker": "^4.1.11",
"@vitest/mocker": "^4.1.12",
"js-yaml": "^4.3.1",
"underscore": "^1.13.8",
"vite": "npm:@voidzero-dev/vite-plus-core@^0.3.3"
"vite": "npm:@voidzero-dev/vite-plus-core@^1.4.0"
},
"packageManager": "npm@11.11.1"
}
+5 -5
View File
@@ -13,11 +13,11 @@
"@yaakapp-internal/rpc-schema": "^1.0.0",
"@yaakapp-internal/web": "^1.0.0",
"@yaakapp-internal/wasm": "^1.0.0",
"@tauri-apps/api": "^2.12.3",
"@tauri-apps/plugin-clipboard-manager": "^2.4.2",
"@tauri-apps/plugin-dialog": "^2.8.2",
"@tauri-apps/plugin-fs": "^2.7.0",
"@tauri-apps/plugin-opener": "^2.7.1",
"@tauri-apps/api": "^2.12.4",
"@tauri-apps/plugin-clipboard-manager": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.8.4",
"@tauri-apps/plugin-fs": "^2.7.1",
"@tauri-apps/plugin-opener": "^2.7.2",
"@tauri-apps/plugin-os": "^2.4.0"
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
"main": "src/index.ts",
"types": "src/index.ts",
"dependencies": {
"@tauri-apps/api": "^2.12.3",
"@tauri-apps/api": "^2.12.4",
"@yaakapp-internal/plugins": "^1.0.0",
"parse-color": "^1.0.0"
}
+5 -5
View File
@@ -15,11 +15,11 @@
"dev": "yaakcli dev"
},
"dependencies": {
"@hono/mcp": "^0.3.2",
"@hono/node-server": "^2.1.4",
"@modelcontextprotocol/sdk": "^1.32.1",
"hono": "^4.13.13",
"zod": "^4.6.6"
"@hono/mcp": "^0.3.5",
"@hono/node-server": "^2.1.5",
"@modelcontextprotocol/sdk": "^1.32.2",
"hono": "^4.13.14",
"zod": "^4.6.7"
},
"devDependencies": {
"@types/node": "^22.20.5",
+1 -1
View File
@@ -10,7 +10,7 @@
"test": "vp test --run tests"
},
"dependencies": {
"@usebruno/filestore": "^0.13.0",
"@usebruno/filestore": "^0.14.0",
"yaml": "^2.8.3"
}
}
+1 -1
View File
@@ -12,7 +12,7 @@
"test": "vp test --run tests"
},
"dependencies": {
"jsonpath-plus": "^11.2.0"
"jsonpath-plus": "^11.2.2"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
+1 -1
View File
@@ -13,6 +13,6 @@
},
"dependencies": {
"@xmldom/xmldom": "^0.9.12",
"xpath": "^0.0.35"
"xpath": "^0.0.36"
}
}