镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit 29ad059

Browse files
committed
fix(native): share the host signal runtime with app module imports
1 parent cb7b2c2 commit 29ad059

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

‎docs/guide/native.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,14 @@ When the host advertises the `fetch` capability (Craft's iOS host does), the bun
639639

640640
Native screens use the ordinary stx signal primitives. A binding effect runs when a signal it reads changes; there is no second native-only redraw or reactivity model.
641641

642+
Shared TypeScript helpers imported by the native app lifecycle may import runtime
643+
functions such as `state`, `derived` and `effect` from `@stacksjs/stx` or `stx`.
644+
The native bundler forwards those functions to the host's existing runtime,
645+
sharing its signal graph instead of loading server rendering and filesystem
646+
modules. Keep initialization inside the app module's default lifecycle function:
647+
its imports are evaluated before the host starts the runtime. Screen client
648+
scripts continue to use their usual auto-imports.
649+
642650
```stx
643651
<script client lang="ts">
644652
import { sportStyle } from '../functions/mobile'

‎packages/stx/src/native/compiler/bundle.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { shareComponentScripts, type SharedScripts } from './shared-scripts'
3333
import { compactDocument, compactManifest } from '../runtime/compact-document'
3434
import { buildSharedModuleRegistries, registeredModuleIdsIn, registeredModuleNamesIn, type SharedRegistries } from '../../client-module-registry'
3535
import type { BrowserScreenOptions } from '../runtime/browser-screen'
36+
import { STX_RUNTIME_GLOBALS } from '../../runtime-globals'
3637

3738
export interface CompileNativeBundleOptions {
3839
/** Screen name to `.stx` path, relative to `root`. */
@@ -207,6 +208,30 @@ function nativeRuntimeModule(name: 'jsc-globals' | 'shared-screen' | 'browser-sc
207208
return resolved
208209
}
209210

211+
/**
212+
* Imported app helpers use the host's existing signal graph, just like screen
213+
* scripts. Resolving the server package root here would pull filesystem and
214+
* build tools into a JavaScriptCore/browser bundle. Forward lazily: app module
215+
* imports run before the host installs its runtime; their default function runs
216+
* after it. Module bodies must keep work inside that lifecycle function.
217+
*/
218+
function nativeAuthoringRuntime(): BunPlugin {
219+
return {
220+
name: 'stx-native-authoring-runtime',
221+
setup(builder) {
222+
builder.onResolve({ filter: /^(?:stx|@stacksjs\/stx)$/ }, () => ({ path: 'api', namespace: 'stx-native-authoring-runtime' }))
223+
builder.onLoad({ filter: /.*/, namespace: 'stx-native-authoring-runtime' }, () => ({
224+
loader: 'js',
225+
contents: STX_RUNTIME_GLOBALS.map(name => `export function ${name}(...args) {
226+
const runtime = globalThis.stx;
227+
if (typeof runtime?.${name} !== 'function') throw new Error('STX native runtime is not ready for ${name}');
228+
return runtime.${name}(...args);
229+
}`).join('\n'),
230+
}))
231+
},
232+
}
233+
}
234+
210235
/**
211236
* Bun takes the first `onLoad` that matches a file, so the inputs are
212237
* tracked, and a module's stand-in (`substitutes`, by absolute path) handed
@@ -225,7 +250,7 @@ async function build(entry: string, plugins: BunPlugin[], minify: boolean, depen
225250
target: 'browser',
226251
format: 'iife',
227252
minify,
228-
plugins: [track, ...plugins],
253+
plugins: [track, nativeAuthoringRuntime(), ...plugins],
229254
define: { 'process.env.NODE_ENV': JSON.stringify(minify ? 'production' : 'development') },
230255
})
231256
if (!result.success) {

‎packages/stx/test/native/native-app.test.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@ async function generate(): Promise<string> {
1515
bundle ??= (async () => {
1616
const root = createTempDir('stx-native-app-')
1717
await Bun.write(path.join(root, 'Home.stx'), '<script client></script><View><Text>Home</Text></View>')
18-
await Bun.write(path.join(root, 'helper.ts'), 'export function where(craft: any): string { return craft.app.url + "/api" }\n')
18+
await Bun.write(path.join(root, 'helper.ts'), `import { state, effect } from '@stacksjs/stx'
19+
export function where(craft: any): string { return craft.app.url + "/api" }
20+
export function reactiveWords() {
21+
const locale = state('en')
22+
effect(() => { globalThis.__appLocaleWord = locale() === 'de' ? 'Bestätigt' : 'Verified' })
23+
locale.set('de')
24+
}\n`)
1925
await Bun.write(path.join(root, 'app.ts'), `
20-
import { where } from './helper'
26+
import { where, reactiveWords } from './helper'
27+
import { batch } from 'stx'
2128
globalThis.__appModuleLoaded = (globalThis.__appModuleLoaded || 0) + 1
2229
export default function app(craft: any) {
30+
batch(reactiveWords)
2331
globalThis.__appRan = { route: craft.route.name, api: where(craft), version: craft.app.version }
2432
const opened = (event: any) => craft.navigation.open(event.detail.href)
2533
globalThis.addEventListener('craftNotificationResponse', opened)
@@ -63,6 +71,7 @@ async function run(route: string | undefined, capabilities: string[] = ['backgro
6371
describe('the app script', () => {
6472
it('runs its default export with craft, and draws nothing', async () => {
6573
const { scope, sent } = await run(NATIVE_APP_ROUTE)
74+
expect(scope.__appLocaleWord).toBe('Bestätigt')
6675
expect(scope.__appRan).toEqual({ route: NATIVE_APP_ROUTE, api: 'https://app.example/api', version: '1.2.0' })
6776
expect(sent.some(message => message.type === 'RENDER')).toBe(false)
6877
expect(scope.craft.app).toEqual({ url: 'https://app.example', version: '1.2.0', build: '7' })

0 commit comments

Comments
 (0)