@stone-js/starters 0.8.10 → 0.8.12

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.
Files changed (78) hide show
  1. package/basic-react-declarative/package.json +5 -5
  2. package/basic-react-imperative/package.json +5 -5
  3. package/basic-react-native-declarative/App.tsx +16 -182
  4. package/basic-react-native-declarative/README.md +136 -21
  5. package/basic-react-native-declarative/app/Application.ts +17 -6
  6. package/basic-react-native-declarative/app/HomeScreen.tsx +80 -0
  7. package/basic-react-native-declarative/app/PortalMark.tsx +32 -0
  8. package/basic-react-native-declarative/app/WelcomeView.tsx +146 -0
  9. package/basic-react-native-declarative/app/theme.ts +66 -0
  10. package/basic-react-native-declarative/app.json +10 -3
  11. package/basic-react-native-declarative/assets/android-icon-background.png +0 -0
  12. package/basic-react-native-declarative/assets/android-icon-foreground.png +0 -0
  13. package/basic-react-native-declarative/assets/android-icon-monochrome.png +0 -0
  14. package/basic-react-native-declarative/assets/favicon.png +0 -0
  15. package/basic-react-native-declarative/assets/icon.png +0 -0
  16. package/basic-react-native-declarative/assets/logo-dark.png +0 -0
  17. package/basic-react-native-declarative/assets/logo-light.png +0 -0
  18. package/basic-react-native-declarative/assets/splash-icon.png +0 -0
  19. package/basic-react-native-declarative/index.ts +16 -8
  20. package/basic-react-native-declarative/metro.config.js +9 -3
  21. package/basic-react-native-declarative/package.json +9 -4
  22. package/basic-react-native-declarative/tests/Application.spec.ts +41 -43
  23. package/basic-react-native-declarative/tests/stubs/react-native.ts +42 -0
  24. package/basic-react-native-declarative/tsconfig.json +0 -1
  25. package/basic-react-native-declarative/vitest.config.ts +13 -5
  26. package/basic-react-native-imperative/App.tsx +26 -0
  27. package/basic-react-native-imperative/LICENSE +21 -0
  28. package/basic-react-native-imperative/README.md +150 -0
  29. package/basic-react-native-imperative/app/Application.ts +25 -0
  30. package/basic-react-native-imperative/app/HomeScreen.tsx +84 -0
  31. package/basic-react-native-imperative/app/PortalMark.tsx +32 -0
  32. package/basic-react-native-imperative/app/WelcomeView.tsx +146 -0
  33. package/basic-react-native-imperative/app/theme.ts +66 -0
  34. package/basic-react-native-imperative/app.json +32 -0
  35. package/basic-react-native-imperative/assets/android-icon-background.png +0 -0
  36. package/basic-react-native-imperative/assets/android-icon-foreground.png +0 -0
  37. package/basic-react-native-imperative/assets/android-icon-monochrome.png +0 -0
  38. package/basic-react-native-imperative/assets/favicon.png +0 -0
  39. package/basic-react-native-imperative/assets/icon.png +0 -0
  40. package/basic-react-native-imperative/assets/logo-dark.png +0 -0
  41. package/basic-react-native-imperative/assets/logo-light.png +0 -0
  42. package/basic-react-native-imperative/assets/splash-icon.png +0 -0
  43. package/basic-react-native-imperative/babel.config.js +16 -0
  44. package/basic-react-native-imperative/index.ts +30 -0
  45. package/basic-react-native-imperative/metro.config.js +13 -0
  46. package/basic-react-native-imperative/package.json +46 -0
  47. package/basic-react-native-imperative/tests/Application.spec.ts +63 -0
  48. package/basic-react-native-imperative/tests/stubs/react-native.ts +42 -0
  49. package/basic-react-native-imperative/tsconfig.json +15 -0
  50. package/basic-react-native-imperative/vitest.config.ts +35 -0
  51. package/basic-service-declarative/package.json +4 -4
  52. package/basic-service-imperative/package.json +4 -4
  53. package/continuum-showcase/package.json +9 -9
  54. package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
  55. package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  56. package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  57. package/full-react-declarative/package.json +10 -10
  58. package/full-react-imperative/app/layout/AppLayout/AppLayout.tsx +1 -1
  59. package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  60. package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  61. package/full-react-imperative/app/layout/SettingsLayout/SettingsLayout.tsx +1 -1
  62. package/full-react-imperative/package.json +10 -10
  63. package/full-service-declarative/package.json +10 -10
  64. package/full-service-imperative/package.json +10 -10
  65. package/package.json +29 -15
  66. package/standard-react-declarative/package.json +9 -9
  67. package/standard-react-imperative/package.json +9 -9
  68. package/standard-service-declarative/package.json +7 -7
  69. package/standard-service-imperative/package.json +7 -7
  70. package/basic-react-native-declarative/adapter/NativeAdapter.ts +0 -108
  71. package/basic-react-native-declarative/adapter/NativeErrorHandler.ts +0 -53
  72. package/basic-react-native-declarative/adapter/NativeEventSource.ts +0 -58
  73. package/basic-react-native-declarative/adapter/RawResponseWrapper.ts +0 -30
  74. package/basic-react-native-declarative/adapter/declarations.ts +0 -40
  75. package/basic-react-native-declarative/adapter/middleware.ts +0 -73
  76. package/basic-react-native-declarative/adapter/nativeAdapterBlueprint.ts +0 -51
  77. package/basic-react-native-declarative/adapter/renderSink.ts +0 -52
  78. 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": "basic-react-native-declarative",
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": "light",
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": "#E6F4FE",
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"
@@ -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
- * Stone.js relies on the full WHATWG URL API, so the polyfill is required.
6
- * - `TextEncoder` is installed only when the JS engine does not provide it
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
- // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
20
- // It also ensures that whether you load the app in Expo Go or in a native build,
21
- // the environment is set up appropriately
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
- * Pinning the project root keeps Metro scoped to this starter, even when the
5
- * starter lives inside a larger workspace (as it does in the Stone.js monorepo).
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
- "typecheck": "tsc --noEmit",
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.10",
24
- "@stone-js/core": "^0.8.10",
25
- "@stone-js/router": "^0.8.10",
24
+ "@stone-js/browser-core": "^0.8.12",
25
+ "@stone-js/core": "^0.8.12",
26
+ "@stone-js/react-native-adapter": "^0.8.12",
27
+ "@stone-js/router": "^0.8.12",
28
+ "@stone-js/use-react-native": "^0.8.12",
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.12",
39
+ "@stone-js/testing": "^0.8.12",
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 { OutgoingBrowserResponse } from '@stone-js/browser-core'
5
- import { nativeEventSource } from '../adapter/NativeEventSource'
6
- import { WelcomeData, WelcomeController } from '../app/WelcomeController'
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
- * Behavioral test of the full continuum chain: the exact modules the native
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
- * The adapter is pure JavaScript, so what passes here is what runs on device;
15
- * only the platform checks (URL polyfill, Hermes) need the real application.
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
- * The application boots ONCE for the whole suite, like on a device: default
18
- * blueprints are shared module-level objects, so booting the same modules
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('Stone.js native proof of concept', () => {
22
- const rendered: OutgoingBrowserResponse[] = []
23
- const errors: Error[] = []
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
- onNativeRender((response) => rendered.push(response))
27
- onNativeError((error) => errors.push(error))
28
- await stoneApp({ modules: [Application, WelcomeController] }).run()
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
- it('boots and renders the landing route on startup', () => {
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
- const payload = rendered[0].content as WelcomeData
36
- expect(payload.route).toBe('/')
37
- expect(payload.message).toContain('Welcome to Stone.js on React Native!')
38
- expect(payload.framework.name).toBe('Stone.js')
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 navigation intent with a parameter to the same domain', async () => {
42
- nativeEventSource.navigate('stone://app/hello/Noowow')
43
- await new Promise((resolve) => setImmediate(resolve))
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
- const payload = rendered.at(-1)?.content as WelcomeData
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('decodes encoded route parameters', async () => {
51
- nativeEventSource.navigate('stone://app/hello/Mr.%20Stone')
52
- await new Promise((resolve) => setImmediate(resolve))
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 routes through the error chain instead of crashing', async () => {
59
- nativeEventSource.navigate('stone://app/nowhere')
60
- await new Promise((resolve) => setImmediate(resolve))
56
+ it('surfaces an unknown route instead of crashing', async () => {
57
+ navigation.navigate('stone://app/nowhere')
58
+ await settle()
61
59
 
62
- const lastPayload = rendered.at(-1)?.content as WelcomeData | undefined
63
- expect(lastPayload?.route === '/nowhere').toBe(false)
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
+ }
@@ -7,7 +7,6 @@
7
7
  },
8
8
  "include": [
9
9
  "app",
10
- "adapter",
11
10
  "tests",
12
11
  "App.tsx",
13
12
  "index.ts",
@@ -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
- // `experimentalDecorators` is enabled in tsconfig.json for tsc TYPING only
5
- // (the framework types its decorators with the legacy signatures). At
6
- // RUNTIME Stone.js requires standard 2023-11 decorators, so esbuild must
7
- // ignore that flag and emit the standard semantics, `Symbol.metadata`
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', 'adapter/**/*.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.