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.
- package/Readme.md +35 -17
- package/android/src/main/AndroidManifest.xml +0 -16
- package/cli/cap-update.js +45 -0
- package/cli/commands/backend-create.js +582 -0
- package/cli/commands/bundle-create.js +113 -0
- package/cli/commands/bundle-sign.js +58 -0
- package/cli/commands/bundle-verify.js +55 -0
- package/cli/commands/init.js +146 -0
- package/cli/commands/keys-generate.js +92 -0
- package/cli/commands/monitor.js +68 -0
- package/cli/commands/server-start.js +96 -0
- package/cli/index.js +269 -0
- package/cli/package.json +12 -0
- package/docs/BUNDLE_SIGNING.md +16 -9
- package/docs/LIVE_UPDATES_GUIDE.md +1 -1
- package/docs/README.md +1 -0
- package/docs/cli-reference.md +321 -0
- package/docs/examples/android-manifest-example.xml +77 -0
- package/docs/getting-started/configuration.md +3 -2
- package/docs/getting-started/installation.md +101 -2
- package/docs/getting-started/quick-start.md +53 -1
- package/docs/guides/deployment-guide.md +9 -7
- package/docs/guides/key-management.md +284 -0
- package/docs/guides/migration-from-codepush.md +9 -5
- package/docs/guides/testing-guide.md +4 -4
- package/package.json +15 -2
|
@@ -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
|
+
}
|