Files
zeekayandHanzo Dev 751ef5c778 rename the framework to @hanzo/gui and the loader to @hanzogui/loader
Both old names are owned by a different npm account (artemis-prime), so
publishing them returns 403 — which is how this surfaced: the 8.0.0 run
shipped @hanzogui/logo and @hanzogui/demos depending on [email protected], a
version that cannot be published and therefore does not exist. Those two
were uninstallable.

@hanzo/gui and @hanzogui/* are ours, so the framework now carries the name
it is imported by everywhere downstream, and the loader moves under the
scope we control.

The rewrite is limited to package references. `DEBUG?.startsWith('hanzogui')`
is a log-namespace filter and join(dir, 'hanzogui') is a path — rewriting
either would change behaviour, so both are untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 20:23:18 -07:00

496 lines
15 KiB
TypeScript

import * as React from 'react'
import * as t from '@babel/types'
import { expect, test } from 'vitest'
import { extractForNative } from './lib/extract'
process.env.GUI_TARGET = 'native'
process.env.IS_STATIC = ''
window['React'] = React
test('basic extraction', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test() {
return (
<YStack backgroundColor="red" />
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('theme value extraction should work when no theme variables used', async () => {
// here we override default "$color" so it should flatten totally
// actually since we do unstyled: false and it expands to $color it acceses it
// were not smart enough yet to detect its later overriden :/
// that could be a perf optimization, but also have work to improve flattening soon anyway
const output = await extractForNative(`
import { Paragraph } from '@hanzo/gui'
export function Test() {
return (
<Paragraph color="red">hello world</Paragraph>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('theme value extraction should work when theme variables used', async () => {
const output = await extractForNative(`
import { Paragraph } from '@hanzo/gui'
export function Test() {
return (
<Paragraph>hello world</Paragraph>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('basic conditional extraction', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
let x = true
export function Test() {
return (
<>
<YStack backgroundColor={x ? 'red' : 'blue'} />
<YStack {...x && { backgroundColor: 'red' }} />
</>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('flat transform props', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test(isLoading) {
return (
<YStack
scale={isLoading ? 1 : 2}
x={10}
y={20}
rotate="10deg"
/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('handles style order merge properly', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test(props) {
return (
<YStack
scale={props.isLoading ? 1 : 2}
x={10}
{...props}
rotate="10deg"
/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test(`normalize ternaries flips the conditional properly`, async () => {
const inputCode = `
import { View } from '@hanzo/gui'
export function Test(props) {
return (
<View marginBottom={props !== 123 ? 12 : 0} />
)
}
`
const output = await extractForNative(inputCode)
const outCode = output?.code ?? ''
expect(outCode).toContain(`props === 123 ? _sheet["0"] : _sheet["1"]`)
expect(outCode).toMatchSnapshot()
})
test(`normalize ternaries with the conditional dynamic values`, async () => {
const inputCode = `
import { View } from '@hanzo/gui'
export function Test(props) {
return (
<View marginBottom={props !== 123 ? 12 : props.mb} />
)
}
`
const output = await extractForNative(inputCode)
const outCode = output?.code ?? ''
expect(outCode).toContain(`props !== 123 ? 12 : props.mb`)
expect(outCode).toMatchSnapshot()
})
test('normalize dynamic values with no theme access', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
const height = 200
export function Test(props) {
return (
<YStack height={height} width={props.width}/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('normalize dynamic values with theme access only', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test(props) {
return (
<YStack bg='$color'/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('do NOT flatten dynamic values with theme access', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test(props) {
return (
<YStack bg='$color' height={props.height}/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('do NOT flatten dynamic values with theme access, dynamic values, and conditional', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test(props) {
return (
<YStack bg={props.isLoading ? '$color' : 'red'} height={props.height}/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('do NOT flatten multiple dynamic values with theme access and conditional', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test(props) {
return (
<YStack bg={props.isLoading ? '$color' : 'red'} height={props.height} width={props.width}/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('$md and $gtMd media queries should respect breakpoint boundaries', async () => {
// Regression test for bug starting in 1.132.17
// On small screens (iPhone), $md should apply, NOT $gtMd
// The bug was that $gtMd was incorrectly applying on mobile
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test() {
return (
<YStack
h={100}
w={100}
bc="red"
$md={{ bc: 'yellow' }}
$gtMd={{ bc: 'green' }}
/>
)
}
`)
const code = output?.code ?? ''
// Verify the output includes both media query conditions
// and they are structured correctly for runtime evaluation
expect(code).toMatchSnapshot()
})
test('$gtMd only should NOT apply on small screens', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test() {
return (
<YStack
h={100}
w={100}
bc="red"
$gtMd={{ bc: 'green' }}
/>
)
}
`)
const code = output?.code ?? ''
expect(code).toMatchSnapshot()
})
test('multiple media query components should not conflict', async () => {
const output = await extractForNative(`
import { YStack, XStack } from '@hanzo/gui'
export function Test() {
return (
<>
<YStack bc="red" $md={{ bc: 'yellow' }} />
<XStack bc="blue" $gtMd={{ bc: 'green' }} />
<YStack bc="purple" $sm={{ bc: 'pink' }} />
</>
)
}
`)
const code = output?.code ?? ''
// Verify each wrapper has a unique name
expect(code).toMatchSnapshot()
// Make sure we don't have duplicate const declarations
const styledMatches = code.match(/const _\w+Styled\d+/g) || []
const uniqueNames = new Set(styledMatches)
expect(styledMatches.length).toBe(uniqueNames.size)
})
test('string ternary test should not be confused with media key', async () => {
// Regression: <View width={someString ? 24 : 66} xs={{ height: 30 }} />
// someString evaluates to a string at runtime, and _withStableStyle's
// resolvedExpressions check `typeof expr === 'string' ? media[expr] : expr`
// would incorrectly treat it as a media key lookup.
// The fix: compiler wraps non-string-literal expressions with !! to coerce to boolean.
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test({ someString }) {
return (
<YStack
width={someString ? 24 : 66}
$sm={{ height: 30 }}
/>
)
}
`)
const code = output?.code ?? ''
// the ternary test expression must be coerced to boolean
expect(code).toContain('!!someString')
// the media key should remain a plain string literal
expect(code).toContain('"sm"')
expect(code).toMatchSnapshot()
})
test('dynamic values in media props require both media and runtime conditions', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test({ fillContainer }) {
return (
<YStack
$gtLg={{
width: fillContainer ? '100%' : '50%',
pb: '$4',
}}
/>
)
}
`)
let expressions: t.ArrayExpression | null = null
t.traverseFast(output?.ast, (node) => {
if (
t.isJSXAttribute(node) &&
t.isJSXIdentifier(node.name, { name: '_expressions' }) &&
t.isJSXExpressionContainer(node.value) &&
t.isArrayExpression(node.value.expression)
) {
expressions = node.value.expression
}
})
expect(expressions).not.toBeNull()
expect(expressions!.elements).toHaveLength(3)
const [whenFill, whenNotFill, staticPadding] = expressions!.elements
expect(t.isArrayExpression(whenFill)).toBe(true)
expect(t.isArrayExpression(whenNotFill)).toBe(true)
const [fillMediaKey, fillCondition] = (whenFill as t.ArrayExpression).elements
expect(t.isStringLiteral(fillMediaKey, { value: 'gtLg' })).toBe(true)
expect(t.isUnaryExpression(fillCondition, { operator: '!' })).toBe(true)
expect(
t.isUnaryExpression((fillCondition as t.UnaryExpression).argument, {
operator: '!',
})
).toBe(true)
expect(
t.isIdentifier(
((fillCondition as t.UnaryExpression).argument as t.UnaryExpression).argument,
{ name: 'fillContainer' }
)
).toBe(true)
const [notFillMediaKey, notFillCondition] = (whenNotFill as t.ArrayExpression).elements
expect(t.isStringLiteral(notFillMediaKey, { value: 'gtLg' })).toBe(true)
expect(t.isUnaryExpression(notFillCondition, { operator: '!' })).toBe(true)
expect(
t.isUnaryExpression((notFillCondition as t.UnaryExpression).argument, {
operator: '!',
})
).toBe(true)
expect(
t.isUnaryExpression(
((notFillCondition as t.UnaryExpression).argument as t.UnaryExpression).argument,
{ operator: '!' }
)
).toBe(true)
expect(t.isStringLiteral(staticPadding, { value: 'gtLg' })).toBe(true)
})
test('a single media style uses the media-aware wrapper', async () => {
const output = await extractForNative(`
import { View } from '@hanzo/gui'
export function Test() {
return <View $gtLg={{ width: 100 }} />
}
`)
let expressions: t.ArrayExpression | null = null
t.traverseFast(output?.ast, (node) => {
if (
t.isJSXAttribute(node) &&
t.isJSXIdentifier(node.name, { name: '_expressions' }) &&
t.isJSXExpressionContainer(node.value) &&
t.isArrayExpression(node.value.expression)
) {
expressions = node.value.expression
}
})
expect(expressions).not.toBeNull()
expect(expressions!.elements).toHaveLength(1)
expect(t.isStringLiteral(expressions!.elements[0], { value: 'gtLg' })).toBe(true)
})
// native has no hover state, so hoverStyle should be dropped instead of
// preserved as runtime work.
test('hoverStyle on native should drop dead hover work', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test() {
return (
<YStack backgroundColor="red" hoverStyle={{ backgroundColor: 'green' }} />
)
}
`)
const code = output?.code ?? ''
// must not serialize hoverStyle into the stylesheet or preserve it as runtime work
expect(code).not.toContain('"hoverStyle"')
expect(code).not.toContain('hoverStyle')
expect(code).not.toContain("backgroundColor: 'green'")
expect(code).toContain('__ReactNativeView')
expect(code).toMatchSnapshot()
})
test('$theme-* on native should de-opt (preserve as inline prop)', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test() {
return (
<YStack backgroundColor="red" $theme-dark={{ backgroundColor: 'green' }} />
)
}
`)
const code = output?.code ?? ''
// must NOT serialize $theme-dark as a sheet key (it would never match on RN)
expect(code).not.toContain('"$theme-dark"')
// must preserve as an inline JSX prop so runtime resolves it via theme
expect(code).toContain('$theme-dark')
expect(code).toContain('<YStack')
expect(code).toMatchSnapshot()
})
test('$group-*-hover on native should drop dead hover work', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test() {
return (
<YStack group="row">
<YStack
backgroundColor="red"
$group-row-hover={{ backgroundColor: 'green' }}
/>
</YStack>
)
}
`)
const code = output?.code ?? ''
// must not serialize or preserve hover-only group styles on native
expect(code).not.toContain('"$group-row-hover"')
expect(code).not.toContain('$group-row-hover')
expect(code).not.toContain("backgroundColor: 'green'")
expect(code).toMatchSnapshot()
})
test('$group-* non-hover on native should de-opt (preserve as inline prop)', async () => {
const output = await extractForNative(`
import { YStack } from '@hanzo/gui'
export function Test() {
return (
<YStack group="row">
<YStack
backgroundColor="red"
$group-row-press={{ backgroundColor: 'green' }}
/>
</YStack>
)
}
`)
const code = output?.code ?? ''
// must not serialize $group-row-press as a sheet key
expect(code).not.toContain('"$group-row-press"')
// must preserve as an inline jsx prop so runtime resolves it via group context
expect(code).toContain('$group-row-press')
// group="row" parent must remain (it provides the runtime container context)
expect(code).toContain('group="row"')
expect(code).toMatchSnapshot()
})
test('ternary with mixed theme-token and non-token values preserves all props', async () => {
const output = await extractForNative(`
import { Text } from '@hanzo/gui'
export function Test({ isActive, label }) {
return (
<Text
fontSize="$3"
fontWeight={isActive ? '600' : '400'}
color={isActive ? '$color12' : '$color11'}
>
{label}
</Text>
)
}
`)
const code = output?.code ?? ''
// fontWeight must be conditional, not unconditional
// the bug was that plain styles (fontWeight) were added unconditionally
// when a ternary branch also had theme tokens (color), causing the last
// branch's value (400) to always win
expect(code).toContain('fontWeight')
// both fontWeight values should appear in the sheet styles
expect(code).toContain('600')
expect(code).toContain('400')
// the fontWeight values should be in different sheet entries, wrapped in a ternary
// NOT both applied unconditionally
expect(code).toMatchSnapshot()
})