@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { PortalMark } from './PortalMark'
|
|
3
|
+
import { useStoneTheme } from './theme'
|
|
4
|
+
import { Linking, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
|
|
5
|
+
|
|
6
|
+
const DOCS = 'https://stonejs.dev/docs'
|
|
7
|
+
const GITHUB = 'https://github.com/stone-foundation/stone-js-framework'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The welcome screen.
|
|
11
|
+
*
|
|
12
|
+
* The native twin of the web starters' welcome page: the same mark, the same palette, the same
|
|
13
|
+
* words. What differs is only what a phone does differently, which is the whole point of the
|
|
14
|
+
* framework: `View` instead of `div`, a `StyleSheet` instead of a stylesheet, `Linking` instead of
|
|
15
|
+
* an anchor, and a glow drawn as a circle because there is no `radial-gradient`.
|
|
16
|
+
*
|
|
17
|
+
* @param props.message - What the handler returned.
|
|
18
|
+
* @returns The screen.
|
|
19
|
+
*/
|
|
20
|
+
export function WelcomeView ({ message }: { message?: string }): JSX.Element {
|
|
21
|
+
const theme = useStoneTheme()
|
|
22
|
+
|
|
23
|
+
const open = (url: string): void => { void Linking.openURL(url) }
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<View style={[styles.screen, { backgroundColor: theme.ink }]}>
|
|
27
|
+
<View style={[styles.glow, { backgroundColor: theme.inkGlow }]} pointerEvents='none' />
|
|
28
|
+
|
|
29
|
+
<View style={styles.hero}>
|
|
30
|
+
<PortalMark size={104} />
|
|
31
|
+
|
|
32
|
+
<Text style={[styles.eyebrow, { color: theme.creamDim }]}>WELCOME TO</Text>
|
|
33
|
+
<Text style={[styles.title, { color: theme.ember2 }]}>Stone.js</Text>
|
|
34
|
+
<Text style={[styles.lead, { color: theme.cream }]}>{message}</Text>
|
|
35
|
+
<Text style={[styles.tagline, { color: theme.creamDim }]}>
|
|
36
|
+
Your app is running. Write your domain once, Stone.js is the context that runs it
|
|
37
|
+
anywhere: server, serverless, browser, CLI and the edge.
|
|
38
|
+
</Text>
|
|
39
|
+
|
|
40
|
+
<View style={styles.links}>
|
|
41
|
+
<Pressable
|
|
42
|
+
accessibilityRole='link'
|
|
43
|
+
onPress={() => open(DOCS)}
|
|
44
|
+
style={[styles.pill, { borderColor: theme.line, backgroundColor: theme.card }]}
|
|
45
|
+
>
|
|
46
|
+
<Text style={[styles.pillText, { color: theme.cream }]}>Documentation</Text>
|
|
47
|
+
</Pressable>
|
|
48
|
+
<Pressable
|
|
49
|
+
accessibilityRole='link'
|
|
50
|
+
onPress={() => open(GITHUB)}
|
|
51
|
+
style={[styles.pill, { borderColor: theme.line, backgroundColor: theme.card }]}
|
|
52
|
+
>
|
|
53
|
+
<Text style={[styles.pillText, { color: theme.cream }]}>GitHub</Text>
|
|
54
|
+
</Pressable>
|
|
55
|
+
</View>
|
|
56
|
+
|
|
57
|
+
<Text style={[styles.edit, { color: theme.creamDim }]}>
|
|
58
|
+
Edit <Text style={[styles.editFile, { color: theme.ember1 }]}>app/HomeScreen.tsx</Text>
|
|
59
|
+
</Text>
|
|
60
|
+
</View>
|
|
61
|
+
|
|
62
|
+
<Text style={[styles.brand, { color: theme.creamDim }]}>
|
|
63
|
+
<Text style={{ color: theme.ember2 }}>●</Text> Stone.js · the continuum framework
|
|
64
|
+
</Text>
|
|
65
|
+
</View>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const styles = StyleSheet.create({
|
|
70
|
+
screen: {
|
|
71
|
+
flex: 1,
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
justifyContent: 'center',
|
|
74
|
+
paddingHorizontal: 24,
|
|
75
|
+
paddingVertical: 48,
|
|
76
|
+
overflow: 'hidden'
|
|
77
|
+
},
|
|
78
|
+
// The web starter's ember glow, as the only shape a phone can draw it with.
|
|
79
|
+
glow: {
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
top: '4%',
|
|
82
|
+
width: 420,
|
|
83
|
+
height: 420,
|
|
84
|
+
borderRadius: 210,
|
|
85
|
+
opacity: 0.55
|
|
86
|
+
},
|
|
87
|
+
hero: {
|
|
88
|
+
width: '100%',
|
|
89
|
+
maxWidth: 420,
|
|
90
|
+
alignItems: 'center'
|
|
91
|
+
},
|
|
92
|
+
eyebrow: {
|
|
93
|
+
marginTop: 22,
|
|
94
|
+
fontSize: 12,
|
|
95
|
+
fontWeight: '600',
|
|
96
|
+
letterSpacing: 3.4
|
|
97
|
+
},
|
|
98
|
+
title: {
|
|
99
|
+
marginTop: 6,
|
|
100
|
+
fontSize: 52,
|
|
101
|
+
fontWeight: '800',
|
|
102
|
+
letterSpacing: -1.6
|
|
103
|
+
},
|
|
104
|
+
lead: {
|
|
105
|
+
marginTop: 16,
|
|
106
|
+
fontSize: 20,
|
|
107
|
+
fontWeight: '600',
|
|
108
|
+
textAlign: 'center'
|
|
109
|
+
},
|
|
110
|
+
tagline: {
|
|
111
|
+
marginTop: 10,
|
|
112
|
+
fontSize: 15,
|
|
113
|
+
lineHeight: 24,
|
|
114
|
+
textAlign: 'center'
|
|
115
|
+
},
|
|
116
|
+
links: {
|
|
117
|
+
marginTop: 30,
|
|
118
|
+
flexDirection: 'row',
|
|
119
|
+
flexWrap: 'wrap',
|
|
120
|
+
justifyContent: 'center',
|
|
121
|
+
gap: 12
|
|
122
|
+
},
|
|
123
|
+
pill: {
|
|
124
|
+
borderWidth: 1,
|
|
125
|
+
borderRadius: 12,
|
|
126
|
+
paddingVertical: 11,
|
|
127
|
+
paddingHorizontal: 18
|
|
128
|
+
},
|
|
129
|
+
pillText: {
|
|
130
|
+
fontSize: 14,
|
|
131
|
+
fontWeight: '600'
|
|
132
|
+
},
|
|
133
|
+
edit: {
|
|
134
|
+
marginTop: 18,
|
|
135
|
+
fontSize: 13,
|
|
136
|
+
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' })
|
|
137
|
+
},
|
|
138
|
+
editFile: {
|
|
139
|
+
fontWeight: '700'
|
|
140
|
+
},
|
|
141
|
+
brand: {
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
bottom: 32,
|
|
144
|
+
fontSize: 13
|
|
145
|
+
}
|
|
146
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useColorScheme } from 'react-native'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Stone.js brand, "Obsidienne & Braise", as values a StyleSheet can use.
|
|
5
|
+
*
|
|
6
|
+
* The same palette the web starters paint in CSS custom properties. A native application has no
|
|
7
|
+
* cascade, so the tokens are plain values and the screen reads them through `useStoneTheme`.
|
|
8
|
+
*/
|
|
9
|
+
export interface StoneTheme {
|
|
10
|
+
/** The page ground. */
|
|
11
|
+
ink: string
|
|
12
|
+
/** The glow at the top of the ground, behind the mark. */
|
|
13
|
+
inkGlow: string
|
|
14
|
+
/** Primary text. */
|
|
15
|
+
cream: string
|
|
16
|
+
/** Secondary text. */
|
|
17
|
+
creamDim: string
|
|
18
|
+
/** The ember gradient, light end. Also the accent for small text. */
|
|
19
|
+
ember1: string
|
|
20
|
+
/** The ember gradient, dark end. */
|
|
21
|
+
ember2: string
|
|
22
|
+
/** Hairlines and card borders. */
|
|
23
|
+
line: string
|
|
24
|
+
/** Card and pill surfaces. */
|
|
25
|
+
card: string
|
|
26
|
+
/** The mark's two arcs. */
|
|
27
|
+
mark: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dark: StoneTheme = {
|
|
31
|
+
ink: '#14110F',
|
|
32
|
+
inkGlow: '#241C17',
|
|
33
|
+
cream: '#F4EFE6',
|
|
34
|
+
creamDim: '#A69A8C',
|
|
35
|
+
ember1: '#FFC46B',
|
|
36
|
+
ember2: '#FF5A1F',
|
|
37
|
+
line: 'rgba(244, 239, 230, 0.10)',
|
|
38
|
+
card: 'rgba(244, 239, 230, 0.05)',
|
|
39
|
+
mark: '#E2D9CC'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const light: StoneTheme = {
|
|
43
|
+
ink: '#F4EFE6',
|
|
44
|
+
inkGlow: '#FFFFFF',
|
|
45
|
+
cream: '#1B1613',
|
|
46
|
+
creamDim: '#6B6154',
|
|
47
|
+
// On a light ground the ember needs the darker end to stay readable, per the brand's
|
|
48
|
+
// contrast notes: `braise.clair` for large elements, `braise.texte` for small text.
|
|
49
|
+
ember1: '#E04E10',
|
|
50
|
+
ember2: '#A83A08',
|
|
51
|
+
line: 'rgba(27, 22, 19, 0.12)',
|
|
52
|
+
card: 'rgba(27, 22, 19, 0.04)',
|
|
53
|
+
mark: '#1B1613'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The palette matching the device's appearance.
|
|
58
|
+
*
|
|
59
|
+
* Dark is the default, as in the web starters: `useColorScheme` returns `null` when the platform
|
|
60
|
+
* expresses no preference, and the brand's ground is obsidian.
|
|
61
|
+
*
|
|
62
|
+
* @returns The active theme.
|
|
63
|
+
*/
|
|
64
|
+
export function useStoneTheme (): StoneTheme {
|
|
65
|
+
return useColorScheme() === 'light' ? light : dark
|
|
66
|
+
}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"expo": {
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "Stone.js",
|
|
4
4
|
"slug": "basic-react-native-declarative",
|
|
5
|
+
"scheme": "stone",
|
|
5
6
|
"version": "1.0.0",
|
|
6
7
|
"orientation": "portrait",
|
|
7
8
|
"icon": "./assets/icon.png",
|
|
8
|
-
"userInterfaceStyle": "
|
|
9
|
+
"userInterfaceStyle": "automatic",
|
|
10
|
+
"backgroundColor": "#14110F",
|
|
11
|
+
"splash": {
|
|
12
|
+
"image": "./assets/splash-icon.png",
|
|
13
|
+
"resizeMode": "contain",
|
|
14
|
+
"backgroundColor": "#14110F"
|
|
15
|
+
},
|
|
9
16
|
"ios": {
|
|
10
17
|
"supportsTablet": true
|
|
11
18
|
},
|
|
12
19
|
"android": {
|
|
13
20
|
"adaptiveIcon": {
|
|
14
|
-
"backgroundColor": "#
|
|
21
|
+
"backgroundColor": "#14110F",
|
|
15
22
|
"foregroundImage": "./assets/android-icon-foreground.png",
|
|
16
23
|
"backgroundImage": "./assets/android-icon-background.png",
|
|
17
24
|
"monochromeImage": "./assets/android-icon-monochrome.png"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Platform polyfills come first, before anything from Stone.js loads.
|
|
3
3
|
*
|
|
4
|
-
* - React Native's built-in `URL` is a stub (no `pathname`, no `searchParams`)
|
|
5
|
-
*
|
|
6
|
-
* - `TextEncoder` is installed only when the
|
|
7
|
-
* (older Hermes versions).
|
|
4
|
+
* - React Native's built-in `URL` is a stub (no `pathname`, no `searchParams`), and the router
|
|
5
|
+
* reads `pathname` on every event, so the polyfill is required rather than nice to have.
|
|
6
|
+
* - `TextEncoder` is installed only when the engine does not provide it (older Hermes).
|
|
8
7
|
*/
|
|
9
8
|
import 'react-native-url-polyfill/auto'
|
|
10
9
|
|
|
@@ -12,11 +11,20 @@ if (typeof globalThis.TextEncoder === 'undefined') {
|
|
|
12
11
|
require('fast-text-encoding')
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
import { stoneApp } from '@stone-js/core'
|
|
15
15
|
import { registerRootComponent } from 'expo'
|
|
16
|
-
|
|
17
16
|
import App from './App'
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Every module under `app/`, collected at build time.
|
|
20
|
+
*
|
|
21
|
+
* Generated by `withStone` in `metro.config.js`, because collecting modules is a bundler question
|
|
22
|
+
* and Metro has no `import.meta.glob`. Never edit it, never commit it.
|
|
23
|
+
*/
|
|
24
|
+
import { modules } from './.stone/modules'
|
|
25
|
+
|
|
26
|
+
// Boot the framework, then hand the root component to Expo. `registerRootComponent` works the same
|
|
27
|
+
// in Expo Go and in a native build.
|
|
28
|
+
void stoneApp({ modules }).run()
|
|
29
|
+
|
|
22
30
|
registerRootComponent(App)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
const { getDefaultConfig } = require('expo/metro-config')
|
|
2
|
+
const { withStone } = require('@stone-js/use-react-native/metro')
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* `withStone` collects every module under `app/` and writes `.stone/modules.ts` before Metro
|
|
6
|
+
* bundles, because Metro resolves imports statically and has no `import.meta.glob`. It hooks in
|
|
7
|
+
* here rather than in a command on purpose: Metro loads this file whatever started it, so
|
|
8
|
+
* `expo start`, `expo run:ios` and an EAS build all get the generation without anyone asking.
|
|
9
|
+
*
|
|
10
|
+
* Pinning the project root also keeps Metro scoped to this starter, even when it lives inside a
|
|
11
|
+
* larger workspace, as it does in the Stone.js monorepo.
|
|
6
12
|
*/
|
|
7
|
-
module.exports = getDefaultConfig(__dirname)
|
|
13
|
+
module.exports = withStone(getDefaultConfig(__dirname), __dirname)
|
|
@@ -15,14 +15,17 @@
|
|
|
15
15
|
"ios": "expo start --ios",
|
|
16
16
|
"web": "expo start --web",
|
|
17
17
|
"export": "expo export",
|
|
18
|
-
"
|
|
18
|
+
"modules": "node -e \"require('@stone-js/use-react-native/metro').writeManifest(process.cwd())\"",
|
|
19
|
+
"typecheck": "npm run modules && tsc --noEmit",
|
|
19
20
|
"test": "vitest run",
|
|
20
21
|
"test:cvg": "npm run test -- --coverage"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@stone-js/browser-core": "^0.8.
|
|
24
|
-
"@stone-js/core": "^0.8.
|
|
25
|
-
"@stone-js/
|
|
24
|
+
"@stone-js/browser-core": "^0.8.13",
|
|
25
|
+
"@stone-js/core": "^0.8.13",
|
|
26
|
+
"@stone-js/react-native-adapter": "^0.8.13",
|
|
27
|
+
"@stone-js/router": "^0.8.13",
|
|
28
|
+
"@stone-js/use-react-native": "^0.8.13",
|
|
26
29
|
"expo": "~57.0.14",
|
|
27
30
|
"expo-status-bar": "~57.0.1",
|
|
28
31
|
"fast-text-encoding": "^1.0.6",
|
|
@@ -32,6 +35,8 @@
|
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@babel/plugin-proposal-decorators": "^7.28.0",
|
|
38
|
+
"@stone-js/cli": "^0.8.13",
|
|
39
|
+
"@stone-js/testing": "^0.8.13",
|
|
35
40
|
"@types/react": "~19.2.2",
|
|
36
41
|
"@vitest/coverage-v8": "^3.0.9",
|
|
37
42
|
"babel-preset-expo": "^57.0.7",
|
|
@@ -1,65 +1,63 @@
|
|
|
1
1
|
import { stoneApp } from '@stone-js/core'
|
|
2
|
-
import { describe, it, expect, beforeAll } from 'vitest'
|
|
3
2
|
import { Application } from '../app/Application'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { onNativeError, onNativeRender } from '../adapter/renderSink'
|
|
3
|
+
import { HomeScreen } from '../app/HomeScreen'
|
|
4
|
+
import { ScreenStack } from '@stone-js/use-react-native'
|
|
5
|
+
import { NavigationSource } from '@stone-js/react-native-adapter'
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
11
|
-
* application boots (domain + router + native adapter) are booted here under
|
|
12
|
-
* Node, navigation intents are emitted, and the rendered payloads asserted.
|
|
8
|
+
* The whole application, booted under Node and asked real questions.
|
|
13
9
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* Nothing is mocked. The kernel, the router, the adapter and the renderer are the ones a phone
|
|
11
|
+
* runs; what a device adds is a screen to draw on, and this asserts what the framework resolved
|
|
12
|
+
* before drawing. Two things are supplied rather than discovered, both through documented
|
|
13
|
+
* configuration: the navigation source, so the test can send a deep link, and the screen stack, so
|
|
14
|
+
* it can read what landed on it.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* twice in one process would accumulate route definitions.
|
|
16
|
+
* Screens are listed here because the generated manifest (`.stone/modules.ts`) is Metro's, written
|
|
17
|
+
* when Metro starts. Under a test runner the modules are imported directly, which is the same set.
|
|
20
18
|
*/
|
|
21
|
-
describe('
|
|
22
|
-
const
|
|
23
|
-
const
|
|
19
|
+
describe('Screens', () => {
|
|
20
|
+
const screens = ScreenStack.create()
|
|
21
|
+
const navigation = NavigationSource.create({ baseUrl: 'stone://app' })
|
|
24
22
|
|
|
23
|
+
// Booted once for the whole suite, as on a device: the default blueprints are shared module
|
|
24
|
+
// objects, so booting the same modules twice in one process would accumulate route definitions.
|
|
25
25
|
beforeAll(async () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
await stoneApp({
|
|
27
|
+
modules: [
|
|
28
|
+
Application,
|
|
29
|
+
HomeScreen,
|
|
30
|
+
{ stone: { reactNative: { navigationSource: navigation }, useReactNative: { screenStack: screens } } }
|
|
31
|
+
]
|
|
32
|
+
}).run()
|
|
29
33
|
})
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
expect(errors).toHaveLength(0)
|
|
33
|
-
expect(rendered).toHaveLength(1)
|
|
35
|
+
const settle = async (): Promise<void> => { await new Promise((resolve) => setImmediate(resolve)) }
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
expect(
|
|
37
|
-
expect(
|
|
38
|
-
expect(
|
|
37
|
+
it('puts the home screen on the stack at launch', () => {
|
|
38
|
+
expect(screens.size()).toBe(1)
|
|
39
|
+
expect(screens.top()?.path).toBe('/')
|
|
40
|
+
expect(screens.top()?.title).toBe('World · Welcome to Stone.js')
|
|
39
41
|
})
|
|
40
42
|
|
|
41
|
-
it('routes a
|
|
42
|
-
|
|
43
|
-
await
|
|
43
|
+
it('routes a deep link to the screen that owns it, parameters included', async () => {
|
|
44
|
+
navigation.navigate('stone://app/?name=Ada')
|
|
45
|
+
await settle()
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
expect(payload.route).toBe('/hello/Noowow')
|
|
47
|
-
expect(payload.message).toContain('Hello Noowow!')
|
|
47
|
+
expect(screens.top()?.title).toBe('Ada · Welcome to Stone.js')
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it('
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const payload = rendered.at(-1)?.content as WelcomeData
|
|
55
|
-
expect(payload.message).toContain('Hello Mr. Stone!')
|
|
50
|
+
it('re-resolving the current route replaces it instead of stacking a duplicate', () => {
|
|
51
|
+
// The deep link above resolved `/` again with fresh data. A user walking back must not find
|
|
52
|
+
// two identical screens, so the stack swaps the top rather than growing.
|
|
53
|
+
expect(screens.size()).toBe(1)
|
|
56
54
|
})
|
|
57
55
|
|
|
58
|
-
it('surfaces unknown
|
|
59
|
-
|
|
60
|
-
await
|
|
56
|
+
it('surfaces an unknown route instead of crashing', async () => {
|
|
57
|
+
navigation.navigate('stone://app/nowhere')
|
|
58
|
+
await settle()
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
expect(
|
|
60
|
+
// An error page is still a screen: the application stays up and shows something.
|
|
61
|
+
expect(screens.top()?.path).not.toBe('/')
|
|
64
62
|
})
|
|
65
63
|
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `react-native` is under a Node test runner: absent.
|
|
3
|
+
*
|
|
4
|
+
* The screen imports `View`, `Text` and friends at module scope, so the specifier has to resolve to
|
|
5
|
+
* something for the application to even load. It resolves to this: the primitives as inert
|
|
6
|
+
* identifiers, which is enough because a resolved screen is a React element that has been *built*,
|
|
7
|
+
* not mounted. React never calls these components here, so there is nothing for them to do.
|
|
8
|
+
*
|
|
9
|
+
* That is the line this test draws, and it is the honest one. What runs under Node is the domain:
|
|
10
|
+
* the kernel, the router, the loader, the head. What a `View` looks like is React Native's business,
|
|
11
|
+
* and the way to see it is to run the application, either on a device or in a browser tab through
|
|
12
|
+
* `npx expo start --web`.
|
|
13
|
+
*/
|
|
14
|
+
const component = (name: string): string => name
|
|
15
|
+
|
|
16
|
+
export const View = component('View')
|
|
17
|
+
export const Text = component('Text')
|
|
18
|
+
export const Image = component('Image')
|
|
19
|
+
export const Pressable = component('Pressable')
|
|
20
|
+
export const ScrollView = component('ScrollView')
|
|
21
|
+
|
|
22
|
+
export const StyleSheet = {
|
|
23
|
+
create: <T>(styles: T): T => styles,
|
|
24
|
+
flatten: (style: unknown): unknown => style
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Platform = {
|
|
28
|
+
OS: 'ios',
|
|
29
|
+
select: <T>(spec: Record<string, T>): T | undefined => spec.ios ?? spec.default
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Linking = {
|
|
33
|
+
openURL: async (): Promise<void> => {},
|
|
34
|
+
getInitialURL: async (): Promise<string | null> => null,
|
|
35
|
+
addEventListener: () => ({ remove: () => {} })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const useColorScheme = (): string | null => null
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
View, Text, Image, Pressable, ScrollView, StyleSheet, Platform, Linking, useColorScheme
|
|
42
|
+
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
1
2
|
import { defineConfig } from 'vitest/config'
|
|
2
3
|
|
|
3
4
|
export default defineConfig({
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
5
|
+
// Under Node there is no React Native, and the screen imports its primitives at module scope, so
|
|
6
|
+
// the specifier has to resolve somewhere. `tests/stubs/react-native.ts` explains what it stands
|
|
7
|
+
// for and where the line is drawn.
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'react-native': fileURLToPath(new URL('./tests/stubs/react-native.ts', import.meta.url))
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
// `experimentalDecorators` is enabled in tsconfig.json for tsc TYPING only (the framework types
|
|
14
|
+
// its decorators with the legacy signatures). At RUNTIME Stone.js requires standard 2023-11
|
|
15
|
+
// decorators, so esbuild must ignore that flag and emit the standard semantics, `Symbol.metadata`
|
|
8
16
|
// included (esbuild 0.25+ implements them correctly on its own).
|
|
9
17
|
esbuild: {
|
|
10
18
|
tsconfigRaw: {
|
|
@@ -19,7 +27,7 @@ export default defineConfig({
|
|
|
19
27
|
include: ['./tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
|
20
28
|
coverage: {
|
|
21
29
|
provider: 'v8',
|
|
22
|
-
include: ['app/**/*.ts
|
|
30
|
+
include: ['app/**/*.{ts,tsx}'],
|
|
23
31
|
reporter: ['text', 'html'],
|
|
24
32
|
reportsDirectory: './coverage'
|
|
25
33
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { StatusBar } from 'expo-status-bar'
|
|
4
|
+
import { useStoneTheme } from './app/theme'
|
|
5
|
+
import { StoneNativeApp } from '@stone-js/use-react-native'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The root component.
|
|
9
|
+
*
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
export default function App (): JSX.Element {
|
|
18
|
+
const theme = useStoneTheme()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<View style={{ flex: 1, backgroundColor: theme.ink }}>
|
|
22
|
+
<StatusBar style='auto' />
|
|
23
|
+
<StoneNativeApp />
|
|
24
|
+
</View>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -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.
|