create-tinybase 0.3.0 → 1.0.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.
package/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
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)});
2
+ import{existsSync as f}from"fs";import{dirname as k,join as u}from"path";import{createCLI as x,detectPackageManager as R}from"tinycreate";import{fileURLToPath as S}from"url";const P=k(S(import.meta.url)),C={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 f(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",b=i?c?"tsx":"ts":c?"jsx":"js",a=d||"remote",T=a!=="none",h=a==="node"||a==="durable-objects",j=a==="durable-objects"?"durable-objects":"node",w=a==="durable-objects",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:T,server:h,serverType:j,isDurableObject:w,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:b}},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(P,"templates"),installCommand:"{pm} install",devCommand:"{pm} run dev",workingDirectory:"client",onSuccess:(e,t)=>{const s=t.syncType,n=s==="node"||s==="durable-objects",o=R();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`)}};x(C).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.3.0",
3
+ "version": "1.0.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 new apps using TinyBase, a reactive data store and sync engine.",
11
+ "description": "A command-line tool to create TinyBase apps with full synchronization and local-first capabilities.",
12
12
  "keywords": [
13
13
  "tiny",
14
14
  "sync engine",
@@ -16,7 +16,10 @@
16
16
  "reactive",
17
17
  "state",
18
18
  "data",
19
- "react"
19
+ "react",
20
+ "sqlite",
21
+ "pglite",
22
+ "durable-objects"
20
23
  ],
21
24
  "type": "module",
22
25
  "bin": {
Binary file
@@ -9,8 +9,8 @@ A TinyBase app built with {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}
9
9
 
10
10
  ## Getting Started
11
11
 
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}}.
12
+ Alongside this file, you will see a directory called `client`{{#if server
13
+ }} (and 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}}.
14
14
 
15
15
  To start the web client with Vite, run the following commands:
16
16
 
@@ -37,6 +37,123 @@ PNPM, Yarn, and Bun should also work. Your app will be available at
37
37
  {{/if}}
38
38
  After that, start hacking!
39
39
 
40
+ ## Structure
41
+
42
+ This project is organized into {{#if server}}two main directories{{else}}a single `client` directory{{/if}}:
43
+
44
+ ### Client
45
+
46
+ The `client` directory contains your {{#if react}}React-based{{else}}vanilla JavaScript{{/if}} web application,
47
+ built with Vite and {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}.
48
+
49
+ #### Key Files
50
+
51
+ - **`index.html`** - The main HTML file that loads your app
52
+ - **`src/index.{{ext}}`** - Entry point that bootstraps the application{{#if react}} and renders the React component tree{{/if}}
53
+ - **`src/{{#if react}}App{{else}}app{{/if}}.{{ext}}`** - {{#if react}}Main React component that renders the {{#if (eq appType "todos")}}todo list{{else if (eq appType "chat")}}chat interface{{else if (eq appType "drawing")}}drawing
54
+ canvas{{else if (eq appType "game")}}game{{/if}}
55
+ {{else}}Main application logic{{/if}}
56
+ -
57
+ **`src/{{#if react}}{{#if (eq appType "chat")}}ChatStore{{else if (eq appType "drawing")}}CanvasStore{{else}}Store{{/if}}{{else}}{{#if (eq appType "chat")}}chatStore{{else if (eq appType "drawing")}}canvasStore{{else}}store{{/if}}{{/if}}.{{ext}}`**
58
+ - TinyBase {{#if (eq appType "chat")}}chat messages{{else if (eq appType "drawing")}}drawing canvas{{else}}main{{/if}} store configuration
59
+ {{#if (eq appType "chat")}}
60
+ - **`src/{{#if react}}SettingsStore{{else}}settingsStore{{/if}}.{{ext}}`** - TinyBase settings store (not synchronized)
61
+ {{/if}}
62
+ {{#if (eq appType "drawing")}}
63
+ - **`src/{{#if react}}SettingsStore{{else}}settingsStore{{/if}}.{{ext}}`** - TinyBase settings store (not synchronized)
64
+ {{/if}}
65
+ - **`src/config.{{ext}}`** - Configuration settings{{#if server}} (including server connection URL){{/if}}
66
+
67
+ #### How It Works
68
+
69
+ The app uses **TinyBase** to manage application state in a reactive data store.
70
+ {{#if schemas}}Type-safe schemas define the structure of your data, providing autocomplete
71
+ and compile-time type checking.{{/if}}
72
+
73
+ {{#if sync}}
74
+ The store is a **MergeableStore**, which supports real-time synchronization across
75
+ multiple clients. When you open the app, it generates a unique room ID in the URL
76
+ path (e.g., `/abc123`). Anyone who visits the same URL will share the same data
77
+ and see live updates from other users.
78
+
79
+ {{/if}}
80
+ {{#if persistLocalStorage}}
81
+ **Persistence** is enabled using browser LocalStorage.
82
+ Your data is automatically saved and restored when you reload the page.
83
+
84
+ {{/if}}
85
+ {{#if persistSqlite}}
86
+ **Persistence** is enabled using SQLite WASM (running entirely in the browser).
87
+ Your data is automatically saved and restored when you reload the page.
88
+
89
+ {{/if}}
90
+ {{#if persistPglite}}
91
+ **Persistence** is enabled using PGlite (Postgres WASM running entirely in the browser).
92
+ Your data is automatically saved and restored when you reload the page.
93
+
94
+ {{/if}}
95
+ {{#if sync}}
96
+ **Synchronization** is powered by TinyBase's WebSocket synchronizer{{#if server}}, connecting
97
+ to your {{#if (eq syncType "durable-objects")}}Cloudflare Durable Objects{{else}}Node.js{{/if}} server{{else}} (connecting to a demo server at vite.tinybase.org){{/if}}.
98
+ When data changes locally, it's sent to the server and broadcast to all other
99
+ clients in the same room. The MergeableStore handles conflict resolution using
100
+ CRDTs, ensuring all clients converge to the same state.
101
+
102
+ {{/if}}
103
+ {{#if react}}
104
+ **React Integration** - TinyBase provides React hooks that automatically subscribe
105
+ to store changes and trigger re-renders when data updates. This means your UI
106
+ stays in sync with your data with minimal boilerplate.
107
+
108
+ Components like {{#if (eq appType "todos")}}`TodoList` and `TodoItem` use hooks like `useSortedRowIds`, `useRow`, and
109
+ callback hooks like `useAddRowCallback` and `useDelRowCallback`
110
+ {{else if (eq appType "chat")}}`MessageList` and `MessageInput` use TinyBase hooks to display and send messages
111
+ {{else if (eq appType "drawing")}}`Canvas` uses TinyBase hooks to track drawing state across users
112
+ {{else}}`Game` uses TinyBase hooks to manage game state{{/if}} to interact
113
+ with the store.
114
+
115
+ {{/if}}
116
+ {{#if server}}
117
+ ### Server
118
+
119
+ The `server` directory contains your {{#if (eq syncType "durable-objects")}}Cloudflare Durable Objects{{else}}Node.js{{/if}} WebSocket server.
120
+
121
+ #### Key Files
122
+
123
+ - **`index.{{ext}}`** - Server entry point
124
+
125
+ #### How It Works
126
+
127
+ {{#if (eq syncType "durable-objects")}}
128
+ The server uses **Cloudflare Durable Objects** to provide persistent, distributed
129
+ synchronization. Each room (identified by the URL path) maps to a Durable Object
130
+ instance that:
131
+
132
+ - Maintains the canonical state in SQL storage (using Durable Object SQL Storage)
133
+ - Handles WebSocket connections from clients
134
+ - Broadcasts changes to all connected clients in the room
135
+ - Automatically persists data, ensuring durability across deployments
136
+
137
+ The `TinyBaseDurableObject` class extends `WsServerDurableObject` and creates a
138
+ MergeableStore with a SQL storage persister. Cloudflare's infrastructure ensures
139
+ that all clients connecting to the same path are routed to the same Durable
140
+ Object instance.
141
+
142
+ To deploy to Cloudflare Workers, you'll need to configure your `wrangler.toml`
143
+ file with your Durable Object bindings.
144
+ {{else}}
145
+ The server runs a **Node.js WebSocket server** using the `ws` library. It acts
146
+ as a relay between clients, broadcasting state changes to all connected clients
147
+ in the same room.
148
+
149
+ When a client connects to a path (e.g., `/abc123`), the server groups them with
150
+ other clients on the same path and ensures all changes are synchronized.
151
+
152
+ The server logs when clients join or leave rooms, helping you monitor activity
153
+ during development.
154
+ {{/if}}
155
+
156
+ {{/if}}
40
157
  ## Learn More
41
158
 
42
159
  - [TinyBase Documentation](https://tinybase.org)
@@ -168,7 +168,11 @@
168
168
  A tic-tac-toe game demonstrating turn-based logic and computed game state.
169
169
  {{/if}}
170
170
  <br><br>
171
- Built with {{#if typescript}}TypeScript{{#if schemas}} (using typed store schemas){{/if}}{{else}}JavaScript{{/if}}{{#if react}} and React{{/if}}.
171
+ Built with: {{#if typescript}}TypeScript{{#if schemas}} (using typed store schemas){{/if}}{{else}}JavaScript{{/if}}{{#if react}} and React{{/if}}.{{#if persist}}
172
+ <br><br>
173
+ Persistence: {{#if persistLocalStorage}}Local Storage{{else if persistSqlite}}SQLite{{else if persistPglite}}PGlite{{/if}}.{{/if}}{{#if sync}}
174
+ <br><br>
175
+ Sync: {{#if (eq syncType "remote")}}Remote{{else if (eq syncType "node")}}Node.js Server{{else if (eq syncType "durable-objects")}}Durable Objects{{/if}}.{{/if}}
172
176
  </div>
173
177
  </div>
174
178
  </div>
@@ -0,0 +1,43 @@
1
+ {{#if isDurableObject}}
2
+ {{addImport "import {createMergeableStore} from 'tinybase';"}}
3
+ {{addImport "import {createDurableObjectSqlStoragePersister} from 'tinybase/persisters/persister-durable-object-sql-storage';"}}
4
+ {{addImport "import {getWsServerDurableObjectFetch, WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';"}}
5
+
6
+ export class TinyBaseDurableObject extends WsServerDurableObject {
7
+ createPersister() {
8
+ const store = createMergeableStore();
9
+ const persister = createDurableObjectSqlStoragePersister(
10
+ store,
11
+ this.ctx.storage.sql,
12
+ );
13
+ return persister;
14
+ }
15
+
16
+ onClientId(
17
+ pathId: string,
18
+ clientId: string,
19
+ addedOrRemoved: IdAddedOrRemoved,
20
+ ) {
21
+ console.log(
22
+ `Client ${clientId} ${addedOrRemoved == 1 ? 'joined' : 'left'} /${pathId}`,
23
+ );
24
+ }
25
+ }
26
+
27
+ export default {
28
+ fetch: getWsServerDurableObjectFetch('TinyBaseDurableObjects'),
29
+ };
30
+ {{else}}
31
+ {{addImport "import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server';"}}
32
+ {{addImport "import {WebSocketServer} from 'ws';"}}
33
+
34
+ const PORT = 8043;
35
+
36
+ const wsServer = createWsServer(new WebSocketServer({port: PORT}));
37
+
38
+ wsServer.addClientIdsListener(null, (wsServer, pathId, clientId, addedOrRemoved) => {
39
+ console.log(`Client ${clientId} ${addedOrRemoved == 1 ? 'joined' : 'left'} /${pathId}`);
40
+ });
41
+
42
+ console.log(`WebSocket server running on ws://localhost:${PORT}`);
43
+ {{/if}}
@@ -5,12 +5,11 @@
5
5
  "main": "index.ts",
6
6
  "scripts": {
7
7
  {{#list}}
8
+ {{includeFile template="server/index.ts.hbs" output="server/index.ts"}}
8
9
  {{#if (eq serverType "node")}}
9
- {{includeFile template="server/index-node.ts.hbs" output="server/index.ts"}}
10
10
  "dev": "tsx watch index.ts"
11
11
  "start": "node index.js"
12
12
  {{else}}
13
- {{includeFile template="server/index-do.ts.hbs" output="server/index.ts"}}
14
13
  {{includeFile template="server/wrangler.toml.hbs" output="server/wrangler.toml"}}
15
14
  {{#if typescript}}
16
15
  {{includeFile template="server/tsconfig.json.hbs" output="server/tsconfig.json"}}
@@ -1,18 +0,0 @@
1
- {{addImport "import {createMergeableStore} from 'tinybase';"}}
2
- {{addImport "import {createDurableObjectSqlStoragePersister} from 'tinybase/persisters/persister-durable-object-sql-storage';"}}
3
- {{addImport "import {getWsServerDurableObjectFetch, WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';"}}
4
-
5
- export class TinyBaseDurableObject extends WsServerDurableObject {
6
- createPersister() {
7
- const store = createMergeableStore();
8
- const persister = createDurableObjectSqlStoragePersister(
9
- store,
10
- this.ctx.storage.sql,
11
- );
12
- return persister;
13
- }
14
- }
15
-
16
- export default {
17
- fetch: getWsServerDurableObjectFetch('TinyBaseDurableObjects'),
18
- };
@@ -1,8 +0,0 @@
1
- {{addImport "import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server';"}}
2
- {{addImport "import {WebSocketServer} from 'ws';"}}
3
-
4
- const PORT = 8043;
5
-
6
- const wsServer = createWsServer(new WebSocketServer({port: PORT}));
7
-
8
- console.log(`WebSocket server running on ws://localhost:${PORT}`);