create-foldkit-app 0.33.0 → 0.34.0-canary.63fa8e97d24a

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 (106) hide show
  1. package/README.md +6 -6
  2. package/dist/commands/create.js +4 -4
  3. package/dist/index.js +7 -7
  4. package/dist/rendering.js +1 -1
  5. package/dist/templates/examples/api-cache/package.json +3 -3
  6. package/dist/templates/examples/api-cache/src/{main.fixtures.ts → main.fixture.ts} +24 -23
  7. package/dist/templates/examples/api-cache/src/scene.test.ts +1 -1
  8. package/dist/templates/examples/api-cache/src/story.test.ts +26 -20
  9. package/dist/templates/examples/auth/package.json +3 -3
  10. package/dist/templates/examples/auth/src/page/loggedOut/page/login.scene.test.ts +5 -12
  11. package/dist/templates/examples/auth/src/page/loggedOut/page/login.story.test.ts +4 -5
  12. package/dist/templates/examples/auth/src/page/loggedOut/page/login.ts +1 -0
  13. package/dist/templates/examples/auth/src/scene.test.ts +4 -5
  14. package/dist/templates/examples/canvas-art/package.json +3 -3
  15. package/dist/templates/examples/canvas-art/src/scene.test.ts +9 -10
  16. package/dist/templates/examples/canvas-art/src/story.test.ts +9 -10
  17. package/dist/templates/examples/charting/package.json +3 -3
  18. package/dist/templates/examples/charting/src/{main.fixtures.ts → main.fixture.ts} +4 -4
  19. package/dist/templates/examples/charting/src/scene.test.ts +13 -9
  20. package/dist/templates/examples/charting/src/story.test.ts +8 -5
  21. package/dist/templates/examples/charting/src/telemetry.test.ts +1 -1
  22. package/dist/templates/examples/counter/package.json +3 -3
  23. package/dist/templates/examples/counters/package.json +3 -3
  24. package/dist/templates/examples/crash-view/package.json +3 -3
  25. package/dist/templates/examples/embedding/package.json +3 -3
  26. package/dist/templates/examples/embedding/src/scene.test.ts +2 -1
  27. package/dist/templates/examples/embedding/src/story.test.ts +2 -1
  28. package/dist/templates/examples/form/package.json +3 -3
  29. package/dist/templates/examples/form/src/main.ts +2 -0
  30. package/dist/templates/examples/form/src/scene.test.ts +16 -19
  31. package/dist/templates/examples/form/src/story.test.ts +11 -13
  32. package/dist/templates/examples/generative-art/package.json +3 -3
  33. package/dist/templates/examples/generative-art/src/scene.test.ts +9 -7
  34. package/dist/templates/examples/generative-art/src/story.test.ts +16 -10
  35. package/dist/templates/examples/interrupting-commands/package.json +3 -3
  36. package/dist/templates/examples/job-application/package.json +3 -3
  37. package/dist/templates/examples/job-application/src/scene.test.ts +44 -42
  38. package/dist/templates/examples/job-application/src/step/personalInfo/story.test.ts +7 -5
  39. package/dist/templates/examples/job-application/src/story.test.ts +26 -23
  40. package/dist/templates/examples/job-application/src/view/field.ts +1 -0
  41. package/dist/templates/examples/kanban/package.json +3 -3
  42. package/dist/templates/examples/managed-resource-layer/package.json +3 -3
  43. package/dist/templates/examples/managed-resource-layer/src/story.test.ts +3 -2
  44. package/dist/templates/examples/map/package.json +3 -3
  45. package/dist/templates/examples/map/src/{main.fixtures.ts → main.fixture.ts} +4 -4
  46. package/dist/templates/examples/map/src/scene.test.ts +24 -18
  47. package/dist/templates/examples/map/src/story.test.ts +17 -13
  48. package/dist/templates/examples/pixel-art/package.json +3 -3
  49. package/dist/templates/examples/pixel-art/src/comparison.bench.test.ts +2 -1
  50. package/dist/templates/examples/pixel-art/src/main.bench.ts +71 -54
  51. package/dist/templates/examples/pixel-art/src/scene.test.ts +35 -33
  52. package/dist/templates/examples/pixel-art/src/story.test.ts +20 -20
  53. package/dist/templates/examples/pixel-art/src/view/dialog.ts +2 -0
  54. package/dist/templates/examples/query-sync/package.json +3 -3
  55. package/dist/templates/examples/query-sync/src/scene.test.ts +26 -20
  56. package/dist/templates/examples/query-sync/src/story.test.ts +11 -8
  57. package/dist/templates/examples/route-transitions/package.json +3 -3
  58. package/dist/templates/examples/routing/package.json +3 -3
  59. package/dist/templates/examples/routing/src/page/people.ts +1 -2
  60. package/dist/templates/examples/shopping-cart/package.json +3 -3
  61. package/dist/templates/examples/shopping-cart/src/scene.test.ts +22 -28
  62. package/dist/templates/examples/shopping-cart/src/story.test.ts +35 -28
  63. package/dist/templates/examples/slow-warnings/package.json +3 -3
  64. package/dist/templates/examples/slow-warnings/src/handleSlow.test.ts +4 -4
  65. package/dist/templates/examples/slow-warnings/src/scene.test.ts +15 -13
  66. package/dist/templates/examples/slow-warnings/src/story.test.ts +16 -14
  67. package/dist/templates/examples/snake/package.json +3 -3
  68. package/dist/templates/examples/snake/src/scene.test.ts +11 -9
  69. package/dist/templates/examples/snake/src/story.test.ts +10 -11
  70. package/dist/templates/examples/ssg/package.json +2 -2
  71. package/dist/templates/examples/ssr/package.json +6 -5
  72. package/dist/templates/examples/state-machine/package.json +3 -3
  73. package/dist/templates/examples/state-machine/src/view.ts +3 -0
  74. package/dist/templates/examples/stopwatch/package.json +3 -3
  75. package/dist/templates/examples/stopwatch/src/story.test.ts +2 -1
  76. package/dist/templates/examples/todo/package.json +3 -3
  77. package/dist/templates/examples/todo/src/scene.test.ts +8 -9
  78. package/dist/templates/examples/todo/src/story.test.ts +37 -42
  79. package/dist/templates/examples/ui-showcase/package.json +3 -3
  80. package/dist/templates/examples/ui-showcase/src/story.test.ts +10 -10
  81. package/dist/templates/examples/ui-showcase/src/ui/view/checkbox.ts +1 -0
  82. package/dist/templates/examples/ui-showcase/src/ui/view/dialog.ts +5 -0
  83. package/dist/templates/examples/ui-showcase/src/ui/view/fieldset.ts +5 -0
  84. package/dist/templates/examples/ui-showcase/src/ui/view/input.ts +2 -0
  85. package/dist/templates/examples/ui-showcase/src/ui/view/radioGroup.ts +2 -0
  86. package/dist/templates/examples/ui-showcase/src/ui/view/select.ts +2 -0
  87. package/dist/templates/examples/ui-showcase/src/ui/view/switch.ts +1 -0
  88. package/dist/templates/examples/ui-showcase/src/ui/view/textarea.ts +2 -0
  89. package/dist/templates/examples/view-transitions/package.json +3 -3
  90. package/dist/templates/examples/weather/package.json +3 -3
  91. package/dist/templates/examples/weather/src/scene.test.ts +1 -1
  92. package/dist/templates/examples/weather/src/story.test.ts +1 -1
  93. package/dist/templates/examples/web-components/package.json +3 -3
  94. package/dist/templates/examples/websocket-chat/package.json +3 -3
  95. package/dist/templates/examples/websocket-chat/src/scene.test.ts +20 -25
  96. package/dist/templates/examples/websocket-chat/src/story.test.ts +19 -20
  97. package/dist/templates/package-managers/pnpm/pnpm-workspace.yaml +0 -1
  98. package/dist/templates/release.json +15 -15
  99. package/dist/templates/rendering/ssr/package.json +2 -2
  100. package/dist/templates/rendering/ssr/scripts/serve.ts +155 -0
  101. package/dist/templates/rendering/ssr/tsconfig.json +1 -1
  102. package/dist/templates/rendering/ssr/vite.config.ts +1 -1
  103. package/dist/utils/packages.js +1 -1
  104. package/package.json +6 -6
  105. package/dist/templates/rendering/ssr/server/main.ts +0 -205
  106. /package/dist/templates/examples/weather/src/{main.fixtures.ts → main.fixture.ts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-foldkit-app
2
2
 
3
- Scaffolding CLI for new Foldkit applications. Creates a ready-to-run project with Vite, Tailwind CSS, TypeScript, [`@foldkit/vite-plugin`](https://www.npmjs.com/package/@foldkit/vite-plugin) for hot reloading with Model preservation, your choice of rendering mode, and a starter example for SPA scaffolds.
3
+ Scaffolding CLI for new Foldkit applications. Creates a ready-to-run project with Vite, Tailwind CSS, TypeScript, [`@foldkit/vite-plugin`](https://www.npmjs.com/package/@foldkit/vite-plugin) for state-preserving live reload, your choice of rendering mode, and a starter example for SPA scaffolds.
4
4
 
5
5
  ## Usage
6
6
 
@@ -18,11 +18,11 @@ The CLI prompts you for a project name, rendering mode, starter example, and pac
18
18
 
19
19
  ## Rendering
20
20
 
21
- | Mode | Description |
22
- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
23
- | `spa` | Render entirely in the browser. Starts from your choice of starter example below. |
24
- | `ssg` | Prerender routes to static HTML at build time, then hydrate in the browser. Scaffolds a small routed app with a server entry and a prerender script. |
25
- | `ssr` | Render each request on a Node server, then hydrate in the browser. Scaffolds a counter whose Flags come from the request, plus an Effect HttpServer host and a `start` script. |
21
+ | Mode | Description |
22
+ | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `spa` | Render entirely in the browser. Starts from your choice of starter example below. |
24
+ | `ssg` | Prerender routes to static HTML at build time, then hydrate in the browser. Scaffolds a small routed app with a server entry. One `vite build` writes every route. |
25
+ | `ssr` | Render each request, then hydrate in the browser. Scaffolds a counter whose Flags come from the request. One `vite build` emits a Web `fetch` handler; `start` runs `node scripts/serve.ts`. |
26
26
 
27
27
  The starter examples apply to `spa` rendering. The `ssg` and `ssr` modes scaffold their own starter apps.
28
28
 
@@ -8,14 +8,14 @@ import { createProject } from '../utils/files.js';
8
8
  import { devCommand, installDependencies, readFoldkitSubtreeRef, } from '../utils/packages.js';
9
9
  import { validateProjectName } from '../validateName.js';
10
10
  const isWindows = process.platform === 'win32';
11
- const promptForName = Prompt.text({
11
+ const promptForName = Prompt.String({
12
12
  message: 'Give your project a name',
13
13
  validate: value => Option.match(validateProjectName(value), {
14
14
  onNone: () => Effect.succeed(value),
15
15
  onSome: message => Effect.fail(message),
16
16
  }),
17
17
  });
18
- const promptForRendering = Prompt.select({
18
+ const promptForRendering = Prompt.Select({
19
19
  message: 'Pick a rendering mode',
20
20
  choices: renderings.map(({ value, title, description }) => ({
21
21
  value,
@@ -23,7 +23,7 @@ const promptForRendering = Prompt.select({
23
23
  description,
24
24
  })),
25
25
  });
26
- const promptForExample = Prompt.autoComplete({
26
+ const promptForExample = Prompt.AutoComplete({
27
27
  message: 'Pick a starting example',
28
28
  choices: examples.map(({ value, title, description }) => ({
29
29
  value,
@@ -31,7 +31,7 @@ const promptForExample = Prompt.autoComplete({
31
31
  description,
32
32
  })),
33
33
  });
34
- const promptForPackageManager = Prompt.select({
34
+ const promptForPackageManager = Prompt.Select({
35
35
  message: 'Pick a package manager',
36
36
  choices: [
37
37
  { value: 'pnpm', title: 'pnpm' },
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { Effect, Layer, Option, Schema } from 'effect';
2
+ import { Effect, Option, Schema } from 'effect';
3
3
  import { Command, Flag } from 'effect/unstable/cli';
4
4
  import { createRequire } from 'node:module';
5
- import { NodeRuntime, NodeServices, NodeStdio } from '@effect/platform-node';
5
+ import { NodeRuntime, NodeServices } from '@effect/platform-node';
6
6
  import { create as create_ } from './commands/create.js';
7
7
  import { EXAMPLE_VALUES } from './examples.js';
8
8
  import { RENDERING_VALUES } from './rendering.js';
@@ -13,10 +13,10 @@ const nameSchema = Schema.String.pipe(Schema.check(Schema.makeFilter(value => Op
13
13
  onNone: () => true,
14
14
  onSome: message => message,
15
15
  }))));
16
- const name = Flag.string('name').pipe(Flag.withAlias('n'), Flag.withDescription('The name of the project to create'), Flag.withSchema(nameSchema), Flag.optional);
17
- const rendering = Flag.choice('rendering', RENDERING_VALUES).pipe(Flag.withAlias('r'), Flag.withDescription('How the application renders: spa renders entirely in the browser, ssg prerenders routes to static HTML at build time, ssr renders each request on a Node server'), Flag.optional);
18
- const example = Flag.choice('example', EXAMPLE_VALUES).pipe(Flag.withAlias('e'), Flag.withDescription("The example application to start from with spa rendering. Run with no flags for an interactive picker that shows each example's description."), Flag.optional);
19
- const packageManager = Flag.choice('package-manager', [
16
+ const name = Flag.String('name').pipe(Flag.withAlias('n'), Flag.withDescription('The name of the project to create'), Flag.withSchema(nameSchema), Flag.optional);
17
+ const rendering = Flag.Literals('rendering', RENDERING_VALUES).pipe(Flag.withAlias('r'), Flag.withDescription('How the application renders: spa renders entirely in the browser, ssg prerenders routes to static HTML at build time, ssr renders each request then hydrates'), Flag.optional);
18
+ const example = Flag.Literals('example', EXAMPLE_VALUES).pipe(Flag.withAlias('e'), Flag.withDescription("The example application to start from with spa rendering. Run with no flags for an interactive picker that shows each example's description."), Flag.optional);
19
+ const packageManager = Flag.Literals('package-manager', [
20
20
  'pnpm',
21
21
  'npm',
22
22
  'yarn',
@@ -32,4 +32,4 @@ const create = Command.make('create', {
32
32
  const cli = Command.run(create, {
33
33
  version: packageJson.version,
34
34
  });
35
- cli.pipe(Effect.provide([Layer.mergeAll(NodeServices.layer, NodeStdio.layer)]), NodeRuntime.runMain);
35
+ cli.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
package/dist/rendering.js CHANGED
@@ -14,7 +14,7 @@ export const renderings = [
14
14
  {
15
15
  value: 'ssr',
16
16
  title: 'SSR',
17
- description: 'Render each request on a Node server, then hydrate in the browser',
17
+ description: 'Render each request, then hydrate in the browser',
18
18
  },
19
19
  ];
20
20
  export const Scaffold = Data.taggedEnum();
@@ -9,9 +9,9 @@
9
9
  "test": "vitest run"
10
10
  },
11
11
  "dependencies": {
12
- "@effect/platform-browser": "4.0.0-rc.112",
12
+ "@effect/platform-browser": "4.0.0-rc.115",
13
13
  "@foldkit/ui": "workspace:*",
14
- "effect": "4.0.0-rc.112",
14
+ "effect": "4.0.0-rc.115",
15
15
  "foldkit": "workspace:*"
16
16
  },
17
17
  "devDependencies": {
@@ -22,6 +22,6 @@
22
22
  "tailwindcss": "^4.3.3",
23
23
  "typescript": "^7.0.2",
24
24
  "vite": "^8.2.2",
25
- "vitest": "^4.1.11"
25
+ "vitest": "^5.0.0"
26
26
  }
27
27
  }
@@ -1,4 +1,5 @@
1
1
  import { HashMap, Option } from 'effect'
2
+ import { evo } from 'foldkit/struct'
2
3
 
3
4
  import { Tabs } from '@foldkit/ui'
4
5
 
@@ -43,28 +44,28 @@ export const loadingPostsModel: Model = {
43
44
  stats: StatsData.Idle(),
44
45
  }
45
46
 
46
- export const loadedPostsModel: Model = {
47
- ...loadingPostsModel,
48
- posts: PostsData.Success({
49
- data: { posts: fixturePosts, fetchedAt: FETCHED_AT },
50
- }),
51
- }
52
-
53
- export const cachedFirstPostModel: Model = {
54
- ...loadedPostsModel,
55
- postDetailById: HashMap.set(
56
- HashMap.empty(),
57
- 'first-post',
58
- PostDetailData.Success({
59
- data: { detail: firstPostDetail, fetchedAt: FETCHED_AT },
47
+ export const loadedPostsModel: Model = evo(loadingPostsModel, {
48
+ posts: () =>
49
+ PostsData.Success({
50
+ data: { posts: fixturePosts, fetchedAt: FETCHED_AT },
60
51
  }),
61
- ),
62
- }
52
+ })
63
53
 
64
- export const loadedStatsModel: Model = {
65
- ...loadedPostsModel,
66
- activeTab: 'Stats',
67
- stats: StatsData.Success({
68
- data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
69
- }),
70
- }
54
+ export const cachedFirstPostModel: Model = evo(loadedPostsModel, {
55
+ postDetailById: () =>
56
+ HashMap.set(
57
+ HashMap.empty(),
58
+ 'first-post',
59
+ PostDetailData.Success({
60
+ data: { detail: firstPostDetail, fetchedAt: FETCHED_AT },
61
+ }),
62
+ ),
63
+ })
64
+
65
+ export const loadedStatsModel: Model = evo(loadedPostsModel, {
66
+ activeTab: () => 'Stats',
67
+ stats: () =>
68
+ StatsData.Success({
69
+ data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
70
+ }),
71
+ })
@@ -21,7 +21,7 @@ import {
21
21
  fixtureStats,
22
22
  loadedPostsModel,
23
23
  loadingPostsModel,
24
- } from './main.fixtures'
24
+ } from './main.fixture'
25
25
 
26
26
  const resolveFocusTab = Command.resolve(
27
27
  Tabs.FocusTab,
@@ -1,5 +1,6 @@
1
1
  import { HashMap, Option, Result } from 'effect'
2
2
  import { Command, given, message, model, story } from 'foldkit/story'
3
+ import { evo } from 'foldkit/struct'
3
4
  import { expect, test } from 'vitest'
4
5
 
5
6
  import { Tabs } from '@foldkit/ui'
@@ -22,7 +23,7 @@ import {
22
23
  fixtureStats,
23
24
  loadedPostsModel,
24
25
  loadedStatsModel,
25
- } from './main.fixtures'
26
+ } from './main.fixture'
26
27
 
27
28
  const postDetailTag = (model: Model, postId: string): string =>
28
29
  HashMap.get(model.postDetailById, postId).pipe(
@@ -96,7 +97,7 @@ test('a revalidation tick keeps stale stats on screen while refetching', () => {
96
97
  FetchStats,
97
98
  Message.SettledFetchStats({
98
99
  result: Result.succeed({
99
- stats: { ...fixtureStats, activeUsers: 99 },
100
+ stats: evo(fixtureStats, { activeUsers: () => 99 }),
100
101
  fetchedAt: FETCHED_AT + 5000,
101
102
  }),
102
103
  }),
@@ -134,7 +135,7 @@ test('a failed refresh keeps the stale stats on screen with the error', () => {
134
135
  test('refresh clicks during an in-flight fetch are deduplicated', () => {
135
136
  story(
136
137
  update,
137
- given({ ...loadedStatsModel, stats: StatsData.Loading() }),
138
+ given(evo(loadedStatsModel, { stats: () => StatsData.Loading() })),
138
139
  message(Message.ClickedRefreshStats()),
139
140
  Command.expectNone(),
140
141
  )
@@ -143,12 +144,14 @@ test('refresh clicks during an in-flight fetch are deduplicated', () => {
143
144
  test('a revalidation tick during a refresh is deduplicated', () => {
144
145
  story(
145
146
  update,
146
- given({
147
- ...loadedStatsModel,
148
- stats: StatsData.Refreshing({
149
- data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
147
+ given(
148
+ evo(loadedStatsModel, {
149
+ stats: () =>
150
+ StatsData.Refreshing({
151
+ data: { stats: fixtureStats, fetchedAt: FETCHED_AT },
152
+ }),
150
153
  }),
151
- }),
154
+ ),
152
155
  message(Message.TickedRevalidateStats()),
153
156
  Command.expectNone(),
154
157
  )
@@ -183,10 +186,11 @@ test('invalidating posts refetches while keeping the current list', () => {
183
186
  test('retrying failed posts shows the loading state and refetches', () => {
184
187
  story(
185
188
  update,
186
- given({
187
- ...loadedPostsModel,
188
- posts: PostsData.Failure({ error: 'The server is down.' }),
189
- }),
189
+ given(
190
+ evo(loadedPostsModel, {
191
+ posts: () => PostsData.Failure({ error: 'The server is down.' }),
192
+ }),
193
+ ),
190
194
  message(Message.ClickedRetryPosts()),
191
195
  model(model => {
192
196
  expect(model.posts._tag).toBe('Loading')
@@ -269,14 +273,16 @@ test('a failed post detail fetch lands in Failure and retry refetches', () => {
269
273
  test('revisiting a post with a cached failure shows it without refetching', () => {
270
274
  story(
271
275
  update,
272
- given({
273
- ...loadedPostsModel,
274
- postDetailById: HashMap.set(
275
- HashMap.empty(),
276
- 'first-post',
277
- PostDetailData.Failure({ error: 'The connection dropped.' }),
278
- ),
279
- }),
276
+ given(
277
+ evo(loadedPostsModel, {
278
+ postDetailById: () =>
279
+ HashMap.set(
280
+ HashMap.empty(),
281
+ 'first-post',
282
+ PostDetailData.Failure({ error: 'The connection dropped.' }),
283
+ ),
284
+ }),
285
+ ),
280
286
  message(Message.ClickedPost({ postId: 'first-post' })),
281
287
  Command.expectNone(),
282
288
  model(model => {
@@ -9,10 +9,10 @@
9
9
  "test": "vitest run"
10
10
  },
11
11
  "dependencies": {
12
- "@effect/platform-browser": "4.0.0-rc.112",
12
+ "@effect/platform-browser": "4.0.0-rc.115",
13
13
  "@foldkit/ui": "workspace:*",
14
14
  "clsx": "^2.1.1",
15
- "effect": "4.0.0-rc.112",
15
+ "effect": "4.0.0-rc.115",
16
16
  "foldkit": "workspace:*"
17
17
  },
18
18
  "devDependencies": {
@@ -23,6 +23,6 @@
23
23
  "tailwindcss": "^4.3.3",
24
24
  "typescript": "^7.0.2",
25
25
  "vite": "^8.2.2",
26
- "vitest": "^4.1.11"
26
+ "vitest": "^5.0.0"
27
27
  }
28
28
  }
@@ -11,21 +11,14 @@ import {
11
11
  type,
12
12
  within,
13
13
  } from 'foldkit/scene'
14
+ import { evo } from 'foldkit/struct'
14
15
  import { describe, test } from 'vitest'
15
16
 
16
- import {
17
- Message,
18
- Model,
19
- SimulateAuthRequest,
20
- initModel,
21
- update,
22
- view,
23
- } from './login'
17
+ import { Message, SimulateAuthRequest, initModel, update, view } from './login'
24
18
 
25
- const validModel = Model.make({
26
- ...initModel(),
27
- email: Valid({ value: 'alice@example.com' }),
28
- password: Valid({ value: 'password' }),
19
+ const validModel = evo(initModel(), {
20
+ email: () => Valid({ value: 'alice@example.com' }),
21
+ password: () => Valid({ value: 'password' }),
29
22
  })
30
23
 
31
24
  const heading = role('heading', { name: 'Sign In' })
@@ -8,21 +8,20 @@ import {
8
8
  model,
9
9
  story,
10
10
  } from 'foldkit/story'
11
+ import { evo } from 'foldkit/struct'
11
12
  import { describe, expect, test } from 'vitest'
12
13
 
13
14
  import {
14
15
  Message,
15
- Model,
16
16
  OutMessage,
17
17
  SimulateAuthRequest,
18
18
  initModel,
19
19
  update,
20
20
  } from './login'
21
21
 
22
- const validModel = Model.make({
23
- ...initModel(),
24
- email: Valid({ value: 'alice@example.com' }),
25
- password: Valid({ value: 'password' }),
22
+ const validModel = evo(initModel(), {
23
+ email: () => Valid({ value: 'alice@example.com' }),
24
+ password: () => Valid({ value: 'password' }),
26
25
  })
27
26
 
28
27
  const aliceSession = { userId: '1', email: 'alice@example.com', name: 'alice' }
@@ -188,6 +188,7 @@ const fieldView = (
188
188
  placeholder,
189
189
  onInput: onUpdate,
190
190
  isInvalid: field._tag === 'Invalid',
191
+ hasDescription: field._tag === 'Invalid',
191
192
  toView: attributes =>
192
193
  h.div(
193
194
  [],
@@ -8,6 +8,7 @@ import {
8
8
  submit,
9
9
  text,
10
10
  } from 'foldkit/scene'
11
+ import { evo } from 'foldkit/struct'
11
12
  import { describe, test } from 'vitest'
12
13
 
13
14
  import { SaveSession } from './command'
@@ -16,7 +17,6 @@ import { Message } from './message'
16
17
  import { LoggedOut } from './model'
17
18
  import {
18
19
  Message as LoginMessage,
19
- Model as LoginModel,
20
20
  SimulateAuthRequest,
21
21
  initModel as initLoginModel,
22
22
  } from './page/loggedOut/page/login'
@@ -26,10 +26,9 @@ import { view } from './view'
26
26
 
27
27
  const validModel = LoggedOut.Model({
28
28
  route: AppRoute.Login(),
29
- loginModel: LoginModel.make({
30
- ...initLoginModel(),
31
- email: Valid({ value: 'alice@example.com' }),
32
- password: Valid({ value: 'password' }),
29
+ loginModel: evo(initLoginModel(), {
30
+ email: () => Valid({ value: 'alice@example.com' }),
31
+ password: () => Valid({ value: 'password' }),
33
32
  }),
34
33
  })
35
34
 
@@ -9,9 +9,9 @@
9
9
  "test": "vitest run"
10
10
  },
11
11
  "dependencies": {
12
- "@effect/platform-browser": "4.0.0-rc.112",
12
+ "@effect/platform-browser": "4.0.0-rc.115",
13
13
  "@foldkit/ui": "workspace:*",
14
- "effect": "4.0.0-rc.112",
14
+ "effect": "4.0.0-rc.115",
15
15
  "foldkit": "workspace:*"
16
16
  },
17
17
  "devDependencies": {
@@ -22,6 +22,6 @@
22
22
  "tailwindcss": "^4.3.3",
23
23
  "typescript": "^7.0.2",
24
24
  "vite": "^8.2.2",
25
- "vitest": "^4.1.11"
25
+ "vitest": "^5.0.0"
26
26
  }
27
27
  }
@@ -1,4 +1,5 @@
1
1
  import { click, expect, given, role, scene, text } from 'foldkit/scene'
2
+ import { evo } from 'foldkit/struct'
2
3
  import { describe, test } from 'vitest'
3
4
 
4
5
  import { type Model, update, view } from './main'
@@ -33,15 +34,14 @@ describe('view', () => {
33
34
  })
34
35
 
35
36
  test('the ball count reflects spawned balls in the Model', () => {
36
- const populatedModel: Model = {
37
- ...emptyModel,
38
- balls: [
37
+ const populatedModel: Model = evo(emptyModel, {
38
+ balls: () => [
39
39
  { id: 0, x: 10, y: 20, vx: 1, vy: 1, radius: 8, color: '#ff2d55' },
40
40
  { id: 1, x: 30, y: 40, vx: -1, vy: 1, radius: 12, color: '#5ac8fa' },
41
41
  { id: 2, x: 50, y: 60, vx: 1, vy: -1, radius: 16, color: '#34c759' },
42
42
  ],
43
- nextId: 3,
44
- }
43
+ nextId: () => 3,
44
+ })
45
45
 
46
46
  scene(
47
47
  { update, view },
@@ -51,13 +51,12 @@ describe('view', () => {
51
51
  })
52
52
 
53
53
  test('Clear empties the rendered ball count', () => {
54
- const populatedModel: Model = {
55
- ...emptyModel,
56
- balls: [
54
+ const populatedModel: Model = evo(emptyModel, {
55
+ balls: () => [
57
56
  { id: 0, x: 10, y: 20, vx: 1, vy: 1, radius: 8, color: '#ff2d55' },
58
57
  ],
59
- nextId: 1,
60
- }
58
+ nextId: () => 1,
59
+ })
61
60
 
62
61
  scene(
63
62
  { update, view },
@@ -1,4 +1,5 @@
1
1
  import { Command, given, message, model, story } from 'foldkit/story'
2
+ import { evo } from 'foldkit/struct'
2
3
  import { describe, expect, test } from 'vitest'
3
4
 
4
5
  import { GenerateBall, Message, type Model, update } from './main'
@@ -9,14 +10,13 @@ const emptyModel: Model = {
9
10
  isRunning: true,
10
11
  }
11
12
 
12
- const populatedModel: Model = {
13
- ...emptyModel,
14
- balls: [
13
+ const populatedModel: Model = evo(emptyModel, {
14
+ balls: () => [
15
15
  { id: 0, x: 100, y: 100, vx: 50, vy: 50, radius: 10, color: '#ff2d55' },
16
16
  { id: 1, x: 200, y: 200, vx: -50, vy: -50, radius: 15, color: '#5ac8fa' },
17
17
  ],
18
- nextId: 2,
19
- }
18
+ nextId: () => 2,
19
+ })
20
20
 
21
21
  describe('update', () => {
22
22
  describe('spawning balls', () => {
@@ -97,9 +97,8 @@ describe('update', () => {
97
97
  })
98
98
 
99
99
  test('bounces a ball off the canvas edges, flipping its velocity', () => {
100
- const movingRightModel: Model = {
101
- ...emptyModel,
102
- balls: [
100
+ const movingRightModel: Model = evo(emptyModel, {
101
+ balls: () => [
103
102
  {
104
103
  id: 0,
105
104
  x: 595,
@@ -110,8 +109,8 @@ describe('update', () => {
110
109
  color: '#fff',
111
110
  },
112
111
  ],
113
- nextId: 1,
114
- }
112
+ nextId: () => 1,
113
+ })
115
114
 
116
115
  story(
117
116
  update,
@@ -9,11 +9,11 @@
9
9
  "test": "vitest run"
10
10
  },
11
11
  "dependencies": {
12
- "@effect/platform-browser": "4.0.0-rc.112",
12
+ "@effect/platform-browser": "4.0.0-rc.115",
13
13
  "@foldkit/ui": "workspace:*",
14
14
  "clsx": "^2.1.1",
15
15
  "echarts": "^6.1.0",
16
- "effect": "4.0.0-rc.112",
16
+ "effect": "4.0.0-rc.115",
17
17
  "foldkit": "workspace:*"
18
18
  },
19
19
  "devDependencies": {
@@ -24,6 +24,6 @@
24
24
  "tailwindcss": "^4.3.3",
25
25
  "typescript": "^7.0.2",
26
26
  "vite": "^8.2.2",
27
- "vitest": "^4.1.11"
27
+ "vitest": "^5.0.0"
28
28
  }
29
29
  }
@@ -1,4 +1,5 @@
1
1
  import { Option } from 'effect'
2
+ import { evo } from 'foldkit/struct'
2
3
 
3
4
  import { RadioGroup } from '@foldkit/ui'
4
5
 
@@ -119,10 +120,9 @@ export const loadingModel = Model.make({
119
120
  maybeSelectedDatumId: Option.none(),
120
121
  })
121
122
 
122
- export const readyModel = Model.make({
123
- ...loadingModel,
124
- telemetry: TelemetryAsyncData.Success({ data: sampleTelemetry }),
125
- maybeChartHostId: Option.some('test-chart-host'),
123
+ export const readyModel = evo(loadingModel, {
124
+ telemetry: () => TelemetryAsyncData.Success({ data: sampleTelemetry }),
125
+ maybeChartHostId: () => Option.some('test-chart-host'),
126
126
  })
127
127
 
128
128
  export const mockGitHubRepository = {
@@ -9,12 +9,13 @@ import {
9
9
  scene,
10
10
  text,
11
11
  } from 'foldkit/scene'
12
+ import { evo } from 'foldkit/struct'
12
13
  import { describe, test } from 'vitest'
13
14
 
14
15
  import { RadioGroup } from '@foldkit/ui'
15
16
 
16
17
  import { SyncChart } from './command'
17
- import { loadingModel, readyModel, sampleTelemetry } from './main.fixtures'
18
+ import { loadingModel, readyModel, sampleTelemetry } from './main.fixture'
18
19
  import { Message } from './message'
19
20
  import { TelemetryAsyncData } from './model'
20
21
  import { update } from './update'
@@ -77,10 +78,12 @@ describe('view', () => {
77
78
  test('refreshing state keeps the dashboard visible', () => {
78
79
  scene(
79
80
  { update, view },
80
- given({
81
- ...readyModel,
82
- telemetry: TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
83
- }),
81
+ given(
82
+ evo(readyModel, {
83
+ telemetry: () =>
84
+ TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
85
+ }),
86
+ ),
84
87
  acknowledgeChartMount,
85
88
  acknowledgeChartSync,
86
89
  expect(text('Refreshing public data')).toExist(),
@@ -91,10 +94,11 @@ describe('view', () => {
91
94
  test('failure without stale data shows retry', () => {
92
95
  scene(
93
96
  { update, view },
94
- given({
95
- ...loadingModel,
96
- telemetry: TelemetryAsyncData.Failure({ error: 'offline' }),
97
- }),
97
+ given(
98
+ evo(loadingModel, {
99
+ telemetry: () => TelemetryAsyncData.Failure({ error: 'offline' }),
100
+ }),
101
+ ),
98
102
  expect(label('Telemetry failed')).toExist(),
99
103
  expect(role('button', { name: 'Retry' })).toExist(),
100
104
  )
@@ -1,4 +1,5 @@
1
1
  import { Command, given, message, model, story } from 'foldkit/story'
2
+ import { evo } from 'foldkit/struct'
2
3
  import { expect, test } from 'vitest'
3
4
 
4
5
  import { RadioGroup } from '@foldkit/ui'
@@ -10,7 +11,7 @@ import {
10
11
  chartModes,
11
12
  packageIds,
12
13
  } from './domain'
13
- import { loadingModel, readyModel, sampleTelemetry } from './main.fixtures'
14
+ import { loadingModel, readyModel, sampleTelemetry } from './main.fixture'
14
15
  import { Message } from './message'
15
16
  import { TelemetryAsyncData } from './model'
16
17
  import { update } from './update'
@@ -101,10 +102,12 @@ test('refreshing with data keeps the old dashboard while fetching', () => {
101
102
  test('a failed refresh preserves stale data in the stale state', () => {
102
103
  story(
103
104
  update,
104
- given({
105
- ...readyModel,
106
- telemetry: TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
107
- }),
105
+ given(
106
+ evo(readyModel, {
107
+ telemetry: () =>
108
+ TelemetryAsyncData.Refreshing({ data: sampleTelemetry }),
109
+ }),
110
+ ),
108
111
  message(Message.FailedFetchTelemetry({ error: 'rate limited' })),
109
112
  model(model => {
110
113
  expect(model.telemetry._tag).toBe('Stale')
@@ -7,7 +7,7 @@ import {
7
7
  mockGitHubRepository,
8
8
  mockNpmDownloads,
9
9
  mockNpmPackument,
10
- } from './main.fixtures'
10
+ } from './main.fixture'
11
11
  import { NpmApiLive } from './npmApi'
12
12
  import { fetchRawTelemetry, transformTelemetry } from './telemetry'
13
13