native-update 1.0.8 → 1.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.
@@ -0,0 +1,582 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ import ora from 'ora';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ export async function createBackend(type, options) {
12
+ const spinner = ora(`Creating ${type} backend template...`).start();
13
+
14
+ try {
15
+ const outputDir = path.resolve(options.output);
16
+
17
+ // Check if directory exists
18
+ try {
19
+ await fs.access(outputDir);
20
+ spinner.fail(`Directory ${outputDir} already exists`);
21
+ return;
22
+ } catch {
23
+ // Directory doesn't exist, good
24
+ }
25
+
26
+ // Create output directory
27
+ await fs.mkdir(outputDir, { recursive: true });
28
+
29
+ switch (type) {
30
+ case 'express':
31
+ await createExpressBackend(outputDir, options);
32
+ break;
33
+ case 'firebase':
34
+ await createFirebaseBackend(outputDir, options);
35
+ break;
36
+ case 'vercel':
37
+ await createVercelBackend(outputDir, options);
38
+ break;
39
+ default:
40
+ throw new Error(`Unknown backend type: ${type}`);
41
+ }
42
+
43
+ spinner.succeed(`${type} backend created successfully!`);
44
+
45
+ console.log('');
46
+ console.log(chalk.bold('Next steps:'));
47
+ console.log(chalk.gray(` 1. cd ${options.output}`));
48
+ console.log(chalk.gray(` 2. npm install`));
49
+ console.log(chalk.gray(` 3. Configure your environment variables`));
50
+ console.log(chalk.gray(` 4. npm run dev`));
51
+
52
+ } catch (error) {
53
+ spinner.fail(`Failed to create backend: ${error.message}`);
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ async function createExpressBackend(outputDir, options) {
59
+ // Create package.json
60
+ const packageJson = {
61
+ name: "native-update-backend",
62
+ version: "1.0.0",
63
+ description: "Native Update backend server",
64
+ type: "module",
65
+ scripts: {
66
+ dev: "node --watch server.js",
67
+ start: "node server.js",
68
+ test: "vitest"
69
+ },
70
+ dependencies: {
71
+ express: "^5.1.0",
72
+ cors: "^2.8.5",
73
+ "express-rate-limit": "^7.4.1",
74
+ multer: "^1.4.5-lts.1",
75
+ dotenv: "^16.4.7",
76
+ jsonwebtoken: "^9.0.2",
77
+ bcrypt: "^5.1.1"
78
+ }
79
+ };
80
+
81
+ if (options.withMonitoring) {
82
+ packageJson.dependencies["@opentelemetry/api"] = "^1.9.0";
83
+ packageJson.dependencies["@opentelemetry/sdk-node"] = "^0.57.0";
84
+ packageJson.dependencies["winston"] = "^3.17.1";
85
+ }
86
+
87
+ await fs.writeFile(
88
+ path.join(outputDir, 'package.json'),
89
+ JSON.stringify(packageJson, null, 2)
90
+ );
91
+
92
+ // Create server.js
93
+ const serverCode = `import express from 'express';
94
+ import cors from 'cors';
95
+ import dotenv from 'dotenv';
96
+ import { router as bundleRouter } from './routes/bundles.js';
97
+ import { router as authRouter } from './routes/auth.js';
98
+ ${options.withMonitoring ? "import { initMonitoring } from './monitoring/index.js';" : ''}
99
+
100
+ dotenv.config();
101
+
102
+ const app = express();
103
+ const PORT = process.env.PORT || 3000;
104
+
105
+ ${options.withMonitoring ? 'initMonitoring(app);' : ''}
106
+
107
+ app.use(cors());
108
+ app.use(express.json());
109
+
110
+ // Routes
111
+ app.use('/api/bundles', bundleRouter);
112
+ app.use('/api/auth', authRouter);
113
+
114
+ // Health check
115
+ app.get('/health', (req, res) => {
116
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
117
+ });
118
+
119
+ app.listen(PORT, () => {
120
+ console.log(\`Server running on port \${PORT}\`);
121
+ });
122
+ `;
123
+
124
+ await fs.writeFile(path.join(outputDir, 'server.js'), serverCode);
125
+
126
+ // Create routes directory
127
+ await fs.mkdir(path.join(outputDir, 'routes'), { recursive: true });
128
+
129
+ // Create bundles route
130
+ const bundlesRoute = `import express from 'express';
131
+ import multer from 'multer';
132
+ import path from 'path';
133
+ import fs from 'fs/promises';
134
+ import crypto from 'crypto';
135
+
136
+ const router = express.Router();
137
+ const upload = multer({ dest: 'uploads/' });
138
+
139
+ // Get latest bundle
140
+ router.get('/latest', async (req, res) => {
141
+ try {
142
+ const { appId, version, channel = 'production' } = req.query;
143
+
144
+ // TODO: Implement bundle lookup logic
145
+ // This is a simplified example
146
+ const bundle = {
147
+ version: '1.0.1',
148
+ url: \`\${req.protocol}://\${req.get('host')}/bundles/latest.zip\`,
149
+ checksum: 'sha256:...',
150
+ signature: 'signature...',
151
+ metadata: {
152
+ releaseNotes: 'Bug fixes and improvements'
153
+ }
154
+ };
155
+
156
+ res.json(bundle);
157
+ } catch (error) {
158
+ res.status(500).json({ error: error.message });
159
+ }
160
+ });
161
+
162
+ // Upload new bundle
163
+ router.post('/upload', upload.single('bundle'), async (req, res) => {
164
+ try {
165
+ const { version, channel, metadata } = req.body;
166
+ const file = req.file;
167
+
168
+ if (!file) {
169
+ return res.status(400).json({ error: 'No bundle file provided' });
170
+ }
171
+
172
+ // Calculate checksum
173
+ const fileBuffer = await fs.readFile(file.path);
174
+ const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
175
+
176
+ // TODO: Store bundle metadata in database
177
+ // TODO: Move file to permanent storage
178
+
179
+ res.json({
180
+ success: true,
181
+ bundle: {
182
+ version,
183
+ channel,
184
+ checksum,
185
+ size: file.size
186
+ }
187
+ });
188
+ } catch (error) {
189
+ res.status(500).json({ error: error.message });
190
+ }
191
+ });
192
+
193
+ export { router };
194
+ `;
195
+
196
+ await fs.writeFile(path.join(outputDir, 'routes', 'bundles.js'), bundlesRoute);
197
+
198
+ // Create .env.example
199
+ const envExample = `# Server Configuration
200
+ PORT=3000
201
+
202
+ # Security
203
+ JWT_SECRET=your-secret-key-here
204
+ API_KEY=your-api-key-here
205
+
206
+ # Storage
207
+ STORAGE_PATH=./storage/bundles
208
+
209
+ # Database (optional)
210
+ # DATABASE_URL=postgresql://user:password@localhost:5432/native_update
211
+ `;
212
+
213
+ await fs.writeFile(path.join(outputDir, '.env.example'), envExample);
214
+
215
+ // Create README
216
+ const readme = `# Native Update Backend
217
+
218
+ Express.js backend for Native Update plugin.
219
+
220
+ ## Setup
221
+
222
+ 1. Copy \`.env.example\` to \`.env\` and configure
223
+ 2. Run \`npm install\`
224
+ 3. Run \`npm run dev\` for development
225
+
226
+ ## API Endpoints
227
+
228
+ - GET /api/bundles/latest - Get latest bundle for app
229
+ - POST /api/bundles/upload - Upload new bundle
230
+ - GET /health - Health check
231
+
232
+ ## Security
233
+
234
+ - Uses JWT for authentication
235
+ - Rate limiting enabled
236
+ - CORS configured
237
+
238
+ ${options.withMonitoring ? '## Monitoring\n\nOpenTelemetry monitoring is configured. Set up your preferred backend (Jaeger, Zipkin, etc.)' : ''}
239
+ `;
240
+
241
+ await fs.writeFile(path.join(outputDir, 'README.md'), readme);
242
+ }
243
+
244
+ async function createVercelBackend(outputDir, options) {
245
+ // Create api directory
246
+ const apiDir = path.join(outputDir, 'api');
247
+ await fs.mkdir(apiDir, { recursive: true });
248
+
249
+ // Create package.json
250
+ const packageJson = {
251
+ name: "native-update-vercel",
252
+ version: "1.0.0",
253
+ description: "Native Update Vercel backend",
254
+ type: "module",
255
+ scripts: {
256
+ dev: "vercel dev",
257
+ deploy: "vercel",
258
+ build: "echo 'No build step required'"
259
+ },
260
+ dependencies: {
261
+ "@vercel/node": "^3.0.0"
262
+ }
263
+ };
264
+
265
+ await fs.writeFile(
266
+ path.join(outputDir, 'package.json'),
267
+ JSON.stringify(packageJson, null, 2)
268
+ );
269
+
270
+ // Create bundles API endpoint
271
+ const bundlesApi = `export default async function handler(req, res) {
272
+ // Enable CORS
273
+ res.setHeader('Access-Control-Allow-Credentials', true);
274
+ res.setHeader('Access-Control-Allow-Origin', '*');
275
+ res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT');
276
+ res.setHeader(
277
+ 'Access-Control-Allow-Headers',
278
+ 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
279
+ );
280
+
281
+ if (req.method === 'OPTIONS') {
282
+ res.status(200).end();
283
+ return;
284
+ }
285
+
286
+ if (req.method === 'GET') {
287
+ // Get latest bundle
288
+ const { appId, version, channel = 'production' } = req.query;
289
+
290
+ // TODO: Implement bundle lookup from your storage solution
291
+ // This is a simplified example
292
+ const bundle = {
293
+ version: '1.0.1',
294
+ url: \`https://\${req.headers.host}/bundles/latest.zip\`,
295
+ checksum: 'sha256:...',
296
+ signature: 'signature...',
297
+ metadata: {
298
+ releaseNotes: 'Bug fixes and improvements'
299
+ }
300
+ };
301
+
302
+ return res.status(200).json(bundle);
303
+ }
304
+
305
+ if (req.method === 'POST') {
306
+ // Handle bundle upload
307
+ const { version, channel, metadata } = req.body;
308
+
309
+ // TODO: Handle file upload to storage (Vercel Blob, S3, etc.)
310
+ // TODO: Save metadata to database (Vercel KV, external DB, etc.)
311
+
312
+ return res.status(200).json({
313
+ success: true,
314
+ bundle: {
315
+ version,
316
+ channel,
317
+ message: 'Bundle uploaded successfully'
318
+ }
319
+ });
320
+ }
321
+
322
+ return res.status(405).json({ error: 'Method not allowed' });
323
+ }`;
324
+
325
+ await fs.writeFile(path.join(apiDir, 'bundles.js'), bundlesApi);
326
+
327
+ // Create health check endpoint
328
+ const healthApi = `export default function handler(req, res) {
329
+ res.status(200).json({
330
+ status: 'ok',
331
+ service: 'vercel-edge',
332
+ timestamp: new Date().toISOString()
333
+ });
334
+ }`;
335
+
336
+ await fs.writeFile(path.join(apiDir, 'health.js'), healthApi);
337
+
338
+ // Create vercel.json
339
+ const vercelConfig = {
340
+ functions: {
341
+ "api/*.js": {
342
+ maxDuration: 30
343
+ }
344
+ },
345
+ rewrites: [
346
+ {
347
+ source: "/api/bundles/latest",
348
+ destination: "/api/bundles"
349
+ }
350
+ ]
351
+ };
352
+
353
+ await fs.writeFile(
354
+ path.join(outputDir, 'vercel.json'),
355
+ JSON.stringify(vercelConfig, null, 2)
356
+ );
357
+
358
+ // Create .env.example
359
+ const envExample = `# Storage Configuration
360
+ STORAGE_URL=your-storage-url
361
+ STORAGE_KEY=your-storage-key
362
+
363
+ # Database Configuration (optional)
364
+ DATABASE_URL=your-database-url
365
+
366
+ # Security
367
+ API_KEY=your-api-key-here
368
+ `;
369
+
370
+ await fs.writeFile(path.join(outputDir, '.env.example'), envExample);
371
+
372
+ // Create README
373
+ const readme = `# Native Update Vercel Backend
374
+
375
+ Serverless backend for Native Update plugin using Vercel Edge Functions.
376
+
377
+ ## Setup
378
+
379
+ 1. Install Vercel CLI: \`npm i -g vercel\`
380
+ 2. Copy \`.env.example\` to \`.env.local\` and configure
381
+ 3. Run \`npm install\`
382
+ 4. Run \`vercel dev\` for local development
383
+
384
+ ## Deployment
385
+
386
+ \`\`\`bash
387
+ vercel
388
+ \`\`\`
389
+
390
+ ## API Endpoints
391
+
392
+ - GET /api/bundles - Get latest bundle
393
+ - POST /api/bundles - Upload new bundle
394
+ - GET /api/health - Health check
395
+
396
+ ## Storage Options
397
+
398
+ - Vercel Blob Storage
399
+ - AWS S3
400
+ - Cloudflare R2
401
+ - Any S3-compatible storage
402
+
403
+ ## Database Options
404
+
405
+ - Vercel KV
406
+ - Vercel Postgres
407
+ - PlanetScale
408
+ - Supabase
409
+
410
+ ${options.withMonitoring ? '## Monitoring\\n\\nUse Vercel Analytics and Logs for monitoring.' : ''}
411
+ `;
412
+
413
+ await fs.writeFile(path.join(outputDir, 'README.md'), readme);
414
+ }
415
+
416
+ async function createFirebaseBackend(outputDir, options) {
417
+ // Create functions directory
418
+ const functionsDir = path.join(outputDir, 'functions');
419
+ await fs.mkdir(functionsDir, { recursive: true });
420
+
421
+ // Create package.json for functions
422
+ const packageJson = {
423
+ name: "native-update-firebase-functions",
424
+ description: "Firebase Functions backend for Native Update",
425
+ type: "module",
426
+ engines: {
427
+ node: "22"
428
+ },
429
+ main: "index.js",
430
+ scripts: {
431
+ serve: "firebase emulators:start --only functions",
432
+ shell: "firebase functions:shell",
433
+ start: "npm run serve",
434
+ deploy: "firebase deploy --only functions",
435
+ logs: "firebase functions:log"
436
+ },
437
+ dependencies: {
438
+ "firebase-admin": "^12.0.0",
439
+ "firebase-functions": "^5.0.0",
440
+ cors: "^2.8.5",
441
+ "express": "^5.1.0"
442
+ }
443
+ };
444
+
445
+ await fs.writeFile(
446
+ path.join(functionsDir, 'package.json'),
447
+ JSON.stringify(packageJson, null, 2)
448
+ );
449
+
450
+ // Create index.js
451
+ const indexJs = `import { onRequest } from 'firebase-functions/v2/https';
452
+ import * as admin from 'firebase-admin';
453
+ import express from 'express';
454
+ import cors from 'cors';
455
+
456
+ admin.initializeApp();
457
+
458
+ const app = express();
459
+ app.use(cors({ origin: true }));
460
+ app.use(express.json());
461
+
462
+ // Get latest bundle
463
+ app.get('/api/bundles/latest', async (req, res) => {
464
+ try {
465
+ const { appId, version, channel = 'production' } = req.query;
466
+
467
+ // Query Firestore for latest bundle
468
+ const db = admin.firestore();
469
+ const snapshot = await db.collection('bundles')
470
+ .where('appId', '==', appId)
471
+ .where('channel', '==', channel)
472
+ .where('enabled', '==', true)
473
+ .orderBy('createdAt', 'desc')
474
+ .limit(1)
475
+ .get();
476
+
477
+ if (snapshot.empty) {
478
+ return res.status(404).json({ error: 'No bundle found' });
479
+ }
480
+
481
+ const bundle = snapshot.docs[0].data();
482
+ res.json(bundle);
483
+ } catch (error) {
484
+ res.status(500).json({ error: error.message });
485
+ }
486
+ });
487
+
488
+ // Upload bundle
489
+ app.post('/api/bundles/upload', async (req, res) => {
490
+ try {
491
+ const { version, channel, metadata } = req.body;
492
+
493
+ // TODO: Handle file upload to Cloud Storage
494
+ // TODO: Save metadata to Firestore
495
+
496
+ res.json({ success: true, version });
497
+ } catch (error) {
498
+ res.status(500).json({ error: error.message });
499
+ }
500
+ });
501
+
502
+ // Health check
503
+ app.get('/health', (req, res) => {
504
+ res.json({ status: 'ok', service: 'firebase-functions' });
505
+ });
506
+
507
+ export const api = onRequest(app);
508
+ `;
509
+
510
+ await fs.writeFile(path.join(functionsDir, 'index.js'), indexJs);
511
+
512
+ // Create firebase.json
513
+ const firebaseConfig = {
514
+ functions: {
515
+ source: "functions",
516
+ runtime: "nodejs22"
517
+ },
518
+ firestore: {
519
+ rules: "firestore.rules",
520
+ indexes: "firestore.indexes.json"
521
+ },
522
+ storage: {
523
+ rules: "storage.rules"
524
+ }
525
+ };
526
+
527
+ await fs.writeFile(
528
+ path.join(outputDir, 'firebase.json'),
529
+ JSON.stringify(firebaseConfig, null, 2)
530
+ );
531
+
532
+ // Create Firestore rules
533
+ const firestoreRules = `rules_version = '2';
534
+ service cloud.firestore {
535
+ match /databases/{database}/documents {
536
+ // Bundles are read-only for clients
537
+ match /bundles/{document=**} {
538
+ allow read: if true;
539
+ allow write: if false;
540
+ }
541
+
542
+ // Analytics can be written by clients
543
+ match /analytics/{document=**} {
544
+ allow create: if true;
545
+ allow read, update, delete: if false;
546
+ }
547
+ }
548
+ }`;
549
+
550
+ await fs.writeFile(path.join(outputDir, 'firestore.rules'), firestoreRules);
551
+
552
+ // Create README
553
+ const readme = `# Native Update Firebase Backend
554
+
555
+ Firebase Functions backend for Native Update plugin.
556
+
557
+ ## Setup
558
+
559
+ 1. Install Firebase CLI: \`npm install -g firebase-tools\`
560
+ 2. Login: \`firebase login\`
561
+ 3. Initialize: \`firebase init\`
562
+ 4. Select your project or create new
563
+ 5. Install dependencies: \`cd functions && npm install\`
564
+ 6. Start emulator: \`npm run serve\`
565
+
566
+ ## Deployment
567
+
568
+ \`\`\`bash
569
+ firebase deploy
570
+ \`\`\`
571
+
572
+ ## Endpoints
573
+
574
+ - GET /api/bundles/latest - Get latest bundle
575
+ - POST /api/bundles/upload - Upload new bundle
576
+ - GET /health - Health check
577
+
578
+ ${options.withMonitoring ? '## Monitoring\n\nUse Firebase Console for monitoring and analytics.' : ''}
579
+ `;
580
+
581
+ await fs.writeFile(path.join(outputDir, 'README.md'), readme);
582
+ }
@@ -0,0 +1,113 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import archiver from 'archiver';
6
+ import { createWriteStream } from 'fs';
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname, join } from 'path';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ export async function createBundle(webDir, options) {
14
+ console.log(chalk.blue('🔨 Creating update bundle...'));
15
+
16
+ try {
17
+ // Validate input directory
18
+ const stats = await fs.stat(webDir);
19
+ if (!stats.isDirectory()) {
20
+ throw new Error(`${webDir} is not a directory`);
21
+ }
22
+
23
+ // Check for index.html
24
+ const indexPath = path.join(webDir, 'index.html');
25
+ try {
26
+ await fs.access(indexPath);
27
+ } catch {
28
+ throw new Error(`No index.html found in ${webDir}. Is this a valid web build directory?`);
29
+ }
30
+
31
+ // Create output directory
32
+ const outputDir = path.resolve(options.output);
33
+ await fs.mkdir(outputDir, { recursive: true });
34
+
35
+ // Get version
36
+ let version = options.version;
37
+ if (!version) {
38
+ try {
39
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
40
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
41
+ version = packageJson.version;
42
+ } catch {
43
+ version = new Date().toISOString().split('T')[0].replace(/-/g, '.');
44
+ }
45
+ }
46
+
47
+ // Create bundle metadata
48
+ const metadata = {
49
+ version,
50
+ channel: options.channel,
51
+ created: new Date().toISOString(),
52
+ platform: 'web',
53
+ ...(options.metadata ? JSON.parse(options.metadata) : {})
54
+ };
55
+
56
+ const bundleId = `${version}-${Date.now()}`;
57
+ const bundleFileName = `bundle-${bundleId}.zip`;
58
+ const bundlePath = path.join(outputDir, bundleFileName);
59
+ const metadataPath = path.join(outputDir, `bundle-${bundleId}.json`);
60
+
61
+ console.log(chalk.gray(` Version: ${version}`));
62
+ console.log(chalk.gray(` Channel: ${options.channel}`));
63
+ console.log(chalk.gray(` Output: ${bundlePath}`));
64
+
65
+ // Create zip archive
66
+ const output = createWriteStream(bundlePath);
67
+ const archive = archiver('zip', {
68
+ zlib: { level: 9 }
69
+ });
70
+
71
+ const archivePromise = new Promise((resolve, reject) => {
72
+ output.on('close', resolve);
73
+ archive.on('error', reject);
74
+ });
75
+
76
+ archive.pipe(output);
77
+
78
+ // Add all files from web directory
79
+ archive.directory(webDir, false);
80
+
81
+ await archive.finalize();
82
+ await archivePromise;
83
+
84
+ // Calculate checksum
85
+ const fileBuffer = await fs.readFile(bundlePath);
86
+ const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
87
+
88
+ // Update metadata with file info
89
+ metadata.checksum = checksum;
90
+ metadata.size = fileBuffer.length;
91
+ metadata.filename = bundleFileName;
92
+
93
+ // Save metadata
94
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
95
+
96
+ console.log(chalk.green('✅ Bundle created successfully!'));
97
+ console.log('');
98
+ console.log(chalk.bold('Bundle Details:'));
99
+ console.log(chalk.gray(` File: ${bundlePath}`));
100
+ console.log(chalk.gray(` Size: ${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB`));
101
+ console.log(chalk.gray(` Checksum: ${checksum}`));
102
+ console.log(chalk.gray(` Metadata: ${metadataPath}`));
103
+ console.log('');
104
+ console.log(chalk.yellow('Next steps:'));
105
+ console.log(chalk.gray(' 1. Sign the bundle:'));
106
+ console.log(chalk.cyan(` npx native-update bundle sign ${bundlePath} --key ./keys/private.pem`));
107
+ console.log(chalk.gray(' 2. Upload to your update server'));
108
+
109
+ } catch (error) {
110
+ console.error(chalk.red('❌ Failed to create bundle:'), error.message);
111
+ process.exit(1);
112
+ }
113
+ }