create-tinybase 0.2.5 → 0.3.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 (80) hide show
  1. package/README.md +62 -18
  2. package/cli.js +2 -2
  3. package/package.json +3 -3
  4. package/screenshots/chat.png +0 -0
  5. package/screenshots/drawing.png +0 -0
  6. package/screenshots/todos.png +0 -0
  7. package/templates/README.md.hbs +13 -12
  8. package/templates/client/eslint.config.js.hbs +7 -0
  9. package/templates/client/package.json.hbs +28 -8
  10. package/templates/client/src/chat/App.tsx.hbs +28 -22
  11. package/templates/client/src/chat/ChatStore.tsx.hbs +108 -27
  12. package/templates/client/src/chat/Message.tsx.hbs +8 -9
  13. package/templates/client/src/chat/MessageInput.tsx.hbs +7 -7
  14. package/templates/client/src/chat/Messages.tsx.hbs +5 -15
  15. package/templates/client/src/chat/SettingsStore.tsx.hbs +86 -9
  16. package/templates/client/src/chat/UsernameInput.tsx.hbs +6 -11
  17. package/templates/client/src/chat/app.ts.hbs +18 -18
  18. package/templates/client/src/chat/chatStore.ts.hbs +97 -35
  19. package/templates/client/src/chat/message.ts.hbs +4 -3
  20. package/templates/client/src/chat/messageInput.css.hbs +1 -1
  21. package/templates/client/src/chat/messageInput.ts.hbs +7 -8
  22. package/templates/client/src/chat/messages.ts.hbs +7 -12
  23. package/templates/client/src/chat/settingsStore.ts.hbs +65 -6
  24. package/templates/client/src/chat/usernameInput.css.hbs +1 -1
  25. package/templates/client/src/chat/usernameInput.ts.hbs +6 -6
  26. package/templates/client/src/chat/utils.ts.hbs +26 -0
  27. package/templates/client/src/drawing/App.tsx.hbs +26 -20
  28. package/templates/client/src/drawing/BrushSize.tsx.hbs +8 -11
  29. package/templates/client/src/drawing/Canvas.tsx.hbs +65 -73
  30. package/templates/client/src/drawing/CanvasStore.tsx.hbs +104 -18
  31. package/templates/client/src/drawing/ColorPicker.tsx.hbs +4 -11
  32. package/templates/client/src/drawing/DrawingControls.tsx.hbs +7 -7
  33. package/templates/client/src/drawing/SettingsStore.tsx.hbs +81 -8
  34. package/templates/client/src/drawing/app.ts.hbs +18 -8
  35. package/templates/client/src/drawing/brushSize.ts.hbs +12 -5
  36. package/templates/client/src/drawing/canvas.ts.hbs +84 -86
  37. package/templates/client/src/drawing/canvasStore.ts.hbs +93 -26
  38. package/templates/client/src/drawing/colorPicker.ts.hbs +3 -3
  39. package/templates/client/src/drawing/drawingControls.ts.hbs +7 -7
  40. package/templates/client/src/drawing/settingsStore.ts.hbs +63 -8
  41. package/templates/client/src/game/App.tsx.hbs +20 -16
  42. package/templates/client/src/game/Board.tsx.hbs +8 -8
  43. package/templates/client/src/game/Game.tsx.hbs +14 -21
  44. package/templates/client/src/game/GameStatus.tsx.hbs +5 -5
  45. package/templates/client/src/game/Square.tsx.hbs +6 -11
  46. package/templates/client/src/game/Store.tsx.hbs +106 -16
  47. package/templates/client/src/game/app.ts.hbs +17 -6
  48. package/templates/client/src/game/board.ts.hbs +7 -7
  49. package/templates/client/src/game/game.ts.hbs +12 -18
  50. package/templates/client/src/game/gameStatus.ts.hbs +3 -3
  51. package/templates/client/src/game/square.ts.hbs +4 -4
  52. package/templates/client/src/game/store.ts.hbs +95 -23
  53. package/templates/client/src/index.tsx.hbs +5 -7
  54. package/templates/client/src/shared/Button.tsx.hbs +3 -3
  55. package/templates/client/src/shared/Input.tsx.hbs +2 -2
  56. package/templates/client/src/shared/Loading.tsx.hbs +5 -0
  57. package/templates/client/src/shared/button.ts.hbs +2 -2
  58. package/templates/client/src/shared/config.ts.hbs +4 -6
  59. package/templates/client/src/shared/input.ts.hbs +2 -2
  60. package/templates/client/src/shared/loading.css.hbs +21 -0
  61. package/templates/client/src/shared/loading.ts.hbs +13 -0
  62. package/templates/client/src/shared/pglite.ts.hbs +10 -0
  63. package/templates/client/src/shared/sqlite.ts.hbs +17 -0
  64. package/templates/client/src/todos/App.tsx.hbs +22 -22
  65. package/templates/client/src/todos/Store.tsx.hbs +106 -23
  66. package/templates/client/src/todos/TodoInput.tsx.hbs +6 -8
  67. package/templates/client/src/todos/TodoItem.tsx.hbs +5 -6
  68. package/templates/client/src/todos/TodoList.tsx.hbs +5 -6
  69. package/templates/client/src/todos/app.ts.hbs +16 -10
  70. package/templates/client/src/todos/store.ts.hbs +94 -30
  71. package/templates/client/src/todos/todoInput.ts.hbs +5 -8
  72. package/templates/client/src/todos/todoItem.ts.hbs +3 -4
  73. package/templates/client/src/todos/todoList.ts.hbs +6 -8
  74. package/templates/client/vite-env.d.ts.hbs +4 -0
  75. package/templates/client/vite.config.js.hbs +53 -3
  76. package/templates/server/index-do.ts.hbs +10 -14
  77. package/templates/server/index-node.ts.hbs +3 -3
  78. package/templates/server/package.json.hbs +5 -5
  79. package/templates/server/wrangler.toml.hbs +1 -1
  80. /package/templates/client/{.prettierrc.hbs → .prettierrc.json.hbs} +0 -0
package/README.md CHANGED
@@ -22,8 +22,8 @@ This will prompt you with questions to configure your new TinyBase app:
22
22
  - **Framework** - React or Vanilla JS
23
23
  - **App type** - Todo app, Chat app, Drawing app, or Tic-tac-toe game
24
24
  - **Store schemas** - TypeScript type safety for stores (TypeScript only)
25
- - **Synchronization** - Enable real-time sync between clients
26
- - **Server code** - Include Node.js or Cloudflare Durable Objects server
25
+ - **Synchronization** - None, remote demo server, local Node server, or local Durable Objects server
26
+ - **Persistence** - None, Local Storage, SQLite, or PGLite
27
27
  - **Prettier** - Include Prettier for code formatting
28
28
  - **ESLint** - Include ESLint for code linting
29
29
 
@@ -146,44 +146,88 @@ Schemas define:
146
146
 
147
147
  ### Synchronization
148
148
 
149
- **Enabled** (default):
149
+ Choose from four synchronization options:
150
150
 
151
- - Real-time sync between browser tabs
152
- - WebSocket-based synchronization
153
- - Connects to demo server by default (wss://vite.tinybase.org)
154
- - MergeableStore for conflict-free replication
155
- - Automatic reconnection handling
156
-
157
- **Disabled**:
151
+ **None**:
158
152
 
159
153
  - Local-only data storage
160
154
  - No network dependencies
161
155
  - Simpler architecture
162
156
  - Still uses MergeableStore for consistency
163
157
 
164
- ### Server Code
158
+ **Via remote demo server (stateless)** (default):
165
159
 
166
- When synchronization is enabled, you can include server code:
160
+ - Real-time sync between browser tabs
161
+ - WebSocket-based synchronization
162
+ - Connects to demo server (wss://vite.tinybase.org)
163
+ - MergeableStore for conflict-free replication
164
+ - Automatic reconnection handling
165
+ - Great for prototyping
166
+ - No local server management needed
167
167
 
168
- **Node.js Server** (port 8043):
168
+ **Via local node server (stateless)** (port 8043):
169
169
 
170
+ - Real-time sync with local Node.js server
170
171
  - WebSocket server using `ws` library
171
172
  - TinyBase server synchronizer
172
173
  - Runs with `npm run dev` in server directory
174
+ - Stateless - no data persistence
173
175
  - Easy to deploy to any Node.js host
176
+ - Full control over server code
174
177
 
175
- **Cloudflare Durable Objects** (port 8787):
178
+ **Via local DurableObjects server (stateful)** (port 8787):
176
179
 
180
+ - Real-time sync with Cloudflare Durable Objects
177
181
  - Serverless WebSocket server
178
182
  - Edge computing with Durable Objects
183
+ - Stateful - data persists in Durable Objects storage
179
184
  - Global distribution
180
185
  - Runs locally with Wrangler
186
+ - Designed for production deployment
181
187
 
182
- **No Server**:
188
+ ### Persistence
183
189
 
184
- - Connects to public demo server
185
- - Great for prototyping
186
- - No local server management needed
190
+ Choose how to persist store data on the client:
191
+
192
+ **None**:
193
+
194
+ - No client-side persistence
195
+ - Data lost on page refresh
196
+ - Simplest option for demos
197
+ - Still works with synchronization
198
+
199
+ **Local Storage** (default):
200
+
201
+ - Browser localStorage persistence
202
+ - Data persists across sessions
203
+ - Simple and widely supported
204
+ - Automatic load/save with `createLocalPersister`
205
+ - Best for most use cases
206
+
207
+ **SQLite**:
208
+
209
+ - Browser-based SQLite via WebAssembly
210
+ - Structured database storage
211
+ - Uses `@sqlite.org/sqlite-wasm` package
212
+ - IndexedDB-backed (`:local:` prefix)
213
+ - Good for complex data structures
214
+ - Slightly larger bundle size
215
+
216
+ **PGLite**:
217
+
218
+ - PostgreSQL in the browser
219
+ - Full PostgreSQL compatibility
220
+ - Uses `@electric-sql/pglite` package
221
+ - IndexedDB-backed (`idb://` prefix)
222
+ - Advanced SQL features
223
+ - Larger bundle size
224
+
225
+ **Important Notes:**
226
+
227
+ - Persistence is configured **before** synchronization to avoid race conditions
228
+ - Works with all sync types (or no sync)
229
+ - In multi-store apps, all stores get persistence
230
+ - Data is automatically loaded on app start and saved on changes
187
231
 
188
232
  ### Prettier & ESLint
189
233
 
package/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{existsSync as y}from"fs";import{dirname as w,join as u}from"path";import{createCLI as f,detectPackageManager as h}from"tinycreate";import{fileURLToPath as k}from"url";const j=w(k(import.meta.url)),x={welcomeMessage:`\u{1F389} Welcome to TinyBase!
3
- `,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinybase-app",validate:e=>{if(e.length===0)return"Project name is required";const t=u(process.cwd(),e);return y(t)?`Directory "${e}" already exists. Please choose a different name.`:!0}},{type:"select",name:"appType",message:"App type:",choices:[{title:"Todo app",value:"todos"},{title:"Chat app",value:"chat"},{title:"Drawing app",value:"drawing"},{title:"Tic-tac-toe game",value:"game"}],initial:0},{type:"select",name:"language",message:"Language:",choices:[{title:"TypeScript",value:"typescript"},{title:"JavaScript",value:"javascript"}],initial:0},{type:"select",name:"framework",message:"Framework:",choices:[{title:"React",value:"react"},{title:"Vanilla",value:"vanilla"}],initial:0},{type:(e,t)=>t.language==="typescript"?"confirm":null,name:"schemas",message:"Include store schemas?",initial:!1},{type:"confirm",name:"sync",message:"Enable synchronization?",initial:!0},{type:(e,t)=>t.sync?"confirm":null,name:"server",message:"Add code for server?",initial:!1},{type:(e,t)=>t.server?"select":null,name:"serverType",message:"Server type:",choices:[{title:"Node",value:"node"},{title:"Durable Objects",value:"durable-objects"}],initial:0},{type:"confirm",name:"prettier",message:"Include Prettier?",initial:!0},{type:"confirm",name:"eslint",message:"Include ESLint?",initial:!0},{type:(e,t)=>t.server?null:"confirm",name:"installAndRun",message:"Install dependencies and start dev server?",initial:!0}],createContext:e=>{const{projectName:t,language:s,framework:n,appType:r,prettier:m,eslint:g,schemas:i,sync:c,server:l,serverType:d,installAndRun:p}=e,a=s==="typescript",v=!a,o=n==="react";return{projectName:t,language:s,framework:n,appType:r,prettier:m,eslint:g,schemas:a&&(i===!0||i==="true"),sync:c===!0||c==="true",server:l===!0||l==="true",serverType:d||"node",installAndRun:p===!0||p==="true",typescript:a,javascript:v,react:o,ext:a?o?"tsx":"ts":o?"jsx":"js"}},createDirectories:async(e,t)=>{const{mkdir:s}=await import("fs/promises"),{join:n}=await import("path"),r=t.server;await s(n(e,"client/src"),{recursive:!0}),await s(n(e,"client/public"),{recursive:!0}),r&&await s(n(e,"server"),{recursive:!0})},getFiles:()=>[{template:"README.md.hbs",output:"README.md",prettier:!0}],processIncludedFile:(e,t)=>{const{javascript:s}=t,n=e.prettier??/\.(js|jsx|ts|tsx|css|json|html|md)$/.test(e.output),r=e.transpile??(/\.(ts|tsx)\.hbs$/.test(e.template)&&s===!0);return{...e,prettier:n,transpile:r}},templateRoot:u(j,"templates"),installCommand:"{pm} install",devCommand:"{pm} run dev",workingDirectory:"client",onSuccess:(e,t)=>{const s=t.server,n=h();console.log("Next steps:"),console.log(),s&&(console.log("To run the server:"),console.log(` cd ${e}/server`),console.log(` ${n} install`),console.log(` ${n} run dev`),console.log()),console.log("To run the client:"),console.log(` cd ${e}/client`),console.log(` ${n} install`),console.log(` ${n} run dev`)}};f(x).catch(e=>{console.error(e),process.exit(1)});
2
+ import{existsSync as w}from"fs";import{dirname as f,join as u}from"path";import{createCLI as k,detectPackageManager as x}from"tinycreate";import{fileURLToPath as R}from"url";const S=f(R(import.meta.url)),P={welcomeMessage:`\u{1F389} Welcome to TinyBase!
3
+ `,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinybase-app",validate:e=>{if(e.length===0)return"Project name is required";const t=u(process.cwd(),e);return w(t)?`Directory "${e}" already exists. Please choose a different name.`:!0}},{type:"select",name:"appType",message:"App type:",choices:[{title:"Todo app",value:"todos"},{title:"Chat app",value:"chat"},{title:"Drawing app",value:"drawing"},{title:"Tic-tac-toe game",value:"game"}],initial:0},{type:"select",name:"language",message:"Language:",choices:[{title:"TypeScript",value:"typescript"},{title:"JavaScript",value:"javascript"}],initial:0},{type:"select",name:"framework",message:"Framework:",choices:[{title:"React",value:"react"},{title:"Vanilla",value:"vanilla"}],initial:0},{type:(e,t)=>t.language==="typescript"?"confirm":null,name:"schemas",message:"Include store schemas?",initial:!1},{type:"select",name:"syncType",message:"Synchronization:",choices:[{title:"None",value:"none"},{title:"Via remote demo server (stateless)",value:"remote"},{title:"Via local node server (stateless)",value:"node"},{title:"Via local DurableObjects server (stateful)",value:"durable-objects"}],initial:1},{type:"select",name:"persistenceType",message:"Persistence:",choices:[{title:"None",value:"none"},{title:"Local Storage",value:"local-storage"},{title:"SQLite",value:"sqlite"},{title:"PGlite",value:"pglite"}],initial:1},{type:"confirm",name:"prettier",message:"Include Prettier?",initial:!0},{type:"confirm",name:"eslint",message:"Include ESLint?",initial:!0},{type:(e,t)=>t.syncType!=="node"&&t.syncType!=="durable-objects"?"confirm":null,name:"installAndRun",message:"Install dependencies and start dev server?",initial:!0}],createContext:e=>{const{projectName:t,language:s,framework:n,appType:o,prettier:m,eslint:g,schemas:l,syncType:d,persistenceType:y,installAndRun:p}=e,i=s==="typescript",v=!i,c=n==="react",T=i?c?"tsx":"ts":c?"jsx":"js",a=d||"remote",h=a!=="none",b=a==="node"||a==="durable-objects",j=a==="durable-objects"?"durable-objects":"node",r=y||"local-storage";return{projectName:t,language:s,framework:n,appType:o,prettier:m,eslint:g,schemas:i&&(l===!0||l==="true"),syncType:a,sync:h,server:b,serverType:j,persistenceType:r,persist:r!=="none",persistLocalStorage:r==="local-storage",persistSqlite:r==="sqlite",persistPglite:r==="pglite",installAndRun:p===!0||p==="true",typescript:i,javascript:v,react:c,ext:T}},createDirectories:async(e,t)=>{const{mkdir:s}=await import("fs/promises"),{join:n}=await import("path"),o=t.server;await s(n(e,"client/src"),{recursive:!0}),await s(n(e,"client/public"),{recursive:!0}),o&&await s(n(e,"server"),{recursive:!0})},getFiles:()=>[{template:"README.md.hbs",output:"README.md",prettier:!0}],processIncludedFile:(e,t)=>{const{javascript:s}=t,n=e.prettier??/\.(js|jsx|ts|tsx|css|json|html|md)$/.test(e.output),o=e.transpile??(/\.(ts|tsx)\.hbs$/.test(e.template)&&s===!0);return{...e,prettier:n,transpile:o}},templateRoot:u(S,"templates"),installCommand:"{pm} install",devCommand:"{pm} run dev",workingDirectory:"client",onSuccess:(e,t)=>{const s=t.syncType,n=s==="node"||s==="durable-objects",o=x();console.log("Next steps:"),console.log(),n&&(console.log("To run the server:"),console.log(` cd ${e}/server`),console.log(` ${o} install`),console.log(` ${o} run dev`),console.log()),console.log("To run the client:"),console.log(` cd ${e}/client`),console.log(` ${o} install`),console.log(` ${o} run dev`)}};k(P).catch(e=>{console.error(e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-tinybase",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "author": "jamesgpearce",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "license": "MIT",
10
10
  "homepage": "https://tinybase.org",
11
- "description": "The CLI to build a new app using TinyBase, a reactive data store and sync engine.",
11
+ "description": "The CLI to build new apps using TinyBase, a reactive data store and sync engine.",
12
12
  "keywords": [
13
13
  "tiny",
14
14
  "sync engine",
@@ -23,7 +23,7 @@
23
23
  "create-tinybase": "cli.js"
24
24
  },
25
25
  "dependencies": {
26
- "tinycreate": "^1.0.3"
26
+ "tinycreate": "^1.0.4"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=18.0.0"
Binary file
Binary file
Binary file
@@ -5,13 +5,12 @@
5
5
 
6
6
  # {{projectName}}
7
7
 
8
- A TinyBase app built with {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}
9
- and {{#if react}}React{{else}}Vanilla JS{{/if}}.
8
+ A TinyBase app built with {{#if typescript}}TypeScript{{else}}JavaScript{{/if}} and {{#if react}}React{{else}}Vanilla JS{{/if}}.
10
9
 
11
10
  ## Getting Started
12
11
 
13
- Alongside this file, you will see a directory called `client` (and optionally
14
- one called `server` if you included server code).
12
+ Alongside this file, you will see a directory called `client`{{#if server}} (and optionally
13
+ one called `server` for your {{#if (eq syncType "node")}}local Node.js server{{else if (eq syncType "durable-objects")}}local Cloudflare Durable Objects server{{/if}}){{/if}}.
15
14
 
16
15
  To start the web client with Vite, run the following commands:
17
16
 
@@ -24,16 +23,18 @@ npm run dev
24
23
  PNPM, Yarn, and Bun should also work. Your app will be available at
25
24
  `http://localhost:5173`, or whichever port Vite specifies in the console.
26
25
 
27
- If you included server code, start the server in a separate terminal, ideally
28
- before starting the client:
26
+ {{#if server}}
27
+ If you included server code, start the server in a separate terminal, ideally
28
+ before starting the client:
29
29
 
30
- ```bash
31
- # In a separate terminal
32
- cd server
33
- npm install
34
- npm run dev
35
- ```
30
+ ```bash
31
+ # In a separate terminal
32
+ cd server
33
+ npm install
34
+ npm run dev
35
+ ```
36
36
 
37
+ {{/if}}
37
38
  After that, start hacking!
38
39
 
39
40
  ## Learn More
@@ -5,13 +5,20 @@
5
5
  {{addImport "import pluginReact from 'eslint-plugin-react';"}}
6
6
  {{addImport "import pluginReactHooks from 'eslint-plugin-react-hooks';"}}
7
7
  {{/if}}
8
+ {{addImport "import globals from 'globals';"}}
8
9
 
9
10
  export default [
11
+ {
12
+ languageOptions: {
13
+ globals: globals.browser,
14
+ },
15
+ },
10
16
  {{#if typescript}}
11
17
  ...tseslint.configs.recommended,
12
18
  {
13
19
  rules: {
14
20
  '@typescript-eslint/no-unused-vars': 'warn',
21
+ '@typescript-eslint/no-explicit-any': 'warn',
15
22
  },
16
23
  },
17
24
  {{else}}
@@ -22,25 +22,31 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  {{#list}}
25
- "vite": "^7.1.3"
25
+ "vite": "^7.3.1"
26
26
  {{#if typescript}}
27
27
  "typescript": "^5.9.3"
28
- "@types/node": "^25.0.3"
28
+ "@types/node": "^25.0.9"
29
29
  {{#if react}}
30
- "@types/react": "^19.2.7"
30
+ "@types/react": "^19.2.8"
31
31
  "@types/react-dom": "^19.2.3"
32
32
  {{/if}}
33
33
  {{/if}}
34
34
  {{#if react}}
35
- "@vitejs/plugin-react": "^4.3.4"
35
+ "@vitejs/plugin-react": "^5.1.2"
36
+ {{/if}}
37
+ {{#if persistSqlite}}
38
+ "vite-plugin-static-copy": "^3.2.0"
39
+ {{/if}}
40
+ {{#if persistPglite}}
41
+ "vite-plugin-static-copy": "^3.2.0"
36
42
  {{/if}}
37
43
  {{#if prettier}}
38
- "prettier": "^3.7.4"
44
+ "prettier": "^3.8.0"
39
45
  {{/if}}
40
46
  {{#if eslint}}
41
47
  "eslint": "^9.39.2"
42
48
  {{#if typescript}}
43
- "typescript-eslint": "^8.51.0"
49
+ "typescript-eslint": "^8.53.1"
44
50
  {{/if}}
45
51
  {{#if react}}
46
52
  "eslint-plugin-react": "7.37.5"
@@ -51,7 +57,7 @@
51
57
  },
52
58
  "dependencies": {
53
59
  {{#list}}
54
- "tinybase": "^7.3.1"
60
+ "tinybase": "^7.3.2"
55
61
  {{#if react}}
56
62
  "react": "^19.2.3"
57
63
  "react-dom": "^19.2.3"
@@ -59,18 +65,32 @@
59
65
  {{#if sync}}
60
66
  "reconnecting-websocket": "^4.4.0"
61
67
  {{/if}}
68
+ {{#if persistSqlite}}
69
+ "@sqlite.org/sqlite-wasm": "^3.50.4-build1"
70
+ {{/if}}
71
+ {{#if persistPglite}}
72
+ "@electric-sql/pglite": "^0.3.15"
73
+ {{/if}}
62
74
  {{/list}}
63
75
  }
64
76
  }
65
77
  {{#if prettier}}
66
- {{includeFile template="client/.prettierrc.hbs" output="client/.prettierrc"}}
78
+ {{includeFile template="client/.prettierrc.json.hbs" output="client/.prettierrc.json"}}
67
79
  {{/if}}
68
80
  {{#if eslint}}
69
81
  {{includeFile template="client/eslint.config.js.hbs" output="client/eslint.config.js"}}
70
82
  {{/if}}
71
83
  {{#if react}}
72
84
  {{includeFile template="client/vite.config.js.hbs" output="client/vite.config.js"}}
85
+ {{else}}
86
+ {{#if persistSqlite}}
87
+ {{includeFile template="client/vite.config.js.hbs" output="client/vite.config.js"}}
88
+ {{/if}}
89
+ {{#if persistPglite}}
90
+ {{includeFile template="client/vite.config.js.hbs" output="client/vite.config.js"}}
91
+ {{/if}}
73
92
  {{/if}}
74
93
  {{#if typescript}}
75
94
  {{includeFile template="client/tsconfig.json.hbs" output="client/tsconfig.json"}}
95
+ {{includeFile template="client/vite-env.d.ts.hbs" output="client/src/vite-env.d.ts"}}
76
96
  {{/if}}
@@ -1,40 +1,46 @@
1
- import {StrictMode} from 'react';
1
+ {{addImport "import {StrictMode, useState} from 'react';"}}
2
+ {{includeFile template="client/src/shared/Loading.tsx.hbs" output="client/src/Loading.{{ext}}"}}
3
+ {{addImport "import {Loading} from './Loading';"}}
2
4
 
3
- {{#if schemas}}
4
- import {Provider} from 'tinybase/ui-react/with-schemas';
5
- {{else}}
6
- import {Provider} from 'tinybase/ui-react';
7
- {{/if}}
8
- import {Inspector} from 'tinybase/ui-react-inspector';
5
+ {{addImport "import {Provider} from 'tinybase/ui-react';"}}
6
+ {{addImport "import {Inspector} from 'tinybase/ui-react-inspector';"}}
9
7
 
10
8
  {{includeFile template="client/src/chat/SettingsStore.tsx.hbs" output="client/src/SettingsStore.{{ext}}"}}
11
- import {SettingsStore} from './SettingsStore';
9
+ {{addImport "import {SettingsStore} from './SettingsStore';"}}
12
10
 
13
11
  {{includeFile template="client/src/chat/ChatStore.tsx.hbs" output="client/src/ChatStore.{{ext}}"}}
14
- import {ChatStore} from './ChatStore';
12
+ {{addImport "import {ChatStore} from './ChatStore';"}}
15
13
 
16
14
  {{includeFile template="client/src/chat/UsernameInput.tsx.hbs" output="client/src/UsernameInput.{{ext}}"}}
17
- import {UsernameInput} from './UsernameInput';
15
+ {{addImport "import {UsernameInput} from './UsernameInput';"}}
18
16
 
19
17
  {{includeFile template="client/src/chat/Messages.tsx.hbs" output="client/src/Messages.{{ext}}"}}
20
- import {Messages} from './Messages';
18
+ {{addImport "import {Messages} from './Messages';"}}
21
19
 
22
20
  {{includeFile template="client/src/chat/MessageInput.tsx.hbs" output="client/src/MessageInput.{{ext}}"}}
23
- import {MessageInput} from './MessageInput';
21
+ {{addImport "import {MessageInput} from './MessageInput';"}}
22
+
23
+ export const App = () => {
24
+ const [settingsReady, setSettingsReady] = useState(false);
25
+ const [chatReady, setChatReady] = useState(false);
26
+ const loading = !settingsReady || !chatReady;
24
27
 
25
- const App = () => {
26
28
  return (
27
29
  <StrictMode>
28
30
  <Provider>
29
- <SettingsStore />
30
- <ChatStore />
31
- <UsernameInput />
32
- <Messages />
33
- <MessageInput />
34
- <Inspector />
31
+ <SettingsStore onReady={()=> setSettingsReady(true)} />
32
+ <ChatStore onReady={()=> setChatReady(true)} />
33
+ {loading ? (
34
+ <Loading />
35
+ ) : (
36
+ <>
37
+ <UsernameInput />
38
+ <Messages />
39
+ <MessageInput />
40
+ <Inspector />
41
+ </>
42
+ )}
35
43
  </Provider>
36
44
  </StrictMode>
37
45
  );
38
- };
39
-
40
- export {App};
46
+ };
@@ -1,56 +1,130 @@
1
1
  {{#if schemas}}
2
- import {createMergeableStore} from 'tinybase/with-schemas';
3
- import * as UiReact from 'tinybase/ui-react/with-schemas';
4
- import {type NoValuesSchema} from 'tinybase/with-schemas';
2
+ {{addImport "import {createMergeableStore, type Row} from 'tinybase/with-schemas';"}}
3
+ {{addImport "import * as UiReact from 'tinybase/ui-react/with-schemas';"}}
4
+ {{addImport "import {type NoValuesSchema} from 'tinybase/with-schemas';"}}
5
5
  {{else}}
6
- import {createMergeableStore} from 'tinybase';
7
- import {useCreateStore, useProvideStore, useAddRowCallback, useRow, useRowIds, useStore} from 'tinybase/ui-react';
6
+ {{addImport "import {createMergeableStore} from 'tinybase';"}}
7
+ {{addImport "import {useCreateMergeableStore, useProvideStore, useAddRowCallback, useRow, useSortedRowIds, useStore} from 'tinybase/ui-react';"}}
8
8
  {{/if}}
9
9
 
10
+ {{includeFile template="client/src/chat/utils.ts.hbs" output="client/src/utils.{{ext}}"}}
11
+ {{addImport "import {getDefaultContent} from './utils';"}}
12
+
10
13
  export const STORE_ID = 'chat';
11
14
 
12
15
  {{#if schemas}}
13
16
  const TABLES_SCHEMA = {
14
17
  messages: {
15
- username: {type: 'string'},
16
- text: {type: 'string'},
17
- timestamp: {type: 'number'},
18
+ username: {type: 'string', default: ''},
19
+ text: {type: 'string', default: ''},
20
+ timestamp: {type: 'number', default: 0},
18
21
  },
19
22
  } as const;
20
23
 
21
24
  type Schemas = [typeof TABLES_SCHEMA, NoValuesSchema];
22
25
 
23
- const {useCreateStore, useProvideStore, useAddRowCallback, useRow, useRowIds, useStore} = UiReact as UiReact.WithSchemas<Schemas>;
26
+ const {useAddRowCallback, useCreateMergeableStore, {{#if persist}}useCreatePersister, {{/if}}useProvideStore, useRow, useSortedRowIds, useStore} = UiReact as UiReact.WithSchemas<Schemas>;
24
27
  {{/if}}
25
28
 
26
- export {useAddRowCallback, useRow, useRowIds, useStore};
29
+ export type MessageRow = {{#if schemas}}Row<typeof TABLES_SCHEMA, 'messages'>{{else}}{username: string; text: string; timestamp: number}{{/if}};
30
+
31
+ export {useAddRowCallback, useRow, useSortedRowIds, useStore};
27
32
 
28
- export const ChatStore = () => {
29
- const store = useCreateStore(() =>
30
- createMergeableStore(STORE_ID){{#if schemas}}
33
+ export const ChatStore = ({onReady}: {onReady?: () => void}) => {
34
+ const store = useCreateMergeableStore(() =>
35
+ createMergeableStore(){{#if schemas}}
31
36
  .setTablesSchema(TABLES_SCHEMA){{/if}}
32
- .setDefaultContent([
33
- {
34
- messages: {
35
- '1': {username: 'Alice', text: 'Hello!', timestamp: Date.now() - 60000},
36
- '2': {username: 'Bob', text: 'Hi there!', timestamp: Date.now() - 30000},
37
- },
38
- },
39
- {},
40
- ]),
37
+ .setDefaultContent(getDefaultContent()),
41
38
  );
42
39
 
43
40
  useProvideStore(STORE_ID, store);
44
41
 
42
+ {{#if persist}}
43
+ {{#if persistLocalStorage}}
44
+ {{#if schemas}}
45
+ {{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
46
+ {{else}}
47
+ {{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
48
+ {{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
49
+ {{/if}}
50
+
51
+ useCreatePersister(
52
+ store,
53
+ (store) => createLocalPersister(store, STORE_ID),
54
+ [],
55
+ async (persister) => {
56
+ await persister.load();
57
+ await persister.startAutoSave();
58
+ onReady?.();
59
+ },
60
+ );
61
+ {{/if}}
62
+ {{#if persistSqlite}}
63
+ {{#if schemas}}
64
+ {{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
65
+ {{else}}
66
+ {{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
67
+ {{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
68
+ {{/if}}
69
+ {{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.{{ext}}"}}
70
+ {{addImport "import {getDb} from './sqlite';"}}
71
+
72
+ useCreatePersister(
73
+ store,
74
+ async (store) => {
75
+ const {sqlite3, db} = await getDb();
76
+ return createSqliteWasmPersister(store, sqlite3, db, STORE_ID);
77
+ },
78
+ [],
79
+ async (persister) => {
80
+ await persister.load();
81
+ await persister.startAutoSave();
82
+ onReady?.();
83
+ },
84
+ );
85
+ {{/if}}
86
+ {{#if persistPglite}}
87
+ {{#if schemas}}
88
+ {{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
89
+ {{else}}
90
+ {{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
91
+ {{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
92
+ {{/if}}
93
+ {{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
94
+ {{addImport "import {getPgLite} from './pglite';"}}
95
+
96
+ useCreatePersister(
97
+ store,
98
+ async (store) => {
99
+ const pgLite = await getPgLite();
100
+ return await createPglitePersister(store, pgLite, 'chat');
101
+ },
102
+ [],
103
+ async (persister) => {
104
+ await persister.load();
105
+ await persister.startAutoSave();
106
+ onReady?.();
107
+ },
108
+ );
109
+ {{/if}}
110
+ {{/if}}
111
+
45
112
  {{#if sync}}
46
113
  {{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
47
114
  {{addImport "import {SERVER} from './config';"}}
48
115
  {{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
49
- {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
50
- {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
51
- {{addImport "import type {MergeableStore} from 'tinybase';"}}
116
+ {{#if schemas}}
117
+ {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client/with-schemas';"}}
118
+ {{else}}
119
+ {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
120
+ {{/if}}
121
+ {{#if schemas}}
122
+ {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react/with-schemas';"}}
123
+ {{else}}
124
+ {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
125
+ {{/if}}
52
126
 
53
- useCreateSynchronizer(store, async (store: MergeableStore) => {
127
+ useCreateSynchronizer(store, async (store) => {
54
128
  const serverPathId = location.pathname;
55
129
  const synchronizer = await createWsSynchronizer(
56
130
  store,
@@ -59,12 +133,19 @@ useProvideStore(STORE_ID, store);
59
133
  await synchronizer.startSync();
60
134
 
61
135
  synchronizer.getWebSocket().addEventListener('open', () => {
62
- synchronizer.load().then(() => synchronizer.save());
136
+ synchronizer.load().then(() => {
137
+ synchronizer.save();
138
+ {{#unless persist}}onReady?.();{{/unless}}
139
+ });
63
140
  });
64
141
 
65
142
  return synchronizer;
66
143
  });
67
144
  {{/if}}
68
145
 
146
+ {{#unless persist}}{{#unless sync}}
147
+ onReady?.();
148
+ {{/unless}}{{/unless}}
149
+
69
150
  return null;
70
151
  };
@@ -1,21 +1,20 @@
1
- import {useRow, STORE_ID} from './ChatStore';
1
+ {{addImport "import {useRow, type MessageRow, STORE_ID} from './ChatStore';"}}
2
2
 
3
3
  {{includeFile template="client/src/chat/message.css.hbs" output="client/src/message.css"}}
4
- import './message.css';
4
+ {{addImport "import './message.css';"}}
5
5
 
6
6
  interface MessageProps {
7
7
  rowId: string;
8
8
  }
9
9
 
10
10
  export const Message = ({rowId}: MessageProps) => {
11
- const row = useRow('messages', rowId, 'chat');
12
- const time = new Date((row as any).timestamp).toLocaleTimeString();
11
+ const {username, text, timestamp} = useRow('messages', rowId, STORE_ID) as MessageRow;
13
12
 
14
13
  return (
15
14
  <div className="message">
16
- <span className="username">{(row as any).username}:</span>
17
- <span className="text">{(row as any).text}</span>
18
- <span className="time">{time}</span>
15
+ <span className="username">{username}:</span>
16
+ <span className="text">{text}</span>
17
+ <span className="time">{new Date(timestamp).toLocaleTimeString()}</span>
19
18
  </div>
20
- );
21
- };
19
+ )
20
+ };
@@ -1,14 +1,14 @@
1
1
  {{includeFile template="client/src/chat/messageInput.css.hbs" output="client/src/messageInput.css"}}
2
- import {useState} from 'react';
3
- import {useAddRowCallback, STORE_ID as CHAT_STORE_ID} from './ChatStore';
4
- import {useValue, STORE_ID as SETTINGS_STORE_ID} from './SettingsStore';
5
- import './messageInput.css';
2
+ {{addImport "import {useState} from 'react';"}}
3
+ {{addImport "import {useAddRowCallback, STORE_ID as CHAT_STORE_ID} from './ChatStore';"}}
4
+ {{addImport "import {useValue, STORE_ID as SETTINGS_STORE_ID} from './SettingsStore';"}}
5
+ {{addImport "import './messageInput.css';"}}
6
6
 
7
7
  {{includeFile template="client/src/shared/Button.tsx.hbs" output="client/src/Button.{{ext}}"}}
8
- import {Button} from './Button';
8
+ {{addImport "import {Button} from './Button';"}}
9
9
 
10
10
  {{includeFile template="client/src/shared/Input.tsx.hbs" output="client/src/Input.{{ext}}"}}
11
- import {Input} from './Input';
11
+ {{addImport "import {Input} from './Input';"}}
12
12
 
13
13
  export const MessageInput = () => {
14
14
  const [message, setMessage] = useState('');
@@ -39,4 +39,4 @@ return (
39
39
  <Button type="submit" variant="primary">Send</Button>
40
40
  </form>
41
41
  );
42
- };
42
+ };
@@ -1,23 +1,13 @@
1
- import {useMemo} from 'react';
2
- import {useRowIds, useStore, STORE_ID} from './ChatStore';
1
+ {{addImport "import {useSortedRowIds, STORE_ID} from './ChatStore';"}}
3
2
 
4
3
  {{includeFile template="client/src/chat/messages.css.hbs" output="client/src/messages.css"}}
5
- import './messages.css';
4
+ {{addImport "import './messages.css';"}}
6
5
 
7
6
  {{includeFile template="client/src/chat/Message.tsx.hbs" output="client/src/Message.{{ext}}"}}
8
- import {Message} from './Message';
7
+ {{addImport "import {Message} from './Message';"}}
9
8
 
10
9
  export const Messages = () => {
11
- const store = useStore(STORE_ID);
12
- const messageIds = useRowIds('messages', STORE_ID);
13
-
14
- const sortedIds = useMemo(() => {
15
- return [...messageIds].sort((a, b) => {
16
- const rowA = store.getRow('messages', a) as any;
17
- const rowB = store.getRow('messages', b) as any;
18
- return rowA.timestamp - rowB.timestamp;
19
- });
20
- }, [messageIds, store]);
10
+ const sortedIds = useSortedRowIds('messages', 'timestamp', false, 0, undefined, STORE_ID);
21
11
 
22
12
  return (
23
13
  <div id="messages">
@@ -26,4 +16,4 @@ return (
26
16
  ))}
27
17
  </div>
28
18
  );
29
- };
19
+ };