Repository: alloy-framework/alloy
Pull request #238 (2025-07-08 20:09:48)

Json values compose
And some migration of json library to the new structure


Patch:
diff --git a/.chronus/changes/json-values-compose-2025-6-8-20-10-8.md b/.chronus/changes/json-values-compose-2025-6-8-20-10-8.md
new file mode 100644
index 0000000..eee525b
--- /dev/null
+++ b/.chronus/changes/json-values-compose-2025-6-8-20-10-8.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: feature
+packages:
+  - "@alloy-js/json"
+---
+
+Support nesting components in `<JsonValue jsValue=`
diff --git a/packages/json/package.json b/packages/json/package.json
index 7c9c3df..d3e539f 100644
--- a/packages/json/package.json
+++ b/packages/json/package.json
@@ -12,6 +12,9 @@
       "import": "./dist/src/components/stc/index.js"
     }
   },
+  "imports": {
+    "#test/*": "./test/*"
+  },
   "scripts": {
     "generate-docs": "api-extractor run",
     "build": "alloy build && pnpm run generate-docs",
diff --git a/packages/json/src/components/index.ts b/packages/json/src/components/index.ts
index 00cc49e..726a43a 100644
--- a/packages/json/src/components/index.ts
+++ b/packages/json/src/components/index.ts
@@ -1,5 +1,5 @@
-export * from "./JsonArray.jsx";
-export * from "./JsonObject.jsx";
-export * from "./JsonValue.jsx";
-export * from "./Reference.jsx";
-export * from "./SourceFile.jsx";
+export * from "./json-array.jsx";
+export * from "./json-object.jsx";
+export * from "./json-value.jsx";
+export * from "./reference.jsx";
+export * from "./source-file.jsx";
diff --git a/packages/json/src/components/JsonArray.tsx b/packages/json/src/components/json-array.tsx
similarity index 98%
rename from packages/json/src/components/JsonArray.tsx
rename to packages/json/src/components/json-array.tsx
index 974a73e..0753bd6 100644
--- a/packages/json/src/components/JsonArray.tsx
+++ b/packages/json/src/components/json-array.tsx
@@ -12,7 +12,7 @@ import {
   useMemberScope,
 } from "@alloy-js/core";
 import { JsonOutputSymbol } from "../symbols/json-symbol.js";
-import { JsonValue } from "./JsonValue.jsx";
+import { JsonValue } from "./json-value.jsx";
 
 export interface JsonArrayPropsBase {
   /**
diff --git a/packages/json/src/components/JsonObject.tsx b/packages/json/src/components/json-object.tsx
similarity index 98%
rename from packages/json/src/components/JsonObject.tsx
rename to packages/json/src/components/json-object.tsx
index 2eef6c6..135c589 100644
--- a/packages/json/src/components/JsonObject.tsx
+++ b/packages/json/src/components/json-object.tsx
@@ -12,7 +12,7 @@ import {
   useMemberDeclaration,
 } from "@alloy-js/core";
 import { JsonOutputSymbol } from "../symbols/json-symbol.js";
-import { JsonValue } from "./JsonValue.jsx";
+import { JsonValue } from "./json-value.jsx";
 
 export interface JsonObjectPropsBase {
   /**
diff --git a/packages/json/src/components/JsonValue.tsx b/packages/json/src/components/json-value.tsx
similarity index 77%
rename from packages/json/src/components/JsonValue.tsx
rename to packages/json/src/components/json-value.tsx
index 6fc1caf..15b7378 100644
--- a/packages/json/src/components/JsonValue.tsx
+++ b/packages/json/src/components/json-value.tsx
@@ -1,6 +1,6 @@
-import { Refkey, useMemberDeclaration } from "@alloy-js/core";
-import { JsonArray } from "./JsonArray.jsx";
-import { JsonObject } from "./JsonObject.jsx";
+import { Children, Refkey, useMemberDeclaration } from "@alloy-js/core";
+import { JsonArray } from "./json-array.jsx";
+import { JsonObject } from "./json-object.jsx";
 
 export interface JsonValueProps {
   /**
@@ -22,7 +22,7 @@ export interface JsonValueProps {
  * value will be serialized to a string, number, boolean, or null as
  * appropriate.
  */
-export function JsonValue(props: JsonValueProps) {
+export function JsonValue(props: JsonValueProps): Children {
   const refkeys = [props.refkey ?? []].flat();
   if (props.jsValue === null) {
     setMemberRefkey(refkeys);
@@ -44,7 +44,12 @@ export function JsonValue(props: JsonValueProps) {
   }
 
   if (typeof props.jsValue === "object") {
-    return <JsonObject jsValue={props.jsValue as any} />;
+    return <JsonObject jsValue={props.jsValue as Record<string, unknown>} />;
+  }
+
+  if (typeof props.jsValue === "function") {
+    // functions are inserted as-is.
+    return props.jsValue as () => Children;
   }
 
   throw new Error("Cannot emit js value with type of " + typeof props.jsValue);
diff --git a/packages/json/src/components/Reference.tsx b/packages/json/src/components/reference.tsx
similarity index 100%
rename from packages/json/src/components/Reference.tsx
rename to packages/json/src/components/reference.tsx
diff --git a/packages/json/src/components/SourceFile.tsx b/packages/json/src/components/source-file.tsx
similarity index 96%
rename from packages/json/src/components/SourceFile.tsx
rename to packages/json/src/components/source-file.tsx
index 85a0c84..aebaf79 100644
--- a/packages/json/src/components/SourceFile.tsx
+++ b/packages/json/src/components/source-file.tsx
@@ -6,7 +6,7 @@ import {
 } from "@alloy-js/core";
 import { JsonFileContext } from "../context/JsonFileContext.js";
 import { JsonOutputSymbol } from "../symbols/json-symbol.js";
-import { Reference } from "./Reference.jsx";
+import { Reference } from "./reference.jsx";
 
 export interface SourceFileProps {
   /** The path for this source file relative to the parent directory */


Test patch:
diff --git a/packages/json/test/array.test.tsx b/packages/json/src/components/array.test.tsx
similarity index 96%
rename from packages/json/test/array.test.tsx
rename to packages/json/src/components/array.test.tsx
index 3efcff7..39be7eb 100644
--- a/packages/json/test/array.test.tsx
+++ b/packages/json/src/components/array.test.tsx
@@ -2,7 +2,7 @@ import { reactive, renderTree } from "@alloy-js/core";
 import "@alloy-js/core/testing";
 import { d, printTree } from "@alloy-js/core/testing";
 import { expect, it } from "vitest";
-import { jsonTest } from "./utils.jsx";
+import { jsonTest } from "../../test/utils.jsx";
 
 it("renders empty arrays", () => {
   const template = jsonTest([]);
diff --git a/packages/json/src/components/json-value.test.tsx b/packages/json/src/components/json-value.test.tsx
new file mode 100644
index 0000000..abfdf1e
--- /dev/null
+++ b/packages/json/src/components/json-value.test.tsx
@@ -0,0 +1,44 @@
+import { Output } from "@alloy-js/core";
+import { expect, it } from "vitest";
+import { JsonValue } from "./json-value.jsx";
+import { SourceFile } from "./source-file.jsx";
+
+const TestingJsonObject = () => {
+  return (
+    <JsonValue
+      jsValue={{
+        id: "test-id",
+        name: "Test Name",
+        nested: () => <Bar />,
+      }}
+    />
+  );
+};
+
+const Bar = () => {
+  return (
+    <JsonValue
+      jsValue={{
+        bar: "Foo",
+      }}
+    />
+  );
+};
+
+it("compose json data with components", () => {
+  expect(
+    <Output>
+      <SourceFile path="test.json">
+        <TestingJsonObject />
+      </SourceFile>
+    </Output>,
+  ).toRenderTo(`
+    {
+      "id": "test-id",
+      "name": "Test Name",
+      "nested": {
+        "bar": "Foo"
+      }
+    }
+  `);
+});
diff --git a/packages/json/test/object.test.tsx b/packages/json/src/components/object.test.tsx
similarity index 88%
rename from packages/json/test/object.test.tsx
rename to packages/json/src/components/object.test.tsx
index 02bc72d..1c9f15e 100644
--- a/packages/json/test/object.test.tsx
+++ b/packages/json/src/components/object.test.tsx
@@ -2,12 +2,9 @@ import { List, Output, reactive, renderTree } from "@alloy-js/core";
 import "@alloy-js/core/testing";
 import { d, printTree } from "@alloy-js/core/testing";
 import { expect, it } from "vitest";
-import {
-  JsonObject,
-  JsonObjectProperty,
-} from "../src/components/JsonObject.jsx";
-import { SourceFile } from "../src/components/SourceFile.jsx";
-import { jsonTest } from "./utils.jsx";
+import { jsonTest } from "../../test/utils.jsx";
+import { JsonObject, JsonObjectProperty } from "./json-object.jsx";
+import { SourceFile } from "./source-file.jsx";
 
 it("renders objects", () => {
   const template = jsonTest({ a: 1, b: "hello" });
diff --git a/packages/json/src/components/primitives.test.tsx b/packages/json/src/components/primitives.test.tsx
new file mode 100644
index 0000000..85d2478
--- /dev/null
+++ b/packages/json/src/components/primitives.test.tsx
@@ -0,0 +1,15 @@
+import { jsonTest } from "#test/utils.jsx";
+import "@alloy-js/core/testing";
+import { expect, it } from "vitest";
+
+it.each([
+  ["boolean (true)", true, "true"],
+  ["boolean (false)", false, "false"],
+  ["integer", 10, "10"],
+  ["negative integer", -10, "-10"],
+  ["float", 1.314, "1.314"],
+  ["string", "Hello, world!", `"Hello, world!"`],
+  ["null", null, "null"],
+])("%s", (_, data, expected) => {
+  expect(jsonTest(data)).toRenderTo(expected);
+});
diff --git a/packages/json/test/reference.test.tsx b/packages/json/src/components/reference.test.tsx
similarity index 94%
rename from packages/json/test/reference.test.tsx
rename to packages/json/src/components/reference.test.tsx
index 578e872..87a56d7 100644
--- a/packages/json/test/reference.test.tsx
+++ b/packages/json/src/components/reference.test.tsx
@@ -1,13 +1,10 @@
 import { Output, refkey, SourceDirectory } from "@alloy-js/core";
 import "@alloy-js/core/testing";
 import { expect, it } from "vitest";
-import { JsonArray, JsonArrayElement } from "../src/components/JsonArray.jsx";
-import {
-  JsonObject,
-  JsonObjectProperty,
-} from "../src/components/JsonObject.jsx";
-import { JsonValue } from "../src/components/JsonValue.jsx";
-import { SourceFile } from "../src/components/SourceFile.jsx";
+import { JsonArray, JsonArrayElement } from "./json-array.jsx";
+import { JsonObject, JsonObjectProperty } from "./json-object.jsx";
+import { JsonValue } from "./json-value.jsx";
+import { SourceFile } from "./source-file.jsx";
 
 it("references objects within the same file", () => {
   const objectValue = refkey();
diff --git a/packages/json/test/primitives.test.tsx b/packages/json/test/primitives.test.tsx
deleted file mode 100644
index 95647bb..0000000
--- a/packages/json/test/primitives.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import "@alloy-js/core/testing";
-import { expect, it } from "vitest";
-import { jsonTest } from "./utils.jsx";
-
-it("renders primitives", () => {
-  expect(jsonTest(true)).toRenderTo("true");
-  expect(jsonTest(false)).toRenderTo("false");
-
-  expect(jsonTest(10)).toRenderTo("10");
-  expect(jsonTest(-10)).toRenderTo("-10");
-  expect(jsonTest(1.314)).toRenderTo("1.314");
-
-  expect(jsonTest("hello")).toRenderTo('"hello"');
-
-  expect(jsonTest(null)).toRenderTo("null");
-});
diff --git a/packages/json/test/utils.tsx b/packages/json/test/utils.tsx
index aca10e7..a1c4a34 100644
--- a/packages/json/test/utils.tsx
+++ b/packages/json/test/utils.tsx
@@ -1,6 +1,6 @@
 import { Output } from "@alloy-js/core";
-import { JsonValue } from "../src/components/JsonValue.jsx";
-import { SourceFile } from "../src/components/SourceFile.jsx";
+import { JsonValue } from "../src/components/json-value.jsx";
+import { SourceFile } from "../src/components/source-file.jsx";
 
 export function jsonTest(jsValue: unknown) {
   return (
diff --git a/packages/json/test/vitest.d.ts b/packages/json/test/vitest.d.ts
deleted file mode 100644
index c4c777d..0000000
--- a/packages/json/test/vitest.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import "vitest";
-
-interface ToRenderToRenderOptions {
-  printWidth?: number;
-  tabWidth?: number;
-  useTabs?: boolean;
-}
-
-interface CustomMatchers<R = unknown> {
-  toRenderTo: (str: string, options?: ToRenderToRenderOptions) => R;
-}
-
-declare module "vitest" {
-  interface Assertion<T = any> extends CustomMatchers<T> {}
-  interface AsymmetricMatchersContaining extends CustomMatchers {}
-}
diff --git a/packages/json/test/vitest.setup.ts b/packages/json/test/vitest.setup.ts
new file mode 100644
index 0000000..96cd441
--- /dev/null
+++ b/packages/json/test/vitest.setup.ts
@@ -0,0 +1,1 @@
+import "@alloy-js/core/testing";
diff --git a/packages/json/vitest.config.ts b/packages/json/vitest.config.ts
index ba661c0..a984519 100644
--- a/packages/json/vitest.config.ts
+++ b/packages/json/vitest.config.ts
@@ -6,5 +6,8 @@ export default defineConfig({
     jsx: "preserve",
     sourcemap: "both",
   },
+  test: {
+    setupFiles: ["./test/vitest.setup.ts"],
+  },
   plugins: [alloyPlugin()],
 });