@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 +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 +8 -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,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 (
|
|
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
|
};
|
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
|
|