create-tinybase 0.1.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 (4) hide show
  1. package/README.md +17 -0
  2. package/cli.js +359 -0
  3. package/cspell.json +8 -0
  4. package/package.json +28 -0
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # create-tinybase
2
+
3
+ CLI tool to scaffold a new TinyBase application.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm init tinybase
9
+ ```
10
+
11
+ This will prompt you with questions to configure your new TinyBase app:
12
+
13
+ - Project name
14
+ - Language (TypeScript or JavaScript)
15
+ - Framework (React or Vanilla)
16
+ - Persistence option (None, Local Storage, SQLite, PostgreSQL)
17
+ - Sync capabilities
package/cli.js ADDED
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+
3
+ import prompts from 'prompts';
4
+ import { mkdir, writeFile } from 'fs/promises';
5
+ import { join } from 'path';
6
+
7
+ console.log('šŸŽ‰ Welcome to TinyBase!\n');
8
+
9
+ const questions = [
10
+ {
11
+ type: 'text',
12
+ name: 'projectName',
13
+ message: 'Project name:',
14
+ initial: 'my-tinybase-app',
15
+ validate: (value) => value.length > 0 ? true : 'Project name is required'
16
+ },
17
+ {
18
+ type: 'select',
19
+ name: 'language',
20
+ message: 'Language:',
21
+ choices: [
22
+ { title: 'TypeScript', value: 'typescript' },
23
+ { title: 'JavaScript', value: 'javascript' }
24
+ ],
25
+ initial: 0
26
+ },
27
+ {
28
+ type: 'select',
29
+ name: 'framework',
30
+ message: 'Framework:',
31
+ choices: [
32
+ { title: 'React', value: 'react' },
33
+ { title: 'Vanilla', value: 'vanilla' }
34
+ ],
35
+ initial: 0
36
+ },
37
+ {
38
+ type: 'select',
39
+ name: 'persister',
40
+ message: 'Persistence:',
41
+ choices: [
42
+ { title: 'None (in-memory only)', value: 'none' },
43
+ { title: 'Local Storage', value: 'local-storage' },
44
+ { title: 'SQLite', value: 'sqlite' },
45
+ { title: 'PostgreSQL', value: 'postgres' }
46
+ ],
47
+ initial: 0
48
+ },
49
+ {
50
+ type: 'confirm',
51
+ name: 'sync',
52
+ message: 'Include sync capabilities?',
53
+ initial: false
54
+ }
55
+ ];
56
+
57
+ async function main() {
58
+ const answers = await prompts(questions, {
59
+ onCancel: () => {
60
+ console.log('\nāŒ Cancelled');
61
+ process.exit(0);
62
+ }
63
+ });
64
+
65
+ console.log('\nšŸ“¦ Creating your TinyBase app...\n');
66
+
67
+ const projectPath = join(process.cwd(), answers.projectName);
68
+
69
+ try {
70
+ await createProject(projectPath, answers);
71
+ console.log('āœ… Done!\n');
72
+ console.log(`Next steps:`);
73
+ console.log(` cd ${answers.projectName}`);
74
+ console.log(` npm install`);
75
+ console.log(` npm run dev`);
76
+ } catch (error) {
77
+ console.error('āŒ Error creating project:', error.message);
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ async function createProject(projectPath, config) {
83
+ await mkdir(projectPath, { recursive: true });
84
+ await mkdir(join(projectPath, 'src'), { recursive: true });
85
+ await mkdir(join(projectPath, 'public'), { recursive: true });
86
+
87
+ // Generate files based on config
88
+ await generatePackageJson(projectPath, config);
89
+ await generateIndexHtml(projectPath, config);
90
+ await generateViteConfig(projectPath, config);
91
+ await generateTsConfig(projectPath, config);
92
+ await generateSourceFiles(projectPath, config);
93
+ await generateReadme(projectPath, config);
94
+ }
95
+
96
+ async function generatePackageJson(projectPath, config) {
97
+ const isTS = config.language === 'typescript';
98
+ const isReact = config.framework === 'react';
99
+
100
+ const pkg = {
101
+ name: config.projectName,
102
+ version: '0.1.0',
103
+ type: 'module',
104
+ scripts: {
105
+ dev: 'vite',
106
+ build: isTS ? 'tsc && vite build' : 'vite build',
107
+ preview: 'vite preview'
108
+ },
109
+ dependencies: {
110
+ tinybase: '^5.4.3'
111
+ },
112
+ devDependencies: {
113
+ vite: '^5.4.11'
114
+ }
115
+ };
116
+
117
+ if (isReact) {
118
+ pkg.dependencies.react = '^18.3.1';
119
+ pkg.dependencies['react-dom'] = '^18.3.1';
120
+ pkg.devDependencies['@vitejs/plugin-react'] = '^4.3.4';
121
+ }
122
+
123
+ if (isTS) {
124
+ pkg.devDependencies.typescript = '^5.6.3';
125
+ }
126
+
127
+ if (config.persister === 'sqlite') {
128
+ pkg.dependencies['tinybase-sqlite-wasm'] = '^1.0.0';
129
+ } else if (config.persister === 'postgres') {
130
+ pkg.dependencies['pg'] = '^8.13.1';
131
+ }
132
+
133
+ if (config.sync) {
134
+ pkg.dependencies['tinybase-ws-server'] = '^1.0.0';
135
+ }
136
+
137
+ await writeFile(
138
+ join(projectPath, 'package.json'),
139
+ JSON.stringify(pkg, null, 2)
140
+ );
141
+ }
142
+
143
+ async function generateIndexHtml(projectPath, config) {
144
+ const isTS = config.language === 'typescript';
145
+ const ext = isTS ? 'tsx' : 'jsx';
146
+
147
+ const html = `<!DOCTYPE html>
148
+ <html lang="en">
149
+ <head>
150
+ <meta charset="UTF-8" />
151
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
152
+ <title>${config.projectName}</title>
153
+ </head>
154
+ <body>
155
+ <div id="app"></div>
156
+ <script type="module" src="/src/index.${ext}"></script>
157
+ </body>
158
+ </html>
159
+ `;
160
+
161
+ await writeFile(join(projectPath, 'index.html'), html);
162
+ }
163
+
164
+ async function generateViteConfig(projectPath, config) {
165
+ const isReact = config.framework === 'react';
166
+ const isTS = config.language === 'typescript';
167
+ const ext = isTS ? 'ts' : 'js';
168
+
169
+ let content = `import { defineConfig } from 'vite';\n`;
170
+
171
+ if (isReact) {
172
+ content += `import react from '@vitejs/plugin-react';\n`;
173
+ }
174
+
175
+ content += `\nexport default defineConfig({\n`;
176
+ content += ` plugins: [${isReact ? 'react()' : ''}],\n`;
177
+ content += `});\n`;
178
+
179
+ await writeFile(join(projectPath, `vite.config.${ext}`), content);
180
+ }
181
+
182
+ async function generateTsConfig(projectPath, config) {
183
+ if (config.language !== 'typescript') return;
184
+
185
+ const tsconfig = {
186
+ compilerOptions: {
187
+ target: 'ES2020',
188
+ useDefineForClassFields: true,
189
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
190
+ module: 'ESNext',
191
+ skipLibCheck: true,
192
+ moduleResolution: 'bundler',
193
+ allowImportingTsExtensions: true,
194
+ resolveJsonModule: true,
195
+ isolatedModules: true,
196
+ noEmit: true,
197
+ jsx: config.framework === 'react' ? 'react-jsx' : undefined,
198
+ strict: true,
199
+ noUnusedLocals: true,
200
+ noUnusedParameters: true,
201
+ noFallthroughCasesInSwitch: true
202
+ },
203
+ include: ['src']
204
+ };
205
+
206
+ await writeFile(
207
+ join(projectPath, 'tsconfig.json'),
208
+ JSON.stringify(tsconfig, null, 2)
209
+ );
210
+ }
211
+
212
+ async function generateSourceFiles(projectPath, config) {
213
+ const isTS = config.language === 'typescript';
214
+ const isReact = config.framework === 'react';
215
+ const ext = isTS ? (isReact ? 'tsx' : 'ts') : (isReact ? 'jsx' : 'js');
216
+
217
+ if (isReact) {
218
+ await generateReactApp(projectPath, config, ext);
219
+ } else {
220
+ await generateVanillaApp(projectPath, config, ext);
221
+ }
222
+ }
223
+
224
+ async function generateReactApp(projectPath, config, ext) {
225
+ const isTS = config.language === 'typescript';
226
+
227
+ // index file
228
+ const indexContent = `import React from 'react';
229
+ import ReactDOM from 'react-dom/client';
230
+ import App from './App';
231
+ import './index.css';
232
+
233
+ ReactDOM.createRoot(document.getElementById('app')${isTS ? '!' : ''}).render(
234
+ <React.StrictMode>
235
+ <App />
236
+ </React.StrictMode>
237
+ );
238
+ `;
239
+
240
+ await writeFile(join(projectPath, 'src', `index.${ext}`), indexContent);
241
+
242
+ // App component
243
+ const appContent = `import { createStore } from 'tinybase';
244
+ import { useCreateStore, useCell } from 'tinybase/ui-react';
245
+
246
+ function App() {
247
+ const store = useCreateStore(() => createStore().setCell('data', 'row1', 'count', 0));
248
+
249
+ return (
250
+ <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
251
+ <h1>šŸŽ‰ TinyBase App</h1>
252
+ <Counter store={store} />
253
+ </div>
254
+ );
255
+ }
256
+
257
+ function Counter({ store }${isTS ? ': { store: any }' : ''}) {
258
+ const count = useCell('data', 'row1', 'count', store) ${isTS ? 'as number' : ''};
259
+
260
+ return (
261
+ <div>
262
+ <p>Count: {count}</p>
263
+ <button onClick={() => store.setCell('data', 'row1', 'count', count + 1)}>
264
+ Increment
265
+ </button>
266
+ </div>
267
+ );
268
+ }
269
+
270
+ export default App;
271
+ `;
272
+
273
+ await writeFile(join(projectPath, 'src', `App.${ext}`), appContent);
274
+
275
+ // CSS
276
+ const cssContent = `body {
277
+ margin: 0;
278
+ padding: 0;
279
+ font-family: system-ui, -apple-system, sans-serif;
280
+ }
281
+
282
+ button {
283
+ padding: 0.5rem 1rem;
284
+ font-size: 1rem;
285
+ cursor: pointer;
286
+ background: #007bff;
287
+ color: white;
288
+ border: none;
289
+ border-radius: 4px;
290
+ }
291
+
292
+ button:hover {
293
+ background: #0056b3;
294
+ }
295
+ `;
296
+
297
+ await writeFile(join(projectPath, 'src', 'index.css'), cssContent);
298
+ }
299
+
300
+ async function generateVanillaApp(projectPath, config, ext) {
301
+ const content = `import { createStore } from 'tinybase';
302
+
303
+ const store = createStore();
304
+ store.setCell('data', 'row1', 'count', 0);
305
+
306
+ const app = document.getElementById('app');
307
+ app.innerHTML = \`
308
+ <div style="padding: 2rem; font-family: system-ui">
309
+ <h1>šŸŽ‰ TinyBase App</h1>
310
+ <p>Count: <span id="count">0</span></p>
311
+ <button id="increment">Increment</button>
312
+ </div>
313
+ \`;
314
+
315
+ const countEl = document.getElementById('count');
316
+ const button = document.getElementById('increment');
317
+
318
+ store.addCellListener('data', 'row1', 'count', () => {
319
+ countEl.textContent = store.getCell('data', 'row1', 'count');
320
+ });
321
+
322
+ button.addEventListener('click', () => {
323
+ const current = store.getCell('data', 'row1', 'count');
324
+ store.setCell('data', 'row1', 'count', current + 1);
325
+ });
326
+ `;
327
+
328
+ await writeFile(join(projectPath, 'src', `index.${ext}`), content);
329
+ }
330
+
331
+ async function generateReadme(projectPath, config) {
332
+ const readme = `# ${config.projectName}
333
+
334
+ A TinyBase application scaffolded with \`tinybase-new\`.
335
+
336
+ ## Configuration
337
+
338
+ - Language: ${config.language}
339
+ - Framework: ${config.framework}
340
+ - Persistence: ${config.persister}
341
+ - Sync: ${config.sync ? 'Yes' : 'No'}
342
+
343
+ ## Getting Started
344
+
345
+ \`\`\`bash
346
+ npm install
347
+ npm run dev
348
+ \`\`\`
349
+
350
+ ## Learn More
351
+
352
+ - [TinyBase Documentation](https://tinybase.org/)
353
+ - [TinyBase GitHub](https://github.com/tinyplex/tinybase)
354
+ `;
355
+
356
+ await writeFile(join(projectPath, 'README.md'), readme);
357
+ }
358
+
359
+ main();
package/cspell.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "ignorePaths": [],
3
+ "words": [
4
+ "jamesgpearce",
5
+ "tinybase",
6
+ "tinyplex"
7
+ ]
8
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "create-tinybase",
3
+ "version": "0.1.0",
4
+ "author": "jamesgpearce",
5
+ "repository": "github:tinyplex/tinybase",
6
+ "license": "MIT",
7
+ "homepage": "https://tinybase.org",
8
+ "description": "The CLI to build a new app using TinyBase, a reactive data store and sync engine.",
9
+ "keywords": [
10
+ "tiny",
11
+ "sync engine",
12
+ "local-first",
13
+ "reactive",
14
+ "state",
15
+ "data",
16
+ "react"
17
+ ],
18
+ "type": "module",
19
+ "bin": {
20
+ "create-tinybase": "./cli.js"
21
+ },
22
+ "dependencies": {
23
+ "prompts": "^2.4.2"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ }