create-foldkit-app 0.26.0 → 0.27.1
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 +12 -2
- package/dist/commands/create.js +28 -11
- package/dist/index.js +4 -1
- package/dist/rendering.js +20 -0
- package/dist/utils/files.js +20 -4
- package/dist/utils/packages.js +41 -12
- package/package.json +1 -1
- package/templates/package-managers/pnpm/pnpm-workspace.yaml +1 -0
- package/templates/rendering/ssg/README.md +61 -0
- package/templates/rendering/ssg/package.json +14 -0
- package/templates/rendering/ssg/scripts/build.mjs +40 -0
- package/templates/rendering/ssg/scripts/prerender.ts +71 -0
- package/templates/rendering/ssg/src/entry.server.ts +18 -0
- package/templates/rendering/ssg/src/entry.ts +28 -0
- package/templates/rendering/ssg/src/main.ts +176 -0
- package/templates/rendering/ssg/src/route.ts +20 -0
- package/templates/rendering/ssg/src/scene.test.ts +27 -0
- package/templates/rendering/ssg/src/vite-env.d.ts +19 -0
- package/templates/rendering/ssg/tsconfig.json +16 -0
- package/templates/rendering/ssg/vite.config.ts +17 -0
- package/templates/rendering/ssr/README.md +65 -0
- package/templates/rendering/ssr/package.json +14 -0
- package/templates/rendering/ssr/scripts/build.mjs +36 -0
- package/templates/rendering/ssr/server/main.ts +205 -0
- package/templates/rendering/ssr/src/cookie.ts +13 -0
- package/templates/rendering/ssr/src/entry.server.ts +50 -0
- package/templates/rendering/ssr/src/entry.ts +17 -0
- package/templates/rendering/ssr/src/main.ts +155 -0
- package/templates/rendering/ssr/src/scene.test.ts +43 -0
- package/templates/rendering/ssr/src/vite-env.d.ts +19 -0
- package/templates/rendering/ssr/tsconfig.json +16 -0
- package/templates/rendering/ssr/vite.config.ts +17 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Effect, Match as M, Schema as S } from 'effect'
|
|
2
|
+
import { Command, Runtime } from 'foldkit'
|
|
3
|
+
import { type Document, type HtmlBuilder } from 'foldkit/html'
|
|
4
|
+
import { m } from 'foldkit/message'
|
|
5
|
+
import { evo } from 'foldkit/struct'
|
|
6
|
+
|
|
7
|
+
import { Button } from '@foldkit/ui'
|
|
8
|
+
|
|
9
|
+
import { COUNT_COOKIE } from './cookie'
|
|
10
|
+
|
|
11
|
+
// MODEL
|
|
12
|
+
|
|
13
|
+
export const Model = S.Struct({
|
|
14
|
+
count: S.Number,
|
|
15
|
+
renderedAt: S.String,
|
|
16
|
+
renderedOn: S.Literals(['Server', 'Client']),
|
|
17
|
+
})
|
|
18
|
+
export type Model = typeof Model.Type
|
|
19
|
+
|
|
20
|
+
// FLAGS
|
|
21
|
+
|
|
22
|
+
export const Flags = S.Struct({
|
|
23
|
+
initialCount: S.Number,
|
|
24
|
+
renderedAt: S.String,
|
|
25
|
+
renderedOn: S.Literals(['Server', 'Client']),
|
|
26
|
+
})
|
|
27
|
+
export type Flags = typeof Flags.Type
|
|
28
|
+
|
|
29
|
+
// MESSAGE
|
|
30
|
+
|
|
31
|
+
export const ClickedDecrement = m('ClickedDecrement')
|
|
32
|
+
export const ClickedIncrement = m('ClickedIncrement')
|
|
33
|
+
export const CompletedPersistCount = m('CompletedPersistCount')
|
|
34
|
+
|
|
35
|
+
export const Message = S.Union([
|
|
36
|
+
ClickedDecrement,
|
|
37
|
+
ClickedIncrement,
|
|
38
|
+
CompletedPersistCount,
|
|
39
|
+
])
|
|
40
|
+
export type Message = typeof Message.Type
|
|
41
|
+
|
|
42
|
+
// UPDATE
|
|
43
|
+
|
|
44
|
+
export const update = (
|
|
45
|
+
model: Model,
|
|
46
|
+
message: Message,
|
|
47
|
+
): readonly [Model, ReadonlyArray<Command.Command<Message>>] =>
|
|
48
|
+
M.value(message).pipe(
|
|
49
|
+
M.withReturnType<
|
|
50
|
+
readonly [Model, ReadonlyArray<Command.Command<Message>>]
|
|
51
|
+
>(),
|
|
52
|
+
M.tagsExhaustive({
|
|
53
|
+
ClickedDecrement: () => {
|
|
54
|
+
const nextCount = model.count - 1
|
|
55
|
+
return [
|
|
56
|
+
evo(model, { count: () => nextCount }),
|
|
57
|
+
[PersistCount({ count: nextCount })],
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
ClickedIncrement: () => {
|
|
61
|
+
const nextCount = model.count + 1
|
|
62
|
+
return [
|
|
63
|
+
evo(model, { count: () => nextCount }),
|
|
64
|
+
[PersistCount({ count: nextCount })],
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
CompletedPersistCount: () => [model, []],
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// COMMAND
|
|
72
|
+
|
|
73
|
+
const COUNT_COOKIE_MAX_AGE_SECONDS = 31536000
|
|
74
|
+
|
|
75
|
+
export const PersistCount = Command.define('PersistCount', {
|
|
76
|
+
args: { count: S.Number },
|
|
77
|
+
messages: [CompletedPersistCount],
|
|
78
|
+
execute: ({ count }) =>
|
|
79
|
+
Effect.try(() => {
|
|
80
|
+
document.cookie = `${COUNT_COOKIE}=${count}; path=/; max-age=${COUNT_COOKIE_MAX_AGE_SECONDS}`
|
|
81
|
+
}).pipe(
|
|
82
|
+
Effect.map(() => CompletedPersistCount()),
|
|
83
|
+
Effect.catch(() => Effect.succeed(CompletedPersistCount())),
|
|
84
|
+
),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// INIT
|
|
88
|
+
|
|
89
|
+
export const init: Runtime.ApplicationInit<Model, Message, Flags> = flags => [
|
|
90
|
+
{
|
|
91
|
+
count: flags.initialCount,
|
|
92
|
+
renderedAt: flags.renderedAt,
|
|
93
|
+
renderedOn: flags.renderedOn,
|
|
94
|
+
},
|
|
95
|
+
[],
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
// VIEW
|
|
99
|
+
|
|
100
|
+
export const view = (model: Model, h: HtmlBuilder<Message>): Document => ({
|
|
101
|
+
title: `Count ${model.count}`,
|
|
102
|
+
body: h.div(
|
|
103
|
+
[
|
|
104
|
+
h.Class(
|
|
105
|
+
'min-h-screen bg-white flex flex-col items-center justify-center gap-6 p-6',
|
|
106
|
+
),
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
h.h1(
|
|
110
|
+
[h.Class('text-2xl font-semibold text-gray-800')],
|
|
111
|
+
['Server-rendered counter'],
|
|
112
|
+
),
|
|
113
|
+
h.p(
|
|
114
|
+
[h.Id('count'), h.Class('text-6xl font-bold text-gray-800')],
|
|
115
|
+
[model.count.toString()],
|
|
116
|
+
),
|
|
117
|
+
h.div(
|
|
118
|
+
[h.Class('flex flex-wrap justify-center gap-4')],
|
|
119
|
+
[
|
|
120
|
+
Button.view(
|
|
121
|
+
{
|
|
122
|
+
onClick: ClickedDecrement(),
|
|
123
|
+
toView: attributes =>
|
|
124
|
+
h.button([...attributes.button, h.Class(buttonStyle)], ['-']),
|
|
125
|
+
},
|
|
126
|
+
h,
|
|
127
|
+
),
|
|
128
|
+
Button.view(
|
|
129
|
+
{
|
|
130
|
+
onClick: ClickedIncrement(),
|
|
131
|
+
toView: attributes =>
|
|
132
|
+
h.button([...attributes.button, h.Class(buttonStyle)], ['+']),
|
|
133
|
+
},
|
|
134
|
+
h,
|
|
135
|
+
),
|
|
136
|
+
],
|
|
137
|
+
),
|
|
138
|
+
h.p(
|
|
139
|
+
[h.Id('provenance'), h.Class('text-sm text-gray-500')],
|
|
140
|
+
[`Rendered on the ${model.renderedOn} at ${model.renderedAt}`],
|
|
141
|
+
),
|
|
142
|
+
h.p(
|
|
143
|
+
[h.Class('text-sm text-gray-500 max-w-md text-center')],
|
|
144
|
+
[
|
|
145
|
+
'The count persists in a cookie. Reload the page and the server ' +
|
|
146
|
+
'renders your latest count into the HTML before any JavaScript runs.',
|
|
147
|
+
],
|
|
148
|
+
),
|
|
149
|
+
],
|
|
150
|
+
),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// STYLE
|
|
154
|
+
|
|
155
|
+
const buttonStyle = 'bg-black text-white hover:bg-gray-700 px-4 py-2 transition'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Command, click, expect, given, role, scene, text } from 'foldkit/scene'
|
|
2
|
+
import { describe, test } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CompletedPersistCount,
|
|
6
|
+
Model,
|
|
7
|
+
PersistCount,
|
|
8
|
+
update,
|
|
9
|
+
view,
|
|
10
|
+
} from './main'
|
|
11
|
+
|
|
12
|
+
const initialModel = Model.make({
|
|
13
|
+
count: 3,
|
|
14
|
+
renderedAt: '2026-07-26T00:00:00.000Z',
|
|
15
|
+
renderedOn: 'Server',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('view', () => {
|
|
19
|
+
test('renders the count and provenance line', () => {
|
|
20
|
+
scene(
|
|
21
|
+
{ update, view },
|
|
22
|
+
given(initialModel),
|
|
23
|
+
expect(text('3')).toExist(),
|
|
24
|
+
expect(
|
|
25
|
+
text('Rendered on the Server at 2026-07-26T00:00:00.000Z'),
|
|
26
|
+
).toExist(),
|
|
27
|
+
expect(role('button', { name: '+' })).toExist(),
|
|
28
|
+
expect(role('button', { name: '-' })).toExist(),
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('clicking + increments and persists the count', () => {
|
|
33
|
+
scene(
|
|
34
|
+
{ update, view },
|
|
35
|
+
given(initialModel),
|
|
36
|
+
click(role('button', { name: '+' })),
|
|
37
|
+
expect(text('4')).toExist(),
|
|
38
|
+
Command.expectHas(PersistCount({ count: 4 })),
|
|
39
|
+
Command.resolve(PersistCount({ count: 4 }), CompletedPersistCount()),
|
|
40
|
+
Command.expectNone(),
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
// `@foldkit/vite-plugin` compiles the deployment's build id in here, from its
|
|
4
|
+
// `buildId` option or the `FOLDKIT_BUILD_ID` environment variable. The server
|
|
5
|
+
// entry hands it to `renderToString` and the client entry to `Runtime.hydrate`,
|
|
6
|
+
// which is how hydration tells a page from this deployment apart from one served
|
|
7
|
+
// by another.
|
|
8
|
+
//
|
|
9
|
+
// Declared as required rather than optional because this project's build always
|
|
10
|
+
// supplies one. A build that did not would produce `undefined` here, and both
|
|
11
|
+
// entries refuse it at runtime; typing it as optional would only push that
|
|
12
|
+
// failure past the compiler and into the served page.
|
|
13
|
+
interface ImportMetaEnv {
|
|
14
|
+
readonly FOLDKIT_BUILD_ID: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ImportMeta {
|
|
18
|
+
readonly env: ImportMetaEnv
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022", "DOM"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"exactOptionalPropertyTypes": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"noEmit": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "server/**/*"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
|
|
3
|
+
import { foldkit } from '@foldkit/vite-plugin'
|
|
4
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
tailwindcss(),
|
|
9
|
+
foldkit({
|
|
10
|
+
devToolsMcpPort: 9988,
|
|
11
|
+
ssr: { serverEntry: '/src/entry.server.ts' },
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
optimizeDeps: {
|
|
15
|
+
entries: ['src/entry.ts'],
|
|
16
|
+
},
|
|
17
|
+
})
|