Skip to content

Commit 65bf144

Browse files
committed
fix(runtime): validate preact class component output
1 parent 478e43d commit 65bf144

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

packages/runtime/src/runtime-component-runtime.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ type PreactLikeModule = {
2323
h(type: unknown, props: unknown, ...children: unknown[]): unknown;
2424
};
2525

26+
type PreactLikeClassComponent = new (
27+
...args: unknown[]
28+
) => {
29+
render(...args: unknown[]): unknown;
30+
};
31+
2632
export type RuntimeComponentFactory = (
2733
props: Record<string, JsonValue>,
2834
context: RuntimeExecutionContext,
@@ -73,7 +79,7 @@ export async function createPreactRenderArtifact(input: {
7379

7480
try {
7581
const component = isPreactClassComponent(input.sourceExport)
76-
? input.sourceExport
82+
? wrapPreactClassComponent(input.sourceExport as PreactLikeClassComponent)
7783
: wrapPreactFunctionComponent(
7884
input.sourceExport as (props: Record<string, JsonValue>) => unknown,
7985
);
@@ -235,6 +241,22 @@ function isPreactClassComponent(value: unknown): boolean {
235241
return typeof prototype?.render === "function";
236242
}
237243

244+
function wrapPreactClassComponent(
245+
sourceComponent: PreactLikeClassComponent,
246+
): PreactLikeClassComponent {
247+
return class RenderifyPreactSourceClassWrapper extends sourceComponent {
248+
render(...args: unknown[]): unknown {
249+
const output = super.render(...args);
250+
if (isPlainObjectPreactOutput(output)) {
251+
throw new Error(
252+
'source.runtime=preact component returned a plain object; return JSX/h() output or use source.runtime="renderify"',
253+
);
254+
}
255+
return output;
256+
}
257+
};
258+
}
259+
238260
function wrapPreactFunctionComponent(
239261
sourceComponent: (props: Record<string, JsonValue>) => unknown,
240262
): (props: Record<string, JsonValue>) => unknown {

tests/runtime.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3266,6 +3266,50 @@ test("runtime preact rendering rejects plain object component output", async ()
32663266
await runtime.terminate();
32673267
});
32683268

3269+
test("runtime preact rendering rejects plain object class component output", async () => {
3270+
const runtime = new DefaultRuntimeManager({
3271+
sourceTranspiler: new PassthroughSourceTranspiler(),
3272+
});
3273+
await runtime.initialize();
3274+
3275+
const plan: RuntimePlan = {
3276+
specVersion: DEFAULT_RUNTIME_PLAN_SPEC_VERSION,
3277+
id: "runtime_preact_plain_object_class_output_plan",
3278+
version: 1,
3279+
root: createElementNode("div", undefined, [createTextNode("fallback")]),
3280+
capabilities: {
3281+
domWrite: true,
3282+
},
3283+
state: {
3284+
initial: {
3285+
count: 4,
3286+
},
3287+
},
3288+
source: {
3289+
language: "js",
3290+
runtime: "preact",
3291+
code: [
3292+
'import { Component } from "preact";',
3293+
"export default class Dashboard extends Component {",
3294+
" render(props) {",
3295+
' return { type: "section", props: { "data-kind": "dashboard" },',
3296+
" children: [`count:${props.state.count}`] };",
3297+
" }",
3298+
"}",
3299+
].join("\n"),
3300+
},
3301+
};
3302+
3303+
const result = await runtime.executePlan(plan);
3304+
assert.equal(result.renderArtifact?.mode, "preact-vnode");
3305+
await assert.rejects(
3306+
() => new DefaultUIRenderer().render(result),
3307+
/plain object/,
3308+
);
3309+
3310+
await runtime.terminate();
3311+
});
3312+
32693313
test("runtime can fail-fast on dependency preflight import errors", async () => {
32703314
const runtime = new DefaultRuntimeManager({
32713315
moduleLoader: new FailingLoader(),

0 commit comments

Comments
 (0)