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.
Files changed (58) hide show
  1. package/dist/templates/base/FOLDKIT.md +3 -1
  2. package/dist/templates/examples/canvas-art/src/main.ts +1 -1
  3. package/dist/templates/examples/charting/src/chartHost.ts +3 -4
  4. package/dist/templates/examples/charting/src/entry.ts +0 -2
  5. package/dist/templates/examples/charting/src/view/chart.ts +101 -36
  6. package/dist/templates/examples/embedding/src/main.ts +3 -3
  7. package/dist/templates/examples/generative-art/src/subscription.ts +1 -1
  8. package/dist/templates/examples/job-application/src/step/education/education.ts +14 -16
  9. package/dist/templates/examples/job-application/src/step/skills/skills.ts +14 -16
  10. package/dist/templates/examples/job-application/src/step/workHistory/workHistory.ts +14 -16
  11. package/dist/templates/examples/map/src/entry.ts +1 -2
  12. package/dist/templates/examples/map/src/main.ts +123 -145
  13. package/dist/templates/examples/map/src/mapHost.ts +10 -12
  14. package/dist/templates/examples/map/src/mount.test.ts +93 -68
  15. package/dist/templates/examples/pixel-art/src/subscription.ts +5 -5
  16. package/dist/templates/examples/routing/src/main.ts +3 -3
  17. package/dist/templates/examples/slow-warnings/src/main.ts +4 -4
  18. package/dist/templates/examples/snake/src/main.ts +4 -3
  19. package/dist/templates/examples/ssg/src/entry.server.ts +2 -0
  20. package/dist/templates/examples/ssg/src/entry.ts +1 -0
  21. package/dist/templates/examples/ssr/package.json +1 -1
  22. package/dist/templates/examples/ssr/src/entry.server.ts +2 -0
  23. package/dist/templates/examples/ssr/src/entry.ts +1 -0
  24. package/dist/templates/examples/stopwatch/src/main.ts +1 -0
  25. package/dist/templates/examples/ui-showcase/src/main.ts +49 -37
  26. package/dist/templates/examples/ui-showcase/src/ui/subscriptions.ts +1 -18
  27. package/dist/templates/examples/ui-showcase/src/ui/update.ts +2 -12
  28. package/dist/templates/examples/ui-showcase/src/ui/view/calendar.ts +9 -7
  29. package/dist/templates/examples/ui-showcase/src/ui/view/checkbox.ts +80 -65
  30. package/dist/templates/examples/ui-showcase/src/ui/view/dialog.ts +102 -123
  31. package/dist/templates/examples/ui-showcase/src/ui/view/disclosure.ts +128 -120
  32. package/dist/templates/examples/ui-showcase/src/ui/view/fieldset.ts +173 -144
  33. package/dist/templates/examples/ui-showcase/src/ui/view/fileDrop.ts +17 -15
  34. package/dist/templates/examples/ui-showcase/src/ui/view/hoverIntent.ts +64 -56
  35. package/dist/templates/examples/ui-showcase/src/ui/view/input.ts +32 -33
  36. package/dist/templates/examples/ui-showcase/src/ui/view/meter.ts +82 -73
  37. package/dist/templates/examples/ui-showcase/src/ui/view/popover.ts +100 -81
  38. package/dist/templates/examples/ui-showcase/src/ui/view/progress.ts +83 -68
  39. package/dist/templates/examples/ui-showcase/src/ui/view/radioGroup.ts +79 -85
  40. package/dist/templates/examples/ui-showcase/src/ui/view/select.ts +63 -67
  41. package/dist/templates/examples/ui-showcase/src/ui/view/slider.ts +72 -72
  42. package/dist/templates/examples/ui-showcase/src/ui/view/switch.ts +25 -26
  43. package/dist/templates/examples/ui-showcase/src/ui/view/tabs.ts +58 -46
  44. package/dist/templates/examples/ui-showcase/src/ui/view/textarea.ts +32 -39
  45. package/dist/templates/examples/ui-showcase/src/ui/view/tooltip.ts +48 -36
  46. package/dist/templates/examples/websocket-chat/src/main.ts +44 -18
  47. package/dist/templates/examples/websocket-chat/src/story.test.ts +33 -6
  48. package/dist/templates/release.json +12 -11
  49. package/dist/templates/rendering/ssg/src/entry.server.ts +2 -0
  50. package/dist/templates/rendering/ssg/src/entry.ts +1 -0
  51. package/dist/templates/rendering/ssg/vite.config.ts +1 -0
  52. package/dist/templates/rendering/ssr/scripts/serve.ts +4 -145
  53. package/dist/templates/rendering/ssr/src/entry.server.ts +2 -0
  54. package/dist/templates/rendering/ssr/src/entry.ts +1 -0
  55. package/dist/templates/rendering/ssr/vite.config.ts +1 -0
  56. package/dist/utils/files.js +2 -0
  57. package/package.json +1 -1
  58. 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: attributes =>
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: attributes =>
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: ({ trigger, panel, isVisible }) =>
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: ({ trigger, panel, isVisible }) =>
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
- Connected: {},
68
- Disconnected: {},
69
- FailedConnect: { error: Schema.String },
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
- Connected: () => ({
98
+ ConnectedChatSocket: () => ({
96
99
  model: modifyFields(model, {
97
100
  connection: () => ConnectionState.Connected(),
98
101
  }),
99
102
  }),
100
103
 
101
- Disconnected: () => ({
104
+ DisconnectedChatSocket: () => ({
102
105
  model: modifyFields(model, {
103
106
  connection: () => ConnectionState.Disconnected(),
104
107
  messages: () => [],
105
108
  }),
106
109
  }),
107
110
 
108
- FailedConnect: ({ error }) => ({
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.FailedConnect],
216
+ messages: [Message.SucceededSendMessage, Message.FailedSendMessage],
200
217
  execute: ({ text }) =>
201
218
  ChatSocket.get.pipe(
202
219
  Effect.flatMap(socket =>
203
- Effect.sync(() => {
204
- socket.send(text)
205
- return Message.SucceededSendMessage({ text })
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(Message.FailedConnect({ error: 'Socket unavailable' })),
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.Connected(),
258
- onReleased: () => Message.Disconnected(),
283
+ onAcquired: () => Message.ConnectedChatSocket(),
284
+ onReleased: () => Message.ReleasedChatSocket(),
259
285
  onAcquireError: error =>
260
- Message.FailedConnect({
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.Disconnected.Type
273
- | typeof Message.FailedConnect.Type
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.Disconnected())
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.FailedConnect({ error: 'Connection error' }),
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('Connected moves into Connected', () => {
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.Connected()),
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('Disconnected returns to Disconnected and clears messages', () => {
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.Disconnected()),
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('FailedConnect captures the error message', () => {
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.FailedConnect({ error: 'Timeout' })),
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": "7590156835c822a0aa135a5fde14c8e2009c9124",
4
+ "sourceCommit": "74071173b1253e9efeec050a31ca86df1931ce5a",
5
5
  "packages": {
6
- "create-foldkit-app": "0.38.0",
7
- "@foldkit/devtools": "0.166.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.166.0",
10
- "@foldkit/markdown": "0.14.1",
11
- "@foldkit/oxlint-plugin": "0.15.2",
12
- "@foldkit/ui": "0.166.0",
13
- "@foldkit/vite-plugin": "0.26.1"
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.166.0",
17
- "@foldkit/vite-plugin": "0.26.1",
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.2",
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,6 +1,7 @@
1
1
  import { Runtime } from 'foldkit'
2
2
 
3
3
  import { Message, Model, init, update, view } from './main'
4
+ import './styles.css'
4
5
 
5
6
  const application = Runtime.makeApplication({
6
7
  Model,
@@ -9,6 +9,7 @@ export default defineConfig({
9
9
  foldkit({
10
10
  ssr: {
11
11
  serverEntry: '/src/entry.server.ts',
12
+ clientEntry: '/src/entry.ts',
12
13
  build: { prerender: true },
13
14
  },
14
15
  }),
@@ -1,155 +1,14 @@
1
- import { Config, Effect, Layer, Match, Option, Predicate, String } from 'effect'
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
- NodeHttpPlatform,
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
- const isFetchHandler = (value: unknown): value is FetchHandler =>
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(),
@@ -1,6 +1,7 @@
1
1
  import { Runtime } from 'foldkit'
2
2
 
3
3
  import { Flags, Message, Model, init, update, view } from './main'
4
+ import './styles.css'
4
5
 
5
6
  const application = Runtime.makeApplication({
6
7
  Model,
@@ -9,6 +9,7 @@ export default defineConfig({
9
9
  foldkit({
10
10
  ssr: {
11
11
  serverEntry: '/src/entry.server.ts',
12
+ clientEntry: '/src/entry.ts',
12
13
  build: true,
13
14
  },
14
15
  }),
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-foldkit-app",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Create Foldkit applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",