@everystack/cli 0.2.23 → 0.2.24

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "publishConfig": {
package/src/cli/aws.ts CHANGED
@@ -31,6 +31,7 @@ export async function uploadToS3(
31
31
  key: string,
32
32
  body: Buffer | Uint8Array,
33
33
  contentType: string,
34
+ cacheControl?: string,
34
35
  ): Promise<void> {
35
36
  const client = await getS3(region);
36
37
  const { PutObjectCommand } = await import('@aws-sdk/client-s3');
@@ -39,6 +40,7 @@ export async function uploadToS3(
39
40
  Key: key,
40
41
  Body: body,
41
42
  ContentType: contentType,
43
+ ...(cacheControl && { CacheControl: cacheControl }),
42
44
  }));
43
45
  }
44
46
 
@@ -410,6 +410,71 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
410
410
  }
411
411
  }
412
412
 
413
+ // 3.5. Sync media assets to the media bucket (email logos, OG images, etc.)
414
+ const assetDirs: Array<{ from: string; to: string; include?: string }> | undefined =
415
+ appConfig.extra?.everystack?.assets;
416
+ if (assetDirs?.length) {
417
+ if (!config.mediaBucket) {
418
+ warn('everystack.assets configured but no mediaBucket found.');
419
+ info('Add `mediaBucket: media.name` to the return block in sst.config.ts and redeploy.');
420
+ } else {
421
+ step('Syncing media assets...');
422
+ // @ts-ignore — minimatch lacks type declarations in this project
423
+ const minimatchMod = await import('minimatch');
424
+ const minimatch: (p: string, pattern: string, opts?: { matchBase?: boolean }) => boolean =
425
+ minimatchMod.minimatch || minimatchMod.default || minimatchMod;
426
+ const MEDIA_CONCURRENCY = 10;
427
+ let mediaUploaded = 0;
428
+ let mediaSkipped = 0;
429
+
430
+ for (const { from: fromDir, to: toPrefix, include } of assetDirs) {
431
+ const absFrom = path.resolve(fromDir);
432
+ let files: Awaited<ReturnType<typeof walkDirectory>>;
433
+ try {
434
+ files = await walkDirectory(absFrom);
435
+ } catch {
436
+ warn(`Media asset directory not found: ${fromDir}`);
437
+ continue;
438
+ }
439
+
440
+ // Filter by include glob when specified
441
+ if (include) {
442
+ files = files.filter(f => minimatch(f.relativePath, include, { matchBase: true }));
443
+ }
444
+
445
+ for (let i = 0; i < files.length; i += MEDIA_CONCURRENCY) {
446
+ const batch = files.slice(i, i + MEDIA_CONCURRENCY);
447
+ await Promise.all(batch.map(async (file) => {
448
+ const s3Key = `${toPrefix}${file.relativePath}`;
449
+ const data = Buffer.from(file.data, 'base64');
450
+
451
+ // Compare MD5 against S3 ETag for diffing
452
+ const md5 = `"${crypto.createHash('md5').update(data).digest('hex')}"`;
453
+ const existing = await headS3(config.region, config.mediaBucket!, s3Key);
454
+ if (existing?.etag === md5) {
455
+ mediaSkipped++;
456
+ return;
457
+ }
458
+
459
+ await uploadToS3(
460
+ config.region,
461
+ config.mediaBucket!,
462
+ s3Key,
463
+ data,
464
+ file.contentType,
465
+ 'public, max-age=31536000, immutable',
466
+ );
467
+ mediaUploaded++;
468
+ }));
469
+ }
470
+ }
471
+
472
+ if (mediaUploaded > 0 || mediaSkipped > 0) {
473
+ success(`Media: ${mediaUploaded} new, ${mediaSkipped} unchanged (skipped)`);
474
+ }
475
+ }
476
+ }
477
+
413
478
  // 4. Write release manifests directly to S3 (source of truth for SSR).
414
479
  step('Writing release manifests...');
415
480
  const updateId = crypto.createHash('sha256')
package/src/cli/config.ts CHANGED
@@ -19,6 +19,7 @@ export interface CliConfig {
19
19
  workerFunctionName?: string;
20
20
  updatesBucket: string;
21
21
  clientBundlesBucket: string;
22
+ mediaBucket?: string;
22
23
  kvsArn?: string;
23
24
  distributionId?: string;
24
25
  }
@@ -32,6 +33,7 @@ interface SstOutputs {
32
33
  workerFunctionName?: string;
33
34
  updatesBucket?: string;
34
35
  clientBundlesBucket?: string;
36
+ mediaBucket?: string;
35
37
  kvsArn?: string;
36
38
  distributionId?: string;
37
39
  }
@@ -101,6 +103,7 @@ export async function resolveConfig(stage?: string): Promise<CliConfig> {
101
103
  workerFunctionName: outputs.workerFunctionName,
102
104
  updatesBucket: outputs.updatesBucket,
103
105
  clientBundlesBucket: outputs.clientBundlesBucket,
106
+ mediaBucket: outputs.mediaBucket,
104
107
  kvsArn: outputs.kvsArn,
105
108
  distributionId: outputs.distributionId,
106
109
  };
@@ -23,6 +23,7 @@ interface DiscoveredConfig {
23
23
  workerFunctionName?: string;
24
24
  updatesBucket: string;
25
25
  clientBundlesBucket: string;
26
+ mediaBucket?: string;
26
27
  kvsArn?: string;
27
28
  distributionId?: string;
28
29
  }
@@ -207,6 +208,7 @@ export async function discoverLambda(
207
208
  interface BucketsResult {
208
209
  updatesBucket?: string;
209
210
  clientBundlesBucket?: string;
211
+ mediaBucket?: string;
210
212
  }
211
213
 
212
214
  /**
@@ -224,19 +226,22 @@ export async function discoverBuckets(
224
226
 
225
227
  const updatesNeedle = `${prefix}updatesbucket-`.toLowerCase();
226
228
  const clientNeedle = `${prefix}clientbundlesbucket-`.toLowerCase();
229
+ const mediaNeedle = `${prefix}mediabucket-`.toLowerCase();
227
230
 
228
231
  let updatesBucket: string | undefined;
229
232
  let clientBundlesBucket: string | undefined;
233
+ let mediaBucket: string | undefined;
230
234
 
231
235
  for (const b of buckets) {
232
236
  if (!b.Name) continue;
233
237
  const lower = b.Name.toLowerCase();
234
238
  if (lower.startsWith(updatesNeedle)) updatesBucket = b.Name;
235
239
  if (lower.startsWith(clientNeedle)) clientBundlesBucket = b.Name;
236
- if (updatesBucket && clientBundlesBucket) break;
240
+ if (lower.startsWith(mediaNeedle)) mediaBucket = b.Name;
241
+ if (updatesBucket && clientBundlesBucket && mediaBucket) break;
237
242
  }
238
243
 
239
- return { updatesBucket, clientBundlesBucket };
244
+ return { updatesBucket, clientBundlesBucket, mediaBucket };
240
245
  }
241
246
 
242
247
  // ---------------------------------------------------------------------------
@@ -295,6 +300,7 @@ export async function discoverConfig(
295
300
  workerFunctionName: workerFn,
296
301
  updatesBucket: bucketsResult.updatesBucket,
297
302
  clientBundlesBucket: bucketsResult.clientBundlesBucket,
303
+ mediaBucket: bucketsResult.mediaBucket,
298
304
  kvsArn: cfResult.kvsArn,
299
305
  distributionId,
300
306
  };
@@ -20,10 +20,14 @@ const MIME_MAP: Record<string, string> = {
20
20
  '.gif': 'image/gif',
21
21
  '.svg': 'image/svg+xml',
22
22
  '.webp': 'image/webp',
23
+ '.avif': 'image/avif',
24
+ '.ico': 'image/x-icon',
23
25
  '.ttf': 'font/ttf',
24
26
  '.otf': 'font/otf',
25
27
  '.woff': 'font/woff',
26
28
  '.woff2': 'font/woff2',
29
+ '.mp4': 'video/mp4',
30
+ '.pdf': 'application/pdf',
27
31
  '.map': 'application/json',
28
32
  };
29
33