@stone-js/starters 0.8.8 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/basic-react-declarative/app/Application.tsx +0 -2
- package/basic-react-declarative/package.json +7 -7
- package/basic-react-declarative/public/favicon.svg +16 -0
- package/basic-react-declarative/tests/Application.spec.ts +25 -100
- package/basic-react-imperative/app/Application.tsx +2 -17
- package/basic-react-imperative/package.json +7 -7
- package/basic-react-imperative/public/favicon.svg +16 -0
- package/basic-react-imperative/tests/Application.spec.ts +25 -86
- package/basic-react-native-declarative/App.tsx +192 -0
- package/basic-react-native-declarative/LICENSE +21 -0
- package/basic-react-native-declarative/README.md +35 -0
- package/basic-react-native-declarative/adapter/NativeAdapter.ts +108 -0
- package/basic-react-native-declarative/adapter/NativeErrorHandler.ts +53 -0
- package/basic-react-native-declarative/adapter/NativeEventSource.ts +58 -0
- package/basic-react-native-declarative/adapter/RawResponseWrapper.ts +30 -0
- package/basic-react-native-declarative/adapter/declarations.ts +40 -0
- package/basic-react-native-declarative/adapter/middleware.ts +73 -0
- package/basic-react-native-declarative/adapter/nativeAdapterBlueprint.ts +51 -0
- package/basic-react-native-declarative/adapter/renderSink.ts +52 -0
- package/basic-react-native-declarative/app/Application.ts +17 -0
- package/basic-react-native-declarative/app/WelcomeController.ts +66 -0
- package/basic-react-native-declarative/app.json +25 -0
- package/basic-react-native-declarative/assets/android-icon-background.png +0 -0
- package/basic-react-native-declarative/assets/android-icon-foreground.png +0 -0
- package/basic-react-native-declarative/assets/android-icon-monochrome.png +0 -0
- package/basic-react-native-declarative/assets/favicon.png +0 -0
- package/basic-react-native-declarative/assets/icon.png +0 -0
- package/basic-react-native-declarative/assets/splash-icon.png +0 -0
- package/basic-react-native-declarative/babel.config.js +16 -0
- package/basic-react-native-declarative/index.ts +22 -0
- package/basic-react-native-declarative/metro.config.js +7 -0
- package/basic-react-native-declarative/package.json +41 -0
- package/basic-react-native-declarative/tests/Application.spec.ts +65 -0
- package/basic-react-native-declarative/tsconfig.json +16 -0
- package/basic-react-native-declarative/vitest.config.ts +27 -0
- package/basic-service-declarative/app/Application.ts +0 -2
- package/basic-service-declarative/package.json +6 -6
- package/basic-service-declarative/tests/Application.spec.ts +20 -53
- package/basic-service-imperative/app/Application.ts +2 -14
- package/basic-service-imperative/package.json +6 -6
- package/basic-service-imperative/tests/Application.spec.ts +19 -44
- package/continuum-showcase/package.json +11 -10
- package/continuum-showcase/public/favicon.svg +16 -0
- package/continuum-showcase/stone.config.mjs +2 -2
- package/continuum-showcase/tests/application.spec.ts +30 -0
- package/full-react-declarative/app/Application.ts +0 -4
- package/full-react-declarative/package.json +12 -12
- package/full-react-declarative/public/favicon.svg +16 -0
- package/full-react-declarative/stone.config.mjs +2 -2
- package/full-react-declarative/tests/application.spec.ts +30 -0
- package/full-react-imperative/app/Application.ts +1 -3
- package/full-react-imperative/package.json +12 -12
- package/full-react-imperative/public/favicon.svg +16 -0
- package/full-react-imperative/stone.config.mjs +2 -2
- package/full-react-imperative/tests/application.spec.ts +30 -0
- package/full-service-declarative/app/Application.ts +5 -2
- package/full-service-declarative/app/configurations/AppConfiguration.ts +1 -3
- package/full-service-declarative/package.json +13 -12
- package/full-service-declarative/stone.config.mjs +2 -2
- package/full-service-declarative/tests/application.spec.ts +30 -0
- package/full-service-imperative/app/Application.ts +8 -2
- package/full-service-imperative/app/configurations/AppConfiguration.ts +1 -3
- package/full-service-imperative/package.json +13 -12
- package/full-service-imperative/stone.config.mjs +2 -2
- package/full-service-imperative/tests/application.spec.ts +30 -0
- package/package.json +15 -1
- package/standard-react-declarative/package.json +11 -10
- package/standard-react-declarative/public/favicon.svg +16 -0
- package/standard-react-declarative/tests/application.spec.ts +30 -0
- package/standard-react-imperative/package.json +11 -10
- package/standard-react-imperative/public/favicon.svg +16 -0
- package/standard-react-imperative/tests/application.spec.ts +30 -0
- package/standard-service-declarative/package.json +9 -8
- package/standard-service-declarative/tests/Application.spec.ts +22 -94
- package/standard-service-imperative/package.json +9 -8
- package/standard-service-imperative/tests/Application.spec.ts +22 -78
- package/basic-react-declarative/vitest.config.ts +0 -24
- package/basic-react-imperative/vitest.config.ts +0 -24
- package/basic-service-declarative/vitest.config.ts +0 -24
- package/basic-service-imperative/vitest.config.ts +0 -24
- package/continuum-showcase/tests/HomePage.spec.tsx +0 -41
- package/continuum-showcase/vitest.config.ts +0 -31
- package/full-react-declarative/tests/AppLayout.spec.ts +0 -51
- package/full-react-declarative/tests/HomePage.spec.ts +0 -38
- package/full-react-declarative/vitest.config.ts +0 -24
- package/full-react-imperative/tests/HomePage.spec.ts +0 -45
- package/full-react-imperative/tests/LayoutHeader.spec.ts +0 -39
- package/full-react-imperative/vitest.config.ts +0 -24
- package/full-service-declarative/tests/PostService.spec.ts +0 -56
- package/full-service-declarative/tests/WelcomeEventHandler.spec.ts +0 -54
- package/full-service-declarative/vitest.config.ts +0 -24
- package/full-service-imperative/tests/PostService.spec.ts +0 -47
- package/full-service-imperative/tests/WelcomeEventHandler.spec.ts +0 -31
- package/full-service-imperative/vitest.config.ts +0 -24
- package/standard-react-declarative/tests/WelcomePage.spec.ts +0 -71
- package/standard-react-declarative/vitest.config.ts +0 -24
- package/standard-react-imperative/tests/WelcomePage.spec.ts +0 -53
- package/standard-react-imperative/vitest.config.ts +0 -24
- package/standard-service-declarative/vitest.config.ts +0 -24
- package/standard-service-imperative/vitest.config.ts +0 -24
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://stonejs.dev)
|
|
5
|
-
[](https://github.com/stone-foundation/stone-js-
|
|
5
|
+
[](https://github.com/stone-foundation/stone-js-framework)
|
|
6
6
|
|
|
7
7
|
This repository contains official **starter templates** for Stone.js applications.
|
|
8
8
|
|
|
@@ -98,4 +98,4 @@ Explore the full documentation: [https://stonejs.dev](https://stonejs.dev)
|
|
|
98
98
|
|
|
99
99
|
## Contributing
|
|
100
100
|
|
|
101
|
-
See [Contributing Guide](https://github.com/stone-foundation/stone-js-
|
|
101
|
+
See [Contributing Guide](https://github.com/stone-foundation/stone-js-framework/blob/main/CONTRIBUTING.md)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { JSX } from 'react'
|
|
2
2
|
import { Browser } from '@stone-js/browser-adapter'
|
|
3
|
-
import { NodeConsole } from '@stone-js/node-cli-adapter'
|
|
4
3
|
import { ILogger, LogLevel, Promiseable, StoneApp } from '@stone-js/core'
|
|
5
4
|
import { ReactIncomingEvent, UseReact, HeadContext, IPage, PageHeadContext, PageRenderContext } from '@stone-js/use-react'
|
|
6
5
|
|
|
@@ -15,7 +14,6 @@ import { ReactIncomingEvent, UseReact, HeadContext, IPage, PageHeadContext, Page
|
|
|
15
14
|
*/
|
|
16
15
|
@Browser()
|
|
17
16
|
@UseReact()
|
|
18
|
-
@NodeConsole()
|
|
19
17
|
@StoneApp({ logger: { level: LogLevel.INFO } })
|
|
20
18
|
export class Application implements IPage<ReactIncomingEvent> {
|
|
21
19
|
/**
|
|
@@ -14,22 +14,22 @@
|
|
|
14
14
|
"build": "stone build",
|
|
15
15
|
"preview": "stone preview",
|
|
16
16
|
"build:ssr": "stone build --ssr",
|
|
17
|
-
"test": "
|
|
18
|
-
"test:cvg": "
|
|
17
|
+
"test": "stone test",
|
|
18
|
+
"test:cvg": "stone test --coverage"
|
|
19
19
|
},
|
|
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.
|
|
26
|
-
"@stone-js/node-cli-adapter": "^0.8.8"
|
|
23
|
+
"@stone-js/core": "^0.8.10",
|
|
24
|
+
"@stone-js/use-react": "^0.8.10",
|
|
25
|
+
"@stone-js/browser-adapter": "^0.8.10"
|
|
27
26
|
},
|
|
28
27
|
"devDependencies": {
|
|
29
28
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
30
29
|
"@babel/preset-env": "^7.26.9",
|
|
31
30
|
"@babel/preset-typescript": "^7.27.0",
|
|
32
|
-
"@stone-js/cli": "^0.8.
|
|
31
|
+
"@stone-js/cli": "^0.8.10",
|
|
32
|
+
"@stone-js/testing": "^0.8.10",
|
|
33
33
|
"@types/node": "^24.0.7",
|
|
34
34
|
"@types/react": "^19.0.7",
|
|
35
35
|
"@types/react-dom": "^19.0.3",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Stone.js">
|
|
2
|
+
<title>Stone.js</title>
|
|
3
|
+
<style>
|
|
4
|
+
.ink { stroke: #1B1613; }
|
|
5
|
+
@media (prefers-color-scheme: dark) { .ink { stroke: #EFE8DC; } }
|
|
6
|
+
</style>
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="braise" gradientUnits="userSpaceOnUse" x1="22" y1="14" x2="74" y2="26.1">
|
|
9
|
+
<stop offset="0" stop-color="#FFC46B" />
|
|
10
|
+
<stop offset="1" stop-color="#FF5A1F" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<path class="ink" d="M 79.96 36.4 A 34 34 0 0 1 53.9 81.5" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
14
|
+
<path class="ink" d="M 42.1 81.5 A 34 34 0 0 1 16.05 36.4" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
15
|
+
<path d="M 22 26.1 A 34 34 0 0 1 74 26.1" fill="none" stroke="url(#braise)" stroke-width="11" stroke-linecap="round" />
|
|
16
|
+
</svg>
|
|
@@ -1,105 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// We must mock decorators to lighten the test environment
|
|
25
|
-
vi.mock('@stone-js/browser-adapter', async (importOriginal) => {
|
|
26
|
-
const actual: any = await importOriginal()
|
|
27
|
-
return {
|
|
28
|
-
...actual,
|
|
29
|
-
Browser: vi.fn(() => vi.fn()),
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// We must mock decorators to lighten the test environment
|
|
34
|
-
vi.mock('@stone-js/use-react', async (importOriginal) => {
|
|
35
|
-
const actual: any = await importOriginal()
|
|
36
|
-
return {
|
|
37
|
-
...actual,
|
|
38
|
-
UseReact: vi.fn(() => vi.fn()),
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('Application', () => {
|
|
43
|
-
let app: Application
|
|
44
|
-
let mockedLogger: ILogger
|
|
45
|
-
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
mockedLogger = {
|
|
48
|
-
info: vi.fn(),
|
|
49
|
-
} as unknown as ILogger
|
|
50
|
-
|
|
51
|
-
app = new Application({ logger: mockedLogger })
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('should create an application instance', () => {
|
|
55
|
-
// Assert
|
|
56
|
-
expect(app).toBeInstanceOf(Application)
|
|
1
|
+
import { createTestApp, makeIncomingHttpEvent } from '@stone-js/testing'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The whole application, booted in memory and asked a real question.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is mocked and nothing is listed: `createTestApp()` discovers `app/**`, and the event goes
|
|
7
|
+
* through the same kernel production runs. If this passes the application works; if it fails the
|
|
8
|
+
* application is broken. That is the only kind of test worth shipping in a starter — the previous one
|
|
9
|
+
* stubbed out the framework's own decorators, which meant it could pass while nothing worked.
|
|
10
|
+
*/
|
|
11
|
+
describe('Pages', () => {
|
|
12
|
+
it('renders the home page, data included', async () => {
|
|
13
|
+
const app = await createTestApp()
|
|
14
|
+
|
|
15
|
+
const response = await app.send(makeIncomingHttpEvent({ method: 'GET', url: '/?name=Ada' }))
|
|
16
|
+
|
|
17
|
+
expect(response.statusCode).toBe(200)
|
|
18
|
+
// A page is a handler, so a rendered page is a response whose body is HTML. Query that HTML with
|
|
19
|
+
// whatever you already use (happy-dom, jsdom, Testing Library): none is bundled here.
|
|
20
|
+
expect(response.html()).toContain('Ada')
|
|
57
21
|
})
|
|
58
22
|
|
|
59
|
-
it('
|
|
60
|
-
|
|
61
|
-
const event = { get: () => 'World' } as unknown as ReactIncomingEvent
|
|
23
|
+
it('renders without a name', async () => {
|
|
24
|
+
const app = await createTestApp()
|
|
62
25
|
|
|
63
|
-
|
|
64
|
-
const head = await app.head({ event })
|
|
26
|
+
const response = await app.send(makeIncomingHttpEvent({ method: 'GET', url: '/' }))
|
|
65
27
|
|
|
66
|
-
|
|
67
|
-
expect(head).toHaveProperty('metas')
|
|
68
|
-
expect(head.title).toContain('Welcome to Stone.js')
|
|
69
|
-
expect(head.title).toContain('World')
|
|
70
|
-
expect(head).toHaveProperty('description')
|
|
28
|
+
expect(response.statusCode).toBe(200)
|
|
71
29
|
})
|
|
72
|
-
|
|
73
|
-
it('should handle incoming events', () => {
|
|
74
|
-
// Arrange
|
|
75
|
-
const expectedMessage = 'Hello World!'
|
|
76
|
-
const event = { get: () => 'World' } as unknown as ReactIncomingEvent
|
|
77
|
-
|
|
78
|
-
// Act
|
|
79
|
-
const response = app.handle(event) as { message: string }
|
|
80
|
-
|
|
81
|
-
// Assert
|
|
82
|
-
expect(response.message).toBe(expectedMessage)
|
|
83
|
-
expect(mockedLogger.info).toHaveBeenCalledWith(expectedMessage)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should render the message', () => {
|
|
87
|
-
// Arrange
|
|
88
|
-
const message = 'Hello World!'
|
|
89
|
-
|
|
90
|
-
// Act
|
|
91
|
-
const response = renderToString(app.render({ data: { message } } as any))
|
|
92
|
-
|
|
93
|
-
// Assert
|
|
94
|
-
expect(response).toContain(message)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('should render the Stone.js logo', () => {
|
|
98
|
-
// Act
|
|
99
|
-
const response = renderToString(app.render({ data: { message: 'Hello World!' } } as any))
|
|
100
|
-
|
|
101
|
-
// Assert
|
|
102
|
-
expect(response).toContain('/logo.svg')
|
|
103
|
-
expect(response).toContain('alt="Stone.js"')
|
|
104
|
-
})
|
|
105
|
-
})
|
|
30
|
+
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { JSX } from 'react'
|
|
2
2
|
import { browserAdapterBlueprint } from '@stone-js/browser-adapter'
|
|
3
|
-
import {
|
|
4
|
-
import { defineCommand, FactoryCommandHandler, NODE_CONSOLE_PLATFORM, nodeConsoleAdapterBlueprint } from '@stone-js/node-cli-adapter'
|
|
3
|
+
import { ILogger, Promiseable } from '@stone-js/core'
|
|
5
4
|
import { defineStoneReactApp, HeadContext, IPage, PageHeadContext, PageRenderContext, ReactIncomingEvent } from '@stone-js/use-react'
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -73,23 +72,9 @@ export const FactoryHandler = ({ logger }: AppOptions): IPage<ReactIncomingEvent
|
|
|
73
72
|
export const Application = defineStoneReactApp(
|
|
74
73
|
FactoryHandler,
|
|
75
74
|
{ debug: true, isFactory: true },
|
|
76
|
-
[browserAdapterBlueprint
|
|
75
|
+
[browserAdapterBlueprint]
|
|
77
76
|
)
|
|
78
77
|
|
|
79
|
-
/**
|
|
80
|
-
* Application configuration.
|
|
81
|
-
*/
|
|
82
|
-
export const AppConfig = defineConfig({
|
|
83
|
-
afterConfigure (blueprint: IBlueprint) {
|
|
84
|
-
if (
|
|
85
|
-
isNotEmpty<FactoryCommandHandler>(FactoryHandler) &&
|
|
86
|
-
blueprint.is('stone.adapter.platform', NODE_CONSOLE_PLATFORM)
|
|
87
|
-
) {
|
|
88
|
-
blueprint.set(defineCommand(FactoryHandler, { name: '*', isFactory: true }))
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
|
|
93
78
|
/**
|
|
94
79
|
* Application options.
|
|
95
80
|
*/
|
|
@@ -14,22 +14,22 @@
|
|
|
14
14
|
"build": "stone build",
|
|
15
15
|
"preview": "stone preview",
|
|
16
16
|
"typings": "stone typings",
|
|
17
|
-
"test": "
|
|
18
|
-
"test:cvg": "
|
|
17
|
+
"test": "stone test",
|
|
18
|
+
"test:cvg": "stone test --coverage"
|
|
19
19
|
},
|
|
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.
|
|
26
|
-
"@stone-js/node-cli-adapter": "^0.8.8"
|
|
23
|
+
"@stone-js/core": "^0.8.10",
|
|
24
|
+
"@stone-js/use-react": "^0.8.10",
|
|
25
|
+
"@stone-js/browser-adapter": "^0.8.10"
|
|
27
26
|
},
|
|
28
27
|
"devDependencies": {
|
|
29
28
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
30
29
|
"@babel/preset-env": "^7.26.9",
|
|
31
30
|
"@babel/preset-typescript": "^7.27.0",
|
|
32
|
-
"@stone-js/cli": "^0.8.
|
|
31
|
+
"@stone-js/cli": "^0.8.10",
|
|
32
|
+
"@stone-js/testing": "^0.8.10",
|
|
33
33
|
"@types/node": "^24.0.7",
|
|
34
34
|
"@types/react": "^19.0.7",
|
|
35
35
|
"@types/react-dom": "^19.0.3",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Stone.js">
|
|
2
|
+
<title>Stone.js</title>
|
|
3
|
+
<style>
|
|
4
|
+
.ink { stroke: #1B1613; }
|
|
5
|
+
@media (prefers-color-scheme: dark) { .ink { stroke: #EFE8DC; } }
|
|
6
|
+
</style>
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="braise" gradientUnits="userSpaceOnUse" x1="22" y1="14" x2="74" y2="26.1">
|
|
9
|
+
<stop offset="0" stop-color="#FFC46B" />
|
|
10
|
+
<stop offset="1" stop-color="#FF5A1F" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<path class="ink" d="M 79.96 36.4 A 34 34 0 0 1 53.9 81.5" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
14
|
+
<path class="ink" d="M 42.1 81.5 A 34 34 0 0 1 16.05 36.4" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
15
|
+
<path d="M 22 26.1 A 34 34 0 0 1 74 26.1" fill="none" stroke="url(#braise)" stroke-width="11" stroke-linecap="round" />
|
|
16
|
+
</svg>
|
|
@@ -1,91 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} as unknown as ILogger
|
|
22
|
-
|
|
23
|
-
app = FactoryHandler({ logger: mockedLogger })
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('should create an application instance', () => {
|
|
27
|
-
// Assert
|
|
28
|
-
expect(app).toBeInstanceOf(Object)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('get head values', async () => {
|
|
32
|
-
// Arrange
|
|
33
|
-
const event = { get: () => 'World' } as unknown as IncomingEvent
|
|
34
|
-
|
|
35
|
-
// Act
|
|
36
|
-
const head = await app.head({ event })
|
|
37
|
-
|
|
38
|
-
// Assert
|
|
39
|
-
expect(head).toHaveProperty('metas')
|
|
40
|
-
expect(head.title).toContain('Welcome to Stone.js')
|
|
41
|
-
expect(head.title).toContain('World')
|
|
42
|
-
expect(head).toHaveProperty('description')
|
|
1
|
+
import { createTestApp, makeIncomingHttpEvent } from '@stone-js/testing'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The whole application, booted in memory and asked a real question.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is mocked and nothing is listed: `createTestApp()` discovers `app/**`, and the event goes
|
|
7
|
+
* through the same kernel production runs. If this passes the application works; if it fails the
|
|
8
|
+
* application is broken. That is the only kind of test worth shipping in a starter — the previous one
|
|
9
|
+
* stubbed out the framework's own decorators, which meant it could pass while nothing worked.
|
|
10
|
+
*/
|
|
11
|
+
describe('Pages', () => {
|
|
12
|
+
it('renders the home page, data included', async () => {
|
|
13
|
+
const app = await createTestApp()
|
|
14
|
+
|
|
15
|
+
const response = await app.send(makeIncomingHttpEvent({ method: 'GET', url: '/?name=Ada' }))
|
|
16
|
+
|
|
17
|
+
expect(response.statusCode).toBe(200)
|
|
18
|
+
// A page is a handler, so a rendered page is a response whose body is HTML. Query that HTML with
|
|
19
|
+
// whatever you already use (happy-dom, jsdom, Testing Library): none is bundled here.
|
|
20
|
+
expect(response.html()).toContain('Ada')
|
|
43
21
|
})
|
|
44
22
|
|
|
45
|
-
it('
|
|
46
|
-
|
|
47
|
-
const expectedMessage = 'Hello World!'
|
|
48
|
-
const event = { get: () => 'World' } as unknown as IncomingEvent
|
|
23
|
+
it('renders without a name', async () => {
|
|
24
|
+
const app = await createTestApp()
|
|
49
25
|
|
|
50
|
-
|
|
51
|
-
const response = app.handle(event) as { message: string }
|
|
26
|
+
const response = await app.send(makeIncomingHttpEvent({ method: 'GET', url: '/' }))
|
|
52
27
|
|
|
53
|
-
|
|
54
|
-
expect(response.message).toBe(expectedMessage)
|
|
55
|
-
expect(mockedLogger.info).toHaveBeenCalledWith(expectedMessage)
|
|
28
|
+
expect(response.statusCode).toBe(200)
|
|
56
29
|
})
|
|
57
|
-
|
|
58
|
-
it('should render the message', () => {
|
|
59
|
-
// Arrange
|
|
60
|
-
const message = 'Hello World!'
|
|
61
|
-
|
|
62
|
-
// Act
|
|
63
|
-
const response = renderToString(app.render({ data: { message } } as any))
|
|
64
|
-
|
|
65
|
-
// Assert
|
|
66
|
-
expect(response).toContain(message)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('should render the Stone.js logo', () => {
|
|
70
|
-
// Act
|
|
71
|
-
const response = renderToString(app.render({ data: { message: 'Hello World!' } } as any))
|
|
72
|
-
|
|
73
|
-
// Assert
|
|
74
|
-
expect(response).toContain('/logo.svg')
|
|
75
|
-
expect(response).toContain('alt="Stone.js"')
|
|
76
|
-
expect(response).toContain('Welcome to')
|
|
77
|
-
expect(response).toContain('Stone.js')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('should get config', () => {
|
|
81
|
-
// Arrange
|
|
82
|
-
const config: any = AppConfig
|
|
83
|
-
const blueprint: any = { is: () => true, set: vi.fn() }
|
|
84
|
-
|
|
85
|
-
// Act
|
|
86
|
-
config.afterConfigure(blueprint)
|
|
87
|
-
|
|
88
|
-
// Assert
|
|
89
|
-
expect(blueprint.set).toHaveBeenCalled()
|
|
90
|
-
})
|
|
91
|
-
})
|
|
30
|
+
})
|
|
@@ -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.
|