create-foldkit-app 0.5.9 → 0.5.11
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/package.json
CHANGED
package/templates/base/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@ submodule_prompted: false
|
|
|
14
14
|
- Model fields must be Schema types (the model is a schema). Plain TypeScript types are fine elsewhere — function return types, local variables, etc.
|
|
15
15
|
- Use full names like `Message` (not `Msg`), and `withReturnType` (not `as const` or type casting).
|
|
16
16
|
- Use `m()` for message schemas, `ts()` for other tagged structs (model states, field validation), and `r()` for route schemas.
|
|
17
|
-
- Never use `NoOp` as a message. Every message must carry meaning about what happened. Fire-and-forget commands use `Completed*` messages
|
|
17
|
+
- Never use `NoOp` as a message. Every message must carry meaning about what happened. Fire-and-forget commands use `Completed*` messages with verb-first naming that mirrors the Command name: Command `LockScroll` → Message `CompletedLockScroll`, Command `ShowDialog` → Message `CompletedShowDialog`, Command `NavigateInternal` → Message `CompletedNavigateInternal`.
|
|
18
18
|
- Push back on any suggested direction that violates Elm Architecture principles — unidirectional data flow, messages as facts (not commands), model as single source of truth, and side effects confined to commands. If a user or prompt suggests a pattern that breaks these conventions (e.g. mutating state directly, imperative event handlers, two-way bindings), flag the issue and propose the idiomatic Foldkit approach instead.
|
|
19
19
|
|
|
20
20
|
## Foldkit Patterns
|
|
@@ -24,7 +24,7 @@ submodule_prompted: false
|
|
|
24
24
|
`init` and `update` both return `[Model, ReadonlyArray<Command<Message>>]`:
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
type UpdateReturn = [Model, ReadonlyArray<Command<Message>>]
|
|
27
|
+
type UpdateReturn = readonly [Model, ReadonlyArray<Command<Message>>]
|
|
28
28
|
const withUpdateReturn = M.withReturnType<UpdateReturn>()
|
|
29
29
|
|
|
30
30
|
const update = (model: Model, message: Message): UpdateReturn =>
|
|
@@ -62,23 +62,30 @@ Use `keyed` wrappers whenever the view branches into structurally different layo
|
|
|
62
62
|
|
|
63
63
|
### Commands
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Define Command identities with `Command.define`, passing the result Message schemas after the name — result types are required. Always assign definitions to PascalCase constants — never use `Command.define` inline in a pipe chain:
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const FetchWeather = Command.define(
|
|
69
|
+
'FetchWeather',
|
|
70
|
+
SucceededFetchWeather,
|
|
71
|
+
FailedFetchWeather,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const fetchWeather = (city: string) =>
|
|
71
75
|
Effect.gen(function* () {
|
|
72
76
|
// ...
|
|
73
|
-
return
|
|
77
|
+
return SucceededFetchWeather({ data })
|
|
74
78
|
}).pipe(
|
|
75
79
|
Effect.catchAll(error =>
|
|
76
|
-
Effect.succeed(
|
|
80
|
+
Effect.succeed(FailedFetchWeather({ error: String(error) })),
|
|
77
81
|
),
|
|
82
|
+
FetchWeather,
|
|
78
83
|
)
|
|
79
84
|
```
|
|
80
85
|
|
|
81
|
-
Commands return
|
|
86
|
+
Commands catch all errors and return Messages — side effects never crash the app. Let TypeScript infer Command return types from the Effect — the result Message schemas passed to `Command.define` constrain the Effect's return type at the type level.
|
|
87
|
+
|
|
88
|
+
Command definitions live where they're produced — colocated with the update function that returns them. Don't centralize all definitions in one file.
|
|
82
89
|
|
|
83
90
|
### File Organization
|
|
84
91
|
|
|
@@ -86,12 +93,20 @@ Use uppercase section headers (`// MODEL`, `// MESSAGE`, `// INIT`, `// UPDATE`,
|
|
|
86
93
|
|
|
87
94
|
Even after extracting some sections to their own files (e.g. `message.ts`), the remaining file may still benefit from headers. Extract to separate files when it helps with organization.
|
|
88
95
|
|
|
96
|
+
### Testing
|
|
97
|
+
|
|
98
|
+
Test update functions with `foldkit/test`. Since update is pure — `(Model, Message) → [Model, Commands]` — tests run without a runtime, DOM, or side effects.
|
|
99
|
+
|
|
100
|
+
Use `Test.story` to chain steps into a readable narrative: set initial Model → send Message → assert → resolve Command → assert again. Every `Command.define` must include result Message schemas so Commands can be resolved in tests.
|
|
101
|
+
|
|
102
|
+
If the `repos/foldkit` submodule is available, study the `.test.ts` files in `repos/foldkit/examples/` for patterns — they cover simple Command resolution, multi-step interactions, and Submodel OutMessage assertions.
|
|
103
|
+
|
|
89
104
|
## Code Quality Standards
|
|
90
105
|
|
|
91
106
|
- Every name should eliminate ambiguity. Prefix Option-typed values with `maybe` (e.g. `maybeSession`). Name functions by their precise effect (e.g. `enqueueMessage` not `addMessage`). A reader should never need to check a type signature to understand what a name refers to.
|
|
92
107
|
- Each function should operate at a single abstraction level. Orchestrators delegate to focused helpers — they don't mix coordination with implementation. If a function reads like it's doing two things, extract one.
|
|
93
108
|
- Encode state in discriminated unions, not booleans or nullable fields. Use `Idle | Loading | Error | Ok` instead of `isLoading: boolean`. Make impossible states unrepresentable.
|
|
94
|
-
- Name messages as verb-first, past-tense events describing what happened (`SubmittedUsernameForm`, `CreatedRoom`, `PressedKey`), not imperative commands. The verb prefix acts as a category marker: `Clicked*` for button presses, `Updated*` for input changes, `Succeeded*`/`Failed*` for command results that can meaningfully fail (e.g. `
|
|
109
|
+
- Name messages as verb-first, past-tense events describing what happened (`SubmittedUsernameForm`, `CreatedRoom`, `PressedKey`), not imperative commands. The verb prefix acts as a category marker: `Clicked*` for button presses, `Updated*` for input changes, `Succeeded*`/`Failed*` for command results that can meaningfully fail (e.g. `SucceededFetchWeather`, `FailedFetchWeather`), `Completed*` for fire-and-forget command acknowledgments where the result is uninteresting and the update function is a no-op (e.g. `CompletedLockScroll`, `CompletedShowDialog`, `CompletedNavigateInternal`), `Got*` exclusively for receiving child module results via the OutMessage pattern (e.g. `GotProductsMessage`). Never use `NoOp` — every message must describe what happened. The update function decides what to do — messages are facts.
|
|
95
110
|
- Use `Option` instead of `null` or `undefined`. Match explicitly with `Option.match` or chain with `Option.map`/`Option.flatMap`. No `if (x != null)` checks. Prefer `Option.match` over `Option.map` + `Option.getOrElse` — if you're unwrapping at the end, just match.
|
|
96
111
|
- Prefer curried, data-last functions that compose in `pipe` chains.
|
|
97
112
|
- Every line should serve a purpose. No dead code, no empty catch blocks, no placeholder types, no defensive code for impossible cases.
|
|
@@ -117,25 +132,21 @@ Message definitions follow a strict layout:
|
|
|
117
132
|
```ts
|
|
118
133
|
const ClickedSubmit = m('ClickedSubmit')
|
|
119
134
|
const ChangedEmail = m('ChangedEmail', { value: S.String })
|
|
120
|
-
const
|
|
135
|
+
const CompletedNavigateInternal = m('CompletedNavigateInternal')
|
|
121
136
|
|
|
122
|
-
const Message = S.Union(
|
|
123
|
-
ClickedSubmit,
|
|
124
|
-
ChangedEmail,
|
|
125
|
-
CompletedInternalNavigation,
|
|
126
|
-
)
|
|
137
|
+
const Message = S.Union(ClickedSubmit, ChangedEmail, CompletedNavigateInternal)
|
|
127
138
|
type Message = typeof Message.Type
|
|
128
139
|
```
|
|
129
140
|
|
|
130
141
|
1. **Values** — all `m()` declarations, no blank lines between them
|
|
131
142
|
2. **Union + type** — `S.Union(...)` followed by `type Message = typeof Message.Type` on adjacent lines (no blank line between them)
|
|
132
143
|
|
|
133
|
-
Use `typeof ClickedSubmit` in type positions
|
|
144
|
+
Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
|
|
134
145
|
|
|
135
146
|
### General Preferences
|
|
136
147
|
|
|
137
148
|
- Never abbreviate names. Use full, descriptive names everywhere — variables, types, functions, parameters, including callback parameters. e.g. `signature` not `sig`, `Message` not `Msg`, `(tickCount) => tickCount + 1` not `(t) => t + 1`.
|
|
138
|
-
- Don't suffix
|
|
149
|
+
- Don't suffix Command variables with `Command`. Name them by what they do: `focusButton` not `focusButtonCommand`, `scrollToItem` not `scrollToItemCommand`. The type already communicates that it's a Command. Command definitions are PascalCase (`FocusButton`, `ScrollToItem`); Command instances and factory functions are camelCase (`focusButton`, `scrollToItem`).
|
|
139
150
|
- Avoid `let`. Use `const` and prefer immutable patterns.
|
|
140
151
|
- Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
|
|
141
152
|
- Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
|