diff --git a/packages/core/src/refkey.ts b/packages/core/src/refkey.ts index e36fc675f..fa487c610 100644 --- a/packages/core/src/refkey.ts +++ b/packages/core/src/refkey.ts @@ -14,6 +14,26 @@ function getObjectKey(value: WeakKey) { return key; } +const JsonSym: unique symbol = Symbol(); +export type Json = { value: unknown; [JsonSym]: true }; + +export function json(value: unknown): Json { + const json: Json = { + value, + [JsonSym]: true, + }; + + markRaw(json); + + return json; +} + +export function isJson(value: unknown): value is Json { + return ( + typeof value === "object" && value !== null && Object.hasOwn(value, JsonSym) + ); +} + const RefkeySym: unique symbol = Symbol(); export type Refkey = { key: string; [RefkeySym]: true }; diff --git a/packages/core/src/render.ts b/packages/core/src/render.ts index 88441d822..6e18d3264 100644 --- a/packages/core/src/render.ts +++ b/packages/core/src/render.ts @@ -14,7 +14,7 @@ import { root, untrack, } from "./reactivity.js"; -import { isRefkey } from "./refkey.js"; +import { isJson, isRefkey, Json } from "./refkey.js"; import { Child, Children, @@ -482,6 +482,24 @@ function appendChild(node: RenderedTextTree, rawChild: Child) { cache.set(child, newNodes); return newNodes; }); + } else if (isJson(child)) { + node.push( + JSON.stringify( + child.value, + (key, value) => { + if (typeof value === "function") { + const componentRoot: RenderedTextTree = []; + pushStack(value.component, value.props); + renderWorker(componentRoot, untrack(value)); + popStack(); + return JSON.parse(componentRoot.toString()); + } else { + return value; + } + }, + 2, + ), + ); } else { throw new Error("Unexpected child type"); } @@ -493,6 +511,7 @@ type NormalizedChild = | string | (() => Child | Children) | CustomContext + | Json | IntrinsicElement; function normalizeChild(child: Child): NormalizedChildren { @@ -521,6 +540,8 @@ function normalizeChild(child: Child): NormalizedChildren { return child; } else if (isIntrinsicElement(child)) { return child; + } else if (isJson(child)) { + return child; } else { return String(child); } diff --git a/packages/core/src/runtime/component.ts b/packages/core/src/runtime/component.ts index c4727d7a2..faaf1312f 100644 --- a/packages/core/src/runtime/component.ts +++ b/packages/core/src/runtime/component.ts @@ -1,6 +1,6 @@ import { Ref } from "@vue/reactivity"; import { CustomContext } from "../reactivity.js"; -import { Refkey } from "../refkey.js"; +import { Json, Refkey } from "../refkey.js"; import { IntrinsicElement } from "./intrinsic.js"; export type Child = @@ -13,6 +13,7 @@ export type Child = | (() => Children) | Ref | Refkey + | Json | CustomContext | IntrinsicElement; diff --git a/packages/core/test/test.test.tsx b/packages/core/test/test.test.tsx new file mode 100644 index 000000000..f1bb3264e --- /dev/null +++ b/packages/core/test/test.test.tsx @@ -0,0 +1,28 @@ +import { Children, json } from "@alloy-js/core"; +import { expect, it } from "vitest"; + +const TestingJsonObject = (): Children => { + return json({ + id: "test-id", + name: "Test Name", + nested: , + }); +}; + +const Bar = (): Children => { + return json({ + bar: "Foo", + }); +}; + +it("works", () => { + expect(TestingJsonObject()).toRenderTo(` + { + "id": "test-id", + "name": "Test Name", + "nested": { + "bar": "Foo" + } + } + `); +}); diff --git a/packages/core/test/vitest.setup.ts b/packages/core/test/vitest.setup.ts new file mode 100644 index 000000000..96cd44191 --- /dev/null +++ b/packages/core/test/vitest.setup.ts @@ -0,0 +1 @@ +import "@alloy-js/core/testing"; diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index ba661c0a9..a98451981 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -6,5 +6,8 @@ export default defineConfig({ jsx: "preserve", sourcemap: "both", }, + test: { + setupFiles: ["./test/vitest.setup.ts"], + }, plugins: [alloyPlugin()], });