@fabricorg/experience-react 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e41c1f5: The handle a pack component receives was frozen, but the `events` and `bindings` arrays inside it were the render plan's own, by reference. `Object.freeze` is shallow, and those arrays are what the session's least-privilege check reads, so a component could push an event onto the plan and then invoke it. They are now frozen copies.
8
+
9
+ The conformance suite certified that hole shut, so it is tightened in the same release. `least-privilege-at-render-edge` now requires the lists to be frozen and not the plan's arrays. `experience-prop-is-sealed` now attempts the smuggle and requires it to reach neither the handle nor the plan. `missing-component-is-visible` now also supplies a `missingComponent` and requires the adapter to use it, since the option was declared and never exercised. The subject's `createElement` type is widened to accept a host element type, which React's own `createElement` already satisfies. An adapter that passed 0.2.0 by handing out live plan arrays, or by ignoring the caller's fallback, will not pass 0.2.1.
10
+
11
+ - Updated dependencies [e41c1f5]
12
+ - @fabricorg/experience-runtime@0.4.1
13
+
14
+ ## 0.2.0
15
+
16
+ ### Minor Changes
17
+
18
+ - f4790e6: Add the conformance suite the React adapter was documented as having. The readiness table called it a
19
+ reference adapter, replaceable "by passing its public conformance suite," and there was none.
20
+ `experienceReactChecks` certifies the properties that keep the render edge governed: every mutation
21
+ and read goes back through the session by fragment id and never by capability reference; a component
22
+ the plan names but no pack supplies is visibly reported rather than silently dropped; a component sees
23
+ only the events and bindings its own fragment declares; nothing the plan did not name is rendered;
24
+ and the handle a component is given is frozen so it cannot be rewired. Every check is headless — the
25
+ subject supplies its own render-to-string, so this package still does not depend on react-dom.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies [8436c64]
30
+ - @fabricorg/experience-runtime@0.4.0
31
+
3
32
  ## 0.1.2
4
33
 
5
34
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -20,10 +20,161 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // index.tsx
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- createExperienceReactRenderer: () => createExperienceReactRenderer
23
+ createExperienceReactRenderer: () => createExperienceReactRenderer,
24
+ experienceReactChecks: () => experienceReactChecks,
25
+ runExperienceReactChecks: () => runExperienceReactChecks
24
26
  });
25
27
  module.exports = __toCommonJS(index_exports);
26
28
  var import_react = require("react");
29
+
30
+ // conformance.ts
31
+ function fixturePlan() {
32
+ return {
33
+ formatVersion: 1,
34
+ planId: "plan-conformance",
35
+ application: "conformance",
36
+ channel: "test",
37
+ releaseDigest: "a".repeat(64),
38
+ assemblyDigest: "b".repeat(64),
39
+ treeId: "root",
40
+ root: {
41
+ id: "root",
42
+ component: "acme.card",
43
+ events: ["press"],
44
+ bindings: ["$data"],
45
+ children: [{ id: "orphan", component: "acme.nowhere" }]
46
+ },
47
+ tokenSet: { name: "reference", version: "1.0.0" },
48
+ expiresAt: "2099-01-01T00:00:00Z"
49
+ };
50
+ }
51
+ function recordingSession(plan) {
52
+ const invokes = [];
53
+ const reads = [];
54
+ const session = {
55
+ plan,
56
+ async invoke(fragmentId, eventName, parameters, idempotencyKey) {
57
+ invokes.push({ fragmentId, eventName });
58
+ return { planId: plan.planId, fragmentId, eventName, parameters, idempotencyKey };
59
+ },
60
+ async read(fragmentId, bindingName) {
61
+ reads.push({ fragmentId, bindingName });
62
+ return { data: null };
63
+ }
64
+ };
65
+ return { session, invokes, reads };
66
+ }
67
+ function fail(message) {
68
+ throw new Error(message);
69
+ }
70
+ function experienceReactChecks() {
71
+ return [
72
+ {
73
+ id: "fabric.experience-react.routes-through-session.v1",
74
+ async run(subject) {
75
+ const plan = fixturePlan();
76
+ const { session, invokes, reads } = recordingSession(plan);
77
+ let captured;
78
+ const Card = ({ experience }) => {
79
+ captured = experience;
80
+ return null;
81
+ };
82
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
83
+ subject.render(subject.createElement(Renderer, { session }));
84
+ if (!captured) fail("the component never received an experience handle");
85
+ await captured.invoke("press", { a: 1 });
86
+ await captured.read("$data");
87
+ if (invokes.length !== 1 || invokes[0]?.fragmentId !== "root" || invokes[0]?.eventName !== "press") {
88
+ fail(`invoke did not reach the session as (root, press); saw ${JSON.stringify(invokes)}`);
89
+ }
90
+ if (reads.length !== 1 || reads[0]?.fragmentId !== "root" || reads[0]?.bindingName !== "$data") {
91
+ fail(`read did not reach the session as (root, $data); saw ${JSON.stringify(reads)}`);
92
+ }
93
+ return ["invoke and read routed by fragment id through the session"];
94
+ }
95
+ },
96
+ {
97
+ id: "fabric.experience-react.missing-component-is-visible.v1",
98
+ async run(subject) {
99
+ const plan = fixturePlan();
100
+ const { session } = recordingSession(plan);
101
+ const Card = ({ children }) => children;
102
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
103
+ const markup = subject.render(subject.createElement(Renderer, { session }));
104
+ if (!/role="alert"/.test(markup)) fail('an unavailable component rendered nothing visible; expected a role="alert" marker');
105
+ if (!markup.includes("orphan")) fail("the missing-component marker does not identify the fragment it stands in for");
106
+ return ["unavailable component rendered as a visible alert naming its fragment"];
107
+ }
108
+ },
109
+ {
110
+ id: "fabric.experience-react.least-privilege-at-render-edge.v1",
111
+ async run(subject) {
112
+ const plan = fixturePlan();
113
+ const { session } = recordingSession(plan);
114
+ let captured;
115
+ const Card = ({ experience }) => {
116
+ captured = experience;
117
+ return null;
118
+ };
119
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
120
+ subject.render(subject.createElement(Renderer, { session }));
121
+ if (!captured) fail("the component never received an experience handle");
122
+ const events = [...captured.events].sort();
123
+ const bindings = [...captured.bindings].sort();
124
+ if (events.join(",") !== "press") fail(`component saw events [${events}]; its fragment declares only [press]`);
125
+ if (bindings.join(",") !== "$data") fail(`component saw bindings [${bindings}]; its fragment declares only [$data]`);
126
+ return ["component received exactly its fragment's events and bindings"];
127
+ }
128
+ },
129
+ {
130
+ id: "fabric.experience-react.no-invented-fragments.v1",
131
+ async run(subject) {
132
+ const plan = fixturePlan();
133
+ const { session } = recordingSession(plan);
134
+ let decoyRendered = false;
135
+ const Card = () => null;
136
+ const Decoy = () => {
137
+ decoyRendered = true;
138
+ return null;
139
+ };
140
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card, "acme.decoy": Decoy } });
141
+ subject.render(subject.createElement(Renderer, { session }));
142
+ if (decoyRendered) fail("a component the plan never named was rendered; the plan, not the registry, decides what appears");
143
+ return ["only plan-named fragments rendered"];
144
+ }
145
+ },
146
+ {
147
+ id: "fabric.experience-react.experience-prop-is-sealed.v1",
148
+ async run(subject) {
149
+ const plan = fixturePlan();
150
+ const { session } = recordingSession(plan);
151
+ let captured;
152
+ const Card = ({ experience }) => {
153
+ captured = experience;
154
+ return null;
155
+ };
156
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
157
+ subject.render(subject.createElement(Renderer, { session }));
158
+ if (!captured) fail("the component never received an experience handle");
159
+ if (!Object.isFrozen(captured)) fail("the experience handle is not frozen; a component could rewire invoke or read");
160
+ return ["experience handle is frozen"];
161
+ }
162
+ }
163
+ ];
164
+ }
165
+ async function runExperienceReactChecks(subject) {
166
+ const checks = [];
167
+ for (const check of experienceReactChecks()) {
168
+ try {
169
+ checks.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
170
+ } catch (error) {
171
+ checks.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
172
+ }
173
+ }
174
+ return { passed: checks.every((check) => check.status === "passed"), checks };
175
+ }
176
+
177
+ // index.tsx
27
178
  function createExperienceReactRenderer(options) {
28
179
  const Missing = options.missingComponent ?? DefaultMissingComponent;
29
180
  const Renderer = ({ session }) => renderFragment(session.plan.root, session, options.components, Missing);
@@ -56,6 +207,8 @@ function DefaultMissingComponent({ name, fragmentId }) {
56
207
  }
57
208
  // Annotate the CommonJS export names for ESM import in node:
58
209
  0 && (module.exports = {
59
- createExperienceReactRenderer
210
+ createExperienceReactRenderer,
211
+ experienceReactChecks,
212
+ runExperienceReactChecks
60
213
  });
61
214
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../index.tsx"],"sourcesContent":["import {\n\tFragment,\n\tcreateElement,\n\ttype ComponentType,\n\ttype ReactElement,\n\ttype ReactNode,\n} from \"react\";\nimport type {\n\tExperienceSession,\n\tRenderPlanFragment,\n} from \"@fabricorg/experience-runtime\";\n\nexport interface ExperienceComponentActions {\n\tevents: readonly string[];\n\tbindings: readonly string[];\n\tinvoke(eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;\n\tread(bindingName: string, parameters?: Record<string, unknown>): Promise<unknown>;\n}\n\nexport type ExperiencePackComponent = ComponentType<Record<string, unknown> & {\n\texperience: ExperienceComponentActions;\n\tchildren?: ReactNode;\n}>;\n\nexport interface ExperienceRendererProps {\n\tsession: ExperienceSession;\n}\n\nexport function createExperienceReactRenderer(options: {\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>;\n\tmissingComponent?: ComponentType<{ name: string; fragmentId: string }>;\n}): ComponentType<ExperienceRendererProps> {\n\tconst Missing = options.missingComponent ?? DefaultMissingComponent;\n\tconst Renderer = ({ session }: ExperienceRendererProps): ReactElement =>\n\t\trenderFragment(session.plan.root, session, options.components, Missing);\n\tRenderer.displayName = \"FabricExperienceRenderer\";\n\treturn Renderer;\n}\n\nfunction renderFragment(\n\tfragment: RenderPlanFragment,\n\tsession: ExperienceSession,\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>,\n\tMissing: ComponentType<{ name: string; fragmentId: string }>,\n): ReactElement {\n\tconst children = (fragment.children ?? []).map((child) =>\n\t\tcreateElement(Fragment, { key: child.id }, renderFragment(child, session, components, Missing)));\n\tif (fragment.component === undefined) return createElement(Fragment, null, ...children);\n\tconst Component = components[fragment.component];\n\tif (!Component) return createElement(Missing, { name: fragment.component, fragmentId: fragment.id });\n\tconst events = fragment.events ?? [];\n\tconst bindings = fragment.bindings ?? [];\n\treturn createElement(Component, {\n\t\t...(fragment.props ?? {}),\n\t\texperience: Object.freeze({\n\t\t\tevents,\n\t\t\tbindings,\n\t\t\tinvoke: (eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string) =>\n\t\t\t\tsession.invoke(fragment.id, eventName, parameters, idempotencyKey),\n\t\t\tread: (bindingName: string, parameters?: Record<string, unknown>) =>\n\t\t\t\tsession.read(fragment.id, bindingName, parameters),\n\t\t}),\n\t}, ...children);\n}\n\nfunction DefaultMissingComponent({ name, fragmentId }: { name: string; fragmentId: string }): ReactElement {\n\treturn createElement(\n\t\t\"div\",\n\t\t{ role: \"alert\", \"data-fabric-fragment\": fragmentId },\n\t\t`Component \"${name}\" is unavailable.`,\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAMO;AAsBA,SAAS,8BAA8B,SAGH;AAC1C,QAAM,UAAU,QAAQ,oBAAoB;AAC5C,QAAM,WAAW,CAAC,EAAE,QAAQ,MAC3B,eAAe,QAAQ,KAAK,MAAM,SAAS,QAAQ,YAAY,OAAO;AACvE,WAAS,cAAc;AACvB,SAAO;AACR;AAEA,SAAS,eACR,UACA,SACA,YACA,SACe;AACf,QAAM,YAAY,SAAS,YAAY,CAAC,GAAG,IAAI,CAAC,cAC/C,4BAAc,uBAAU,EAAE,KAAK,MAAM,GAAG,GAAG,eAAe,OAAO,SAAS,YAAY,OAAO,CAAC,CAAC;AAChG,MAAI,SAAS,cAAc,OAAW,YAAO,4BAAc,uBAAU,MAAM,GAAG,QAAQ;AACtF,QAAM,YAAY,WAAW,SAAS,SAAS;AAC/C,MAAI,CAAC,UAAW,YAAO,4BAAc,SAAS,EAAE,MAAM,SAAS,WAAW,YAAY,SAAS,GAAG,CAAC;AACnG,QAAM,SAAS,SAAS,UAAU,CAAC;AACnC,QAAM,WAAW,SAAS,YAAY,CAAC;AACvC,aAAO,4BAAc,WAAW;AAAA,IAC/B,GAAI,SAAS,SAAS,CAAC;AAAA,IACvB,YAAY,OAAO,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,MACA,QAAQ,CAAC,WAAmB,YAAqC,mBAChE,QAAQ,OAAO,SAAS,IAAI,WAAW,YAAY,cAAc;AAAA,MAClE,MAAM,CAAC,aAAqB,eAC3B,QAAQ,KAAK,SAAS,IAAI,aAAa,UAAU;AAAA,IACnD,CAAC;AAAA,EACF,GAAG,GAAG,QAAQ;AACf;AAEA,SAAS,wBAAwB,EAAE,MAAM,WAAW,GAAuD;AAC1G,aAAO;AAAA,IACN;AAAA,IACA,EAAE,MAAM,SAAS,wBAAwB,WAAW;AAAA,IACpD,cAAc,IAAI;AAAA,EACnB;AACD;","names":[]}
1
+ {"version":3,"sources":["../index.tsx","../conformance.ts"],"sourcesContent":["import {\n\tFragment,\n\tcreateElement,\n\ttype ComponentType,\n\ttype ReactElement,\n\ttype ReactNode,\n} from \"react\";\nimport type {\n\tExperienceSession,\n\tRenderPlanFragment,\n} from \"@fabricorg/experience-runtime\";\n\nexport interface ExperienceComponentActions {\n\tevents: readonly string[];\n\tbindings: readonly string[];\n\tinvoke(eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;\n\tread(bindingName: string, parameters?: Record<string, unknown>): Promise<unknown>;\n}\n\nexport type ExperiencePackComponent = ComponentType<Record<string, unknown> & {\n\texperience: ExperienceComponentActions;\n\tchildren?: ReactNode;\n}>;\n\nexport interface ExperienceRendererProps {\n\tsession: ExperienceSession;\n}\n\nexport function createExperienceReactRenderer(options: {\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>;\n\tmissingComponent?: ComponentType<{ name: string; fragmentId: string }>;\n}): ComponentType<ExperienceRendererProps> {\n\tconst Missing = options.missingComponent ?? DefaultMissingComponent;\n\tconst Renderer = ({ session }: ExperienceRendererProps): ReactElement =>\n\t\trenderFragment(session.plan.root, session, options.components, Missing);\n\tRenderer.displayName = \"FabricExperienceRenderer\";\n\treturn Renderer;\n}\n\nfunction renderFragment(\n\tfragment: RenderPlanFragment,\n\tsession: ExperienceSession,\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>,\n\tMissing: ComponentType<{ name: string; fragmentId: string }>,\n): ReactElement {\n\tconst children = (fragment.children ?? []).map((child) =>\n\t\tcreateElement(Fragment, { key: child.id }, renderFragment(child, session, components, Missing)));\n\tif (fragment.component === undefined) return createElement(Fragment, null, ...children);\n\tconst Component = components[fragment.component];\n\tif (!Component) return createElement(Missing, { name: fragment.component, fragmentId: fragment.id });\n\tconst events = fragment.events ?? [];\n\tconst bindings = fragment.bindings ?? [];\n\treturn createElement(Component, {\n\t\t...(fragment.props ?? {}),\n\t\texperience: Object.freeze({\n\t\t\tevents,\n\t\t\tbindings,\n\t\t\tinvoke: (eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string) =>\n\t\t\t\tsession.invoke(fragment.id, eventName, parameters, idempotencyKey),\n\t\t\tread: (bindingName: string, parameters?: Record<string, unknown>) =>\n\t\t\t\tsession.read(fragment.id, bindingName, parameters),\n\t\t}),\n\t}, ...children);\n}\n\nfunction DefaultMissingComponent({ name, fragmentId }: { name: string; fragmentId: string }): ReactElement {\n\treturn createElement(\n\t\t\"div\",\n\t\t{ role: \"alert\", \"data-fabric-fragment\": fragmentId },\n\t\t`Component \"${name}\" is unavailable.`,\n\t);\n}\n\n// The suite a replacement adapter must pass. Kept in its own module so this\n// file stays the reference implementation and nothing else.\nexport {\n\texperienceReactChecks,\n\trunExperienceReactChecks,\n\ttype ExperienceReactCheck,\n\ttype ExperienceReactConformanceResult,\n\ttype ExperienceReactConformanceSubject,\n} from \"./conformance\";\n","import type { ComponentType, ReactElement } from \"react\";\nimport type { ExperienceSession, ScopedRenderPlan } from \"@fabricorg/experience-runtime\";\nimport type { ExperienceComponentActions, ExperiencePackComponent, ExperienceRendererProps } from \"./index\";\n\n// ── React adapter conformance ───────────────────────────────────────────────\n//\n// The React adapter is a reference implementation, deliberately replaceable.\n// \"Replaceable by passing its public conformance suite\" was a claim the docs\n// made and the code did not keep: there was no suite. This is it. Every check\n// is headless. The subject supplies its own render-to-string, so this package\n// never depends on react-dom.\n\n/**\n * What a replacement adapter supplies to be certified.\n *\n * `render` turns an element into markup — `renderToStaticMarkup` from\n * `react-dom/server` is the obvious choice, but the suite does not care which.\n */\nexport interface ExperienceReactConformanceSubject {\n\tcreateRenderer(options: {\n\t\tcomponents: Readonly<Record<string, ExperiencePackComponent>>;\n\t\tmissingComponent?: ComponentType<{ name: string; fragmentId: string }>;\n\t}): ComponentType<ExperienceRendererProps>;\n\trender(element: ReactElement): string;\n\tcreateElement: (type: ComponentType<ExperienceRendererProps>, props: ExperienceRendererProps) => ReactElement;\n}\n\nexport interface ExperienceReactCheck {\n\tid:\n\t\t| \"fabric.experience-react.routes-through-session.v1\"\n\t\t| \"fabric.experience-react.missing-component-is-visible.v1\"\n\t\t| \"fabric.experience-react.least-privilege-at-render-edge.v1\"\n\t\t| \"fabric.experience-react.no-invented-fragments.v1\"\n\t\t| \"fabric.experience-react.experience-prop-is-sealed.v1\";\n\trun(subject: ExperienceReactConformanceSubject): Promise<string[]>;\n}\n\nexport interface ExperienceReactConformanceResult {\n\tpassed: boolean;\n\tchecks: Array<{ id: ExperienceReactCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }>;\n}\n\n/** A two-fragment plan: a known component with declared events, and a child no pack supplies. */\nfunction fixturePlan(): ScopedRenderPlan {\n\treturn {\n\t\tformatVersion: 1,\n\t\tplanId: \"plan-conformance\",\n\t\tapplication: \"conformance\",\n\t\tchannel: \"test\",\n\t\treleaseDigest: \"a\".repeat(64),\n\t\tassemblyDigest: \"b\".repeat(64),\n\t\ttreeId: \"root\",\n\t\troot: {\n\t\t\tid: \"root\",\n\t\t\tcomponent: \"acme.card\",\n\t\t\tevents: [\"press\"],\n\t\t\tbindings: [\"$data\"],\n\t\t\tchildren: [{ id: \"orphan\", component: \"acme.nowhere\" }],\n\t\t},\n\t\ttokenSet: { name: \"reference\", version: \"1.0.0\" },\n\t\texpiresAt: \"2099-01-01T00:00:00Z\",\n\t} as ScopedRenderPlan;\n}\n\n/** A session that records every call so routing can be asserted, and never resolves a capability reference. */\nfunction recordingSession(plan: ScopedRenderPlan) {\n\tconst invokes: Array<{ fragmentId: string; eventName: string }> = [];\n\tconst reads: Array<{ fragmentId: string; bindingName: string }> = [];\n\tconst session: ExperienceSession = {\n\t\tplan,\n\t\tasync invoke(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tinvokes.push({ fragmentId, eventName });\n\t\t\treturn { planId: plan.planId, fragmentId, eventName, parameters, idempotencyKey };\n\t\t},\n\t\tasync read(fragmentId, bindingName) {\n\t\t\treads.push({ fragmentId, bindingName });\n\t\t\treturn { data: null };\n\t\t},\n\t};\n\treturn { session, invokes, reads };\n}\n\nfunction fail(message: string): never {\n\tthrow new Error(message);\n}\n\n/**\n * The suite any React adapter must pass.\n *\n * It certifies the properties that keep the render edge governed: every\n * mutation and read goes back through the session by fragment id and never by\n * capability reference; a component the plan names but no pack supplies is\n * visibly reported rather than silently dropped; a component sees only the\n * events and bindings its own fragment declares; nothing the plan did not name\n * is rendered; and the handle a component is given cannot be rewired.\n */\nexport function experienceReactChecks(): readonly ExperienceReactCheck[] {\n\treturn [\n\t\t{\n\t\t\tid: \"fabric.experience-react.routes-through-session.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session, invokes, reads } = recordingSession(plan);\n\t\t\t\tlet captured: ExperienceComponentActions | undefined;\n\t\t\t\tconst Card: ExperiencePackComponent = ({ experience }) => {\n\t\t\t\t\tcaptured = experience;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (!captured) fail(\"the component never received an experience handle\");\n\t\t\t\tawait captured.invoke(\"press\", { a: 1 });\n\t\t\t\tawait captured.read(\"$data\");\n\t\t\t\tif (invokes.length !== 1 || invokes[0]?.fragmentId !== \"root\" || invokes[0]?.eventName !== \"press\") {\n\t\t\t\t\tfail(`invoke did not reach the session as (root, press); saw ${JSON.stringify(invokes)}`);\n\t\t\t\t}\n\t\t\t\tif (reads.length !== 1 || reads[0]?.fragmentId !== \"root\" || reads[0]?.bindingName !== \"$data\") {\n\t\t\t\t\tfail(`read did not reach the session as (root, $data); saw ${JSON.stringify(reads)}`);\n\t\t\t\t}\n\t\t\t\treturn [\"invoke and read routed by fragment id through the session\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.missing-component-is-visible.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tconst Card: ExperiencePackComponent = ({ children }) => children as ReactElement;\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tconst markup = subject.render(subject.createElement(Renderer, { session }));\n\t\t\t\t// A subtree the plan named but no pack supplies must be reported\n\t\t\t\t// where a person will see it. An empty region is indistinguishable\n\t\t\t\t// from \"there was nothing here\", which is the wrong thing to tell\n\t\t\t\t// someone whose screen just lost a section.\n\t\t\t\tif (!/role=\"alert\"/.test(markup)) fail(\"an unavailable component rendered nothing visible; expected a role=\\\"alert\\\" marker\");\n\t\t\t\tif (!markup.includes(\"orphan\")) fail(\"the missing-component marker does not identify the fragment it stands in for\");\n\t\t\t\treturn [\"unavailable component rendered as a visible alert naming its fragment\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.least-privilege-at-render-edge.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tlet captured: ExperienceComponentActions | undefined;\n\t\t\t\tconst Card: ExperiencePackComponent = ({ experience }) => {\n\t\t\t\t\tcaptured = experience;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (!captured) fail(\"the component never received an experience handle\");\n\t\t\t\tconst events = [...captured.events].sort();\n\t\t\t\tconst bindings = [...captured.bindings].sort();\n\t\t\t\tif (events.join(\",\") !== \"press\") fail(`component saw events [${events}]; its fragment declares only [press]`);\n\t\t\t\tif (bindings.join(\",\") !== \"$data\") fail(`component saw bindings [${bindings}]; its fragment declares only [$data]`);\n\t\t\t\treturn [\"component received exactly its fragment's events and bindings\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.no-invented-fragments.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tlet decoyRendered = false;\n\t\t\t\tconst Card: ExperiencePackComponent = () => null;\n\t\t\t\tconst Decoy: ExperiencePackComponent = () => {\n\t\t\t\t\tdecoyRendered = true;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\t// The registry offers a component the plan never names. A renderer\n\t\t\t\t// that reaches into the registry rather than following the plan\n\t\t\t\t// would render it.\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card, \"acme.decoy\": Decoy } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (decoyRendered) fail(\"a component the plan never named was rendered; the plan, not the registry, decides what appears\");\n\t\t\t\treturn [\"only plan-named fragments rendered\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.experience-prop-is-sealed.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tlet captured: ExperienceComponentActions | undefined;\n\t\t\t\tconst Card: ExperiencePackComponent = ({ experience }) => {\n\t\t\t\t\tcaptured = experience;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (!captured) fail(\"the component never received an experience handle\");\n\t\t\t\t// A component that could reassign invoke could route around the\n\t\t\t\t// session. Sealing the handle is cheap and closes it.\n\t\t\t\tif (!Object.isFrozen(captured)) fail(\"the experience handle is not frozen; a component could rewire invoke or read\");\n\t\t\t\treturn [\"experience handle is frozen\"];\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** Run every check, returning each result rather than throwing on the first. */\nexport async function runExperienceReactChecks(\n\tsubject: ExperienceReactConformanceSubject,\n): Promise<ExperienceReactConformanceResult> {\n\tconst checks: ExperienceReactConformanceResult[\"checks\"] = [];\n\tfor (const check of experienceReactChecks()) {\n\t\ttry {\n\t\t\tchecks.push({ id: check.id, status: \"passed\", evidence: await check.run(subject) });\n\t\t} catch (error) {\n\t\t\tchecks.push({ id: check.id, status: \"failed\", evidence: [], error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { passed: checks.every((check) => check.status === \"passed\"), checks };\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,50 @@
1
- import { ComponentType, ReactNode } from 'react';
1
+ import { ComponentType, ReactElement, ReactNode } from 'react';
2
2
  import { ExperienceSession } from '@fabricorg/experience-runtime';
3
3
 
4
+ /**
5
+ * What a replacement adapter supplies to be certified.
6
+ *
7
+ * `render` turns an element into markup — `renderToStaticMarkup` from
8
+ * `react-dom/server` is the obvious choice, but the suite does not care which.
9
+ */
10
+ interface ExperienceReactConformanceSubject {
11
+ createRenderer(options: {
12
+ components: Readonly<Record<string, ExperiencePackComponent>>;
13
+ missingComponent?: ComponentType<{
14
+ name: string;
15
+ fragmentId: string;
16
+ }>;
17
+ }): ComponentType<ExperienceRendererProps>;
18
+ render(element: ReactElement): string;
19
+ createElement: (type: ComponentType<ExperienceRendererProps>, props: ExperienceRendererProps) => ReactElement;
20
+ }
21
+ interface ExperienceReactCheck {
22
+ id: "fabric.experience-react.routes-through-session.v1" | "fabric.experience-react.missing-component-is-visible.v1" | "fabric.experience-react.least-privilege-at-render-edge.v1" | "fabric.experience-react.no-invented-fragments.v1" | "fabric.experience-react.experience-prop-is-sealed.v1";
23
+ run(subject: ExperienceReactConformanceSubject): Promise<string[]>;
24
+ }
25
+ interface ExperienceReactConformanceResult {
26
+ passed: boolean;
27
+ checks: Array<{
28
+ id: ExperienceReactCheck["id"];
29
+ status: "passed" | "failed";
30
+ evidence: string[];
31
+ error?: string;
32
+ }>;
33
+ }
34
+ /**
35
+ * The suite any React adapter must pass.
36
+ *
37
+ * It certifies the properties that keep the render edge governed: every
38
+ * mutation and read goes back through the session by fragment id and never by
39
+ * capability reference; a component the plan names but no pack supplies is
40
+ * visibly reported rather than silently dropped; a component sees only the
41
+ * events and bindings its own fragment declares; nothing the plan did not name
42
+ * is rendered; and the handle a component is given cannot be rewired.
43
+ */
44
+ declare function experienceReactChecks(): readonly ExperienceReactCheck[];
45
+ /** Run every check, returning each result rather than throwing on the first. */
46
+ declare function runExperienceReactChecks(subject: ExperienceReactConformanceSubject): Promise<ExperienceReactConformanceResult>;
47
+
4
48
  interface ExperienceComponentActions {
5
49
  events: readonly string[];
6
50
  bindings: readonly string[];
@@ -22,4 +66,4 @@ declare function createExperienceReactRenderer(options: {
22
66
  }>;
23
67
  }): ComponentType<ExperienceRendererProps>;
24
68
 
25
- export { type ExperienceComponentActions, type ExperiencePackComponent, type ExperienceRendererProps, createExperienceReactRenderer };
69
+ export { type ExperienceComponentActions, type ExperiencePackComponent, type ExperienceReactCheck, type ExperienceReactConformanceResult, type ExperienceReactConformanceSubject, type ExperienceRendererProps, createExperienceReactRenderer, experienceReactChecks, runExperienceReactChecks };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,50 @@
1
- import { ComponentType, ReactNode } from 'react';
1
+ import { ComponentType, ReactElement, ReactNode } from 'react';
2
2
  import { ExperienceSession } from '@fabricorg/experience-runtime';
3
3
 
4
+ /**
5
+ * What a replacement adapter supplies to be certified.
6
+ *
7
+ * `render` turns an element into markup — `renderToStaticMarkup` from
8
+ * `react-dom/server` is the obvious choice, but the suite does not care which.
9
+ */
10
+ interface ExperienceReactConformanceSubject {
11
+ createRenderer(options: {
12
+ components: Readonly<Record<string, ExperiencePackComponent>>;
13
+ missingComponent?: ComponentType<{
14
+ name: string;
15
+ fragmentId: string;
16
+ }>;
17
+ }): ComponentType<ExperienceRendererProps>;
18
+ render(element: ReactElement): string;
19
+ createElement: (type: ComponentType<ExperienceRendererProps>, props: ExperienceRendererProps) => ReactElement;
20
+ }
21
+ interface ExperienceReactCheck {
22
+ id: "fabric.experience-react.routes-through-session.v1" | "fabric.experience-react.missing-component-is-visible.v1" | "fabric.experience-react.least-privilege-at-render-edge.v1" | "fabric.experience-react.no-invented-fragments.v1" | "fabric.experience-react.experience-prop-is-sealed.v1";
23
+ run(subject: ExperienceReactConformanceSubject): Promise<string[]>;
24
+ }
25
+ interface ExperienceReactConformanceResult {
26
+ passed: boolean;
27
+ checks: Array<{
28
+ id: ExperienceReactCheck["id"];
29
+ status: "passed" | "failed";
30
+ evidence: string[];
31
+ error?: string;
32
+ }>;
33
+ }
34
+ /**
35
+ * The suite any React adapter must pass.
36
+ *
37
+ * It certifies the properties that keep the render edge governed: every
38
+ * mutation and read goes back through the session by fragment id and never by
39
+ * capability reference; a component the plan names but no pack supplies is
40
+ * visibly reported rather than silently dropped; a component sees only the
41
+ * events and bindings its own fragment declares; nothing the plan did not name
42
+ * is rendered; and the handle a component is given cannot be rewired.
43
+ */
44
+ declare function experienceReactChecks(): readonly ExperienceReactCheck[];
45
+ /** Run every check, returning each result rather than throwing on the first. */
46
+ declare function runExperienceReactChecks(subject: ExperienceReactConformanceSubject): Promise<ExperienceReactConformanceResult>;
47
+
4
48
  interface ExperienceComponentActions {
5
49
  events: readonly string[];
6
50
  bindings: readonly string[];
@@ -22,4 +66,4 @@ declare function createExperienceReactRenderer(options: {
22
66
  }>;
23
67
  }): ComponentType<ExperienceRendererProps>;
24
68
 
25
- export { type ExperienceComponentActions, type ExperiencePackComponent, type ExperienceRendererProps, createExperienceReactRenderer };
69
+ export { type ExperienceComponentActions, type ExperiencePackComponent, type ExperienceReactCheck, type ExperienceReactConformanceResult, type ExperienceReactConformanceSubject, type ExperienceRendererProps, createExperienceReactRenderer, experienceReactChecks, runExperienceReactChecks };
package/dist/index.js CHANGED
@@ -3,6 +3,155 @@ import {
3
3
  Fragment,
4
4
  createElement
5
5
  } from "react";
6
+
7
+ // conformance.ts
8
+ function fixturePlan() {
9
+ return {
10
+ formatVersion: 1,
11
+ planId: "plan-conformance",
12
+ application: "conformance",
13
+ channel: "test",
14
+ releaseDigest: "a".repeat(64),
15
+ assemblyDigest: "b".repeat(64),
16
+ treeId: "root",
17
+ root: {
18
+ id: "root",
19
+ component: "acme.card",
20
+ events: ["press"],
21
+ bindings: ["$data"],
22
+ children: [{ id: "orphan", component: "acme.nowhere" }]
23
+ },
24
+ tokenSet: { name: "reference", version: "1.0.0" },
25
+ expiresAt: "2099-01-01T00:00:00Z"
26
+ };
27
+ }
28
+ function recordingSession(plan) {
29
+ const invokes = [];
30
+ const reads = [];
31
+ const session = {
32
+ plan,
33
+ async invoke(fragmentId, eventName, parameters, idempotencyKey) {
34
+ invokes.push({ fragmentId, eventName });
35
+ return { planId: plan.planId, fragmentId, eventName, parameters, idempotencyKey };
36
+ },
37
+ async read(fragmentId, bindingName) {
38
+ reads.push({ fragmentId, bindingName });
39
+ return { data: null };
40
+ }
41
+ };
42
+ return { session, invokes, reads };
43
+ }
44
+ function fail(message) {
45
+ throw new Error(message);
46
+ }
47
+ function experienceReactChecks() {
48
+ return [
49
+ {
50
+ id: "fabric.experience-react.routes-through-session.v1",
51
+ async run(subject) {
52
+ const plan = fixturePlan();
53
+ const { session, invokes, reads } = recordingSession(plan);
54
+ let captured;
55
+ const Card = ({ experience }) => {
56
+ captured = experience;
57
+ return null;
58
+ };
59
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
60
+ subject.render(subject.createElement(Renderer, { session }));
61
+ if (!captured) fail("the component never received an experience handle");
62
+ await captured.invoke("press", { a: 1 });
63
+ await captured.read("$data");
64
+ if (invokes.length !== 1 || invokes[0]?.fragmentId !== "root" || invokes[0]?.eventName !== "press") {
65
+ fail(`invoke did not reach the session as (root, press); saw ${JSON.stringify(invokes)}`);
66
+ }
67
+ if (reads.length !== 1 || reads[0]?.fragmentId !== "root" || reads[0]?.bindingName !== "$data") {
68
+ fail(`read did not reach the session as (root, $data); saw ${JSON.stringify(reads)}`);
69
+ }
70
+ return ["invoke and read routed by fragment id through the session"];
71
+ }
72
+ },
73
+ {
74
+ id: "fabric.experience-react.missing-component-is-visible.v1",
75
+ async run(subject) {
76
+ const plan = fixturePlan();
77
+ const { session } = recordingSession(plan);
78
+ const Card = ({ children }) => children;
79
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
80
+ const markup = subject.render(subject.createElement(Renderer, { session }));
81
+ if (!/role="alert"/.test(markup)) fail('an unavailable component rendered nothing visible; expected a role="alert" marker');
82
+ if (!markup.includes("orphan")) fail("the missing-component marker does not identify the fragment it stands in for");
83
+ return ["unavailable component rendered as a visible alert naming its fragment"];
84
+ }
85
+ },
86
+ {
87
+ id: "fabric.experience-react.least-privilege-at-render-edge.v1",
88
+ async run(subject) {
89
+ const plan = fixturePlan();
90
+ const { session } = recordingSession(plan);
91
+ let captured;
92
+ const Card = ({ experience }) => {
93
+ captured = experience;
94
+ return null;
95
+ };
96
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
97
+ subject.render(subject.createElement(Renderer, { session }));
98
+ if (!captured) fail("the component never received an experience handle");
99
+ const events = [...captured.events].sort();
100
+ const bindings = [...captured.bindings].sort();
101
+ if (events.join(",") !== "press") fail(`component saw events [${events}]; its fragment declares only [press]`);
102
+ if (bindings.join(",") !== "$data") fail(`component saw bindings [${bindings}]; its fragment declares only [$data]`);
103
+ return ["component received exactly its fragment's events and bindings"];
104
+ }
105
+ },
106
+ {
107
+ id: "fabric.experience-react.no-invented-fragments.v1",
108
+ async run(subject) {
109
+ const plan = fixturePlan();
110
+ const { session } = recordingSession(plan);
111
+ let decoyRendered = false;
112
+ const Card = () => null;
113
+ const Decoy = () => {
114
+ decoyRendered = true;
115
+ return null;
116
+ };
117
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card, "acme.decoy": Decoy } });
118
+ subject.render(subject.createElement(Renderer, { session }));
119
+ if (decoyRendered) fail("a component the plan never named was rendered; the plan, not the registry, decides what appears");
120
+ return ["only plan-named fragments rendered"];
121
+ }
122
+ },
123
+ {
124
+ id: "fabric.experience-react.experience-prop-is-sealed.v1",
125
+ async run(subject) {
126
+ const plan = fixturePlan();
127
+ const { session } = recordingSession(plan);
128
+ let captured;
129
+ const Card = ({ experience }) => {
130
+ captured = experience;
131
+ return null;
132
+ };
133
+ const Renderer = subject.createRenderer({ components: { "acme.card": Card } });
134
+ subject.render(subject.createElement(Renderer, { session }));
135
+ if (!captured) fail("the component never received an experience handle");
136
+ if (!Object.isFrozen(captured)) fail("the experience handle is not frozen; a component could rewire invoke or read");
137
+ return ["experience handle is frozen"];
138
+ }
139
+ }
140
+ ];
141
+ }
142
+ async function runExperienceReactChecks(subject) {
143
+ const checks = [];
144
+ for (const check of experienceReactChecks()) {
145
+ try {
146
+ checks.push({ id: check.id, status: "passed", evidence: await check.run(subject) });
147
+ } catch (error) {
148
+ checks.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
149
+ }
150
+ }
151
+ return { passed: checks.every((check) => check.status === "passed"), checks };
152
+ }
153
+
154
+ // index.tsx
6
155
  function createExperienceReactRenderer(options) {
7
156
  const Missing = options.missingComponent ?? DefaultMissingComponent;
8
157
  const Renderer = ({ session }) => renderFragment(session.plan.root, session, options.components, Missing);
@@ -34,6 +183,8 @@ function DefaultMissingComponent({ name, fragmentId }) {
34
183
  );
35
184
  }
36
185
  export {
37
- createExperienceReactRenderer
186
+ createExperienceReactRenderer,
187
+ experienceReactChecks,
188
+ runExperienceReactChecks
38
189
  };
39
190
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../index.tsx"],"sourcesContent":["import {\n\tFragment,\n\tcreateElement,\n\ttype ComponentType,\n\ttype ReactElement,\n\ttype ReactNode,\n} from \"react\";\nimport type {\n\tExperienceSession,\n\tRenderPlanFragment,\n} from \"@fabricorg/experience-runtime\";\n\nexport interface ExperienceComponentActions {\n\tevents: readonly string[];\n\tbindings: readonly string[];\n\tinvoke(eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;\n\tread(bindingName: string, parameters?: Record<string, unknown>): Promise<unknown>;\n}\n\nexport type ExperiencePackComponent = ComponentType<Record<string, unknown> & {\n\texperience: ExperienceComponentActions;\n\tchildren?: ReactNode;\n}>;\n\nexport interface ExperienceRendererProps {\n\tsession: ExperienceSession;\n}\n\nexport function createExperienceReactRenderer(options: {\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>;\n\tmissingComponent?: ComponentType<{ name: string; fragmentId: string }>;\n}): ComponentType<ExperienceRendererProps> {\n\tconst Missing = options.missingComponent ?? DefaultMissingComponent;\n\tconst Renderer = ({ session }: ExperienceRendererProps): ReactElement =>\n\t\trenderFragment(session.plan.root, session, options.components, Missing);\n\tRenderer.displayName = \"FabricExperienceRenderer\";\n\treturn Renderer;\n}\n\nfunction renderFragment(\n\tfragment: RenderPlanFragment,\n\tsession: ExperienceSession,\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>,\n\tMissing: ComponentType<{ name: string; fragmentId: string }>,\n): ReactElement {\n\tconst children = (fragment.children ?? []).map((child) =>\n\t\tcreateElement(Fragment, { key: child.id }, renderFragment(child, session, components, Missing)));\n\tif (fragment.component === undefined) return createElement(Fragment, null, ...children);\n\tconst Component = components[fragment.component];\n\tif (!Component) return createElement(Missing, { name: fragment.component, fragmentId: fragment.id });\n\tconst events = fragment.events ?? [];\n\tconst bindings = fragment.bindings ?? [];\n\treturn createElement(Component, {\n\t\t...(fragment.props ?? {}),\n\t\texperience: Object.freeze({\n\t\t\tevents,\n\t\t\tbindings,\n\t\t\tinvoke: (eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string) =>\n\t\t\t\tsession.invoke(fragment.id, eventName, parameters, idempotencyKey),\n\t\t\tread: (bindingName: string, parameters?: Record<string, unknown>) =>\n\t\t\t\tsession.read(fragment.id, bindingName, parameters),\n\t\t}),\n\t}, ...children);\n}\n\nfunction DefaultMissingComponent({ name, fragmentId }: { name: string; fragmentId: string }): ReactElement {\n\treturn createElement(\n\t\t\"div\",\n\t\t{ role: \"alert\", \"data-fabric-fragment\": fragmentId },\n\t\t`Component \"${name}\" is unavailable.`,\n\t);\n}\n"],"mappings":";AAAA;AAAA,EACC;AAAA,EACA;AAAA,OAIM;AAsBA,SAAS,8BAA8B,SAGH;AAC1C,QAAM,UAAU,QAAQ,oBAAoB;AAC5C,QAAM,WAAW,CAAC,EAAE,QAAQ,MAC3B,eAAe,QAAQ,KAAK,MAAM,SAAS,QAAQ,YAAY,OAAO;AACvE,WAAS,cAAc;AACvB,SAAO;AACR;AAEA,SAAS,eACR,UACA,SACA,YACA,SACe;AACf,QAAM,YAAY,SAAS,YAAY,CAAC,GAAG,IAAI,CAAC,UAC/C,cAAc,UAAU,EAAE,KAAK,MAAM,GAAG,GAAG,eAAe,OAAO,SAAS,YAAY,OAAO,CAAC,CAAC;AAChG,MAAI,SAAS,cAAc,OAAW,QAAO,cAAc,UAAU,MAAM,GAAG,QAAQ;AACtF,QAAM,YAAY,WAAW,SAAS,SAAS;AAC/C,MAAI,CAAC,UAAW,QAAO,cAAc,SAAS,EAAE,MAAM,SAAS,WAAW,YAAY,SAAS,GAAG,CAAC;AACnG,QAAM,SAAS,SAAS,UAAU,CAAC;AACnC,QAAM,WAAW,SAAS,YAAY,CAAC;AACvC,SAAO,cAAc,WAAW;AAAA,IAC/B,GAAI,SAAS,SAAS,CAAC;AAAA,IACvB,YAAY,OAAO,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,MACA,QAAQ,CAAC,WAAmB,YAAqC,mBAChE,QAAQ,OAAO,SAAS,IAAI,WAAW,YAAY,cAAc;AAAA,MAClE,MAAM,CAAC,aAAqB,eAC3B,QAAQ,KAAK,SAAS,IAAI,aAAa,UAAU;AAAA,IACnD,CAAC;AAAA,EACF,GAAG,GAAG,QAAQ;AACf;AAEA,SAAS,wBAAwB,EAAE,MAAM,WAAW,GAAuD;AAC1G,SAAO;AAAA,IACN;AAAA,IACA,EAAE,MAAM,SAAS,wBAAwB,WAAW;AAAA,IACpD,cAAc,IAAI;AAAA,EACnB;AACD;","names":[]}
1
+ {"version":3,"sources":["../index.tsx","../conformance.ts"],"sourcesContent":["import {\n\tFragment,\n\tcreateElement,\n\ttype ComponentType,\n\ttype ReactElement,\n\ttype ReactNode,\n} from \"react\";\nimport type {\n\tExperienceSession,\n\tRenderPlanFragment,\n} from \"@fabricorg/experience-runtime\";\n\nexport interface ExperienceComponentActions {\n\tevents: readonly string[];\n\tbindings: readonly string[];\n\tinvoke(eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;\n\tread(bindingName: string, parameters?: Record<string, unknown>): Promise<unknown>;\n}\n\nexport type ExperiencePackComponent = ComponentType<Record<string, unknown> & {\n\texperience: ExperienceComponentActions;\n\tchildren?: ReactNode;\n}>;\n\nexport interface ExperienceRendererProps {\n\tsession: ExperienceSession;\n}\n\nexport function createExperienceReactRenderer(options: {\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>;\n\tmissingComponent?: ComponentType<{ name: string; fragmentId: string }>;\n}): ComponentType<ExperienceRendererProps> {\n\tconst Missing = options.missingComponent ?? DefaultMissingComponent;\n\tconst Renderer = ({ session }: ExperienceRendererProps): ReactElement =>\n\t\trenderFragment(session.plan.root, session, options.components, Missing);\n\tRenderer.displayName = \"FabricExperienceRenderer\";\n\treturn Renderer;\n}\n\nfunction renderFragment(\n\tfragment: RenderPlanFragment,\n\tsession: ExperienceSession,\n\tcomponents: Readonly<Record<string, ExperiencePackComponent>>,\n\tMissing: ComponentType<{ name: string; fragmentId: string }>,\n): ReactElement {\n\tconst children = (fragment.children ?? []).map((child) =>\n\t\tcreateElement(Fragment, { key: child.id }, renderFragment(child, session, components, Missing)));\n\tif (fragment.component === undefined) return createElement(Fragment, null, ...children);\n\tconst Component = components[fragment.component];\n\tif (!Component) return createElement(Missing, { name: fragment.component, fragmentId: fragment.id });\n\tconst events = fragment.events ?? [];\n\tconst bindings = fragment.bindings ?? [];\n\treturn createElement(Component, {\n\t\t...(fragment.props ?? {}),\n\t\texperience: Object.freeze({\n\t\t\tevents,\n\t\t\tbindings,\n\t\t\tinvoke: (eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string) =>\n\t\t\t\tsession.invoke(fragment.id, eventName, parameters, idempotencyKey),\n\t\t\tread: (bindingName: string, parameters?: Record<string, unknown>) =>\n\t\t\t\tsession.read(fragment.id, bindingName, parameters),\n\t\t}),\n\t}, ...children);\n}\n\nfunction DefaultMissingComponent({ name, fragmentId }: { name: string; fragmentId: string }): ReactElement {\n\treturn createElement(\n\t\t\"div\",\n\t\t{ role: \"alert\", \"data-fabric-fragment\": fragmentId },\n\t\t`Component \"${name}\" is unavailable.`,\n\t);\n}\n\n// The suite a replacement adapter must pass. Kept in its own module so this\n// file stays the reference implementation and nothing else.\nexport {\n\texperienceReactChecks,\n\trunExperienceReactChecks,\n\ttype ExperienceReactCheck,\n\ttype ExperienceReactConformanceResult,\n\ttype ExperienceReactConformanceSubject,\n} from \"./conformance\";\n","import type { ComponentType, ReactElement } from \"react\";\nimport type { ExperienceSession, ScopedRenderPlan } from \"@fabricorg/experience-runtime\";\nimport type { ExperienceComponentActions, ExperiencePackComponent, ExperienceRendererProps } from \"./index\";\n\n// ── React adapter conformance ───────────────────────────────────────────────\n//\n// The React adapter is a reference implementation, deliberately replaceable.\n// \"Replaceable by passing its public conformance suite\" was a claim the docs\n// made and the code did not keep: there was no suite. This is it. Every check\n// is headless. The subject supplies its own render-to-string, so this package\n// never depends on react-dom.\n\n/**\n * What a replacement adapter supplies to be certified.\n *\n * `render` turns an element into markup — `renderToStaticMarkup` from\n * `react-dom/server` is the obvious choice, but the suite does not care which.\n */\nexport interface ExperienceReactConformanceSubject {\n\tcreateRenderer(options: {\n\t\tcomponents: Readonly<Record<string, ExperiencePackComponent>>;\n\t\tmissingComponent?: ComponentType<{ name: string; fragmentId: string }>;\n\t}): ComponentType<ExperienceRendererProps>;\n\trender(element: ReactElement): string;\n\tcreateElement: (type: ComponentType<ExperienceRendererProps>, props: ExperienceRendererProps) => ReactElement;\n}\n\nexport interface ExperienceReactCheck {\n\tid:\n\t\t| \"fabric.experience-react.routes-through-session.v1\"\n\t\t| \"fabric.experience-react.missing-component-is-visible.v1\"\n\t\t| \"fabric.experience-react.least-privilege-at-render-edge.v1\"\n\t\t| \"fabric.experience-react.no-invented-fragments.v1\"\n\t\t| \"fabric.experience-react.experience-prop-is-sealed.v1\";\n\trun(subject: ExperienceReactConformanceSubject): Promise<string[]>;\n}\n\nexport interface ExperienceReactConformanceResult {\n\tpassed: boolean;\n\tchecks: Array<{ id: ExperienceReactCheck[\"id\"]; status: \"passed\" | \"failed\"; evidence: string[]; error?: string }>;\n}\n\n/** A two-fragment plan: a known component with declared events, and a child no pack supplies. */\nfunction fixturePlan(): ScopedRenderPlan {\n\treturn {\n\t\tformatVersion: 1,\n\t\tplanId: \"plan-conformance\",\n\t\tapplication: \"conformance\",\n\t\tchannel: \"test\",\n\t\treleaseDigest: \"a\".repeat(64),\n\t\tassemblyDigest: \"b\".repeat(64),\n\t\ttreeId: \"root\",\n\t\troot: {\n\t\t\tid: \"root\",\n\t\t\tcomponent: \"acme.card\",\n\t\t\tevents: [\"press\"],\n\t\t\tbindings: [\"$data\"],\n\t\t\tchildren: [{ id: \"orphan\", component: \"acme.nowhere\" }],\n\t\t},\n\t\ttokenSet: { name: \"reference\", version: \"1.0.0\" },\n\t\texpiresAt: \"2099-01-01T00:00:00Z\",\n\t} as ScopedRenderPlan;\n}\n\n/** A session that records every call so routing can be asserted, and never resolves a capability reference. */\nfunction recordingSession(plan: ScopedRenderPlan) {\n\tconst invokes: Array<{ fragmentId: string; eventName: string }> = [];\n\tconst reads: Array<{ fragmentId: string; bindingName: string }> = [];\n\tconst session: ExperienceSession = {\n\t\tplan,\n\t\tasync invoke(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tinvokes.push({ fragmentId, eventName });\n\t\t\treturn { planId: plan.planId, fragmentId, eventName, parameters, idempotencyKey };\n\t\t},\n\t\tasync read(fragmentId, bindingName) {\n\t\t\treads.push({ fragmentId, bindingName });\n\t\t\treturn { data: null };\n\t\t},\n\t};\n\treturn { session, invokes, reads };\n}\n\nfunction fail(message: string): never {\n\tthrow new Error(message);\n}\n\n/**\n * The suite any React adapter must pass.\n *\n * It certifies the properties that keep the render edge governed: every\n * mutation and read goes back through the session by fragment id and never by\n * capability reference; a component the plan names but no pack supplies is\n * visibly reported rather than silently dropped; a component sees only the\n * events and bindings its own fragment declares; nothing the plan did not name\n * is rendered; and the handle a component is given cannot be rewired.\n */\nexport function experienceReactChecks(): readonly ExperienceReactCheck[] {\n\treturn [\n\t\t{\n\t\t\tid: \"fabric.experience-react.routes-through-session.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session, invokes, reads } = recordingSession(plan);\n\t\t\t\tlet captured: ExperienceComponentActions | undefined;\n\t\t\t\tconst Card: ExperiencePackComponent = ({ experience }) => {\n\t\t\t\t\tcaptured = experience;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (!captured) fail(\"the component never received an experience handle\");\n\t\t\t\tawait captured.invoke(\"press\", { a: 1 });\n\t\t\t\tawait captured.read(\"$data\");\n\t\t\t\tif (invokes.length !== 1 || invokes[0]?.fragmentId !== \"root\" || invokes[0]?.eventName !== \"press\") {\n\t\t\t\t\tfail(`invoke did not reach the session as (root, press); saw ${JSON.stringify(invokes)}`);\n\t\t\t\t}\n\t\t\t\tif (reads.length !== 1 || reads[0]?.fragmentId !== \"root\" || reads[0]?.bindingName !== \"$data\") {\n\t\t\t\t\tfail(`read did not reach the session as (root, $data); saw ${JSON.stringify(reads)}`);\n\t\t\t\t}\n\t\t\t\treturn [\"invoke and read routed by fragment id through the session\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.missing-component-is-visible.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tconst Card: ExperiencePackComponent = ({ children }) => children as ReactElement;\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tconst markup = subject.render(subject.createElement(Renderer, { session }));\n\t\t\t\t// A subtree the plan named but no pack supplies must be reported\n\t\t\t\t// where a person will see it. An empty region is indistinguishable\n\t\t\t\t// from \"there was nothing here\", which is the wrong thing to tell\n\t\t\t\t// someone whose screen just lost a section.\n\t\t\t\tif (!/role=\"alert\"/.test(markup)) fail(\"an unavailable component rendered nothing visible; expected a role=\\\"alert\\\" marker\");\n\t\t\t\tif (!markup.includes(\"orphan\")) fail(\"the missing-component marker does not identify the fragment it stands in for\");\n\t\t\t\treturn [\"unavailable component rendered as a visible alert naming its fragment\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.least-privilege-at-render-edge.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tlet captured: ExperienceComponentActions | undefined;\n\t\t\t\tconst Card: ExperiencePackComponent = ({ experience }) => {\n\t\t\t\t\tcaptured = experience;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (!captured) fail(\"the component never received an experience handle\");\n\t\t\t\tconst events = [...captured.events].sort();\n\t\t\t\tconst bindings = [...captured.bindings].sort();\n\t\t\t\tif (events.join(\",\") !== \"press\") fail(`component saw events [${events}]; its fragment declares only [press]`);\n\t\t\t\tif (bindings.join(\",\") !== \"$data\") fail(`component saw bindings [${bindings}]; its fragment declares only [$data]`);\n\t\t\t\treturn [\"component received exactly its fragment's events and bindings\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.no-invented-fragments.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tlet decoyRendered = false;\n\t\t\t\tconst Card: ExperiencePackComponent = () => null;\n\t\t\t\tconst Decoy: ExperiencePackComponent = () => {\n\t\t\t\t\tdecoyRendered = true;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\t// The registry offers a component the plan never names. A renderer\n\t\t\t\t// that reaches into the registry rather than following the plan\n\t\t\t\t// would render it.\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card, \"acme.decoy\": Decoy } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (decoyRendered) fail(\"a component the plan never named was rendered; the plan, not the registry, decides what appears\");\n\t\t\t\treturn [\"only plan-named fragments rendered\"];\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"fabric.experience-react.experience-prop-is-sealed.v1\",\n\t\t\tasync run(subject) {\n\t\t\t\tconst plan = fixturePlan();\n\t\t\t\tconst { session } = recordingSession(plan);\n\t\t\t\tlet captured: ExperienceComponentActions | undefined;\n\t\t\t\tconst Card: ExperiencePackComponent = ({ experience }) => {\n\t\t\t\t\tcaptured = experience;\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\t\t\t\tconst Renderer = subject.createRenderer({ components: { \"acme.card\": Card } });\n\t\t\t\tsubject.render(subject.createElement(Renderer, { session }));\n\t\t\t\tif (!captured) fail(\"the component never received an experience handle\");\n\t\t\t\t// A component that could reassign invoke could route around the\n\t\t\t\t// session. Sealing the handle is cheap and closes it.\n\t\t\t\tif (!Object.isFrozen(captured)) fail(\"the experience handle is not frozen; a component could rewire invoke or read\");\n\t\t\t\treturn [\"experience handle is frozen\"];\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** Run every check, returning each result rather than throwing on the first. */\nexport async function runExperienceReactChecks(\n\tsubject: ExperienceReactConformanceSubject,\n): Promise<ExperienceReactConformanceResult> {\n\tconst checks: ExperienceReactConformanceResult[\"checks\"] = [];\n\tfor (const check of experienceReactChecks()) {\n\t\ttry {\n\t\t\tchecks.push({ id: check.id, status: \"passed\", evidence: await check.run(subject) });\n\t\t} catch (error) {\n\t\t\tchecks.push({ id: check.id, status: \"failed\", evidence: [], error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { passed: checks.every((check) => check.status === \"passed\"), checks };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabricorg/experience-react",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "React renderer adapter for scoped Fabric experience render plans.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "dependencies": {
33
- "@fabricorg/experience-runtime": "^0.3.0"
33
+ "@fabricorg/experience-runtime": "^0.4.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": "^18.3.0 || ^19.0.0"