@stone-js/starters 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/basic-react-declarative/app/Application.tsx +22 -6
- package/basic-react-declarative/assets/css/index.css +163 -18
- package/basic-react-declarative/package.json +8 -5
- package/basic-react-declarative/public/logo.svg +16 -0
- package/basic-react-declarative/tests/Application.spec.ts +13 -4
- package/basic-react-imperative/app/Application.tsx +22 -6
- package/basic-react-imperative/assets/css/index.css +163 -18
- package/basic-react-imperative/package.json +8 -5
- package/basic-react-imperative/public/logo.svg +16 -0
- package/basic-react-imperative/tests/Application.spec.ts +15 -4
- package/basic-service-declarative/app/Application.ts +18 -3
- package/basic-service-declarative/package.json +7 -4
- package/basic-service-declarative/tests/Application.spec.ts +3 -2
- package/basic-service-imperative/app/Application.ts +18 -3
- package/basic-service-imperative/package.json +7 -4
- package/basic-service-imperative/tests/Application.spec.ts +3 -2
- package/continuum-showcase/LICENSE +21 -0
- package/continuum-showcase/app/Application.ts +45 -0
- package/continuum-showcase/app/layouts/MainLayout.tsx +13 -8
- package/continuum-showcase/app/pages/BlogPostPage.tsx +6 -3
- package/continuum-showcase/app/pages/HomePage.tsx +21 -8
- package/continuum-showcase/assets/css/index.css +198 -0
- package/continuum-showcase/assets/images/logo.svg +16 -0
- package/continuum-showcase/package.json +28 -10
- package/continuum-showcase/tests/HomePage.spec.tsx +41 -0
- package/continuum-showcase/tsconfig.json +33 -0
- package/continuum-showcase/vitest.config.ts +31 -0
- package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
- package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-declarative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-react-declarative/app/pages/home/HomePage.tsx +22 -21
- package/full-react-declarative/assets/css/index.css +167 -0
- package/full-react-declarative/package.json +13 -10
- package/full-react-declarative/public/logo.svg +16 -0
- package/full-react-declarative/tests/AppLayout.spec.ts +51 -0
- package/full-react-declarative/tests/HomePage.spec.ts +38 -0
- package/full-react-imperative/app/components/LayoutHeader/LayoutHeader.tsx +1 -1
- package/full-react-imperative/app/components/PostWidget/PostWidget.tsx +1 -1
- package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.css +10 -0
- package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.tsx +66 -0
- package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-imperative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-react-imperative/app/pages/home/HomePage.tsx +32 -16
- package/full-react-imperative/assets/css/index.css +167 -0
- package/full-react-imperative/package.json +14 -12
- package/full-react-imperative/public/logo.svg +16 -0
- package/full-react-imperative/tests/HomePage.spec.ts +45 -0
- package/full-react-imperative/tests/LayoutHeader.spec.ts +39 -0
- package/full-service-declarative/app/handlers/WelcomeEventHandler.ts +47 -0
- package/full-service-declarative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-service-declarative/package.json +12 -9
- package/full-service-declarative/tests/PostService.spec.ts +56 -0
- package/full-service-declarative/tests/WelcomeEventHandler.spec.ts +54 -0
- package/full-service-declarative/vitest.config.ts +1 -1
- package/full-service-imperative/app/handlers/WelcomeEventHandler.ts +51 -0
- package/full-service-imperative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-service-imperative/package.json +12 -9
- package/full-service-imperative/tests/PostService.spec.ts +47 -0
- package/full-service-imperative/tests/WelcomeEventHandler.spec.ts +31 -0
- package/full-service-imperative/vitest.config.ts +1 -1
- package/package.json +1 -1
- package/standard-react-declarative/app/pages/welcome/WelcomePage.tsx +37 -4
- package/standard-react-declarative/assets/css/index.css +163 -18
- package/standard-react-declarative/package.json +11 -8
- package/standard-react-declarative/public/logo.svg +16 -0
- package/standard-react-declarative/tests/WelcomePage.spec.ts +71 -0
- package/standard-react-imperative/app/pages/welcome/WelcomePage.tsx +37 -4
- package/standard-react-imperative/assets/css/index.css +163 -18
- package/standard-react-imperative/package.json +11 -8
- package/standard-react-imperative/public/logo.svg +16 -0
- package/standard-react-imperative/tests/WelcomePage.spec.ts +53 -0
- package/standard-service-declarative/app/handlers/WelcomeEventHandler.ts +2 -2
- package/standard-service-declarative/app/services/WelcomeService.ts +28 -2
- package/standard-service-declarative/package.json +9 -6
- package/standard-service-declarative/tests/Application.spec.ts +100 -0
- package/standard-service-declarative/vitest.config.ts +1 -1
- package/standard-service-imperative/app/handlers/welcomeEventHandler.ts +2 -4
- package/standard-service-imperative/app/services/welcomeService.ts +28 -2
- package/standard-service-imperative/package.json +9 -6
- package/standard-service-imperative/tests/Application.spec.ts +84 -0
- package/standard-service-imperative/vitest.config.ts +1 -1
- package/basic-react-declarative/public/logo.png +0 -0
- package/basic-react-declarative/tests/__snapshots__/Application.spec.ts.snap +0 -3
- package/basic-react-imperative/public/logo.png +0 -0
- package/basic-react-imperative/tests/__snapshots__/Application.spec.ts.snap +0 -3
- package/full-react-declarative/assets/img/logo.png +0 -0
- package/full-react-declarative/public/logo.png +0 -0
- package/full-react-imperative/assets/img/logo.png +0 -0
- package/full-react-imperative/public/logo.png +0 -0
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@libsql/client": "^0.14.0",
|
|
25
|
-
"@stone-js/core": "^0.8.
|
|
26
|
-
"@stone-js/env": "^0.8.
|
|
27
|
-
"@stone-js/filesystem": "^0.8.
|
|
28
|
-
"@stone-js/http-core": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
30
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
31
|
-
"@stone-js/pipeline": "^0.8.
|
|
32
|
-
"@stone-js/router": "^0.8.
|
|
25
|
+
"@stone-js/core": "^0.8.5",
|
|
26
|
+
"@stone-js/env": "^0.8.5",
|
|
27
|
+
"@stone-js/filesystem": "^0.8.5",
|
|
28
|
+
"@stone-js/http-core": "^0.8.5",
|
|
29
|
+
"@stone-js/node-cli-adapter": "^0.8.5",
|
|
30
|
+
"@stone-js/node-http-adapter": "^0.8.5",
|
|
31
|
+
"@stone-js/pipeline": "^0.8.5",
|
|
32
|
+
"@stone-js/router": "^0.8.5",
|
|
33
33
|
"bcryptjs": "^3.0.2",
|
|
34
34
|
"drizzle-orm": "^0.44.2",
|
|
35
35
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -38,11 +38,14 @@
|
|
|
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.
|
|
41
|
+
"@stone-js/cli": "^0.8.5",
|
|
42
42
|
"@types/jsonwebtoken": "^9.0.8",
|
|
43
43
|
"@types/node": "^24.0.3",
|
|
44
44
|
"@vitest/coverage-v8": "^3.0.9",
|
|
45
45
|
"drizzle-kit": "^0.31.1",
|
|
46
46
|
"vitest": "^3.0.9"
|
|
47
|
+
},
|
|
48
|
+
"overrides": {
|
|
49
|
+
"glob": "^11.0.0"
|
|
47
50
|
}
|
|
48
51
|
}
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { IncomingHttpEvent } from '@stone-js/http-core'
|
|
2
|
+
import { factoryWelcomeEventHandler, WelcomePayload } from '../app/handlers/WelcomeEventHandler'
|
|
3
|
+
|
|
4
|
+
describe('factoryWelcomeEventHandler', () => {
|
|
5
|
+
const makeEvent = (name?: string): IncomingHttpEvent => ({
|
|
6
|
+
get: vi.fn((_key: string, fallback: string) => name ?? fallback),
|
|
7
|
+
}) as unknown as IncomingHttpEvent
|
|
8
|
+
|
|
9
|
+
it('should return a branded Stone.js payload greeting the given name', () => {
|
|
10
|
+
// Act
|
|
11
|
+
const handler = factoryWelcomeEventHandler()
|
|
12
|
+
const payload = handler(makeEvent('Ada')) as unknown as WelcomePayload
|
|
13
|
+
|
|
14
|
+
// Assert
|
|
15
|
+
expect(payload.message).toBe('Hello Ada! Welcome to Stone.js.')
|
|
16
|
+
expect(payload.framework.name).toBe('Stone.js')
|
|
17
|
+
expect(payload.framework.tagline).toBe('The continuum framework')
|
|
18
|
+
expect(payload.framework.docs).toBe('https://stonejs.dev')
|
|
19
|
+
expect(payload.framework.github).toBe('https://github.com/stone-foundation/stone-js-framework')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should fall back to the default name when none is provided', () => {
|
|
23
|
+
// Act
|
|
24
|
+
const handler = factoryWelcomeEventHandler()
|
|
25
|
+
const payload = handler(makeEvent()) as unknown as WelcomePayload
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(payload.message).toBe('Hello Stone! Welcome to Stone.js.')
|
|
29
|
+
expect(payload.framework.name).toBe('Stone.js')
|
|
30
|
+
})
|
|
31
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JSX } from 'react'
|
|
2
|
+
import { Promiseable } from '@stone-js/core'
|
|
2
3
|
import { WelcomeService } from '../../services/WelcomeService'
|
|
3
|
-
import { Page, ReactIncomingEvent, PageRenderContext, IPage } from '@stone-js/use-react'
|
|
4
|
+
import { Page, ReactIncomingEvent, PageRenderContext, PageHeadContext, HeadContext, IPage } from '@stone-js/use-react'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* WelcomePage Options
|
|
@@ -42,6 +43,22 @@ export class WelcomePage implements IPage<ReactIncomingEvent> {
|
|
|
42
43
|
return this.welcomeService.welcome(event.get<string>('name', 'World'))
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Set the page head tags, like title, meta, link, script, style.
|
|
48
|
+
*
|
|
49
|
+
* @returns The head context.
|
|
50
|
+
*/
|
|
51
|
+
head ({ event }: PageHeadContext): Promiseable<HeadContext> {
|
|
52
|
+
return {
|
|
53
|
+
title: `${String(event.get<string>('name', 'World'))} · Welcome to Stone.js`,
|
|
54
|
+
description: 'A universal Stone.js application. Write your domain once; Stone.js is the context that runs it anywhere.',
|
|
55
|
+
metas: [
|
|
56
|
+
{ name: 'author', content: 'Stone.js' },
|
|
57
|
+
{ name: 'keywords', content: 'stonejs,continuum,react,universal,framework' }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
/**
|
|
46
63
|
* Render the component.
|
|
47
64
|
*
|
|
@@ -49,9 +66,25 @@ export class WelcomePage implements IPage<ReactIncomingEvent> {
|
|
|
49
66
|
*/
|
|
50
67
|
render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
|
|
51
68
|
return (
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
69
|
+
<main className='stone-welcome'>
|
|
70
|
+
<div className='glow' aria-hidden='true' />
|
|
71
|
+
<section className='hero'>
|
|
72
|
+
<img className='mark' src='/logo.svg' alt='Stone.js' width={104} height={104} />
|
|
73
|
+
<p className='eyebrow'>Welcome to</p>
|
|
74
|
+
<h1 className='title'>Stone.js</h1>
|
|
75
|
+
<p className='lead'>{data?.message}</p>
|
|
76
|
+
<p className='tagline'>
|
|
77
|
+
Your app is running. Write your domain once, Stone.js is the context that runs it
|
|
78
|
+
anywhere: server, serverless, browser, CLI and the edge.
|
|
79
|
+
</p>
|
|
80
|
+
<nav className='links'>
|
|
81
|
+
<a href='https://stonejs.dev/docs' target='_blank' rel='noreferrer noopener'>Documentation</a>
|
|
82
|
+
<a href='https://github.com/stone-foundation/stone-js-framework' target='_blank' rel='noreferrer noopener'>GitHub</a>
|
|
83
|
+
<span className='edit'>Edit <b>app/pages/welcome/WelcomePage.tsx</b></span>
|
|
84
|
+
</nav>
|
|
85
|
+
</section>
|
|
86
|
+
<footer className='brand'><span className='dot'>●</span> Stone.js — the continuum framework</footer>
|
|
87
|
+
</main>
|
|
55
88
|
)
|
|
56
89
|
}
|
|
57
90
|
}
|
|
@@ -1,26 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Stone.js starter — brand welcome screen.
|
|
3
|
+
* Identity: "Obsidienne & Braise" — obsidian ground, ember (braise) accent, the Portal mark.
|
|
4
|
+
* Theme-aware: dark by default, light under prefers-color-scheme: light.
|
|
5
|
+
*/
|
|
6
|
+
:root {
|
|
7
|
+
--ink: #14100E;
|
|
8
|
+
--ink-2: #1E1815;
|
|
9
|
+
--cream: #F4EFE6;
|
|
10
|
+
--cream-dim: #B8AE9E;
|
|
11
|
+
--ember-1: #FFC46B;
|
|
12
|
+
--ember-2: #FF5A1F;
|
|
13
|
+
--line: rgba(244, 239, 230, 0.10);
|
|
14
|
+
--card: rgba(244, 239, 230, 0.04);
|
|
15
|
+
--bg: radial-gradient(1200px 600px at 50% -10%, #241C17 0%, var(--ink) 55%);
|
|
9
16
|
}
|
|
10
|
-
|
|
17
|
+
|
|
18
|
+
@media (prefers-color-scheme: light) {
|
|
19
|
+
:root {
|
|
20
|
+
--ink: #F4EFE6;
|
|
21
|
+
--ink-2: #ffffff;
|
|
22
|
+
--cream: #1B1613;
|
|
23
|
+
--cream-dim: #6B6154;
|
|
24
|
+
--line: rgba(27, 22, 19, 0.10);
|
|
25
|
+
--card: rgba(27, 22, 19, 0.03);
|
|
26
|
+
--bg: radial-gradient(1200px 600px at 50% -10%, #FFFFFF 0%, #F1E9DC 60%);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
|
|
32
|
+
body {
|
|
11
33
|
margin: 0;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
34
|
+
min-height: 100vh;
|
|
35
|
+
color: var(--cream);
|
|
36
|
+
background: var(--ink);
|
|
37
|
+
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
15
39
|
}
|
|
16
|
-
|
|
17
|
-
|
|
40
|
+
|
|
41
|
+
.stone-welcome {
|
|
42
|
+
position: relative;
|
|
43
|
+
min-height: 100vh;
|
|
18
44
|
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
19
47
|
justify-content: center;
|
|
48
|
+
padding: 48px 24px;
|
|
49
|
+
background: var(--bg);
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Ember glow behind the mark. */
|
|
54
|
+
.stone-welcome .glow {
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 8%;
|
|
57
|
+
left: 50%;
|
|
58
|
+
width: 520px;
|
|
59
|
+
height: 520px;
|
|
60
|
+
transform: translateX(-50%);
|
|
61
|
+
background: radial-gradient(circle, rgba(255, 90, 31, 0.22) 0%, rgba(255, 90, 31, 0) 68%);
|
|
62
|
+
filter: blur(8px);
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
z-index: 0;
|
|
20
65
|
}
|
|
21
|
-
|
|
66
|
+
|
|
67
|
+
.stone-welcome .hero {
|
|
68
|
+
position: relative;
|
|
69
|
+
z-index: 1;
|
|
70
|
+
width: 100%;
|
|
71
|
+
max-width: 680px;
|
|
22
72
|
text-align: center;
|
|
73
|
+
animation: rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.stone-welcome .mark {
|
|
77
|
+
width: 104px;
|
|
78
|
+
height: 104px;
|
|
79
|
+
filter: drop-shadow(0 8px 28px rgba(255, 90, 31, 0.35));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.stone-welcome .eyebrow {
|
|
83
|
+
margin: 22px 0 0;
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
letter-spacing: 0.28em;
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
color: var(--cream-dim);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stone-welcome .title {
|
|
92
|
+
margin: 8px 0 0;
|
|
93
|
+
font-size: clamp(44px, 8vw, 76px);
|
|
94
|
+
font-weight: 800;
|
|
95
|
+
letter-spacing: -0.03em;
|
|
96
|
+
line-height: 1.02;
|
|
97
|
+
background: linear-gradient(92deg, var(--ember-1), var(--ember-2));
|
|
98
|
+
-webkit-background-clip: text;
|
|
99
|
+
background-clip: text;
|
|
100
|
+
color: transparent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.stone-welcome .lead {
|
|
104
|
+
margin: 18px auto 0;
|
|
105
|
+
font-size: 20px;
|
|
106
|
+
font-weight: 600;
|
|
107
|
+
color: var(--cream);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.stone-welcome .tagline {
|
|
111
|
+
margin: 10px auto 0;
|
|
112
|
+
max-width: 46ch;
|
|
113
|
+
font-size: 16px;
|
|
114
|
+
line-height: 1.6;
|
|
115
|
+
color: var(--cream-dim);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.stone-welcome .links {
|
|
119
|
+
margin-top: 34px;
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
gap: 12px;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.stone-welcome .links a,
|
|
127
|
+
.stone-welcome .links .edit {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
padding: 11px 18px;
|
|
132
|
+
border: 1px solid var(--line);
|
|
133
|
+
border-radius: 12px;
|
|
134
|
+
background: var(--card);
|
|
135
|
+
color: var(--cream);
|
|
136
|
+
font-size: 14px;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
transition: border-color 0.2s, transform 0.2s, background 0.2s;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.stone-welcome .links a:hover {
|
|
143
|
+
transform: translateY(-2px);
|
|
144
|
+
border-color: rgba(255, 90, 31, 0.6);
|
|
145
|
+
background: rgba(255, 90, 31, 0.08);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.stone-welcome .links .edit {
|
|
149
|
+
font-family: ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
|
150
|
+
color: var(--cream-dim);
|
|
151
|
+
}
|
|
152
|
+
.stone-welcome .links .edit b { color: var(--ember-1); font-weight: 700; }
|
|
153
|
+
|
|
154
|
+
.stone-welcome .brand {
|
|
155
|
+
position: relative;
|
|
156
|
+
z-index: 1;
|
|
157
|
+
margin-top: 56px;
|
|
158
|
+
font-size: 13px;
|
|
159
|
+
letter-spacing: 0.02em;
|
|
160
|
+
color: var(--cream-dim);
|
|
161
|
+
}
|
|
162
|
+
.stone-welcome .brand .dot { color: var(--ember-2); }
|
|
163
|
+
|
|
164
|
+
@keyframes rise {
|
|
165
|
+
from { opacity: 0; transform: translateY(14px); }
|
|
166
|
+
to { opacity: 1; transform: translateY(0); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
.stone-welcome .hero { animation: none; }
|
|
23
171
|
}
|
|
24
|
-
.mt-64 {
|
|
25
|
-
margin-top: 64px;
|
|
26
|
-
}
|
|
@@ -20,23 +20,26 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0",
|
|
23
|
-
"@stone-js/use-react": "^0.8.
|
|
24
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
25
|
-
"@stone-js/pipeline": "^0.8.
|
|
26
|
-
"@stone-js/http-core": "^0.8.
|
|
27
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
28
|
-
"@stone-js/router": "^0.8.
|
|
29
|
-
"@stone-js/core": "^0.8.
|
|
23
|
+
"@stone-js/use-react": "^0.8.5",
|
|
24
|
+
"@stone-js/browser-adapter": "^0.8.5",
|
|
25
|
+
"@stone-js/pipeline": "^0.8.5",
|
|
26
|
+
"@stone-js/http-core": "^0.8.5",
|
|
27
|
+
"@stone-js/node-http-adapter": "^0.8.5",
|
|
28
|
+
"@stone-js/router": "^0.8.5",
|
|
29
|
+
"@stone-js/core": "^0.8.5"
|
|
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.
|
|
35
|
+
"@stone-js/cli": "^0.8.5",
|
|
36
36
|
"@types/node": "^24.0.7",
|
|
37
37
|
"@types/react": "^19.0.7",
|
|
38
38
|
"@types/react-dom": "^19.0.3",
|
|
39
39
|
"@vitest/coverage-v8": "^3.0.9",
|
|
40
40
|
"vitest": "^3.0.9"
|
|
41
|
+
},
|
|
42
|
+
"overrides": {
|
|
43
|
+
"glob": "^11.0.0"
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -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,71 @@
|
|
|
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
|
+
expect(response).toContain('Welcome')
|
|
69
|
+
expect(response).toContain('Stone.js')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JSX } from 'react'
|
|
2
|
+
import { Promiseable } from '@stone-js/core'
|
|
2
3
|
import { WelcomeService } from '../../services/WelcomeService'
|
|
3
|
-
import { definePage, IPage, PageRenderContext, ReactIncomingEvent } from '@stone-js/use-react'
|
|
4
|
+
import { definePage, IPage, PageRenderContext, PageHeadContext, HeadContext, ReactIncomingEvent } from '@stone-js/use-react'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* WelcomePage Options
|
|
@@ -30,6 +31,22 @@ export const WelcomePage = ({ welcomeService }: WelcomePageOptions): IPage<React
|
|
|
30
31
|
return welcomeService.welcome(event.get<string>('name', 'World'))
|
|
31
32
|
},
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Set the page head tags, like title, meta, link, script, style.
|
|
36
|
+
*
|
|
37
|
+
* @returns The head context.
|
|
38
|
+
*/
|
|
39
|
+
head ({ event }: PageHeadContext): Promiseable<HeadContext> {
|
|
40
|
+
return {
|
|
41
|
+
title: `${String(event.get<string>('name', 'World'))} · Welcome to Stone.js`,
|
|
42
|
+
description: 'A universal Stone.js application. Write your domain once; Stone.js is the context that runs it anywhere.',
|
|
43
|
+
metas: [
|
|
44
|
+
{ name: 'author', content: 'Stone.js' },
|
|
45
|
+
{ name: 'keywords', content: 'stonejs,continuum,react,universal,framework' }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
33
50
|
/**
|
|
34
51
|
* Render the component.
|
|
35
52
|
*
|
|
@@ -37,9 +54,25 @@ export const WelcomePage = ({ welcomeService }: WelcomePageOptions): IPage<React
|
|
|
37
54
|
*/
|
|
38
55
|
render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
|
|
39
56
|
return (
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
57
|
+
<main className='stone-welcome'>
|
|
58
|
+
<div className='glow' aria-hidden='true' />
|
|
59
|
+
<section className='hero'>
|
|
60
|
+
<img className='mark' src='/logo.svg' alt='Stone.js' width={104} height={104} />
|
|
61
|
+
<p className='eyebrow'>Welcome to</p>
|
|
62
|
+
<h1 className='title'>Stone.js</h1>
|
|
63
|
+
<p className='lead'>{data?.message}</p>
|
|
64
|
+
<p className='tagline'>
|
|
65
|
+
Your app is running. Write your domain once, Stone.js is the context that runs it
|
|
66
|
+
anywhere: server, serverless, browser, CLI and the edge.
|
|
67
|
+
</p>
|
|
68
|
+
<nav className='links'>
|
|
69
|
+
<a href='https://stonejs.dev/docs' target='_blank' rel='noreferrer noopener'>Documentation</a>
|
|
70
|
+
<a href='https://github.com/stone-foundation/stone-js-framework' target='_blank' rel='noreferrer noopener'>GitHub</a>
|
|
71
|
+
<span className='edit'>Edit <b>app/pages/welcome/WelcomePage.tsx</b></span>
|
|
72
|
+
</nav>
|
|
73
|
+
</section>
|
|
74
|
+
<footer className='brand'><span className='dot'>●</span> Stone.js — the continuum framework</footer>
|
|
75
|
+
</main>
|
|
43
76
|
)
|
|
44
77
|
}
|
|
45
78
|
})
|