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.
- package/README.md +17 -0
- package/cli.js +359 -0
- package/cspell.json +8 -0
- 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
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
|
+
}
|