@projectdochelp/s3te 3.1.1 → 3.1.2
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 +36 -25
- package/package.json +1 -1
- package/packages/aws-adapter/src/deploy.mjs +2 -2
- package/packages/aws-adapter/src/features.mjs +17 -4
- package/packages/aws-adapter/src/package.mjs +4 -3
- package/packages/cli/bin/s3te.mjs +1 -0
- package/packages/cli/src/project.mjs +275 -31
- package/packages/core/src/config.mjs +173 -23
- package/packages/core/src/index.mjs +3 -0
- package/packages/core/src/render.mjs +3 -2
package/README.md
CHANGED
|
@@ -6,14 +6,19 @@ This README is the user guide for the rewrite generation. The deeper implementat
|
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
- [S3TemplateEngine](#s3templateengine)
|
|
10
|
+
- [Table of Contents](#table-of-contents)
|
|
11
|
+
- [Motivation](#motivation)
|
|
12
|
+
- [Support](#support)
|
|
13
|
+
- [Concept](#concept)
|
|
14
|
+
- [Installation (AWS)](#installation-aws)
|
|
15
|
+
- [Installation (VSCode)](#installation-vscode)
|
|
16
|
+
- [Installation (S3TE)](#installation-s3te)
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [Daily Workflow](#daily-workflow)
|
|
19
|
+
- [CLI Commands](#cli-commands)
|
|
20
|
+
- [Template Commands](#template-commands)
|
|
21
|
+
- [Optional: Webiny CMS](#optional-webiny-cms)
|
|
17
22
|
|
|
18
23
|
## Motivation
|
|
19
24
|
|
|
@@ -152,7 +157,7 @@ With the local package installed, initialize the project like this:
|
|
|
152
157
|
npx s3te init --project-name mywebsite --base-url example.com
|
|
153
158
|
```
|
|
154
159
|
|
|
155
|
-
If `npm install` already created a minimal `package.json`, `s3te init` extends it with the missing S3TE defaults and scripts instead of failing.
|
|
160
|
+
You can safely run `s3te init` more than once. If `npm install` already created a minimal `package.json`, `s3te init` extends it with the missing S3TE defaults and scripts instead of failing. An existing `s3te.config.json` is completed with missing scaffold defaults, explicit `--project-name` and `--base-url` values are refreshed on re-run, and the generated schema file is updated to the current package version. Existing content files and templates stay untouched unless you use `--force`.
|
|
156
161
|
|
|
157
162
|
If you want a one-shot scaffold without installing first, and `@projectdochelp/s3te` is already published on npm, this also works:
|
|
158
163
|
|
|
@@ -160,19 +165,6 @@ If you want a one-shot scaffold without installing first, and `@projectdochelp/s
|
|
|
160
165
|
npx --package @projectdochelp/s3te s3te init --project-name mywebsite --base-url example.com
|
|
161
166
|
```
|
|
162
167
|
|
|
163
|
-
That command only works after a real npm publish. A GitHub repository on its own is not enough.
|
|
164
|
-
|
|
165
|
-
If you are still working from this repository before the first npm publish, run the CLI directly from the repo root instead:
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
node packages/cli/bin/s3te.mjs init --dir ./mywebsite --project-name mywebsite --base-url example.com
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
</details>
|
|
172
|
-
|
|
173
|
-
<details>
|
|
174
|
-
<summary>4. What the scaffold creates</summary>
|
|
175
|
-
|
|
176
168
|
The default scaffold creates:
|
|
177
169
|
|
|
178
170
|
```text
|
|
@@ -198,7 +190,7 @@ mywebsite/
|
|
|
198
190
|
</details>
|
|
199
191
|
|
|
200
192
|
<details>
|
|
201
|
-
<summary>
|
|
193
|
+
<summary>4. Fill in the real AWS values in <code>s3te.config.json</code></summary>
|
|
202
194
|
|
|
203
195
|
The most important fields for a first deployment are:
|
|
204
196
|
|
|
@@ -227,10 +219,12 @@ The most important fields for a first deployment are:
|
|
|
227
219
|
|
|
228
220
|
`route53HostedZoneId` is optional. Leave it out if you want to manage DNS yourself.
|
|
229
221
|
|
|
222
|
+
Use plain hostnames in `baseUrl` and `cloudFrontAliases`, not full URLs. If your config contains a `prod` environment plus additional environments such as `test` or `stage`, S3TE keeps the `prod` hostname unchanged and derives non-production hostnames automatically by prepending `<env>.`.
|
|
223
|
+
|
|
230
224
|
</details>
|
|
231
225
|
|
|
232
226
|
<details>
|
|
233
|
-
<summary>
|
|
227
|
+
<summary>5. Run the first local check and deploy</summary>
|
|
234
228
|
|
|
235
229
|
```bash
|
|
236
230
|
npx s3te validate
|
|
@@ -378,6 +372,13 @@ npx s3te migrate --enable-webiny --webiny-source-table webiny-1234567 --webiny-t
|
|
|
378
372
|
|
|
379
373
|
`staticContent` and `staticCodeContent` are kept automatically. Add `--webiny-model` once per custom model you want S3TE to mirror.
|
|
380
374
|
|
|
375
|
+
If different environments should read from different Webiny installations or tenants, run the migration per environment:
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
npx s3te migrate --env test --enable-webiny --webiny-source-table webiny-test-1234567 --webiny-tenant preview --write
|
|
379
|
+
npx s3te migrate --env prod --enable-webiny --webiny-source-table webiny-live-1234567 --webiny-tenant root --write
|
|
380
|
+
```
|
|
381
|
+
|
|
381
382
|
4. Turn on DynamoDB Streams for the Webiny source table with `NEW_AND_OLD_IMAGES`.
|
|
382
383
|
5. If your S3TE language keys are not identical to your Webiny locales, add `webinyLocale` per language in `s3te.config.json`, for example `"en": { "webinyLocale": "en-US" }`.
|
|
383
384
|
6. If your Webiny installation hosts multiple tenants, keep `integrations.webiny.tenant` set so S3TE only mirrors the intended tenant.
|
|
@@ -411,7 +412,17 @@ Example config block:
|
|
|
411
412
|
"sourceTableName": "webiny-1234567",
|
|
412
413
|
"mirrorTableName": "{stackPrefix}_s3te_content_{project}",
|
|
413
414
|
"tenant": "root",
|
|
414
|
-
"relevantModels": ["article", "staticContent", "staticCodeContent"]
|
|
415
|
+
"relevantModels": ["article", "staticContent", "staticCodeContent"],
|
|
416
|
+
"environments": {
|
|
417
|
+
"test": {
|
|
418
|
+
"sourceTableName": "webiny-test-1234567",
|
|
419
|
+
"tenant": "preview"
|
|
420
|
+
},
|
|
421
|
+
"prod": {
|
|
422
|
+
"sourceTableName": "webiny-live-1234567",
|
|
423
|
+
"tenant": "root"
|
|
424
|
+
}
|
|
425
|
+
}
|
|
415
426
|
}
|
|
416
427
|
}
|
|
417
428
|
```
|
package/package.json
CHANGED
|
@@ -243,12 +243,12 @@ export async function deployAwsProject({
|
|
|
243
243
|
}) {
|
|
244
244
|
const runtimeConfig = buildEnvironmentRuntimeConfig(config, environment);
|
|
245
245
|
const requestedFeatureSet = new Set(features);
|
|
246
|
-
const featureSet = new Set(resolveRequestedFeatures(config, features));
|
|
246
|
+
const featureSet = new Set(resolveRequestedFeatures(config, features, environment));
|
|
247
247
|
const stackName = resolveStackName(config, environment);
|
|
248
248
|
const tempStackName = temporaryStackName(stackName);
|
|
249
249
|
const runtimeManifestPath = path.join(projectDir, packageDir ?? path.join("offline", "IAAS", "package", environment), "runtime-manifest.json");
|
|
250
250
|
|
|
251
|
-
if (requestedFeatureSet.has("webiny") && !
|
|
251
|
+
if (requestedFeatureSet.has("webiny") && !runtimeConfig.integrations.webiny.enabled) {
|
|
252
252
|
throw new S3teError("ADAPTER_ERROR", "Feature webiny was requested but is not enabled in s3te.config.json.");
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { resolveEnvironmentWebinyIntegration } from "../../core/src/index.mjs";
|
|
2
|
+
|
|
3
|
+
export function getConfiguredFeatures(config, environment) {
|
|
2
4
|
const features = [];
|
|
3
5
|
|
|
4
|
-
if (
|
|
6
|
+
if (environment) {
|
|
7
|
+
if (resolveEnvironmentWebinyIntegration(config, environment).enabled) {
|
|
8
|
+
features.push("webiny");
|
|
9
|
+
}
|
|
10
|
+
return features;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const hasAnyEnvironmentWebiny = Object.keys(config.environments ?? {}).some((environmentName) => (
|
|
14
|
+
resolveEnvironmentWebinyIntegration(config, environmentName).enabled
|
|
15
|
+
));
|
|
16
|
+
|
|
17
|
+
if (hasAnyEnvironmentWebiny) {
|
|
5
18
|
features.push("webiny");
|
|
6
19
|
}
|
|
7
20
|
|
|
8
21
|
return features;
|
|
9
22
|
}
|
|
10
23
|
|
|
11
|
-
export function resolveRequestedFeatures(config, requestedFeatures = []) {
|
|
24
|
+
export function resolveRequestedFeatures(config, requestedFeatures = [], environment) {
|
|
12
25
|
return [...new Set([
|
|
13
26
|
...requestedFeatures,
|
|
14
|
-
...getConfiguredFeatures(config)
|
|
27
|
+
...getConfiguredFeatures(config, environment)
|
|
15
28
|
])].sort();
|
|
16
29
|
}
|
|
@@ -221,11 +221,13 @@ export async function packageAwsProject({
|
|
|
221
221
|
clean = false,
|
|
222
222
|
features = []
|
|
223
223
|
}) {
|
|
224
|
-
|
|
224
|
+
const runtimeConfig = buildEnvironmentRuntimeConfig(config, environment);
|
|
225
|
+
|
|
226
|
+
if (features.includes("webiny") && !runtimeConfig.integrations.webiny.enabled) {
|
|
225
227
|
throw new S3teError("ADAPTER_ERROR", "Feature webiny was requested but is not enabled in s3te.config.json.");
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
const resolvedFeatures = resolveRequestedFeatures(config, features);
|
|
230
|
+
const resolvedFeatures = resolveRequestedFeatures(config, features, environment);
|
|
229
231
|
const packageDir = outDir
|
|
230
232
|
? path.join(projectDir, outDir)
|
|
231
233
|
: path.join(projectDir, "offline", "IAAS", "package", environment);
|
|
@@ -234,7 +236,6 @@ export async function packageAwsProject({
|
|
|
234
236
|
}
|
|
235
237
|
await ensureDirectory(packageDir);
|
|
236
238
|
|
|
237
|
-
const runtimeConfig = buildEnvironmentRuntimeConfig(config, environment);
|
|
238
239
|
const lambdaDir = path.join(packageDir, "lambda");
|
|
239
240
|
const templatePath = path.join(packageDir, "cloudformation.template.json");
|
|
240
241
|
const packagingManifestPath = path.join(packageDir, "manifest.json");
|
|
@@ -349,6 +349,7 @@ async function main() {
|
|
|
349
349
|
const rawConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
350
350
|
const migration = await migrateProject(configPath, rawConfig, {
|
|
351
351
|
writeChanges: Boolean(options.write) && !Boolean(options["dry-run"]),
|
|
352
|
+
environment: asArray(options.env)[0],
|
|
352
353
|
enableWebiny: Boolean(options["enable-webiny"]),
|
|
353
354
|
disableWebiny: Boolean(options["disable-webiny"]),
|
|
354
355
|
webinySourceTable: options["webiny-source-table"],
|
|
@@ -31,6 +31,26 @@ function normalizePath(value) {
|
|
|
31
31
|
return String(value).replace(/\\/g, "/");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function normalizeBaseUrl(value) {
|
|
35
|
+
const trimmed = String(value).trim();
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(trimmed).host;
|
|
43
|
+
} catch {
|
|
44
|
+
// fall back to lightweight normalization below
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return trimmed
|
|
49
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
|
|
50
|
+
.replace(/^\/+|\/+$/g, "")
|
|
51
|
+
.split("/")[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
function schemaTemplate() {
|
|
35
55
|
return {
|
|
36
56
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -67,23 +87,53 @@ function schemaTemplate() {
|
|
|
67
87
|
}
|
|
68
88
|
}
|
|
69
89
|
},
|
|
90
|
+
rendering: {
|
|
91
|
+
type: "object",
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
properties: {
|
|
94
|
+
minifyHtml: { type: "boolean" },
|
|
95
|
+
renderExtensions: {
|
|
96
|
+
type: "array",
|
|
97
|
+
items: { type: "string" }
|
|
98
|
+
},
|
|
99
|
+
outputDir: { type: "string" },
|
|
100
|
+
maxRenderDepth: {
|
|
101
|
+
type: "integer",
|
|
102
|
+
minimum: 1
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
70
106
|
variants: {
|
|
71
107
|
type: "object",
|
|
72
108
|
additionalProperties: {
|
|
73
109
|
type: "object",
|
|
74
|
-
additionalProperties:
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
required: ["defaultLanguage", "languages"],
|
|
75
112
|
properties: {
|
|
113
|
+
sourceDir: { type: "string" },
|
|
114
|
+
partDir: { type: "string" },
|
|
115
|
+
defaultLanguage: { type: "string" },
|
|
116
|
+
routing: {
|
|
117
|
+
type: "object",
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
properties: {
|
|
120
|
+
indexDocument: { type: "string" },
|
|
121
|
+
notFoundDocument: { type: "string" }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
76
124
|
languages: {
|
|
77
125
|
type: "object",
|
|
78
126
|
additionalProperties: {
|
|
79
127
|
type: "object",
|
|
80
128
|
additionalProperties: false,
|
|
129
|
+
required: ["baseUrl", "cloudFrontAliases"],
|
|
81
130
|
properties: {
|
|
82
131
|
baseUrl: { type: "string" },
|
|
83
132
|
targetBucket: { type: "string" },
|
|
84
133
|
cloudFrontAliases: {
|
|
85
134
|
type: "array",
|
|
86
|
-
items: { type: "string" }
|
|
135
|
+
items: { type: "string" },
|
|
136
|
+
minItems: 1
|
|
87
137
|
},
|
|
88
138
|
webinyLocale: { type: "string" }
|
|
89
139
|
}
|
|
@@ -92,6 +142,56 @@ function schemaTemplate() {
|
|
|
92
142
|
}
|
|
93
143
|
}
|
|
94
144
|
},
|
|
145
|
+
aws: {
|
|
146
|
+
type: "object",
|
|
147
|
+
additionalProperties: false,
|
|
148
|
+
properties: {
|
|
149
|
+
codeBuckets: {
|
|
150
|
+
type: "object",
|
|
151
|
+
additionalProperties: { type: "string" }
|
|
152
|
+
},
|
|
153
|
+
dependencyStore: {
|
|
154
|
+
type: "object",
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
properties: {
|
|
157
|
+
tableName: { type: "string" }
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
contentStore: {
|
|
161
|
+
type: "object",
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
properties: {
|
|
164
|
+
tableName: { type: "string" },
|
|
165
|
+
contentIdIndexName: { type: "string" }
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
invalidationStore: {
|
|
169
|
+
type: "object",
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
properties: {
|
|
172
|
+
tableName: { type: "string" },
|
|
173
|
+
debounceSeconds: {
|
|
174
|
+
type: "integer",
|
|
175
|
+
minimum: 0
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
lambda: {
|
|
180
|
+
type: "object",
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
properties: {
|
|
183
|
+
runtime: {
|
|
184
|
+
type: "string",
|
|
185
|
+
enum: ["nodejs22.x"]
|
|
186
|
+
},
|
|
187
|
+
architecture: {
|
|
188
|
+
type: "string",
|
|
189
|
+
enum: ["arm64", "x86_64"]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
95
195
|
integrations: {
|
|
96
196
|
type: "object",
|
|
97
197
|
additionalProperties: false,
|
|
@@ -107,6 +207,23 @@ function schemaTemplate() {
|
|
|
107
207
|
relevantModels: {
|
|
108
208
|
type: "array",
|
|
109
209
|
items: { type: "string" }
|
|
210
|
+
},
|
|
211
|
+
environments: {
|
|
212
|
+
type: "object",
|
|
213
|
+
additionalProperties: {
|
|
214
|
+
type: "object",
|
|
215
|
+
additionalProperties: false,
|
|
216
|
+
properties: {
|
|
217
|
+
enabled: { type: "boolean" },
|
|
218
|
+
sourceTableName: { type: "string" },
|
|
219
|
+
mirrorTableName: { type: "string" },
|
|
220
|
+
tenant: { type: "string" },
|
|
221
|
+
relevantModels: {
|
|
222
|
+
type: "array",
|
|
223
|
+
items: { type: "string" }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
110
227
|
}
|
|
111
228
|
}
|
|
112
229
|
}
|
|
@@ -129,14 +246,34 @@ function isPlainObject(value) {
|
|
|
129
246
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
130
247
|
}
|
|
131
248
|
|
|
132
|
-
async function writeProjectFile(targetPath, body, force = false) {
|
|
133
|
-
if (!force && await fileExists(targetPath)) {
|
|
134
|
-
|
|
249
|
+
async function writeProjectFile(targetPath, body, force = false, overwriteExisting = false) {
|
|
250
|
+
if (!force && !overwriteExisting && await fileExists(targetPath)) {
|
|
251
|
+
return;
|
|
135
252
|
}
|
|
136
253
|
await writeTextFile(targetPath, body);
|
|
137
254
|
}
|
|
138
255
|
|
|
139
|
-
function
|
|
256
|
+
function mergeDefaults(existingValue, defaultValue) {
|
|
257
|
+
if (existingValue === undefined) {
|
|
258
|
+
return defaultValue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (isPlainObject(defaultValue)) {
|
|
262
|
+
if (!isPlainObject(existingValue)) {
|
|
263
|
+
throw new Error("Existing JSON content must use an object where S3TE expects one.");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const mergedValue = { ...existingValue };
|
|
267
|
+
for (const [key, value] of Object.entries(defaultValue)) {
|
|
268
|
+
mergedValue[key] = mergeDefaults(existingValue[key], value);
|
|
269
|
+
}
|
|
270
|
+
return mergedValue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return existingValue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function mergeProjectPackageJson(existingPackageJson, projectPackageJson, scaffoldOptions = {}) {
|
|
140
277
|
if (!isPlainObject(existingPackageJson)) {
|
|
141
278
|
throw new Error("Existing package.json must contain a JSON object.");
|
|
142
279
|
}
|
|
@@ -167,10 +304,14 @@ function mergeProjectPackageJson(existingPackageJson, projectPackageJson) {
|
|
|
167
304
|
mergedPackageJson.scripts = mergedScripts;
|
|
168
305
|
}
|
|
169
306
|
|
|
307
|
+
if (scaffoldOptions.projectNameProvided) {
|
|
308
|
+
mergedPackageJson.name = scaffoldOptions.projectName;
|
|
309
|
+
}
|
|
310
|
+
|
|
170
311
|
return mergedPackageJson;
|
|
171
312
|
}
|
|
172
313
|
|
|
173
|
-
async function writeProjectPackageJson(targetPath, projectPackageJson, force = false) {
|
|
314
|
+
async function writeProjectPackageJson(targetPath, projectPackageJson, scaffoldOptions = {}, force = false) {
|
|
174
315
|
if (force || !await fileExists(targetPath)) {
|
|
175
316
|
await writeTextFile(targetPath, JSON.stringify(projectPackageJson, null, 2) + "\n");
|
|
176
317
|
return;
|
|
@@ -183,10 +324,63 @@ async function writeProjectPackageJson(targetPath, projectPackageJson, force = f
|
|
|
183
324
|
throw new Error(`Existing package.json is not valid JSON: ${targetPath}`, { cause: error });
|
|
184
325
|
}
|
|
185
326
|
|
|
186
|
-
const mergedPackageJson = mergeProjectPackageJson(existingPackageJson, projectPackageJson);
|
|
327
|
+
const mergedPackageJson = mergeProjectPackageJson(existingPackageJson, projectPackageJson, scaffoldOptions);
|
|
187
328
|
await writeTextFile(targetPath, JSON.stringify(mergedPackageJson, null, 2) + "\n");
|
|
188
329
|
}
|
|
189
330
|
|
|
331
|
+
function applyScaffoldConfigOverrides(config, scaffoldOptions = {}) {
|
|
332
|
+
if (scaffoldOptions.projectNameProvided) {
|
|
333
|
+
config.project.name = scaffoldOptions.projectName;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const variantConfig = config.variants?.[scaffoldOptions.variant];
|
|
337
|
+
const languageConfig = variantConfig?.languages?.[scaffoldOptions.language];
|
|
338
|
+
if (!variantConfig || !languageConfig) {
|
|
339
|
+
return config;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (scaffoldOptions.languageProvided) {
|
|
343
|
+
variantConfig.defaultLanguage = scaffoldOptions.language;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (languageConfig.webinyLocale === undefined) {
|
|
347
|
+
languageConfig.webinyLocale = scaffoldOptions.language;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (scaffoldOptions.baseUrlProvided) {
|
|
351
|
+
const previousBaseUrl = languageConfig.baseUrl;
|
|
352
|
+
languageConfig.baseUrl = scaffoldOptions.baseUrl;
|
|
353
|
+
if (!Array.isArray(languageConfig.cloudFrontAliases)
|
|
354
|
+
|| languageConfig.cloudFrontAliases.length === 0
|
|
355
|
+
|| (languageConfig.cloudFrontAliases.length === 1 && languageConfig.cloudFrontAliases[0] === previousBaseUrl)) {
|
|
356
|
+
languageConfig.cloudFrontAliases = [scaffoldOptions.baseUrl];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return config;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function writeProjectConfigJson(targetPath, projectConfig, scaffoldOptions = {}, force = false) {
|
|
364
|
+
if (force || !await fileExists(targetPath)) {
|
|
365
|
+
await writeTextFile(targetPath, JSON.stringify(projectConfig, null, 2) + "\n");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let existingConfig;
|
|
370
|
+
try {
|
|
371
|
+
existingConfig = JSON.parse(await fs.readFile(targetPath, "utf8"));
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw new Error(`Existing s3te.config.json is not valid JSON: ${targetPath}`, { cause: error });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!isPlainObject(existingConfig)) {
|
|
377
|
+
throw new Error("Existing s3te.config.json must contain a JSON object.");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const mergedConfig = applyScaffoldConfigOverrides(mergeDefaults(existingConfig, projectConfig), scaffoldOptions);
|
|
381
|
+
await writeTextFile(targetPath, JSON.stringify(mergedConfig, null, 2) + "\n");
|
|
382
|
+
}
|
|
383
|
+
|
|
190
384
|
async function loadRenderState(projectDir, environment) {
|
|
191
385
|
const statePath = path.join(projectDir, "offline", "S3TELocal", "render-state", `${environment}.json`);
|
|
192
386
|
try {
|
|
@@ -268,10 +462,20 @@ export async function validateProject(projectDir, config, options = {}) {
|
|
|
268
462
|
|
|
269
463
|
export async function scaffoldProject(projectDir, options = {}) {
|
|
270
464
|
const projectName = options.projectName ?? path.basename(projectDir).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
271
|
-
const baseUrl = options.baseUrl ?? "example.com";
|
|
465
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? "example.com");
|
|
272
466
|
const variant = options.variant ?? "website";
|
|
273
467
|
const language = options.language ?? "en";
|
|
274
468
|
const force = Boolean(options.force);
|
|
469
|
+
const scaffoldOptions = {
|
|
470
|
+
projectName,
|
|
471
|
+
projectNameProvided: options.projectName !== undefined,
|
|
472
|
+
baseUrl,
|
|
473
|
+
baseUrlProvided: options.baseUrl !== undefined,
|
|
474
|
+
variant,
|
|
475
|
+
variantProvided: options.variant !== undefined,
|
|
476
|
+
language,
|
|
477
|
+
languageProvided: options.language !== undefined
|
|
478
|
+
};
|
|
275
479
|
|
|
276
480
|
await ensureDirectory(path.join(projectDir, "app", "part"));
|
|
277
481
|
await ensureDirectory(path.join(projectDir, "app", variant));
|
|
@@ -323,9 +527,9 @@ export async function scaffoldProject(projectDir, options = {}) {
|
|
|
323
527
|
}
|
|
324
528
|
};
|
|
325
529
|
|
|
326
|
-
await writeProjectPackageJson(path.join(projectDir, "package.json"), projectPackageJson, force);
|
|
327
|
-
await
|
|
328
|
-
await writeProjectFile(path.join(projectDir, "offline", "schemas", "s3te.config.schema.json"), JSON.stringify(schemaTemplate(), null, 2) + "\n", force);
|
|
530
|
+
await writeProjectPackageJson(path.join(projectDir, "package.json"), projectPackageJson, scaffoldOptions, force);
|
|
531
|
+
await writeProjectConfigJson(path.join(projectDir, "s3te.config.json"), config, scaffoldOptions, force);
|
|
532
|
+
await writeProjectFile(path.join(projectDir, "offline", "schemas", "s3te.config.schema.json"), JSON.stringify(schemaTemplate(), null, 2) + "\n", force, true);
|
|
329
533
|
await writeProjectFile(path.join(projectDir, "app", "part", "head.part"), "<meta charset='utf-8'>\n<title>My S3TE Site</title>\n", force);
|
|
330
534
|
await writeProjectFile(path.join(projectDir, "app", variant, "index.html"), "<!doctype html>\n<html lang=\"<lang>2</lang>\">\n <head>\n <part>head.part</part>\n </head>\n <body>\n <h1>Hello from S3TemplateEngine</h1>\n </body>\n</html>\n", force);
|
|
331
535
|
await writeProjectFile(path.join(projectDir, "offline", "content", `${language}.json`), "[]\n", force);
|
|
@@ -529,6 +733,7 @@ export async function migrateProject(configPath, rawConfig, writeChanges) {
|
|
|
529
733
|
|
|
530
734
|
const enableWebiny = Boolean(options.enableWebiny);
|
|
531
735
|
const disableWebiny = Boolean(options.disableWebiny);
|
|
736
|
+
const targetEnvironment = options.environment ? String(options.environment).trim() : "";
|
|
532
737
|
const webinySourceTable = options.webinySourceTable ? String(options.webinySourceTable).trim() : "";
|
|
533
738
|
const webinyTenant = options.webinyTenant ? String(options.webinyTenant).trim() : "";
|
|
534
739
|
const webinyModels = normalizeStringList(options.webinyModels);
|
|
@@ -537,45 +742,84 @@ export async function migrateProject(configPath, rawConfig, writeChanges) {
|
|
|
537
742
|
throw new S3teError("CONFIG_CONFLICT_ERROR", "migrate does not allow --enable-webiny and --disable-webiny at the same time.");
|
|
538
743
|
}
|
|
539
744
|
|
|
540
|
-
const touchesWebiny = enableWebiny || disableWebiny || Boolean(webinySourceTable) || webinyModels.length > 0;
|
|
745
|
+
const touchesWebiny = enableWebiny || disableWebiny || Boolean(webinySourceTable) || Boolean(webinyTenant) || webinyModels.length > 0;
|
|
541
746
|
if (touchesWebiny) {
|
|
747
|
+
if (targetEnvironment && !nextConfig.environments?.[targetEnvironment]) {
|
|
748
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", `Unknown environment for migrate: ${targetEnvironment}.`);
|
|
749
|
+
}
|
|
750
|
+
|
|
542
751
|
const existingIntegrations = nextConfig.integrations ?? {};
|
|
543
752
|
const existingWebiny = existingIntegrations.webiny ?? {};
|
|
544
|
-
const
|
|
753
|
+
const existingEnvironmentOverrides = existingWebiny.environments ?? {};
|
|
754
|
+
const existingTargetWebiny = targetEnvironment
|
|
755
|
+
? (existingEnvironmentOverrides[targetEnvironment] ?? {})
|
|
756
|
+
: existingWebiny;
|
|
757
|
+
const inheritedModels = normalizeStringList(
|
|
758
|
+
existingTargetWebiny.relevantModels
|
|
759
|
+
?? (targetEnvironment ? existingWebiny.relevantModels : undefined)
|
|
760
|
+
?? ["staticContent", "staticCodeContent"]
|
|
761
|
+
);
|
|
545
762
|
const shouldEnableWebiny = disableWebiny
|
|
546
763
|
? false
|
|
547
764
|
: (enableWebiny || Boolean(webinySourceTable) || webinyModels.length > 0
|
|
548
765
|
? true
|
|
549
|
-
: Boolean(
|
|
550
|
-
|
|
766
|
+
: Boolean(targetEnvironment
|
|
767
|
+
? (existingTargetWebiny.enabled ?? existingWebiny.enabled)
|
|
768
|
+
: existingWebiny.enabled));
|
|
769
|
+
const nextSourceTableName = webinySourceTable
|
|
770
|
+
|| existingTargetWebiny.sourceTableName
|
|
771
|
+
|| (targetEnvironment ? existingWebiny.sourceTableName : "")
|
|
772
|
+
|| "";
|
|
551
773
|
|
|
552
774
|
if (shouldEnableWebiny && !nextSourceTableName) {
|
|
553
|
-
throw new S3teError(
|
|
775
|
+
throw new S3teError(
|
|
776
|
+
"CONFIG_CONFLICT_ERROR",
|
|
777
|
+
targetEnvironment
|
|
778
|
+
? `Enabling Webiny for environment ${targetEnvironment} requires --webiny-source-table <table> or an existing sourceTableName.`
|
|
779
|
+
: "Enabling Webiny requires --webiny-source-table <table> or an existing integrations.webiny.sourceTableName."
|
|
780
|
+
);
|
|
554
781
|
}
|
|
555
782
|
|
|
783
|
+
const nextWebinyConfig = {
|
|
784
|
+
enabled: shouldEnableWebiny,
|
|
785
|
+
sourceTableName: nextSourceTableName || undefined,
|
|
786
|
+
mirrorTableName: existingTargetWebiny.mirrorTableName
|
|
787
|
+
?? (targetEnvironment ? existingWebiny.mirrorTableName : undefined)
|
|
788
|
+
?? "{stackPrefix}_s3te_content_{project}",
|
|
789
|
+
tenant: webinyTenant || existingTargetWebiny.tenant || (targetEnvironment ? existingWebiny.tenant : undefined) || undefined,
|
|
790
|
+
relevantModels: normalizeStringList([
|
|
791
|
+
...(inheritedModels.length > 0 ? inheritedModels : ["staticContent", "staticCodeContent"]),
|
|
792
|
+
...webinyModels
|
|
793
|
+
])
|
|
794
|
+
};
|
|
795
|
+
|
|
556
796
|
nextConfig.integrations = {
|
|
557
797
|
...existingIntegrations,
|
|
558
|
-
webiny:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
798
|
+
webiny: targetEnvironment
|
|
799
|
+
? {
|
|
800
|
+
...existingWebiny,
|
|
801
|
+
environments: {
|
|
802
|
+
...existingEnvironmentOverrides,
|
|
803
|
+
[targetEnvironment]: nextWebinyConfig
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
: {
|
|
807
|
+
...existingWebiny,
|
|
808
|
+
...nextWebinyConfig,
|
|
809
|
+
environments: existingEnvironmentOverrides
|
|
810
|
+
}
|
|
568
811
|
};
|
|
569
812
|
|
|
570
|
-
|
|
813
|
+
const scopeLabel = targetEnvironment ? ` for environment ${targetEnvironment}` : "";
|
|
814
|
+
changes.push(shouldEnableWebiny ? `Enabled Webiny integration${scopeLabel}.` : `Disabled Webiny integration${scopeLabel}.`);
|
|
571
815
|
if (webinySourceTable) {
|
|
572
|
-
changes.push(`Set Webiny source table to ${webinySourceTable}.`);
|
|
816
|
+
changes.push(`Set Webiny source table${scopeLabel} to ${webinySourceTable}.`);
|
|
573
817
|
}
|
|
574
818
|
if (webinyTenant) {
|
|
575
|
-
changes.push(`Set Webiny tenant to ${webinyTenant}.`);
|
|
819
|
+
changes.push(`Set Webiny tenant${scopeLabel} to ${webinyTenant}.`);
|
|
576
820
|
}
|
|
577
821
|
if (webinyModels.length > 0) {
|
|
578
|
-
changes.push(`Added Webiny models: ${webinyModels.join(", ")}.`);
|
|
822
|
+
changes.push(`Added Webiny models${scopeLabel}: ${webinyModels.join(", ")}.`);
|
|
579
823
|
}
|
|
580
824
|
}
|
|
581
825
|
|
|
@@ -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
|
}
|