@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
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { JSX } from 'react'
|
|
2
|
-
import { createPost } from '../utils'
|
|
3
2
|
import { Post } from '../../models/Post'
|
|
4
|
-
import { ILogger } from '@stone-js/core'
|
|
5
|
-
import { PostForm } from '../../components/PostForm/PostForm'
|
|
6
|
-
import { PostWidget } from '../../components/PostWidget/PostWidget'
|
|
7
3
|
import { IPostService } from '../../services/contracts/IPostService'
|
|
8
|
-
import { IPage, ReactIncomingEvent, HeadContext, definePage,
|
|
4
|
+
import { IPage, ReactIncomingEvent, HeadContext, definePage, StoneLink, PageRenderContext } from '@stone-js/use-react'
|
|
9
5
|
|
|
10
6
|
/**
|
|
11
7
|
* HomePage options.
|
|
12
8
|
*/
|
|
13
9
|
export interface HomePageOptions {
|
|
14
|
-
router: IRouter
|
|
15
|
-
logger: ILogger
|
|
16
10
|
postService: IPostService
|
|
17
11
|
}
|
|
18
12
|
|
|
19
13
|
/**
|
|
20
14
|
* Home Page.
|
|
21
15
|
*/
|
|
22
|
-
export const HomePage = ({
|
|
16
|
+
export const HomePage = ({ postService }: HomePageOptions): IPage<ReactIncomingEvent> => ({
|
|
23
17
|
head (): HeadContext {
|
|
24
18
|
return {
|
|
25
|
-
title: '
|
|
26
|
-
description: '
|
|
19
|
+
title: 'Welcome to Stone.js',
|
|
20
|
+
description: 'Stone.js timeline — write your domain once, Stone.js is the context that runs it anywhere.'
|
|
27
21
|
}
|
|
28
22
|
},
|
|
29
23
|
|
|
@@ -34,7 +28,12 @@ export const HomePage = ({ router, logger, postService }: HomePageOptions): IPag
|
|
|
34
28
|
* @returns List of posts.
|
|
35
29
|
*/
|
|
36
30
|
async handle (event: ReactIncomingEvent): Promise<Post[]> {
|
|
37
|
-
|
|
31
|
+
try {
|
|
32
|
+
return await postService.list(event.get<number>('limit', 10))
|
|
33
|
+
} catch {
|
|
34
|
+
// The welcome hero is public: render gracefully even when the API is unreachable.
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
38
37
|
},
|
|
39
38
|
|
|
40
39
|
/**
|
|
@@ -42,12 +41,29 @@ export const HomePage = ({ router, logger, postService }: HomePageOptions): IPag
|
|
|
42
41
|
*
|
|
43
42
|
* @returns The rendered component.
|
|
44
43
|
*/
|
|
45
|
-
render ({ data = []
|
|
44
|
+
render ({ data = [] }: PageRenderContext<Post[]>): JSX.Element {
|
|
45
|
+
const count = data.length
|
|
46
|
+
const lead = `Your timeline is live — ${count} ${count === 1 ? 'post' : 'posts'} published.`
|
|
46
47
|
return (
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
48
|
+
<main className='stone-welcome'>
|
|
49
|
+
<div className='glow' aria-hidden='true' />
|
|
50
|
+
<section className='hero'>
|
|
51
|
+
<img className='mark' src='/logo.svg' alt='Stone.js' width={104} height={104} />
|
|
52
|
+
<p className='eyebrow'>Welcome to</p>
|
|
53
|
+
<h1 className='title'>Stone.js</h1>
|
|
54
|
+
<p className='lead'>{lead}</p>
|
|
55
|
+
<p className='tagline'>
|
|
56
|
+
Write your domain once, Stone.js is the context that runs it anywhere:
|
|
57
|
+
server, serverless, browser, CLI and the edge.
|
|
58
|
+
</p>
|
|
59
|
+
<nav className='links'>
|
|
60
|
+
<StoneLink to='/users'>Browse Users</StoneLink>
|
|
61
|
+
<a href='https://stonejs.dev/docs' target='_blank' rel='noreferrer noopener'>Documentation</a>
|
|
62
|
+
<a href='https://github.com/stone-foundation/stone-js-framework' target='_blank' rel='noreferrer noopener'>GitHub</a>
|
|
63
|
+
</nav>
|
|
64
|
+
</section>
|
|
65
|
+
<footer className='brand'><span className='dot'>●</span> Stone.js — the continuum framework</footer>
|
|
66
|
+
</main>
|
|
51
67
|
)
|
|
52
68
|
}
|
|
53
69
|
})
|
|
@@ -299,3 +299,170 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
299
299
|
background: #FEE2E2;
|
|
300
300
|
border-left: 4px solid #EF4444;
|
|
301
301
|
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Stone.js brand welcome hero — "Obsidienne & Braise".
|
|
305
|
+
* Fully scoped under .stone-welcome so it never clashes with the app's styles.
|
|
306
|
+
* Theme-aware: dark by default, light under prefers-color-scheme: light.
|
|
307
|
+
*/
|
|
308
|
+
:root {
|
|
309
|
+
--sw-ink: #14100E;
|
|
310
|
+
--sw-ink-2: #1E1815;
|
|
311
|
+
--sw-cream: #F4EFE6;
|
|
312
|
+
--sw-cream-dim: #B8AE9E;
|
|
313
|
+
--sw-ember-1: #FFC46B;
|
|
314
|
+
--sw-ember-2: #FF5A1F;
|
|
315
|
+
--sw-line: rgba(244, 239, 230, 0.10);
|
|
316
|
+
--sw-card: rgba(244, 239, 230, 0.04);
|
|
317
|
+
--sw-bg: radial-gradient(1200px 600px at 50% -10%, #241C17 0%, var(--sw-ink) 55%);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@media (prefers-color-scheme: light) {
|
|
321
|
+
:root {
|
|
322
|
+
--sw-ink: #F4EFE6;
|
|
323
|
+
--sw-ink-2: #ffffff;
|
|
324
|
+
--sw-cream: #1B1613;
|
|
325
|
+
--sw-cream-dim: #6B6154;
|
|
326
|
+
--sw-line: rgba(27, 22, 19, 0.10);
|
|
327
|
+
--sw-card: rgba(27, 22, 19, 0.03);
|
|
328
|
+
--sw-bg: radial-gradient(1200px 600px at 50% -10%, #FFFFFF 0%, #F1E9DC 60%);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.stone-welcome,
|
|
333
|
+
.stone-welcome * { box-sizing: border-box; }
|
|
334
|
+
|
|
335
|
+
.stone-welcome {
|
|
336
|
+
position: relative;
|
|
337
|
+
min-height: 100vh;
|
|
338
|
+
display: flex;
|
|
339
|
+
flex-direction: column;
|
|
340
|
+
align-items: center;
|
|
341
|
+
justify-content: center;
|
|
342
|
+
padding: 48px 24px;
|
|
343
|
+
color: var(--sw-cream);
|
|
344
|
+
background: var(--sw-bg);
|
|
345
|
+
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
346
|
+
-webkit-font-smoothing: antialiased;
|
|
347
|
+
overflow: hidden;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Ember glow behind the mark. */
|
|
351
|
+
.stone-welcome .glow {
|
|
352
|
+
position: absolute;
|
|
353
|
+
top: 8%;
|
|
354
|
+
left: 50%;
|
|
355
|
+
width: 520px;
|
|
356
|
+
height: 520px;
|
|
357
|
+
transform: translateX(-50%);
|
|
358
|
+
background: radial-gradient(circle, rgba(255, 90, 31, 0.22) 0%, rgba(255, 90, 31, 0) 68%);
|
|
359
|
+
filter: blur(8px);
|
|
360
|
+
pointer-events: none;
|
|
361
|
+
z-index: 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.stone-welcome .hero {
|
|
365
|
+
position: relative;
|
|
366
|
+
z-index: 1;
|
|
367
|
+
width: 100%;
|
|
368
|
+
max-width: 680px;
|
|
369
|
+
text-align: center;
|
|
370
|
+
animation: sw-rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.stone-welcome .mark {
|
|
374
|
+
width: 104px;
|
|
375
|
+
height: 104px;
|
|
376
|
+
filter: drop-shadow(0 8px 28px rgba(255, 90, 31, 0.35));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.stone-welcome .eyebrow {
|
|
380
|
+
margin: 22px 0 0;
|
|
381
|
+
font-size: 12px;
|
|
382
|
+
font-weight: 600;
|
|
383
|
+
letter-spacing: 0.28em;
|
|
384
|
+
text-transform: uppercase;
|
|
385
|
+
color: var(--sw-cream-dim);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.stone-welcome .title {
|
|
389
|
+
margin: 8px 0 0;
|
|
390
|
+
font-size: clamp(44px, 8vw, 76px);
|
|
391
|
+
font-weight: 800;
|
|
392
|
+
letter-spacing: -0.03em;
|
|
393
|
+
line-height: 1.02;
|
|
394
|
+
background: linear-gradient(92deg, var(--sw-ember-1), var(--sw-ember-2));
|
|
395
|
+
-webkit-background-clip: text;
|
|
396
|
+
background-clip: text;
|
|
397
|
+
color: transparent;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.stone-welcome .lead {
|
|
401
|
+
margin: 18px auto 0;
|
|
402
|
+
font-size: 20px;
|
|
403
|
+
font-weight: 600;
|
|
404
|
+
color: var(--sw-cream);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.stone-welcome .tagline {
|
|
408
|
+
margin: 10px auto 0;
|
|
409
|
+
max-width: 46ch;
|
|
410
|
+
font-size: 16px;
|
|
411
|
+
line-height: 1.6;
|
|
412
|
+
color: var(--sw-cream-dim);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.stone-welcome .links {
|
|
416
|
+
margin-top: 34px;
|
|
417
|
+
display: flex;
|
|
418
|
+
flex-wrap: wrap;
|
|
419
|
+
gap: 12px;
|
|
420
|
+
justify-content: center;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.stone-welcome .links a,
|
|
424
|
+
.stone-welcome .links .edit {
|
|
425
|
+
display: inline-flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 8px;
|
|
428
|
+
padding: 11px 18px;
|
|
429
|
+
border: 1px solid var(--sw-line);
|
|
430
|
+
border-radius: 12px;
|
|
431
|
+
background: var(--sw-card);
|
|
432
|
+
color: var(--sw-cream);
|
|
433
|
+
font-size: 14px;
|
|
434
|
+
font-weight: 600;
|
|
435
|
+
text-decoration: none;
|
|
436
|
+
transition: border-color 0.2s, transform 0.2s, background 0.2s;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.stone-welcome .links a:hover {
|
|
440
|
+
transform: translateY(-2px);
|
|
441
|
+
border-color: rgba(255, 90, 31, 0.6);
|
|
442
|
+
background: rgba(255, 90, 31, 0.08);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.stone-welcome .links .edit {
|
|
446
|
+
font-family: ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
|
447
|
+
color: var(--sw-cream-dim);
|
|
448
|
+
}
|
|
449
|
+
.stone-welcome .links .edit b { color: var(--sw-ember-1); font-weight: 700; }
|
|
450
|
+
|
|
451
|
+
.stone-welcome .brand {
|
|
452
|
+
position: relative;
|
|
453
|
+
z-index: 1;
|
|
454
|
+
margin-top: 56px;
|
|
455
|
+
font-size: 13px;
|
|
456
|
+
letter-spacing: 0.02em;
|
|
457
|
+
color: var(--sw-cream-dim);
|
|
458
|
+
}
|
|
459
|
+
.stone-welcome .brand .dot { color: var(--sw-ember-2); }
|
|
460
|
+
|
|
461
|
+
@keyframes sw-rise {
|
|
462
|
+
from { opacity: 0; transform: translateY(14px); }
|
|
463
|
+
to { opacity: 1; transform: translateY(0); }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@media (prefers-reduced-motion: reduce) {
|
|
467
|
+
.stone-welcome .hero { animation: none; }
|
|
468
|
+
}
|
|
@@ -18,31 +18,33 @@
|
|
|
18
18
|
"test:cvg": "npm run test -- --coverage"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@stone-js/env": "^0.8.
|
|
22
|
-
"@stone-js/http-core": "^0.8.
|
|
23
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
24
|
-
"@stone-js/router": "^0.8.
|
|
25
|
-
"@stone-js/filesystem": "^0.8.
|
|
26
|
-
"@stone-js/core": "^0.8.
|
|
27
|
-
"@stone-js/use-react": "^0.8.
|
|
28
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
21
|
+
"@stone-js/env": "^0.8.5",
|
|
22
|
+
"@stone-js/http-core": "^0.8.5",
|
|
23
|
+
"@stone-js/node-http-adapter": "^0.8.5",
|
|
24
|
+
"@stone-js/router": "^0.8.5",
|
|
25
|
+
"@stone-js/filesystem": "^0.8.5",
|
|
26
|
+
"@stone-js/core": "^0.8.5",
|
|
27
|
+
"@stone-js/use-react": "^0.8.5",
|
|
28
|
+
"@stone-js/browser-adapter": "^0.8.5",
|
|
29
|
+
"@stone-js/node-cli-adapter": "^0.8.5",
|
|
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.
|
|
39
|
+
"@stone-js/cli": "^0.8.5",
|
|
41
40
|
"@types/axios": "^0.14.4",
|
|
42
41
|
"@types/node": "^24.0.7",
|
|
43
42
|
"@types/react": "^19.0.7",
|
|
44
43
|
"@types/react-dom": "^19.0.3",
|
|
45
44
|
"@vitest/coverage-v8": "^3.0.9",
|
|
46
45
|
"vitest": "^3.0.9"
|
|
46
|
+
},
|
|
47
|
+
"overrides": {
|
|
48
|
+
"glob": "^11.0.0"
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -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,45 @@
|
|
|
1
|
+
import { renderToString } from 'react-dom/server'
|
|
2
|
+
import { HomePage } from '../app/pages/home/HomePage'
|
|
3
|
+
|
|
4
|
+
// We must mock router-bound components to lighten the test environment
|
|
5
|
+
vi.mock('@stone-js/use-react', async (importOriginal) => {
|
|
6
|
+
const actual: any = await importOriginal()
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
StoneLink: ({ children }: any) => children
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('HomePage', () => {
|
|
14
|
+
const render = (data: any[] = []): string => {
|
|
15
|
+
const page = HomePage({ postService: {} as any })
|
|
16
|
+
return renderToString(page.render?.({ data } as any))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
it('should render the premium Stone.js welcome hero with the real logo', () => {
|
|
20
|
+
// Act
|
|
21
|
+
const html = render()
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(html).toContain('/logo.svg')
|
|
25
|
+
expect(html).toContain('alt="Stone.js"')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should render the Welcome to Stone.js headline', () => {
|
|
29
|
+
// Act
|
|
30
|
+
const html = render()
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(html).toContain('Welcome')
|
|
34
|
+
expect(html).toContain('Stone.js')
|
|
35
|
+
expect(html).toContain('stone-welcome')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should reflect the number of published posts in the lead', () => {
|
|
39
|
+
// Act
|
|
40
|
+
const html = render([{ id: 1 }, { id: 2 }])
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect(html).toContain('2 posts')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EventHandler, Get } from '@stone-js/router'
|
|
2
|
+
import { IncomingHttpEvent, JsonHttpResponse } from '@stone-js/http-core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Framework brand descriptor returned by the welcome endpoint.
|
|
6
|
+
*/
|
|
7
|
+
export interface FrameworkBrand {
|
|
8
|
+
name: string
|
|
9
|
+
tagline: string
|
|
10
|
+
docs: string
|
|
11
|
+
github: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Branded welcome payload.
|
|
16
|
+
*/
|
|
17
|
+
export interface WelcomePayload {
|
|
18
|
+
message: string
|
|
19
|
+
framework: FrameworkBrand
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Welcome Event Handler
|
|
24
|
+
*
|
|
25
|
+
* Greets the caller at the application root with a branded Stone.js payload.
|
|
26
|
+
* The `name` query param keeps the domain behavior meaningful.
|
|
27
|
+
*/
|
|
28
|
+
@EventHandler('/', { name: 'welcome' })
|
|
29
|
+
export class WelcomeEventHandler {
|
|
30
|
+
/**
|
|
31
|
+
* Return the branded welcome payload.
|
|
32
|
+
*/
|
|
33
|
+
@Get('/', { name: 'index' })
|
|
34
|
+
@JsonHttpResponse(200)
|
|
35
|
+
welcome (event: IncomingHttpEvent): WelcomePayload {
|
|
36
|
+
const name = event.get<string>('name', 'Stone')
|
|
37
|
+
return {
|
|
38
|
+
message: `Hello ${name}! Welcome to Stone.js.`,
|
|
39
|
+
framework: {
|
|
40
|
+
name: 'Stone.js',
|
|
41
|
+
tagline: 'The continuum framework',
|
|
42
|
+
docs: 'https://stonejs.dev',
|
|
43
|
+
github: 'https://github.com/stone-foundation/stone-js-framework'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -14,7 +14,7 @@ export interface AuthMiddlewareOptions {
|
|
|
14
14
|
*/
|
|
15
15
|
@Middleware({
|
|
16
16
|
global: true,
|
|
17
|
-
params: [['/login', '/register']]
|
|
17
|
+
params: [['/', '/login', '/register']]
|
|
18
18
|
})
|
|
19
19
|
export class AuthMiddleware implements IMiddleware<IncomingHttpEvent, OutgoingHttpResponse> {
|
|
20
20
|
private readonly securityService: SecurityService
|
|
@@ -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,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
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { WelcomeEventHandler } from '../app/handlers/WelcomeEventHandler'
|
|
2
|
+
import { IncomingHttpEvent } from '@stone-js/http-core'
|
|
3
|
+
|
|
4
|
+
// Neutralize the routing/http decorators to lighten the test environment,
|
|
5
|
+
// while asserting the real branded payload the handler returns.
|
|
6
|
+
vi.mock(import('@stone-js/router'), async (importOriginal) => {
|
|
7
|
+
const actual = await importOriginal()
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
EventHandler: vi.fn(() => vi.fn()),
|
|
11
|
+
Get: vi.fn(() => vi.fn()),
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
vi.mock(import('@stone-js/http-core'), async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal()
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
JsonHttpResponse: vi.fn(() => vi.fn()),
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('WelcomeEventHandler', () => {
|
|
24
|
+
let handler: WelcomeEventHandler
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
handler = new WelcomeEventHandler()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const makeEvent = (name?: string): IncomingHttpEvent => ({
|
|
31
|
+
get: vi.fn((_key: string, fallback: string) => name ?? fallback),
|
|
32
|
+
}) as unknown as IncomingHttpEvent
|
|
33
|
+
|
|
34
|
+
it('should return a branded Stone.js payload greeting the given name', () => {
|
|
35
|
+
// Act
|
|
36
|
+
const payload = handler.welcome(makeEvent('Ada'))
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(payload.message).toBe('Hello Ada! Welcome to Stone.js.')
|
|
40
|
+
expect(payload.framework.name).toBe('Stone.js')
|
|
41
|
+
expect(payload.framework.tagline).toBe('The continuum framework')
|
|
42
|
+
expect(payload.framework.docs).toBe('https://stonejs.dev')
|
|
43
|
+
expect(payload.framework.github).toBe('https://github.com/stone-foundation/stone-js-framework')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should fall back to the default name when none is provided', () => {
|
|
47
|
+
// Act
|
|
48
|
+
const payload = handler.welcome(makeEvent())
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(payload.message).toBe('Hello Stone! Welcome to Stone.js.')
|
|
52
|
+
expect(payload.framework.name).toBe('Stone.js')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IncomingHttpEvent } from '@stone-js/http-core'
|
|
2
|
+
import { FunctionalEventHandler } from '@stone-js/core'
|
|
3
|
+
import { defineRoutes, GET } from '@stone-js/router'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Framework brand descriptor returned by the welcome endpoint.
|
|
7
|
+
*/
|
|
8
|
+
export interface FrameworkBrand {
|
|
9
|
+
name: string
|
|
10
|
+
tagline: string
|
|
11
|
+
docs: string
|
|
12
|
+
github: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Branded welcome payload.
|
|
17
|
+
*/
|
|
18
|
+
export interface WelcomePayload {
|
|
19
|
+
message: string
|
|
20
|
+
framework: FrameworkBrand
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Welcome event handler factory.
|
|
25
|
+
*
|
|
26
|
+
* Greets the caller at the application root with a branded Stone.js payload.
|
|
27
|
+
* The `name` query param keeps the domain behavior meaningful.
|
|
28
|
+
*/
|
|
29
|
+
export function factoryWelcomeEventHandler (): FunctionalEventHandler<IncomingHttpEvent> {
|
|
30
|
+
return function (event: IncomingHttpEvent): WelcomePayload {
|
|
31
|
+
const name = event.get<string>('name', 'Stone')
|
|
32
|
+
return {
|
|
33
|
+
message: `Hello ${name}! Welcome to Stone.js.`,
|
|
34
|
+
framework: {
|
|
35
|
+
name: 'Stone.js',
|
|
36
|
+
tagline: 'The continuum framework',
|
|
37
|
+
docs: 'https://stonejs.dev',
|
|
38
|
+
github: 'https://github.com/stone-foundation/stone-js-framework'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Welcome Event Handler
|
|
46
|
+
*/
|
|
47
|
+
export const WelcomeEventHandler = defineRoutes(
|
|
48
|
+
[
|
|
49
|
+
[factoryWelcomeEventHandler, { isFactory: true, path: '/', method: GET, name: 'welcome.index' }]
|
|
50
|
+
]
|
|
51
|
+
)
|
|
@@ -41,4 +41,4 @@ export function factoryAuthMiddleware ({ securityService }: AuthMiddlewareOption
|
|
|
41
41
|
/**
|
|
42
42
|
* Auth middleware
|
|
43
43
|
*/
|
|
44
|
-
export const AuthMiddleware = defineMiddleware(factoryAuthMiddleware, { isFactory: true, global: true, params: [['/login', '/register']] })
|
|
44
|
+
export const AuthMiddleware = defineMiddleware(factoryAuthMiddleware, { isFactory: true, global: true, params: [['/', '/login', '/register']] })
|