@stone-js/starters 0.8.9 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/basic-react-declarative/package.json +5 -5
- package/basic-react-imperative/package.json +5 -5
- package/basic-react-native-declarative/App.tsx +192 -0
- package/basic-react-native-declarative/LICENSE +21 -0
- package/basic-react-native-declarative/README.md +35 -0
- package/basic-react-native-declarative/adapter/NativeAdapter.ts +108 -0
- package/basic-react-native-declarative/adapter/NativeErrorHandler.ts +53 -0
- package/basic-react-native-declarative/adapter/NativeEventSource.ts +58 -0
- package/basic-react-native-declarative/adapter/RawResponseWrapper.ts +30 -0
- package/basic-react-native-declarative/adapter/declarations.ts +40 -0
- package/basic-react-native-declarative/adapter/middleware.ts +73 -0
- package/basic-react-native-declarative/adapter/nativeAdapterBlueprint.ts +51 -0
- package/basic-react-native-declarative/adapter/renderSink.ts +52 -0
- package/basic-react-native-declarative/app/Application.ts +17 -0
- package/basic-react-native-declarative/app/WelcomeController.ts +66 -0
- package/basic-react-native-declarative/app.json +25 -0
- package/basic-react-native-declarative/assets/android-icon-background.png +0 -0
- package/basic-react-native-declarative/assets/android-icon-foreground.png +0 -0
- package/basic-react-native-declarative/assets/android-icon-monochrome.png +0 -0
- package/basic-react-native-declarative/assets/favicon.png +0 -0
- package/basic-react-native-declarative/assets/icon.png +0 -0
- package/basic-react-native-declarative/assets/splash-icon.png +0 -0
- package/basic-react-native-declarative/babel.config.js +16 -0
- package/basic-react-native-declarative/index.ts +22 -0
- package/basic-react-native-declarative/metro.config.js +7 -0
- package/basic-react-native-declarative/package.json +41 -0
- package/basic-react-native-declarative/tests/Application.spec.ts +65 -0
- package/basic-react-native-declarative/tsconfig.json +16 -0
- package/basic-react-native-declarative/vitest.config.ts +27 -0
- package/basic-service-declarative/package.json +4 -4
- package/basic-service-imperative/package.json +4 -4
- package/continuum-showcase/package.json +9 -9
- package/full-react-declarative/package.json +10 -10
- package/full-react-imperative/package.json +10 -10
- package/full-service-declarative/package.json +10 -10
- package/full-service-imperative/package.json +10 -10
- package/package.json +15 -1
- package/standard-react-declarative/package.json +9 -9
- package/standard-react-imperative/package.json +9 -9
- package/standard-service-declarative/package.json +7 -7
- package/standard-service-imperative/package.json +7 -7
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0",
|
|
23
|
-
"@stone-js/core": "^0.8.
|
|
24
|
-
"@stone-js/use-react": "^0.8.
|
|
25
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
23
|
+
"@stone-js/core": "^0.8.10",
|
|
24
|
+
"@stone-js/use-react": "^0.8.10",
|
|
25
|
+
"@stone-js/browser-adapter": "^0.8.10"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
29
29
|
"@babel/preset-env": "^7.26.9",
|
|
30
30
|
"@babel/preset-typescript": "^7.27.0",
|
|
31
|
-
"@stone-js/cli": "^0.8.
|
|
32
|
-
"@stone-js/testing": "^0.8.
|
|
31
|
+
"@stone-js/cli": "^0.8.10",
|
|
32
|
+
"@stone-js/testing": "^0.8.10",
|
|
33
33
|
"@types/node": "^24.0.7",
|
|
34
34
|
"@types/react": "^19.0.7",
|
|
35
35
|
"@types/react-dom": "^19.0.3",
|
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0",
|
|
23
|
-
"@stone-js/core": "^0.8.
|
|
24
|
-
"@stone-js/use-react": "^0.8.
|
|
25
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
23
|
+
"@stone-js/core": "^0.8.10",
|
|
24
|
+
"@stone-js/use-react": "^0.8.10",
|
|
25
|
+
"@stone-js/browser-adapter": "^0.8.10"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
29
29
|
"@babel/preset-env": "^7.26.9",
|
|
30
30
|
"@babel/preset-typescript": "^7.27.0",
|
|
31
|
-
"@stone-js/cli": "^0.8.
|
|
32
|
-
"@stone-js/testing": "^0.8.
|
|
31
|
+
"@stone-js/cli": "^0.8.10",
|
|
32
|
+
"@stone-js/testing": "^0.8.10",
|
|
33
33
|
"@types/node": "^24.0.7",
|
|
34
34
|
"@types/react": "^19.0.7",
|
|
35
35
|
"@types/react-dom": "^19.0.3",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { stoneApp } from '@stone-js/core'
|
|
2
|
+
import { StatusBar } from 'expo-status-bar'
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Application } from './app/Application'
|
|
5
|
+
import { WelcomeData, WelcomeController } from './app/WelcomeController'
|
|
6
|
+
import { nativeEventSource } from './adapter/NativeEventSource'
|
|
7
|
+
import { onNativeError, onNativeRender } from './adapter/renderSink'
|
|
8
|
+
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One platform self-check, shown as a green or red row on screen.
|
|
12
|
+
*/
|
|
13
|
+
interface PlatformCheck {
|
|
14
|
+
label: string
|
|
15
|
+
passed: boolean
|
|
16
|
+
detail: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A minimal stage-3 decorator writing into `context.metadata`: if Babel emits
|
|
21
|
+
* the 2023-11 semantics correctly under Metro/Hermes, the value is readable
|
|
22
|
+
* back through `Symbol.metadata` (or its `Symbol.for` fallback, the one
|
|
23
|
+
* Stone.js core uses).
|
|
24
|
+
*/
|
|
25
|
+
const probe: any = (_value: unknown, context: { metadata: Record<PropertyKey, unknown> }): undefined => {
|
|
26
|
+
context.metadata.stonePoc = 'ok'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@probe
|
|
30
|
+
class MetadataProbe {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run the runtime platform checks Stone.js depends on.
|
|
34
|
+
*
|
|
35
|
+
* @returns The list of checks with their outcome.
|
|
36
|
+
*/
|
|
37
|
+
function runPlatformChecks (): PlatformCheck[] {
|
|
38
|
+
const checks: PlatformCheck[] = []
|
|
39
|
+
|
|
40
|
+
const metadataSymbol: symbol = (Symbol as any).metadata ?? Symbol.for('Symbol.metadata')
|
|
41
|
+
const probeMetadata = (MetadataProbe as any)[metadataSymbol]
|
|
42
|
+
checks.push({
|
|
43
|
+
label: 'Decorators 2023-11 + Symbol.metadata',
|
|
44
|
+
passed: probeMetadata?.stonePoc === 'ok',
|
|
45
|
+
detail: probeMetadata?.stonePoc === 'ok' ? 'context.metadata readable' : 'metadata missing'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
let urlPassed = false
|
|
49
|
+
let urlDetail = 'URL API incomplete'
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL('stone://app/hello/Noowow?from=poc')
|
|
52
|
+
urlPassed = url.pathname === '/hello/Noowow' && url.searchParams.get('from') === 'poc'
|
|
53
|
+
urlDetail = urlPassed ? 'pathname + searchParams OK' : `pathname=${String(url.pathname)}`
|
|
54
|
+
} catch (error: any) {
|
|
55
|
+
urlDetail = String(error?.message ?? error)
|
|
56
|
+
}
|
|
57
|
+
checks.push({ label: 'WHATWG URL (polyfill)', passed: urlPassed, detail: urlDetail })
|
|
58
|
+
|
|
59
|
+
const encoderPassed = typeof globalThis.TextEncoder !== 'undefined' &&
|
|
60
|
+
new TextEncoder().encode('stone').byteLength === 5
|
|
61
|
+
checks.push({
|
|
62
|
+
label: 'TextEncoder',
|
|
63
|
+
passed: encoderPassed,
|
|
64
|
+
detail: encoderPassed ? 'available' : 'missing'
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return checks
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The proof-of-concept screen: boots the Stone.js application through the
|
|
72
|
+
* native adapter, shows the platform checks and lets you navigate between
|
|
73
|
+
* routes of the same domain you would deploy on any other platform.
|
|
74
|
+
*/
|
|
75
|
+
export default function App (): React.JSX.Element {
|
|
76
|
+
const [checks, setChecks] = useState<PlatformCheck[]>([])
|
|
77
|
+
const [payload, setPayload] = useState<WelcomeData>()
|
|
78
|
+
const [error, setError] = useState<string>()
|
|
79
|
+
const [booted, setBooted] = useState(false)
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
setChecks(runPlatformChecks())
|
|
83
|
+
|
|
84
|
+
onNativeRender((response) => {
|
|
85
|
+
setError(undefined)
|
|
86
|
+
setBooted(true)
|
|
87
|
+
setPayload(response.content as WelcomeData)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
onNativeError((err) => {
|
|
91
|
+
setError(String(err?.message ?? err))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
stoneApp({ modules: [Application, WelcomeController] })
|
|
95
|
+
.run()
|
|
96
|
+
.catch((err: Error) => setError(String(err?.message ?? err)))
|
|
97
|
+
}, [])
|
|
98
|
+
|
|
99
|
+
const allPassed = checks.every((check) => check.passed) && booted && error === undefined
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={styles.container}>
|
|
103
|
+
<StatusBar style='light' />
|
|
104
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
105
|
+
<Text style={styles.title}>Stone.js × React Native</Text>
|
|
106
|
+
<Text style={styles.subtitle}>Continuum proof of concept</Text>
|
|
107
|
+
|
|
108
|
+
<View style={[styles.banner, allPassed ? styles.bannerOk : styles.bannerKo]}>
|
|
109
|
+
<Text style={styles.bannerText}>
|
|
110
|
+
{allPassed ? 'ALL CHECKS GREEN' : (booted ? 'CHECKS FAILING' : 'BOOTING…')}
|
|
111
|
+
</Text>
|
|
112
|
+
</View>
|
|
113
|
+
|
|
114
|
+
{checks.map((check) => (
|
|
115
|
+
<View key={check.label} style={styles.checkRow}>
|
|
116
|
+
<Text style={check.passed ? styles.checkOk : styles.checkKo}>
|
|
117
|
+
{check.passed ? '✓' : '✗'} {check.label}
|
|
118
|
+
</Text>
|
|
119
|
+
<Text style={styles.checkDetail}>{check.detail}</Text>
|
|
120
|
+
</View>
|
|
121
|
+
))}
|
|
122
|
+
|
|
123
|
+
<View style={styles.checkRow}>
|
|
124
|
+
<Text style={booted ? styles.checkOk : styles.checkKo}>
|
|
125
|
+
{booted ? '✓' : '…'} Kernel boot + router dispatch
|
|
126
|
+
</Text>
|
|
127
|
+
<Text style={styles.checkDetail}>
|
|
128
|
+
{booted ? `matched ${String(payload?.route)}` : 'waiting for the first render'}
|
|
129
|
+
</Text>
|
|
130
|
+
</View>
|
|
131
|
+
|
|
132
|
+
{error !== undefined && (
|
|
133
|
+
<View style={styles.errorBox}>
|
|
134
|
+
<Text style={styles.errorText}>{error}</Text>
|
|
135
|
+
</View>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{payload !== undefined && (
|
|
139
|
+
<View style={styles.payloadBox}>
|
|
140
|
+
<Text style={styles.payloadMessage}>{payload.message}</Text>
|
|
141
|
+
<Text style={styles.payloadMeta}>
|
|
142
|
+
{payload.framework.name} · {payload.framework.tagline}
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
<Text style={styles.sectionTitle}>Navigate the same domain</Text>
|
|
148
|
+
<View style={styles.buttons}>
|
|
149
|
+
<NavButton label='Home' url='stone://app/' />
|
|
150
|
+
<NavButton label='Hello Noowow' url='stone://app/hello/Noowow' />
|
|
151
|
+
<NavButton label='Hello Mr. Stone' url='stone://app/hello/Mr.%20Stone' />
|
|
152
|
+
<NavButton label='Unknown route' url='stone://app/nowhere' />
|
|
153
|
+
</View>
|
|
154
|
+
</ScrollView>
|
|
155
|
+
</View>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A navigation button emitting an intent into the native event source.
|
|
161
|
+
*/
|
|
162
|
+
function NavButton ({ label, url }: { label: string, url: string }): React.JSX.Element {
|
|
163
|
+
return (
|
|
164
|
+
<Pressable style={styles.button} onPress={() => nativeEventSource.navigate(url)}>
|
|
165
|
+
<Text style={styles.buttonText}>{label}</Text>
|
|
166
|
+
</Pressable>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const styles = StyleSheet.create({
|
|
171
|
+
container: { flex: 1, backgroundColor: '#141210' },
|
|
172
|
+
content: { padding: 24, paddingTop: 72 },
|
|
173
|
+
title: { color: '#f5f1ea', fontSize: 26, fontWeight: '700' },
|
|
174
|
+
subtitle: { color: '#b8b0a4', fontSize: 14, marginTop: 4, marginBottom: 20 },
|
|
175
|
+
banner: { borderRadius: 8, paddingVertical: 10, alignItems: 'center', marginBottom: 20 },
|
|
176
|
+
bannerOk: { backgroundColor: '#1f5130' },
|
|
177
|
+
bannerKo: { backgroundColor: '#5e2b1e' },
|
|
178
|
+
bannerText: { color: '#f5f1ea', fontWeight: '700', letterSpacing: 1 },
|
|
179
|
+
checkRow: { marginBottom: 12 },
|
|
180
|
+
checkOk: { color: '#7fd39a', fontSize: 15, fontWeight: '600' },
|
|
181
|
+
checkKo: { color: '#e88d70', fontSize: 15, fontWeight: '600' },
|
|
182
|
+
checkDetail: { color: '#8d8578', fontSize: 12, marginLeft: 18, marginTop: 2 },
|
|
183
|
+
errorBox: { backgroundColor: '#3a1f18', borderRadius: 8, padding: 12, marginTop: 8 },
|
|
184
|
+
errorText: { color: '#e88d70', fontSize: 13 },
|
|
185
|
+
payloadBox: { backgroundColor: '#221e1a', borderRadius: 8, padding: 16, marginTop: 16 },
|
|
186
|
+
payloadMessage: { color: '#f5f1ea', fontSize: 16, fontWeight: '600' },
|
|
187
|
+
payloadMeta: { color: '#b8b0a4', fontSize: 12, marginTop: 6 },
|
|
188
|
+
sectionTitle: { color: '#f5f1ea', fontSize: 16, fontWeight: '700', marginTop: 28, marginBottom: 12 },
|
|
189
|
+
buttons: { gap: 10 },
|
|
190
|
+
button: { backgroundColor: '#2e2823', borderRadius: 8, paddingVertical: 12, alignItems: 'center' },
|
|
191
|
+
buttonText: { color: '#f5f1ea', fontSize: 15, fontWeight: '600' }
|
|
192
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Stone.js · Basic React Native starter (declarative API)
|
|
2
|
+
|
|
3
|
+
The same Stone.js domain you deploy on Node, serverless, edge or the browser, running as a **native mobile application** with React Native and Expo.
|
|
4
|
+
|
|
5
|
+
## What this starter demonstrates
|
|
6
|
+
|
|
7
|
+
- A Stone.js domain (`app/`) written with the declarative API: `@StoneApp`, `@Routing`, `@Controller`, `@Match`. The domain never knows it runs inside a native application.
|
|
8
|
+
- The canonical activation paths: modules are enabled by their decorator, and the local adapter blueprint is activated through `@StoneApp(options, [blueprints])`.
|
|
9
|
+
- Stage-3 decorators (2023-11) with `Symbol.metadata`, transformed by Babel under Metro (see `babel.config.js`: the semantics are set through `babel-preset-expo`'s `decorators` option).
|
|
10
|
+
- The universal Stone router matching navigation intents (`stone://app/hello/:name`) exactly like it matches URLs in a browser SPA or paths behind an HTTP adapter.
|
|
11
|
+
- A minimal native adapter (`adapter/`): it captures navigation intents, normalizes them into `IncomingBrowserEvent` and executes the render effect. It prefigures `@stone-js/react-native-adapter` and will be replaced by it once published.
|
|
12
|
+
- The platform polyfills React Native needs (`index.ts`): the WHATWG `URL` API and, on older Hermes versions, `TextEncoder`.
|
|
13
|
+
|
|
14
|
+
The start screen is a live self-check: decorators, URL API, TextEncoder, kernel boot and router dispatch are verified at runtime and shown as green or red rows.
|
|
15
|
+
|
|
16
|
+
## Run it
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
npm run ios # opens the iOS simulator directly
|
|
21
|
+
npm start # or the interactive way: then press i (iOS) or a (Android)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Test it
|
|
25
|
+
|
|
26
|
+
The whole chain (domain, router, adapter) is pure JavaScript, so the exact modules the application boots are also tested under Node:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm test
|
|
30
|
+
npm run typecheck
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What is shared with your other platforms
|
|
34
|
+
|
|
35
|
+
Domain, routing, services and data loading. Not the UI components: native screens are React Native components, browser pages are DOM components.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { RawResponseWrapper } from './RawResponseWrapper'
|
|
2
|
+
import { NativeAdapterContext, NativeResponse, RawNativeResponseOptions } from './declarations'
|
|
3
|
+
import { NativeEventSource, NativeNavigationEvent, nativeEventSource } from './NativeEventSource'
|
|
4
|
+
import { Adapter, AdapterEventBuilder, AdapterEventHandlerType, IBlueprint } from '@stone-js/core'
|
|
5
|
+
import { IncomingBrowserEvent, IncomingBrowserEventOptions, OutgoingBrowserResponse } from '@stone-js/browser-core'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Native proof-of-concept adapter for Stone.js.
|
|
9
|
+
*
|
|
10
|
+
* Captures navigation intents from the in-memory event source, normalizes them
|
|
11
|
+
* into `IncomingBrowserEvent` instances, sends them through the kernel and
|
|
12
|
+
* executes the deferred render effect. It is the exact native counterpart of
|
|
13
|
+
* `BrowserAdapter`, with the event source replacing `window`.
|
|
14
|
+
*
|
|
15
|
+
* Temporary by design: it prefigures `@stone-js/react-native-adapter`, which
|
|
16
|
+
* will capture real deep links (`Linking`) and app-state changes on top of the
|
|
17
|
+
* same skeleton.
|
|
18
|
+
*/
|
|
19
|
+
export class NativeAdapter extends Adapter<
|
|
20
|
+
NativeNavigationEvent,
|
|
21
|
+
NativeResponse,
|
|
22
|
+
NativeEventSource,
|
|
23
|
+
IncomingBrowserEvent,
|
|
24
|
+
IncomingBrowserEventOptions,
|
|
25
|
+
OutgoingBrowserResponse,
|
|
26
|
+
NativeAdapterContext
|
|
27
|
+
> {
|
|
28
|
+
private unsubscribe?: () => void
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates an instance of the `NativeAdapter`.
|
|
32
|
+
*
|
|
33
|
+
* @param blueprint - The application blueprint.
|
|
34
|
+
* @returns A new instance of `NativeAdapter`.
|
|
35
|
+
*/
|
|
36
|
+
static create (blueprint: IBlueprint): NativeAdapter {
|
|
37
|
+
return new this(blueprint)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Executes the adapter: subscribes to navigation intents and dispatches the
|
|
42
|
+
* initial event, exactly like the browser adapter dispatches its synthetic
|
|
43
|
+
* first navigation on startup.
|
|
44
|
+
*
|
|
45
|
+
* Idempotent: running again tears down the previous subscription first.
|
|
46
|
+
*/
|
|
47
|
+
public async run<ExecutionResultType = undefined>(): Promise<ExecutionResultType> {
|
|
48
|
+
await this.executeHooks('onStart')
|
|
49
|
+
|
|
50
|
+
const eventHandler = this.resolveEventHandler()
|
|
51
|
+
|
|
52
|
+
await this.executeEventHandlerHooks('onInit', eventHandler)
|
|
53
|
+
|
|
54
|
+
this.unsubscribe?.()
|
|
55
|
+
this.unsubscribe = nativeEventSource.subscribe((rawEvent) => {
|
|
56
|
+
void this.eventListener(eventHandler, rawEvent)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await this.eventListener(eventHandler, { url: nativeEventSource.initialUrl })
|
|
60
|
+
|
|
61
|
+
return undefined as ExecutionResultType
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tear down the adapter: remove the navigation subscription and run the
|
|
66
|
+
* `onStop` hooks. Safe to call multiple times.
|
|
67
|
+
*/
|
|
68
|
+
public async stop (): Promise<void> {
|
|
69
|
+
if (this.unsubscribe === undefined) { return }
|
|
70
|
+
this.unsubscribe()
|
|
71
|
+
this.unsubscribe = undefined
|
|
72
|
+
await this.executeHooks('onStop')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Processes one navigation intent through the kernel.
|
|
77
|
+
*
|
|
78
|
+
* @param eventHandler - The resolved kernel event handler.
|
|
79
|
+
* @param rawEvent - The raw navigation event.
|
|
80
|
+
* @returns The platform response.
|
|
81
|
+
*/
|
|
82
|
+
protected async eventListener (
|
|
83
|
+
eventHandler: AdapterEventHandlerType<IncomingBrowserEvent, OutgoingBrowserResponse>,
|
|
84
|
+
rawEvent: NativeNavigationEvent
|
|
85
|
+
): Promise<NativeResponse> {
|
|
86
|
+
const incomingEventBuilder = AdapterEventBuilder.create<IncomingBrowserEventOptions, IncomingBrowserEvent>({
|
|
87
|
+
resolver: (options) => IncomingBrowserEvent.create(options)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const rawResponseBuilder = AdapterEventBuilder.create<RawNativeResponseOptions, RawResponseWrapper>({
|
|
91
|
+
resolver: (options) => RawResponseWrapper.create(options)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const context: NativeAdapterContext = {
|
|
95
|
+
rawEvent,
|
|
96
|
+
executionContext: nativeEventSource,
|
|
97
|
+
rawResponseBuilder,
|
|
98
|
+
incomingEventBuilder
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return await this.sendEventThroughDestination(context, eventHandler)
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
const builder = await this.handleError(error, context)
|
|
105
|
+
return await this.buildRawResponse({ ...context, rawResponseBuilder: builder })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { emitNativeError } from './renderSink'
|
|
2
|
+
import { NativeResponse } from './declarations'
|
|
3
|
+
import { NativeEventSource, NativeNavigationEvent } from './NativeEventSource'
|
|
4
|
+
import {
|
|
5
|
+
ILogger,
|
|
6
|
+
IBlueprint,
|
|
7
|
+
LoggerResolver,
|
|
8
|
+
AdapterErrorContext,
|
|
9
|
+
IAdapterErrorHandler,
|
|
10
|
+
defaultLoggerResolver,
|
|
11
|
+
AdapterEventBuilderType
|
|
12
|
+
} from '@stone-js/core'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* NativeErrorHandler options.
|
|
16
|
+
*/
|
|
17
|
+
export interface NativeErrorHandlerOptions {
|
|
18
|
+
blueprint: IBlueprint
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adapter-level error handler: logs the error and surfaces it on screen
|
|
23
|
+
* through the render sink, so a broken chain is visible instead of silent.
|
|
24
|
+
*/
|
|
25
|
+
export class NativeErrorHandler implements IAdapterErrorHandler<NativeNavigationEvent, NativeResponse, NativeEventSource> {
|
|
26
|
+
private readonly logger: ILogger
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a NativeErrorHandler.
|
|
30
|
+
*
|
|
31
|
+
* @param options - NativeErrorHandler options.
|
|
32
|
+
*/
|
|
33
|
+
constructor ({ blueprint }: NativeErrorHandlerOptions) {
|
|
34
|
+
this.logger = blueprint.get<LoggerResolver>('stone.logger.resolver', defaultLoggerResolver)(blueprint)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handle an error.
|
|
39
|
+
*
|
|
40
|
+
* @param error - The error to handle.
|
|
41
|
+
* @param context - The context of the adapter.
|
|
42
|
+
* @returns The raw response builder.
|
|
43
|
+
*/
|
|
44
|
+
public handle (
|
|
45
|
+
error: Error,
|
|
46
|
+
context: AdapterErrorContext<NativeNavigationEvent, NativeResponse, NativeEventSource>
|
|
47
|
+
): AdapterEventBuilderType<NativeResponse> {
|
|
48
|
+
this.logger.error(error.message, { error })
|
|
49
|
+
emitNativeError(error)
|
|
50
|
+
|
|
51
|
+
return context.rawResponseBuilder
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native cause captured by the adapter: an intent to show a URL.
|
|
3
|
+
*
|
|
4
|
+
* On a real device this is what a deep link, a push-notification tap or an
|
|
5
|
+
* in-app navigation collapses to. The proof-of-concept models all of them as
|
|
6
|
+
* one tiny in-memory event source; the future `@stone-js/react-native-adapter`
|
|
7
|
+
* will plug `Linking` and `AppState` into the exact same shape.
|
|
8
|
+
*/
|
|
9
|
+
export interface NativeNavigationEvent {
|
|
10
|
+
/** The URL to resolve, e.g. `stone://app/hello/Noowow`. */
|
|
11
|
+
url: string
|
|
12
|
+
/** Optional navigation metadata, mirroring the browser's `history.state`. */
|
|
13
|
+
metadata?: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A navigation listener registered by the adapter.
|
|
18
|
+
*/
|
|
19
|
+
export type NativeNavigationListener = (event: NativeNavigationEvent) => void
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The in-memory navigation event source: the native counterpart of `window`
|
|
23
|
+
* for the browser adapter. It is pure JavaScript, so the same code runs under
|
|
24
|
+
* React Native, Node (tests) and anywhere else.
|
|
25
|
+
*/
|
|
26
|
+
export class NativeEventSource {
|
|
27
|
+
/** The synthetic first event, dispatched when the adapter starts. */
|
|
28
|
+
public readonly initialUrl = 'stone://app/'
|
|
29
|
+
|
|
30
|
+
private readonly listeners = new Set<NativeNavigationListener>()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a navigation listener.
|
|
34
|
+
*
|
|
35
|
+
* @param listener - The listener to register.
|
|
36
|
+
* @returns A teardown function removing the listener.
|
|
37
|
+
*/
|
|
38
|
+
subscribe (listener: NativeNavigationListener): () => void {
|
|
39
|
+
this.listeners.add(listener)
|
|
40
|
+
return () => { this.listeners.delete(listener) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Emit a navigation intent, exactly like the router does in the browser
|
|
45
|
+
* through the `@stonejs/router.navigate` custom event.
|
|
46
|
+
*
|
|
47
|
+
* @param url - The URL to navigate to.
|
|
48
|
+
* @param metadata - Optional navigation metadata.
|
|
49
|
+
*/
|
|
50
|
+
navigate (url: string, metadata?: unknown): void {
|
|
51
|
+
this.listeners.forEach((listener) => listener({ url, metadata }))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The application-wide event source instance.
|
|
57
|
+
*/
|
|
58
|
+
export const nativeEventSource = new NativeEventSource()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IRawResponseWrapper } from '@stone-js/core'
|
|
2
|
+
import { NativeResponse, RawNativeResponseOptions } from './declarations'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps the raw native response: the platform effect is the deferred `render`
|
|
6
|
+
* closure contributed by the response middleware, executed once the kernel has
|
|
7
|
+
* resolved the outgoing response.
|
|
8
|
+
*/
|
|
9
|
+
export class RawResponseWrapper implements IRawResponseWrapper<NativeResponse> {
|
|
10
|
+
/**
|
|
11
|
+
* Factory method to create an instance of `RawResponseWrapper`.
|
|
12
|
+
*
|
|
13
|
+
* @param options - The raw response options.
|
|
14
|
+
* @returns A new instance of `RawResponseWrapper`.
|
|
15
|
+
*/
|
|
16
|
+
static create (options: RawNativeResponseOptions): RawResponseWrapper {
|
|
17
|
+
return new this(options)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private constructor (private readonly options: RawNativeResponseOptions) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute the deferred render effect and return its result.
|
|
24
|
+
*
|
|
25
|
+
* @returns The platform response.
|
|
26
|
+
*/
|
|
27
|
+
async respond (): Promise<NativeResponse> {
|
|
28
|
+
return await this.options?.render?.()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { RawResponseWrapper } from './RawResponseWrapper'
|
|
2
|
+
import { NativeEventSource, NativeNavigationEvent } from './NativeEventSource'
|
|
3
|
+
import { AdapterContext, IAdapterEventBuilder, RawResponseOptions } from '@stone-js/core'
|
|
4
|
+
import { IncomingBrowserEvent, IncomingBrowserEventOptions, OutgoingBrowserResponse } from '@stone-js/browser-core'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The platform identifier exposed on every incoming event's source.
|
|
8
|
+
*/
|
|
9
|
+
export const NATIVE_PLATFORM = 'react-native'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The raw platform response. The native platform has no transport response:
|
|
13
|
+
* the effect is a render, so the raw response is whatever the renderer returns.
|
|
14
|
+
*/
|
|
15
|
+
export type NativeResponse = unknown
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Raw response options carrying the deferred `render` effect, mirroring the
|
|
19
|
+
* browser adapter's contract.
|
|
20
|
+
*/
|
|
21
|
+
export interface RawNativeResponseOptions extends RawResponseOptions {
|
|
22
|
+
render?: () => NativeResponse | Promise<NativeResponse>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The adapter context for the native proof-of-concept adapter.
|
|
27
|
+
*/
|
|
28
|
+
export type NativeAdapterContext = AdapterContext<
|
|
29
|
+
NativeNavigationEvent,
|
|
30
|
+
NativeResponse,
|
|
31
|
+
NativeEventSource,
|
|
32
|
+
IncomingBrowserEvent,
|
|
33
|
+
IncomingBrowserEventOptions,
|
|
34
|
+
OutgoingBrowserResponse
|
|
35
|
+
>
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The response builder used by the native adapter middleware.
|
|
39
|
+
*/
|
|
40
|
+
export type NativeAdapterResponseBuilder = IAdapterEventBuilder<RawNativeResponseOptions, RawResponseWrapper>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { emitNativeRender } from './renderSink'
|
|
2
|
+
import { NextMiddleware } from '@stone-js/core'
|
|
3
|
+
import { CookieCollection } from '@stone-js/browser-core'
|
|
4
|
+
import { NATIVE_PLATFORM, NativeAdapterContext, NativeAdapterResponseBuilder } from './declarations'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Transforms the raw navigation event into the intention: an
|
|
8
|
+
* `IncomingBrowserEvent`, reused as-is from `@stone-js/browser-core`.
|
|
9
|
+
*
|
|
10
|
+
* This is the native mirror of the browser adapter's IncomingEventMiddleware:
|
|
11
|
+
* `location` comes from the event's URL instead of `window.location`, and the
|
|
12
|
+
* cookie collection is purely in memory (no `document`).
|
|
13
|
+
*/
|
|
14
|
+
export class IncomingEventMiddleware {
|
|
15
|
+
/**
|
|
16
|
+
* Handle the incoming raw event.
|
|
17
|
+
*
|
|
18
|
+
* @param context - The adapter context.
|
|
19
|
+
* @param next - The next middleware.
|
|
20
|
+
* @returns The response builder.
|
|
21
|
+
*/
|
|
22
|
+
async handle (
|
|
23
|
+
context: NativeAdapterContext,
|
|
24
|
+
next: NextMiddleware<NativeAdapterContext, NativeAdapterResponseBuilder>
|
|
25
|
+
): Promise<NativeAdapterResponseBuilder> {
|
|
26
|
+
const url = new URL(context.rawEvent.url)
|
|
27
|
+
|
|
28
|
+
context
|
|
29
|
+
.incomingEventBuilder
|
|
30
|
+
.add('url', url)
|
|
31
|
+
.add('queryString', url.search)
|
|
32
|
+
.add('protocol', url.protocol.replace(':', ''))
|
|
33
|
+
.add('metadata', context.rawEvent.metadata ?? {})
|
|
34
|
+
.add('cookies', CookieCollection.create(undefined, {}))
|
|
35
|
+
.add('source', {
|
|
36
|
+
rawEvent: context.rawEvent,
|
|
37
|
+
platform: NATIVE_PLATFORM,
|
|
38
|
+
rawContext: context.executionContext
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return await next(context)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Contributes the deferred render effect: once the kernel has produced the
|
|
47
|
+
* outgoing response, hand it to the registered render target.
|
|
48
|
+
*
|
|
49
|
+
* This is the native mirror of the role `BrowserResponseMiddleware` plays for
|
|
50
|
+
* the browser platform in `@stone-js/use-react`.
|
|
51
|
+
*/
|
|
52
|
+
export class ResponseMiddleware {
|
|
53
|
+
/**
|
|
54
|
+
* Handle the outgoing response.
|
|
55
|
+
*
|
|
56
|
+
* @param context - The adapter context.
|
|
57
|
+
* @param next - The next middleware.
|
|
58
|
+
* @returns The response builder.
|
|
59
|
+
*/
|
|
60
|
+
async handle (
|
|
61
|
+
context: NativeAdapterContext,
|
|
62
|
+
next: NextMiddleware<NativeAdapterContext, NativeAdapterResponseBuilder>
|
|
63
|
+
): Promise<NativeAdapterResponseBuilder> {
|
|
64
|
+
context.rawResponseBuilder.add('render', () => {
|
|
65
|
+
if (context.outgoingResponse !== undefined) {
|
|
66
|
+
emitNativeRender(context.outgoingResponse)
|
|
67
|
+
}
|
|
68
|
+
return context.outgoingResponse
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return await next(context)
|
|
72
|
+
}
|
|
73
|
+
}
|