@stone-js/starters 0.8.3 → 0.8.4

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 (65) hide show
  1. package/basic-react-declarative/app/Application.tsx +1 -0
  2. package/basic-react-declarative/package.json +5 -5
  3. package/basic-react-declarative/public/logo.svg +16 -0
  4. package/basic-react-declarative/tests/Application.spec.ts +11 -2
  5. package/basic-react-imperative/app/Application.tsx +1 -0
  6. package/basic-react-imperative/package.json +5 -5
  7. package/basic-react-imperative/public/logo.svg +16 -0
  8. package/basic-react-imperative/tests/Application.spec.ts +11 -2
  9. package/basic-service-declarative/package.json +4 -4
  10. package/basic-service-imperative/package.json +4 -4
  11. package/continuum-showcase/LICENSE +21 -0
  12. package/continuum-showcase/app/Application.ts +45 -0
  13. package/continuum-showcase/app/layouts/MainLayout.tsx +1 -2
  14. package/continuum-showcase/app/pages/BlogPostPage.tsx +1 -1
  15. package/continuum-showcase/app/pages/HomePage.tsx +2 -2
  16. package/continuum-showcase/assets/css/index.css +26 -0
  17. package/continuum-showcase/assets/images/logo.svg +16 -0
  18. package/continuum-showcase/package.json +22 -10
  19. package/continuum-showcase/tests/HomePage.spec.tsx +40 -0
  20. package/continuum-showcase/tsconfig.json +33 -0
  21. package/continuum-showcase/vitest.config.ts +31 -0
  22. package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
  23. package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  24. package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  25. package/full-react-declarative/package.json +10 -10
  26. package/full-react-declarative/public/logo.svg +16 -0
  27. package/full-react-declarative/tests/AppLayout.spec.ts +51 -0
  28. package/full-react-imperative/app/components/LayoutHeader/LayoutHeader.tsx +1 -1
  29. package/full-react-imperative/app/components/PostWidget/PostWidget.tsx +1 -1
  30. package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.css +10 -0
  31. package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.tsx +66 -0
  32. package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  33. package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  34. package/full-react-imperative/package.json +11 -12
  35. package/full-react-imperative/public/logo.svg +16 -0
  36. package/full-react-imperative/tests/LayoutHeader.spec.ts +39 -0
  37. package/full-service-declarative/package.json +9 -9
  38. package/full-service-declarative/tests/PostService.spec.ts +56 -0
  39. package/full-service-declarative/vitest.config.ts +1 -1
  40. package/full-service-imperative/package.json +9 -9
  41. package/full-service-imperative/tests/PostService.spec.ts +47 -0
  42. package/full-service-imperative/vitest.config.ts +1 -1
  43. package/package.json +1 -1
  44. package/standard-react-declarative/app/pages/welcome/WelcomePage.tsx +1 -0
  45. package/standard-react-declarative/package.json +8 -8
  46. package/standard-react-declarative/public/logo.svg +16 -0
  47. package/standard-react-declarative/tests/WelcomePage.spec.ts +69 -0
  48. package/standard-react-imperative/app/pages/welcome/WelcomePage.tsx +1 -0
  49. package/standard-react-imperative/package.json +8 -8
  50. package/standard-react-imperative/public/logo.svg +16 -0
  51. package/standard-react-imperative/tests/WelcomePage.spec.ts +51 -0
  52. package/standard-service-declarative/package.json +6 -6
  53. package/standard-service-declarative/tests/Application.spec.ts +91 -0
  54. package/standard-service-declarative/vitest.config.ts +1 -1
  55. package/standard-service-imperative/package.json +6 -6
  56. package/standard-service-imperative/tests/Application.spec.ts +75 -0
  57. package/standard-service-imperative/vitest.config.ts +1 -1
  58. package/basic-react-declarative/public/logo.png +0 -0
  59. package/basic-react-declarative/tests/__snapshots__/Application.spec.ts.snap +0 -3
  60. package/basic-react-imperative/public/logo.png +0 -0
  61. package/basic-react-imperative/tests/__snapshots__/Application.spec.ts.snap +0 -3
  62. package/full-react-declarative/assets/img/logo.png +0 -0
  63. package/full-react-declarative/public/logo.png +0 -0
  64. package/full-react-imperative/assets/img/logo.png +0 -0
  65. package/full-react-imperative/public/logo.png +0 -0
@@ -2,9 +2,9 @@ import { User } from '../../models/User'
2
2
  import { Post } from '../../models/Post'
3
3
  import { deletePost } from '../../pages/utils'
4
4
  import { PostCard } from '../PostCard/PostCard'
5
- import PullToRefresh from 'react-pull-to-refresh'
6
5
  import { IContainer, ILogger } from '@stone-js/core'
7
6
  import { FC, useEffect, useRef, useState } from 'react'
7
+ import { PullToRefresh } from '../PullToRefresh/PullToRefresh'
8
8
  import { PostSkeleton } from '../PostSkeleton/PostSkeleton'
9
9
  import { CommentWidget } from '../CommentWidget/CommentWidget'
10
10
  import { IRouter, ReactIncomingEvent } from '@stone-js/use-react'
@@ -0,0 +1,10 @@
1
+ .pull-to-refresh {
2
+ will-change: transform;
3
+ }
4
+
5
+ .pull-to-refresh__indicator {
6
+ text-align: center;
7
+ padding: 8px 0;
8
+ font-size: 0.85rem;
9
+ opacity: 0.7;
10
+ }
@@ -0,0 +1,66 @@
1
+ import './PullToRefresh.css'
2
+ import { FC, ReactNode, TouchEvent, useRef, useState } from 'react'
3
+
4
+ /**
5
+ * PullToRefresh options.
6
+ */
7
+ export interface PullToRefreshOptions {
8
+ children: ReactNode
9
+ threshold?: number
10
+ onRefresh: () => Promise<void> | void
11
+ }
12
+
13
+ /**
14
+ * Lightweight, SSR-safe pull-to-refresh wrapper.
15
+ *
16
+ * Implemented in-app (with no dependency that touches `window` at import time)
17
+ * so it renders safely during server-side rendering and static route
18
+ * generation. The pull gesture is a progressive enhancement that only activates
19
+ * from touch interactions on the client.
20
+ */
21
+ export const PullToRefresh: FC<PullToRefreshOptions> = ({ children, onRefresh, threshold = 64 }) => {
22
+ const startY = useRef<number | null>(null)
23
+ const [pullY, setPullY] = useState(0)
24
+ const [refreshing, setRefreshing] = useState(false)
25
+
26
+ const onTouchStart = (event: TouchEvent): void => {
27
+ if (refreshing) { return }
28
+ // Only start a pull when the page is scrolled to the very top.
29
+ if (document.documentElement.scrollTop <= 0) {
30
+ startY.current = event.touches[0].clientY
31
+ }
32
+ }
33
+
34
+ const onTouchMove = (event: TouchEvent): void => {
35
+ if (startY.current === null || refreshing) { return }
36
+ const delta = event.touches[0].clientY - startY.current
37
+ if (delta > 0) { setPullY(Math.min(delta, threshold * 1.5)) }
38
+ }
39
+
40
+ const onTouchEnd = (): void => {
41
+ if (startY.current === null) { return }
42
+ const shouldRefresh = pullY >= threshold
43
+ startY.current = null
44
+ setPullY(0)
45
+ if (shouldRefresh) {
46
+ setRefreshing(true)
47
+ void Promise.resolve(onRefresh()).finally(() => setRefreshing(false))
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div
53
+ className='pull-to-refresh'
54
+ onTouchStart={onTouchStart}
55
+ onTouchMove={onTouchMove}
56
+ onTouchEnd={onTouchEnd}
57
+ style={{
58
+ transform: pullY > 0 ? `translateY(${pullY}px)` : undefined,
59
+ transition: pullY > 0 ? 'none' : 'transform .2s ease'
60
+ }}
61
+ >
62
+ {refreshing && <div className='pull-to-refresh__indicator'>Refreshing…</div>}
63
+ {children}
64
+ </div>
65
+ )
66
+ }
@@ -18,7 +18,7 @@ export const ErrorLayout = (): IPageLayout => ({
18
18
  <header className='mt-64'>
19
19
  <div className='container'>
20
20
  <p className='pa-24'>
21
- <img src='/logo.png' className='logo' alt='Stone.js Logo' />
21
+ <img src='/logo.svg' className='logo' alt='Stone.js' />
22
22
  </p>
23
23
  </div>
24
24
  </header>
@@ -18,7 +18,7 @@ export const SecurityLayout = (): IPageLayout => ({
18
18
  <header className='mt-64'>
19
19
  <div className='container'>
20
20
  <p className='pa-24'>
21
- <img src='/logo.png' className='logo' alt='Stone.js Logo' />
21
+ <img src='/logo.svg' className='logo' alt='Stone.js' />
22
22
  </p>
23
23
  </div>
24
24
  </header>
@@ -18,26 +18,25 @@
18
18
  "test:cvg": "npm run test -- --coverage"
19
19
  },
20
20
  "dependencies": {
21
- "@stone-js/env": "^0.8.3",
22
- "@stone-js/http-core": "^0.8.3",
23
- "@stone-js/node-http-adapter": "^0.8.3",
24
- "@stone-js/router": "^0.8.3",
25
- "@stone-js/filesystem": "^0.8.3",
26
- "@stone-js/core": "^0.8.3",
27
- "@stone-js/use-react": "^0.8.3",
28
- "@stone-js/browser-adapter": "^0.8.3",
29
- "@stone-js/node-cli-adapter": "^0.8.3",
21
+ "@stone-js/env": "^0.8.4",
22
+ "@stone-js/http-core": "^0.8.4",
23
+ "@stone-js/node-http-adapter": "^0.8.4",
24
+ "@stone-js/router": "^0.8.4",
25
+ "@stone-js/filesystem": "^0.8.4",
26
+ "@stone-js/core": "^0.8.4",
27
+ "@stone-js/use-react": "^0.8.4",
28
+ "@stone-js/browser-adapter": "^0.8.4",
29
+ "@stone-js/node-cli-adapter": "^0.8.4",
30
30
  "axios": "^1.7.9",
31
31
  "dayjs": "^1.11.13",
32
32
  "react": "^19.0.0",
33
- "react-dom": "^19.0.0",
34
- "react-pull-to-refresh": "^2.0.1"
33
+ "react-dom": "^19.0.0"
35
34
  },
36
35
  "devDependencies": {
37
36
  "@babel/plugin-proposal-decorators": "^7.25.9",
38
37
  "@babel/preset-env": "^7.26.9",
39
38
  "@babel/preset-typescript": "^7.27.0",
40
- "@stone-js/cli": "^0.8.3",
39
+ "@stone-js/cli": "^0.8.4",
41
40
  "@types/axios": "^0.14.4",
42
41
  "@types/node": "^24.0.7",
43
42
  "@types/react": "^19.0.7",
@@ -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>
@@ -0,0 +1,39 @@
1
+ import { createElement } from 'react'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { LayoutHeader } from '../app/components/LayoutHeader/LayoutHeader'
4
+
5
+ // We must mock router-bound components to lighten the test environment
6
+ vi.mock('@stone-js/use-react', async (importOriginal) => {
7
+ const actual: any = await importOriginal()
8
+ return {
9
+ ...actual,
10
+ StoneLink: ({ children }: any) => children
11
+ }
12
+ })
13
+
14
+ describe('LayoutHeader', () => {
15
+ const render = (): string => {
16
+ const container = {
17
+ make: (key: string) => key === 'event' ? { getUser: () => undefined } : {}
18
+ } as any
19
+ return renderToString(createElement(LayoutHeader, { container }))
20
+ }
21
+
22
+ it('should render the real Stone.js logo', () => {
23
+ // Act
24
+ const html = render()
25
+
26
+ // Assert
27
+ expect(html).toContain('/logo.svg')
28
+ expect(html).toContain('alt="Stone.js"')
29
+ })
30
+
31
+ it('should render the Stone.js brand for an unauthenticated visitor', () => {
32
+ // Act
33
+ const html = render()
34
+
35
+ // Assert
36
+ expect(html).toContain('Stone.js')
37
+ expect(html).toContain('app-navbar')
38
+ })
39
+ })
@@ -22,14 +22,14 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@libsql/client": "^0.14.0",
25
- "@stone-js/core": "^0.8.3",
26
- "@stone-js/env": "^0.8.3",
27
- "@stone-js/filesystem": "^0.8.3",
28
- "@stone-js/http-core": "^0.8.3",
29
- "@stone-js/node-cli-adapter": "^0.8.3",
30
- "@stone-js/node-http-adapter": "^0.8.3",
31
- "@stone-js/pipeline": "^0.8.3",
32
- "@stone-js/router": "^0.8.3",
25
+ "@stone-js/core": "^0.8.4",
26
+ "@stone-js/env": "^0.8.4",
27
+ "@stone-js/filesystem": "^0.8.4",
28
+ "@stone-js/http-core": "^0.8.4",
29
+ "@stone-js/node-cli-adapter": "^0.8.4",
30
+ "@stone-js/node-http-adapter": "^0.8.4",
31
+ "@stone-js/pipeline": "^0.8.4",
32
+ "@stone-js/router": "^0.8.4",
33
33
  "bcryptjs": "^3.0.2",
34
34
  "drizzle-orm": "^0.44.2",
35
35
  "jsonwebtoken": "^9.0.2"
@@ -38,7 +38,7 @@
38
38
  "@babel/plugin-proposal-decorators": "^7.25.9",
39
39
  "@babel/preset-env": "^7.26.9",
40
40
  "@babel/preset-typescript": "^7.27.0",
41
- "@stone-js/cli": "^0.8.3",
41
+ "@stone-js/cli": "^0.8.4",
42
42
  "@types/jsonwebtoken": "^9.0.8",
43
43
  "@types/node": "^24.0.3",
44
44
  "@vitest/coverage-v8": "^3.0.9",
@@ -0,0 +1,56 @@
1
+ import { NotFoundError } from '@stone-js/http-core'
2
+ import { PostService } from '../app/services/PostService'
3
+ import { PostRepository } from '../app/repositories/PostRepository'
4
+ import { Post } from '../app/models/Post'
5
+
6
+ // Neutralize the @Service decorator to lighten the test environment,
7
+ // while keeping the real `isNotEmpty` helper so we assert real behavior.
8
+ vi.mock(import('@stone-js/core'), async (importOriginal) => {
9
+ const actual = await importOriginal()
10
+ return {
11
+ ...actual,
12
+ Service: vi.fn(() => vi.fn()),
13
+ }
14
+ })
15
+
16
+ describe('PostService', () => {
17
+ let service: PostService
18
+ let postRepository: PostRepository
19
+
20
+ beforeEach(() => {
21
+ postRepository = {
22
+ list: vi.fn(),
23
+ findById: vi.fn(),
24
+ } as unknown as PostRepository
25
+
26
+ service = new PostService({ postRepository })
27
+ })
28
+
29
+ it('should delegate list() to the repository with the given limit', async () => {
30
+ // Arrange
31
+ const posts = [{ id: 1, title: 'Hello' }] as unknown as Post[]
32
+ ;(postRepository.list as any).mockResolvedValue(posts)
33
+
34
+ // Act
35
+ const result = await service.list(5)
36
+
37
+ // Assert
38
+ expect(result).toBe(posts)
39
+ expect(postRepository.list).toHaveBeenCalledWith({ limit: 5 })
40
+ })
41
+
42
+ it('should return the post when found and throw NotFoundError otherwise', async () => {
43
+ // Arrange
44
+ const post = { id: 42, title: 'Answer' } as unknown as Post
45
+ ;(postRepository.findById as any).mockResolvedValueOnce(post)
46
+
47
+ // Act & Assert (found)
48
+ await expect(service.findById(42)).resolves.toBe(post)
49
+
50
+ // Arrange (not found)
51
+ ;(postRepository.findById as any).mockResolvedValueOnce(undefined)
52
+
53
+ // Act & Assert (not found)
54
+ await expect(service.findById(99)).rejects.toBeInstanceOf(NotFoundError)
55
+ })
56
+ })
@@ -1,4 +1,4 @@
1
- import { defineConfig } from 'vitest/config.js'
1
+ import { defineConfig } from 'vitest/config'
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
@@ -22,14 +22,14 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@libsql/client": "^0.14.0",
25
- "@stone-js/core": "^0.8.3",
26
- "@stone-js/env": "^0.8.3",
27
- "@stone-js/filesystem": "^0.8.3",
28
- "@stone-js/http-core": "^0.8.3",
29
- "@stone-js/node-cli-adapter": "^0.8.3",
30
- "@stone-js/node-http-adapter": "^0.8.3",
31
- "@stone-js/pipeline": "^0.8.3",
32
- "@stone-js/router": "^0.8.3",
25
+ "@stone-js/core": "^0.8.4",
26
+ "@stone-js/env": "^0.8.4",
27
+ "@stone-js/filesystem": "^0.8.4",
28
+ "@stone-js/http-core": "^0.8.4",
29
+ "@stone-js/node-cli-adapter": "^0.8.4",
30
+ "@stone-js/node-http-adapter": "^0.8.4",
31
+ "@stone-js/pipeline": "^0.8.4",
32
+ "@stone-js/router": "^0.8.4",
33
33
  "bcryptjs": "^3.0.2",
34
34
  "drizzle-orm": "^0.44.2",
35
35
  "jsonwebtoken": "^9.0.2"
@@ -38,7 +38,7 @@
38
38
  "@babel/plugin-proposal-decorators": "^7.25.9",
39
39
  "@babel/preset-env": "^7.26.9",
40
40
  "@babel/preset-typescript": "^7.27.0",
41
- "@stone-js/cli": "^0.8.3",
41
+ "@stone-js/cli": "^0.8.4",
42
42
  "@types/jsonwebtoken": "^9.0.8",
43
43
  "@types/node": "^24.0.3",
44
44
  "@vitest/coverage-v8": "^3.0.9",
@@ -0,0 +1,47 @@
1
+ import { NotFoundError } from '@stone-js/http-core'
2
+ import { factoryPostService } from '../app/services/PostService'
3
+ import { IPostService } from '../app/services/contracts/IPostService'
4
+ import { IPostRepository } from '../app/repositories/contracts/IPostRepository'
5
+ import { Post } from '../app/models/Post'
6
+
7
+ describe('factoryPostService', () => {
8
+ let service: IPostService
9
+ let postRepository: IPostRepository
10
+
11
+ beforeEach(() => {
12
+ postRepository = {
13
+ list: vi.fn(),
14
+ findById: vi.fn(),
15
+ } as unknown as IPostRepository
16
+
17
+ service = factoryPostService({ postRepository })
18
+ })
19
+
20
+ it('should delegate list() to the repository with the given limit', async () => {
21
+ // Arrange
22
+ const posts = [{ id: 1, title: 'Hello' }] as unknown as Post[]
23
+ ;(postRepository.list as any).mockResolvedValue(posts)
24
+
25
+ // Act
26
+ const result = await service.list(5)
27
+
28
+ // Assert
29
+ expect(result).toBe(posts)
30
+ expect(postRepository.list).toHaveBeenCalledWith({ limit: 5 })
31
+ })
32
+
33
+ it('should return the post when found and throw NotFoundError otherwise', async () => {
34
+ // Arrange
35
+ const post = { id: 42, title: 'Answer' } as unknown as Post
36
+ ;(postRepository.findById as any).mockResolvedValueOnce(post)
37
+
38
+ // Act & Assert (found)
39
+ await expect(service.findById(42)).resolves.toBe(post)
40
+
41
+ // Arrange (not found)
42
+ ;(postRepository.findById as any).mockResolvedValueOnce(undefined)
43
+
44
+ // Act & Assert (not found)
45
+ await expect(service.findById(99)).rejects.toBeInstanceOf(NotFoundError)
46
+ })
47
+ })
@@ -1,4 +1,4 @@
1
- import { defineConfig } from 'vitest/config.js'
1
+ import { defineConfig } from 'vitest/config'
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stone-js/starters",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "This repository contains official **starter templates** for Stone.js applications.",
5
5
  "author": "Mr. Stone <evensstone@gmail.com>",
6
6
  "license": "MIT",
@@ -50,6 +50,7 @@ export class WelcomePage implements IPage<ReactIncomingEvent> {
50
50
  render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
51
51
  return (
52
52
  <section className='container'>
53
+ <img src='/logo.svg' alt='Stone.js' width={96} height={96} />
53
54
  <h1 className='h1 text-center mt-64'>{data?.message}</h1>
54
55
  </section>
55
56
  )
@@ -20,19 +20,19 @@
20
20
  "dependencies": {
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0",
23
- "@stone-js/use-react": "^0.8.3",
24
- "@stone-js/browser-adapter": "^0.8.3",
25
- "@stone-js/pipeline": "^0.8.3",
26
- "@stone-js/http-core": "^0.8.3",
27
- "@stone-js/node-http-adapter": "^0.8.3",
28
- "@stone-js/router": "^0.8.3",
29
- "@stone-js/core": "^0.8.3"
23
+ "@stone-js/use-react": "^0.8.4",
24
+ "@stone-js/browser-adapter": "^0.8.4",
25
+ "@stone-js/pipeline": "^0.8.4",
26
+ "@stone-js/http-core": "^0.8.4",
27
+ "@stone-js/node-http-adapter": "^0.8.4",
28
+ "@stone-js/router": "^0.8.4",
29
+ "@stone-js/core": "^0.8.4"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@babel/plugin-proposal-decorators": "^7.25.9",
33
33
  "@babel/preset-env": "^7.26.9",
34
34
  "@babel/preset-typescript": "^7.27.0",
35
- "@stone-js/cli": "^0.8.3",
35
+ "@stone-js/cli": "^0.8.4",
36
36
  "@types/node": "^24.0.7",
37
37
  "@types/react": "^19.0.7",
38
38
  "@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>
@@ -0,0 +1,69 @@
1
+ import { ILogger } from '@stone-js/core'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { ReactIncomingEvent } from '@stone-js/use-react'
4
+ import { WelcomePage } from '../app/pages/welcome/WelcomePage'
5
+ import { WelcomeService } from '../app/services/WelcomeService'
6
+
7
+ // We must mock decorators to lighten the test environment
8
+ vi.mock('@stone-js/use-react', async (importOriginal) => {
9
+ const actual: any = await importOriginal()
10
+ return {
11
+ ...actual,
12
+ Page: vi.fn(() => vi.fn()),
13
+ }
14
+ })
15
+
16
+ // We must mock decorators to lighten the test environment
17
+ vi.mock('@stone-js/core', async (importOriginal) => {
18
+ const actual: any = await importOriginal()
19
+ return {
20
+ ...actual,
21
+ Service: vi.fn(() => vi.fn()),
22
+ }
23
+ })
24
+
25
+ describe('WelcomePage', () => {
26
+ let page: WelcomePage
27
+ let mockedLogger: ILogger
28
+
29
+ beforeEach(() => {
30
+ mockedLogger = {
31
+ info: vi.fn(),
32
+ } as unknown as ILogger
33
+
34
+ page = new WelcomePage({ welcomeService: new WelcomeService({ logger: mockedLogger }) })
35
+ })
36
+
37
+ it('should handle incoming events', () => {
38
+ // Arrange
39
+ const expectedMessage = 'Hello World!'
40
+ const event = { get: () => 'World' } as unknown as ReactIncomingEvent
41
+
42
+ // Act
43
+ const response = page.handle(event)
44
+
45
+ // Assert
46
+ expect(response.message).toBe(expectedMessage)
47
+ expect(mockedLogger.info).toHaveBeenCalledWith('Welcome World')
48
+ })
49
+
50
+ it('should render the message', () => {
51
+ // Arrange
52
+ const message = 'Hello World!'
53
+
54
+ // Act
55
+ const response = renderToString(page.render({ data: { message } } as any))
56
+
57
+ // Assert
58
+ expect(response).toContain(message)
59
+ })
60
+
61
+ it('should render the Stone.js logo', () => {
62
+ // Act
63
+ const response = renderToString(page.render({ data: { message: 'Hello World!' } } as any))
64
+
65
+ // Assert
66
+ expect(response).toContain('/logo.svg')
67
+ expect(response).toContain('alt="Stone.js"')
68
+ })
69
+ })
@@ -38,6 +38,7 @@ export const WelcomePage = ({ welcomeService }: WelcomePageOptions): IPage<React
38
38
  render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
39
39
  return (
40
40
  <section className='container'>
41
+ <img src='/logo.svg' alt='Stone.js' width={96} height={96} />
41
42
  <h1 className='h1 text-center mt-64'>{data?.message}</h1>
42
43
  </section>
43
44
  )
@@ -20,19 +20,19 @@
20
20
  "dependencies": {
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0",
23
- "@stone-js/use-react": "^0.8.3",
24
- "@stone-js/browser-adapter": "^0.8.3",
25
- "@stone-js/pipeline": "^0.8.3",
26
- "@stone-js/http-core": "^0.8.3",
27
- "@stone-js/node-http-adapter": "^0.8.3",
28
- "@stone-js/router": "^0.8.3",
29
- "@stone-js/core": "^0.8.3"
23
+ "@stone-js/use-react": "^0.8.4",
24
+ "@stone-js/browser-adapter": "^0.8.4",
25
+ "@stone-js/pipeline": "^0.8.4",
26
+ "@stone-js/http-core": "^0.8.4",
27
+ "@stone-js/node-http-adapter": "^0.8.4",
28
+ "@stone-js/router": "^0.8.4",
29
+ "@stone-js/core": "^0.8.4"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@babel/plugin-proposal-decorators": "^7.25.9",
33
33
  "@babel/preset-env": "^7.26.9",
34
34
  "@babel/preset-typescript": "^7.27.0",
35
- "@stone-js/cli": "^0.8.3",
35
+ "@stone-js/cli": "^0.8.4",
36
36
  "@types/node": "^24.0.7",
37
37
  "@types/react": "^19.0.7",
38
38
  "@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>
@@ -0,0 +1,51 @@
1
+ import { ILogger } from '@stone-js/core'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { ReactIncomingEvent } from '@stone-js/use-react'
4
+ import { WelcomePage } from '../app/pages/welcome/WelcomePage'
5
+ import { WelcomeService } from '../app/services/WelcomeService'
6
+
7
+ describe('WelcomePage', () => {
8
+ let page: any
9
+ let mockedLogger: ILogger
10
+
11
+ beforeEach(() => {
12
+ mockedLogger = {
13
+ info: vi.fn(),
14
+ } as unknown as ILogger
15
+
16
+ page = WelcomePage({ welcomeService: WelcomeService({ logger: mockedLogger }) })
17
+ })
18
+
19
+ it('should handle incoming events', () => {
20
+ // Arrange
21
+ const expectedMessage = 'Hello World!'
22
+ const event = { get: () => 'World' } as unknown as ReactIncomingEvent
23
+
24
+ // Act
25
+ const response = page.handle(event) as { message: string }
26
+
27
+ // Assert
28
+ expect(response.message).toBe(expectedMessage)
29
+ expect(mockedLogger.info).toHaveBeenCalledWith('Welcome World')
30
+ })
31
+
32
+ it('should render the message', () => {
33
+ // Arrange
34
+ const message = 'Hello World!'
35
+
36
+ // Act
37
+ const response = renderToString(page.render({ data: { message } } as any))
38
+
39
+ // Assert
40
+ expect(response).toContain(message)
41
+ })
42
+
43
+ it('should render the Stone.js logo', () => {
44
+ // Act
45
+ const response = renderToString(page.render({ data: { message: 'Hello World!' } } as any))
46
+
47
+ // Assert
48
+ expect(response).toContain('/logo.svg')
49
+ expect(response).toContain('alt="Stone.js"')
50
+ })
51
+ })