create-foldkit-app 0.38.0 → 0.39.0
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/dist/templates/base/FOLDKIT.md +3 -1
- package/dist/templates/examples/canvas-art/src/main.ts +1 -1
- package/dist/templates/examples/charting/src/chartHost.ts +3 -4
- package/dist/templates/examples/charting/src/entry.ts +0 -2
- package/dist/templates/examples/charting/src/view/chart.ts +101 -36
- package/dist/templates/examples/embedding/src/main.ts +3 -3
- package/dist/templates/examples/generative-art/src/subscription.ts +1 -1
- package/dist/templates/examples/job-application/src/step/education/education.ts +14 -16
- package/dist/templates/examples/job-application/src/step/skills/skills.ts +14 -16
- package/dist/templates/examples/job-application/src/step/workHistory/workHistory.ts +14 -16
- package/dist/templates/examples/map/src/entry.ts +1 -2
- package/dist/templates/examples/map/src/main.ts +123 -145
- package/dist/templates/examples/map/src/mapHost.ts +10 -12
- package/dist/templates/examples/map/src/mount.test.ts +93 -68
- package/dist/templates/examples/pixel-art/src/subscription.ts +5 -5
- package/dist/templates/examples/routing/src/main.ts +3 -3
- package/dist/templates/examples/slow-warnings/src/main.ts +4 -4
- package/dist/templates/examples/snake/src/main.ts +4 -3
- package/dist/templates/examples/ssg/src/entry.server.ts +2 -0
- package/dist/templates/examples/ssg/src/entry.ts +1 -0
- package/dist/templates/examples/ssr/package.json +1 -1
- package/dist/templates/examples/ssr/src/entry.server.ts +2 -0
- package/dist/templates/examples/ssr/src/entry.ts +1 -0
- package/dist/templates/examples/stopwatch/src/main.ts +1 -0
- package/dist/templates/examples/ui-showcase/src/main.ts +49 -37
- package/dist/templates/examples/ui-showcase/src/ui/subscriptions.ts +1 -18
- package/dist/templates/examples/ui-showcase/src/ui/update.ts +2 -12
- package/dist/templates/examples/ui-showcase/src/ui/view/calendar.ts +9 -7
- package/dist/templates/examples/ui-showcase/src/ui/view/checkbox.ts +80 -65
- package/dist/templates/examples/ui-showcase/src/ui/view/dialog.ts +102 -123
- package/dist/templates/examples/ui-showcase/src/ui/view/disclosure.ts +128 -120
- package/dist/templates/examples/ui-showcase/src/ui/view/fieldset.ts +173 -144
- package/dist/templates/examples/ui-showcase/src/ui/view/fileDrop.ts +17 -15
- package/dist/templates/examples/ui-showcase/src/ui/view/hoverIntent.ts +64 -56
- package/dist/templates/examples/ui-showcase/src/ui/view/input.ts +32 -33
- package/dist/templates/examples/ui-showcase/src/ui/view/meter.ts +82 -73
- package/dist/templates/examples/ui-showcase/src/ui/view/popover.ts +100 -81
- package/dist/templates/examples/ui-showcase/src/ui/view/progress.ts +83 -68
- package/dist/templates/examples/ui-showcase/src/ui/view/radioGroup.ts +79 -85
- package/dist/templates/examples/ui-showcase/src/ui/view/select.ts +63 -67
- package/dist/templates/examples/ui-showcase/src/ui/view/slider.ts +72 -72
- package/dist/templates/examples/ui-showcase/src/ui/view/switch.ts +25 -26
- package/dist/templates/examples/ui-showcase/src/ui/view/tabs.ts +58 -46
- package/dist/templates/examples/ui-showcase/src/ui/view/textarea.ts +32 -39
- package/dist/templates/examples/ui-showcase/src/ui/view/tooltip.ts +48 -36
- package/dist/templates/examples/websocket-chat/src/main.ts +44 -18
- package/dist/templates/examples/websocket-chat/src/story.test.ts +33 -6
- package/dist/templates/release.json +12 -11
- package/dist/templates/rendering/ssg/src/entry.server.ts +2 -0
- package/dist/templates/rendering/ssg/src/entry.ts +1 -0
- package/dist/templates/rendering/ssg/vite.config.ts +1 -0
- package/dist/templates/rendering/ssr/scripts/serve.ts +4 -145
- package/dist/templates/rendering/ssr/src/entry.server.ts +2 -0
- package/dist/templates/rendering/ssr/src/entry.ts +1 -0
- package/dist/templates/rendering/ssr/vite.config.ts +1 -0
- package/dist/utils/files.js +2 -0
- package/package.json +1 -1
- package/dist/templates/examples/charting/src/subscription.ts +0 -70
|
@@ -15,6 +15,36 @@ const descriptionClassName = 'text-sm text-gray-500'
|
|
|
15
15
|
|
|
16
16
|
export const view = Submodel.defineView<UiModel, UiMessage>(
|
|
17
17
|
(model, h): Html => {
|
|
18
|
+
const basicTextareaView = (
|
|
19
|
+
attributes: Textarea.TextareaAttributes<UiMessage>,
|
|
20
|
+
): Html =>
|
|
21
|
+
h.div(
|
|
22
|
+
[h.Class('flex flex-col gap-1.5 w-full')],
|
|
23
|
+
[
|
|
24
|
+
h.label([...attributes.label, h.Class(labelClassName)], ['Bio']),
|
|
25
|
+
h.textarea([...attributes.textarea, h.Class(textareaClassName)]),
|
|
26
|
+
h.span(
|
|
27
|
+
[...attributes.description, h.Class(descriptionClassName)],
|
|
28
|
+
['A brief introduction about yourself.'],
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const disabledTextareaView = (
|
|
34
|
+
attributes: Textarea.TextareaAttributes<UiMessage>,
|
|
35
|
+
): Html =>
|
|
36
|
+
h.div(
|
|
37
|
+
[h.Class('flex flex-col gap-1.5 max-w-sm')],
|
|
38
|
+
[
|
|
39
|
+
h.label([...attributes.label, h.Class(labelClassName)], ['Bio']),
|
|
40
|
+
h.textarea([...attributes.textarea, h.Class(textareaClassName)]),
|
|
41
|
+
h.span(
|
|
42
|
+
[...attributes.description, h.Class(descriptionClassName)],
|
|
43
|
+
['This textarea is disabled.'],
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
|
|
18
48
|
return h.div(
|
|
19
49
|
[],
|
|
20
50
|
[
|
|
@@ -35,27 +65,7 @@ export const view = Submodel.defineView<UiModel, UiMessage>(
|
|
|
35
65
|
onInput: value => UiMessage.UpdatedTextareaDemoValue({ value }),
|
|
36
66
|
placeholder: 'Tell us about yourself...',
|
|
37
67
|
rows: 4,
|
|
38
|
-
toView:
|
|
39
|
-
h.div(
|
|
40
|
-
[h.Class('flex flex-col gap-1.5 w-full')],
|
|
41
|
-
[
|
|
42
|
-
h.label(
|
|
43
|
-
[...attributes.label, h.Class(labelClassName)],
|
|
44
|
-
['Bio'],
|
|
45
|
-
),
|
|
46
|
-
h.textarea([
|
|
47
|
-
...attributes.textarea,
|
|
48
|
-
h.Class(textareaClassName),
|
|
49
|
-
]),
|
|
50
|
-
h.span(
|
|
51
|
-
[
|
|
52
|
-
...attributes.description,
|
|
53
|
-
h.Class(descriptionClassName),
|
|
54
|
-
],
|
|
55
|
-
['A brief introduction about yourself.'],
|
|
56
|
-
),
|
|
57
|
-
],
|
|
58
|
-
),
|
|
68
|
+
toView: basicTextareaView,
|
|
59
69
|
},
|
|
60
70
|
h,
|
|
61
71
|
),
|
|
@@ -74,24 +84,7 @@ export const view = Submodel.defineView<UiModel, UiMessage>(
|
|
|
74
84
|
value:
|
|
75
85
|
"Mathematician and writer, known for work on Charles Babbage's Analytical Engine.",
|
|
76
86
|
rows: 3,
|
|
77
|
-
toView:
|
|
78
|
-
h.div(
|
|
79
|
-
[h.Class('flex flex-col gap-1.5 max-w-sm')],
|
|
80
|
-
[
|
|
81
|
-
h.label(
|
|
82
|
-
[...attributes.label, h.Class(labelClassName)],
|
|
83
|
-
['Bio'],
|
|
84
|
-
),
|
|
85
|
-
h.textarea([
|
|
86
|
-
...attributes.textarea,
|
|
87
|
-
h.Class(textareaClassName),
|
|
88
|
-
]),
|
|
89
|
-
h.span(
|
|
90
|
-
[...attributes.description, h.Class(descriptionClassName)],
|
|
91
|
-
['This textarea is disabled.'],
|
|
92
|
-
),
|
|
93
|
-
],
|
|
94
|
-
),
|
|
87
|
+
toView: disabledTextareaView,
|
|
95
88
|
},
|
|
96
89
|
h,
|
|
97
90
|
),
|
|
@@ -23,6 +23,52 @@ const TOOLTIP_ANCHOR: AnchorConfig = {
|
|
|
23
23
|
|
|
24
24
|
export const view = Submodel.defineView<UiModel, UiMessage>(
|
|
25
25
|
(model, h): Html => {
|
|
26
|
+
const basicTooltipView = ({
|
|
27
|
+
trigger,
|
|
28
|
+
panel,
|
|
29
|
+
isVisible,
|
|
30
|
+
}: Tooltip.RenderInfo): Html =>
|
|
31
|
+
h.div(
|
|
32
|
+
[h.Class(wrapperClassName)],
|
|
33
|
+
[
|
|
34
|
+
h.button(
|
|
35
|
+
[...trigger, h.Class(triggerClassName)],
|
|
36
|
+
[h.span([], ['Hover or focus me'])],
|
|
37
|
+
),
|
|
38
|
+
...(isVisible
|
|
39
|
+
? [
|
|
40
|
+
h.div(
|
|
41
|
+
[...panel, h.Class(panelClassName)],
|
|
42
|
+
[h.span([], ['This is a tooltip'])],
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
: []),
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const noDelayTooltipView = ({
|
|
50
|
+
trigger,
|
|
51
|
+
panel,
|
|
52
|
+
isVisible,
|
|
53
|
+
}: Tooltip.RenderInfo): Html =>
|
|
54
|
+
h.div(
|
|
55
|
+
[h.Class(wrapperClassName)],
|
|
56
|
+
[
|
|
57
|
+
h.button(
|
|
58
|
+
[...trigger, h.Class(triggerClassName)],
|
|
59
|
+
[h.span([], ['No delay'])],
|
|
60
|
+
),
|
|
61
|
+
...(isVisible
|
|
62
|
+
? [
|
|
63
|
+
h.div(
|
|
64
|
+
[...panel, h.Class(panelClassName)],
|
|
65
|
+
[h.span([], ['Shows immediately'])],
|
|
66
|
+
),
|
|
67
|
+
]
|
|
68
|
+
: []),
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
|
|
26
72
|
return h.div(
|
|
27
73
|
[],
|
|
28
74
|
[
|
|
@@ -48,24 +94,7 @@ export const view = Submodel.defineView<UiModel, UiMessage>(
|
|
|
48
94
|
view: Tooltip.view,
|
|
49
95
|
viewInputs: {
|
|
50
96
|
anchor: TOOLTIP_ANCHOR,
|
|
51
|
-
toView:
|
|
52
|
-
h.div(
|
|
53
|
-
[h.Class(wrapperClassName)],
|
|
54
|
-
[
|
|
55
|
-
h.button(
|
|
56
|
-
[...trigger, h.Class(triggerClassName)],
|
|
57
|
-
[h.span([], ['Hover or focus me'])],
|
|
58
|
-
),
|
|
59
|
-
...(isVisible
|
|
60
|
-
? [
|
|
61
|
-
h.div(
|
|
62
|
-
[...panel, h.Class(panelClassName)],
|
|
63
|
-
[h.span([], ['This is a tooltip'])],
|
|
64
|
-
),
|
|
65
|
-
]
|
|
66
|
-
: []),
|
|
67
|
-
],
|
|
68
|
-
),
|
|
97
|
+
toView: basicTooltipView,
|
|
69
98
|
},
|
|
70
99
|
toParentMessage: message =>
|
|
71
100
|
UiMessage.GotTooltipBasicDemoMessage({ message }),
|
|
@@ -93,24 +122,7 @@ export const view = Submodel.defineView<UiModel, UiMessage>(
|
|
|
93
122
|
view: Tooltip.view,
|
|
94
123
|
viewInputs: {
|
|
95
124
|
anchor: TOOLTIP_ANCHOR,
|
|
96
|
-
toView:
|
|
97
|
-
h.div(
|
|
98
|
-
[h.Class(wrapperClassName)],
|
|
99
|
-
[
|
|
100
|
-
h.button(
|
|
101
|
-
[...trigger, h.Class(triggerClassName)],
|
|
102
|
-
[h.span([], ['No delay'])],
|
|
103
|
-
),
|
|
104
|
-
...(isVisible
|
|
105
|
-
? [
|
|
106
|
-
h.div(
|
|
107
|
-
[...panel, h.Class(panelClassName)],
|
|
108
|
-
[h.span([], ['Shows immediately'])],
|
|
109
|
-
),
|
|
110
|
-
]
|
|
111
|
-
: []),
|
|
112
|
-
],
|
|
113
|
-
),
|
|
125
|
+
toView: noDelayTooltipView,
|
|
114
126
|
},
|
|
115
127
|
toParentMessage: message =>
|
|
116
128
|
UiMessage.GotTooltipNoDelayDemoMessage({ message }),
|
|
@@ -64,9 +64,12 @@ export type Model = typeof Model.Type
|
|
|
64
64
|
|
|
65
65
|
export const Message = defineMessageUnion({
|
|
66
66
|
ClickedConnect: {},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
ConnectedChatSocket: {},
|
|
68
|
+
DisconnectedChatSocket: {},
|
|
69
|
+
ReleasedChatSocket: {},
|
|
70
|
+
FailedConnectChatSocket: { error: Schema.String },
|
|
71
|
+
FailedChatSocket: { error: Schema.String },
|
|
72
|
+
FailedSendMessage: { error: Schema.String },
|
|
70
73
|
UpdatedMessageInput: { value: Schema.String },
|
|
71
74
|
SubmittedMessage: {},
|
|
72
75
|
SucceededSendMessage: { text: Schema.String },
|
|
@@ -92,20 +95,34 @@ export const update = (model: Model, message: Message) =>
|
|
|
92
95
|
}),
|
|
93
96
|
}),
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
ConnectedChatSocket: () => ({
|
|
96
99
|
model: modifyFields(model, {
|
|
97
100
|
connection: () => ConnectionState.Connected(),
|
|
98
101
|
}),
|
|
99
102
|
}),
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
DisconnectedChatSocket: () => ({
|
|
102
105
|
model: modifyFields(model, {
|
|
103
106
|
connection: () => ConnectionState.Disconnected(),
|
|
104
107
|
messages: () => [],
|
|
105
108
|
}),
|
|
106
109
|
}),
|
|
107
110
|
|
|
108
|
-
|
|
111
|
+
ReleasedChatSocket: () => ({ model }),
|
|
112
|
+
|
|
113
|
+
FailedConnectChatSocket: ({ error }) => ({
|
|
114
|
+
model: modifyFields(model, {
|
|
115
|
+
connection: () => ConnectionState.Error({ error }),
|
|
116
|
+
}),
|
|
117
|
+
}),
|
|
118
|
+
|
|
119
|
+
FailedChatSocket: ({ error }) => ({
|
|
120
|
+
model: modifyFields(model, {
|
|
121
|
+
connection: () => ConnectionState.Error({ error }),
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
FailedSendMessage: ({ error }) => ({
|
|
109
126
|
model: modifyFields(model, {
|
|
110
127
|
connection: () => ConnectionState.Error({ error }),
|
|
111
128
|
}),
|
|
@@ -196,17 +213,26 @@ export const TimestampReceivedMessage = Command.define(
|
|
|
196
213
|
|
|
197
214
|
export const SendMessage = Command.define('SendMessage', {
|
|
198
215
|
args: { text: Schema.String },
|
|
199
|
-
messages: [Message.SucceededSendMessage, Message.
|
|
216
|
+
messages: [Message.SucceededSendMessage, Message.FailedSendMessage],
|
|
200
217
|
execute: ({ text }) =>
|
|
201
218
|
ChatSocket.get.pipe(
|
|
202
219
|
Effect.flatMap(socket =>
|
|
203
|
-
Effect.
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
Effect.try({
|
|
221
|
+
try: () => {
|
|
222
|
+
socket.send(text)
|
|
223
|
+
return Message.SucceededSendMessage({ text })
|
|
224
|
+
},
|
|
225
|
+
catch: error =>
|
|
226
|
+
error instanceof Error ? error.message : 'Failed to send message',
|
|
206
227
|
}),
|
|
207
228
|
),
|
|
208
229
|
Effect.catchTag('ResourceNotAvailable', () =>
|
|
209
|
-
Effect.succeed(
|
|
230
|
+
Effect.succeed(
|
|
231
|
+
Message.FailedSendMessage({ error: 'Socket unavailable' }),
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
Effect.catch(error =>
|
|
235
|
+
Effect.succeed(Message.FailedSendMessage({ error })),
|
|
210
236
|
),
|
|
211
237
|
),
|
|
212
238
|
})
|
|
@@ -254,10 +280,10 @@ export const managedResources = ManagedResource.make<Model, Message>()(
|
|
|
254
280
|
Effect.sync(() => {
|
|
255
281
|
socket.close()
|
|
256
282
|
}),
|
|
257
|
-
onAcquired: () => Message.
|
|
258
|
-
onReleased: () => Message.
|
|
283
|
+
onAcquired: () => Message.ConnectedChatSocket(),
|
|
284
|
+
onReleased: () => Message.ReleasedChatSocket(),
|
|
259
285
|
onAcquireError: error =>
|
|
260
|
-
Message.
|
|
286
|
+
Message.FailedConnectChatSocket({
|
|
261
287
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
262
288
|
}),
|
|
263
289
|
}),
|
|
@@ -269,8 +295,8 @@ export const managedResources = ManagedResource.make<Model, Message>()(
|
|
|
269
295
|
const streamChatSocketMessages = (socket: WebSocket) =>
|
|
270
296
|
Stream.callback<
|
|
271
297
|
| typeof Message.ReceivedMessage.Type
|
|
272
|
-
| typeof Message.
|
|
273
|
-
| typeof Message.
|
|
298
|
+
| typeof Message.DisconnectedChatSocket.Type
|
|
299
|
+
| typeof Message.FailedChatSocket.Type
|
|
274
300
|
>(queue =>
|
|
275
301
|
Effect.acquireRelease(
|
|
276
302
|
Effect.sync(() => {
|
|
@@ -281,13 +307,13 @@ const streamChatSocketMessages = (socket: WebSocket) =>
|
|
|
281
307
|
)
|
|
282
308
|
}
|
|
283
309
|
const handleClose = () => {
|
|
284
|
-
Queue.offerUnsafe(queue, Message.
|
|
310
|
+
Queue.offerUnsafe(queue, Message.DisconnectedChatSocket())
|
|
285
311
|
Queue.endUnsafe(queue)
|
|
286
312
|
}
|
|
287
313
|
const handleError = () => {
|
|
288
314
|
Queue.offerUnsafe(
|
|
289
315
|
queue,
|
|
290
|
-
Message.
|
|
316
|
+
Message.FailedChatSocket({ error: 'Connection error' }),
|
|
291
317
|
)
|
|
292
318
|
Queue.endUnsafe(queue)
|
|
293
319
|
}
|
|
@@ -38,7 +38,7 @@ describe('update', () => {
|
|
|
38
38
|
)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
test('
|
|
41
|
+
test('ConnectedChatSocket moves into Connected', () => {
|
|
42
42
|
story(
|
|
43
43
|
update,
|
|
44
44
|
given(
|
|
@@ -46,14 +46,14 @@ describe('update', () => {
|
|
|
46
46
|
connection: () => ConnectionState.Connecting(),
|
|
47
47
|
}),
|
|
48
48
|
),
|
|
49
|
-
message(Message.
|
|
49
|
+
message(Message.ConnectedChatSocket()),
|
|
50
50
|
model(model => {
|
|
51
51
|
expect(model.connection._tag).toBe('Connected')
|
|
52
52
|
}),
|
|
53
53
|
)
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
test('
|
|
56
|
+
test('DisconnectedChatSocket returns to Disconnected and clears messages', () => {
|
|
57
57
|
story(
|
|
58
58
|
update,
|
|
59
59
|
given(
|
|
@@ -61,7 +61,7 @@ describe('update', () => {
|
|
|
61
61
|
messages: () => [{ text: 'old', zoned: zonedNow, isSent: true }],
|
|
62
62
|
}),
|
|
63
63
|
),
|
|
64
|
-
message(Message.
|
|
64
|
+
message(Message.DisconnectedChatSocket()),
|
|
65
65
|
model(model => {
|
|
66
66
|
expect(model.connection._tag).toBe('Disconnected')
|
|
67
67
|
expect(model.messages).toHaveLength(0)
|
|
@@ -69,7 +69,7 @@ describe('update', () => {
|
|
|
69
69
|
)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
test('
|
|
72
|
+
test('FailedConnectChatSocket captures the error message', () => {
|
|
73
73
|
story(
|
|
74
74
|
update,
|
|
75
75
|
given(
|
|
@@ -77,7 +77,7 @@ describe('update', () => {
|
|
|
77
77
|
connection: () => ConnectionState.Connecting(),
|
|
78
78
|
}),
|
|
79
79
|
),
|
|
80
|
-
message(Message.
|
|
80
|
+
message(Message.FailedConnectChatSocket({ error: 'Timeout' })),
|
|
81
81
|
model(model => {
|
|
82
82
|
if (model.connection._tag === 'Error') {
|
|
83
83
|
expect(model.connection.error).toBe('Timeout')
|
|
@@ -87,6 +87,33 @@ describe('update', () => {
|
|
|
87
87
|
}),
|
|
88
88
|
)
|
|
89
89
|
})
|
|
90
|
+
|
|
91
|
+
test('releasing the socket preserves a connection error', () => {
|
|
92
|
+
story(
|
|
93
|
+
update,
|
|
94
|
+
given(connectedModel),
|
|
95
|
+
message(Message.FailedChatSocket({ error: 'Connection error' })),
|
|
96
|
+
message(Message.ReleasedChatSocket()),
|
|
97
|
+
model(model => {
|
|
98
|
+
expect(model.connection).toEqual(
|
|
99
|
+
ConnectionState.Error({ error: 'Connection error' }),
|
|
100
|
+
)
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('FailedSendMessage reports an unavailable socket', () => {
|
|
106
|
+
story(
|
|
107
|
+
update,
|
|
108
|
+
given(connectedModel),
|
|
109
|
+
message(Message.FailedSendMessage({ error: 'Socket unavailable' })),
|
|
110
|
+
model(model => {
|
|
111
|
+
expect(model.connection).toEqual(
|
|
112
|
+
ConnectionState.Error({ error: 'Socket unavailable' }),
|
|
113
|
+
)
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
})
|
|
90
117
|
})
|
|
91
118
|
|
|
92
119
|
describe('message input', () => {
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"channel": "stable",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "74071173b1253e9efeec050a31ca86df1931ce5a",
|
|
5
5
|
"packages": {
|
|
6
|
-
"create-foldkit-app": "0.
|
|
7
|
-
"@foldkit/devtools": "0.
|
|
6
|
+
"create-foldkit-app": "0.39.0",
|
|
7
|
+
"@foldkit/devtools": "0.167.0",
|
|
8
8
|
"@foldkit/devtools-mcp": "0.24.1",
|
|
9
|
-
"foldkit": "0.
|
|
10
|
-
"@foldkit/markdown": "0.14.
|
|
11
|
-
"@foldkit/
|
|
12
|
-
"@foldkit/
|
|
13
|
-
"@foldkit/
|
|
9
|
+
"foldkit": "0.167.0",
|
|
10
|
+
"@foldkit/markdown": "0.14.2",
|
|
11
|
+
"@foldkit/node": "0.1.0",
|
|
12
|
+
"@foldkit/oxlint-plugin": "0.15.3",
|
|
13
|
+
"@foldkit/ui": "0.167.0",
|
|
14
|
+
"@foldkit/vite-plugin": "0.27.0"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"@foldkit/devtools": "0.
|
|
17
|
-
"@foldkit/vite-plugin": "0.
|
|
17
|
+
"@foldkit/devtools": "0.167.0",
|
|
18
|
+
"@foldkit/vite-plugin": "0.27.0",
|
|
18
19
|
"@foldkit/devtools-mcp": "0.24.1",
|
|
19
|
-
"@foldkit/oxlint-plugin": "0.15.
|
|
20
|
+
"@foldkit/oxlint-plugin": "0.15.3",
|
|
20
21
|
"@types/node": "26.4.0",
|
|
21
22
|
"happy-dom": "^20.14.5",
|
|
22
23
|
"oxfmt": "^0.66.0",
|
|
@@ -3,6 +3,8 @@ import { Server } from 'foldkit/experimental'
|
|
|
3
3
|
|
|
4
4
|
import { init, view } from './main'
|
|
5
5
|
|
|
6
|
+
export const renderDocument = Server.renderDocument
|
|
7
|
+
|
|
6
8
|
export const prerenderPaths: ReadonlyArray<string> = ['/', '/about']
|
|
7
9
|
|
|
8
10
|
export const renderPage = (request: Request): Promise<Server.EntryResult> =>
|
|
@@ -1,155 +1,14 @@
|
|
|
1
|
-
import { Config,
|
|
2
|
-
import {
|
|
3
|
-
HttpServer,
|
|
4
|
-
HttpServerError,
|
|
5
|
-
HttpServerRequest,
|
|
6
|
-
HttpServerResponse,
|
|
7
|
-
HttpStaticServer,
|
|
8
|
-
} from 'effect/http'
|
|
9
|
-
import { Server } from 'foldkit/experimental'
|
|
10
|
-
import { createServer } from 'node:http'
|
|
11
|
-
import { dirname, resolve } from 'node:path'
|
|
12
|
-
import { fileURLToPath } from 'node:url'
|
|
1
|
+
import { Config, Option, String } from 'effect'
|
|
13
2
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
NodeHttpServer,
|
|
17
|
-
NodeRuntime,
|
|
18
|
-
NodeServices,
|
|
19
|
-
} from '@effect/platform-node'
|
|
3
|
+
import { NodeRuntime } from '@effect/platform-node'
|
|
4
|
+
import * as Node from '@foldkit/node'
|
|
20
5
|
|
|
21
|
-
type FetchHandler = {
|
|
22
|
-
readonly fetch: (request: Request) => Promise<Response>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const EXAMPLE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
26
|
-
const CLIENT_DIR = resolve(EXAMPLE_DIR, 'dist/client')
|
|
27
|
-
const FETCH_MODULE_URL = new URL('../dist/server/fetch.js', import.meta.url)
|
|
28
|
-
.href
|
|
29
6
|
const DEFAULT_PORT = 3000
|
|
30
7
|
|
|
31
8
|
const PORT = Config.withDefault(Config.Port('PORT'), DEFAULT_PORT)
|
|
32
9
|
|
|
33
|
-
// NOTE: the origin this deployment serves. A client may send an absolute-form
|
|
34
|
-
// target or a network-path reference such as `//elsewhere.example/page`.
|
|
35
|
-
// Deriving the origin from the request would let the client choose redirects
|
|
36
|
-
// and cookie domains, so a target that resolves anywhere else is refused
|
|
37
|
-
// before static files or fetch. Set ORIGIN when deploying behind a proxy or
|
|
38
|
-
// TLS terminator.
|
|
39
10
|
const ORIGIN = Config.option(Config.String('ORIGIN')).pipe(
|
|
40
11
|
Config.map(Option.filter(String.isNonEmpty)),
|
|
41
12
|
)
|
|
42
13
|
|
|
43
|
-
|
|
44
|
-
Predicate.hasProperty(value, 'fetch') && Predicate.isFunction(value.fetch)
|
|
45
|
-
|
|
46
|
-
const isFetchModule = (
|
|
47
|
-
value: unknown,
|
|
48
|
-
): value is { readonly default: FetchHandler } =>
|
|
49
|
-
Predicate.hasProperty(value, 'default') && isFetchHandler(value.default)
|
|
50
|
-
|
|
51
|
-
const isRouteNotFound = (error: HttpServerError.HttpServerError): boolean =>
|
|
52
|
-
error.reason._tag === 'RouteNotFound'
|
|
53
|
-
|
|
54
|
-
const hostSettledResponse = () =>
|
|
55
|
-
HttpServerResponse.setHeader(
|
|
56
|
-
HttpServerResponse.empty({
|
|
57
|
-
status: Server.HOST_METHOD_ANSWERS.refusedStatus,
|
|
58
|
-
}),
|
|
59
|
-
'allow',
|
|
60
|
-
Server.HOST_METHOD_ANSWERS.allow,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
const fetchResponse = (
|
|
64
|
-
app: FetchHandler,
|
|
65
|
-
request: HttpServerRequest.HttpServerRequest,
|
|
66
|
-
requestUrl: string,
|
|
67
|
-
) =>
|
|
68
|
-
Effect.gen(function* () {
|
|
69
|
-
const webRequest = yield* HttpServerRequest.toWeb(request)
|
|
70
|
-
const response = yield* Effect.promise(() =>
|
|
71
|
-
app.fetch(new Request(requestUrl, webRequest)),
|
|
72
|
-
)
|
|
73
|
-
return HttpServerResponse.fromWeb(response)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const loadFetchHandler = Effect.gen(function* () {
|
|
77
|
-
const loaded: unknown = yield* Effect.tryPromise({
|
|
78
|
-
try: () => import(FETCH_MODULE_URL),
|
|
79
|
-
catch: cause =>
|
|
80
|
-
new Error(
|
|
81
|
-
'dist/server/fetch.js could not be loaded. Run `vite build` first.',
|
|
82
|
-
{ cause },
|
|
83
|
-
),
|
|
84
|
-
})
|
|
85
|
-
if (!isFetchModule(loaded)) {
|
|
86
|
-
return yield* Effect.die(
|
|
87
|
-
new Error('dist/server/fetch.js must default-export a Web fetch handler'),
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
return loaded.default
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
const makeHandler = Effect.gen(function* () {
|
|
94
|
-
const port = yield* PORT
|
|
95
|
-
const origin = Option.getOrElse(
|
|
96
|
-
yield* ORIGIN,
|
|
97
|
-
() => `http://localhost:${globalThis.String(port)}`,
|
|
98
|
-
)
|
|
99
|
-
const app = yield* loadFetchHandler
|
|
100
|
-
// NOTE: directory requests reach `fetch` even when a generated index file
|
|
101
|
-
// exists. This host serves assets as files and renders pages at request time.
|
|
102
|
-
const staticFiles = yield* HttpStaticServer.make({
|
|
103
|
-
root: CLIENT_DIR,
|
|
104
|
-
index: undefined,
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
return HttpServerRequest.HttpServerRequest.use(request => {
|
|
108
|
-
// NOTE: refuse an off-origin target before looking for a file. A
|
|
109
|
-
// network-path request such as `//evil.example/../assets/app.js` names
|
|
110
|
-
// another host, then a path that exists on disk. The fetch handler trusts
|
|
111
|
-
// the `Request.url` it is given, so this is the one place that resolves it.
|
|
112
|
-
const requestUrl = Server.resolveRequestUrl(request.url, origin)
|
|
113
|
-
if (requestUrl === undefined) {
|
|
114
|
-
return Effect.succeed(HttpServerResponse.empty({ status: 400 }))
|
|
115
|
-
}
|
|
116
|
-
const resolved = new URL(requestUrl)
|
|
117
|
-
const normalizedRequest = request.modify({
|
|
118
|
-
url: `${resolved.pathname}${resolved.search}`,
|
|
119
|
-
})
|
|
120
|
-
const response = Match.value(normalizedRequest.method).pipe(
|
|
121
|
-
Match.whenOr('GET', 'HEAD', () => {
|
|
122
|
-
if (Server.resolvesToIndexHtml(requestUrl)) {
|
|
123
|
-
return fetchResponse(app, normalizedRequest, requestUrl)
|
|
124
|
-
}
|
|
125
|
-
return staticFiles.pipe(
|
|
126
|
-
Effect.catchIf(isRouteNotFound, () =>
|
|
127
|
-
fetchResponse(app, normalizedRequest, requestUrl),
|
|
128
|
-
),
|
|
129
|
-
)
|
|
130
|
-
}),
|
|
131
|
-
Match.orElse(() => {
|
|
132
|
-
if (Server.isHostSettledMethod(normalizedRequest.method)) {
|
|
133
|
-
return Effect.succeed(hostSettledResponse())
|
|
134
|
-
}
|
|
135
|
-
return fetchResponse(app, normalizedRequest, requestUrl)
|
|
136
|
-
}),
|
|
137
|
-
)
|
|
138
|
-
return Effect.provideService(
|
|
139
|
-
response,
|
|
140
|
-
HttpServerRequest.HttpServerRequest,
|
|
141
|
-
normalizedRequest,
|
|
142
|
-
)
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
const Main = Layer.unwrap(
|
|
147
|
-
Effect.map(makeHandler, handler => HttpServer.serve(handler)),
|
|
148
|
-
).pipe(
|
|
149
|
-
HttpServer.withLogAddress,
|
|
150
|
-
Layer.provide(NodeHttpServer.layerConfig(createServer, { port: PORT })),
|
|
151
|
-
Layer.provide(NodeHttpPlatform.layer),
|
|
152
|
-
Layer.provide(NodeServices.layer),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
NodeRuntime.runMain(Layer.launch(Main))
|
|
14
|
+
NodeRuntime.runMain(Node.serve({ port: PORT, origin: ORIGIN }))
|
|
@@ -4,6 +4,8 @@ import { Server } from 'foldkit/experimental'
|
|
|
4
4
|
import { readCountCookie } from './cookie'
|
|
5
5
|
import { Flags, init, view } from './main'
|
|
6
6
|
|
|
7
|
+
export const renderDocument = Server.renderDocument
|
|
8
|
+
|
|
7
9
|
const flagsForRequest = (cookieHeader: string): Flags => ({
|
|
8
10
|
initialCount: readCountCookie(cookieHeader),
|
|
9
11
|
renderedAt: new Date().toISOString(),
|
package/dist/utils/files.js
CHANGED
|
@@ -70,10 +70,12 @@ const createPackageManagerFiles = (projectPath, packageManager) => Effect.gen(fu
|
|
|
70
70
|
yield* createFiles(projectPath, packageManagerFiles);
|
|
71
71
|
});
|
|
72
72
|
const overlayRenderingFiles = (projectPath, directory) => Effect.gen(function* () {
|
|
73
|
+
const fs = yield* FileSystem.FileSystem;
|
|
73
74
|
const path = yield* Path.Path;
|
|
74
75
|
const templateRoot = yield* getTemplateRoot;
|
|
75
76
|
const renderingFiles = yield* getTemplateFiles(path.join(templateRoot, 'rendering', directory));
|
|
76
77
|
yield* createFiles(projectPath, renderingFiles);
|
|
78
|
+
yield* fs.remove(path.join(projectPath, 'index.html'));
|
|
77
79
|
});
|
|
78
80
|
const createRenderingFiles = (projectPath, scaffold) => Match.value(scaffold).pipe(Match.tagsExhaustive({
|
|
79
81
|
Spa: () => Effect.void,
|