create-sonicjs 3.0.0-beta.2 → 3.0.0-beta.21
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/package.json +1 -1
- package/src/cli.js +101 -136
- package/src/telemetry.js +10 -5
- package/templates/starter/package.json +2 -1
- package/templates/starter/scripts/update-plugin.ts +101 -0
- package/templates/starter/src/collections/blog-posts.collection.ts +32 -36
- package/templates/starter/src/index.ts +15 -9
- package/templates/starter/src/plugins/example/collections/moods.collection.ts +56 -0
- package/templates/starter/src/plugins/example/index.ts +486 -0
- package/templates/starter/src/plugins/example/routes/admin.ts +159 -0
- package/templates/starter/src/plugins/example/routes/api.ts +125 -0
- package/templates/starter/wrangler.toml +2 -2
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
+
import { randomBytes } from 'crypto'
|
|
6
7
|
import prompts from 'prompts'
|
|
7
8
|
import kleur from 'kleur'
|
|
8
9
|
import ora from 'ora'
|
|
@@ -50,7 +51,9 @@ const flags = {
|
|
|
50
51
|
databaseName: args.find(arg => arg.startsWith('--database='))?.split('=')[1],
|
|
51
52
|
bucketName: args.find(arg => arg.startsWith('--bucket='))?.split('=')[1],
|
|
52
53
|
skipExample: args.includes('--skip-example'),
|
|
53
|
-
includeExample: args.includes('--include-example')
|
|
54
|
+
includeExample: args.includes('--include-example'),
|
|
55
|
+
adminEmail: args.find(arg => arg.startsWith('--admin-email='))?.split('=')[1],
|
|
56
|
+
adminPassword: args.find(arg => arg.startsWith('--admin-password='))?.split('=')[1],
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
async function main() {
|
|
@@ -171,48 +174,39 @@ async function getProjectDetails(initialName) {
|
|
|
171
174
|
})
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
// Seed admin user
|
|
175
|
-
|
|
176
|
-
type: 'confirm',
|
|
177
|
-
name: 'seedAdmin',
|
|
178
|
-
message: 'Create admin user?',
|
|
179
|
-
initial: true
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
// Admin email (only if seeding)
|
|
183
|
-
questions.push({
|
|
184
|
-
type: (prev, values) => values.seedAdmin ? 'text' : null,
|
|
185
|
-
name: 'adminEmail',
|
|
186
|
-
message: 'Admin email:',
|
|
187
|
-
validate: (value) => {
|
|
188
|
-
if (!value) return 'Admin email is required'
|
|
189
|
-
// Basic email validation
|
|
190
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
191
|
-
if (!emailRegex.test(value)) return 'Please enter a valid email address'
|
|
192
|
-
return true
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// Admin password (only if seeding)
|
|
197
|
-
questions.push({
|
|
198
|
-
type: (prev, values) => values.seedAdmin ? 'password' : null,
|
|
199
|
-
name: 'adminPassword',
|
|
200
|
-
message: 'Admin password:',
|
|
201
|
-
validate: (value) => {
|
|
202
|
-
if (!value) return 'Admin password is required'
|
|
203
|
-
if (value.length < 8) return 'Password must be at least 8 characters'
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
// Include example collection (only ask if neither flag is set)
|
|
209
|
-
if (!flags.skipExample && !flags.includeExample) {
|
|
177
|
+
// Seed admin user (skip prompt if credentials provided via flags)
|
|
178
|
+
if (!flags.adminEmail || !flags.adminPassword) {
|
|
210
179
|
questions.push({
|
|
211
180
|
type: 'confirm',
|
|
212
|
-
name: '
|
|
213
|
-
message: '
|
|
181
|
+
name: 'seedAdmin',
|
|
182
|
+
message: 'Create admin user?',
|
|
214
183
|
initial: true
|
|
215
184
|
})
|
|
185
|
+
|
|
186
|
+
// Admin email (only if seeding)
|
|
187
|
+
questions.push({
|
|
188
|
+
type: (prev, values) => values.seedAdmin ? 'text' : null,
|
|
189
|
+
name: 'adminEmail',
|
|
190
|
+
message: 'Admin email:',
|
|
191
|
+
validate: (value) => {
|
|
192
|
+
if (!value) return 'Admin email is required'
|
|
193
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
194
|
+
if (!emailRegex.test(value)) return 'Please enter a valid email address'
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Admin password (only if seeding)
|
|
200
|
+
questions.push({
|
|
201
|
+
type: (prev, values) => values.seedAdmin ? 'password' : null,
|
|
202
|
+
name: 'adminPassword',
|
|
203
|
+
message: 'Admin password:',
|
|
204
|
+
validate: (value) => {
|
|
205
|
+
if (!value) return 'Admin password is required'
|
|
206
|
+
if (value.length < 8) return 'Password must be at least 8 characters'
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
})
|
|
216
210
|
}
|
|
217
211
|
|
|
218
212
|
// Create Cloudflare resources
|
|
@@ -246,10 +240,10 @@ async function getProjectDetails(initialName) {
|
|
|
246
240
|
template: flags.template || 'starter', // Always default to starter template
|
|
247
241
|
databaseName: flags.databaseName || answers.databaseName || `${initialName || answers.projectName}-db`,
|
|
248
242
|
bucketName: flags.bucketName || answers.bucketName || `${initialName || answers.projectName}-media`,
|
|
249
|
-
seedAdmin: answers.seedAdmin !== undefined ? answers.seedAdmin : true,
|
|
250
|
-
adminEmail: answers.adminEmail,
|
|
251
|
-
adminPassword: answers.adminPassword,
|
|
252
|
-
includeExample:
|
|
243
|
+
seedAdmin: (flags.adminEmail && flags.adminPassword) ? true : (answers.seedAdmin !== undefined ? answers.seedAdmin : true),
|
|
244
|
+
adminEmail: flags.adminEmail || answers.adminEmail,
|
|
245
|
+
adminPassword: flags.adminPassword || answers.adminPassword,
|
|
246
|
+
includeExample: true,
|
|
253
247
|
createResources: flags.skipCloudflare ? false : answers.createResources,
|
|
254
248
|
runMigrations: true, // Always run migrations automatically
|
|
255
249
|
initGit: flags.skipGit ? false : answers.initGit,
|
|
@@ -415,7 +409,7 @@ async function copyTemplate(templateName, targetDir, options) {
|
|
|
415
409
|
|
|
416
410
|
// Add @sonicjs-cms/core dependency
|
|
417
411
|
packageJson.dependencies = {
|
|
418
|
-
'@sonicjs-cms/core': '^3.0.0-beta.
|
|
412
|
+
'@sonicjs-cms/core': '^3.0.0-beta.21',
|
|
419
413
|
...packageJson.dependencies
|
|
420
414
|
}
|
|
421
415
|
|
|
@@ -454,100 +448,88 @@ async function copyTemplate(templateName, targetDir, options) {
|
|
|
454
448
|
password: options.adminPassword
|
|
455
449
|
})
|
|
456
450
|
}
|
|
451
|
+
|
|
452
|
+
// Generate .dev.vars with a random BETTER_AUTH_SECRET for local dev
|
|
453
|
+
const devVarsPath = path.join(targetDir, '.dev.vars')
|
|
454
|
+
if (!fs.existsSync(devVarsPath)) {
|
|
455
|
+
const secret = randomBytes(32).toString('hex')
|
|
456
|
+
await fs.writeFile(devVarsPath, `BETTER_AUTH_SECRET="${secret}"\n`)
|
|
457
|
+
}
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
async function createAdminSeedScript(targetDir, { email, password }) {
|
|
460
|
-
const seedScriptContent = `import {
|
|
461
|
-
import { eq } from 'drizzle-orm'
|
|
462
|
-
import * as crypto from 'crypto'
|
|
461
|
+
const seedScriptContent = `import { bootstrapDocumentTypes, RbacService } from '@sonicjs-cms/core'
|
|
463
462
|
import { getPlatformProxy } from 'wrangler'
|
|
464
463
|
|
|
465
464
|
/**
|
|
466
465
|
* Seed script to create initial admin user
|
|
467
466
|
*
|
|
468
|
-
* Run this script after migrations:
|
|
469
|
-
* npm run db:migrate:local
|
|
470
|
-
* npm run seed
|
|
471
|
-
*
|
|
472
467
|
* Admin credentials:
|
|
473
468
|
* Email: ${email}
|
|
474
469
|
* Password: [as entered during setup]
|
|
475
470
|
*/
|
|
476
471
|
|
|
472
|
+
async function hashPassword(password) {
|
|
473
|
+
const iterations = 100000
|
|
474
|
+
const salt = new Uint8Array(16)
|
|
475
|
+
crypto.getRandomValues(salt)
|
|
476
|
+
const encoder = new TextEncoder()
|
|
477
|
+
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits'])
|
|
478
|
+
const hashBuffer = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' }, keyMaterial, 256)
|
|
479
|
+
const saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
480
|
+
const hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
481
|
+
return \`pbkdf2:\${iterations}:\${saltHex}:\${hashHex}\`
|
|
482
|
+
}
|
|
483
|
+
|
|
477
484
|
async function seed() {
|
|
478
|
-
// Get D1 database from Cloudflare environment using wrangler's getPlatformProxy
|
|
479
485
|
const { env, dispose } = await getPlatformProxy()
|
|
480
486
|
|
|
481
487
|
if (!env?.DB) {
|
|
482
|
-
console.error('❌ Error: DB binding not found')
|
|
483
|
-
console.error('')
|
|
484
|
-
console.error('Make sure you have:')
|
|
485
|
-
console.error('1. Created your D1 database: wrangler d1 create <database-name>')
|
|
486
|
-
console.error('2. Updated wrangler.toml with the database_id')
|
|
487
|
-
console.error('3. Run migrations: npm run db:migrate:local')
|
|
488
|
-
console.error('')
|
|
488
|
+
console.error('❌ Error: DB binding not found. Run migrations first: npm run db:migrate:local')
|
|
489
489
|
process.exit(1)
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
const db = createDb(env.DB)
|
|
493
|
-
|
|
494
492
|
try {
|
|
495
493
|
// Check if admin user already exists
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
.from(users)
|
|
499
|
-
.where(eq(users.email, '${email}'))
|
|
500
|
-
.get()
|
|
501
|
-
|
|
502
|
-
if (existingUser) {
|
|
494
|
+
const existing = await env.DB.prepare('SELECT id FROM auth_user WHERE email = ?').bind('${email}').first()
|
|
495
|
+
if (existing) {
|
|
503
496
|
console.log('✓ Admin user already exists')
|
|
504
|
-
|
|
505
|
-
console.log(\` Role: \${existingUser.role}\`)
|
|
497
|
+
await dispose()
|
|
506
498
|
return
|
|
507
499
|
}
|
|
508
500
|
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
isActive: true,
|
|
527
|
-
createdAt: now,
|
|
528
|
-
updatedAt: now
|
|
529
|
-
})
|
|
530
|
-
.run()
|
|
501
|
+
const passwordHash = await hashPassword('${password}')
|
|
502
|
+
const nowMs = Date.now()
|
|
503
|
+
const odid = \`admin-\${nowMs}-\${Math.random().toString(36).substr(2, 9)}\`
|
|
504
|
+
|
|
505
|
+
await env.DB.batch([
|
|
506
|
+
env.DB.prepare(
|
|
507
|
+
'INSERT INTO auth_user (id, email, first_name, last_name, role, is_active, created_at, updated_at, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
508
|
+
).bind(odid, '${email}', 'Admin', 'User', 'admin', 1, nowMs, nowMs, 'Admin User'),
|
|
509
|
+
env.DB.prepare(
|
|
510
|
+
'INSERT INTO auth_account (id, user_id, account_id, provider_id, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
511
|
+
).bind(crypto.randomUUID(), odid, odid, 'credential', passwordHash, nowMs, nowMs),
|
|
512
|
+
])
|
|
513
|
+
|
|
514
|
+
await bootstrapDocumentTypes(env.DB)
|
|
515
|
+
const rbac = new RbacService(env.DB)
|
|
516
|
+
await rbac.ensureSystemRbacSeed()
|
|
517
|
+
await rbac.addUserRoleByName(odid, 'admin')
|
|
531
518
|
|
|
532
519
|
console.log('✓ Admin user created successfully')
|
|
533
520
|
console.log(\` Email: ${email}\`)
|
|
534
521
|
console.log(\` Role: admin\`)
|
|
535
|
-
console.log('')
|
|
536
|
-
console.log('You can now login at: http://localhost:8787/auth/login')
|
|
537
522
|
} catch (error) {
|
|
538
523
|
console.error('❌ Error creating admin user:', error)
|
|
539
524
|
await dispose()
|
|
540
525
|
process.exit(1)
|
|
541
526
|
}
|
|
542
527
|
|
|
543
|
-
// Clean up the platform proxy
|
|
544
528
|
await dispose()
|
|
545
529
|
}
|
|
546
530
|
|
|
547
|
-
// Run seed
|
|
548
531
|
seed()
|
|
549
532
|
.then(() => {
|
|
550
|
-
console.log('')
|
|
551
533
|
console.log('✓ Seeding complete')
|
|
552
534
|
process.exit(0)
|
|
553
535
|
})
|
|
@@ -607,11 +589,19 @@ Or they should be automatically available after installation.
|
|
|
607
589
|
}
|
|
608
590
|
|
|
609
591
|
async function createCloudflareResources(databaseName, bucketName, targetDir) {
|
|
610
|
-
//
|
|
592
|
+
// Resolve wrangler binary — prefer global, fall back to npx
|
|
593
|
+
let wranglerCmd = 'wrangler'
|
|
594
|
+
let wranglerArgs = []
|
|
611
595
|
try {
|
|
612
596
|
await execa('wrangler', ['--version'], { cwd: targetDir })
|
|
613
597
|
} catch {
|
|
614
|
-
|
|
598
|
+
try {
|
|
599
|
+
await execa('npx', ['--yes', 'wrangler', '--version'], { cwd: targetDir })
|
|
600
|
+
wranglerCmd = 'npx'
|
|
601
|
+
wranglerArgs = ['wrangler']
|
|
602
|
+
} catch {
|
|
603
|
+
throw new Error('wrangler is not installed. Install with: npm install -g wrangler')
|
|
604
|
+
}
|
|
615
605
|
}
|
|
616
606
|
|
|
617
607
|
let databaseId
|
|
@@ -620,7 +610,7 @@ async function createCloudflareResources(databaseName, bucketName, targetDir) {
|
|
|
620
610
|
|
|
621
611
|
// Create D1 database
|
|
622
612
|
try {
|
|
623
|
-
const { stdout, stderr } = await execa(
|
|
613
|
+
const { stdout, stderr } = await execa(wranglerCmd, [...wranglerArgs, 'd1', 'create', databaseName], {
|
|
624
614
|
cwd: targetDir
|
|
625
615
|
})
|
|
626
616
|
|
|
@@ -648,7 +638,7 @@ async function createCloudflareResources(databaseName, bucketName, targetDir) {
|
|
|
648
638
|
|
|
649
639
|
// Create R2 bucket
|
|
650
640
|
try {
|
|
651
|
-
await execa(
|
|
641
|
+
await execa(wranglerCmd, [...wranglerArgs, 'r2', 'bucket', 'create', bucketName], {
|
|
652
642
|
cwd: targetDir
|
|
653
643
|
})
|
|
654
644
|
bucketCreated = true
|
|
@@ -774,49 +764,25 @@ function printSuccessMessage(answers) {
|
|
|
774
764
|
console.log()
|
|
775
765
|
console.log(kleur.bold().green('🎉 Success!'))
|
|
776
766
|
console.log()
|
|
777
|
-
console.log(kleur.bold('
|
|
767
|
+
console.log(kleur.bold('Get started:'))
|
|
778
768
|
console.log()
|
|
779
769
|
console.log(kleur.cyan(` cd ${projectName}`))
|
|
780
770
|
|
|
781
771
|
if (skipInstall) {
|
|
782
772
|
console.log(kleur.cyan(' npm install'))
|
|
783
773
|
console.log()
|
|
784
|
-
console.log(kleur.yellow('⚠
|
|
774
|
+
console.log(kleur.yellow('⚠ After npm install, copy migrations:'))
|
|
785
775
|
console.log(kleur.dim(' cp -r node_modules/@sonicjs-cms/core/migrations ./'))
|
|
786
776
|
}
|
|
787
777
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
console.log()
|
|
791
|
-
console.log(kleur.bold('Create Cloudflare resources:'))
|
|
792
|
-
if (!databaseIdSet) {
|
|
793
|
-
console.log(kleur.cyan(` wrangler d1 create ${answers.databaseName}`))
|
|
794
|
-
console.log(kleur.dim(' # Copy database_id to wrangler.toml'))
|
|
795
|
-
}
|
|
796
|
-
console.log(kleur.cyan(` wrangler r2 bucket create ${answers.bucketName}`))
|
|
778
|
+
if (!migrationsRan) {
|
|
779
|
+
console.log(kleur.cyan(' npm run db:migrate:local'))
|
|
797
780
|
}
|
|
798
781
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const needsSeeding = seedAdmin && !adminSeeded
|
|
802
|
-
|
|
803
|
-
if (needsMigrations || needsSeeding) {
|
|
804
|
-
console.log()
|
|
805
|
-
console.log(kleur.bold('Complete setup:'))
|
|
806
|
-
if (needsMigrations) {
|
|
807
|
-
console.log(kleur.cyan(' npm run db:migrate:local'))
|
|
808
|
-
}
|
|
809
|
-
if (needsSeeding) {
|
|
810
|
-
console.log(kleur.cyan(' npm run seed'))
|
|
811
|
-
}
|
|
782
|
+
if (seedAdmin && !adminSeeded) {
|
|
783
|
+
console.log(kleur.cyan(' npm run seed'))
|
|
812
784
|
}
|
|
813
785
|
|
|
814
|
-
console.log()
|
|
815
|
-
if (migrationsRan && (!seedAdmin || adminSeeded)) {
|
|
816
|
-
console.log(kleur.bold().green('✓ Database is ready! Start development:'))
|
|
817
|
-
} else {
|
|
818
|
-
console.log(kleur.bold('Start development:'))
|
|
819
|
-
}
|
|
820
786
|
console.log(kleur.cyan(' npm run dev'))
|
|
821
787
|
|
|
822
788
|
if (seedAdmin && answers.adminEmail) {
|
|
@@ -826,15 +792,14 @@ function printSuccessMessage(answers) {
|
|
|
826
792
|
console.log(kleur.dim(` Password: [as entered]`))
|
|
827
793
|
}
|
|
828
794
|
|
|
829
|
-
if (migrationsRan && (!seedAdmin || adminSeeded)) {
|
|
830
|
-
console.log()
|
|
831
|
-
console.log(kleur.green('✓ Everything is set up! Just run npm run dev and login.'))
|
|
832
|
-
}
|
|
833
|
-
|
|
834
795
|
console.log()
|
|
835
796
|
console.log(kleur.bold('Visit:'))
|
|
836
797
|
console.log(kleur.cyan(' http://localhost:8787/admin'))
|
|
837
798
|
|
|
799
|
+
console.log()
|
|
800
|
+
console.log(kleur.bold('Deploy to Cloudflare (when ready):'))
|
|
801
|
+
console.log(kleur.cyan(' npm run deploy'))
|
|
802
|
+
|
|
838
803
|
console.log()
|
|
839
804
|
console.log(kleur.dim('Need help? Visit https://sonicjs.com'))
|
|
840
805
|
console.log()
|
package/src/telemetry.js
CHANGED
|
@@ -76,6 +76,8 @@ async function initTelemetry() {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const pendingRequests = []
|
|
80
|
+
|
|
79
81
|
/**
|
|
80
82
|
* Track an event using custom SonicJS stats endpoint
|
|
81
83
|
*/
|
|
@@ -105,12 +107,13 @@ async function track(event, properties = {}) {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
fetch(`${TELEMETRY_ENDPOINT}/v1/events`, {
|
|
110
|
+
const req = fetch(`${TELEMETRY_ENDPOINT}/v1/events`, {
|
|
110
111
|
method: 'POST',
|
|
111
112
|
headers: { 'Content-Type': 'application/json' },
|
|
112
113
|
body: JSON.stringify(payload)
|
|
113
|
-
}).catch(() => {})
|
|
114
|
+
}).catch(() => {})
|
|
115
|
+
|
|
116
|
+
pendingRequests.push(req)
|
|
114
117
|
|
|
115
118
|
if (DEBUG) {
|
|
116
119
|
console.log('[Telemetry] Tracked:', event, payload)
|
|
@@ -124,10 +127,12 @@ async function track(event, properties = {}) {
|
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|
|
127
|
-
* Shutdown telemetry (
|
|
130
|
+
* Shutdown telemetry — flush pending requests (max 3s wait)
|
|
128
131
|
*/
|
|
129
132
|
async function shutdown() {
|
|
130
|
-
|
|
133
|
+
if (pendingRequests.length === 0) return
|
|
134
|
+
const timeout = new Promise(resolve => setTimeout(resolve, 3000))
|
|
135
|
+
await Promise.race([Promise.all(pendingRequests), timeout])
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
/**
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"test": "vitest --run",
|
|
16
16
|
"test:watch": "vitest",
|
|
17
17
|
"update": "npm install @sonicjs-cms/core@latest --save",
|
|
18
|
-
"update:beta": "npm install @sonicjs-cms/core@beta --save"
|
|
18
|
+
"update:beta": "npm install @sonicjs-cms/core@beta --save",
|
|
19
|
+
"update:plugin": "tsx scripts/update-plugin.ts"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {},
|
|
21
22
|
"devDependencies": {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-plugin.ts
|
|
3
|
+
*
|
|
4
|
+
* Downloads the latest version of a bundled plugin from the SonicJS starter
|
|
5
|
+
* template on GitHub and overwrites the local copies.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx scripts/update-plugin.ts [plugin-name]
|
|
9
|
+
* npx tsx scripts/update-plugin.ts example # default
|
|
10
|
+
* npx tsx scripts/update-plugin.ts example --dry-run
|
|
11
|
+
*
|
|
12
|
+
* Why this exists:
|
|
13
|
+
* `create-sonicjs` scaffolds plugin source files into your project — they are
|
|
14
|
+
* your code, not a package dependency. Running `npm install` updates
|
|
15
|
+
* @sonicjs-cms/core but leaves the plugin files untouched. This script pulls
|
|
16
|
+
* the latest files from the canonical starter template on GitHub main and
|
|
17
|
+
* overwrites your local copies, giving you bug fixes and improvements without
|
|
18
|
+
* starting a fresh project.
|
|
19
|
+
*
|
|
20
|
+
* Caution:
|
|
21
|
+
* This overwrites the plugin files — commit or stash local changes first.
|
|
22
|
+
* Custom modifications you've made will be lost.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { writeFileSync, mkdirSync } from 'fs'
|
|
26
|
+
import { dirname, join } from 'path'
|
|
27
|
+
import { fileURLToPath } from 'url'
|
|
28
|
+
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
30
|
+
const PROJECT_ROOT = join(__dirname, '..')
|
|
31
|
+
|
|
32
|
+
const GITHUB_RAW_BASE =
|
|
33
|
+
'https://raw.githubusercontent.com/SonicJs-Org/sonicjs/main/packages/create-app/templates/starter/src/plugins'
|
|
34
|
+
|
|
35
|
+
// Map of plugin name → files relative to its plugin directory.
|
|
36
|
+
// Add new plugins here as they are added to the starter template.
|
|
37
|
+
const PLUGIN_MANIFEST: Record<string, string[]> = {
|
|
38
|
+
example: [
|
|
39
|
+
'index.ts',
|
|
40
|
+
'routes/api.ts',
|
|
41
|
+
'routes/admin.ts',
|
|
42
|
+
'collections/moods.collection.ts',
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchText(url: string): Promise<string> {
|
|
47
|
+
const res = await fetch(url)
|
|
48
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`)
|
|
49
|
+
return res.text()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function updatePlugin(pluginName: string, dryRun: boolean) {
|
|
53
|
+
const files = PLUGIN_MANIFEST[pluginName]
|
|
54
|
+
if (!files) {
|
|
55
|
+
console.error(
|
|
56
|
+
`Unknown plugin "${pluginName}". Available: ${Object.keys(PLUGIN_MANIFEST).join(', ')}`
|
|
57
|
+
)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(
|
|
62
|
+
`\n${dryRun ? '[dry-run] ' : ''}Updating plugin: ${pluginName} from SonicJS main branch\n`
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const url = `${GITHUB_RAW_BASE}/${pluginName}/${file}`
|
|
67
|
+
const localPath = join(PROJECT_ROOT, 'src', 'plugins', pluginName, file)
|
|
68
|
+
|
|
69
|
+
process.stdout.write(` fetching ${file} ... `)
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const content = await fetchText(url)
|
|
73
|
+
if (!dryRun) {
|
|
74
|
+
mkdirSync(dirname(localPath), { recursive: true })
|
|
75
|
+
writeFileSync(localPath, content, 'utf8')
|
|
76
|
+
}
|
|
77
|
+
console.log('✓')
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log('✗')
|
|
80
|
+
console.error(` Error: ${(err as Error).message}`)
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(
|
|
86
|
+
`\n${dryRun ? '[dry-run] no files written — ' : ''}Done. ${files.length} file(s) updated.\n`
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
console.log('Next steps:')
|
|
91
|
+
console.log(' 1. Review the changes: git diff src/plugins/' + pluginName)
|
|
92
|
+
console.log(' 2. Run type check: npm run type-check')
|
|
93
|
+
console.log(' 3. Commit if happy: git add src/plugins/' + pluginName + ' && git commit -m "chore(plugins): update ' + pluginName + ' plugin"\n')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const args = process.argv.slice(2)
|
|
98
|
+
const pluginName = args.find(a => !a.startsWith('--')) ?? 'example'
|
|
99
|
+
const dryRun = args.includes('--dry-run')
|
|
100
|
+
|
|
101
|
+
updatePlugin(pluginName, dryRun)
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Example collection configuration for blog posts
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { CollectionConfig } from '@sonicjs-cms/core'
|
|
7
|
+
import type { CollectionConfig } from '@sonicjs-cms/core';
|
|
8
8
|
|
|
9
9
|
export default {
|
|
10
|
-
name: '
|
|
11
|
-
displayName: 'Blog
|
|
10
|
+
name: 'blog_post',
|
|
11
|
+
displayName: 'Blog Post',
|
|
12
|
+
slug: 'blog-posts',
|
|
12
13
|
description: 'Manage your blog posts',
|
|
13
14
|
icon: '📝',
|
|
14
15
|
|
|
@@ -19,57 +20,52 @@ export default {
|
|
|
19
20
|
type: 'string',
|
|
20
21
|
title: 'Title',
|
|
21
22
|
required: true,
|
|
22
|
-
maxLength: 200
|
|
23
|
+
maxLength: 200,
|
|
23
24
|
},
|
|
24
25
|
slug: {
|
|
25
26
|
type: 'slug',
|
|
26
27
|
title: 'URL Slug',
|
|
27
28
|
required: true,
|
|
28
|
-
maxLength: 200
|
|
29
|
-
},
|
|
30
|
-
excerpt: {
|
|
31
|
-
type: 'textarea',
|
|
32
|
-
title: 'Excerpt',
|
|
33
|
-
maxLength: 500,
|
|
34
|
-
helpText: 'A short summary of the post'
|
|
29
|
+
maxLength: 200,
|
|
35
30
|
},
|
|
36
31
|
content: {
|
|
37
|
-
type: '
|
|
32
|
+
type: 'lexical',
|
|
38
33
|
title: 'Content',
|
|
39
|
-
required: true
|
|
40
|
-
},
|
|
41
|
-
featuredImage: {
|
|
42
|
-
type: 'media',
|
|
43
|
-
title: 'Featured Image'
|
|
34
|
+
required: true,
|
|
44
35
|
},
|
|
45
36
|
author: {
|
|
46
|
-
type: '
|
|
37
|
+
type: 'user',
|
|
47
38
|
title: 'Author',
|
|
48
|
-
required: true
|
|
39
|
+
required: true,
|
|
49
40
|
},
|
|
50
41
|
publishedAt: {
|
|
51
42
|
type: 'datetime',
|
|
52
|
-
title: 'Published Date'
|
|
53
|
-
},
|
|
54
|
-
status: {
|
|
55
|
-
type: 'select',
|
|
56
|
-
title: 'Status',
|
|
57
|
-
enum: ['draft', 'published', 'archived'],
|
|
58
|
-
enumLabels: ['Draft', 'Published', 'Archived'],
|
|
59
|
-
default: 'draft'
|
|
43
|
+
title: 'Published Date',
|
|
60
44
|
},
|
|
61
|
-
tags: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
title: 'Tags',
|
|
64
|
-
helpText: 'Comma-separated tags'
|
|
65
|
-
}
|
|
66
45
|
},
|
|
67
|
-
required: ['title', 'slug', 'content', 'author']
|
|
46
|
+
required: ['title', 'slug', 'content', 'author'],
|
|
68
47
|
},
|
|
69
48
|
|
|
70
49
|
// List view configuration
|
|
71
50
|
listFields: ['title', 'author', 'status', 'publishedAt'],
|
|
72
|
-
searchFields: ['title', '
|
|
51
|
+
searchFields: ['title', 'content', 'author'],
|
|
73
52
|
defaultSort: 'createdAt',
|
|
74
|
-
defaultSortOrder: 'desc'
|
|
75
|
-
|
|
53
|
+
defaultSortOrder: 'desc',
|
|
54
|
+
|
|
55
|
+
// Mark as config-managed (code-based) collection
|
|
56
|
+
managed: true,
|
|
57
|
+
isActive: true,
|
|
58
|
+
|
|
59
|
+
// Opt in to public read access. Without this, only authenticated users
|
|
60
|
+
// (admin/editor) can read content via the API. See docs/authentication.md.
|
|
61
|
+
access: {
|
|
62
|
+
public: ['read'],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Per-collection cache override. TTL in seconds; falls back to the cache plugin
|
|
66
|
+
// default (CACHE_CONFIGS.api.ttl, currently 300s) if unset.
|
|
67
|
+
cache: {
|
|
68
|
+
enabled: true,
|
|
69
|
+
ttl: 5,
|
|
70
|
+
},
|
|
71
|
+
} satisfies CollectionConfig;
|