@projectdochelp/s3te 1.0.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/LICENSE +21 -0
- package/README.md +442 -0
- package/bin/s3te.mjs +2 -0
- package/package.json +66 -0
- package/packages/aws-adapter/src/aws-cli.mjs +102 -0
- package/packages/aws-adapter/src/deploy.mjs +433 -0
- package/packages/aws-adapter/src/features.mjs +16 -0
- package/packages/aws-adapter/src/index.mjs +7 -0
- package/packages/aws-adapter/src/manifest.mjs +88 -0
- package/packages/aws-adapter/src/package.mjs +323 -0
- package/packages/aws-adapter/src/runtime/common.mjs +917 -0
- package/packages/aws-adapter/src/runtime/content-mirror.mjs +301 -0
- package/packages/aws-adapter/src/runtime/invalidation-executor.mjs +61 -0
- package/packages/aws-adapter/src/runtime/invalidation-scheduler.mjs +59 -0
- package/packages/aws-adapter/src/runtime/render-worker.mjs +83 -0
- package/packages/aws-adapter/src/runtime/source-dispatcher.mjs +106 -0
- package/packages/aws-adapter/src/template.mjs +578 -0
- package/packages/aws-adapter/src/zip.mjs +111 -0
- package/packages/cli/bin/s3te.mjs +383 -0
- package/packages/cli/src/fs-adapters.mjs +221 -0
- package/packages/cli/src/project.mjs +535 -0
- package/packages/core/src/config.mjs +464 -0
- package/packages/core/src/content-query.mjs +176 -0
- package/packages/core/src/errors.mjs +14 -0
- package/packages/core/src/index.mjs +24 -0
- package/packages/core/src/mime.mjs +29 -0
- package/packages/core/src/minify.mjs +82 -0
- package/packages/core/src/render.mjs +537 -0
- package/packages/testkit/src/index.mjs +136 -0
- package/src/index.mjs +3 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
CopyObjectCommand,
|
|
4
|
+
DeleteObjectCommand,
|
|
5
|
+
GetObjectCommand,
|
|
6
|
+
HeadObjectCommand,
|
|
7
|
+
ListObjectsV2Command,
|
|
8
|
+
PutObjectCommand,
|
|
9
|
+
S3Client
|
|
10
|
+
} from "@aws-sdk/client-s3";
|
|
11
|
+
import { CloudFrontClient, CreateInvalidationCommand } from "@aws-sdk/client-cloudfront";
|
|
12
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
13
|
+
import { InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda";
|
|
14
|
+
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
|
|
15
|
+
import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
|
|
16
|
+
import {
|
|
17
|
+
BatchWriteCommand,
|
|
18
|
+
DeleteCommand,
|
|
19
|
+
DynamoDBDocumentClient,
|
|
20
|
+
PutCommand,
|
|
21
|
+
QueryCommand,
|
|
22
|
+
ScanCommand
|
|
23
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
24
|
+
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
applyContentQuery,
|
|
28
|
+
createManualRenderTargets,
|
|
29
|
+
getContentTypeForPath,
|
|
30
|
+
isRenderableKey,
|
|
31
|
+
renderSourceTemplate
|
|
32
|
+
} from "../../../core/src/index.mjs";
|
|
33
|
+
|
|
34
|
+
function normalizeKey(value) {
|
|
35
|
+
return String(value).replace(/\\/g, "/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildSourceId(environment, variant, language, outputKey) {
|
|
39
|
+
return `${environment}#${variant}#${language}#${outputKey}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function chunk(items, size) {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
for (let index = 0; index < items.length; index += size) {
|
|
45
|
+
chunks.push(items.slice(index, index + size));
|
|
46
|
+
}
|
|
47
|
+
return chunks;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeLocale(value) {
|
|
51
|
+
return String(value).trim().toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildLocaleCandidates(language, languageLocaleMap) {
|
|
55
|
+
const requested = String(language ?? "").trim();
|
|
56
|
+
const configured = String(languageLocaleMap?.[requested] ?? requested).trim();
|
|
57
|
+
return [...new Set([configured, requested].filter(Boolean).map(normalizeLocale))];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function localeMatchScore(itemLocale, language, languageLocaleMap) {
|
|
61
|
+
if (!itemLocale) {
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const normalizedItemLocale = normalizeLocale(itemLocale);
|
|
66
|
+
const candidates = buildLocaleCandidates(language, languageLocaleMap);
|
|
67
|
+
if (candidates.includes(normalizedItemLocale)) {
|
|
68
|
+
return 3;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (candidates.some((candidate) => candidate.length >= 2 && normalizedItemLocale.startsWith(`${candidate}-`))) {
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matchesRequestedLocale(item, language, languageLocaleMap) {
|
|
79
|
+
return localeMatchScore(item?.locale, language, languageLocaleMap) > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function filterItemsByRequestedLocale(items, language, languageLocaleMap) {
|
|
83
|
+
const grouped = new Map();
|
|
84
|
+
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
const groupKey = item.contentId ?? item.id ?? JSON.stringify(item);
|
|
87
|
+
if (!grouped.has(groupKey)) {
|
|
88
|
+
grouped.set(groupKey, []);
|
|
89
|
+
}
|
|
90
|
+
grouped.get(groupKey).push(item);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [...grouped.values()].flatMap((groupItems) => {
|
|
94
|
+
const scored = groupItems
|
|
95
|
+
.map((item) => ({
|
|
96
|
+
item,
|
|
97
|
+
score: localeMatchScore(item.locale, language, languageLocaleMap)
|
|
98
|
+
}))
|
|
99
|
+
.filter((entry) => entry.score > 0);
|
|
100
|
+
if (scored.length === 0) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const bestScore = Math.max(...scored.map((entry) => entry.score));
|
|
105
|
+
return scored.filter((entry) => entry.score === bestScore).map((entry) => entry.item);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function wrapCommandClient(client, mapping) {
|
|
110
|
+
return Object.fromEntries(Object.entries(mapping).map(([name, CommandType]) => [
|
|
111
|
+
name,
|
|
112
|
+
(input) => ({
|
|
113
|
+
promise: () => client.send(new CommandType(input))
|
|
114
|
+
})
|
|
115
|
+
]));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createAwsClients(region = process.env.AWS_REGION) {
|
|
119
|
+
const clientConfig = region ? { region } : {};
|
|
120
|
+
const dynamoBaseClient = new DynamoDBClient(clientConfig);
|
|
121
|
+
const dynamoDocumentClient = DynamoDBDocumentClient.from(dynamoBaseClient);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
AWS: {
|
|
125
|
+
DynamoDB: {
|
|
126
|
+
Converter: {
|
|
127
|
+
unmarshall
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
s3: wrapCommandClient(new S3Client(clientConfig), {
|
|
132
|
+
getObject: GetObjectCommand,
|
|
133
|
+
headObject: HeadObjectCommand,
|
|
134
|
+
listObjectsV2: ListObjectsV2Command,
|
|
135
|
+
putObject: PutObjectCommand,
|
|
136
|
+
copyObject: CopyObjectCommand,
|
|
137
|
+
deleteObject: DeleteObjectCommand
|
|
138
|
+
}),
|
|
139
|
+
ssm: wrapCommandClient(new SSMClient(clientConfig), {
|
|
140
|
+
getParameter: GetParameterCommand
|
|
141
|
+
}),
|
|
142
|
+
lambda: wrapCommandClient(new LambdaClient(clientConfig), {
|
|
143
|
+
invoke: InvokeCommand
|
|
144
|
+
}),
|
|
145
|
+
dynamo: wrapCommandClient(dynamoDocumentClient, {
|
|
146
|
+
query: QueryCommand,
|
|
147
|
+
scan: ScanCommand,
|
|
148
|
+
batchWrite: BatchWriteCommand,
|
|
149
|
+
put: PutCommand,
|
|
150
|
+
delete: DeleteCommand
|
|
151
|
+
}),
|
|
152
|
+
stepFunctions: wrapCommandClient(new SFNClient(clientConfig), {
|
|
153
|
+
startExecution: StartExecutionCommand
|
|
154
|
+
}),
|
|
155
|
+
cloudFront: wrapCommandClient(new CloudFrontClient(clientConfig), {
|
|
156
|
+
createInvalidation: CreateInvalidationCommand
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function loadEnvironmentManifest(ssm, parameterName, environmentName) {
|
|
162
|
+
const response = await ssm.getParameter({ Name: parameterName }).promise();
|
|
163
|
+
const manifest = JSON.parse(response.Parameter.Value);
|
|
164
|
+
const environment = manifest.environments?.[environmentName];
|
|
165
|
+
if (!environment) {
|
|
166
|
+
throw new Error(`Runtime manifest does not contain environment ${environmentName}.`);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
manifest,
|
|
170
|
+
environment
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function decodeS3Key(value) {
|
|
175
|
+
return decodeURIComponent(String(value).replace(/\+/g, " "));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function bodyToUtf8(body) {
|
|
179
|
+
if (typeof body?.transformToString === "function") {
|
|
180
|
+
return body.transformToString("utf8");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (Buffer.isBuffer(body)) {
|
|
184
|
+
return body.toString("utf8");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (body instanceof Uint8Array) {
|
|
188
|
+
return Buffer.from(body).toString("utf8");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return String(body ?? "");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function buildCoreConfigFromEnvironment(manifest, environmentName) {
|
|
195
|
+
const environment = manifest.environments[environmentName];
|
|
196
|
+
return {
|
|
197
|
+
project: {
|
|
198
|
+
name: manifest.project.name,
|
|
199
|
+
displayName: manifest.project.displayName
|
|
200
|
+
},
|
|
201
|
+
environments: {
|
|
202
|
+
[environmentName]: {
|
|
203
|
+
name: environment.name,
|
|
204
|
+
awsRegion: environment.awsRegion,
|
|
205
|
+
stackPrefix: environment.stackPrefix,
|
|
206
|
+
certificateArn: "",
|
|
207
|
+
route53HostedZoneId: undefined
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
rendering: {
|
|
211
|
+
...environment.rendering
|
|
212
|
+
},
|
|
213
|
+
variants: Object.fromEntries(Object.entries(environment.variants).map(([variantName, variantConfig]) => [
|
|
214
|
+
variantName,
|
|
215
|
+
{
|
|
216
|
+
name: variantName,
|
|
217
|
+
sourceDir: variantConfig.sourceDir,
|
|
218
|
+
partDir: variantConfig.partDir,
|
|
219
|
+
defaultLanguage: variantConfig.defaultLanguage,
|
|
220
|
+
routing: { ...variantConfig.routing },
|
|
221
|
+
languages: Object.fromEntries(Object.entries(variantConfig.languages).map(([languageCode, languageConfig]) => [
|
|
222
|
+
languageCode,
|
|
223
|
+
{
|
|
224
|
+
code: languageCode,
|
|
225
|
+
baseUrl: languageConfig.baseUrl,
|
|
226
|
+
targetBucket: languageConfig.targetBucket,
|
|
227
|
+
cloudFrontAliases: [...languageConfig.cloudFrontAliases],
|
|
228
|
+
webinyLocale: languageConfig.webinyLocale ?? languageCode
|
|
229
|
+
}
|
|
230
|
+
]))
|
|
231
|
+
}
|
|
232
|
+
])),
|
|
233
|
+
aws: {
|
|
234
|
+
codeBuckets: Object.fromEntries(Object.entries(environment.variants).map(([variantName, variantConfig]) => [
|
|
235
|
+
variantName,
|
|
236
|
+
variantConfig.codeBucket
|
|
237
|
+
])),
|
|
238
|
+
dependencyStore: {
|
|
239
|
+
tableName: environment.tables.dependency
|
|
240
|
+
},
|
|
241
|
+
contentStore: {
|
|
242
|
+
tableName: environment.tables.content,
|
|
243
|
+
contentIdIndexName: environment.tables.contentIdIndexName ?? "contentid"
|
|
244
|
+
},
|
|
245
|
+
invalidationStore: {
|
|
246
|
+
tableName: environment.tables.invalidation,
|
|
247
|
+
debounceSeconds: 60
|
|
248
|
+
},
|
|
249
|
+
lambda: {
|
|
250
|
+
runtime: "nodejs22.x",
|
|
251
|
+
architecture: "arm64"
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
integrations: {
|
|
255
|
+
webiny: {
|
|
256
|
+
enabled: environment.integrations.webiny.enabled,
|
|
257
|
+
sourceTableName: environment.integrations.webiny.sourceTableName,
|
|
258
|
+
mirrorTableName: environment.integrations.webiny.mirrorTableName,
|
|
259
|
+
relevantModels: [...environment.integrations.webiny.relevantModels],
|
|
260
|
+
tenant: environment.integrations.webiny.tenant
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export class S3TemplateRepository {
|
|
267
|
+
constructor({ s3, environmentManifest, activeVariantName }) {
|
|
268
|
+
this.s3 = s3;
|
|
269
|
+
this.environmentManifest = environmentManifest;
|
|
270
|
+
this.activeVariantName = activeVariantName;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
resolveLogicalKey(key) {
|
|
274
|
+
const normalized = normalizeKey(key);
|
|
275
|
+
const activeVariant = this.environmentManifest.variants[this.activeVariantName];
|
|
276
|
+
|
|
277
|
+
if (normalized.startsWith(`${this.activeVariantName}/`)) {
|
|
278
|
+
return {
|
|
279
|
+
bucket: activeVariant.codeBucket,
|
|
280
|
+
objectKey: normalized
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (normalized.startsWith(`${activeVariant.partDir}/`)) {
|
|
285
|
+
return {
|
|
286
|
+
bucket: activeVariant.codeBucket,
|
|
287
|
+
objectKey: `part/${normalized.slice(activeVariant.partDir.length + 1)}`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (normalized.startsWith("part/")) {
|
|
292
|
+
return {
|
|
293
|
+
bucket: activeVariant.codeBucket,
|
|
294
|
+
objectKey: normalized
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
bucket: activeVariant.codeBucket,
|
|
300
|
+
objectKey: normalized
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async get(key) {
|
|
305
|
+
const resolved = this.resolveLogicalKey(key);
|
|
306
|
+
try {
|
|
307
|
+
const response = await this.s3.getObject({
|
|
308
|
+
Bucket: resolved.bucket,
|
|
309
|
+
Key: resolved.objectKey
|
|
310
|
+
}).promise();
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
key: normalizeKey(key),
|
|
314
|
+
body: await bodyToUtf8(response.Body),
|
|
315
|
+
contentType: response.ContentType ?? getContentTypeForPath(resolved.objectKey),
|
|
316
|
+
lastModified: response.LastModified?.toISOString?.()
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const errorCode = error?.name ?? error?.Code ?? error?.code;
|
|
320
|
+
if (errorCode === "NoSuchKey" || errorCode === "NoSuchBucket") {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async listVariantEntries(variantName) {
|
|
328
|
+
const variant = this.environmentManifest.variants[variantName];
|
|
329
|
+
const entries = [];
|
|
330
|
+
let continuationToken;
|
|
331
|
+
|
|
332
|
+
do {
|
|
333
|
+
const response = await this.s3.listObjectsV2({
|
|
334
|
+
Bucket: variant.codeBucket,
|
|
335
|
+
Prefix: `${variantName}/`,
|
|
336
|
+
ContinuationToken: continuationToken
|
|
337
|
+
}).promise();
|
|
338
|
+
|
|
339
|
+
for (const item of response.Contents ?? []) {
|
|
340
|
+
if (!item.Key.endsWith("/")) {
|
|
341
|
+
entries.push({
|
|
342
|
+
key: item.Key,
|
|
343
|
+
body: null,
|
|
344
|
+
contentType: getContentTypeForPath(item.Key),
|
|
345
|
+
lastModified: item.LastModified?.toISOString?.()
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
continuationToken = response.NextContinuationToken;
|
|
351
|
+
} while (continuationToken);
|
|
352
|
+
|
|
353
|
+
return entries;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async exists(key) {
|
|
357
|
+
const resolved = this.resolveLogicalKey(key);
|
|
358
|
+
try {
|
|
359
|
+
await this.s3.headObject({
|
|
360
|
+
Bucket: resolved.bucket,
|
|
361
|
+
Key: resolved.objectKey
|
|
362
|
+
}).promise();
|
|
363
|
+
return true;
|
|
364
|
+
} catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export class DynamoContentRepository {
|
|
371
|
+
constructor({ dynamo, tableName, indexName, languageLocaleMap = {} }) {
|
|
372
|
+
this.dynamo = dynamo;
|
|
373
|
+
this.tableName = tableName;
|
|
374
|
+
this.indexName = indexName;
|
|
375
|
+
this.languageLocaleMap = languageLocaleMap;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async getByContentId(contentId, language) {
|
|
379
|
+
const response = await this.dynamo.query({
|
|
380
|
+
TableName: this.tableName,
|
|
381
|
+
IndexName: this.indexName,
|
|
382
|
+
KeyConditionExpression: "contentId = :contentId",
|
|
383
|
+
ExpressionAttributeValues: {
|
|
384
|
+
":contentId": contentId
|
|
385
|
+
}
|
|
386
|
+
}).promise();
|
|
387
|
+
const items = response.Items ?? [];
|
|
388
|
+
const candidates = items
|
|
389
|
+
.map((item) => ({
|
|
390
|
+
item,
|
|
391
|
+
score: localeMatchScore(item.locale, language, this.languageLocaleMap)
|
|
392
|
+
}))
|
|
393
|
+
.filter((entry) => entry.score > 0)
|
|
394
|
+
.sort((left, right) => right.score - left.score || String(left.item.id).localeCompare(String(right.item.id)));
|
|
395
|
+
return candidates[0]?.item ?? null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async query(query, language) {
|
|
399
|
+
let lastEvaluatedKey;
|
|
400
|
+
const items = [];
|
|
401
|
+
|
|
402
|
+
do {
|
|
403
|
+
const response = await this.dynamo.scan({
|
|
404
|
+
TableName: this.tableName,
|
|
405
|
+
ExclusiveStartKey: lastEvaluatedKey
|
|
406
|
+
}).promise();
|
|
407
|
+
items.push(...(response.Items ?? []));
|
|
408
|
+
lastEvaluatedKey = response.LastEvaluatedKey;
|
|
409
|
+
} while (lastEvaluatedKey);
|
|
410
|
+
|
|
411
|
+
return applyContentQuery(filterItemsByRequestedLocale(items, language, this.languageLocaleMap), query);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export class DynamoDependencyStore {
|
|
416
|
+
constructor({ dynamo, tableName }) {
|
|
417
|
+
this.dynamo = dynamo;
|
|
418
|
+
this.tableName = tableName;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async queryBySourceId(sourceId) {
|
|
422
|
+
const response = await this.dynamo.query({
|
|
423
|
+
TableName: this.tableName,
|
|
424
|
+
KeyConditionExpression: "sourceId = :sourceId",
|
|
425
|
+
ExpressionAttributeValues: {
|
|
426
|
+
":sourceId": sourceId
|
|
427
|
+
}
|
|
428
|
+
}).promise();
|
|
429
|
+
return response.Items ?? [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async batchWrite(requests) {
|
|
433
|
+
for (const batch of chunk(requests, 25)) {
|
|
434
|
+
await this.dynamo.batchWrite({
|
|
435
|
+
RequestItems: {
|
|
436
|
+
[this.tableName]: batch
|
|
437
|
+
}
|
|
438
|
+
}).promise();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async replaceSourceDependencies(record) {
|
|
443
|
+
const existing = await this.queryBySourceId(record.sourceId);
|
|
444
|
+
if (existing.length > 0) {
|
|
445
|
+
await this.batchWrite(existing.map((item) => ({
|
|
446
|
+
DeleteRequest: {
|
|
447
|
+
Key: {
|
|
448
|
+
sourceId: item.sourceId,
|
|
449
|
+
dependencyKey: item.dependencyKey
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
})));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!record.dependencies || record.dependencies.length === 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await this.batchWrite(record.dependencies.map((dependency) => ({
|
|
460
|
+
PutRequest: {
|
|
461
|
+
Item: {
|
|
462
|
+
sourceId: record.sourceId,
|
|
463
|
+
dependencyKey: `${dependency.kind}#${dependency.id}`,
|
|
464
|
+
templateKey: record.templateKey,
|
|
465
|
+
outputKey: record.outputKey,
|
|
466
|
+
environment: record.environment,
|
|
467
|
+
variant: record.variant,
|
|
468
|
+
language: record.language
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
})));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async findDependentsByDependency(ref) {
|
|
475
|
+
const response = await this.dynamo.query({
|
|
476
|
+
TableName: this.tableName,
|
|
477
|
+
IndexName: "dependencyKey-index",
|
|
478
|
+
KeyConditionExpression: "dependencyKey = :dependencyKey",
|
|
479
|
+
ExpressionAttributeValues: {
|
|
480
|
+
":dependencyKey": `${ref.kind}#${ref.id}`
|
|
481
|
+
}
|
|
482
|
+
}).promise();
|
|
483
|
+
|
|
484
|
+
const grouped = new Map();
|
|
485
|
+
for (const item of response.Items ?? []) {
|
|
486
|
+
if (!grouped.has(item.sourceId)) {
|
|
487
|
+
grouped.set(item.sourceId, {
|
|
488
|
+
sourceId: item.sourceId,
|
|
489
|
+
environment: item.environment,
|
|
490
|
+
variant: item.variant,
|
|
491
|
+
language: item.language,
|
|
492
|
+
templateKey: item.templateKey,
|
|
493
|
+
outputKey: item.outputKey,
|
|
494
|
+
dependencies: []
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
grouped.get(item.sourceId).dependencies.push({
|
|
498
|
+
kind: item.dependencyKey.split("#")[0],
|
|
499
|
+
id: item.dependencyKey.split("#").slice(1).join("#")
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return [...grouped.values()];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async findGeneratedOutputsByTemplate(templateKey, scope) {
|
|
507
|
+
const response = await this.dynamo.query({
|
|
508
|
+
TableName: this.tableName,
|
|
509
|
+
IndexName: "dependencyKey-index",
|
|
510
|
+
KeyConditionExpression: "dependencyKey = :dependencyKey",
|
|
511
|
+
ExpressionAttributeValues: {
|
|
512
|
+
":dependencyKey": `generated-template#${templateKey}`
|
|
513
|
+
}
|
|
514
|
+
}).promise();
|
|
515
|
+
|
|
516
|
+
return (response.Items ?? [])
|
|
517
|
+
.filter((item) => item.environment === scope.environment
|
|
518
|
+
&& item.variant === scope.variant
|
|
519
|
+
&& item.language === scope.language)
|
|
520
|
+
.map((item) => ({
|
|
521
|
+
environment: item.environment,
|
|
522
|
+
variant: item.variant,
|
|
523
|
+
language: item.language,
|
|
524
|
+
templateKey: item.templateKey,
|
|
525
|
+
outputKey: item.outputKey
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async deleteOutput(output) {
|
|
530
|
+
const sourceId = buildSourceId(output.environment, output.variant, output.language, output.outputKey);
|
|
531
|
+
const existing = await this.queryBySourceId(sourceId);
|
|
532
|
+
if (existing.length === 0) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
await this.batchWrite(existing.map((item) => ({
|
|
536
|
+
DeleteRequest: {
|
|
537
|
+
Key: {
|
|
538
|
+
sourceId: item.sourceId,
|
|
539
|
+
dependencyKey: item.dependencyKey
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
})));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export class S3OutputPublisher {
|
|
547
|
+
constructor({ s3, environmentManifest }) {
|
|
548
|
+
this.s3 = s3;
|
|
549
|
+
this.environmentManifest = environmentManifest;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
resolveTargetBucket(target) {
|
|
553
|
+
return this.environmentManifest.variants[target.variant].languages[target.language].targetBucket;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async put(artifact, target) {
|
|
557
|
+
await this.s3.putObject({
|
|
558
|
+
Bucket: this.resolveTargetBucket(target),
|
|
559
|
+
Key: artifact.outputKey,
|
|
560
|
+
Body: artifact.body,
|
|
561
|
+
ContentType: artifact.contentType,
|
|
562
|
+
CacheControl: artifact.cacheControl
|
|
563
|
+
}).promise();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async copySourceObject(sourceKey, target) {
|
|
567
|
+
const sourceBucket = this.environmentManifest.variants[target.variant].codeBucket;
|
|
568
|
+
const encodedSource = `${sourceBucket}/${sourceKey.split("/").map(encodeURIComponent).join("/")}`;
|
|
569
|
+
await this.s3.copyObject({
|
|
570
|
+
Bucket: this.resolveTargetBucket(target),
|
|
571
|
+
Key: target.outputKey,
|
|
572
|
+
CopySource: encodedSource
|
|
573
|
+
}).promise();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async delete(outputKey, target) {
|
|
577
|
+
await this.s3.deleteObject({
|
|
578
|
+
Bucket: this.resolveTargetBucket(target),
|
|
579
|
+
Key: outputKey
|
|
580
|
+
}).promise();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export class LambdaInvalidationScheduler {
|
|
585
|
+
constructor({ lambda, functionName }) {
|
|
586
|
+
this.lambda = lambda;
|
|
587
|
+
this.functionName = functionName;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async enqueue(request) {
|
|
591
|
+
if (!request.distributionId) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
await this.lambda.invoke({
|
|
595
|
+
FunctionName: this.functionName,
|
|
596
|
+
InvocationType: "Event",
|
|
597
|
+
Payload: JSON.stringify(request)
|
|
598
|
+
}).promise();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function buildInvalidationRequest(environmentName, environmentManifest, variantName, languageCode, buildId, paths = ["/*"]) {
|
|
603
|
+
const language = environmentManifest.variants[variantName].languages[languageCode];
|
|
604
|
+
return {
|
|
605
|
+
buildId,
|
|
606
|
+
environment: environmentName,
|
|
607
|
+
variant: variantName,
|
|
608
|
+
language: languageCode,
|
|
609
|
+
distributionId: language.distributionId,
|
|
610
|
+
distributionAliases: [...language.cloudFrontAliases],
|
|
611
|
+
paths,
|
|
612
|
+
requestedAt: new Date().toISOString()
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function isRenderableBucketKey(environmentManifest, variantName, key, renderExtensions) {
|
|
617
|
+
if (key.startsWith("part/")) {
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!key.startsWith(`${variantName}/`)) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const extension = path.extname(key).toLowerCase();
|
|
626
|
+
return renderExtensions.includes(extension);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function defaultDeleteOutputKey(variantName, sourceKey) {
|
|
630
|
+
if (!sourceKey.startsWith(`${variantName}/`)) {
|
|
631
|
+
return sourceKey;
|
|
632
|
+
}
|
|
633
|
+
return sourceKey.slice(variantName.length + 1);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function uniqueTargets(targets) {
|
|
637
|
+
const seen = new Set();
|
|
638
|
+
return targets.filter((target) => {
|
|
639
|
+
const key = `${target.variant}#${target.language}#${target.sourceKey}`;
|
|
640
|
+
if (seen.has(key)) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
seen.add(key);
|
|
644
|
+
return true;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export async function resolveRenderTargetsForEvent({
|
|
649
|
+
event,
|
|
650
|
+
manifest,
|
|
651
|
+
environmentName,
|
|
652
|
+
environmentManifest,
|
|
653
|
+
dependencyStore,
|
|
654
|
+
templateRepositoryFactory
|
|
655
|
+
}) {
|
|
656
|
+
const coreConfig = buildCoreConfigFromEnvironment(manifest, environmentName);
|
|
657
|
+
|
|
658
|
+
if (event.type === "manual-build") {
|
|
659
|
+
const variants = event.variant ? [event.variant] : Object.keys(environmentManifest.variants);
|
|
660
|
+
const targets = [];
|
|
661
|
+
for (const variantName of variants) {
|
|
662
|
+
const templateRepository = templateRepositoryFactory(variantName);
|
|
663
|
+
const templateEntries = await templateRepository.listVariantEntries(variantName);
|
|
664
|
+
targets.push(...createManualRenderTargets({
|
|
665
|
+
config: coreConfig,
|
|
666
|
+
templateEntries,
|
|
667
|
+
environment: environmentName,
|
|
668
|
+
variant: variantName,
|
|
669
|
+
language: event.language,
|
|
670
|
+
entry: event.sourceKey
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
return uniqueTargets(targets);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (event.type === "content-item") {
|
|
677
|
+
const targets = [];
|
|
678
|
+
for (const variantName of Object.keys(environmentManifest.variants)) {
|
|
679
|
+
const templateRepository = templateRepositoryFactory(variantName);
|
|
680
|
+
const templateEntries = await templateRepository.listVariantEntries(variantName);
|
|
681
|
+
targets.push(...createManualRenderTargets({
|
|
682
|
+
config: coreConfig,
|
|
683
|
+
templateEntries,
|
|
684
|
+
environment: environmentName,
|
|
685
|
+
variant: variantName
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
return uniqueTargets(targets);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (event.type === "source-object" && event.key.startsWith("part/")) {
|
|
692
|
+
const dependencyId = event.key.slice("part/".length);
|
|
693
|
+
const dependentRecords = await dependencyStore.findDependentsByDependency({
|
|
694
|
+
kind: "partial",
|
|
695
|
+
id: dependencyId
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
if (dependentRecords.length === 0) {
|
|
699
|
+
const templateRepository = templateRepositoryFactory(event.variant);
|
|
700
|
+
const templateEntries = await templateRepository.listVariantEntries(event.variant);
|
|
701
|
+
return createManualRenderTargets({
|
|
702
|
+
config: coreConfig,
|
|
703
|
+
templateEntries,
|
|
704
|
+
environment: environmentName,
|
|
705
|
+
variant: event.variant
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return uniqueTargets(dependentRecords
|
|
710
|
+
.filter((record) => record.environment === environmentName && record.variant === event.variant)
|
|
711
|
+
.map((record) => ({
|
|
712
|
+
environment: environmentName,
|
|
713
|
+
variant: record.variant,
|
|
714
|
+
language: record.language,
|
|
715
|
+
sourceKey: record.templateKey,
|
|
716
|
+
outputKey: defaultDeleteOutputKey(record.variant, record.templateKey),
|
|
717
|
+
baseUrl: environmentManifest.variants[record.variant].languages[record.language].baseUrl
|
|
718
|
+
})));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (event.type === "source-object") {
|
|
722
|
+
return uniqueTargets(Object.keys(environmentManifest.variants[event.variant].languages).map((languageCode) => ({
|
|
723
|
+
environment: environmentName,
|
|
724
|
+
variant: event.variant,
|
|
725
|
+
language: languageCode,
|
|
726
|
+
sourceKey: event.key,
|
|
727
|
+
outputKey: defaultDeleteOutputKey(event.variant, event.key),
|
|
728
|
+
baseUrl: environmentManifest.variants[event.variant].languages[languageCode].baseUrl
|
|
729
|
+
})));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return [];
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export async function deleteOutputsForTemplate({
|
|
736
|
+
event,
|
|
737
|
+
environmentName,
|
|
738
|
+
environmentManifest,
|
|
739
|
+
dependencyStore,
|
|
740
|
+
publisher,
|
|
741
|
+
invalidationScheduler,
|
|
742
|
+
buildId
|
|
743
|
+
}) {
|
|
744
|
+
const languages = Object.keys(environmentManifest.variants[event.variant].languages);
|
|
745
|
+
const deletedOutputs = [];
|
|
746
|
+
|
|
747
|
+
for (const languageCode of languages) {
|
|
748
|
+
const generated = await dependencyStore.findGeneratedOutputsByTemplate(event.key, {
|
|
749
|
+
environment: environmentName,
|
|
750
|
+
variant: event.variant,
|
|
751
|
+
language: languageCode
|
|
752
|
+
});
|
|
753
|
+
const outputs = generated.length > 0 ? generated : [{
|
|
754
|
+
environment: environmentName,
|
|
755
|
+
variant: event.variant,
|
|
756
|
+
language: languageCode,
|
|
757
|
+
templateKey: event.key,
|
|
758
|
+
outputKey: defaultDeleteOutputKey(event.variant, event.key)
|
|
759
|
+
}];
|
|
760
|
+
|
|
761
|
+
for (const output of outputs) {
|
|
762
|
+
await publisher.delete(output.outputKey, {
|
|
763
|
+
environment: environmentName,
|
|
764
|
+
variant: event.variant,
|
|
765
|
+
language: languageCode,
|
|
766
|
+
sourceKey: event.key,
|
|
767
|
+
outputKey: output.outputKey,
|
|
768
|
+
baseUrl: environmentManifest.variants[event.variant].languages[languageCode].baseUrl
|
|
769
|
+
});
|
|
770
|
+
await dependencyStore.deleteOutput(output);
|
|
771
|
+
await invalidationScheduler.enqueue(buildInvalidationRequest(
|
|
772
|
+
environmentName,
|
|
773
|
+
environmentManifest,
|
|
774
|
+
event.variant,
|
|
775
|
+
languageCode,
|
|
776
|
+
buildId
|
|
777
|
+
));
|
|
778
|
+
deletedOutputs.push(output.outputKey);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return deletedOutputs;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function renderAndPublishTargets({
|
|
786
|
+
manifest,
|
|
787
|
+
environmentName,
|
|
788
|
+
environmentManifest,
|
|
789
|
+
contentRepository,
|
|
790
|
+
dependencyStore,
|
|
791
|
+
publisher,
|
|
792
|
+
invalidationScheduler,
|
|
793
|
+
targets,
|
|
794
|
+
buildId
|
|
795
|
+
}) {
|
|
796
|
+
const coreConfig = buildCoreConfigFromEnvironment(manifest, environmentName);
|
|
797
|
+
const rendered = [];
|
|
798
|
+
const deleted = [];
|
|
799
|
+
const warnings = [];
|
|
800
|
+
|
|
801
|
+
for (const target of targets) {
|
|
802
|
+
const templateRepository = new S3TemplateRepository({
|
|
803
|
+
s3: publisher.s3,
|
|
804
|
+
environmentManifest,
|
|
805
|
+
activeVariantName: target.variant
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const previousOutputs = await dependencyStore.findGeneratedOutputsByTemplate(target.sourceKey, {
|
|
809
|
+
environment: environmentName,
|
|
810
|
+
variant: target.variant,
|
|
811
|
+
language: target.language
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const results = await renderSourceTemplate({
|
|
815
|
+
config: coreConfig,
|
|
816
|
+
templateRepository,
|
|
817
|
+
contentRepository,
|
|
818
|
+
environment: environmentName,
|
|
819
|
+
variantName: target.variant,
|
|
820
|
+
languageCode: target.language,
|
|
821
|
+
sourceKey: target.sourceKey
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const nextOutputKeys = new Set();
|
|
825
|
+
|
|
826
|
+
for (const result of results) {
|
|
827
|
+
nextOutputKeys.add(result.artifact.outputKey);
|
|
828
|
+
await publisher.put(result.artifact, result.target);
|
|
829
|
+
await dependencyStore.replaceSourceDependencies({
|
|
830
|
+
sourceId: buildSourceId(environmentName, result.target.variant, result.target.language, result.artifact.outputKey),
|
|
831
|
+
environment: environmentName,
|
|
832
|
+
variant: result.target.variant,
|
|
833
|
+
language: result.target.language,
|
|
834
|
+
templateKey: target.sourceKey,
|
|
835
|
+
outputKey: result.artifact.outputKey,
|
|
836
|
+
dependencies: result.dependencies
|
|
837
|
+
});
|
|
838
|
+
await invalidationScheduler.enqueue(buildInvalidationRequest(
|
|
839
|
+
environmentName,
|
|
840
|
+
environmentManifest,
|
|
841
|
+
result.target.variant,
|
|
842
|
+
result.target.language,
|
|
843
|
+
buildId,
|
|
844
|
+
result.invalidationPaths
|
|
845
|
+
));
|
|
846
|
+
warnings.push(...result.warnings);
|
|
847
|
+
rendered.push(result.artifact.outputKey);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
for (const previousOutput of previousOutputs) {
|
|
851
|
+
if (nextOutputKeys.has(previousOutput.outputKey)) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
await publisher.delete(previousOutput.outputKey, {
|
|
855
|
+
environment: environmentName,
|
|
856
|
+
variant: previousOutput.variant,
|
|
857
|
+
language: previousOutput.language,
|
|
858
|
+
sourceKey: previousOutput.templateKey,
|
|
859
|
+
outputKey: previousOutput.outputKey,
|
|
860
|
+
baseUrl: environmentManifest.variants[previousOutput.variant].languages[previousOutput.language].baseUrl
|
|
861
|
+
});
|
|
862
|
+
await dependencyStore.deleteOutput(previousOutput);
|
|
863
|
+
await invalidationScheduler.enqueue(buildInvalidationRequest(
|
|
864
|
+
environmentName,
|
|
865
|
+
environmentManifest,
|
|
866
|
+
previousOutput.variant,
|
|
867
|
+
previousOutput.language,
|
|
868
|
+
buildId
|
|
869
|
+
));
|
|
870
|
+
deleted.push(previousOutput.outputKey);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return { rendered, deleted, warnings };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function createRepositoriesAndPublishers({ clients, environmentManifest }) {
|
|
878
|
+
const languageLocaleMap = Object.fromEntries(Object.entries(environmentManifest.variants).flatMap(([, variantConfig]) => (
|
|
879
|
+
Object.entries(variantConfig.languages).map(([languageCode, languageConfig]) => [
|
|
880
|
+
languageCode,
|
|
881
|
+
languageConfig.webinyLocale ?? languageCode
|
|
882
|
+
])
|
|
883
|
+
)));
|
|
884
|
+
|
|
885
|
+
return {
|
|
886
|
+
contentRepository: new DynamoContentRepository({
|
|
887
|
+
dynamo: clients.dynamo,
|
|
888
|
+
tableName: environmentManifest.tables.content,
|
|
889
|
+
indexName: environmentManifest.tables.contentIdIndexName ?? "contentid",
|
|
890
|
+
languageLocaleMap
|
|
891
|
+
}),
|
|
892
|
+
dependencyStore: new DynamoDependencyStore({
|
|
893
|
+
dynamo: clients.dynamo,
|
|
894
|
+
tableName: environmentManifest.tables.dependency
|
|
895
|
+
}),
|
|
896
|
+
publisher: new S3OutputPublisher({
|
|
897
|
+
s3: clients.s3,
|
|
898
|
+
environmentManifest
|
|
899
|
+
}),
|
|
900
|
+
invalidationScheduler: new LambdaInvalidationScheduler({
|
|
901
|
+
lambda: clients.lambda,
|
|
902
|
+
functionName: environmentManifest.functions.invalidationScheduler
|
|
903
|
+
})
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export async function invokeLambdaEvent(lambda, functionName, payload) {
|
|
908
|
+
await lambda.invoke({
|
|
909
|
+
FunctionName: functionName,
|
|
910
|
+
InvocationType: "Event",
|
|
911
|
+
Payload: JSON.stringify(payload)
|
|
912
|
+
}).promise();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export function createBuildId(prefix = "build") {
|
|
916
|
+
return `${prefix}-${Date.now()}`;
|
|
917
|
+
}
|