Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8262fbf
feat: add Voltra widget Metro PoC
V3RON Jun 2, 2026
89f9130
feat: render generated Voltra widget entries
V3RON Jun 2, 2026
40883d0
Merge remote-tracking branch 'upstream/codex/metro-widget-poc' into p…
burczu Jun 2, 2026
4908358
feat(track-5): add WidgetEnvironment type for client-rendered widgets…
burczu Jun 3, 2026
b2b4f99
feat(track-5): thread env into the generated render entry (Phase 2)
burczu Jun 3, 2026
b01a9fc
feat(track-5): runtime smoke test for client-rendered widgets (Phase 3a)
burczu Jun 3, 2026
a0597bf
feat(track-5): env capture + dev hot-reload helper (Phase 3b-ii)
burczu Jun 3, 2026
7b64c8f
feat(track-5): client-rendered widget detection helper (Phase 3b-iii …
burczu Jun 3, 2026
1279bfa
feat(track-5): client-rendered widget runtime helpers (Phase 3b-iii s…
burczu Jun 3, 2026
571d027
feat(track-5): per-widget Swift dispatch on rendering mode (Phase 3b-…
burczu Jun 3, 2026
0e0f0d6
feat(track-5): client-rendered widget prerender + initial state (Phas…
burczu Jun 3, 2026
4719a85
feat(track-5): clientWidgetHotReload opt-in flag, drop polling (Phase…
burczu Jun 3, 2026
d5bad38
chore: gitignore SPM build caches under any iOS-bearing package
burczu Jun 3, 2026
4221ef2
feat(track-5): enableClientWidgetHotReload helper (Phase 3b-iii step 5b)
burczu Jun 3, 2026
78678b7
feat(track-5): wire IosWeatherWidget as plugin-generated widget (Phas…
burczu Jun 3, 2026
c91bfc8
chore(track-5): Phase 3b-iii hot-reload exploration — rip out failed …
burczu Jun 5, 2026
c7edd32
feat(track-5): silent-push handler for client widget hot reload (Phas…
burczu Jun 5, 2026
1752db4
feat(track-5): main app remote-notification background mode (Phase 3b…
burczu Jun 5, 2026
10820aa
feat(track-5): Metro-driven silent push for client widget hot reload …
burczu Jun 8, 2026
4173db8
fix(track-5): register silent-push handler at framework load time (Ph…
burczu Jun 8, 2026
20f17ad
chore(track-5): collapse demo widgets to Track5DemoWidget only
burczu Jun 8, 2026
d0a33b0
Merge remote-tracking branch 'upstream/main' into poc/widget-reactivi…
burczu Jun 8, 2026
5f006f9
chore: strip codename/phase prefixes from client-rendered widget comm…
burczu Jun 8, 2026
7762ca1
refactor: rename Track5DemoWidget to ClientRenderedDemoWidget
burczu Jun 8, 2026
0c3f6e5
refactor: remove silent-push hot-reload wiring for client-rendered wi…
burczu Jun 9, 2026
0a63eea
feat(ios-client): hook Metro __accept to refresh widgets on Fast Refresh
burczu Jun 9, 2026
b12582f
Merge remote-tracking branch 'upstream/main' into poc/widget-reactivi…
burczu Jun 9, 2026
bebc75b
refactor: drop client-rendered widget smoke test surface
burczu Jun 9, 2026
e8afa50
fix: pnpm-strict-layout resolution for client-rendered widgets
burczu Jun 9, 2026
9e4c6b7
fix(ios-client): resolve Metro dev-server URL via RCTBundleURLProvide…
burczu Jun 11, 2026
9b0fbbe
refactor(metro): discover client widgets by filesystem scan + watcher
burczu Jun 11, 2026
921e6cf
feat(ios-client): configure client-rendered widgets via AppIntent
burczu Jun 11, 2026
e9e5ee1
fix(metro): restore client-rendered widget hot reload via generated d…
burczu Jun 11, 2026
34a424a
feat(example/metro): one-shot production bundler for client-rendered …
burczu Jun 12, 2026
7473cfb
feat(ios-client): bake client-rendered widget bundles in release builds
burczu Jun 12, 2026
fc40617
fix(ios-client): harden client-widget release render + bundling
burczu Jun 12, 2026
a4d94d3
docs(ios-client): mark client-rendered widgets experimental
burczu Jun 12, 2026
9836b82
test(ios-client): cover AppIntent codegen, experimental warning, rele…
burczu Jun 12, 2026
8e7f419
feat: add Voltra compiler package
V3RON Jun 12, 2026
42bcef4
chore: merge origin/main into widget reactivity track
V3RON Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/client-rendered-widgets-experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@use-voltra/ios-client': minor
---

**Experimental: client-rendered widgets (iOS).** A widget component marked with the `'use voltra'`
directive now renders on-device from its own JS bundle, called as `(props, env) => JSX` on every
render, so it reacts to live environment values (widget family, color scheme, locale, and
user-editable `configuration` via a native AppIntent "Edit Widget" sheet). In development the
bundle is served by Metro and editing the JSX hot-reloads the home-screen widget; in release builds
the bundle is baked into the widget extension at build time.

This feature is **experimental** — usable in production at your own risk; the API and generated
build output may change. Verify release rendering on a real device (the iOS Simulator is unreliable
for widget rendering).
7 changes: 7 additions & 0 deletions .changeset/twelve-widgets-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@use-voltra/compiler': minor
'@use-voltra/ios-client': minor
'@use-voltra/metro': minor
---

Add the Voltra compiler package for shared directive scanning, wire Metro and iOS prebuild validation to it, and keep the Metro scanner subpath as a re-export.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ npm-debug.log

## Build
/build
/packages/ios-client/ios/.build/
# SPM build caches under any iOS package (ios-client, voltra, etc.)
/packages/*/ios/.build/
3 changes: 2 additions & 1 deletion example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ expo-env.d.ts

# Metro
.metro-health-check*
.voltra/

# debug
npm-debug.*
Expand All @@ -37,4 +38,4 @@ yarn-error.*
*.tsbuildinfo

/ios
/android
/android
22 changes: 22 additions & 0 deletions example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@
"pl": "./widgets/ios/ios-weather-initial.tsx"
}
},
{
"id": "ClientRenderedDemoWidget",
"displayName": {
"en": "Client-Rendered Demo",
"pl": "Client-Rendered Demo"
},
"description": {
"en": "Plain widget showing env values — for verifying client-rendered hot reload.",
"pl": "Prosty widget pokazujący wartości env — do weryfikacji hot reload."
},
"supportedFamilies": ["systemSmall", "systemMedium", "systemLarge"],
"initialStatePath": "./widgets/ios/ClientRenderedDemoWidget.tsx",
"appIntent": {
"parameters": [
{
"name": "label",
"title": "Label",
"default": "Hello"
}
]
}
},
{
"id": "portfolio",
"displayName": {
Expand Down
3 changes: 3 additions & 0 deletions example/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Stack } from 'expo-router'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableWidgetHotReload } from '@use-voltra/ios-client'
import '@use-voltra/widget-hot-reload'

import { useVoltraEvents } from '~/hooks/useVoltraEvents'
import { useServerDrivenWidgetToken } from '~/hooks/useServerDrivenWidgetToken'
import { updateAndroidVoltraWidget } from '~/widgets/android/updateAndroidVoltraWidget'

enableWidgetHotReload()
updateAndroidVoltraWidget({ width: 300, height: 200 })

const STACK_SCREEN_OPTIONS = {
Expand Down
23 changes: 23 additions & 0 deletions example/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const path = require('node:path')

const { getDefaultConfig } = require('expo/metro-config')
const { withVoltra } = require('@use-voltra/metro')

const config = getDefaultConfig(__dirname)
const repoRoot = path.resolve(__dirname, '..')

config.resolver.extraNodeModules = {
...config.resolver.extraNodeModules,
'@use-voltra/android': path.join(repoRoot, 'packages/android'),
'@use-voltra/android-client': path.join(repoRoot, 'packages/android-client'),
'@use-voltra/core': path.join(repoRoot, 'packages/core'),
'@use-voltra/expo-plugin': path.join(repoRoot, 'packages/expo-plugin'),
'@use-voltra/ios': path.join(repoRoot, 'packages/ios'),
'@use-voltra/ios-client': path.join(repoRoot, 'packages/ios-client'),
'@use-voltra/server': path.join(repoRoot, 'packages/server'),
'~': __dirname,
}

config.watchFolders = Array.from(new Set([...(config.watchFolders || []), path.join(repoRoot, 'packages')]))

module.exports = withVoltra(config)
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"react-native-web": "^0.21.0",
"react-native-webview": "13.16.0",
"react-native-worklets": "~0.7.0",
"@use-voltra/metro": "workspace:*",
"@use-voltra/ios": "workspace:*",
"@use-voltra/ios-client": "workspace:*",
"@use-voltra/android": "workspace:*",
Expand Down
65 changes: 65 additions & 0 deletions example/widgets/ios/ClientRenderedDemoWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Voltra, type WidgetEnvironment } from '@use-voltra/ios'

// Minimal client-rendered widget for verifying the dev loop.
//
// Plain black tile with the env values the runtime captured per render, plus a single
// editable literal (`hotReloadMarker` below) for proving hot reload end-to-end.
// Edit the literal, save, watch the home-screen widget update within ~1 second.

export const ClientRenderedDemoWidget = (_props: object, env: WidgetEnvironment = {} as WidgetEnvironment) => {
'use voltra'

// ▼ EDIT THIS LITERAL TO TEST HOT RELOAD ▼
const hotReloadMarker = 'edit me'

const date = env.date ? new Date(env.date) : new Date()
const renderedAt = date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})

const config = env.configuration as Record<string, unknown> | undefined
const configLabel = typeof config?.label === 'string' ? config.label : '(unset)'

const labelStyle = { fontSize: 9, color: '#FFFFFF' } as const
const valueStyle = { fontSize: 9, color: '#94A3B8' } as const

return (
<Voltra.VStack alignment="leading" spacing={4} style={{ flex: 1, padding: 12, backgroundColor: '#000000' }}>
<Voltra.Text style={{ fontSize: 11, fontWeight: '700', color: '#FFFFFF' }}>Client-rendered demo</Voltra.Text>

<Voltra.Text style={{ fontSize: 14, fontWeight: '600', color: '#34D399' }}>{hotReloadMarker}</Voltra.Text>

<Voltra.HStack spacing={4}>
<Voltra.Text style={labelStyle}>family:</Voltra.Text>
<Voltra.Text style={valueStyle}>{env.widgetFamily ?? '?'}</Voltra.Text>
</Voltra.HStack>
<Voltra.HStack spacing={4}>
<Voltra.Text style={labelStyle}>scheme:</Voltra.Text>
<Voltra.Text style={valueStyle}>{env.colorScheme ?? '?'}</Voltra.Text>
</Voltra.HStack>
<Voltra.HStack spacing={4}>
<Voltra.Text style={labelStyle}>mode:</Voltra.Text>
<Voltra.Text style={valueStyle}>{env.widgetRenderingMode ?? '?'}</Voltra.Text>
</Voltra.HStack>
<Voltra.HStack spacing={4}>
<Voltra.Text style={labelStyle}>locale:</Voltra.Text>
<Voltra.Text style={valueStyle}>{env.locale ?? '?'}</Voltra.Text>
</Voltra.HStack>
<Voltra.HStack spacing={4}>
<Voltra.Text style={labelStyle}>config:</Voltra.Text>
<Voltra.Text style={valueStyle}>{configLabel}</Voltra.Text>
</Voltra.HStack>
<Voltra.HStack spacing={4}>
<Voltra.Text style={labelStyle}>dev:</Voltra.Text>
<Voltra.Text style={valueStyle}>{String(env.build?.isDev ?? '?')}</Voltra.Text>
</Voltra.HStack>
<Voltra.HStack spacing={4}>
<Voltra.Text style={labelStyle}>time:</Voltra.Text>
<Voltra.Text style={valueStyle}>{renderedAt}</Voltra.Text>
</Voltra.HStack>
</Voltra.VStack>
)
}
2 changes: 2 additions & 0 deletions packages/android/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,5 @@ export type { LinearProgressIndicatorProps } from './jsx/LinearProgressIndicator
export type { RowProps } from './jsx/Row.js'
export type { SpacerProps } from './jsx/Spacer.js'
export type { TextProps } from './jsx/Text.js'
export { isAndroidEnv, isIosEnv } from '@use-voltra/core'
export type { MaterialColorScheme, WidgetBuildEnvironment, WidgetEnvironment } from '@use-voltra/core'
93 changes: 93 additions & 0 deletions packages/compiler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
![voltra-banner](https://use-voltra.dev/voltra-baner.jpg)

### Shared source analysis utilities for Voltra

[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]

`@use-voltra/compiler` contains the source analysis helpers that power Voltra's client-rendered widget workflow. It parses JavaScript and TypeScript files, finds `'use voltra'` directives, and returns the widget records that Metro and related tooling consume.

## Features

- **Directive scanning**: Detect `'use voltra'` in function declarations, function expressions, and arrow functions.

- **Export-aware analysis**: Match directives to named and default exports so only real widget entry points are returned.

- **TypeScript and JSX support**: Parse common React and React Native source formats, including `.ts`, `.tsx`, `.js`, and `.jsx` files.

- **Shared by Voltra tooling**: Powers `@use-voltra/metro` and other build-time widget workflows.

## Documentation

This package is an internal building block for Voltra's widget toolchain. Relevant topics:

- [Getting Started](https://use-voltra.dev/getting-started/installation)
- [iOS Widgets](https://use-voltra.dev/ios/development/developing-widgets)
- [Android Widgets](https://use-voltra.dev/android/development/developing-widgets)

## Getting started

`@use-voltra/compiler` is usually installed as a transitive dependency of the Metro package, but you can install it directly if you want to build custom tooling on top of Voltra's source scanner.

```sh
npm install @use-voltra/compiler
```

Use `scanVoltraDirectives()` to inspect a file:

```ts
import { scanVoltraDirectives } from '@use-voltra/compiler'

const widgets = scanVoltraDirectives({
filePath: '/app/widgets/OrderTracker.tsx',
source: `
export function OrderTracker() {
'use voltra'
return null
}
`,
})

console.log(widgets)
```

## Quick example

```ts
import { scanVoltraDirectives } from '@use-voltra/compiler'

const widgets = scanVoltraDirectives({
filePath: 'src/widgets/WeatherWidget.tsx',
source: `
export const WeatherWidget = () => {
'use voltra'
return null
}
`,
})

for (const widget of widgets) {
console.log(widget.id, widget.exportName, widget.sourcePath)
}
```

## Platform compatibility

This package is runtime-agnostic and works in Node.js or build-time tooling that needs to analyze Voltra widget source code.

## Authors

Voltra is an open source collaboration between [Saúl Sharma](https://github.com/saulsharma) and [Szymon Chmal](https://github.com/szymonchmal) at [Callstack][callstack-readme-with-love].

If you think it's cool, please star it 🌟. This project will always remain free to use.

[Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!

Like the project? ⚛️ [Join the Callstack team](https://callstack.com/careers/?utm_campaign=Senior_RN&utm_source=github&utm_medium=readme) who does amazing stuff for clients and drives React Native Open Source! 🔥

[callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=voltra&utm_term=readme-with-love
[license-badge]: https://img.shields.io/npm/l/@use-voltra/compiler?style=for-the-badge
[license]: https://github.com/callstackincubator/voltra/blob/main/LICENSE.txt
[npm-downloads-badge]: https://img.shields.io/npm/dm/@use-voltra/compiler?style=for-the-badge
[npm-downloads]: https://www.npmjs.com/package/@use-voltra/compiler
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
[prs-welcome]: ../../CONTRIBUTING.md
48 changes: 48 additions & 0 deletions packages/compiler/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@use-voltra/compiler",
"version": "1.4.1",
"description": "Shared Voltra source analysis utilities",
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
"types": "build/types/index.d.ts",
"exports": {
".": {
"types": "./build/types/index.d.ts",
"require": "./build/cjs/index.js",
"import": "./build/esm/index.js",
"default": "./build/esm/index.js"
},
"./package.json": "./package.json"
},
"files": [
"build",
"README.md"
],
"scripts": {
"build": "node ../../scripts/build-package.mjs packages/compiler",
"clean": "rm -rf build",
"lint": "oxlint src",
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
"test": "node --test"
},
"dependencies": {
"@babel/parser": "^7.27.4",
"@babel/types": "^7.27.4"
},
"keywords": [
"voltra",
"compiler",
"directive"
],
"author": "Saúl Sharma (https://x.com/saul_sharma), Szymon Chmal (https://x.com/chmalszymon)",
"repository": {
"type": "git",
"url": "git+https://github.com/callstackincubator/voltra.git",
"directory": "packages/compiler"
},
"bugs": {
"url": "https://github.com/callstackincubator/voltra/issues"
},
"license": "MIT",
"homepage": "https://use-voltra.dev"
}
Loading
Loading