diff --git a/.claude/commands/release/generate-release-notes.md b/.claude/commands/release/generate-release-notes.md index 6a42ea32..97fba5bc 100644 --- a/.claude/commands/release/generate-release-notes.md +++ b/.claude/commands/release/generate-release-notes.md @@ -37,6 +37,7 @@ The skill generates markdown-formatted release notes following this structure: **IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last **IMPORTANT**: PRs by `@gschier` should not mention the @username +**IMPORTANT**: These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process. ## After Generating Release Notes diff --git a/.codex/skills/release-generate-release-notes/SKILL.md b/.codex/skills/release-generate-release-notes/SKILL.md index cc42395c..965c4434 100644 --- a/.codex/skills/release-generate-release-notes/SKILL.md +++ b/.codex/skills/release-generate-release-notes/SKILL.md @@ -32,6 +32,7 @@ Generate formatted markdown release notes for a Yaak tag. - Keep a blank line before and after the code fence. - Output the markdown code block last. - Do not append `by @gschier` for PRs authored by `@gschier`. +- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process. ## Release Creation Prompt diff --git a/.github/workflows/release-app.yml b/.github/workflows/release-app.yml index 8b4d36e9..f7a51f54 100644 --- a/.github/workflows/release-app.yml +++ b/.github/workflows/release-app.yml @@ -95,7 +95,7 @@ jobs: - name: Run JS Tests run: npm test - name: Run Rust Tests - run: cargo test --all + run: cargo test --all --exclude yaak-cli - name: Set version run: npm run replace-version diff --git a/Cargo.lock b/Cargo.lock index d571d9d6..a6dc3993 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,17 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "apollo-parser" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947e21ff51879f8a40d7519dfe619268de2afba4042a8a43878276de3cb910f0" +dependencies = [ + "memchr", + "rowan", + "thiserror 2.0.17", +] + [[package]] name = "append-only-vec" version = "0.1.8" @@ -1369,6 +1380,12 @@ dependencies = [ "libc", ] +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "cow-utils" version = "0.1.3" @@ -4584,7 +4601,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850" dependencies = [ "flate2", "postcard", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 2.0.17", @@ -4627,7 +4644,7 @@ dependencies = [ "hashbrown 0.16.1", "oxc_data_structures", "oxc_estree", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -4683,7 +4700,7 @@ dependencies = [ "oxc_index", "oxc_syntax", "petgraph 0.8.3", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4704,7 +4721,7 @@ dependencies = [ "oxc_sourcemap", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4716,7 +4733,7 @@ dependencies = [ "cow-utils", "oxc-browserslist", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -4791,7 +4808,7 @@ dependencies = [ "oxc_ecmascript", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4808,7 +4825,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4833,7 +4850,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4855,7 +4872,7 @@ dependencies = [ "oxc_regular_expression", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", "seq-macro", ] @@ -4871,7 +4888,7 @@ dependencies = [ "oxc_diagnostics", "oxc_span", "phf 0.13.1", - "rustc-hash", + "rustc-hash 2.1.1", "unicode-id-start", ] @@ -4888,7 +4905,7 @@ dependencies = [ "once_cell", "papaya", "pnp", - "rustc-hash", + "rustc-hash 2.1.1", "self_cell", "serde", "serde_json", @@ -4918,7 +4935,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "phf 0.13.1", - "rustc-hash", + "rustc-hash 2.1.1", "self_cell", ] @@ -4930,7 +4947,7 @@ checksum = "c7f89482522f3cd820817d48ee4ade5b10822060d6e5e4d419f05f6d8bd29d70" dependencies = [ "base64-simd", "json-escape-simd", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", ] @@ -4994,7 +5011,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "sha1", @@ -5019,7 +5036,7 @@ dependencies = [ "oxc_syntax", "oxc_transformer", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5037,7 +5054,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5452,7 +5469,7 @@ dependencies = [ "nodejs-built-in-modules", "pathdiff", "radix_trie", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 2.0.17", @@ -5571,6 +5588,18 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_graphql" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8c38ecedb3d28a998ea783469a78587f5f984d61226cf071f6979861e9e6a9" +dependencies = [ + "apollo-parser", + "memchr", + "rowan", + "tiny_pretty", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -5751,7 +5780,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "socket2 0.5.10", "thiserror 2.0.17", @@ -5771,7 +5800,7 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", @@ -6278,7 +6307,7 @@ dependencies = [ "rolldown_tracing", "rolldown_utils", "rolldown_watcher", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "string_wizard", @@ -6368,7 +6397,7 @@ dependencies = [ "rolldown_sourcemap", "rolldown_std_utils", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "simdutf8", @@ -6386,7 +6415,7 @@ dependencies = [ "blake3", "dashmap", "rolldown_debug_action", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "tracing", @@ -6443,7 +6472,7 @@ dependencies = [ "rolldown-ariadne", "rolldown_utils", "ropey", - "rustc-hash", + "rustc-hash 2.1.1", "sugar_path", ] @@ -6477,7 +6506,7 @@ dependencies = [ "rolldown_resolver", "rolldown_sourcemap", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "string_wizard", @@ -6497,7 +6526,7 @@ dependencies = [ "rolldown_common", "rolldown_plugin", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde_json", "xxhash-rust", ] @@ -6568,7 +6597,7 @@ dependencies = [ "oxc", "oxc_sourcemap", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -6620,7 +6649,7 @@ dependencies = [ "regex 1.11.1", "regress", "rolldown_std_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde_json", "simdutf8", "sugar_path", @@ -6650,6 +6679,18 @@ dependencies = [ "str_indices", ] +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -6692,6 +6733,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -7507,7 +7554,7 @@ dependencies = [ "memchr", "oxc_index", "oxc_sourcemap", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -8208,6 +8255,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "textwrap" version = "0.16.2" @@ -8330,6 +8383,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tiny_pretty" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650d82e943da333637be9f1567d33d605e76810a26464edfd7ae74f7ef181e95" + [[package]] name = "tinystr" version = "0.8.1" @@ -10252,6 +10311,7 @@ dependencies = [ "md5 0.8.0", "mime_guess", "openssl-sys", + "pretty_graphql", "r2d2", "r2d2_sqlite", "rand 0.9.1", @@ -10488,6 +10548,7 @@ dependencies = [ "urlencoding", "yaak-common", "yaak-models", + "yaak-templates", "yaak-tls", "zstd", ] diff --git a/apps/yaak-client/components/GrpcEditor.tsx b/apps/yaak-client/components/GrpcEditor.tsx index 4a98582a..6727e8f6 100644 --- a/apps/yaak-client/components/GrpcEditor.tsx +++ b/apps/yaak-client/components/GrpcEditor.tsx @@ -1,4 +1,4 @@ -import { jsonLanguage } from '@codemirror/lang-json'; +import { jsoncLanguage } from '@shopify/lang-jsonc'; import { linter } from '@codemirror/lint'; import type { EditorView } from '@codemirror/view'; import type { GrpcRequest } from '@yaakapp-internal/models'; @@ -115,7 +115,7 @@ export function GrpcEditor({ delay: 200, needsRefresh: handleRefresh, }), - jsonLanguage.data.of({ + jsoncLanguage.data.of({ autocomplete: jsonCompletion(), }), stateExtensions({}), diff --git a/apps/yaak-client/components/HttpRequestPane.tsx b/apps/yaak-client/components/HttpRequestPane.tsx index 70300900..75f857b3 100644 --- a/apps/yaak-client/components/HttpRequestPane.tsx +++ b/apps/yaak-client/components/HttpRequestPane.tsx @@ -48,6 +48,7 @@ import { FormMultipartEditor } from './FormMultipartEditor'; import { FormUrlencodedEditor } from './FormUrlencodedEditor'; import { HeadersEditor } from './HeadersEditor'; import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; +import { JsonBodyEditor } from './JsonBodyEditor'; import { MarkdownEditor } from './MarkdownEditor'; import { RequestMethodDropdown } from './RequestMethodDropdown'; import { UrlBar } from './UrlBar'; @@ -257,7 +258,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: ); const handleBodyTextChange = useCallback( - (text: string) => patchModel(activeRequest, { body: { text } }), + (text: string) => patchModel(activeRequest, { body: { ...activeRequest.body, text } }), [activeRequest], ); @@ -370,16 +371,10 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }: {activeRequest.bodyType === BODY_TYPE_JSON ? ( - ) : activeRequest.bodyType === BODY_TYPE_XML ? ( patchModel(request, { body: { ...request.body, text } }), + [request], + ); + + const autoFix = request.body?.sendJsonComments !== true; + + const lintExtension = useMemo( + () => + linter( + jsonParseLinter( + autoFix + ? { allowComments: true, allowTrailingCommas: true } + : { allowComments: false, allowTrailingCommas: false }, + ), + ), + [autoFix], + ); + + const hasComments = useMemo( + () => textLikelyContainsJsonComments(request.body?.text ?? ''), + [request.body?.text], + ); + + const { value: bannerDismissed, set: setBannerDismissed } = useKeyValue({ + namespace: 'no_sync', + key: ['json-fix-3', request.workspaceId], + fallback: false, + }); + + const handleToggleAutoFix = useCallback(() => { + const newBody = { ...request.body }; + if (autoFix) { + newBody.sendJsonComments = true; + } else { + delete newBody.sendJsonComments; + } + patchModel(request, { body: newBody }); + }, [request, autoFix]); + + const handleDropdownOpen = useCallback(() => { + if (!bannerDismissed) { + setBannerDismissed(true); + } + }, [bannerDismissed, setBannerDismissed]); + + const showBanner = hasComments && autoFix && !bannerDismissed; + + const stripMessage = 'Automatically strip comments and trailing commas before sending'; + const actions = useMemo( + () => [ + showBanner && ( + +

+ Auto-fix enabled + +

+
+ ), +
+ , + leftSlot: ( + + ), + }, + ] satisfies DropdownItem[] + } + > + + +
, + ], + [handleDropdownOpen, handleToggleAutoFix, autoFix, showBanner], + ); + + return ( + + ); +} diff --git a/apps/yaak-client/components/Settings/SettingsLicense.tsx b/apps/yaak-client/components/Settings/SettingsLicense.tsx index dd060384..e88bc33e 100644 --- a/apps/yaak-client/components/Settings/SettingsLicense.tsx +++ b/apps/yaak-client/components/Settings/SettingsLicense.tsx @@ -13,7 +13,6 @@ import { Link } from '../core/Link'; import { PlainInput } from '../core/PlainInput'; import { Separator } from '../core/Separator'; import { HStack, VStack } from '../core/Stacks'; -import { LocalImage } from '../LocalImage'; export function SettingsLicense() { return ( diff --git a/apps/yaak-client/components/core/Dialog.tsx b/apps/yaak-client/components/core/Dialog.tsx index 5e837eb7..455e0358 100644 --- a/apps/yaak-client/components/core/Dialog.tsx +++ b/apps/yaak-client/components/core/Dialog.tsx @@ -5,7 +5,7 @@ import { useMemo } from 'react'; import { Overlay } from '../Overlay'; import { Heading } from './Heading'; import { IconButton } from './IconButton'; -import { DialogSize } from '@yaakapp-internal/plugins'; +import type { DialogSize } from '@yaakapp-internal/plugins'; export interface DialogProps { children: ReactNode; diff --git a/apps/yaak-client/components/core/Editor/Editor.tsx b/apps/yaak-client/components/core/Editor/Editor.tsx index 829211fe..01ff2d21 100644 --- a/apps/yaak-client/components/core/Editor/Editor.tsx +++ b/apps/yaak-client/components/core/Editor/Editor.tsx @@ -78,6 +78,7 @@ export interface EditorProps { hideGutter?: boolean; id?: string; language?: EditorLanguage | 'pairs' | 'url' | 'timeline' | null; + lintExtension?: Extension; graphQLSchema?: GraphQLSchema | null; onBlur?: () => void; onChange?: (value: string) => void; @@ -124,6 +125,7 @@ function EditorInner({ hideGutter, graphQLSchema, language, + lintExtension, onBlur, onChange, onFocus, @@ -325,13 +327,14 @@ function EditorInner({ ); // Update the language extension when the language changes - // biome-ignore lint/correctness/useExhaustiveDependencies: none + // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally limited deps useEffect(() => { if (cm.current === null) return; const { view, languageCompartment } = cm.current; const ext = getLanguageExtension({ useTemplating, language, + lintExtension, hideGutter, environmentVariables, autocomplete, @@ -344,6 +347,7 @@ function EditorInner({ view.dispatch({ effects: languageCompartment.reconfigure(ext) }); }, [ language, + lintExtension, autocomplete, environmentVariables, onClickFunction, @@ -357,7 +361,7 @@ function EditorInner({ ]); // Initialize the editor when ref mounts - // biome-ignore lint/correctness/useExhaustiveDependencies: Only reinitialize when necessary + // biome-ignore lint/correctness/useExhaustiveDependencies: only reinitialize when necessary const initEditorRef = useCallback( function initEditorRef(container: HTMLDivElement | null) { if (container === null) { @@ -371,6 +375,7 @@ function EditorInner({ const langExt = getLanguageExtension({ useTemplating, language, + lintExtension, completionOptions, autocomplete, environmentVariables, diff --git a/apps/yaak-client/components/core/Editor/extensions.ts b/apps/yaak-client/components/core/Editor/extensions.ts index 1ec1eb07..8db6750d 100644 --- a/apps/yaak-client/components/core/Editor/extensions.ts +++ b/apps/yaak-client/components/core/Editor/extensions.ts @@ -8,7 +8,6 @@ import { history, historyKeymap } from '@codemirror/commands'; import { go } from '@codemirror/lang-go'; import { java } from '@codemirror/lang-java'; import { javascript } from '@codemirror/lang-javascript'; -import { json } from '@codemirror/lang-json'; import { markdown } from '@codemirror/lang-markdown'; import { php } from '@codemirror/lang-php'; import { python } from '@codemirror/lang-python'; @@ -34,7 +33,6 @@ import { ruby } from '@codemirror/legacy-modes/mode/ruby'; import { shell } from '@codemirror/legacy-modes/mode/shell'; import { swift } from '@codemirror/legacy-modes/mode/swift'; import { linter, lintGutter, lintKeymap } from '@codemirror/lint'; - import { search, searchKeymap } from '@codemirror/search'; import type { Extension } from '@codemirror/state'; import { EditorState } from '@codemirror/state'; @@ -50,6 +48,7 @@ import { rectangularSelection, } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; +import { jsonc, jsoncLanguage } from '@shopify/lang-jsonc'; import { graphql } from 'cm6-graphql'; import type { GraphQLSchema } from 'graphql'; import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId'; @@ -61,13 +60,13 @@ import { showGraphQLDocExplorerAtom } from '../../graphql/graphqlAtoms'; import type { EditorProps } from './Editor'; import { jsonParseLinter } from './json-lint'; import { pairs } from './pairs/extension'; +import { searchMatchCount } from './searchMatchCount'; import { text } from './text/extension'; import { timeline } from './timeline/extension'; import type { TwigCompletionOption } from './twig/completion'; import { twig } from './twig/extension'; import { pathParametersPlugin } from './twig/pathParameters'; import { url } from './url/extension'; -import { searchMatchCount } from './searchMatchCount'; export const syntaxHighlightStyle = HighlightStyle.define([ { @@ -107,7 +106,7 @@ const syntaxExtensions: Record< null | (() => LanguageSupport) > = { graphql: null, - json: json, + json: jsonc, javascript: javascript, // HTML as XML because HTML is oddly slow html: xml, @@ -140,6 +139,7 @@ const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript export function getLanguageExtension({ useTemplating, language = 'text', + lintExtension, environmentVariables, autocomplete, hideGutter, @@ -156,7 +156,7 @@ export function getLanguageExtension({ onClickPathParameter: (name: string) => void; completionOptions: TwigCompletionOption[]; graphQLSchema: GraphQLSchema | null; -} & Pick) { +} & Pick) { const extraExtensions: Extension[] = []; if (language === 'url') { @@ -193,7 +193,12 @@ export function getLanguageExtension({ } if (language === 'json') { - extraExtensions.push(linter(jsonParseLinter())); + extraExtensions.push(lintExtension ?? linter(jsonParseLinter())); + extraExtensions.push( + jsoncLanguage.data.of({ + commentTokens: { line: '//', block: { open: '/*', close: '*/' } }, + }), + ); if (!hideGutter) { extraExtensions.push(lintGutter()); } diff --git a/apps/yaak-client/components/core/Editor/json-lint.ts b/apps/yaak-client/components/core/Editor/json-lint.ts index f4b080dd..32652f15 100644 --- a/apps/yaak-client/components/core/Editor/json-lint.ts +++ b/apps/yaak-client/components/core/Editor/json-lint.ts @@ -4,14 +4,22 @@ import { parse as jsonLintParse } from '@prantlf/jsonlint'; const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g; -export function jsonParseLinter() { +interface JsonLintOptions { + allowComments?: boolean; + allowTrailingCommas?: boolean; +} + +export function jsonParseLinter(options?: JsonLintOptions) { return (view: EditorView): Diagnostic[] => { try { const doc = view.state.doc.toString(); // We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template // syntax with repeating `1` characters, so it's valid JSON and the position is still correct. const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => '1'.repeat(m.length)); - jsonLintParse(escapedDoc); + jsonLintParse(escapedDoc, { + mode: (options?.allowComments ?? true) ? 'cjson' : 'json', + ignoreTrailingCommas: options?.allowTrailingCommas ?? false, + }); // biome-ignore lint/suspicious/noExplicitAny: none } catch (err: any) { if (!('location' in err)) { diff --git a/apps/yaak-client/components/git/GitCommitDialog.tsx b/apps/yaak-client/components/git/GitCommitDialog.tsx index de932537..07b1f8e3 100644 --- a/apps/yaak-client/components/git/GitCommitDialog.tsx +++ b/apps/yaak-client/components/git/GitCommitDialog.tsx @@ -1,4 +1,4 @@ -import { useCachedNode } from '@dnd-kit/core/dist/hooks/utilities'; + import type { GitStatusEntry } from '@yaakapp-internal/git'; import { useGit } from '@yaakapp-internal/git'; import type { @@ -12,7 +12,6 @@ import type { import classNames from 'classnames'; import { useCallback, useMemo, useState } from 'react'; import { modelToYaml } from '../../lib/diffYaml'; -import { isSubEnvironment } from '../../lib/model_util'; import { resolvedModelName } from '../../lib/resolvedModelName'; import { showErrorToast } from '../../lib/toast'; import { Banner } from '../core/Banner'; diff --git a/apps/yaak-client/components/git/GitDropdown.tsx b/apps/yaak-client/components/git/GitDropdown.tsx index d11e3b66..803e835c 100644 --- a/apps/yaak-client/components/git/GitDropdown.tsx +++ b/apps/yaak-client/components/git/GitDropdown.tsx @@ -75,7 +75,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { const currentBranch = status.data.headRefShorthand; const hasChanges = status.data.entries.some((e) => e.status !== 'current'); - const hasRemotes = (status.data.origins ?? []).length > 0; + const _hasRemotes = (status.data.origins ?? []).length > 0; const { ahead, behind } = status.data; const tryCheckout = (branch: string, force: boolean) => { diff --git a/apps/yaak-client/components/graphql/GraphQLEditor.tsx b/apps/yaak-client/components/graphql/GraphQLEditor.tsx index 70e177c6..4f0edef7 100644 --- a/apps/yaak-client/components/graphql/GraphQLEditor.tsx +++ b/apps/yaak-client/components/graphql/GraphQLEditor.tsx @@ -1,6 +1,5 @@ import type { HttpRequest } from '@yaakapp-internal/models'; -import { formatSdl } from 'format-graphql'; import { useAtom } from 'jotai'; import { useCallback, useMemo } from 'react'; import { useLocalStorage } from 'react-use'; @@ -16,6 +15,7 @@ import { Editor } from '../core/Editor/LazyEditor'; import { FormattedError } from '../core/FormattedError'; import { Icon } from '@yaakapp-internal/ui'; import { Separator } from '../core/Separator'; +import { tryFormatGraphql } from '../../lib/formatters'; import { showGraphQLDocExplorerAtom } from './graphqlAtoms'; type Props = Pick & { @@ -156,6 +156,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp { type: 'separator', label: 'Setting' }, { label: 'Automatic Introspection', + keepOpenOnSelect: true, onSelect: () => { setAutoIntrospectDisabled({ ...autoIntrospectDisabled, @@ -210,7 +211,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp language="graphql" heightMode="auto" graphQLSchema={schema} - format={formatSdl} + format={tryFormatGraphql} defaultValue={currentBody.query} onChange={handleChangeQuery} placeholder="..." diff --git a/apps/yaak-client/lib/formatters.ts b/apps/yaak-client/lib/formatters.ts index f0d9bf37..0ac6381f 100644 --- a/apps/yaak-client/lib/formatters.ts +++ b/apps/yaak-client/lib/formatters.ts @@ -20,6 +20,18 @@ export async function tryFormatJson(text: string): Promise { return text; } +export async function tryFormatGraphql(text: string): Promise { + if (text === '') return text; + + try { + return await invokeCmd('cmd_format_graphql', { text }); + } catch (err) { + console.warn('Failed to format GraphQL', err); + } + + return text; +} + export async function tryFormatXml(text: string): Promise { if (text === '') return text; diff --git a/apps/yaak-client/lib/jsonComments.ts b/apps/yaak-client/lib/jsonComments.ts new file mode 100644 index 00000000..3c308955 --- /dev/null +++ b/apps/yaak-client/lib/jsonComments.ts @@ -0,0 +1,30 @@ +/** + * Simple heuristic to detect if a string likely contains JSON/JSONC comments. + * Checks for // and /* patterns that are NOT inside double-quoted strings. + * Used for UI hints only — doesn't need to be perfect. + */ +export function textLikelyContainsJsonComments(text: string): boolean { + let inString = false; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + if (inString) { + if (ch === '"') { + inString = false; + } else if (ch === '\\') { + i++; // skip escaped char + } + continue; + } + if (ch === '"') { + inString = true; + continue; + } + if (ch === '/' && i + 1 < text.length) { + const next = text[i + 1]; + if (next === '/' || next === '*') { + return true; + } + } + } + return false; +} diff --git a/apps/yaak-client/lib/tauri.ts b/apps/yaak-client/lib/tauri.ts index 470ebc32..df37d0d1 100644 --- a/apps/yaak-client/lib/tauri.ts +++ b/apps/yaak-client/lib/tauri.ts @@ -17,6 +17,7 @@ type TauriCmd = | 'cmd_delete_send_history' | 'cmd_dismiss_notification' | 'cmd_export_data' + | 'cmd_format_graphql' | 'cmd_format_json' | 'cmd_get_http_authentication_config' | 'cmd_get_http_authentication_summaries' diff --git a/apps/yaak-client/modules.d.ts b/apps/yaak-client/modules.d.ts index 29ce54f6..2dcdc452 100644 --- a/apps/yaak-client/modules.d.ts +++ b/apps/yaak-client/modules.d.ts @@ -1,2 +1 @@ -declare module 'format-graphql'; declare module 'vkbeautify'; diff --git a/apps/yaak-client/package.json b/apps/yaak-client/package.json index ad088725..4eab0384 100644 --- a/apps/yaak-client/package.json +++ b/apps/yaak-client/package.json @@ -27,6 +27,7 @@ "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", + "@shopify/lang-jsonc": "^1.0.1", "@tanstack/react-query": "^5.90.5", "@tanstack/react-router": "^1.133.13", "@tanstack/react-virtual": "^3.13.12", @@ -46,7 +47,6 @@ "deep-equal": "^2.2.3", "eventemitter3": "^5.0.1", "focus-trap-react": "^11.0.4", - "format-graphql": "^1.5.0", "fuzzbunny": "^1.0.1", "hexy": "^0.3.5", "history": "^5.3.0", diff --git a/biome.json b/biome.json index df48641a..fbda2b07 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", "linter": { "enabled": true, "rules": { @@ -48,7 +48,8 @@ "!apps/yaak-client/routeTree.gen.ts", "!packages/plugin-runtime-types/lib", "!**/bindings", - "!flatpak" + "!flatpak", + "!npm" ] } } diff --git a/crates-cli/yaak-cli/src/main.rs b/crates-cli/yaak-cli/src/main.rs index ba439c1f..b5a2ad30 100644 --- a/crates-cli/yaak-cli/src/main.rs +++ b/crates-cli/yaak-cli/src/main.rs @@ -10,6 +10,7 @@ mod version_check; use clap::Parser; use cli::{Cli, Commands, PluginCommands, RequestCommands}; use context::{CliContext, CliExecutionContext}; +use std::path::PathBuf; use yaak_models::queries::any_request::AnyRequest; #[tokio::main] @@ -30,9 +31,7 @@ async fn main() { let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" }; - let data_dir = data_dir.unwrap_or_else(|| { - dirs::data_dir().expect("Could not determine data directory").join(app_id) - }); + let data_dir = data_dir.unwrap_or_else(|| resolve_data_dir(app_id)); version_check::maybe_check_for_updates().await; @@ -239,3 +238,46 @@ fn resolve_cookie_jar_id( .map(|jar| jar.id); Ok(default_cookie_jar) } + +fn resolve_data_dir(app_id: &str) -> PathBuf { + if let Some(dir) = wsl_data_dir(app_id) { + return dir; + } + dirs::data_dir().expect("Could not determine data directory").join(app_id) +} + +/// Detect WSL and resolve the Windows AppData\Roaming path for the Yaak data directory. +fn wsl_data_dir(app_id: &str) -> Option { + if !cfg!(target_os = "linux") { + return None; + } + + let proc_version = std::fs::read_to_string("/proc/version").ok()?; + let is_wsl = proc_version.to_lowercase().contains("microsoft"); + if !is_wsl { + return None; + } + + // We're in WSL, so try to resolve the Yaak app's data directory in Windows + + // Get the Windows %APPDATA% path via cmd.exe + let appdata_output = + std::process::Command::new("cmd.exe").args(["/C", "echo", "%APPDATA%"]).output().ok()?; + + let win_path = String::from_utf8(appdata_output.stdout).ok()?.trim().to_string(); + if win_path.is_empty() || win_path == "%APPDATA%" { + return None; + } + + // Convert Windows path to WSL path using wslpath (handles custom mount points) + let wslpath_output = std::process::Command::new("wslpath").arg(&win_path).output().ok()?; + + let wsl_appdata = String::from_utf8(wslpath_output.stdout).ok()?.trim().to_string(); + if wsl_appdata.is_empty() { + return None; + } + + let wsl_path = PathBuf::from(wsl_appdata).join(app_id); + + if wsl_path.exists() { Some(wsl_path) } else { None } +} diff --git a/crates-cli/yaak-cli/src/plugin_events.rs b/crates-cli/yaak-cli/src/plugin_events.rs index 1e7f9f36..cadc42dc 100644 --- a/crates-cli/yaak-cli/src/plugin_events.rs +++ b/crates-cli/yaak-cli/src/plugin_events.rs @@ -3,7 +3,7 @@ use arboard::Clipboard; use console::Term; use inquire::{Confirm, Editor, Password, PasswordDisplayMode, Select, Text}; use serde_json::Value; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::io::IsTerminal; use std::path::PathBuf; use std::sync::Arc; @@ -11,11 +11,11 @@ use tokio::task::JoinHandle; use yaak::plugin_events::{ GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event, }; -use yaak::render::render_http_request; +use yaak::render::{render_grpc_request, render_http_request}; use yaak::send::{SendHttpRequestWithPluginsParams, send_http_request_with_plugins}; use yaak_crypto::manager::EncryptionManager; use yaak_models::blob_manager::BlobManager; -use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader}; +use yaak_models::models::Environment; use yaak_models::queries::any_request::AnyRequest; use yaak_models::query_manager::QueryManager; use yaak_models::render::make_vars_hashmap; @@ -29,7 +29,7 @@ use yaak_plugins::events::{ }; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; -use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; +use yaak_templates::{RenderOptions, TemplateCallback, render_json_value_raw}; pub struct CliPluginEventBridge { rx_id: String, @@ -269,7 +269,7 @@ async fn build_plugin_reply( ); let render_options = RenderOptions::throw(); - match render_grpc_request_for_cli( + match render_grpc_request( &grpc_request, environment_chain, &template_callback, @@ -532,60 +532,6 @@ async fn render_json_value_for_cli( render_json_value_raw(value, vars, cb, opt).await } -async fn render_grpc_request_for_cli( - grpc_request: &GrpcRequest, - environment_chain: Vec, - cb: &T, - opt: &RenderOptions, -) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(environment_chain); - - let mut metadata = Vec::new(); - for p in grpc_request.metadata.clone() { - if !p.enabled { - continue; - } - metadata.push(HttpRequestHeader { - enabled: p.enabled, - name: parse_and_render(p.name.as_str(), vars, cb, opt).await?, - value: parse_and_render(p.value.as_str(), vars, cb, opt).await?, - id: p.id, - }) - } - - let authentication = { - let mut disabled = false; - let mut auth = BTreeMap::new(); - match grpc_request.authentication.get("disabled") { - Some(Value::Bool(true)) => { - disabled = true; - } - Some(Value::String(tmpl)) => { - disabled = parse_and_render(tmpl.as_str(), vars, cb, opt) - .await - .unwrap_or_default() - .is_empty(); - } - _ => {} - } - if disabled { - auth.insert("disabled".to_string(), Value::Bool(true)); - } else { - for (k, v) in grpc_request.authentication.clone() { - if k == "disabled" { - auth.insert(k, Value::Bool(false)); - } else { - auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?); - } - } - } - auth - }; - - let url = parse_and_render(grpc_request.url.as_str(), vars, cb, opt).await?; - - Ok(GrpcRequest { url, metadata, authentication, ..grpc_request.to_owned() }) -} fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> { let first_part = raw_cookie.split(';').next()?.trim(); diff --git a/crates-tauri/yaak-app-client/Cargo.toml b/crates-tauri/yaak-app-client/Cargo.toml index f79a1082..cc00735c 100644 --- a/crates-tauri/yaak-app-client/Cargo.toml +++ b/crates-tauri/yaak-app-client/Cargo.toml @@ -30,6 +30,7 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client http = { version = "1.2.0", default-features = false } log = { workspace = true } md5 = "0.8.0" +pretty_graphql = "0.2" r2d2 = "0.8.10" r2d2_sqlite = "0.25.0" mime_guess = "2.0.5" diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index 225fba29..e4788582 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -34,6 +34,7 @@ use tokio::time; use yaak_common::command::new_checked_command; use yaak_crypto::manager::EncryptionManager; use yaak_grpc::manager::{GrpcConfig, GrpcHandle}; +use yaak_templates::strip_json_comments::strip_json_comments; use yaak_grpc::{Code, ServiceDefinition, serialize_message}; use yaak_mac_window::AppHandleMacWindowExt; use yaak_models::models::{ @@ -502,6 +503,7 @@ async fn cmd_grpc_go( result.expect("Failed to render template") }) }); + let msg = strip_json_comments(&msg); in_msg_tx.try_send(msg.clone()).unwrap(); } Ok(IncomingMsg::Commit) => { @@ -537,6 +539,7 @@ async fn cmd_grpc_go( &RenderOptions { error_behavior: RenderErrorBehavior::Throw }, ) .await?; + let msg = strip_json_comments(&msg); app_handle.db().upsert_grpc_event( &GrpcEvent { @@ -938,6 +941,14 @@ async fn cmd_format_json(text: &str) -> YaakResult { Ok(format_json(text, " ")) } +#[tauri::command] +async fn cmd_format_graphql(text: &str) -> YaakResult { + match pretty_graphql::format_text(text, &Default::default()) { + Ok(formatted) => Ok(formatted), + Err(_) => Ok(text.to_string()), + } +} + #[tauri::command] async fn cmd_http_response_body( window: WebviewWindow, @@ -1718,6 +1729,7 @@ pub fn run() { cmd_http_request_body, cmd_http_response_body, cmd_format_json, + cmd_format_graphql, cmd_get_http_authentication_summaries, cmd_get_http_authentication_config, cmd_get_sse_events, diff --git a/crates-tauri/yaak-app-client/src/render.rs b/crates-tauri/yaak-app-client/src/render.rs index 8cb3fd00..a6abe0d3 100644 --- a/crates-tauri/yaak-app-client/src/render.rs +++ b/crates-tauri/yaak-app-client/src/render.rs @@ -1,8 +1,6 @@ -use log::info; use serde_json::Value; -use std::collections::BTreeMap; -pub use yaak::render::render_http_request; -use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader}; +pub use yaak::render::{render_grpc_request, render_http_request}; +use yaak_models::models::Environment; use yaak_models::render::make_vars_hashmap; use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; @@ -25,61 +23,3 @@ pub async fn render_json_value( let vars = &make_vars_hashmap(environment_chain); render_json_value_raw(value, vars, cb, opt).await } - -pub async fn render_grpc_request( - r: &GrpcRequest, - environment_chain: Vec, - cb: &T, - opt: &RenderOptions, -) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(environment_chain); - - let mut metadata = Vec::new(); - for p in r.metadata.clone() { - if !p.enabled { - continue; - } - metadata.push(HttpRequestHeader { - enabled: p.enabled, - name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?, - value: parse_and_render(p.value.as_str(), vars, cb, &opt).await?, - id: p.id, - }) - } - - let authentication = { - let mut disabled = false; - let mut auth = BTreeMap::new(); - match r.authentication.get("disabled") { - Some(Value::Bool(true)) => { - disabled = true; - } - Some(Value::String(tmpl)) => { - disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt) - .await - .unwrap_or_default() - .is_empty(); - info!( - "Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\"" - ); - } - _ => {} - } - if disabled { - auth.insert("disabled".to_string(), Value::Bool(true)); - } else { - for (k, v) in r.authentication.clone() { - if k == "disabled" { - auth.insert(k, Value::Bool(false)); - } else { - auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?); - } - } - } - auth - }; - - let url = parse_and_render(r.url.as_str(), vars, cb, &opt).await?; - - Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() }) -} diff --git a/crates-tauri/yaak-app-client/src/ws_ext.rs b/crates-tauri/yaak-app-client/src/ws_ext.rs index 28580f93..96b6f438 100644 --- a/crates-tauri/yaak-app-client/src/ws_ext.rs +++ b/crates-tauri/yaak-app-client/src/ws_ext.rs @@ -24,6 +24,7 @@ use yaak_models::util::UpdateSource; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose}; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; +use yaak_templates::strip_json_comments::maybe_strip_json_comments; use yaak_templates::{RenderErrorBehavior, RenderOptions}; use yaak_tls::find_client_certificate; use yaak_ws::{WebsocketManager, render_websocket_request}; @@ -72,8 +73,10 @@ pub async fn cmd_ws_send( ) .await?; + let message = maybe_strip_json_comments(&request.message); + let mut ws_manager = ws_manager.lock().await; - ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?; + ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?; app_handle.db().upsert_websocket_event( &WebsocketEvent { @@ -82,7 +85,7 @@ pub async fn cmd_ws_send( workspace_id: connection.workspace_id.clone(), is_server: false, message_type: WebsocketEventType::Text, - message: request.message.into(), + message: message.into(), ..Default::default() }, &UpdateSource::from_window_label(window.label()), diff --git a/crates-tauri/yaak-mac-window/src/mac.rs b/crates-tauri/yaak-mac-window/src/mac.rs index 4be6ab43..c5b5cdba 100644 --- a/crates-tauri/yaak-mac-window/src/mac.rs +++ b/crates-tauri/yaak-mac-window/src/mac.rs @@ -12,6 +12,11 @@ unsafe impl Sync for UnsafeWindowHandle {} const WINDOW_CONTROL_PAD_X: f64 = 13.0; const WINDOW_CONTROL_PAD_Y: f64 = 18.0; +/// Extra pixels to add to the title bar height when the default title bar is +/// already as tall as button_height + PAD_Y (i.e. macOS Tahoe 26+, where the +/// default is 32px and 14 + 18 = 32). On pre-Tahoe this is unused because the +/// default title bar is shorter than button_height + PAD_Y. +const TITLEBAR_EXTRA_HEIGHT: f64 = 4.0; const MAIN_WINDOW_PREFIX: &str = "main_"; pub(crate) fn update_window_title(window: Window, title: String) { @@ -95,12 +100,29 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); - let title_bar_container_view = close.superview().superview(); - let close_rect: NSRect = msg_send![close, frame]; let button_height = close_rect.size.height; - let title_bar_frame_height = button_height + y; + let title_bar_container_view = close.superview().superview(); + + // Capture the OS default title bar height on the first call, before + // we've modified it. This avoids the height growing on repeated calls. + use std::sync::OnceLock; + static DEFAULT_TITLEBAR_HEIGHT: OnceLock = OnceLock::new(); + let default_height = + *DEFAULT_TITLEBAR_HEIGHT.get_or_init(|| NSView::frame(title_bar_container_view).size.height); + + // On pre-Tahoe, button_height + y is larger than the default title bar + // height, so the resize works as before. On Tahoe (26+), the default is + // already 32px and button_height + y = 32, so nothing changes. In that + // case, add TITLEBAR_EXTRA_HEIGHT extra pixels to push the buttons down. + let desired = button_height + y; + let title_bar_frame_height = if desired > default_height { + desired + } else { + default_height + TITLEBAR_EXTRA_HEIGHT + }; + let mut title_bar_rect = NSView::frame(title_bar_container_view); title_bar_rect.size.height = title_bar_frame_height; title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height; diff --git a/crates/yaak-common/src/serde.rs b/crates/yaak-common/src/serde.rs index 683cc25d..49146aaa 100644 --- a/crates/yaak-common/src/serde.rs +++ b/crates/yaak-common/src/serde.rs @@ -21,3 +21,10 @@ pub fn get_str_map<'a>(v: &'a BTreeMap, key: &str) -> &'a str { Some(v) => v.as_str().unwrap_or_default(), } } + +pub fn get_bool_map(v: &BTreeMap, key: &str, fallback: bool) -> bool { + match v.get(key) { + None => fallback, + Some(v) => v.as_bool().unwrap_or(fallback), + } +} diff --git a/crates/yaak-http/Cargo.toml b/crates/yaak-http/Cargo.toml index 20183503..34ebba4b 100644 --- a/crates/yaak-http/Cargo.toml +++ b/crates/yaak-http/Cargo.toml @@ -29,4 +29,5 @@ tower-service = "0.3.3" urlencoding = "2.1.3" yaak-common = { workspace = true } yaak-models = { workspace = true } +yaak-templates = { workspace = true } yaak-tls = { workspace = true } diff --git a/crates/yaak-http/src/types.rs b/crates/yaak-http/src/types.rs index 3ca14e15..b1e6e655 100644 --- a/crates/yaak-http/src/types.rs +++ b/crates/yaak-http/src/types.rs @@ -9,8 +9,9 @@ use std::collections::BTreeMap; use std::pin::Pin; use std::time::Duration; use tokio::io::AsyncRead; -use yaak_common::serde::{get_bool, get_str, get_str_map}; +use yaak_common::serde::{get_bool, get_bool_map, get_str, get_str_map}; use yaak_models::models::HttpRequest; +use yaak_templates::strip_json_comments::{maybe_strip_json_comments, strip_json_comments}; pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary"; @@ -134,16 +135,69 @@ pub fn append_query_params(url: &str, params: Vec<(String, String)>) -> String { result } +fn strip_query_params(url: &str, names: &[&str]) -> String { + // Split off fragment + let (base_and_query, fragment) = if let Some(hash_pos) = url.find('#') { + (&url[..hash_pos], Some(&url[hash_pos..])) + } else { + (url, None) + }; + + let result = if let Some(q_pos) = base_and_query.find('?') { + let base = &base_and_query[..q_pos]; + let query = &base_and_query[q_pos + 1..]; + let filtered: Vec<&str> = query + .split('&') + .filter(|pair| { + let key = pair.split('=').next().unwrap_or(""); + let decoded = urlencoding::decode(key).unwrap_or_default(); + !names.contains(&decoded.as_ref()) + }) + .collect(); + if filtered.is_empty() { + base.to_string() + } else { + format!("{}?{}", base, filtered.join("&")) + } + } else { + base_and_query.to_string() + }; + + match fragment { + Some(f) => format!("{}{}", result, f), + None => result, + } +} + fn build_url(r: &HttpRequest) -> String { let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters); - append_query_params( + let mut url = append_query_params( &url_string, params .iter() .filter(|p| p.enabled && !p.name.is_empty()) .map(|p| (p.name.clone(), p.value.clone())) .collect(), - ) + ); + + // GraphQL GET requests encode query/variables as URL query parameters + if r.method.to_lowercase() == "get" && r.body_type.as_deref() == Some("graphql") { + url = append_graphql_query_params(&url, &r.body); + } + + url +} + +fn append_graphql_query_params(url: &str, body: &BTreeMap) -> String { + let query = get_str_map(body, "query").to_string(); + let variables = strip_json_comments(&get_str_map(body, "variables")); + let mut params = vec![("query".to_string(), query)]; + if !variables.trim().is_empty() { + params.push(("variables".to_string(), variables)); + } + // Strip existing query/variables params to avoid duplicates + let url = strip_query_params(url, &["query", "variables"]); + append_query_params(&url, params) } fn build_headers(r: &HttpRequest) -> Vec<(String, String)> { @@ -177,7 +231,7 @@ async fn build_body( (build_form_body(&body), Some("application/x-www-form-urlencoded".to_string())) } "multipart/form-data" => build_multipart_body(&body, &headers).await?, - _ if body.contains_key("text") => (build_text_body(&body), None), + _ if body.contains_key("text") => (build_text_body(&body, body_type), None), t => { warn!("Unsupported body type: {}", t); (None, None) @@ -252,13 +306,20 @@ async fn build_binary_body( })) } -fn build_text_body(body: &BTreeMap) -> Option { +fn build_text_body(body: &BTreeMap, body_type: &str) -> Option { let text = get_str_map(body, "text"); if text.is_empty() { - None - } else { - Some(SendableBodyWithMeta::Bytes(Bytes::from(text.to_string()))) + return None; } + + let send_comments = get_bool_map(body, "sendJsonComments", false); + let text = if !send_comments && body_type == "application/json" { + maybe_strip_json_comments(text) + } else { + text.to_string() + }; + + Some(SendableBodyWithMeta::Bytes(Bytes::from(text))) } fn build_graphql_body( @@ -266,7 +327,7 @@ fn build_graphql_body( body: &BTreeMap, ) -> Option { let query = get_str_map(body, "query"); - let variables = get_str_map(body, "variables"); + let variables = strip_json_comments(&get_str_map(body, "variables")); if method.to_lowercase() == "get" { // GraphQL GET requests use query parameters, not a body @@ -684,7 +745,7 @@ mod tests { let mut body = BTreeMap::new(); body.insert("text".to_string(), json!("Hello, World!")); - let result = build_text_body(&body); + let result = build_text_body(&body, "application/json"); match result { Some(SendableBodyWithMeta::Bytes(bytes)) => { assert_eq!(bytes, Bytes::from("Hello, World!")) @@ -698,7 +759,7 @@ mod tests { let mut body = BTreeMap::new(); body.insert("text".to_string(), json!("")); - let result = build_text_body(&body); + let result = build_text_body(&body, "application/json"); assert!(result.is_none()); } @@ -706,10 +767,57 @@ mod tests { async fn test_text_body_missing() { let body = BTreeMap::new(); - let result = build_text_body(&body); + let result = build_text_body(&body, "application/json"); assert!(result.is_none()); } + #[tokio::test] + async fn test_text_body_strips_json_comments_by_default() { + let mut body = BTreeMap::new(); + body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}")); + + let result = build_text_body(&body, "application/json"); + match result { + Some(SendableBodyWithMeta::Bytes(bytes)) => { + let text = String::from_utf8_lossy(&bytes); + assert!(!text.contains("// comment")); + assert!(text.contains("\"foo\": \"bar\"")); + } + _ => panic!("Expected Some(SendableBody::Bytes)"), + } + } + + #[tokio::test] + async fn test_text_body_send_json_comments_when_opted_in() { + let mut body = BTreeMap::new(); + body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}")); + body.insert("sendJsonComments".to_string(), json!(true)); + + let result = build_text_body(&body, "application/json"); + match result { + Some(SendableBodyWithMeta::Bytes(bytes)) => { + let text = String::from_utf8_lossy(&bytes); + assert!(text.contains("// comment")); + } + _ => panic!("Expected Some(SendableBody::Bytes)"), + } + } + + #[tokio::test] + async fn test_text_body_no_strip_for_non_json() { + let mut body = BTreeMap::new(); + body.insert("text".to_string(), json!("// not json\nsome text")); + + let result = build_text_body(&body, "text/plain"); + match result { + Some(SendableBodyWithMeta::Bytes(bytes)) => { + let text = String::from_utf8_lossy(&bytes); + assert!(text.contains("// not json")); + } + _ => panic!("Expected Some(SendableBody::Bytes)"), + } + } + #[tokio::test] async fn test_form_urlencoded_body() -> Result<()> { let mut body = BTreeMap::new(); diff --git a/crates/yaak-templates/src/format_json.rs b/crates/yaak-templates/src/format_json.rs index 1a7b5cd4..98f1a90e 100644 --- a/crates/yaak-templates/src/format_json.rs +++ b/crates/yaak-templates/src/format_json.rs @@ -11,6 +11,7 @@ pub fn format_json(text: &str, tab: &str) -> String { let mut new_json = "".to_string(); let mut depth = 0; let mut state = FormatState::None; + let mut saw_newline_in_whitespace = false; loop { let rest_of_chars = chars.clone(); @@ -61,6 +62,62 @@ pub fn format_json(text: &str, tab: &str) -> String { continue; } + // Handle line comments (//) + if current_char == '/' && chars.peek() == Some(&'/') { + chars.next(); // Skip second / + // Collect the rest of the comment until newline + let mut comment = String::from("//"); + loop { + match chars.peek() { + Some(&'\n') | None => break, + Some(_) => comment.push(chars.next().unwrap()), + } + } + // Check if the comma handler already added \n + indent + let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t'); + if trimmed.ends_with(",\n") && !saw_newline_in_whitespace { + // Trailing comment on the same line as comma (e.g. "foo",// comment) + new_json.truncate(trimmed.len() - 1); + new_json.push(' '); + } else if !trimmed.ends_with('\n') && !new_json.is_empty() { + // Trailing comment after a value (no newline before us) + new_json.push(' '); + } + new_json.push_str(&comment); + new_json.push('\n'); + new_json.push_str(tab.to_string().repeat(depth).as_str()); + saw_newline_in_whitespace = false; + continue; + } + + // Handle block comments (/* ... */) + if current_char == '/' && chars.peek() == Some(&'*') { + chars.next(); // Skip * + let mut comment = String::from("/*"); + loop { + match chars.next() { + None => break, + Some('*') if chars.peek() == Some(&'/') => { + chars.next(); // Skip / + comment.push_str("*/"); + break; + } + Some(c) => comment.push(c), + } + } + // If we're not already on a fresh line, add newline + indent before comment + let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t'); + if !trimmed.is_empty() && !trimmed.ends_with('\n') { + new_json.push('\n'); + new_json.push_str(tab.to_string().repeat(depth).as_str()); + } + new_json.push_str(&comment); + // After block comment, add newline + indent for the next content + new_json.push('\n'); + new_json.push_str(tab.to_string().repeat(depth).as_str()); + continue; + } + match current_char { ',' => { new_json.push(current_char); @@ -125,20 +182,37 @@ pub fn format_json(text: &str, tab: &str) -> String { || current_char == '\t' || current_char == '\r' { + if current_char == '\n' { + saw_newline_in_whitespace = true; + } // Don't add these } else { + saw_newline_in_whitespace = false; new_json.push(current_char); } } } } - // Replace only lines containing whitespace with nothing - new_json - .lines() - .filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines - .collect::>() // Collect the non-empty lines into a vector - .join("\n") // Join the lines back into a single string + // Filter out whitespace-only lines, but preserve empty lines inside block comments + let mut result_lines: Vec<&str> = Vec::new(); + let mut in_block_comment = false; + for line in new_json.lines() { + if in_block_comment { + result_lines.push(line); + if line.contains("*/") { + in_block_comment = false; + } + } else { + if line.contains("/*") && !line.contains("*/") { + in_block_comment = true; + } + if !line.trim().is_empty() { + result_lines.push(line); + } + } + } + result_lines.iter().map(|line| line.trim_end()).collect::>().join("\n") } #[cfg(test)] @@ -297,6 +371,161 @@ mod tests { r#" {} } +"# + .trim() + ); + } + + #[test] + fn test_line_comment_between_keys() { + assert_eq!( + format_json( + r#"{"foo":"bar",// a comment +"baz":"qux"}"#, + " " + ), + r#" +{ + "foo": "bar", // a comment + "baz": "qux" +} +"# + .trim() + ); + } + + #[test] + fn test_line_comment_at_end() { + assert_eq!( + format_json( + r#"{"foo":"bar" // trailing +}"#, + " " + ), + r#" +{ + "foo": "bar" // trailing +} +"# + .trim() + ); + } + + #[test] + fn test_block_comment() { + assert_eq!( + format_json(r#"{"foo":"bar",/* comment */"baz":"qux"}"#, " "), + r#" +{ + "foo": "bar", + /* comment */ + "baz": "qux" +} +"# + .trim() + ); + } + + #[test] + fn test_comment_in_array() { + assert_eq!( + format_json( + r#"[1,// item comment +2,3]"#, + " " + ), + r#" +[ + 1, // item comment + 2, + 3 +] +"# + .trim() + ); + } + + #[test] + fn test_comment_only_line() { + assert_eq!( + format_json( + r#"{ + // this is a standalone comment + "foo": "bar" +}"#, + " " + ), + r#" +{ + // this is a standalone comment + "foo": "bar" +} +"# + .trim() + ); + } + + #[test] + fn test_multiline_block_comment() { + assert_eq!( + format_json( + r#"{ + "foo": "bar" + /** + Hello World! + + Hi there + */ +}"#, + " " + ), + r#" +{ + "foo": "bar" + /** + Hello World! + + Hi there + */ +} +"# + .trim() + ); + } + + // NOTE: trailing whitespace on output lines is trimmed by the formatter. + // We can't easily add a test for this because raw string literals get + // trailing whitespace stripped by the editor/linter. + + #[test] + fn test_comment_inside_string_ignored() { + assert_eq!( + format_json(r#"{"foo":"// not a comment","bar":"/* also not */"}"#, " "), + r#" +{ + "foo": "// not a comment", + "bar": "/* also not */" +} +"# + .trim() + ); + } + + #[test] + fn test_comment_on_line_after_comma() { + assert_eq!( + format_json( + r#"{ + "a": "aaa", + // "b": "bbb" +}"#, + " " + ), + r#" +{ + "a": "aaa", + // "b": "bbb" +} "# .trim() ); diff --git a/crates/yaak-templates/src/lib.rs b/crates/yaak-templates/src/lib.rs index 93708b66..db5c4e14 100644 --- a/crates/yaak-templates/src/lib.rs +++ b/crates/yaak-templates/src/lib.rs @@ -1,6 +1,7 @@ pub mod error; pub mod escape; pub mod format_json; +pub mod strip_json_comments; pub mod parser; pub mod renderer; pub mod wasm; diff --git a/crates/yaak-templates/src/strip_json_comments.rs b/crates/yaak-templates/src/strip_json_comments.rs new file mode 100644 index 00000000..ade19c8c --- /dev/null +++ b/crates/yaak-templates/src/strip_json_comments.rs @@ -0,0 +1,318 @@ +/// Strips JSON comments only if the result is valid JSON. If stripping comments +/// produces invalid JSON, the original text is returned unchanged. +pub fn maybe_strip_json_comments(text: &str) -> String { + let stripped = strip_json_comments(text); + if serde_json::from_str::(&stripped).is_ok() { + stripped + } else { + text.to_string() + } +} + +/// Strips comments from JSONC, preserving the original formatting as much as possible. +/// +/// - Trailing comments on a line are removed (along with preceding whitespace) +/// - Whole-line comments are removed, including the line itself +/// - Block comments are removed, including any lines that become empty +/// - Comments inside strings and template tags are left alone +pub fn strip_json_comments(text: &str) -> String { + let mut chars = text.chars().peekable(); + let mut result = String::with_capacity(text.len()); + let mut in_string = false; + let mut in_template_tag = false; + + loop { + let current_char = match chars.next() { + None => break, + Some(c) => c, + }; + + // Handle JSON strings + if in_string { + result.push(current_char); + match current_char { + '"' => in_string = false, + '\\' => { + if let Some(c) = chars.next() { + result.push(c); + } + } + _ => {} + } + continue; + } + + // Handle template tags + if in_template_tag { + result.push(current_char); + if current_char == ']' && chars.peek() == Some(&'}') { + result.push(chars.next().unwrap()); + in_template_tag = false; + } + continue; + } + + // Check for template tag start + if current_char == '$' && chars.peek() == Some(&'{') { + let mut lookahead = chars.clone(); + lookahead.next(); // skip { + if lookahead.peek() == Some(&'[') { + in_template_tag = true; + result.push(current_char); + result.push(chars.next().unwrap()); // { + result.push(chars.next().unwrap()); // [ + continue; + } + } + + // Check for line comment + if current_char == '/' && chars.peek() == Some(&'/') { + chars.next(); // skip second / + // Consume until newline + loop { + match chars.peek() { + Some(&'\n') | None => break, + Some(_) => { + chars.next(); + } + } + } + // Trim trailing whitespace that preceded the comment + let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len(); + result.truncate(trimmed_len); + continue; + } + + // Check for block comment + if current_char == '/' && chars.peek() == Some(&'*') { + chars.next(); // skip * + // Consume until */ + loop { + match chars.next() { + None => break, + Some('*') if chars.peek() == Some(&'/') => { + chars.next(); // skip / + break; + } + Some(_) => {} + } + } + // Trim trailing whitespace that preceded the comment + let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len(); + result.truncate(trimmed_len); + // Skip whitespace/newline after the block comment if the next line is content + // (this handles the case where the block comment is on its own line) + continue; + } + + if current_char == '"' { + in_string = true; + } + + result.push(current_char); + } + + // Remove lines that are now empty (were comment-only lines) + let result = result + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n"); + + // Remove trailing commas before } or ] + strip_trailing_commas(&result) +} + +/// Removes trailing commas before closing braces/brackets, respecting strings. +fn strip_trailing_commas(text: &str) -> String { + let mut result = String::with_capacity(text.len()); + let chars: Vec = text.chars().collect(); + let mut i = 0; + let mut in_string = false; + + while i < chars.len() { + let ch = chars[i]; + + if in_string { + result.push(ch); + match ch { + '"' => in_string = false, + '\\' => { + i += 1; + if i < chars.len() { + result.push(chars[i]); + } + } + _ => {} + } + i += 1; + continue; + } + + if ch == '"' { + in_string = true; + result.push(ch); + i += 1; + continue; + } + + if ch == ',' { + // Look ahead past whitespace/newlines for } or ] + let mut j = i + 1; + while j < chars.len() && chars[j].is_whitespace() { + j += 1; + } + if j < chars.len() && (chars[j] == '}' || chars[j] == ']') { + // Skip the comma + i += 1; + continue; + } + } + + result.push(ch); + i += 1; + } + + result +} + +#[cfg(test)] +mod tests { + use crate::strip_json_comments::strip_json_comments; + + #[test] + fn test_no_comments() { + let input = r#"{ + "foo": "bar", + "baz": 123 +}"#; + assert_eq!(strip_json_comments(input), input); + } + + #[test] + fn test_trailing_line_comment() { + assert_eq!( + strip_json_comments(r#"{ + "foo": "bar", // this is a comment + "baz": 123 +}"#), + r#"{ + "foo": "bar", + "baz": 123 +}"# + ); + } + + #[test] + fn test_whole_line_comment() { + assert_eq!( + strip_json_comments(r#"{ + // this is a comment + "foo": "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_inline_block_comment() { + assert_eq!( + strip_json_comments(r#"{ + "foo": /* a comment */ "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_whole_line_block_comment() { + assert_eq!( + strip_json_comments(r#"{ + /* a comment */ + "foo": "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_multiline_block_comment() { + assert_eq!( + strip_json_comments(r#"{ + /** + * Hello World! + */ + "foo": "bar" +}"#), + r#"{ + "foo": "bar" +}"# + ); + } + + #[test] + fn test_comment_inside_string_preserved() { + let input = r#"{ + "foo": "// not a comment", + "bar": "/* also not */" +}"#; + assert_eq!(strip_json_comments(input), input); + } + + #[test] + fn test_comment_inside_template_tag_preserved() { + let input = r#"{ + "foo": ${[ fn("// hi", "/* hey */") ]} +}"#; + assert_eq!(strip_json_comments(input), input); + } + + #[test] + fn test_multiple_comments() { + assert_eq!( + strip_json_comments(r#"{ + // first comment + "foo": "bar", // trailing + /* block */ + "baz": 123 +}"#), + r#"{ + "foo": "bar", + "baz": 123 +}"# + ); + } + + #[test] + fn test_trailing_comma_after_comment_removed() { + assert_eq!( + strip_json_comments(r#"{ + "a": "aaa", + // "b": "bbb" +}"#), + r#"{ + "a": "aaa" +}"# + ); + } + + #[test] + fn test_trailing_comma_in_array() { + assert_eq!( + strip_json_comments(r#"[1, 2, /* 3 */]"#), + r#"[1, 2]"# + ); + } + + #[test] + fn test_comma_inside_string_preserved() { + let input = r#"{"a": "hello,}"#; + assert_eq!(strip_json_comments(input), input); + } +} diff --git a/crates/yaak/src/render.rs b/crates/yaak/src/render.rs index 64b5e04e..75015ae7 100644 --- a/crates/yaak/src/render.rs +++ b/crates/yaak/src/render.rs @@ -2,7 +2,7 @@ use log::info; use serde_json::Value; use std::collections::BTreeMap; use yaak_http::path_placeholders::apply_path_placeholders; -use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter}; +use yaak_models::models::{Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter}; use yaak_models::render::make_vars_hashmap; use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; @@ -89,6 +89,64 @@ pub async fn render_http_request( Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() }) } +pub async fn render_grpc_request( + r: &GrpcRequest, + environment_chain: Vec, + cb: &T, + opt: &RenderOptions, +) -> yaak_templates::error::Result { + let vars = &make_vars_hashmap(environment_chain); + + let mut metadata = Vec::new(); + for p in r.metadata.clone() { + if !p.enabled { + continue; + } + metadata.push(HttpRequestHeader { + enabled: p.enabled, + name: parse_and_render(p.name.as_str(), vars, cb, opt).await?, + value: parse_and_render(p.value.as_str(), vars, cb, opt).await?, + id: p.id, + }) + } + + let authentication = { + let mut disabled = false; + let mut auth = BTreeMap::new(); + match r.authentication.get("disabled") { + Some(Value::Bool(true)) => { + disabled = true; + } + Some(Value::String(tmpl)) => { + disabled = parse_and_render(tmpl.as_str(), vars, cb, opt) + .await + .unwrap_or_default() + .is_empty(); + info!( + "Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\"" + ); + } + _ => {} + } + if disabled { + auth.insert("disabled".to_string(), Value::Bool(true)); + } else { + for (k, v) in r.authentication.clone() { + if k == "disabled" { + auth.insert(k, Value::Bool(false)); + } else { + auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?); + } + } + } + auth + }; + + let url = parse_and_render(r.url.as_str(), vars, cb, opt).await?; + + Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() }) +} + fn strip_disabled_form_entries(v: Value) -> Value { match v { Value::Array(items) => Value::Array( diff --git a/package-lock.json b/package-lock.json index c7bca5de..a3235a71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,7 @@ "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", + "@shopify/lang-jsonc": "^1.0.1", "@tanstack/react-query": "^5.90.5", "@tanstack/react-router": "^1.133.13", "@tanstack/react-virtual": "^3.13.12", @@ -130,7 +131,6 @@ "deep-equal": "^2.2.3", "eventemitter3": "^5.0.1", "focus-trap-react": "^11.0.4", - "format-graphql": "^1.5.0", "fuzzbunny": "^1.0.1", "hexy": "^0.3.5", "history": "^5.3.0", @@ -2744,6 +2744,16 @@ "ebnf": "^1.9.1" } }, + "node_modules/@shopify/lang-jsonc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shopify/lang-jsonc/-/lang-jsonc-1.0.1.tgz", + "integrity": "sha512-KrBrRFhvr1qJiZBODAtqbL1u1e67UR3plBN79Z8nd5TQAAzpx66jS4zs7Ss9M22ygGrpWFhyhSoNVlp5VCYktQ==", + "license": "MIT", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.7" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -5515,105 +5525,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cliui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/clone-regexp": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", @@ -6229,15 +6140,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -7378,19 +7280,6 @@ "dev": true, "license": "MIT" }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/focus-trap": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.7.1.tgz", @@ -7483,21 +7372,6 @@ "node": ">=0.4.x" } }, - "node_modules/format-graphql": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/format-graphql/-/format-graphql-1.5.0.tgz", - "integrity": "sha512-ZZWcjwJ1IMdnW9l3CeYccC/J7skqOB18tY3autO5OUQuGVZpQu6Es3SThRm25SfiMeZO1+UbzIqnGbjAURu/UA==", - "dependencies": { - "graphql": "^15.1.0", - "yargs": "^15.3.1" - }, - "bin": { - "format-graphql": "dist/bin/index.js" - }, - "engines": { - "node": ">=10.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7933,6 +7807,7 @@ "version": "15.10.1", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", + "peer": true, "engines": { "node": ">= 10.x" } @@ -9418,18 +9293,6 @@ "node": ">=4" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -11748,33 +11611,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -11801,15 +11637,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -11903,15 +11730,6 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "license": "MIT" }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -13011,12 +12829,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -13481,12 +13293,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15712,12 +15518,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -15933,12 +15733,6 @@ "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -15961,91 +15755,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-parser/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -16203,9 +15912,9 @@ "version": "0.2.1", "dependencies": { "@hono/mcp": "^0.2.3", - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.10", "@modelcontextprotocol/sdk": "^1.26.0", - "hono": "^4.11.10", + "hono": "^4.12.4", "zod": "^3.25.76" }, "devDependencies": { @@ -16213,6 +15922,27 @@ "typescript": "^5.9.3" } }, + "plugins-external/mcp-server/node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "plugins-external/mcp-server/node_modules/hono": { + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "plugins/action-copy-curl": { "name": "@yaak/action-copy-curl", "version": "0.1.0" diff --git a/packages/theme/src/window.ts b/packages/theme/src/window.ts index 9d429f53..2bef44cf 100644 --- a/packages/theme/src/window.ts +++ b/packages/theme/src/window.ts @@ -140,6 +140,17 @@ function bannerColorVariables(color: YaakColor | null): Partial { }; } +function _inputCSS(color: YaakColor | null): Partial { + if (color == null) return {}; + + const theme: Partial = { + border: color.css(), + }; + + return theme; +} + + function buttonSolidColorVariables( color: YaakColor | null, isDefault = false, diff --git a/packages/ui/src/components/HeaderSize.tsx b/packages/ui/src/components/HeaderSize.tsx index 34a3926f..a9c705b0 100644 --- a/packages/ui/src/components/HeaderSize.tsx +++ b/packages/ui/src/components/HeaderSize.tsx @@ -43,7 +43,7 @@ export function HeaderSize({ } else if (osType === 'macos') { if (!isFullscreen) { // Add large padding for window controls - s.paddingLeft = 72 / interfaceScale; + s.paddingLeft = 76 / interfaceScale; } } else if (!ignoreControlsSpacing && !hideWindowControls) { s.paddingRight = WINDOW_CONTROLS_WIDTH; diff --git a/packages/ui/src/lib/constants.ts b/packages/ui/src/lib/constants.ts index 14eab995..67dbaf8c 100644 --- a/packages/ui/src/lib/constants.ts +++ b/packages/ui/src/lib/constants.ts @@ -1,4 +1,4 @@ -export const HEADER_SIZE_MD = '27px'; +export const HEADER_SIZE_MD = '30px'; export const HEADER_SIZE_LG = '40px'; export const WINDOW_CONTROLS_WIDTH = '10.5rem'; diff --git a/plugins-external/mcp-server/package.json b/plugins-external/mcp-server/package.json index 6d7e6581..ebeba65e 100644 --- a/plugins-external/mcp-server/package.json +++ b/plugins-external/mcp-server/package.json @@ -16,9 +16,9 @@ }, "dependencies": { "@hono/mcp": "^0.2.3", - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.10", "@modelcontextprotocol/sdk": "^1.26.0", - "hono": "^4.11.10", + "hono": "^4.12.4", "zod": "^3.25.76" }, "devDependencies": {