From dbb27b22712e59d5412c5f4af065ffd3fe89e7d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 02:27:33 +0000 Subject: [PATCH 1/5] Initial plan From 8ac061e5d24234deef4da579777fd6a999cabae2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 02:47:30 +0000 Subject: [PATCH 2/5] fix: resolve strict TypeScript typing issues --- src/binding.ts | 56 +++++++++++++----------- src/compile.ts | 107 ++++++++++++++++++++++++--------------------- src/jsx-runtime.ts | 15 +++---- src/useRef.ts | 21 +++++---- src/useState.ts | 3 +- src/widgets.tsx | 23 +++++----- tsconfig.json | 1 + 7 files changed, 121 insertions(+), 105 deletions(-) diff --git a/src/binding.ts b/src/binding.ts index 6d1e7bc..feb45e9 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -115,7 +115,7 @@ export type Binding = BindingBase & { }; export type ObjectBinding = Binding; -const typeNameMap = { +const typeNameMap: Partial> = { [CType.Double]: "double", [CType.Size]: "size_t", [CType.Int]: "int", @@ -137,9 +137,9 @@ export function getObjectTypeName(obj: ObjectBinding) { } switch (init.kind) { case SyntaxKind.NumericLiteral: - return typeNameMap[init.type]; + return typeNameMap[init.type] || "unknown"; case SyntaxKind.StringLiteral: - return typeNameMap[CType.String]; + return typeNameMap[CType.String] || "char*"; case SyntaxKind.NewExpression: return init.identifier; default: @@ -330,13 +330,9 @@ function compileFunction({ return [ signature, "{", - formatFuncBody( - locals.map((item) => compiler.compileVariableDeclaration(item)) - ), + formatFuncBody(locals.map((item) => compileVariableDeclaration(item))), formatFuncBody(body), - formatFuncBody( - locals.map((item) => compiler.compileObjectDestroyer(item.initializer)) - ), + formatFuncBody(locals.map((item) => compileObjectDestroyer(item.initializer))), "}", ] .filter(Boolean) @@ -347,7 +343,7 @@ function compileComponentMethod({ ctx, name, args = "", - body = ctx.body, + body, thatId = "w", }: { ctx?: FunctionContext; @@ -357,16 +353,17 @@ function compileComponentMethod({ body?: string[]; }) { const className = getComponentContext().name; + const methodBody = [ + `${className}_react_t *_that = ui_widget_get_data(${thatId}, ${className}_proto)`, + ...(body || ctx?.body || []), + ctx?.hasStateOperation ? `${className}_react_update(${thatId})` : undefined, + ].filter((line): line is string => Boolean(line)); return compileFunction({ locals: ctx?.locals || [], signature: `static void ${className}_${ name || ctx?.name || "unnamed_func" }(ui_widget_t *w${args})`, - body: [ - `${className}_react_t *_that = ui_widget_get_data(${thatId}, ${className}_proto)`, - ...(body || []), - ctx?.hasStateOperation && `${className}_react_update(${thatId})`, - ], + body: methodBody, }); } @@ -492,8 +489,8 @@ export function pushFunctionComponent(ctx: FunctionContext) { contextList.push(ctx); } -export function popFunctionComponent(ctx: FunctionContext) { - contextList.push(ctx); +export function popFunctionComponent() { + contextList.pop(); } export function setComponentContext(ctx: ComponentContext) { @@ -515,13 +512,16 @@ function createStringLiteral(value: string | null = null): StringLiteral { }; } -function createBinding(meta: BindingMeta, data: Record = {}) { +function createBinding( + meta: BindingMeta, + data: T = {} as T +) { const binding = new Proxy( - { __meta__: meta, ...data }, + { __meta__: meta, ...data } as Record, { get(target, p, receiver) { if (p in target) { - return target[p]; + return Reflect.get(target, p, receiver); } if (typeof p !== "string") { return null; @@ -533,7 +533,7 @@ function createBinding(meta: BindingMeta, data: Record = {}) { name: p, }); }, - construct(target, args) { + construct(target: { __meta__: BindingMeta }, args) { if (target.__meta__.kind !== BindingKind.Object) { throw new SyntaxError("Module cannot be used as a constructor"); } @@ -542,28 +542,32 @@ function createBinding(meta: BindingMeta, data: Record = {}) { } return createVariable(meta.name, args); }, - apply(target: Binding, _thisArg, args) { + apply(target: { __meta__: BindingMeta }, _thisArg, args) { if (target.__meta__.kind === BindingKind.Module) { throw new SyntaxError("Module cannot be used as a function"); } const ctx = getFunctionContext(); ctx.body.push( compileCallExpression( - resolveBindingIdentify(target as ObjectBinding), + resolveBindingIdentify(target as unknown as ObjectBinding), args ) ); + return undefined; }, } - ) as Binding; + ) as unknown as Binding & T; return binding; } -function createObjectBinding(meta: Omit, data = {}) { +function createObjectBinding( + meta: Omit, + data: T = {} as T +): ObjectBinding & T { return createBinding( { ...meta, kind: BindingKind.Object }, data - ) as ObjectBinding; + ) as ObjectBinding & T; } export function isObjectBinding(val: any): val is ObjectBinding { diff --git a/src/compile.ts b/src/compile.ts index 17797ac..74300a7 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -7,6 +7,7 @@ import { getComponentContext, setComponentContext, isObjectBinding, + ObjectBinding, compiler, } from "./binding.js"; import fmt from "./fmt.js"; @@ -25,19 +26,26 @@ function isElement(el: ReactElement): el is Element { return typeof el.type === "function"; } +interface Node { + type: string; + name: string; + text: string; + attributes: Record; + children: Node[]; + isRoot: boolean; +} + function createNode(name = "") { return { type: "element", name, text: "", - attributes: {} as Record, - children: [], + attributes: {} as Record, + children: [] as Node[], isRoot: false, }; } -type Node = ReturnType; - function allocRef(ctx: ComponentContext, node: Node, prefix = "ref_") { if (node.isRoot) { return { @@ -80,10 +88,10 @@ function transformNodeStyle(node: Node, style: Record) { }); } -function transformNodeChildren(node: Node, rawChildren: ReactNode[]) { +function transformNodeChildren(node: Node, rawChildren: ReactNode) { let isPureText = true; let needFormat = true; - const children = []; + const children: (string | ObjectBinding | ReactElement)[] = []; React.Children.forEach(rawChildren, (child) => { switch (typeof child) { @@ -120,26 +128,28 @@ function transformNodeChildren(node: Node, rawChildren: ReactNode[]) { return; } - node.children = children.map((child) => { - if (typeof child === "string") { - return { - ...createNode("text"), - text: child, - }; - } - if (isObjectBinding(child)) { - const str = fmt(child); - const childNode = createNode("text"); - const ref = allocRef(ctx, childNode); + node.children = children + .map((child) => { + if (typeof child === "string") { + return { + ...createNode("text"), + text: child, + }; + } + if (isObjectBinding(child)) { + const str = fmt(child); + const childNode = createNode("text"); + const ref = allocRef(ctx, childNode); - ctx.body.push(`ui_widget_set_text(${ref.cName}, ${str.__meta__.name})`); - return childNode; - } - return transformReactNode(child); - }); + ctx.body.push(`ui_widget_set_text(${ref.cName}, ${str.__meta__.name})`); + return childNode; + } + return transformReactNode(child); + }) + .filter((n): n is Node => n !== undefined); } -function transformReactNode(el: ReactNode, isRoot = false) { +function transformReactNode(el: ReactNode, isRoot = false): Node | undefined { let node = createNode(); node.isRoot = isRoot; @@ -169,15 +179,16 @@ function transformReactNode(el: ReactNode, isRoot = false) { return; } - const attrMap = { + const props = el.props as Record; + const attrMap: Record = { className: "class", $ref: "ref", }; - const handlerNames = []; + const handlerNames: string[] = []; - Object.keys(el.props).forEach((propKey) => { + Object.keys(props).forEach((propKey) => { let key = propKey; - let value = el.props[key]; + let value = props[key]; if (key in attrMap) { key = attrMap[key]; @@ -204,24 +215,19 @@ function transformReactNode(el: ReactNode, isRoot = false) { ref.current.type = typeof el.type === "string" ? el.type : el.type.name; } } - if (el.props.children) { - transformNodeChildren(node, el.props.children); + if (props.children) { + transformNodeChildren(node, props.children); } - if (el.props.style) { - if (typeof el.props.style !== "object") { + if (props.style) { + if (typeof props.style !== "object") { throw SyntaxError( - `The style attribute value must be an object, not ${typeof el.props - .style}` + `The style attribute value must be an object, not ${typeof props.style}` ); } - transformNodeStyle(node, el.props.style); + transformNodeStyle(node, props.style); } handlerNames.forEach((name) => { - transformEventHandler( - node, - name.substring(2).toLocaleLowerCase(), - el.props[name] - ); + transformEventHandler(node, name.substring(2).toLocaleLowerCase(), props[name]); }); return node; } @@ -296,18 +302,17 @@ export default function compile( setComponentContext(ctx); - let el: ReactElement; - switch (options?.target) { - case "AppRouter": - el = componentFunc({ - ...props, - children: React.createElement(RouterView), - }); - break; - default: - el = componentFunc(props); - break; - } + const el: ReactElement = (() => { + switch (options?.target) { + case "AppRouter": + return componentFunc({ + ...props, + children: React.createElement(RouterView), + }); + default: + return componentFunc(props); + } + })(); const hasBaseType = el.type !== "div" && el.type !== Widget; return { name: options.name || ctx.name, diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index c9278a4..71fbd1b 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -5,14 +5,9 @@ import { ObjectBinding, isObjectBinding } from "./binding.js"; type JSXFactor = ( type: React.ElementType, props: Record, - key?: string + key?: React.Key ) => React.ReactElement; -declare module "react/jsx-runtime" { - export const jsx: JSXFactor; - export const jsxs: JSXFactor; -} - export function JSXObjectBinding({ value }: { value: ObjectBinding }) { return `[JSXObjectBinding ${value.__meta__.name}]`; } @@ -37,7 +32,11 @@ function transformElementProps(props: Record) { } export const jsx: JSXFactor = (type, props, key) => - rt.jsx(type, transformElementProps(props), key); + (rt as unknown as { jsx: JSXFactor }).jsx(type, transformElementProps(props), key); export const jsxs: JSXFactor = (type, props, key) => - rt.jsxs(type, transformElementProps(props), key); + (rt as unknown as { jsxs: JSXFactor }).jsxs( + type, + transformElementProps(props), + key + ); diff --git a/src/useRef.ts b/src/useRef.ts index a3cac90..dc1d511 100644 --- a/src/useRef.ts +++ b/src/useRef.ts @@ -3,6 +3,7 @@ import { factory, getComponentContext, getFunctionContext, + ObjectBinding, stringifyValue, } from "./binding.js"; @@ -36,13 +37,13 @@ class WidgetInstance { return str; } - setTextInputValue(value: string) { + setTextInputValue(value: string | ObjectBinding) { const ctx = getFunctionContext(); const str = stringifyValue(value); ctx.body.push(`ui_textinput_set_text(${this.ident}, ${str.__meta__.name}`); } - get value() { + get value(): ObjectBinding { switch (this.type) { case "textinput": return this.getTextInputValue(); @@ -52,7 +53,7 @@ class WidgetInstance { throw SyntaxError(`Unable to get value of ${this.type} type component`); } - set value(newValue: any) { + set value(newValue: string | ObjectBinding) { switch (this.type) { case "textinput": this.setTextInputValue(newValue); @@ -64,7 +65,12 @@ class WidgetInstance { } } -export default function useRef() { +interface WidgetRefBinding { + name: string; + current: WidgetInstance; +} + +export default function useRef(): ObjectBinding & WidgetRefBinding { const ctx = getComponentContext(); const name = ctx.refNames[ctx.refs.length] || `ref_${ctx.refs.length}`; const cName = `_that->refs.${name}`; @@ -72,14 +78,11 @@ export default function useRef() { ctx.refs.push(name); ctx.headerFiles.add(''); ctx.headerFiles.add(''); - return factory.createObjectBinding( + return factory.createObjectBinding( { name: cName, type: CType.Object, }, { name, current: new WidgetInstance(cName) } - ) as unknown as { - name: string; - current: WidgetInstance; - }; + ); } diff --git a/src/useState.ts b/src/useState.ts index e8f3aa2..142d7b4 100644 --- a/src/useState.ts +++ b/src/useState.ts @@ -84,7 +84,7 @@ export default function useState(initialValue: Value, valueType?: CType) { value = factory.createStringBinding(stateCName, initialValue); break; case "number": - if (isNumericType(valueType)) { + if (valueType !== undefined && isNumericType(valueType)) { value = factory.createNumericBinding( stateCName, initialValue, @@ -92,6 +92,7 @@ export default function useState(initialValue: Value, valueType?: CType) { ); break; } + throw new SyntaxError(`Unsupported numeric CType: ${valueType}`); default: throw new SyntaxError(`Unsupported type: ${typeof initialValue}`); } diff --git a/src/widgets.tsx b/src/widgets.tsx index 5896f6e..6bb182d 100644 --- a/src/widgets.tsx +++ b/src/widgets.tsx @@ -54,6 +54,9 @@ export type WidgetBaseProps = WidgetBaseAttributes; export type LinkProps = LinkAttributes; export type RouterViewProps = RouterViewAttributes; export type ScrollbarProps = ScrollbarAttributes; +type WidgetComponent = ((props: T) => React.ReactElement) & { + shouldPreRender?: boolean; +}; export interface RouterLinkProps extends WidgetBaseProps { to: string; @@ -119,13 +122,13 @@ export function RouterView(props: RouterViewProps) { return ; } -Widget.shouldPreRender = true; -Button.shouldPreRender = true; -Link.shouldPreRender = true; -Text.shouldPreRender = true; -TextInput.shouldPreRender = true; -ScrollArea.shouldPreRender = true; -ScrollAreaContent.shouldPreRender = true; -Scrollbar.shouldPreRender = true; -RouterLink.shouldPreRender = true; -RouterView.shouldPreRender = true; +(Widget as WidgetComponent).shouldPreRender = true; +(Button as WidgetComponent).shouldPreRender = true; +(Link as WidgetComponent).shouldPreRender = true; +(Text as WidgetComponent).shouldPreRender = true; +(TextInput as WidgetComponent).shouldPreRender = true; +(ScrollArea as WidgetComponent).shouldPreRender = true; +(ScrollAreaContent as WidgetComponent).shouldPreRender = true; +(Scrollbar as WidgetComponent).shouldPreRender = true; +(RouterLink as WidgetComponent).shouldPreRender = true; +(RouterView as WidgetComponent).shouldPreRender = true; diff --git a/tsconfig.json b/tsconfig.json index 706aa0f..4520604 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "module": "NodeNext", "declaration": true, "outDir": "./lib", + "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true From 1fff519ad0ba217ffabccb72d7702786841f4740 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 07:09:31 +0000 Subject: [PATCH 3/5] feat: add CI workflow and widget test coverage --- .github/workflows/ci.yml | 21 ++++++++++++ package.json | 5 ++- src/fmt.ts | 3 +- src/useRef.ts | 4 +-- src/widgets.tsx | 72 +++++++++++++++------------------------- test/widgets.test.mjs | 52 +++++++++++++++++++++++++++++ tsconfig.json | 1 + 7 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 test/widgets.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b48a713 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm install + - run: npm run typecheck + - run: npm test diff --git a/package.json b/package.json index cde492b..ec7db88 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "types.d.ts" ], "scripts": { - "test": "echo \"no test specified\"", + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "npm run test:unit", + "test:unit": "npm run build && node --test ./test/**/*.test.mjs", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "version": "npm run changelog && git add CHANGELOG.md" }, diff --git a/src/fmt.ts b/src/fmt.ts index 378bcba..2938748 100644 --- a/src/fmt.ts +++ b/src/fmt.ts @@ -1,5 +1,6 @@ import { CType, + ObjectBinding, Value, factory, getComponentContext, @@ -7,7 +8,7 @@ import { stringifyValue, } from "./binding.js"; -export default function fmt(...args: Value[]) { +export default function fmt(...args: Value[]): ObjectBinding { const component = getComponentContext(); const ctx = getFunctionContext(); const str = factory.createStringVariable(); diff --git a/src/useRef.ts b/src/useRef.ts index dc1d511..48ae3e5 100644 --- a/src/useRef.ts +++ b/src/useRef.ts @@ -15,7 +15,7 @@ class WidgetInstance { this.ident = ident; } - getTextInputValue() { + getTextInputValue(): ObjectBinding { const ctx = getFunctionContext(); const str = factory.createStringVariable(); const len = factory.createNumericVariable( @@ -40,7 +40,7 @@ class WidgetInstance { setTextInputValue(value: string | ObjectBinding) { const ctx = getFunctionContext(); const str = stringifyValue(value); - ctx.body.push(`ui_textinput_set_text(${this.ident}, ${str.__meta__.name}`); + ctx.body.push(`ui_textinput_set_text(${this.ident}, ${str.__meta__.name})`); } get value(): ObjectBinding { diff --git a/src/widgets.tsx b/src/widgets.tsx index 6bb182d..358d4fb 100644 --- a/src/widgets.tsx +++ b/src/widgets.tsx @@ -54,7 +54,8 @@ export type WidgetBaseProps = WidgetBaseAttributes; export type LinkProps = LinkAttributes; export type RouterViewProps = RouterViewAttributes; export type ScrollbarProps = ScrollbarAttributes; -type WidgetComponent = ((props: T) => React.ReactElement) & { + +export type FunctionWidget = ((props: T) => React.ReactElement) & { shouldPreRender?: boolean; }; @@ -69,45 +70,38 @@ export interface TextInputProps extends WidgetBaseProps { placeholder?: string; } -export function Text(props: WidgetBaseProps) { - return ; -} - -export function TextInput(props: TextInputProps) { - return ; -} - -export function Link(props: LinkProps) { - return ; -} - -export function Button(props: WidgetBaseProps) { - return