@fabricorg/experience-react 0.2.0 → 0.2.2

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - The 0.2.1 tarball on the registry was packed from a stale build and does not contain the change its changelog describes: the handle a pack component receives still shared the render plan's own `events` and `bindings` arrays, and the conformance suite was the 0.2.0 suite. This version carries the frozen copies and the tightened suite. The publish script now builds every package before it packs, so a tarball cannot again lag its source.
8
+
9
+ ## 0.2.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 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.
14
+
15
+ 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.
16
+
17
+ - Updated dependencies [e41c1f5]
18
+ - @fabricorg/experience-runtime@0.4.1
19
+
3
20
  ## 0.2.0
4
21
 
5
22
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -29,7 +29,7 @@ var import_react = require("react");
29
29
 
30
30
  // conformance.ts
31
31
  function fixturePlan() {
32
- return {
32
+ const plan = {
33
33
  formatVersion: 1,
34
34
  planId: "plan-conformance",
35
35
  application: "conformance",
@@ -47,6 +47,7 @@ function fixturePlan() {
47
47
  tokenSet: { name: "reference", version: "1.0.0" },
48
48
  expiresAt: "2099-01-01T00:00:00Z"
49
49
  };
50
+ return plan;
50
51
  }
51
52
  function recordingSession(plan) {
52
53
  const invokes = [];
@@ -103,7 +104,15 @@ function experienceReactChecks() {
103
104
  const markup = subject.render(subject.createElement(Renderer, { session }));
104
105
  if (!/role="alert"/.test(markup)) fail('an unavailable component rendered nothing visible; expected a role="alert" marker');
105
106
  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
+ const custom = subject.createRenderer({
108
+ components: { "acme.card": Card },
109
+ missingComponent: ({ name, fragmentId }) => subject.createElement("p", { role: "alert", "data-conformance-fallback": `${fragmentId}:${name}` })
110
+ });
111
+ const customMarkup = subject.render(subject.createElement(custom, { session }));
112
+ if (!customMarkup.includes('data-conformance-fallback="orphan:acme.nowhere"')) {
113
+ fail("a supplied missingComponent was not used for the unavailable component; the adapter substituted its own");
114
+ }
115
+ return ["unavailable component rendered as a visible alert naming its fragment", "supplied missingComponent honoured"];
107
116
  }
108
117
  },
109
118
  {
@@ -123,7 +132,13 @@ function experienceReactChecks() {
123
132
  const bindings = [...captured.bindings].sort();
124
133
  if (events.join(",") !== "press") fail(`component saw events [${events}]; its fragment declares only [press]`);
125
134
  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"];
135
+ if (captured.events === plan.root.events || captured.bindings === plan.root.bindings) {
136
+ fail("the handle shares the plan's own event or binding arrays; a component could grow the plan through them");
137
+ }
138
+ if (!Object.isFrozen(captured.events) || !Object.isFrozen(captured.bindings)) {
139
+ fail("the handle's event or binding lists are not frozen; a component could add an event it was never given");
140
+ }
141
+ return ["component received exactly its fragment's events and bindings, as frozen copies"];
127
142
  }
128
143
  },
129
144
  {
@@ -157,7 +172,13 @@ function experienceReactChecks() {
157
172
  subject.render(subject.createElement(Renderer, { session }));
158
173
  if (!captured) fail("the component never received an experience handle");
159
174
  if (!Object.isFrozen(captured)) fail("the experience handle is not frozen; a component could rewire invoke or read");
160
- return ["experience handle is frozen"];
175
+ try {
176
+ captured.events.push("smuggled");
177
+ } catch {
178
+ }
179
+ if (captured.events.includes("smuggled")) fail("an event pushed onto the handle stayed there; the event list is mutable");
180
+ if (plan.root.events?.includes("smuggled")) fail("an event pushed onto the handle reached the render plan");
181
+ return ["experience handle is frozen", "mutation through the handle does not reach the plan"];
161
182
  }
162
183
  }
163
184
  ];
@@ -186,8 +207,8 @@ function renderFragment(fragment, session, components, Missing) {
186
207
  if (fragment.component === void 0) return (0, import_react.createElement)(import_react.Fragment, null, ...children);
187
208
  const Component = components[fragment.component];
188
209
  if (!Component) return (0, import_react.createElement)(Missing, { name: fragment.component, fragmentId: fragment.id });
189
- const events = fragment.events ?? [];
190
- const bindings = fragment.bindings ?? [];
210
+ const events = Object.freeze([...fragment.events ?? []]);
211
+ const bindings = Object.freeze([...fragment.bindings ?? []]);
191
212
  return (0, import_react.createElement)(Component, {
192
213
  ...fragment.props ?? {},
193
214
  experience: Object.freeze({
@@ -1 +1 @@
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":[]}
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\t// Copies, frozen. Object.freeze on the handle is shallow, and the plan's own\n\t// arrays are what the session's least-privilege check reads: a component\n\t// handed them by reference could push an event onto the plan and then\n\t// invoke it.\n\tconst events = Object.freeze([...(fragment.events ?? [])]);\n\tconst bindings = Object.freeze([...(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, ReactNode } 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\t/** React's own `createElement` fits; the suite also uses it to build a host element for a fallback fixture. */\n\tcreateElement: (\n\t\ttype: string | ComponentType<ExperienceRendererProps>,\n\t\tprops: Record<string, unknown> | null,\n\t\t...children: ReactNode[]\n\t) => 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\t// Annotated rather than asserted, so a field added to ScopedRenderPlan\n\t// later fails here instead of being hidden by `as`.\n\tconst plan: ScopedRenderPlan = {\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};\n\treturn plan;\n}\n\n/**\n * A session that records every call so routing can be asserted. It routes by\n * fragment id only, which is what excludes a capability reference: a renderer\n * that named one would fail the fragment-id assertion.\n */\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, which is\n * what rules out routing by capability reference; a component the plan names\n * but no pack supplies is visibly reported rather than silently dropped, and\n * the caller's own fallback is honoured when one is supplied; a component sees\n * only the events and bindings its own fragment declares, as copies it cannot\n * grow; nothing the plan did not name is rendered; and the handle a component\n * is given cannot be rewired, nor can the plan be reached through it.\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\t// The caller's fallback must be the one used when supplied. An\n\t\t\t\t// adapter that hardcodes its own and ignores the option would pass\n\t\t\t\t// the default check and still leave the caller unable to brand or\n\t\t\t\t// localise what their users see.\n\t\t\t\tconst custom = subject.createRenderer({\n\t\t\t\t\tcomponents: { \"acme.card\": Card },\n\t\t\t\t\tmissingComponent: ({ name, fragmentId }) =>\n\t\t\t\t\t\tsubject.createElement(\"p\", { role: \"alert\", \"data-conformance-fallback\": `${fragmentId}:${name}` }),\n\t\t\t\t});\n\t\t\t\tconst customMarkup = subject.render(subject.createElement(custom, { session }));\n\t\t\t\tif (!customMarkup.includes('data-conformance-fallback=\"orphan:acme.nowhere\"')) {\n\t\t\t\t\tfail(\"a supplied missingComponent was not used for the unavailable component; the adapter substituted its own\");\n\t\t\t\t}\n\t\t\t\treturn [\"unavailable component rendered as a visible alert naming its fragment\", \"supplied missingComponent honoured\"];\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\t// Seeing only its own events is half of it. The lists must also be\n\t\t\t\t// the component's to look at and not the plan's to grow: the\n\t\t\t\t// session's least-privilege check reads the plan's arrays, so a\n\t\t\t\t// handle that shares them lets a component push an event and then\n\t\t\t\t// invoke it.\n\t\t\t\tif (captured.events === plan.root.events || captured.bindings === plan.root.bindings) {\n\t\t\t\t\tfail(\"the handle shares the plan's own event or binding arrays; a component could grow the plan through them\");\n\t\t\t\t}\n\t\t\t\tif (!Object.isFrozen(captured.events) || !Object.isFrozen(captured.bindings)) {\n\t\t\t\t\tfail(\"the handle's event or binding lists are not frozen; a component could add an event it was never given\");\n\t\t\t\t}\n\t\t\t\treturn [\"component received exactly its fragment's events and bindings, as frozen copies\"];\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\t// Freezing is shallow, so prove the property it is meant to give:\n\t\t\t\t// nothing a component does to the handle reaches the plan or the\n\t\t\t\t// session's check. Try the smuggle and confirm it fails at every\n\t\t\t\t// point it could have landed.\n\t\t\t\ttry {\n\t\t\t\t\t(captured.events as string[]).push(\"smuggled\");\n\t\t\t\t} catch {\n\t\t\t\t\t// A frozen array throws in strict mode; that is the expected shape.\n\t\t\t\t}\n\t\t\t\tif (captured.events.includes(\"smuggled\")) fail(\"an event pushed onto the handle stayed there; the event list is mutable\");\n\t\t\t\tif (plan.root.events?.includes(\"smuggled\")) fail(\"an event pushed onto the handle reached the render plan\");\n\t\t\t\t// The session here records rather than gates, so whether a\n\t\t\t\t// smuggled event would be refused is the client suite's question;\n\t\t\t\t// this one is that the renderer gave the component no way to\n\t\t\t\t// change what the client would check.\n\t\t\t\treturn [\"experience handle is frozen\", \"mutation through the handle does not reach the plan\"];\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
@@ -16,7 +16,8 @@ interface ExperienceReactConformanceSubject {
16
16
  }>;
17
17
  }): ComponentType<ExperienceRendererProps>;
18
18
  render(element: ReactElement): string;
19
- createElement: (type: ComponentType<ExperienceRendererProps>, props: ExperienceRendererProps) => ReactElement;
19
+ /** React's own `createElement` fits; the suite also uses it to build a host element for a fallback fixture. */
20
+ createElement: (type: string | ComponentType<ExperienceRendererProps>, props: Record<string, unknown> | null, ...children: ReactNode[]) => ReactElement;
20
21
  }
21
22
  interface ExperienceReactCheck {
22
23
  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";
@@ -35,11 +36,13 @@ interface ExperienceReactConformanceResult {
35
36
  * The suite any React adapter must pass.
36
37
  *
37
38
  * 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.
39
+ * mutation and read goes back through the session by fragment id, which is
40
+ * what rules out routing by capability reference; a component the plan names
41
+ * but no pack supplies is visibly reported rather than silently dropped, and
42
+ * the caller's own fallback is honoured when one is supplied; a component sees
43
+ * only the events and bindings its own fragment declares, as copies it cannot
44
+ * grow; nothing the plan did not name is rendered; and the handle a component
45
+ * is given cannot be rewired, nor can the plan be reached through it.
43
46
  */
44
47
  declare function experienceReactChecks(): readonly ExperienceReactCheck[];
45
48
  /** Run every check, returning each result rather than throwing on the first. */
package/dist/index.d.ts CHANGED
@@ -16,7 +16,8 @@ interface ExperienceReactConformanceSubject {
16
16
  }>;
17
17
  }): ComponentType<ExperienceRendererProps>;
18
18
  render(element: ReactElement): string;
19
- createElement: (type: ComponentType<ExperienceRendererProps>, props: ExperienceRendererProps) => ReactElement;
19
+ /** React's own `createElement` fits; the suite also uses it to build a host element for a fallback fixture. */
20
+ createElement: (type: string | ComponentType<ExperienceRendererProps>, props: Record<string, unknown> | null, ...children: ReactNode[]) => ReactElement;
20
21
  }
21
22
  interface ExperienceReactCheck {
22
23
  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";
@@ -35,11 +36,13 @@ interface ExperienceReactConformanceResult {
35
36
  * The suite any React adapter must pass.
36
37
  *
37
38
  * 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.
39
+ * mutation and read goes back through the session by fragment id, which is
40
+ * what rules out routing by capability reference; a component the plan names
41
+ * but no pack supplies is visibly reported rather than silently dropped, and
42
+ * the caller's own fallback is honoured when one is supplied; a component sees
43
+ * only the events and bindings its own fragment declares, as copies it cannot
44
+ * grow; nothing the plan did not name is rendered; and the handle a component
45
+ * is given cannot be rewired, nor can the plan be reached through it.
43
46
  */
44
47
  declare function experienceReactChecks(): readonly ExperienceReactCheck[];
45
48
  /** Run every check, returning each result rather than throwing on the first. */
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
 
7
7
  // conformance.ts
8
8
  function fixturePlan() {
9
- return {
9
+ const plan = {
10
10
  formatVersion: 1,
11
11
  planId: "plan-conformance",
12
12
  application: "conformance",
@@ -24,6 +24,7 @@ function fixturePlan() {
24
24
  tokenSet: { name: "reference", version: "1.0.0" },
25
25
  expiresAt: "2099-01-01T00:00:00Z"
26
26
  };
27
+ return plan;
27
28
  }
28
29
  function recordingSession(plan) {
29
30
  const invokes = [];
@@ -80,7 +81,15 @@ function experienceReactChecks() {
80
81
  const markup = subject.render(subject.createElement(Renderer, { session }));
81
82
  if (!/role="alert"/.test(markup)) fail('an unavailable component rendered nothing visible; expected a role="alert" marker');
82
83
  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
+ const custom = subject.createRenderer({
85
+ components: { "acme.card": Card },
86
+ missingComponent: ({ name, fragmentId }) => subject.createElement("p", { role: "alert", "data-conformance-fallback": `${fragmentId}:${name}` })
87
+ });
88
+ const customMarkup = subject.render(subject.createElement(custom, { session }));
89
+ if (!customMarkup.includes('data-conformance-fallback="orphan:acme.nowhere"')) {
90
+ fail("a supplied missingComponent was not used for the unavailable component; the adapter substituted its own");
91
+ }
92
+ return ["unavailable component rendered as a visible alert naming its fragment", "supplied missingComponent honoured"];
84
93
  }
85
94
  },
86
95
  {
@@ -100,7 +109,13 @@ function experienceReactChecks() {
100
109
  const bindings = [...captured.bindings].sort();
101
110
  if (events.join(",") !== "press") fail(`component saw events [${events}]; its fragment declares only [press]`);
102
111
  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"];
112
+ if (captured.events === plan.root.events || captured.bindings === plan.root.bindings) {
113
+ fail("the handle shares the plan's own event or binding arrays; a component could grow the plan through them");
114
+ }
115
+ if (!Object.isFrozen(captured.events) || !Object.isFrozen(captured.bindings)) {
116
+ fail("the handle's event or binding lists are not frozen; a component could add an event it was never given");
117
+ }
118
+ return ["component received exactly its fragment's events and bindings, as frozen copies"];
104
119
  }
105
120
  },
106
121
  {
@@ -134,7 +149,13 @@ function experienceReactChecks() {
134
149
  subject.render(subject.createElement(Renderer, { session }));
135
150
  if (!captured) fail("the component never received an experience handle");
136
151
  if (!Object.isFrozen(captured)) fail("the experience handle is not frozen; a component could rewire invoke or read");
137
- return ["experience handle is frozen"];
152
+ try {
153
+ captured.events.push("smuggled");
154
+ } catch {
155
+ }
156
+ if (captured.events.includes("smuggled")) fail("an event pushed onto the handle stayed there; the event list is mutable");
157
+ if (plan.root.events?.includes("smuggled")) fail("an event pushed onto the handle reached the render plan");
158
+ return ["experience handle is frozen", "mutation through the handle does not reach the plan"];
138
159
  }
139
160
  }
140
161
  ];
@@ -163,8 +184,8 @@ function renderFragment(fragment, session, components, Missing) {
163
184
  if (fragment.component === void 0) return createElement(Fragment, null, ...children);
164
185
  const Component = components[fragment.component];
165
186
  if (!Component) return createElement(Missing, { name: fragment.component, fragmentId: fragment.id });
166
- const events = fragment.events ?? [];
167
- const bindings = fragment.bindings ?? [];
187
+ const events = Object.freeze([...fragment.events ?? []]);
188
+ const bindings = Object.freeze([...fragment.bindings ?? []]);
168
189
  return createElement(Component, {
169
190
  ...fragment.props ?? {},
170
191
  experience: Object.freeze({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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":[]}
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\t// Copies, frozen. Object.freeze on the handle is shallow, and the plan's own\n\t// arrays are what the session's least-privilege check reads: a component\n\t// handed them by reference could push an event onto the plan and then\n\t// invoke it.\n\tconst events = Object.freeze([...(fragment.events ?? [])]);\n\tconst bindings = Object.freeze([...(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, ReactNode } 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\t/** React's own `createElement` fits; the suite also uses it to build a host element for a fallback fixture. */\n\tcreateElement: (\n\t\ttype: string | ComponentType<ExperienceRendererProps>,\n\t\tprops: Record<string, unknown> | null,\n\t\t...children: ReactNode[]\n\t) => 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\t// Annotated rather than asserted, so a field added to ScopedRenderPlan\n\t// later fails here instead of being hidden by `as`.\n\tconst plan: ScopedRenderPlan = {\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};\n\treturn plan;\n}\n\n/**\n * A session that records every call so routing can be asserted. It routes by\n * fragment id only, which is what excludes a capability reference: a renderer\n * that named one would fail the fragment-id assertion.\n */\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, which is\n * what rules out routing by capability reference; a component the plan names\n * but no pack supplies is visibly reported rather than silently dropped, and\n * the caller's own fallback is honoured when one is supplied; a component sees\n * only the events and bindings its own fragment declares, as copies it cannot\n * grow; nothing the plan did not name is rendered; and the handle a component\n * is given cannot be rewired, nor can the plan be reached through it.\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\t// The caller's fallback must be the one used when supplied. An\n\t\t\t\t// adapter that hardcodes its own and ignores the option would pass\n\t\t\t\t// the default check and still leave the caller unable to brand or\n\t\t\t\t// localise what their users see.\n\t\t\t\tconst custom = subject.createRenderer({\n\t\t\t\t\tcomponents: { \"acme.card\": Card },\n\t\t\t\t\tmissingComponent: ({ name, fragmentId }) =>\n\t\t\t\t\t\tsubject.createElement(\"p\", { role: \"alert\", \"data-conformance-fallback\": `${fragmentId}:${name}` }),\n\t\t\t\t});\n\t\t\t\tconst customMarkup = subject.render(subject.createElement(custom, { session }));\n\t\t\t\tif (!customMarkup.includes('data-conformance-fallback=\"orphan:acme.nowhere\"')) {\n\t\t\t\t\tfail(\"a supplied missingComponent was not used for the unavailable component; the adapter substituted its own\");\n\t\t\t\t}\n\t\t\t\treturn [\"unavailable component rendered as a visible alert naming its fragment\", \"supplied missingComponent honoured\"];\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\t// Seeing only its own events is half of it. The lists must also be\n\t\t\t\t// the component's to look at and not the plan's to grow: the\n\t\t\t\t// session's least-privilege check reads the plan's arrays, so a\n\t\t\t\t// handle that shares them lets a component push an event and then\n\t\t\t\t// invoke it.\n\t\t\t\tif (captured.events === plan.root.events || captured.bindings === plan.root.bindings) {\n\t\t\t\t\tfail(\"the handle shares the plan's own event or binding arrays; a component could grow the plan through them\");\n\t\t\t\t}\n\t\t\t\tif (!Object.isFrozen(captured.events) || !Object.isFrozen(captured.bindings)) {\n\t\t\t\t\tfail(\"the handle's event or binding lists are not frozen; a component could add an event it was never given\");\n\t\t\t\t}\n\t\t\t\treturn [\"component received exactly its fragment's events and bindings, as frozen copies\"];\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\t// Freezing is shallow, so prove the property it is meant to give:\n\t\t\t\t// nothing a component does to the handle reaches the plan or the\n\t\t\t\t// session's check. Try the smuggle and confirm it fails at every\n\t\t\t\t// point it could have landed.\n\t\t\t\ttry {\n\t\t\t\t\t(captured.events as string[]).push(\"smuggled\");\n\t\t\t\t} catch {\n\t\t\t\t\t// A frozen array throws in strict mode; that is the expected shape.\n\t\t\t\t}\n\t\t\t\tif (captured.events.includes(\"smuggled\")) fail(\"an event pushed onto the handle stayed there; the event list is mutable\");\n\t\t\t\tif (plan.root.events?.includes(\"smuggled\")) fail(\"an event pushed onto the handle reached the render plan\");\n\t\t\t\t// The session here records rather than gates, so whether a\n\t\t\t\t// smuggled event would be refused is the client suite's question;\n\t\t\t\t// this one is that the renderer gave the component no way to\n\t\t\t\t// change what the client would check.\n\t\t\t\treturn [\"experience handle is frozen\", \"mutation through the handle does not reach the plan\"];\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.2.0",
3
+ "version": "0.2.2",
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.4.0"
33
+ "@fabricorg/experience-runtime": "^0.4.2"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": "^18.3.0 || ^19.0.0"