feat: support react-native imports in widget code - #270
Merged
Conversation
Widget source is loaded in three places with different execution models:
`voltra apply`, the Expo config plugins' prebuild prerender, and Metro when
it bundles a Dynamic Widget for the device. Each had its own notion of what a
widget file may import. The two Node loaders were forked copies that had
drifted apart, and the client-package redirect existed in only one of them;
Metro rejected every `react-native` import outright. `import { StyleSheet }
from 'react-native'` therefore crashed the apply pipeline with
`Unexpected token 'typeof'`.
Move widget module resolution into @use-voltra/compiler so all three consume
one contract: a policy that decides every bare import, one Babel + VM loader
the CLI and the plugins adapt, and a `react-native` shim that both the loader
and the Metro resolver serve from the same file. The shim is an allowlist —
StyleSheet and Platform — and every other symbol fails the build naming
itself rather than becoming undefined at render time.
`Platform.OS` is the platform being built for, which every call site already
knows, so the loader takes it explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wxpgc46zjDeyGzkUpL1TjK
Six defects found reviewing the previous commit: - The compiler's ESM entry stopped loading. Its relative re-exports were emitted extensionless under `build/esm/package.json`'s `"type": "module"`; the package worked before only because it was a single file with no relative imports. Use explicit `.js` specifiers, as `@use-voltra/core` already does. - The unsupported-symbol guard lived only in the Node VM loader, so on device `Animated` and friends read as `undefined` rather than failing — contradicting the ADR. The shim now names the React Native exports widget code is most likely to reach for and exports each as a stub that throws on use, so Metro bundles fail loudly too. - `configFile: false` plus a three-name filename list dropped Babel's own root-config discovery, so `babel.config.json` and `.ts` projects silently lost their plugins in `voltra apply`. Ask Babel via `loadPartialConfig` instead of guessing filenames. - The fallback preset order changed Expo prebuild's preset from `babel-preset-expo` to `@react-native/babel-preset`. Prefer the Expo preset, which wraps the other, and cover the fallback path with a test. - Metro dropped the client-package redirect warning both Node loaders emit. - The changeset did not mention the expo-plugin signature changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wxpgc46zjDeyGzkUpL1TjK
Contributor
|
@V3RON : |
…mports-refactor-aqcd6y # Conflicts: # docs/adr/README.md
expo-plugin and the cli package now import runtime helpers from @use-voltra/compiler, but their tsconfig.typecheck.json files lacked a paths mapping to its source, so tsc could only resolve the package through its build output (missing on a fresh checkout/CI run).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Widget files can now import
StyleSheetandPlatformfromreact-native, so widget styles can live outside the element tree the same way they do everywhere else in an app. Previously any import fromreact-nativein a widget file failedvoltra applyandexpo prebuildwithUnexpected token 'typeof', and Dynamic Widgets rejected the import at bundle time.Closes #199.
What is this?
Widget source is loaded in three places with different execution models: the CLI's apply pipeline, the Expo config plugins' prebuild prerender, and Metro when it bundles a Dynamic Widget for the device. Each had its own notion of what a widget file may import.
The two Node loaders were forked copies that had drifted apart — different resolvable extensions, different Babel configuration lookup, different fallback presets — and the client-package redirect from #200 existed in only one of them, so
voltra applystill broke on@use-voltra/*-clientimports that prebuild handled fine. Metro had a third answer: it rejected everyreact-nativeimport outright.The reported crash is a symptom of that. React Native's published entry point is untranspiled Flow, and bare imports bypassed Babel and fell through to plain Node
require. But the deeper problem is the missing single answer: when the three environments disagree, a widget can prerender successfully and still fail to bundle, or render differently on device than the build-time placeholder it was prerendered from.How does it work?
@use-voltra/compilernow owns widget module resolution, and all three environments consume it:react-nativeresolves to Voltra's shim, and deepreact-native/...paths are rejected.react-nativesurface widget code sees, served from the same file by both the Node loader and the Metro resolver, so build-time evaluation and on-device rendering cannot diverge.The shim is an allowlist:
StyleSheet(createas identity,flatten,compose,absoluteFill,hairlineWidth) andPlatform(OS,select).Platform.OSis the platform the widget is being built for, which every call site already knew, so the loader takes it explicitly rather than guessing.Rejection happens at two levels, because the two environments offer different interception points. The Node loader hands widget code a proxy over the shim, so any unimplemented symbol fails the moment the module is evaluated. Metro resolves the shim file directly and has no such hook, so the shim additionally names the React Native exports widget code is most likely to reach for and exports each as a stub that throws on use. Importing a stub is harmless; rendering, calling, or reading a property off it is not.
Decisions and their reasoning are recorded in ADR 0002.
Why is this useful?
Keeping styles out of the element tree is ordinary React Native practice, and until now it broke the build with an error that pointed at nothing a user could act on. Beyond that specific fix, adding to the widget-visible surface now means changing one shim that serves all three environments at once — there is no longer a way to fix one and forget the others.
The allowlist means a
react-nativeAPI that would silently misbehave in a widget fails loudly instead. That is a deliberate trade: an explicit error is cheaper than a widget that renders wrong on someone's Home Screen.Two pre-existing bugs fall out of the consolidation.
voltra applynow applies the client-package redirect that prebuild already had, and it now honours Babel configurations it previously ignored — a project keeping its setup inbabel.config.jsonorbabel.config.tssilently lost its plugins when widgets were transpiled.Validation.
pnpm build,typecheck,lint,test(25 tasks) andformat:js:checkall pass. 19 new tests in@use-voltra/compilercover the loader, the policy, and the shim entry points Metro resolves. I also ran the loader against the realexample/project end to end:AndroidClientDemoWidget.tsxloads and renders,StyleSheet.createandPlatform.OSresolve per platform, and unsupported symbols fail with the intended messages.Swift and Kotlin tests were not run — no toolchain was available in the environment this was developed in. No native code is touched by this change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Wxpgc46zjDeyGzkUpL1TjK
Generated by Claude Code