@stone-js/starters 0.8.11 → 0.8.13
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 +16 -182
- package/basic-react-native-declarative/README.md +130 -29
- package/basic-react-native-declarative/app/Application.ts +17 -6
- package/basic-react-native-declarative/app/HomeScreen.tsx +80 -0
- package/basic-react-native-declarative/app/PortalMark.tsx +32 -0
- package/basic-react-native-declarative/app/WelcomeView.tsx +146 -0
- package/basic-react-native-declarative/app/theme.ts +66 -0
- package/basic-react-native-declarative/app.json +10 -3
- 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/logo-dark.png +0 -0
- package/basic-react-native-declarative/assets/logo-light.png +0 -0
- package/basic-react-native-declarative/assets/splash-icon.png +0 -0
- package/basic-react-native-declarative/index.ts +16 -8
- package/basic-react-native-declarative/metro.config.js +9 -3
- package/basic-react-native-declarative/package.json +9 -4
- package/basic-react-native-declarative/tests/Application.spec.ts +41 -43
- package/basic-react-native-declarative/tests/stubs/react-native.ts +42 -0
- package/basic-react-native-declarative/tsconfig.json +0 -1
- package/basic-react-native-declarative/vitest.config.ts +13 -5
- package/basic-react-native-imperative/App.tsx +26 -0
- package/basic-react-native-imperative/LICENSE +21 -0
- package/basic-react-native-imperative/README.md +150 -0
- package/basic-react-native-imperative/app/Application.ts +25 -0
- package/basic-react-native-imperative/app/HomeScreen.tsx +84 -0
- package/basic-react-native-imperative/app/PortalMark.tsx +32 -0
- package/basic-react-native-imperative/app/WelcomeView.tsx +146 -0
- package/basic-react-native-imperative/app/theme.ts +66 -0
- package/basic-react-native-imperative/app.json +32 -0
- package/basic-react-native-imperative/assets/android-icon-background.png +0 -0
- package/basic-react-native-imperative/assets/android-icon-foreground.png +0 -0
- package/basic-react-native-imperative/assets/android-icon-monochrome.png +0 -0
- package/basic-react-native-imperative/assets/favicon.png +0 -0
- package/basic-react-native-imperative/assets/icon.png +0 -0
- package/basic-react-native-imperative/assets/logo-dark.png +0 -0
- package/basic-react-native-imperative/assets/logo-light.png +0 -0
- package/basic-react-native-imperative/assets/splash-icon.png +0 -0
- package/basic-react-native-imperative/babel.config.js +16 -0
- package/basic-react-native-imperative/index.ts +30 -0
- package/basic-react-native-imperative/metro.config.js +13 -0
- package/basic-react-native-imperative/package.json +46 -0
- package/basic-react-native-imperative/tests/Application.spec.ts +63 -0
- package/basic-react-native-imperative/tests/stubs/react-native.ts +42 -0
- package/basic-react-native-imperative/tsconfig.json +15 -0
- package/basic-react-native-imperative/vitest.config.ts +35 -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/app/layout/AppLayout/AppLayout.tsx +1 -1
- package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-declarative/package.json +10 -10
- package/full-react-imperative/app/layout/AppLayout/AppLayout.tsx +1 -1
- package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-imperative/app/layout/SettingsLayout/SettingsLayout.tsx +1 -1
- 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 +29 -15
- 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
- package/basic-react-native-declarative/adapter/NativeAdapter.ts +0 -108
- package/basic-react-native-declarative/adapter/NativeErrorHandler.ts +0 -53
- package/basic-react-native-declarative/adapter/NativeEventSource.ts +0 -58
- package/basic-react-native-declarative/adapter/RawResponseWrapper.ts +0 -30
- package/basic-react-native-declarative/adapter/declarations.ts +0 -40
- package/basic-react-native-declarative/adapter/middleware.ts +0 -73
- package/basic-react-native-declarative/adapter/nativeAdapterBlueprint.ts +0 -51
- package/basic-react-native-declarative/adapter/renderSink.ts +0 -52
- package/basic-react-native-declarative/app/WelcomeController.ts +0 -66
|
@@ -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.13",
|
|
24
|
+
"@stone-js/use-react": "^0.8.13",
|
|
25
|
+
"@stone-js/browser-adapter": "^0.8.13"
|
|
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.13",
|
|
32
|
+
"@stone-js/testing": "^0.8.13",
|
|
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.13",
|
|
24
|
+
"@stone-js/use-react": "^0.8.13",
|
|
25
|
+
"@stone-js/browser-adapter": "^0.8.13"
|
|
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.13",
|
|
32
|
+
"@stone-js/testing": "^0.8.13",
|
|
33
33
|
"@types/node": "^24.0.7",
|
|
34
34
|
"@types/react": "^19.0.7",
|
|
35
35
|
"@types/react-dom": "^19.0.3",
|
|
@@ -1,192 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { View } from 'react-native'
|
|
2
3
|
import { StatusBar } from 'expo-status-bar'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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'
|
|
4
|
+
import { useStoneTheme } from './app/theme'
|
|
5
|
+
import { StoneNativeApp } from '@stone-js/use-react-native'
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
|
-
*
|
|
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.
|
|
8
|
+
* The root component.
|
|
34
9
|
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
10
|
+
* `StoneNativeApp` shows the screen on top of the stack, which is all a first run needs, and it
|
|
11
|
+
* needs nothing installed to do it. When the application grows a real navigator, this is the
|
|
12
|
+
* component it replaces: the stack is public state, so `@react-navigation/native-stack` drives
|
|
13
|
+
* itself from the same object through `useScreens()`. The README shows that wiring.
|
|
14
|
+
*
|
|
15
|
+
* @returns The application.
|
|
74
16
|
*/
|
|
75
|
-
export default function App ():
|
|
76
|
-
const
|
|
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
|
|
17
|
+
export default function App (): JSX.Element {
|
|
18
|
+
const theme = useStoneTheme()
|
|
100
19
|
|
|
101
20
|
return (
|
|
102
|
-
<View style={
|
|
103
|
-
<StatusBar style='
|
|
104
|
-
<
|
|
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>
|
|
21
|
+
<View style={{ flex: 1, backgroundColor: theme.ink }}>
|
|
22
|
+
<StatusBar style='auto' />
|
|
23
|
+
<StoneNativeApp />
|
|
155
24
|
</View>
|
|
156
25
|
)
|
|
157
26
|
}
|
|
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
|
-
})
|
|
@@ -1,49 +1,150 @@
|
|
|
1
|
-
# Stone.js
|
|
1
|
+
# Stone.js - Basic React Native starter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Stone.js's basic starter to create a React Native (Expo) app using the declarative API.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The same welcome screen the web starters show, on a phone. Same mark, same palette, same words,
|
|
6
|
+
because it is the same domain: `app/HomeScreen.tsx` is a page, with `handle`, `head` and `render`,
|
|
7
|
+
and only `render` knows it is native.
|
|
6
8
|
|
|
7
|
-
|
|
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`.
|
|
9
|
+
## Project Setup
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
```sh
|
|
12
|
+
npm install
|
|
13
|
+
```
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
### Run it
|
|
17
16
|
|
|
18
|
-
```
|
|
19
|
-
npm
|
|
20
|
-
npm run ios
|
|
21
|
-
npm
|
|
17
|
+
```sh
|
|
18
|
+
npm start # Expo dev server: press i for iOS, a for Android
|
|
19
|
+
npm run ios # straight to the iOS simulator
|
|
20
|
+
npm run android # straight to an Android emulator
|
|
22
21
|
```
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
### Run it in a browser
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
The fastest loop is often a browser tab: Fast Refresh, and the same code then runs on a device
|
|
26
|
+
untouched. It is opt-in, because the web target needs two dependencies a native-only app has no
|
|
27
|
+
reason to carry.
|
|
28
28
|
|
|
29
|
-
```
|
|
29
|
+
```sh
|
|
30
30
|
npx expo install react-dom react-native-web
|
|
31
|
-
|
|
31
|
+
npm run web
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
Use it for the domain, the navigation and most of the interface. Use a device before believing
|
|
35
|
+
anything about the parts that are actually native: `react-native-web` covers the core primitives,
|
|
36
|
+
not the camera, secure storage or a native gesture handler.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
### Bundle for production
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm run export # a Hermes bytecode bundle, per platform
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
An installable build is `npx expo run:ios` or an [EAS build](https://docs.expo.dev/build/setup/):
|
|
45
|
+
both need a native toolchain, and they are better commands than a wrapper would be.
|
|
46
|
+
|
|
47
|
+
### Run tests
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npm run test
|
|
51
|
+
npm run test:cvg
|
|
52
|
+
```
|
|
39
53
|
|
|
40
|
-
The
|
|
54
|
+
The tests boot the real kernel, router, adapter and renderer under Node, send a deep link, and
|
|
55
|
+
assert what landed on the navigation stack. No device, no simulator, no Metro.
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
### Type-check
|
|
58
|
+
|
|
59
|
+
```sh
|
|
44
60
|
npm run typecheck
|
|
45
61
|
```
|
|
46
62
|
|
|
47
|
-
## What is
|
|
63
|
+
## What is where
|
|
64
|
+
|
|
65
|
+
| File | What it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `app/Application.ts` | Enables the router, the adapter and the renderer. Four decorators. |
|
|
68
|
+
| `app/HomeScreen.tsx` | The page answering `/`. Adding a screen is adding a file like it. |
|
|
69
|
+
| `app/WelcomeView.tsx` | What the screen draws, in React Native components. |
|
|
70
|
+
| `app/theme.ts` | The Stone.js palette, following the device's light or dark appearance. |
|
|
71
|
+
| `index.ts` | Polyfills, boot, and `registerRootComponent`. |
|
|
72
|
+
| `metro.config.js` | `withStone`: collects `app/` into `.stone/modules.ts` before Metro bundles. |
|
|
73
|
+
|
|
74
|
+
## Nothing lists your screens
|
|
75
|
+
|
|
76
|
+
A web application never lists its pages, and this one does not either. `withStone` collects
|
|
77
|
+
everything under `app/` and writes `.stone/modules.ts` whenever Metro starts, so `expo start`,
|
|
78
|
+
`expo run:ios` and an EAS build all get it without anyone remembering to ask.
|
|
79
|
+
|
|
80
|
+
It runs when Metro starts, not while it runs: **adding** a screen to a running dev server means
|
|
81
|
+
restarting it. **Editing** one needs nothing, Fast Refresh was never involved.
|
|
82
|
+
|
|
83
|
+
`.stone/` is generated. It is in `.gitignore`, and there is no reason to open it.
|
|
84
|
+
|
|
85
|
+
## Navigating
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useNavigate, useGoBack } from '@stone-js/use-react-native'
|
|
89
|
+
|
|
90
|
+
const navigate = useNavigate()
|
|
91
|
+
|
|
92
|
+
navigate('/tasks/42') // push a screen
|
|
93
|
+
navigate('/tasks/42', 'replace') // swap the current one
|
|
94
|
+
navigate('/sign-in', 'reset') // start again, leaving no history
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Navigation goes through the router, so a screen never renders another screen itself. `useGoBack()`
|
|
98
|
+
returns `{ goBack, canGoBack }`: wire `goBack` to your header button and to Android's hardware back
|
|
99
|
+
button, and let the platform leave the application when `canGoBack` is false.
|
|
100
|
+
|
|
101
|
+
Deep links arrive the same way. This starter's scheme is `stone`, so `stone://app/?name=Ada` reaches
|
|
102
|
+
the page that owns `/`, with `name` readable through `event.get('name')`. Change `scheme` in
|
|
103
|
+
`app.json` to your own.
|
|
104
|
+
|
|
105
|
+
## Using a native navigator
|
|
106
|
+
|
|
107
|
+
`App.tsx` renders `StoneNativeApp`, which shows the screen on top of the stack. That is the floor,
|
|
108
|
+
not the ceiling: it is what makes the first run work with nothing installed. The platform's
|
|
109
|
+
transitions, the swipe-back gesture, and a screen keeping its own state while another covers it are
|
|
110
|
+
things only a native navigator gives you.
|
|
111
|
+
|
|
112
|
+
The stack is public state, so a navigator drives itself from it:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { useScreens } from '@stone-js/use-react-native'
|
|
116
|
+
import { NavigationContainer } from '@react-navigation/native'
|
|
117
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
|
118
|
+
|
|
119
|
+
const Stack = createNativeStackNavigator()
|
|
120
|
+
|
|
121
|
+
export default function App () {
|
|
122
|
+
const screens = useScreens()
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<NavigationContainer>
|
|
126
|
+
<Stack.Navigator>
|
|
127
|
+
{screens.map((screen) => (
|
|
128
|
+
<Stack.Screen key={screen.key} name={screen.key} options={{ title: screen.title }}>
|
|
129
|
+
{() => screen.element}
|
|
130
|
+
</Stack.Screen>
|
|
131
|
+
))}
|
|
132
|
+
</Stack.Navigator>
|
|
133
|
+
</NavigationContainer>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Nothing about your pages changes.
|
|
139
|
+
|
|
140
|
+
## The imperative twin
|
|
141
|
+
|
|
142
|
+
The same application, written with values instead of decorators, is
|
|
143
|
+
[`basic-react-native-imperative`](../basic-react-native-imperative). Neither paradigm wraps the
|
|
144
|
+
other; both write to the same manifest.
|
|
145
|
+
|
|
146
|
+
## Learn more
|
|
48
147
|
|
|
49
|
-
|
|
148
|
+
- [Stone.js documentation](https://stonejs.dev/docs)
|
|
149
|
+
- [`@stone-js/use-react-native`](https://www.npmjs.com/package/@stone-js/use-react-native)
|
|
150
|
+
- [`@stone-js/react-native-adapter`](https://www.npmjs.com/package/@stone-js/react-native-adapter)
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { Routing } from '@stone-js/router'
|
|
2
2
|
import { LogLevel, StoneApp } from '@stone-js/core'
|
|
3
|
-
import {
|
|
3
|
+
import { ReactNative } from '@stone-js/react-native-adapter'
|
|
4
|
+
import { UseReactNative } from '@stone-js/use-react-native'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Application
|
|
7
8
|
*
|
|
8
9
|
* This is the main application entry point.
|
|
9
10
|
*
|
|
10
|
-
* @Routing()
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* @Routing() is used to enable the router.
|
|
12
|
+
* @ReactNative() is used to enable the React Native adapter, which turns deep links and
|
|
13
|
+
* in-app navigation into events.
|
|
14
|
+
* @UseReactNative() is used to enable the React Native renderer, which turns what a page
|
|
15
|
+
* resolves into a screen.
|
|
16
|
+
* @StoneApp() is used to enable the Stone application, it is required.
|
|
17
|
+
*
|
|
18
|
+
* The web starter's Application carries the same decorators with two swapped: `@Browser()` for
|
|
19
|
+
* `@ReactNative()`, `@UseReact()` for `@UseReactNative()`. That is the promise, in four lines:
|
|
20
|
+
* the domain is written once, and what changes is the context it runs in.
|
|
21
|
+
*
|
|
22
|
+
* Screens live in their own files, one route each. See `app/HomeScreen.tsx`.
|
|
14
23
|
*/
|
|
15
24
|
@Routing()
|
|
16
|
-
@
|
|
25
|
+
@ReactNative()
|
|
26
|
+
@UseReactNative()
|
|
27
|
+
@StoneApp({ logger: { level: LogLevel.INFO } })
|
|
17
28
|
export class Application {}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { WelcomeView } from './WelcomeView'
|
|
3
|
+
import { ILogger, Promiseable } from '@stone-js/core'
|
|
4
|
+
import { HeadContext, IPage, Page, PageHeadContext, PageRenderContext, ReactIncomingEvent } from '@stone-js/use-react-native'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The home screen.
|
|
8
|
+
*
|
|
9
|
+
* A page, exactly as on the web: `@Page` binds it to a route, `handle` answers, `head` names it and
|
|
10
|
+
* `render` draws it. The only native thing here is what `render` returns, and it returns React
|
|
11
|
+
* Native components instead of DOM ones.
|
|
12
|
+
*
|
|
13
|
+
* Adding a screen is adding a file like this one. Nothing lists it: `withStone` collects everything
|
|
14
|
+
* under `app/` before Metro bundles.
|
|
15
|
+
*/
|
|
16
|
+
@Page('/')
|
|
17
|
+
export class HomeScreen implements IPage<ReactIncomingEvent> {
|
|
18
|
+
/**
|
|
19
|
+
* Logger is a service for logging.
|
|
20
|
+
*/
|
|
21
|
+
private readonly logger: ILogger
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new instance of HomeScreen.
|
|
25
|
+
* At this point, all the dependencies are resolved and injected.
|
|
26
|
+
*
|
|
27
|
+
* @param logger - Logger service.
|
|
28
|
+
*/
|
|
29
|
+
constructor ({ logger }: { logger: ILogger }) {
|
|
30
|
+
this.logger = logger
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handle incoming events.
|
|
35
|
+
*
|
|
36
|
+
* A deep link carries its parameters here: `stone://app/?name=Ada` arrives as an event, and
|
|
37
|
+
* `event.get` reads from it the same way an HTTP request would be read.
|
|
38
|
+
*
|
|
39
|
+
* @param event - Incoming event.
|
|
40
|
+
* @returns The screen's data.
|
|
41
|
+
*/
|
|
42
|
+
handle (event: ReactIncomingEvent): ResponseData {
|
|
43
|
+
const message = `Hello ${String(event.get<string>('name', 'World'))}!`
|
|
44
|
+
|
|
45
|
+
this.logger.info(message)
|
|
46
|
+
|
|
47
|
+
return { message }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set the screen's head.
|
|
52
|
+
*
|
|
53
|
+
* A phone has no meta tags, so the title is what a navigator shows in its header. The same `head`
|
|
54
|
+
* a web page declares, read by a different renderer.
|
|
55
|
+
*
|
|
56
|
+
* @returns The head context.
|
|
57
|
+
*/
|
|
58
|
+
head ({ event }: PageHeadContext): Promiseable<HeadContext> {
|
|
59
|
+
return {
|
|
60
|
+
title: `${String(event.get<string>('name', 'World'))} · Welcome to Stone.js`,
|
|
61
|
+
description: 'A universal Stone.js application. Write your domain once; Stone.js is the context that runs it anywhere.'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render the screen.
|
|
67
|
+
*
|
|
68
|
+
* @returns The rendered screen.
|
|
69
|
+
*/
|
|
70
|
+
render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
|
|
71
|
+
return <WelcomeView message={data?.message} />
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Response data
|
|
77
|
+
*/
|
|
78
|
+
export interface ResponseData {
|
|
79
|
+
message: string
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { Image, useColorScheme } from 'react-native'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Le Portail, the Stone.js mark.
|
|
6
|
+
*
|
|
7
|
+
* The same three arcs as `public/logo.svg` in the web starters: two stone segments rising, and the
|
|
8
|
+
* keystone lit in ember. Two files rather than one, because the segments have to contrast with the
|
|
9
|
+
* ground the screen paints, exactly as the brand ships a dark-ground and a light-ground Portal.
|
|
10
|
+
*
|
|
11
|
+
* Shipped as an image on purpose. Drawing it would mean `react-native-svg`, a native module, and
|
|
12
|
+
* a starter's first job is to run on a device the moment it is installed, with nothing to link
|
|
13
|
+
* and no version to match against Expo Go's.
|
|
14
|
+
*
|
|
15
|
+
* @param props.size - Side length in points.
|
|
16
|
+
* @returns The mark.
|
|
17
|
+
*/
|
|
18
|
+
export function PortalMark ({ size = 104 }: { size?: number }): JSX.Element {
|
|
19
|
+
const scheme = useColorScheme()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Image
|
|
23
|
+
accessibilityRole='image'
|
|
24
|
+
accessibilityLabel='Stone.js'
|
|
25
|
+
source={scheme === 'light'
|
|
26
|
+
? require('../assets/logo-light.png')
|
|
27
|
+
: require('../assets/logo-dark.png')}
|
|
28
|
+
style={{ width: size, height: size }}
|
|
29
|
+
resizeMode='contain'
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|