@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.
Files changed (41) 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 +192 -0
  4. package/basic-react-native-declarative/LICENSE +21 -0
  5. package/basic-react-native-declarative/README.md +35 -0
  6. package/basic-react-native-declarative/adapter/NativeAdapter.ts +108 -0
  7. package/basic-react-native-declarative/adapter/NativeErrorHandler.ts +53 -0
  8. package/basic-react-native-declarative/adapter/NativeEventSource.ts +58 -0
  9. package/basic-react-native-declarative/adapter/RawResponseWrapper.ts +30 -0
  10. package/basic-react-native-declarative/adapter/declarations.ts +40 -0
  11. package/basic-react-native-declarative/adapter/middleware.ts +73 -0
  12. package/basic-react-native-declarative/adapter/nativeAdapterBlueprint.ts +51 -0
  13. package/basic-react-native-declarative/adapter/renderSink.ts +52 -0
  14. package/basic-react-native-declarative/app/Application.ts +17 -0
  15. package/basic-react-native-declarative/app/WelcomeController.ts +66 -0
  16. package/basic-react-native-declarative/app.json +25 -0
  17. package/basic-react-native-declarative/assets/android-icon-background.png +0 -0
  18. package/basic-react-native-declarative/assets/android-icon-foreground.png +0 -0
  19. package/basic-react-native-declarative/assets/android-icon-monochrome.png +0 -0
  20. package/basic-react-native-declarative/assets/favicon.png +0 -0
  21. package/basic-react-native-declarative/assets/icon.png +0 -0
  22. package/basic-react-native-declarative/assets/splash-icon.png +0 -0
  23. package/basic-react-native-declarative/babel.config.js +16 -0
  24. package/basic-react-native-declarative/index.ts +22 -0
  25. package/basic-react-native-declarative/metro.config.js +7 -0
  26. package/basic-react-native-declarative/package.json +41 -0
  27. package/basic-react-native-declarative/tests/Application.spec.ts +65 -0
  28. package/basic-react-native-declarative/tsconfig.json +16 -0
  29. package/basic-react-native-declarative/vitest.config.ts +27 -0
  30. package/basic-service-declarative/package.json +4 -4
  31. package/basic-service-imperative/package.json +4 -4
  32. package/continuum-showcase/package.json +9 -9
  33. package/full-react-declarative/package.json +10 -10
  34. package/full-react-imperative/package.json +10 -10
  35. package/full-service-declarative/package.json +10 -10
  36. package/full-service-imperative/package.json +10 -10
  37. package/package.json +15 -1
  38. package/standard-react-declarative/package.json +9 -9
  39. package/standard-react-imperative/package.json +9 -9
  40. package/standard-service-declarative/package.json +7 -7
  41. 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.9",
24
- "@stone-js/use-react": "^0.8.9",
25
- "@stone-js/browser-adapter": "^0.8.9"
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.9",
32
- "@stone-js/testing": "^0.8.9",
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.9",
24
- "@stone-js/use-react": "^0.8.9",
25
- "@stone-js/browser-adapter": "^0.8.9"
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.9",
32
- "@stone-js/testing": "^0.8.9",
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
+ }