From 8ef6233ef36b12142d992df4cd9664210c3c76e8 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sun, 9 Aug 2026 13:24:18 +0530 Subject: [PATCH] fix(compiler): reject malformed structured props --- packages/compiler/src/codegen.ts | 28 ++++++++----------- .../__snapshots__/resilience.test.ts.snap | 28 ++++++++----------- packages/compiler/test/compiler.test.ts | 11 ++++++++ 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index ba7657f4..e6d8e28f 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -2414,7 +2414,7 @@ function generateComponent(ast: PageAst): string { ); } decls.push( - ` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify(runtimeTypeOf(prop.valueType))});`, + ` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify(runtimeTypeOf(prop.valueType))}, ${JSON.stringify(prop.name)});`, ); } if (!effectiveProps.some((prop) => prop.name === "attrs")) { @@ -2504,7 +2504,7 @@ function generateComponent(ast: PageAst): string { ); } - out.push(`function __coerce(v: any, def: any, declared: string = "unknown"): any { + out.push(`function __coerce(v: any, def: any, declared: string = "unknown", propName: string = "prop"): any { if (v === undefined || v === null) { return def; } @@ -2529,14 +2529,14 @@ function generateComponent(ast: PageAst): string { if (typeof v === "string") { try { const parsed = JSON.parse(v); - return Array.isArray(parsed) ? parsed : def; + if (!Array.isArray(parsed)) throw new TypeError("Expected an array prop '" + propName + "'"); + return parsed; } catch { - if (declared === "array") throw new TypeError("Expected an array prop"); - return def; + throw new TypeError("Expected an array prop '" + propName + "'"); } } - return def; + throw new TypeError("Expected an array prop '" + propName + "'"); } if (declared === "object" || (def !== null && typeof def === "object")) { @@ -2552,20 +2552,16 @@ function generateComponent(ast: PageAst): string { try { const parsed = JSON.parse(v); - return ( - parsed !== null && - typeof parsed === "object" && - !Array.isArray(parsed) - ) - ? parsed - : def; + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new TypeError("Expected an object prop '" + propName + "'"); + } + return parsed; } catch { - if (declared === "object") throw new TypeError("Expected an object prop"); - return def; + throw new TypeError("Expected an object prop '" + propName + "'"); } } - return def; + throw new TypeError("Expected an object prop '" + propName + "'"); } if (declared === "bigint") return BigInt(v); diff --git a/packages/compiler/test/__snapshots__/resilience.test.ts.snap b/packages/compiler/test/__snapshots__/resilience.test.ts.snap index ee5e1b9b..1920b3c7 100644 --- a/packages/compiler/test/__snapshots__/resilience.test.ts.snap +++ b/packages/compiler/test/__snapshots__/resilience.test.ts.snap @@ -45,7 +45,7 @@ export interface CounterOutputs { "change"(value: number): void; } -function __coerce(v: any, def: any, declared: string = "unknown"): any { +function __coerce(v: any, def: any, declared: string = "unknown", propName: string = "prop"): any { if (v === undefined || v === null) { return def; } @@ -70,14 +70,14 @@ function __coerce(v: any, def: any, declared: string = "unknown"): any { if (typeof v === "string") { try { const parsed = JSON.parse(v); - return Array.isArray(parsed) ? parsed : def; + if (!Array.isArray(parsed)) throw new TypeError("Expected an array prop '" + propName + "'"); + return parsed; } catch { - if (declared === "array") throw new TypeError("Expected an array prop"); - return def; + throw new TypeError("Expected an array prop '" + propName + "'"); } } - return def; + throw new TypeError("Expected an array prop '" + propName + "'"); } if (declared === "object" || (def !== null && typeof def === "object")) { @@ -93,20 +93,16 @@ function __coerce(v: any, def: any, declared: string = "unknown"): any { try { const parsed = JSON.parse(v); - return ( - parsed !== null && - typeof parsed === "object" && - !Array.isArray(parsed) - ) - ? parsed - : def; + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new TypeError("Expected an object prop '" + propName + "'"); + } + return parsed; } catch { - if (declared === "object") throw new TypeError("Expected an object prop"); - return def; + throw new TypeError("Expected an object prop '" + propName + "'"); } } - return def; + throw new TypeError("Expected an object prop '" + propName + "'"); } if (declared === "bigint") return BigInt(v); @@ -264,7 +260,7 @@ function __wrnexusSerializeScopeValue(value: any): string { export function render(props: CounterProps = {} as CounterProps): string { const __p = props || {}; - const label: string = __coerce(__p["label"], ("Count"), "string"); + const label: string = __coerce(__p["label"], ("Count"), "string", "label"); const __attrs = __restProps(__p, new Set(["label"])); let count = (0); const __scopeState = { "label": label, "count": count }; diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index 3ef0b8e4..06e6acb0 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -526,6 +526,17 @@ component NativeProps { ); }); +test("malformed structured prop strings fail with the prop name", async () => { + const mod = await compileAndImport(`component StructuredProps { + props { items = [] options = {} } + view {
} + }`); + const render = mod.render as (props?: Record) => string; + + expect(() => render({ items: "not-json" })).toThrow("Expected an array prop 'items'"); + expect(() => render({ options: "[]" })).toThrow("Expected an object prop 'options'"); +}); + test("parses a layout block", () => { const ast = parse(` layout AppLayout {