VS Code extension — visual JSON editor with tree sidebar, schema support, and JSONC
+
VS Code extension — visual editor with tree sidebar, schema support, JSONC, and YAML
diff --git a/apps/vscode/README.md b/apps/vscode/README.md
index d00df25..9679c2c 100644
--- a/apps/vscode/README.md
+++ b/apps/vscode/README.md
@@ -6,12 +6,13 @@ VS Code extension for [visual-json](https://github.com/vercel-labs/visual-json)
## Features
-- **Visual editor** for `.json` and `.jsonc` files via "Open With..." > "visual-json"
+- **Visual editor** for `.json`, `.jsonc`, `.yaml`, and `.yml` files via "Open With..." > "visual-json"
+- **YAML support** — open and edit YAML files with the same tree and form views used for JSON
- **Tree sidebar** with expand/collapse, drag-and-drop, keyboard navigation, and search
- **Form editor** with inline editing, schema-aware inputs, and breadcrumb navigation
-- **Schema support** — auto-detects schemas from `$schema` or known filenames (`package.json`, `tsconfig.json`, etc.)
+- **Schema support** — auto-detects schemas from `$schema` or known filenames (`package.json`, `tsconfig.json`, `docker-compose.yml`, GitHub Actions workflows, etc.)
- **Theme integration** — adapts to your VS Code color theme automatically
-- **Explorer panel** — sidebar view that syncs with the active JSON file
+- **Explorer panel** — sidebar view that syncs with the active JSON or YAML file
## Development
diff --git a/apps/web/package.json b/apps/web/package.json
index d8af029..f52f87b 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -20,6 +20,7 @@
"@upstash/redis": "^1.36.2",
"@visual-json/core": "workspace:*",
"@visual-json/react": "workspace:*",
+ "@visual-json/yaml": "workspace:*",
"ai": "^6.0.94",
"bash-tool": "^1.3.15",
"class-variance-authority": "^0.7.1",
diff --git a/apps/web/src/app/docs/changelog/page.mdx b/apps/web/src/app/docs/changelog/page.mdx
index a9a7c30..f3d8d59 100644
--- a/apps/web/src/app/docs/changelog/page.mdx
+++ b/apps/web/src/app/docs/changelog/page.mdx
@@ -2,6 +2,15 @@ export { metadata } from "./metadata";
# Changelog
+## 0.4.0
+
+### Features
+
+- **YAML support** — new `@visual-json/yaml` package for parsing, serializing, and round-tripping YAML through the visual-json tree model. The VS Code extension now opens `.yaml` and `.yml` files with the same tree and form views used for JSON.
+- **YAML schema detection** — `resolveSchema` in `@visual-json/core` now recognizes well-known YAML filenames (`docker-compose.yml`, `.gitlab-ci.yml`, `pnpm-workspace.yaml`, etc.) and glob patterns (`.github/workflows/*.yml`, `.github/actions/*/action.yml`) for automatic schema-aware editing.
+
+---
+
## 0.3.0
### Features
diff --git a/apps/web/src/app/docs/core/page.mdx b/apps/web/src/app/docs/core/page.mdx
index 15d31f3..d694b44 100644
--- a/apps/web/src/app/docs/core/page.mdx
+++ b/apps/web/src/app/docs/core/page.mdx
@@ -85,12 +85,16 @@ console.log(history.canUndo, history.canRedo);
## Schema
Resolve JSON Schemas from well-known filenames (e.g. `package.json`, `tsconfig.json`) and
-navigate schema properties by path.
+navigate schema properties by path. YAML config files are supported too — `resolveSchema`
+recognizes filenames like `docker-compose.yml`, `.gitlab-ci.yml`, `pnpm-workspace.yaml`,
+and glob patterns like `.github/workflows/*.yml`.
```ts
import { resolveSchema, getPropertySchema, clearSchemaCache } from "@visual-json/core";
const schema = await resolveSchema(json, "package.json");
+// Also works with YAML filenames:
+// const schema = await resolveSchema(value, "docker-compose.yml");
if (schema) {
const propSchema = getPropertySchema(schema, "$.scripts.build");
diff --git a/apps/web/src/app/docs/getting-started/page.mdx b/apps/web/src/app/docs/getting-started/page.mdx
index eff6a4d..13fad96 100644
--- a/apps/web/src/app/docs/getting-started/page.mdx
+++ b/apps/web/src/app/docs/getting-started/page.mdx
@@ -17,7 +17,8 @@ visual-json is split into focused packages:
@visual-json/core
Headless tree model, operations, schema resolution, diffing, search, and validation
@visual-json/react
React UI components — TreeView, FormView, DiffView, and more
@visual-json/vue
Vue 3 UI components — TreeView, FormView, DiffView, and more
-
@visual-json/vscode
VS Code extension — open any JSON/JSONC file with a visual editor
+
@visual-json/yaml
YAML support — parse, serialize, and schema-detect YAML files
+
@visual-json/vscode
VS Code extension — open any JSON, JSONC, or YAML file with a visual editor
@@ -92,12 +93,15 @@ visual-json supports multiple ways to view and edit your data:
## VS Code Extension
-visual-json is also available as a VS Code extension. Open any `.json` or `.jsonc` file,
+visual-json is also available as a VS Code extension. Open any `.json`, `.jsonc`, `.yaml`, or `.yml` file,
right-click the tab, and choose **"Open With..." > "visual-json"** to use the visual editor.
The extension includes a tree sidebar, form editor, schema support (auto-detected from
-`$schema` or known filenames like `package.json` and `tsconfig.json`), and adapts to your
-VS Code theme colors automatically.
+`$schema` or known filenames like `package.json`, `tsconfig.json`, `docker-compose.yml`,
+GitHub Actions workflows, and more), and adapts to your VS Code theme colors automatically.
+
+YAML files are parsed and serialized transparently — you edit them with the same tree and
+form views used for JSON, and changes are saved back as YAML.
## Next Steps
diff --git a/apps/web/src/app/editor.tsx b/apps/web/src/app/editor.tsx
index 6367927..bebec1b 100644
--- a/apps/web/src/app/editor.tsx
+++ b/apps/web/src/app/editor.tsx
@@ -3,6 +3,11 @@
import { useState, useCallback, useEffect, useRef } from "react";
import type { JsonValue, JsonSchema } from "@visual-json/core";
import { resolveSchema } from "@visual-json/core";
+import {
+ isYamlFile,
+ parseYamlContent,
+ stringifyYamlContent,
+} from "@visual-json/yaml";
import { JsonEditor } from "@visual-json/react";
import { Button } from "@/components/ui/button";
import {
@@ -34,6 +39,18 @@ import {
X,
} from "lucide-react";
+function serializeValue(value: JsonValue, fname: string): string {
+ return isYamlFile(fname)
+ ? stringifyYamlContent(value)
+ : JSON.stringify(value, null, 2);
+}
+
+function parseText(text: string, fname: string): JsonValue {
+ return isYamlFile(fname)
+ ? parseYamlContent(text)
+ : (JSON.parse(text) as JsonValue);
+}
+
type ViewMode = "tree" | "raw";
const VIEW_MODES: { id: ViewMode; label: string }[] = [
@@ -70,7 +87,7 @@ const samples: { name: string; filename: string; data: JsonValue }[] = [
},
{
name: "OpenAPI spec",
- filename: "openapi.json",
+ filename: "openapi.yaml",
data: {
openapi: "3.1.0",
info: {
@@ -481,7 +498,7 @@ export function Editor({
const [editorShowDescriptions, setEditorShowDescriptions] = useState(false);
const [editorShowCounts, setEditorShowCounts] = useState(false);
const [rawText, setRawText] = useState(
- JSON.stringify(samples[0].data, null, 2),
+ serializeValue(samples[0].data, samples[0].filename),
);
const [rawError, setRawError] = useState(null);
const [parseError, setParseError] = useState(null);
@@ -506,12 +523,12 @@ export function Editor({
skipRawSync.current = false;
return;
}
- setRawText(JSON.stringify(jsonValue, null, 2));
- }, [jsonValue]);
+ setRawText(serializeValue(jsonValue, filename));
+ }, [jsonValue, filename]);
- const loadJson = useCallback((text: string, fname: string) => {
+ const loadContent = useCallback((text: string, fname: string) => {
try {
- const parsed = JSON.parse(text);
+ const parsed = parseText(text, fname);
setJsonValue(parsed);
setFilename(fname);
setActiveSample(fname);
@@ -519,7 +536,7 @@ export function Editor({
setRawError(null);
setParseError(null);
} catch {
- setParseError("Invalid JSON");
+ setParseError(isYamlFile(fname) ? "Invalid YAML" : "Invalid JSON");
}
}, []);
@@ -537,24 +554,26 @@ export function Editor({
const handlePaste = useCallback(async () => {
try {
const text = await navigator.clipboard.readText();
- loadJson(text, "pasted.json");
+ loadContent(text, "pasted.json");
} catch {
setPasteText("");
setPasteDialogOpen(true);
}
- }, [loadJson]);
+ }, [loadContent]);
const handlePasteSubmit = useCallback(() => {
if (pasteText.trim()) {
- loadJson(pasteText, "pasted.json");
+ loadContent(pasteText, "pasted.json");
}
setPasteDialogOpen(false);
setPasteText("");
- }, [pasteText, loadJson]);
+ }, [pasteText, loadContent]);
const handleDownload = useCallback(() => {
- const text = JSON.stringify(jsonValue, null, 2);
- const blob = new Blob([text], { type: "application/json" });
+ const yaml = isYamlFile(filename);
+ const text = serializeValue(jsonValue, filename);
+ const mime = yaml ? "text/yaml" : "application/json";
+ const blob = new Blob([text], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
@@ -565,23 +584,32 @@ export function Editor({
const handleCopyJson = useCallback(async () => {
try {
- await navigator.clipboard.writeText(JSON.stringify(jsonValue, null, 2));
+ await navigator.clipboard.writeText(serializeValue(jsonValue, filename));
} catch {
// clipboard access may be denied
}
- }, [jsonValue]);
+ }, [jsonValue, filename]);
- const handleRawChange = useCallback((newText: string) => {
- setRawText(newText);
- try {
- const parsed = JSON.parse(newText);
- setRawError(null);
- skipRawSync.current = true;
- setJsonValue(parsed);
- } catch (e) {
- setRawError(e instanceof Error ? e.message : "Invalid JSON");
- }
- }, []);
+ const handleRawChange = useCallback(
+ (newText: string) => {
+ setRawText(newText);
+ try {
+ const parsed = parseText(newText, filename);
+ setRawError(null);
+ skipRawSync.current = true;
+ setJsonValue(parsed);
+ } catch (e) {
+ setRawError(
+ e instanceof Error
+ ? e.message
+ : isYamlFile(filename)
+ ? "Invalid YAML"
+ : "Invalid JSON",
+ );
+ }
+ },
+ [filename],
+ );
useEffect(() => {
const el = dropRef.current;
@@ -606,7 +634,7 @@ export function Editor({
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string")
- loadJson(reader.result, file.name);
+ loadContent(reader.result, file.name);
};
reader.readAsText(file);
}
@@ -619,7 +647,7 @@ export function Editor({
el.removeEventListener("dragleave", handleDragLeave);
el.removeEventListener("drop", handleDrop);
};
- }, [loadJson]);
+ }, [loadContent]);
return (
- Drop JSON file here
+ Drop JSON or YAML file here