@korajs/cli 0.1.8 → 0.1.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.
Files changed (50) hide show
  1. package/dist/bin.cjs +52 -8
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +2 -2
  4. package/dist/chunk-N36PFOSA.js +0 -0
  5. package/dist/{chunk-AUDUNRYA.js → chunk-SYOFLJLB.js} +33 -4
  6. package/dist/chunk-SYOFLJLB.js.map +1 -0
  7. package/dist/{chunk-ZVB4HAB3.js → chunk-VGOOQ56H.js} +22 -7
  8. package/dist/chunk-VGOOQ56H.js.map +1 -0
  9. package/dist/create.cjs +52 -8
  10. package/dist/create.cjs.map +1 -1
  11. package/dist/create.js +2 -2
  12. package/dist/index.cjs +21 -6
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +1 -1
  17. package/package.json +13 -13
  18. package/templates/react-basic/src/App.tsx +129 -31
  19. package/templates/react-basic/src/index.css +281 -0
  20. package/templates/react-basic/src/main.tsx +2 -0
  21. package/templates/react-basic/vite.config.ts +31 -1
  22. package/templates/react-sync/kora.config.ts +1 -1
  23. package/templates/react-sync/server.ts +13 -2
  24. package/templates/react-sync/src/App.tsx +132 -32
  25. package/templates/react-sync/src/index.css +292 -0
  26. package/templates/react-sync/src/main.tsx +2 -0
  27. package/templates/react-sync/vite.config.ts +31 -1
  28. package/templates/react-tailwind/index.html.hbs +12 -0
  29. package/templates/react-tailwind/kora.config.ts +13 -0
  30. package/templates/react-tailwind/package.json.hbs +31 -0
  31. package/templates/react-tailwind/src/App.tsx +212 -0
  32. package/templates/react-tailwind/src/index.css +7 -0
  33. package/templates/react-tailwind/src/kora-worker.ts +3 -0
  34. package/templates/react-tailwind/src/main.tsx +23 -0
  35. package/templates/react-tailwind/src/schema.ts +15 -0
  36. package/templates/react-tailwind/tsconfig.json +14 -0
  37. package/templates/react-tailwind/vite.config.ts +40 -0
  38. package/templates/react-tailwind-sync/index.html.hbs +12 -0
  39. package/templates/react-tailwind-sync/kora.config.ts +17 -0
  40. package/templates/react-tailwind-sync/package.json.hbs +33 -0
  41. package/templates/react-tailwind-sync/server.ts +22 -0
  42. package/templates/react-tailwind-sync/src/App.tsx +241 -0
  43. package/templates/react-tailwind-sync/src/index.css +7 -0
  44. package/templates/react-tailwind-sync/src/kora-worker.ts +3 -0
  45. package/templates/react-tailwind-sync/src/main.tsx +29 -0
  46. package/templates/react-tailwind-sync/src/schema.ts +15 -0
  47. package/templates/react-tailwind-sync/tsconfig.json +14 -0
  48. package/templates/react-tailwind-sync/vite.config.ts +40 -0
  49. package/dist/chunk-AUDUNRYA.js.map +0 -1
  50. package/dist/chunk-ZVB4HAB3.js.map +0 -1
package/dist/index.d.cts CHANGED
@@ -4,7 +4,7 @@ import { KoraError, SchemaDefinition } from '@korajs/core';
4
4
  declare const PACKAGE_MANAGERS: readonly ["pnpm", "npm", "yarn", "bun"];
5
5
  type PackageManager = (typeof PACKAGE_MANAGERS)[number];
6
6
  /** Available project templates */
7
- declare const TEMPLATES: readonly ["react-basic", "react-sync"];
7
+ declare const TEMPLATES: readonly ["react-tailwind-sync", "react-tailwind", "react-sync", "react-basic"];
8
8
  type TemplateName = (typeof TEMPLATES)[number];
9
9
  /** Metadata for a project template */
10
10
  interface TemplateInfo {
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { KoraError, SchemaDefinition } from '@korajs/core';
4
4
  declare const PACKAGE_MANAGERS: readonly ["pnpm", "npm", "yarn", "bun"];
5
5
  type PackageManager = (typeof PACKAGE_MANAGERS)[number];
6
6
  /** Available project templates */
7
- declare const TEMPLATES: readonly ["react-basic", "react-sync"];
7
+ declare const TEMPLATES: readonly ["react-tailwind-sync", "react-tailwind", "react-sync", "react-basic"];
8
8
  type TemplateName = (typeof TEMPLATES)[number];
9
9
  /** Metadata for a project template */
10
10
  interface TemplateInfo {
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  SchemaNotFoundError,
12
12
  TEMPLATES,
13
13
  TEMPLATE_INFO
14
- } from "./chunk-ZVB4HAB3.js";
14
+ } from "./chunk-VGOOQ56H.js";
15
15
  export {
16
16
  CliError,
17
17
  DevServerError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korajs/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "Kora.js CLI tooling and project scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -30,16 +30,6 @@
30
30
  "dist",
31
31
  "templates"
32
32
  ],
33
- "dependencies": {
34
- "citty": "^0.1.6",
35
- "@korajs/core": "0.1.2"
36
- },
37
- "devDependencies": {
38
- "typescript": "^5.7.3",
39
- "tsup": "^8.3.6",
40
- "vitest": "^3.0.4"
41
- },
42
- "license": "MIT",
43
33
  "scripts": {
44
34
  "build": "tsup",
45
35
  "dev": "tsup --watch",
@@ -48,5 +38,15 @@
48
38
  "typecheck": "tsc --noEmit",
49
39
  "lint": "biome check src/ tests/",
50
40
  "clean": "rm -rf dist .turbo coverage"
51
- }
52
- }
41
+ },
42
+ "dependencies": {
43
+ "@korajs/core": "workspace:*",
44
+ "citty": "^0.1.6"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.7.3",
48
+ "tsup": "^8.3.6",
49
+ "vitest": "^3.0.4"
50
+ },
51
+ "license": "MIT"
52
+ }
@@ -1,48 +1,146 @@
1
+ import { useState } from 'react'
1
2
  import { useQuery, useMutation, useCollection } from '@korajs/react'
2
3
 
4
+ type Filter = 'all' | 'active' | 'completed'
5
+
3
6
  export function App() {
4
7
  const todos = useCollection('todos')
5
- const activeTodos = useQuery(todos.where({ completed: false }))
8
+ const allTodos = useQuery(todos.orderBy('createdAt', 'desc'))
6
9
  const { mutate: addTodo } = useMutation(
7
10
  (data: { title: string }) => todos.insert(data)
8
11
  )
9
12
  const { mutate: toggleTodo } = useMutation(
10
13
  (id: string, data: { completed: boolean }) => todos.update(id, data)
11
14
  )
15
+ const { mutate: deleteTodo } = useMutation(
16
+ (id: string) => todos.delete(id)
17
+ )
18
+
19
+ const [filter, setFilter] = useState<Filter>('all')
20
+ const [input, setInput] = useState('')
21
+
22
+ const activeTodos = allTodos.filter((t) => !t.completed)
23
+ const completedTodos = allTodos.filter((t) => !!t.completed)
24
+ const filteredTodos =
25
+ filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
26
+
27
+ const handleSubmit = (e: React.FormEvent) => {
28
+ e.preventDefault()
29
+ const title = input.trim()
30
+ if (title) {
31
+ addTodo({ title })
32
+ setInput('')
33
+ }
34
+ }
35
+
36
+ const clearCompleted = () => {
37
+ for (const todo of completedTodos) {
38
+ deleteTodo(todo.id)
39
+ }
40
+ }
12
41
 
13
42
  return (
14
- <div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
15
- <h1>Kora Todo App</h1>
16
-
17
- <form
18
- onSubmit={async (e) => {
19
- e.preventDefault()
20
- const form = e.currentTarget
21
- const input = form.elements.namedItem('title') as HTMLInputElement
22
- if (input.value.trim()) {
23
- addTodo({ title: input.value.trim() })
24
- input.value = ''
25
- }
26
- }}
27
- >
28
- <input name="title" placeholder="What needs to be done?" />
29
- <button type="submit">Add</button>
43
+ <div className="app">
44
+ <div className="header">
45
+ <h1>My Tasks</h1>
46
+ <span className="local-badge">Local-first</span>
47
+ </div>
48
+
49
+ <div className="stats">
50
+ <div className="stat-card">
51
+ <div className="label">Total</div>
52
+ <div className="value muted">{allTodos.length}</div>
53
+ </div>
54
+ <div className="stat-card">
55
+ <div className="label">Remaining</div>
56
+ <div className="value warning">{activeTodos.length}</div>
57
+ </div>
58
+ <div className="stat-card">
59
+ <div className="label">Done</div>
60
+ <div className="value success">{completedTodos.length}</div>
61
+ </div>
62
+ </div>
63
+
64
+ <form className="add-form" onSubmit={handleSubmit}>
65
+ <input
66
+ type="text"
67
+ value={input}
68
+ onChange={(e) => setInput(e.target.value)}
69
+ placeholder="What needs to be done?"
70
+ />
71
+ <button type="submit" disabled={!input.trim()}>
72
+ Add
73
+ </button>
30
74
  </form>
31
75
 
32
- <ul>
33
- {activeTodos.map((todo) => (
34
- <li key={todo.id}>
35
- <label>
36
- <input
37
- type="checkbox"
38
- checked={!!todo.completed}
39
- onChange={() => toggleTodo(todo.id, { completed: !todo.completed })}
40
- />
41
- {String(todo.title)}
42
- </label>
43
- </li>
44
- ))}
45
- </ul>
76
+ <div className="filters">
77
+ {(['all', 'active', 'completed'] as const).map((f) => {
78
+ const count = f === 'all' ? allTodos.length : f === 'active' ? activeTodos.length : completedTodos.length
79
+ return (
80
+ <button
81
+ key={f}
82
+ className={`filter-btn ${filter === f ? 'active' : ''}`}
83
+ onClick={() => setFilter(f)}
84
+ >
85
+ {f.charAt(0).toUpperCase() + f.slice(1)}
86
+ <span className="badge">{count}</span>
87
+ </button>
88
+ )
89
+ })}
90
+ </div>
91
+
92
+ <div className="todo-list">
93
+ {filteredTodos.length === 0 ? (
94
+ <div className="empty-state">
95
+ {filter === 'all' && 'No tasks yet. Add one above!'}
96
+ {filter === 'active' && 'All caught up! No active tasks.'}
97
+ {filter === 'completed' && 'No completed tasks yet.'}
98
+ </div>
99
+ ) : (
100
+ filteredTodos.map((todo) => (
101
+ <div key={todo.id} className="todo-item">
102
+ <button
103
+ className={`toggle ${todo.completed ? 'checked' : ''}`}
104
+ onClick={() => toggleTodo(todo.id, { completed: !todo.completed })}
105
+ >
106
+ {todo.completed ? '\u2713' : ''}
107
+ </button>
108
+ <span className={`title ${todo.completed ? 'done' : ''}`}>
109
+ {String(todo.title)}
110
+ </span>
111
+ {todo.createdAt && (
112
+ <span className="time">{formatTime(Number(todo.createdAt))}</span>
113
+ )}
114
+ <button
115
+ className="delete-btn"
116
+ onClick={() => deleteTodo(todo.id)}
117
+ >
118
+ \u00d7
119
+ </button>
120
+ </div>
121
+ ))
122
+ )}
123
+ </div>
124
+
125
+ {allTodos.length > 0 && (
126
+ <div className="footer">
127
+ <span>{activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left</span>
128
+ {completedTodos.length > 0 && (
129
+ <button onClick={clearCompleted}>Clear completed</button>
130
+ )}
131
+ </div>
132
+ )}
133
+
134
+ <p className="branding">Powered by Kora &mdash; offline-first, real-time sync</p>
46
135
  </div>
47
136
  )
48
137
  }
138
+
139
+ function formatTime(timestamp: number): string {
140
+ const date = new Date(timestamp)
141
+ const now = new Date()
142
+ if (date.toDateString() === now.toDateString()) {
143
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
144
+ }
145
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
146
+ }
@@ -0,0 +1,281 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ :root {
10
+ --color-bg: #0a0a0f;
11
+ --color-surface: #111118;
12
+ --color-border: #1e1e2a;
13
+ --color-border-hover: #2a2a3a;
14
+ --color-text: #e4e4e7;
15
+ --color-text-muted: #71717a;
16
+ --color-text-dim: #3f3f46;
17
+ --color-primary: #6366f1;
18
+ --color-primary-hover: #818cf8;
19
+ --color-success: #34d399;
20
+ --color-warning: #fbbf24;
21
+ --color-danger: #f87171;
22
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
23
+ --radius: 8px;
24
+ }
25
+
26
+ body {
27
+ font-family: var(--font-sans);
28
+ background: var(--color-bg);
29
+ color: var(--color-text);
30
+ line-height: 1.6;
31
+ -webkit-font-smoothing: antialiased;
32
+ }
33
+
34
+ .app {
35
+ max-width: 640px;
36
+ margin: 0 auto;
37
+ padding: 48px 16px;
38
+ }
39
+
40
+ .header {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ margin-bottom: 32px;
45
+ }
46
+
47
+ .header h1 {
48
+ font-size: 1.5rem;
49
+ font-weight: 700;
50
+ }
51
+
52
+ .local-badge {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 6px;
56
+ padding: 4px 12px;
57
+ border-radius: 999px;
58
+ background: var(--color-surface);
59
+ font-size: 0.8rem;
60
+ color: var(--color-text-muted);
61
+ }
62
+
63
+ .stats {
64
+ display: grid;
65
+ grid-template-columns: repeat(3, 1fr);
66
+ gap: 12px;
67
+ margin-bottom: 32px;
68
+ }
69
+
70
+ .stat-card {
71
+ background: var(--color-surface);
72
+ border: 1px solid var(--color-border);
73
+ border-radius: var(--radius);
74
+ padding: 16px;
75
+ }
76
+
77
+ .stat-card .label {
78
+ font-size: 0.8rem;
79
+ color: var(--color-text-muted);
80
+ }
81
+
82
+ .stat-card .value {
83
+ font-size: 1.5rem;
84
+ font-weight: 700;
85
+ }
86
+
87
+ .stat-card .value.muted { color: var(--color-text-muted); }
88
+ .stat-card .value.warning { color: var(--color-warning); }
89
+ .stat-card .value.success { color: var(--color-success); }
90
+
91
+ .add-form {
92
+ display: flex;
93
+ gap: 12px;
94
+ margin-bottom: 32px;
95
+ }
96
+
97
+ .add-form input {
98
+ flex: 1;
99
+ padding: 10px 16px;
100
+ border: 1px solid var(--color-border);
101
+ border-radius: var(--radius);
102
+ background: var(--color-surface);
103
+ color: var(--color-text);
104
+ font-size: 0.95rem;
105
+ outline: none;
106
+ }
107
+
108
+ .add-form input:focus {
109
+ border-color: var(--color-primary);
110
+ }
111
+
112
+ .add-form input::placeholder {
113
+ color: var(--color-text-muted);
114
+ }
115
+
116
+ .add-form button {
117
+ padding: 10px 20px;
118
+ border: none;
119
+ border-radius: var(--radius);
120
+ background: var(--color-primary);
121
+ color: white;
122
+ font-size: 0.95rem;
123
+ font-weight: 500;
124
+ cursor: pointer;
125
+ }
126
+
127
+ .add-form button:hover {
128
+ background: var(--color-primary-hover);
129
+ }
130
+
131
+ .add-form button:disabled {
132
+ opacity: 0.5;
133
+ cursor: not-allowed;
134
+ }
135
+
136
+ .filters {
137
+ display: flex;
138
+ gap: 8px;
139
+ margin-bottom: 24px;
140
+ }
141
+
142
+ .filter-btn {
143
+ padding: 6px 16px;
144
+ border: none;
145
+ border-radius: var(--radius);
146
+ background: var(--color-surface);
147
+ color: var(--color-text-muted);
148
+ font-size: 0.85rem;
149
+ font-weight: 500;
150
+ cursor: pointer;
151
+ }
152
+
153
+ .filter-btn:hover {
154
+ background: var(--color-border-hover);
155
+ color: var(--color-text);
156
+ }
157
+
158
+ .filter-btn.active {
159
+ background: var(--color-primary);
160
+ color: white;
161
+ }
162
+
163
+ .filter-btn .badge {
164
+ display: inline-block;
165
+ margin-left: 6px;
166
+ padding: 1px 6px;
167
+ border-radius: 999px;
168
+ font-size: 0.75rem;
169
+ background: rgba(255, 255, 255, 0.15);
170
+ }
171
+
172
+ .filter-btn:not(.active) .badge {
173
+ background: var(--color-border);
174
+ }
175
+
176
+ .todo-list {
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: 8px;
180
+ }
181
+
182
+ .todo-item {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 12px;
186
+ padding: 12px 16px;
187
+ border: 1px solid var(--color-border);
188
+ border-radius: var(--radius);
189
+ background: var(--color-surface);
190
+ }
191
+
192
+ .todo-item:hover {
193
+ border-color: var(--color-border-hover);
194
+ }
195
+
196
+ .todo-item .toggle {
197
+ background: none;
198
+ border: 2px solid var(--color-text-muted);
199
+ width: 20px;
200
+ height: 20px;
201
+ border-radius: 50%;
202
+ cursor: pointer;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ color: transparent;
207
+ font-size: 0.7rem;
208
+ flex-shrink: 0;
209
+ }
210
+
211
+ .todo-item .toggle.checked {
212
+ border-color: var(--color-success);
213
+ background: var(--color-success);
214
+ color: white;
215
+ }
216
+
217
+ .todo-item .title {
218
+ flex: 1;
219
+ }
220
+
221
+ .todo-item .title.done {
222
+ text-decoration: line-through;
223
+ color: var(--color-text-muted);
224
+ }
225
+
226
+ .todo-item .time {
227
+ font-size: 0.75rem;
228
+ color: var(--color-text-dim);
229
+ }
230
+
231
+ .todo-item .delete-btn {
232
+ background: none;
233
+ border: none;
234
+ color: var(--color-text-dim);
235
+ cursor: pointer;
236
+ padding: 4px;
237
+ opacity: 0;
238
+ transition: opacity 0.15s;
239
+ }
240
+
241
+ .todo-item:hover .delete-btn {
242
+ opacity: 1;
243
+ }
244
+
245
+ .todo-item .delete-btn:hover {
246
+ color: var(--color-danger);
247
+ }
248
+
249
+ .empty-state {
250
+ padding: 48px 0;
251
+ text-align: center;
252
+ color: var(--color-text-dim);
253
+ border: 1px dashed var(--color-border);
254
+ border-radius: var(--radius);
255
+ }
256
+
257
+ .footer {
258
+ display: flex;
259
+ justify-content: space-between;
260
+ margin-top: 24px;
261
+ font-size: 0.85rem;
262
+ color: var(--color-text-muted);
263
+ }
264
+
265
+ .footer button {
266
+ background: none;
267
+ border: none;
268
+ color: var(--color-text-muted);
269
+ cursor: pointer;
270
+ }
271
+
272
+ .footer button:hover {
273
+ color: var(--color-text);
274
+ }
275
+
276
+ .branding {
277
+ margin-top: 48px;
278
+ text-align: center;
279
+ font-size: 0.75rem;
280
+ color: var(--color-text-dim);
281
+ }
@@ -4,12 +4,14 @@ import { StrictMode } from 'react'
4
4
  import { createRoot } from 'react-dom/client'
5
5
  import schema from './schema'
6
6
  import { App } from './App'
7
+ import './index.css'
7
8
 
8
9
  const app = createApp({
9
10
  schema,
10
11
  store: {
11
12
  workerUrl: new URL('./kora-worker.ts', import.meta.url),
12
13
  },
14
+ devtools: true,
13
15
  })
14
16
 
15
17
  createRoot(document.getElementById('root')!).render(
@@ -1,9 +1,39 @@
1
1
  import react from '@vitejs/plugin-react'
2
+ import type { Plugin } from 'vite'
2
3
  import { defineConfig } from 'vite'
3
4
 
5
+ /**
6
+ * Adds Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers
7
+ * required for SharedArrayBuffer. Without these headers, OPFS SAH Pool VFS
8
+ * cannot be used and SQLite WASM falls back to in-memory storage (no persistence).
9
+ */
10
+ function crossOriginIsolation(): Plugin {
11
+ return {
12
+ name: 'cross-origin-isolation',
13
+ configureServer(server) {
14
+ server.middlewares.use((_req, res, next) => {
15
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
16
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
17
+ next()
18
+ })
19
+ },
20
+ configurePreviewServer(server) {
21
+ server.middlewares.use((_req, res, next) => {
22
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
23
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
24
+ next()
25
+ })
26
+ },
27
+ }
28
+ }
29
+
4
30
  export default defineConfig({
5
- plugins: [react()],
31
+ plugins: [react(), crossOriginIsolation()],
6
32
  optimizeDeps: {
7
33
  exclude: ['@sqlite.org/sqlite-wasm', '@korajs/store'],
34
+ include: ['yjs'],
35
+ },
36
+ resolve: {
37
+ dedupe: ['yjs'],
8
38
  },
9
39
  })
@@ -7,7 +7,7 @@ export default defineConfig({
7
7
  sync: {
8
8
  enabled: true,
9
9
  port: 3001,
10
- store: 'memory',
10
+ store: 'sqlite',
11
11
  },
12
12
  watch: {
13
13
  enabled: true,
@@ -1,6 +1,17 @@
1
- import { createKoraServer, MemoryServerStore } from '@korajs/server'
1
+ import { createKoraServer, createSqliteServerStore } from '@korajs/server'
2
+
3
+ // SQLite persists data to disk — survives server restarts
4
+ const store = createSqliteServerStore({ filename: './kora-server.db' })
5
+
6
+ // To use PostgreSQL instead:
7
+ // 1. Install: pnpm add postgres
8
+ // 2. Replace the store above with:
9
+ //
10
+ // import { createPostgresServerStore } from '@korajs/server'
11
+ // const store = await createPostgresServerStore({
12
+ // connectionString: 'postgresql://user:password@localhost:5432/mydb',
13
+ // })
2
14
 
3
- const store = new MemoryServerStore()
4
15
  const server = createKoraServer({
5
16
  store,
6
17
  port: 3001,