ai-localize-aws-cloudfront 2.0.3 → 2.0.5
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 +24 -4
- package/src/cdn-migrator.ts +0 -81
- package/src/cloudfront-invalidator.ts +0 -37
- package/src/index.ts +0 -3
- package/src/s3-uploader.ts +0 -110
- package/tsconfig.json +0 -6
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-localize-aws-cloudfront",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "S3 upload, CloudFront invalidation
|
|
3
|
+
"version": "2.0.5",
|
|
4
|
+
"description": "S3 upload, CloudFront invalidation and CDN URL migration for ai-localize-core",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"CHANGELOG.md"
|
|
12
|
+
],
|
|
8
13
|
"exports": {
|
|
9
14
|
".": {
|
|
10
15
|
"types": "./dist/index.d.ts",
|
|
@@ -12,13 +17,28 @@
|
|
|
12
17
|
"require": "./dist/index.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"i18n",
|
|
22
|
+
"localization",
|
|
23
|
+
"l10n",
|
|
24
|
+
"internationalization",
|
|
25
|
+
"ai-localize",
|
|
26
|
+
"aws",
|
|
27
|
+
"s3",
|
|
28
|
+
"cloudfront",
|
|
29
|
+
"cdn",
|
|
30
|
+
"migration"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
15
35
|
"dependencies": {
|
|
16
36
|
"@aws-sdk/client-s3": "^3.511.0",
|
|
17
37
|
"@aws-sdk/client-cloudfront": "^3.511.0",
|
|
18
38
|
"@aws-sdk/lib-storage": "^3.511.0",
|
|
19
39
|
"mime-types": "^2.1.35",
|
|
20
|
-
"ai-localize-shared": "2.0.
|
|
21
|
-
"ai-localize-codemods": "2.0.
|
|
40
|
+
"ai-localize-shared": "2.0.5",
|
|
41
|
+
"ai-localize-codemods": "2.0.5"
|
|
22
42
|
},
|
|
23
43
|
"devDependencies": {
|
|
24
44
|
"@types/mime-types": "^2.1.4",
|
package/src/cdn-migrator.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import * as path from "path";
|
|
2
|
-
import type { AwsConfig, CloudFrontAsset, LegacyCdnUrl } from "ai-localize-shared";
|
|
3
|
-
import { S3Uploader } from "./s3-uploader.js";
|
|
4
|
-
import { CloudFrontInvalidator } from "./cloudfront-invalidator.js";
|
|
5
|
-
|
|
6
|
-
export interface MigrationOptions {
|
|
7
|
-
sourceDir: string;
|
|
8
|
-
assetsDir: string;
|
|
9
|
-
legacyCdnUrls: LegacyCdnUrl[];
|
|
10
|
-
dryRun?: boolean;
|
|
11
|
-
invalidateCache?: boolean;
|
|
12
|
-
onProgress?: (step: string) => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface MigrationResult {
|
|
16
|
-
uploadedAssets: CloudFrontAsset[];
|
|
17
|
-
replacedUrls: number;
|
|
18
|
-
invalidationId?: string;
|
|
19
|
-
duration: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Orchestrates the full CDN migration:
|
|
24
|
-
* 1. Upload assets to S3
|
|
25
|
-
* 2. Replace legacy CDN URLs in source files
|
|
26
|
-
* 3. Invalidate CloudFront cache
|
|
27
|
-
*/
|
|
28
|
-
export class CdnMigrator {
|
|
29
|
-
private config: AwsConfig;
|
|
30
|
-
|
|
31
|
-
constructor(config: AwsConfig) {
|
|
32
|
-
this.config = config;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async migrate(options: MigrationOptions): Promise<MigrationResult> {
|
|
36
|
-
const startTime = Date.now();
|
|
37
|
-
const { dryRun = false, onProgress } = options;
|
|
38
|
-
|
|
39
|
-
onProgress?.("Uploading assets to S3...");
|
|
40
|
-
let uploadedAssets: CloudFrontAsset[] = [];
|
|
41
|
-
|
|
42
|
-
if (!dryRun) {
|
|
43
|
-
const uploader = new S3Uploader(this.config);
|
|
44
|
-
const uploadResult = await uploader.uploadDirectory({
|
|
45
|
-
assetsDir: options.assetsDir,
|
|
46
|
-
onProgress: (asset, done, total) => onProgress?.(`Uploading ${done}/${total}: ${path.basename(asset)}`),
|
|
47
|
-
});
|
|
48
|
-
uploadedAssets = uploadResult.uploaded;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
onProgress?.("Replacing legacy CDN URLs...");
|
|
52
|
-
let replacedUrls = 0;
|
|
53
|
-
|
|
54
|
-
if (!dryRun && uploadedAssets.length > 0) {
|
|
55
|
-
const { batchReplaceCdnUrls } = await import("ai-localize-codemods");
|
|
56
|
-
const fileUrlMap = new Map<string, LegacyCdnUrl[]>();
|
|
57
|
-
for (const url of options.legacyCdnUrls) {
|
|
58
|
-
const existing = fileUrlMap.get(url.filePath) || [];
|
|
59
|
-
existing.push(url);
|
|
60
|
-
fileUrlMap.set(url.filePath, existing);
|
|
61
|
-
}
|
|
62
|
-
const results = await batchReplaceCdnUrls(fileUrlMap, uploadedAssets, dryRun);
|
|
63
|
-
replacedUrls = results.reduce((sum, r) => sum + r.replacedCount, 0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let invalidationId: string | undefined;
|
|
67
|
-
if (!dryRun && options.invalidateCache && uploadedAssets.length > 0) {
|
|
68
|
-
onProgress?.("Invalidating CloudFront cache...");
|
|
69
|
-
const invalidator = new CloudFrontInvalidator(this.config);
|
|
70
|
-
const inv = await invalidator.invalidate(["/*"]);
|
|
71
|
-
invalidationId = inv.invalidationId;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
uploadedAssets,
|
|
76
|
-
replacedUrls,
|
|
77
|
-
invalidationId,
|
|
78
|
-
duration: Date.now() - startTime,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { CloudFrontClient, CreateInvalidationCommand } from "@aws-sdk/client-cloudfront";
|
|
2
|
-
import type { AwsConfig } from "ai-localize-shared";
|
|
3
|
-
|
|
4
|
-
export interface InvalidationResult {
|
|
5
|
-
invalidationId: string;
|
|
6
|
-
paths: string[];
|
|
7
|
-
status: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class CloudFrontInvalidator {
|
|
11
|
-
private client: CloudFrontClient;
|
|
12
|
-
private distributionId: string;
|
|
13
|
-
|
|
14
|
-
constructor(config: AwsConfig) {
|
|
15
|
-
this.distributionId = config.distributionId;
|
|
16
|
-
this.client = new CloudFrontClient({ region: config.region || "us-east-1" });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async invalidate(paths: string[] = ["/*"]): Promise<InvalidationResult> {
|
|
20
|
-
const callerReference = `ai-localize-${Date.now()}`;
|
|
21
|
-
const response = await this.client.send(
|
|
22
|
-
new CreateInvalidationCommand({
|
|
23
|
-
DistributionId: this.distributionId,
|
|
24
|
-
InvalidationBatch: {
|
|
25
|
-
CallerReference: callerReference,
|
|
26
|
-
Paths: { Quantity: paths.length, Items: paths },
|
|
27
|
-
},
|
|
28
|
-
})
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
invalidationId: response.Invalidation?.Id || "",
|
|
33
|
-
paths,
|
|
34
|
-
status: response.Invalidation?.Status || "InProgress",
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/index.ts
DELETED
package/src/s3-uploader.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as crypto from "crypto";
|
|
4
|
-
import { S3Client, PutObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3";
|
|
5
|
-
import * as mime from "mime-types";
|
|
6
|
-
|
|
7
|
-
import type { AwsConfig, CloudFrontAsset } from "ai-localize-shared";
|
|
8
|
-
import { collectFiles, ASSET_EXTENSIONS } from "ai-localize-shared";
|
|
9
|
-
|
|
10
|
-
export interface UploadOptions {
|
|
11
|
-
assetsDir: string;
|
|
12
|
-
force?: boolean;
|
|
13
|
-
onProgress?: (asset: string, done: number, total: number) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface UploadResult {
|
|
17
|
-
uploaded: CloudFrontAsset[];
|
|
18
|
-
skipped: string[];
|
|
19
|
-
failed: string[];
|
|
20
|
-
duration: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Uploads static assets to S3 with content-hash in filename for cache-busting.
|
|
25
|
-
*/
|
|
26
|
-
export class S3Uploader {
|
|
27
|
-
private client: S3Client;
|
|
28
|
-
private config: AwsConfig;
|
|
29
|
-
|
|
30
|
-
constructor(config: AwsConfig) {
|
|
31
|
-
this.config = config;
|
|
32
|
-
this.client = new S3Client({
|
|
33
|
-
region: config.region || "us-east-1",
|
|
34
|
-
...(config.profile ? { credentials: { accessKeyId: "", secretAccessKey: "" } } : {}),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async uploadDirectory(options: UploadOptions): Promise<UploadResult> {
|
|
39
|
-
const startTime = Date.now();
|
|
40
|
-
const assetFiles = collectFiles(options.assetsDir, ASSET_EXTENSIONS, ["node_modules"]);
|
|
41
|
-
const uploaded: CloudFrontAsset[] = [];
|
|
42
|
-
const skipped: string[] = [];
|
|
43
|
-
const failed: string[] = [];
|
|
44
|
-
let done = 0;
|
|
45
|
-
|
|
46
|
-
for (const filePath of assetFiles) {
|
|
47
|
-
try {
|
|
48
|
-
const asset = await this.uploadFile(filePath, options.assetsDir, options.force);
|
|
49
|
-
if (asset) {
|
|
50
|
-
uploaded.push(asset);
|
|
51
|
-
} else {
|
|
52
|
-
skipped.push(filePath);
|
|
53
|
-
}
|
|
54
|
-
} catch (err) {
|
|
55
|
-
failed.push(filePath);
|
|
56
|
-
console.error(`Failed to upload ${filePath}:`, err);
|
|
57
|
-
}
|
|
58
|
-
done++;
|
|
59
|
-
options.onProgress?.(filePath, done, assetFiles.length);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { uploaded, skipped, failed, duration: Date.now() - startTime };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async uploadFile(filePath: string, baseDir: string, force = false): Promise<CloudFrontAsset | null> {
|
|
66
|
-
const content = fs.readFileSync(filePath);
|
|
67
|
-
const hash = crypto.createHash("md5").update(content).digest("hex").slice(0, 8);
|
|
68
|
-
const ext = path.extname(filePath);
|
|
69
|
-
const basename = path.basename(filePath, ext);
|
|
70
|
-
const hashedName = `${basename}.${hash}${ext}`;
|
|
71
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
72
|
-
const s3Key = path.join(this.config.assetsPrefix || "assets", path.dirname(relativePath), hashedName).replace(/\\/g, "/");
|
|
73
|
-
|
|
74
|
-
// Check if already uploaded
|
|
75
|
-
if (!force) {
|
|
76
|
-
const exists = await this.objectExists(s3Key);
|
|
77
|
-
if (exists) return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const contentType = mime.lookup(filePath) || "application/octet-stream";
|
|
81
|
-
await this.client.send(
|
|
82
|
-
new PutObjectCommand({
|
|
83
|
-
Bucket: this.config.bucket,
|
|
84
|
-
Key: s3Key,
|
|
85
|
-
Body: content,
|
|
86
|
-
ContentType: contentType,
|
|
87
|
-
CacheControl: "public, max-age=31536000, immutable",
|
|
88
|
-
})
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const cdnBase = this.config.cdnBaseUrl || `https://${this.config.distributionId}.cloudfront.net`;
|
|
92
|
-
return {
|
|
93
|
-
localPath: filePath,
|
|
94
|
-
s3Key,
|
|
95
|
-
hash,
|
|
96
|
-
cloudfrontUrl: `${cdnBase}/${s3Key}`,
|
|
97
|
-
contentType,
|
|
98
|
-
size: content.length,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private async objectExists(key: string): Promise<boolean> {
|
|
103
|
-
try {
|
|
104
|
-
await this.client.send(new HeadObjectCommand({ Bucket: this.config.bucket, Key: key }));
|
|
105
|
-
return true;
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|