@projectdochelp/s3te 3.1.1 → 3.1.3
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/README.md +152 -29
- package/package.json +1 -1
- package/packages/aws-adapter/src/deploy.mjs +11 -15
- package/packages/aws-adapter/src/features.mjs +17 -4
- package/packages/aws-adapter/src/index.mjs +1 -0
- package/packages/aws-adapter/src/package.mjs +4 -3
- package/packages/aws-adapter/src/sync.mjs +155 -0
- package/packages/cli/bin/s3te.mjs +34 -0
- package/packages/cli/src/project.mjs +337 -32
- package/packages/core/src/config.mjs +173 -23
- package/packages/core/src/index.mjs +3 -0
- package/packages/core/src/render.mjs +3 -2
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { assert, S3teError } from "./errors.mjs";
|
|
5
5
|
|
|
6
|
-
const KNOWN_PLACEHOLDERS = new Set(["env", "stackPrefix", "project", "variant", "lang"]);
|
|
6
|
+
const KNOWN_PLACEHOLDERS = new Set(["env", "envPrefix", "stackPrefix", "project", "variant", "lang"]);
|
|
7
7
|
|
|
8
8
|
function upperSnakeCase(value) {
|
|
9
9
|
return value.replace(/-/g, "_").toUpperCase();
|
|
@@ -28,7 +28,7 @@ function ensureKnownPlaceholders(input, fieldPath, errors) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function replacePlaceholders(input, values) {
|
|
31
|
-
return String(input).replace(/\{(env|stackPrefix|project|variant|lang)\}/g, (_, token) => values[token]);
|
|
31
|
+
return String(input).replace(/\{(env|envPrefix|stackPrefix|project|variant|lang)\}/g, (_, token) => values[token]);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function normalizeRelativeProjectPath(relativePath) {
|
|
@@ -46,12 +46,58 @@ function isValidUpperSnake(value) {
|
|
|
46
46
|
return /^[A-Z0-9_]+$/.test(value);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function normalizeStringList(values, fallback = []) {
|
|
50
|
+
const source = values ?? fallback;
|
|
51
|
+
const items = Array.isArray(source) ? source : [source];
|
|
52
|
+
return [...new Set(items
|
|
53
|
+
.map((value) => String(value).trim())
|
|
54
|
+
.filter(Boolean))];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isProductionEnvironment(environmentName) {
|
|
58
|
+
return String(environmentName).trim().toLowerCase() === "prod";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasProductionEnvironment(config) {
|
|
62
|
+
return Object.keys(config.environments ?? {}).some((environmentName) => isProductionEnvironment(environmentName));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function environmentResourcePrefix(environmentName) {
|
|
66
|
+
return isProductionEnvironment(environmentName) ? "" : `${environmentName}-`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function environmentHostPrefix(config, environmentName) {
|
|
70
|
+
if (!hasProductionEnvironment(config) || isProductionEnvironment(environmentName)) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `${environmentName}.`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function prefixHostForEnvironment(config, host, environmentName) {
|
|
78
|
+
const prefix = environmentHostPrefix(config, environmentName);
|
|
79
|
+
if (!prefix) {
|
|
80
|
+
return host;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return host.startsWith(prefix) ? host : `${prefix}${host}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isValidConfiguredHost(value) {
|
|
87
|
+
const candidate = String(value).trim();
|
|
88
|
+
if (!candidate || candidate.includes("://") || candidate.includes("/") || candidate.includes(":")) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return /^[A-Za-z0-9.-]+$/.test(candidate);
|
|
93
|
+
}
|
|
94
|
+
|
|
49
95
|
function defaultTargetBucketPattern({ variant, language, languageCount, isDefaultLanguage, project }) {
|
|
50
96
|
if (languageCount === 1 || isDefaultLanguage) {
|
|
51
|
-
return `{
|
|
97
|
+
return `{envPrefix}${variant}-${project}`;
|
|
52
98
|
}
|
|
53
99
|
|
|
54
|
-
return `{
|
|
100
|
+
return `{envPrefix}${variant}-${project}-${language}`;
|
|
55
101
|
}
|
|
56
102
|
|
|
57
103
|
async function ensureDirectoryExists(projectDir, relativePath, errors) {
|
|
@@ -79,6 +125,7 @@ function createPlaceholderContext(config, environmentName, variantName, language
|
|
|
79
125
|
const variantConfig = variantName ? config.variants[variantName] : null;
|
|
80
126
|
return {
|
|
81
127
|
env: environmentName,
|
|
128
|
+
envPrefix: environmentResourcePrefix(environmentName),
|
|
82
129
|
stackPrefix: environmentConfig.stackPrefix,
|
|
83
130
|
project: config.project.name,
|
|
84
131
|
variant: variantName ?? "website",
|
|
@@ -86,6 +133,35 @@ function createPlaceholderContext(config, environmentName, variantName, language
|
|
|
86
133
|
};
|
|
87
134
|
}
|
|
88
135
|
|
|
136
|
+
function resolveWebinyConfigDefaults(webinyConfig = {}) {
|
|
137
|
+
return {
|
|
138
|
+
enabled: webinyConfig.enabled ?? false,
|
|
139
|
+
sourceTableName: webinyConfig.sourceTableName,
|
|
140
|
+
mirrorTableName: webinyConfig.mirrorTableName ?? "{stackPrefix}_s3te_content_{project}",
|
|
141
|
+
relevantModels: normalizeStringList(webinyConfig.relevantModels, ["staticContent", "staticCodeContent"]),
|
|
142
|
+
tenant: webinyConfig.tenant
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveProjectWebinyConfig(projectConfig) {
|
|
147
|
+
const baseConfig = resolveWebinyConfigDefaults(projectConfig.integrations?.webiny ?? {});
|
|
148
|
+
const environmentConfigs = Object.fromEntries(Object.entries(projectConfig.integrations?.webiny?.environments ?? {}).map(([environmentName, webinyConfig]) => ([
|
|
149
|
+
environmentName,
|
|
150
|
+
{
|
|
151
|
+
enabled: webinyConfig.enabled,
|
|
152
|
+
sourceTableName: webinyConfig.sourceTableName,
|
|
153
|
+
mirrorTableName: webinyConfig.mirrorTableName,
|
|
154
|
+
relevantModels: webinyConfig.relevantModels ? normalizeStringList(webinyConfig.relevantModels) : undefined,
|
|
155
|
+
tenant: webinyConfig.tenant
|
|
156
|
+
}
|
|
157
|
+
])));
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...baseConfig,
|
|
161
|
+
environments: environmentConfigs
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
89
165
|
export async function loadProjectConfig(configPath) {
|
|
90
166
|
const raw = await fs.readFile(configPath, "utf8");
|
|
91
167
|
try {
|
|
@@ -148,7 +224,7 @@ export function resolveProjectConfig(projectConfig) {
|
|
|
148
224
|
languages
|
|
149
225
|
};
|
|
150
226
|
|
|
151
|
-
awsCodeBuckets[variantName] = awsCodeBuckets[variantName] ?? "{
|
|
227
|
+
awsCodeBuckets[variantName] = awsCodeBuckets[variantName] ?? "{envPrefix}{variant}-code-{project}";
|
|
152
228
|
}
|
|
153
229
|
|
|
154
230
|
const aws = {
|
|
@@ -171,13 +247,7 @@ export function resolveProjectConfig(projectConfig) {
|
|
|
171
247
|
};
|
|
172
248
|
|
|
173
249
|
const integrations = {
|
|
174
|
-
webiny:
|
|
175
|
-
enabled: projectConfig.integrations?.webiny?.enabled ?? false,
|
|
176
|
-
sourceTableName: projectConfig.integrations?.webiny?.sourceTableName,
|
|
177
|
-
mirrorTableName: projectConfig.integrations?.webiny?.mirrorTableName ?? "{stackPrefix}_s3te_content_{project}",
|
|
178
|
-
relevantModels: projectConfig.integrations?.webiny?.relevantModels ?? ["staticContent", "staticCodeContent"],
|
|
179
|
-
tenant: projectConfig.integrations?.webiny?.tenant
|
|
180
|
-
}
|
|
250
|
+
webiny: resolveProjectWebinyConfig(projectConfig)
|
|
181
251
|
};
|
|
182
252
|
|
|
183
253
|
for (const [variantName, variantConfig] of Object.entries(variants)) {
|
|
@@ -219,13 +289,42 @@ export function resolveTargetBucketName(config, environmentName, variantName, la
|
|
|
219
289
|
);
|
|
220
290
|
}
|
|
221
291
|
|
|
292
|
+
export function resolveBaseUrl(config, environmentName, variantName, languageCode) {
|
|
293
|
+
return prefixHostForEnvironment(
|
|
294
|
+
config,
|
|
295
|
+
config.variants[variantName].languages[languageCode].baseUrl,
|
|
296
|
+
environmentName
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function resolveCloudFrontAliases(config, environmentName, variantName, languageCode) {
|
|
301
|
+
return config.variants[variantName].languages[languageCode].cloudFrontAliases
|
|
302
|
+
.map((alias) => prefixHostForEnvironment(config, alias, environmentName));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function resolveEnvironmentWebinyIntegration(config, environmentName) {
|
|
306
|
+
const baseConfig = resolveWebinyConfigDefaults(config.integrations?.webiny ?? {});
|
|
307
|
+
const environmentOverride = config.integrations?.webiny?.environments?.[environmentName] ?? {};
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
enabled: environmentOverride.enabled ?? baseConfig.enabled,
|
|
311
|
+
sourceTableName: environmentOverride.sourceTableName ?? baseConfig.sourceTableName,
|
|
312
|
+
mirrorTableName: environmentOverride.mirrorTableName ?? baseConfig.mirrorTableName,
|
|
313
|
+
relevantModels: environmentOverride.relevantModels
|
|
314
|
+
? normalizeStringList(environmentOverride.relevantModels)
|
|
315
|
+
: [...baseConfig.relevantModels],
|
|
316
|
+
tenant: environmentOverride.tenant ?? baseConfig.tenant
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
222
320
|
export function resolveTableNames(config, environmentName) {
|
|
223
321
|
const context = createPlaceholderContext(config, environmentName);
|
|
322
|
+
const webinyConfig = resolveEnvironmentWebinyIntegration(config, environmentName);
|
|
224
323
|
return {
|
|
225
324
|
dependency: replacePlaceholders(config.aws.dependencyStore.tableName, context),
|
|
226
325
|
content: replacePlaceholders(config.aws.contentStore.tableName, context),
|
|
227
326
|
invalidation: replacePlaceholders(config.aws.invalidationStore.tableName, context),
|
|
228
|
-
webinyMirror: replacePlaceholders(
|
|
327
|
+
webinyMirror: replacePlaceholders(webinyConfig.mirrorTableName, context)
|
|
229
328
|
};
|
|
230
329
|
}
|
|
231
330
|
|
|
@@ -240,6 +339,7 @@ export function resolveStackName(config, environmentName) {
|
|
|
240
339
|
|
|
241
340
|
export function buildEnvironmentRuntimeConfig(config, environmentName, stackOutputs = {}) {
|
|
242
341
|
const environmentConfig = config.environments[environmentName];
|
|
342
|
+
const webinyConfig = resolveEnvironmentWebinyIntegration(config, environmentName);
|
|
243
343
|
const tables = resolveTableNames(config, environmentName);
|
|
244
344
|
const runtimeParameterName = resolveRuntimeManifestParameterName(config, environmentName);
|
|
245
345
|
const stackName = resolveStackName(config, environmentName);
|
|
@@ -249,11 +349,13 @@ export function buildEnvironmentRuntimeConfig(config, environmentName, stackOutp
|
|
|
249
349
|
const languages = {};
|
|
250
350
|
for (const [languageCode, languageConfig] of Object.entries(variantConfig.languages)) {
|
|
251
351
|
const targetBucket = resolveTargetBucketName(config, environmentName, variantName, languageCode);
|
|
352
|
+
const baseUrl = resolveBaseUrl(config, environmentName, variantName, languageCode);
|
|
353
|
+
const cloudFrontAliases = resolveCloudFrontAliases(config, environmentName, variantName, languageCode);
|
|
252
354
|
languages[languageCode] = {
|
|
253
355
|
code: languageCode,
|
|
254
|
-
baseUrl
|
|
356
|
+
baseUrl,
|
|
255
357
|
targetBucket,
|
|
256
|
-
cloudFrontAliases
|
|
358
|
+
cloudFrontAliases,
|
|
257
359
|
webinyLocale: languageConfig.webinyLocale,
|
|
258
360
|
distributionId: stackOutputs.distributionIds?.[variantName]?.[languageCode] ?? "",
|
|
259
361
|
distributionDomainName: stackOutputs.distributionDomains?.[variantName]?.[languageCode] ?? ""
|
|
@@ -284,7 +386,7 @@ export function buildEnvironmentRuntimeConfig(config, environmentName, stackOutp
|
|
|
284
386
|
rendering: { ...config.rendering },
|
|
285
387
|
integrations: {
|
|
286
388
|
webiny: {
|
|
287
|
-
...
|
|
389
|
+
...webinyConfig,
|
|
288
390
|
mirrorTableName: tables.webinyMirror
|
|
289
391
|
}
|
|
290
392
|
},
|
|
@@ -363,12 +465,28 @@ export async function validateAndResolveProjectConfig(projectConfig, options = {
|
|
|
363
465
|
code: "CONFIG_SCHEMA_ERROR",
|
|
364
466
|
message: `Variant ${variantName} language ${languageCode} is missing baseUrl.`
|
|
365
467
|
});
|
|
468
|
+
} else if (!isValidConfiguredHost(languageConfig.baseUrl)) {
|
|
469
|
+
errors.push({
|
|
470
|
+
code: "CONFIG_SCHEMA_ERROR",
|
|
471
|
+
message: `Variant ${variantName} language ${languageCode} baseUrl must be a hostname without protocol or path.`,
|
|
472
|
+
details: { value: languageConfig.baseUrl }
|
|
473
|
+
});
|
|
366
474
|
}
|
|
367
475
|
if (!Array.isArray(languageConfig.cloudFrontAliases) || languageConfig.cloudFrontAliases.length === 0) {
|
|
368
476
|
errors.push({
|
|
369
477
|
code: "CONFIG_SCHEMA_ERROR",
|
|
370
478
|
message: `Variant ${variantName} language ${languageCode} needs at least one cloudFrontAlias.`
|
|
371
479
|
});
|
|
480
|
+
} else {
|
|
481
|
+
for (const alias of languageConfig.cloudFrontAliases) {
|
|
482
|
+
if (!isValidConfiguredHost(alias)) {
|
|
483
|
+
errors.push({
|
|
484
|
+
code: "CONFIG_SCHEMA_ERROR",
|
|
485
|
+
message: `Variant ${variantName} language ${languageCode} cloudFrontAliases must contain hostnames without protocol or path.`,
|
|
486
|
+
details: { value: alias }
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
372
490
|
}
|
|
373
491
|
if (languageConfig.webinyLocale !== undefined && typeof languageConfig.webinyLocale !== "string") {
|
|
374
492
|
errors.push({
|
|
@@ -383,11 +501,27 @@ export async function validateAndResolveProjectConfig(projectConfig, options = {
|
|
|
383
501
|
}
|
|
384
502
|
}
|
|
385
503
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
504
|
+
const configuredWebiny = projectConfig.integrations?.webiny;
|
|
505
|
+
for (const [environmentName] of environmentEntries) {
|
|
506
|
+
const environmentWebinyConfig = resolveEnvironmentWebinyIntegration(resolveProjectConfig({
|
|
507
|
+
...projectConfig,
|
|
508
|
+
environments: Object.fromEntries(environmentEntries)
|
|
509
|
+
}), environmentName);
|
|
510
|
+
if (environmentWebinyConfig.enabled && !environmentWebinyConfig.sourceTableName) {
|
|
511
|
+
errors.push({
|
|
512
|
+
code: "CONFIG_CONFLICT_ERROR",
|
|
513
|
+
message: `Webiny integration requires sourceTableName when enabled for environment ${environmentName}.`
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
for (const environmentName of Object.keys(configuredWebiny?.environments ?? {})) {
|
|
519
|
+
if (!projectConfig.environments?.[environmentName]) {
|
|
520
|
+
errors.push({
|
|
521
|
+
code: "CONFIG_CONFLICT_ERROR",
|
|
522
|
+
message: `integrations.webiny.environments.${environmentName} does not match a configured environment.`
|
|
523
|
+
});
|
|
524
|
+
}
|
|
391
525
|
}
|
|
392
526
|
|
|
393
527
|
for (const [variantName, pattern] of Object.entries(projectConfig.aws?.codeBuckets ?? {})) {
|
|
@@ -403,8 +537,13 @@ export async function validateAndResolveProjectConfig(projectConfig, options = {
|
|
|
403
537
|
if (projectConfig.aws?.invalidationStore?.tableName) {
|
|
404
538
|
ensureKnownPlaceholders(projectConfig.aws.invalidationStore.tableName, "aws.invalidationStore.tableName", errors);
|
|
405
539
|
}
|
|
406
|
-
if (
|
|
407
|
-
ensureKnownPlaceholders(
|
|
540
|
+
if (configuredWebiny?.mirrorTableName) {
|
|
541
|
+
ensureKnownPlaceholders(configuredWebiny.mirrorTableName, "integrations.webiny.mirrorTableName", errors);
|
|
542
|
+
}
|
|
543
|
+
for (const [environmentName, webinyConfig] of Object.entries(configuredWebiny?.environments ?? {})) {
|
|
544
|
+
if (webinyConfig.mirrorTableName) {
|
|
545
|
+
ensureKnownPlaceholders(webinyConfig.mirrorTableName, `integrations.webiny.environments.${environmentName}.mirrorTableName`, errors);
|
|
546
|
+
}
|
|
408
547
|
}
|
|
409
548
|
|
|
410
549
|
if (errors.length > 0) {
|
|
@@ -414,6 +553,7 @@ export async function validateAndResolveProjectConfig(projectConfig, options = {
|
|
|
414
553
|
const resolvedConfig = resolveProjectConfig(projectConfig);
|
|
415
554
|
const seenTargetBuckets = new Set();
|
|
416
555
|
const seenCodeBuckets = new Set();
|
|
556
|
+
const seenCloudFrontAliases = new Set();
|
|
417
557
|
|
|
418
558
|
for (const variantConfig of Object.values(resolvedConfig.variants)) {
|
|
419
559
|
await ensureDirectoryExists(projectDir, variantConfig.sourceDir, errors);
|
|
@@ -452,6 +592,16 @@ export async function validateAndResolveProjectConfig(projectConfig, options = {
|
|
|
452
592
|
});
|
|
453
593
|
}
|
|
454
594
|
seenTargetBuckets.add(targetBucket);
|
|
595
|
+
|
|
596
|
+
for (const alias of resolveCloudFrontAliases(resolvedConfig, environmentName, variantName, languageCode)) {
|
|
597
|
+
if (seenCloudFrontAliases.has(alias)) {
|
|
598
|
+
errors.push({
|
|
599
|
+
code: "CONFIG_CONFLICT_ERROR",
|
|
600
|
+
message: `Duplicate cloudFrontAlias ${alias}.`
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
seenCloudFrontAliases.add(alias);
|
|
604
|
+
}
|
|
455
605
|
}
|
|
456
606
|
}
|
|
457
607
|
}
|
|
@@ -4,7 +4,10 @@ export { minifyHtml, repairTruncatedHtml } from "./minify.mjs";
|
|
|
4
4
|
export {
|
|
5
5
|
buildEnvironmentRuntimeConfig,
|
|
6
6
|
loadProjectConfig,
|
|
7
|
+
resolveBaseUrl,
|
|
7
8
|
resolveCodeBucketName,
|
|
9
|
+
resolveCloudFrontAliases,
|
|
10
|
+
resolveEnvironmentWebinyIntegration,
|
|
8
11
|
resolveRuntimeManifestParameterName,
|
|
9
12
|
resolveProjectConfig,
|
|
10
13
|
resolveStackName,
|
|
@@ -4,6 +4,7 @@ import { assert, S3teError } from "./errors.mjs";
|
|
|
4
4
|
import { getContentTypeForPath } from "./mime.mjs";
|
|
5
5
|
import { minifyHtml, repairTruncatedHtml } from "./minify.mjs";
|
|
6
6
|
import { readContentField, serializeContentValue } from "./content-query.mjs";
|
|
7
|
+
import { resolveBaseUrl } from "./config.mjs";
|
|
7
8
|
|
|
8
9
|
function createWarning(code, message, sourceKey) {
|
|
9
10
|
return { code, message, sourceKey };
|
|
@@ -442,7 +443,7 @@ export async function renderSourceTemplate({ config, templateRepository, content
|
|
|
442
443
|
language: languageCode,
|
|
443
444
|
sourceKey,
|
|
444
445
|
outputKey: sourceWithinVariant,
|
|
445
|
-
baseUrl: buildDefaultBaseUrl(
|
|
446
|
+
baseUrl: buildDefaultBaseUrl(resolveBaseUrl(config, environment, variantName, languageCode))
|
|
446
447
|
};
|
|
447
448
|
|
|
448
449
|
const trimmed = stripLeadingWhitespace(body);
|
|
@@ -527,7 +528,7 @@ export function createManualRenderTargets({ config, templateEntries, environment
|
|
|
527
528
|
language: languageCode,
|
|
528
529
|
sourceKey: templateEntry.key,
|
|
529
530
|
outputKey,
|
|
530
|
-
baseUrl: config
|
|
531
|
+
baseUrl: resolveBaseUrl(config, environment, variantName, languageCode)
|
|
531
532
|
});
|
|
532
533
|
}
|
|
533
534
|
}
|