@everystack/cli 0.2.23 → 0.2.25
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/aws.ts +2 -0
- package/src/cli/commands/update.ts +65 -0
- package/src/cli/config.ts +3 -0
- package/src/cli/discover.ts +13 -2
- package/src/cli/utils/walk.ts +4 -0
package/package.json
CHANGED
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
|
};
|
package/src/cli/discover.ts
CHANGED
|
@@ -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,24 @@ export async function discoverBuckets(
|
|
|
224
226
|
|
|
225
227
|
const updatesNeedle = `${prefix}updatesbucket-`.toLowerCase();
|
|
226
228
|
const clientNeedle = `${prefix}clientbundlesbucket-`.toLowerCase();
|
|
229
|
+
// Media bucket resource is named 'Media' (not 'MediaBucket'), so match both patterns
|
|
230
|
+
const mediaNeedle = `${prefix}media-`.toLowerCase();
|
|
231
|
+
const mediaBucketNeedle = `${prefix}mediabucket-`.toLowerCase();
|
|
227
232
|
|
|
228
233
|
let updatesBucket: string | undefined;
|
|
229
234
|
let clientBundlesBucket: string | undefined;
|
|
235
|
+
let mediaBucket: string | undefined;
|
|
230
236
|
|
|
231
237
|
for (const b of buckets) {
|
|
232
238
|
if (!b.Name) continue;
|
|
233
239
|
const lower = b.Name.toLowerCase();
|
|
234
240
|
if (lower.startsWith(updatesNeedle)) updatesBucket = b.Name;
|
|
235
241
|
if (lower.startsWith(clientNeedle)) clientBundlesBucket = b.Name;
|
|
236
|
-
if (
|
|
242
|
+
if (!mediaBucket && (lower.startsWith(mediaNeedle) || lower.startsWith(mediaBucketNeedle))) mediaBucket = b.Name;
|
|
243
|
+
if (updatesBucket && clientBundlesBucket && mediaBucket) break;
|
|
237
244
|
}
|
|
238
245
|
|
|
239
|
-
return { updatesBucket, clientBundlesBucket };
|
|
246
|
+
return { updatesBucket, clientBundlesBucket, mediaBucket };
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
// ---------------------------------------------------------------------------
|
|
@@ -295,6 +302,7 @@ export async function discoverConfig(
|
|
|
295
302
|
workerFunctionName: workerFn,
|
|
296
303
|
updatesBucket: bucketsResult.updatesBucket,
|
|
297
304
|
clientBundlesBucket: bucketsResult.clientBundlesBucket,
|
|
305
|
+
mediaBucket: bucketsResult.mediaBucket,
|
|
298
306
|
kvsArn: cfResult.kvsArn,
|
|
299
307
|
distributionId,
|
|
300
308
|
};
|
|
@@ -314,6 +322,7 @@ interface CachedOutputs {
|
|
|
314
322
|
workerFunctionName?: string;
|
|
315
323
|
updatesBucket?: string;
|
|
316
324
|
clientBundlesBucket?: string;
|
|
325
|
+
mediaBucket?: string;
|
|
317
326
|
kvsArn?: string;
|
|
318
327
|
distributionId?: string;
|
|
319
328
|
}
|
|
@@ -338,6 +347,7 @@ export async function getCachedConfig(stage: string): Promise<DiscoveredConfig |
|
|
|
338
347
|
workerFunctionName: cached.workerFunctionName,
|
|
339
348
|
updatesBucket: cached.updatesBucket,
|
|
340
349
|
clientBundlesBucket: cached.clientBundlesBucket,
|
|
350
|
+
mediaBucket: cached.mediaBucket,
|
|
341
351
|
kvsArn: cached.kvsArn,
|
|
342
352
|
distributionId: cached.distributionId,
|
|
343
353
|
};
|
|
@@ -355,6 +365,7 @@ export async function setCachedConfig(stage: string, config: DiscoveredConfig):
|
|
|
355
365
|
workerFunctionName: config.workerFunctionName,
|
|
356
366
|
updatesBucket: config.updatesBucket,
|
|
357
367
|
clientBundlesBucket: config.clientBundlesBucket,
|
|
368
|
+
mediaBucket: config.mediaBucket,
|
|
358
369
|
kvsArn: config.kvsArn,
|
|
359
370
|
distributionId: config.distributionId,
|
|
360
371
|
};
|
package/src/cli/utils/walk.ts
CHANGED
|
@@ -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
|
|