create-sonicjs 2.0.0-alpha.2

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/src/cli.js ADDED
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs-extra'
4
+ import path from 'path'
5
+ import { fileURLToPath } from 'url'
6
+ import prompts from 'prompts'
7
+ import kleur from 'kleur'
8
+ import ora from 'ora'
9
+ import { execa } from 'execa'
10
+ import validatePackageName from 'validate-npm-package-name'
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
+
14
+ // Version
15
+ const VERSION = '2.0.0-alpha.2'
16
+
17
+ // Templates available
18
+ const TEMPLATES = {
19
+ starter: {
20
+ name: 'Starter (Blog & Content)',
21
+ description: 'Perfect for blogs, documentation, and content sites',
22
+ color: 'blue'
23
+ },
24
+ // Future templates
25
+ // ecommerce: {
26
+ // name: 'E-commerce',
27
+ // description: 'Online store with products and checkout',
28
+ // color: 'green'
29
+ // },
30
+ }
31
+
32
+ // Banner
33
+ console.log()
34
+ console.log(kleur.bold().cyan('✨ Create SonicJS App'))
35
+ console.log(kleur.dim(` v${VERSION}`))
36
+ console.log()
37
+
38
+ // Parse arguments
39
+ const args = process.argv.slice(2)
40
+ const projectName = args[0]
41
+ const flags = {
42
+ skipInstall: args.includes('--skip-install'),
43
+ skipGit: args.includes('--skip-git'),
44
+ skipCloudflare: args.includes('--skip-cloudflare'),
45
+ template: args.find(arg => arg.startsWith('--template='))?.split('=')[1],
46
+ databaseName: args.find(arg => arg.startsWith('--database='))?.split('=')[1],
47
+ bucketName: args.find(arg => arg.startsWith('--bucket='))?.split('=')[1],
48
+ skipExample: args.includes('--skip-example'),
49
+ includeExample: args.includes('--include-example')
50
+ }
51
+
52
+ async function main() {
53
+ try {
54
+ // Get project details
55
+ const answers = await getProjectDetails(projectName)
56
+
57
+ // Create project
58
+ await createProject(answers, flags)
59
+
60
+ // Success message
61
+ printSuccessMessage(answers)
62
+
63
+ } catch (error) {
64
+ if (error.message === 'cancelled') {
65
+ console.log()
66
+ console.log(kleur.yellow('⚠ Cancelled'))
67
+ process.exit(0)
68
+ }
69
+
70
+ console.error()
71
+ console.error(kleur.red('✖ Error:'), error.message)
72
+ console.error()
73
+ process.exit(1)
74
+ }
75
+ }
76
+
77
+ async function getProjectDetails(initialName) {
78
+ const questions = []
79
+
80
+ // Project name
81
+ if (!initialName) {
82
+ questions.push({
83
+ type: 'text',
84
+ name: 'projectName',
85
+ message: 'Project name:',
86
+ initial: 'my-sonicjs-app',
87
+ validate: (value) => {
88
+ if (!value) return 'Project name is required'
89
+ const validation = validatePackageName(value)
90
+ if (!validation.validForNewPackages) {
91
+ return validation.errors?.[0] || 'Invalid package name'
92
+ }
93
+ if (fs.existsSync(value)) {
94
+ return `Directory "${value}" already exists`
95
+ }
96
+ return true
97
+ }
98
+ })
99
+ }
100
+
101
+ // Template selection
102
+ if (!flags.template) {
103
+ questions.push({
104
+ type: 'select',
105
+ name: 'template',
106
+ message: 'Choose a template:',
107
+ choices: Object.entries(TEMPLATES).map(([key, { name, description }]) => ({
108
+ title: name,
109
+ description: description,
110
+ value: key
111
+ })),
112
+ initial: 0
113
+ })
114
+ }
115
+
116
+ // Database name
117
+ if (!flags.databaseName) {
118
+ questions.push({
119
+ type: 'text',
120
+ name: 'databaseName',
121
+ message: 'Database name:',
122
+ initial: (prev, values) => `${values.projectName || initialName}-db`,
123
+ validate: (value) => value ? true : 'Database name is required'
124
+ })
125
+ }
126
+
127
+ // R2 bucket name
128
+ if (!flags.bucketName) {
129
+ questions.push({
130
+ type: 'text',
131
+ name: 'bucketName',
132
+ message: 'R2 bucket name:',
133
+ initial: (prev, values) => `${values.projectName || initialName}-media`,
134
+ validate: (value) => value ? true : 'Bucket name is required'
135
+ })
136
+ }
137
+
138
+ // Include example collection (only ask if neither flag is set)
139
+ if (!flags.skipExample && !flags.includeExample) {
140
+ questions.push({
141
+ type: 'confirm',
142
+ name: 'includeExample',
143
+ message: 'Include example blog collection?',
144
+ initial: true
145
+ })
146
+ }
147
+
148
+ // Create Cloudflare resources
149
+ if (!flags.skipCloudflare) {
150
+ questions.push({
151
+ type: 'confirm',
152
+ name: 'createResources',
153
+ message: 'Create Cloudflare resources now? (requires wrangler)',
154
+ initial: false
155
+ })
156
+ }
157
+
158
+ // Initialize git
159
+ if (!flags.skipGit) {
160
+ questions.push({
161
+ type: 'confirm',
162
+ name: 'initGit',
163
+ message: 'Initialize git repository?',
164
+ initial: true
165
+ })
166
+ }
167
+
168
+ const answers = await prompts(questions, {
169
+ onCancel: () => {
170
+ throw new Error('cancelled')
171
+ }
172
+ })
173
+
174
+ return {
175
+ projectName: initialName || answers.projectName,
176
+ template: flags.template || answers.template,
177
+ databaseName: flags.databaseName || answers.databaseName || `${initialName || answers.projectName}-db`,
178
+ bucketName: flags.bucketName || answers.bucketName || `${initialName || answers.projectName}-media`,
179
+ includeExample: flags.skipExample ? false : (flags.includeExample ? true : (answers.includeExample !== undefined ? answers.includeExample : true)),
180
+ createResources: flags.skipCloudflare ? false : answers.createResources,
181
+ initGit: flags.skipGit ? false : answers.initGit,
182
+ skipInstall: flags.skipInstall
183
+ }
184
+ }
185
+
186
+ async function createProject(answers, flags) {
187
+ const {
188
+ projectName,
189
+ template,
190
+ databaseName,
191
+ bucketName,
192
+ includeExample,
193
+ createResources,
194
+ initGit,
195
+ skipInstall
196
+ } = answers
197
+
198
+ const targetDir = path.resolve(process.cwd(), projectName)
199
+
200
+ console.log()
201
+ const spinner = ora('Creating project...').start()
202
+
203
+ try {
204
+ // 1. Copy template
205
+ spinner.text = 'Copying template files...'
206
+ await copyTemplate(template, targetDir, {
207
+ projectName,
208
+ databaseName,
209
+ bucketName,
210
+ includeExample
211
+ })
212
+ spinner.succeed('Copied template files')
213
+
214
+ // 2. Create Cloudflare resources
215
+ let databaseId = 'YOUR_DATABASE_ID'
216
+ if (createResources) {
217
+ spinner.start('Creating Cloudflare resources...')
218
+ try {
219
+ databaseId = await createCloudflareResources(databaseName, bucketName, targetDir)
220
+ spinner.succeed('Created Cloudflare resources')
221
+ } catch (error) {
222
+ spinner.warn('Failed to create Cloudflare resources')
223
+ console.log(kleur.dim(' You can create them manually later'))
224
+ }
225
+ }
226
+
227
+ // 3. Update wrangler.toml with database ID
228
+ spinner.start('Updating configuration...')
229
+ await updateWranglerConfig(targetDir, { databaseName, databaseId, bucketName })
230
+ spinner.succeed('Updated configuration')
231
+
232
+ // 4. Install dependencies
233
+ if (!skipInstall) {
234
+ spinner.start('Installing dependencies...')
235
+ await installDependencies(targetDir)
236
+ spinner.succeed('Installed dependencies')
237
+ }
238
+
239
+ // 5. Initialize git
240
+ if (initGit) {
241
+ spinner.start('Initializing git repository...')
242
+ await initializeGit(targetDir)
243
+ spinner.succeed('Initialized git repository')
244
+ }
245
+
246
+ spinner.succeed(kleur.bold().green('✓ Project created successfully!'))
247
+
248
+ } catch (error) {
249
+ spinner.fail('Failed to create project')
250
+ throw error
251
+ }
252
+ }
253
+
254
+ async function copyTemplate(templateName, targetDir, options) {
255
+ const templateDir = path.resolve(__dirname, '../../../templates/starter')
256
+
257
+ // Check if template exists
258
+ if (!fs.existsSync(templateDir)) {
259
+ throw new Error(`Template "${templateName}" not found`)
260
+ }
261
+
262
+ // Copy template
263
+ await fs.copy(templateDir, targetDir, {
264
+ filter: (src) => {
265
+ // Skip node_modules, .git, dist, etc.
266
+ const name = path.basename(src)
267
+ if (['.git', 'node_modules', 'dist', '.wrangler', '.mf'].includes(name)) {
268
+ return false
269
+ }
270
+ return true
271
+ }
272
+ })
273
+
274
+ // Update package.json
275
+ const packageJsonPath = path.join(targetDir, 'package.json')
276
+ const packageJson = await fs.readJson(packageJsonPath)
277
+ packageJson.name = options.projectName
278
+ packageJson.version = '0.1.0'
279
+ packageJson.private = true
280
+
281
+ // Add @sonicjs-cms/core dependency
282
+ packageJson.dependencies = {
283
+ '@sonicjs-cms/core': '^2.0.0-alpha.2',
284
+ ...packageJson.dependencies
285
+ }
286
+
287
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 })
288
+
289
+ // Remove example collection if not wanted
290
+ if (!options.includeExample) {
291
+ const examplePath = path.join(targetDir, 'src/collections/blog-posts.collection.ts')
292
+ if (fs.existsSync(examplePath)) {
293
+ await fs.remove(examplePath)
294
+ }
295
+ }
296
+ }
297
+
298
+ async function createCloudflareResources(databaseName, bucketName, targetDir) {
299
+ // Check if wrangler is installed
300
+ try {
301
+ await execa('wrangler', ['--version'], { cwd: targetDir })
302
+ } catch {
303
+ throw new Error('wrangler is not installed. Install with: npm install -g wrangler')
304
+ }
305
+
306
+ // Create D1 database
307
+ let databaseId
308
+ try {
309
+ const { stdout } = await execa('wrangler', ['d1', 'create', databaseName], {
310
+ cwd: targetDir
311
+ })
312
+
313
+ // Parse database_id from output
314
+ const match = stdout.match(/database_id\s*=\s*["']([^"']+)["']/)
315
+ if (match) {
316
+ databaseId = match[1]
317
+ }
318
+ } catch (error) {
319
+ console.log(kleur.yellow(' D1 database creation failed'))
320
+ }
321
+
322
+ // Create R2 bucket
323
+ try {
324
+ await execa('wrangler', ['r2', 'bucket', 'create', bucketName], {
325
+ cwd: targetDir
326
+ })
327
+ } catch (error) {
328
+ console.log(kleur.yellow(' R2 bucket creation failed'))
329
+ }
330
+
331
+ return databaseId
332
+ }
333
+
334
+ async function updateWranglerConfig(targetDir, { databaseName, databaseId, bucketName }) {
335
+ const wranglerPath = path.join(targetDir, 'wrangler.toml')
336
+ let content = await fs.readFile(wranglerPath, 'utf-8')
337
+
338
+ // Update database_name
339
+ content = content.replace(/database_name\s*=\s*"[^"]*"/, `database_name = "${databaseName}"`)
340
+
341
+ // Update database_id
342
+ content = content.replace(/database_id\s*=\s*"[^"]*"/, `database_id = "${databaseId}"`)
343
+
344
+ // Update bucket_name
345
+ content = content.replace(/bucket_name\s*=\s*"[^"]*"/, `bucket_name = "${bucketName}"`)
346
+
347
+ await fs.writeFile(wranglerPath, content)
348
+ }
349
+
350
+ async function installDependencies(targetDir) {
351
+ // Detect package manager
352
+ const packageManager = await detectPackageManager()
353
+
354
+ const installCmd = packageManager === 'yarn' ? 'yarn' :
355
+ packageManager === 'pnpm' ? 'pnpm install' :
356
+ 'npm install'
357
+
358
+ await execa(packageManager, packageManager === 'yarn' ? [] : ['install'], {
359
+ cwd: targetDir,
360
+ stdio: 'ignore'
361
+ })
362
+ }
363
+
364
+ async function detectPackageManager() {
365
+ // Check parent directories for lock files
366
+ let dir = process.cwd()
367
+
368
+ while (dir !== path.parse(dir).root) {
369
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm'
370
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn'
371
+ if (fs.existsSync(path.join(dir, 'package-lock.json'))) return 'npm'
372
+ dir = path.dirname(dir)
373
+ }
374
+
375
+ return 'npm'
376
+ }
377
+
378
+ async function initializeGit(targetDir) {
379
+ try {
380
+ await execa('git', ['init'], { cwd: targetDir })
381
+ await execa('git', ['add', '.'], { cwd: targetDir })
382
+ await execa('git', ['commit', '-m', 'Initial commit from create-sonicjs-app'], {
383
+ cwd: targetDir
384
+ })
385
+ } catch (error) {
386
+ // Git init is optional, don't fail
387
+ }
388
+ }
389
+
390
+ function printSuccessMessage(answers) {
391
+ const { projectName, createResources, skipInstall } = answers
392
+
393
+ console.log()
394
+ console.log(kleur.bold().green('🎉 Success!'))
395
+ console.log()
396
+ console.log(kleur.bold('Next steps:'))
397
+ console.log()
398
+ console.log(kleur.cyan(` cd ${projectName}`))
399
+
400
+ if (skipInstall) {
401
+ console.log(kleur.cyan(' npm install'))
402
+ }
403
+
404
+ if (!createResources) {
405
+ console.log()
406
+ console.log(kleur.bold('Create Cloudflare resources:'))
407
+ console.log(kleur.cyan(` wrangler d1 create ${answers.databaseName}`))
408
+ console.log(kleur.dim(' # Copy database_id to wrangler.toml'))
409
+ console.log(kleur.cyan(` wrangler r2 bucket create ${answers.bucketName}`))
410
+ }
411
+
412
+ console.log()
413
+ console.log(kleur.bold('Run migrations:'))
414
+ console.log(kleur.cyan(' npm run db:migrate:local'))
415
+
416
+ console.log()
417
+ console.log(kleur.bold('Start development:'))
418
+ console.log(kleur.cyan(' npm run dev'))
419
+
420
+ console.log()
421
+ console.log(kleur.bold('Visit:'))
422
+ console.log(kleur.cyan(' http://localhost:8787/admin'))
423
+
424
+ console.log()
425
+ console.log(kleur.dim('Need help? Visit https://docs.sonicjs.com'))
426
+ console.log()
427
+ }
428
+
429
+ // Run
430
+ main()
@@ -0,0 +1,132 @@
1
+ # My SonicJS Application
2
+
3
+ A modern headless CMS built with [SonicJS](https://sonicjs.com) on Cloudflare's edge platform.
4
+
5
+ ## Getting Started
6
+
7
+ ### Prerequisites
8
+
9
+ - Node.js 18 or higher
10
+ - A Cloudflare account (free tier works great)
11
+ - Wrangler CLI (installed with dependencies)
12
+
13
+ ### Installation
14
+
15
+ 1. **Install dependencies:**
16
+ ```bash
17
+ npm install
18
+ ```
19
+
20
+ 2. **Create your D1 database:**
21
+ ```bash
22
+ npx wrangler d1 create my-sonicjs-db
23
+ ```
24
+
25
+ Copy the `database_id` from the output and update it in `wrangler.toml`.
26
+
27
+ 3. **Create your R2 bucket:**
28
+ ```bash
29
+ npx wrangler r2 bucket create my-sonicjs-media
30
+ ```
31
+
32
+ 4. **Run migrations:**
33
+ ```bash
34
+ npm run db:migrate:local
35
+ ```
36
+
37
+ 5. **Start the development server:**
38
+ ```bash
39
+ npm run dev
40
+ ```
41
+
42
+ 6. **Open your browser:**
43
+ Navigate to `http://localhost:8787/admin` to access the admin interface.
44
+
45
+ Default credentials:
46
+ - Email: `admin@sonicjs.com`
47
+ - Password: `admin`
48
+
49
+ ## Project Structure
50
+
51
+ ```
52
+ my-sonicjs-app/
53
+ ├── src/
54
+ │ ├── collections/ # Your content type definitions
55
+ │ │ └── blog-posts.collection.ts
56
+ │ └── index.ts # Application entry point
57
+ ├── wrangler.toml # Cloudflare Workers configuration
58
+ ├── package.json
59
+ └── tsconfig.json
60
+ ```
61
+
62
+ ## Available Scripts
63
+
64
+ - `npm run dev` - Start development server
65
+ - `npm run deploy` - Deploy to Cloudflare
66
+ - `npm run db:migrate` - Run migrations on production database
67
+ - `npm run db:migrate:local` - Run migrations locally
68
+ - `npm run type-check` - Check TypeScript types
69
+ - `npm run test` - Run tests
70
+
71
+ ## Creating Collections
72
+
73
+ Collections define your content types. Create a new file in `src/collections/`:
74
+
75
+ ```typescript
76
+ // src/collections/products.collection.ts
77
+ import type { CollectionConfig } from '@sonicjs-cms/core'
78
+
79
+ export default {
80
+ name: 'products',
81
+ label: 'Products',
82
+ fields: {
83
+ name: { type: 'text', required: true },
84
+ price: { type: 'number', required: true },
85
+ description: { type: 'markdown' }
86
+ }
87
+ } satisfies CollectionConfig
88
+ ```
89
+
90
+ ## API Access
91
+
92
+ Your collections are automatically available via REST API:
93
+
94
+ - `GET /api/content/blog-posts` - List all blog posts
95
+ - `GET /api/content/blog-posts/:id` - Get a single post
96
+ - `POST /api/content/blog-posts` - Create a post (requires auth)
97
+ - `PUT /api/content/blog-posts/:id` - Update a post (requires auth)
98
+ - `DELETE /api/content/blog-posts/:id` - Delete a post (requires auth)
99
+
100
+ ## Deployment
101
+
102
+ 1. **Login to Cloudflare:**
103
+ ```bash
104
+ npx wrangler login
105
+ ```
106
+
107
+ 2. **Deploy your application:**
108
+ ```bash
109
+ npm run deploy
110
+ ```
111
+
112
+ 3. **Run migrations on production:**
113
+ ```bash
114
+ npm run db:migrate
115
+ ```
116
+
117
+ ## Documentation
118
+
119
+ - [SonicJS Documentation](https://docs.sonicjs.com)
120
+ - [Collection Configuration](https://docs.sonicjs.com/collections)
121
+ - [Plugin Development](https://docs.sonicjs.com/plugins)
122
+ - [API Reference](https://docs.sonicjs.com/api)
123
+
124
+ ## Support
125
+
126
+ - [GitHub Issues](https://github.com/sonicjs/sonicjs/issues)
127
+ - [Discord Community](https://discord.gg/sonicjs)
128
+ - [Documentation](https://docs.sonicjs.com)
129
+
130
+ ## License
131
+
132
+ MIT