@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.
@@ -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 `{env}-${variant}-${project}`;
97
+ return `{envPrefix}${variant}-${project}`;
52
98
  }
53
99
 
54
- return `{env}-${variant}-${project}-${language}`;
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] ?? "{env}-{variant}-code-{project}";
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(config.integrations.webiny.mirrorTableName, context)
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: languageConfig.baseUrl,
356
+ baseUrl,
255
357
  targetBucket,
256
- cloudFrontAliases: [...languageConfig.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
- ...config.integrations.webiny,
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
- if (projectConfig.integrations?.webiny?.enabled && !projectConfig.integrations?.webiny?.sourceTableName) {
387
- errors.push({
388
- code: "CONFIG_CONFLICT_ERROR",
389
- message: "Webiny integration requires sourceTableName when enabled."
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 (projectConfig.integrations?.webiny?.mirrorTableName) {
407
- ensureKnownPlaceholders(projectConfig.integrations.webiny.mirrorTableName, "integrations.webiny.mirrorTableName", errors);
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(languageConfig.baseUrl)
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.variants[variantName].languages[languageCode].baseUrl
531
+ baseUrl: resolveBaseUrl(config, environment, variantName, languageCode)
531
532
  });
532
533
  }
533
534
  }