@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,535 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
S3teError,
|
|
7
|
+
createManualRenderTargets,
|
|
8
|
+
isRenderableKey,
|
|
9
|
+
loadProjectConfig,
|
|
10
|
+
renderSourceTemplate,
|
|
11
|
+
validateAndResolveProjectConfig
|
|
12
|
+
} from "../../core/src/index.mjs";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
deployAwsProject,
|
|
16
|
+
ensureAwsCliAvailable,
|
|
17
|
+
ensureAwsCredentials,
|
|
18
|
+
packageAwsProject
|
|
19
|
+
} from "../../aws-adapter/src/index.mjs";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
FileSystemTemplateRepository,
|
|
23
|
+
copyFile,
|
|
24
|
+
ensureDirectory,
|
|
25
|
+
loadLocalContent,
|
|
26
|
+
removeDirectory,
|
|
27
|
+
writeTextFile
|
|
28
|
+
} from "./fs-adapters.mjs";
|
|
29
|
+
|
|
30
|
+
function normalizePath(value) {
|
|
31
|
+
return String(value).replace(/\\/g, "/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function schemaTemplate() {
|
|
35
|
+
return {
|
|
36
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
37
|
+
title: "S3TemplateEngine Project Config",
|
|
38
|
+
type: "object",
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
required: ["project", "environments", "variants"],
|
|
41
|
+
properties: {
|
|
42
|
+
$schema: { type: "string" },
|
|
43
|
+
configVersion: { type: "integer", minimum: 1 },
|
|
44
|
+
project: {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
required: ["name"],
|
|
48
|
+
properties: {
|
|
49
|
+
name: {
|
|
50
|
+
type: "string",
|
|
51
|
+
pattern: "^[a-z0-9-]+$"
|
|
52
|
+
},
|
|
53
|
+
displayName: { type: "string" }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
environments: {
|
|
57
|
+
type: "object",
|
|
58
|
+
additionalProperties: {
|
|
59
|
+
type: "object",
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
required: ["awsRegion", "certificateArn"],
|
|
62
|
+
properties: {
|
|
63
|
+
awsRegion: { type: "string" },
|
|
64
|
+
stackPrefix: { type: "string" },
|
|
65
|
+
certificateArn: { type: "string" },
|
|
66
|
+
route53HostedZoneId: { type: "string" }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
variants: {
|
|
71
|
+
type: "object",
|
|
72
|
+
additionalProperties: {
|
|
73
|
+
type: "object",
|
|
74
|
+
additionalProperties: true,
|
|
75
|
+
properties: {
|
|
76
|
+
languages: {
|
|
77
|
+
type: "object",
|
|
78
|
+
additionalProperties: {
|
|
79
|
+
type: "object",
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: {
|
|
82
|
+
baseUrl: { type: "string" },
|
|
83
|
+
targetBucket: { type: "string" },
|
|
84
|
+
cloudFrontAliases: {
|
|
85
|
+
type: "array",
|
|
86
|
+
items: { type: "string" }
|
|
87
|
+
},
|
|
88
|
+
webinyLocale: { type: "string" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
integrations: {
|
|
96
|
+
type: "object",
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
properties: {
|
|
99
|
+
webiny: {
|
|
100
|
+
type: "object",
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
properties: {
|
|
103
|
+
enabled: { type: "boolean" },
|
|
104
|
+
sourceTableName: { type: "string" },
|
|
105
|
+
mirrorTableName: { type: "string" },
|
|
106
|
+
tenant: { type: "string" },
|
|
107
|
+
relevantModels: {
|
|
108
|
+
type: "array",
|
|
109
|
+
items: { type: "string" }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function fileExists(targetPath) {
|
|
120
|
+
try {
|
|
121
|
+
await fs.stat(targetPath);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function writeProjectFile(targetPath, body, force = false) {
|
|
129
|
+
if (!force && await fileExists(targetPath)) {
|
|
130
|
+
throw new Error(`Refusing to overwrite existing file: ${targetPath}`);
|
|
131
|
+
}
|
|
132
|
+
await writeTextFile(targetPath, body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function loadRenderState(projectDir, environment) {
|
|
136
|
+
const statePath = path.join(projectDir, "offline", "S3TELocal", "render-state", `${environment}.json`);
|
|
137
|
+
try {
|
|
138
|
+
const raw = await fs.readFile(statePath, "utf8");
|
|
139
|
+
return { statePath, state: JSON.parse(raw) };
|
|
140
|
+
} catch {
|
|
141
|
+
return { statePath, state: { templates: {} } };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function saveRenderState(statePath, state) {
|
|
146
|
+
await writeTextFile(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderStateKey(target) {
|
|
150
|
+
return `${target.variant}#${target.language}#${target.sourceKey}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeStringList(values) {
|
|
154
|
+
return [...new Set((values ?? [])
|
|
155
|
+
.map((value) => String(value).trim())
|
|
156
|
+
.filter(Boolean))];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function loadResolvedConfig(projectDir, configPath) {
|
|
160
|
+
const rawConfig = await loadProjectConfig(configPath);
|
|
161
|
+
const result = await validateAndResolveProjectConfig(rawConfig, { projectDir });
|
|
162
|
+
return { rawConfig, ...result };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function validateProject(projectDir, config, options = {}) {
|
|
166
|
+
const templateRepository = new FileSystemTemplateRepository(projectDir, config);
|
|
167
|
+
const contentRepository = await loadLocalContent(projectDir, config);
|
|
168
|
+
const warnings = [];
|
|
169
|
+
const errors = [];
|
|
170
|
+
const checkedTemplates = [];
|
|
171
|
+
const environments = options.environment ? [options.environment] : Object.keys(config.environments);
|
|
172
|
+
|
|
173
|
+
for (const environment of environments) {
|
|
174
|
+
for (const [variantName, variantConfig] of Object.entries(config.variants)) {
|
|
175
|
+
const entries = await templateRepository.listVariantEntries(variantName);
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
if (!isRenderableKey(config, entry.key)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const languageCode of Object.keys(variantConfig.languages)) {
|
|
182
|
+
try {
|
|
183
|
+
const results = await renderSourceTemplate({
|
|
184
|
+
config,
|
|
185
|
+
templateRepository,
|
|
186
|
+
contentRepository,
|
|
187
|
+
environment,
|
|
188
|
+
variantName,
|
|
189
|
+
languageCode,
|
|
190
|
+
sourceKey: entry.key
|
|
191
|
+
});
|
|
192
|
+
warnings.push(...results.flatMap((result) => result.warnings));
|
|
193
|
+
checkedTemplates.push(`${environment}:${variantName}:${languageCode}:${entry.key}`);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
errors.push({
|
|
196
|
+
code: error.code ?? "TEMPLATE_SYNTAX_ERROR",
|
|
197
|
+
message: error.message,
|
|
198
|
+
details: error.details
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
ok: errors.length === 0,
|
|
208
|
+
errors,
|
|
209
|
+
warnings,
|
|
210
|
+
checkedTemplates
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function scaffoldProject(projectDir, options = {}) {
|
|
215
|
+
const projectName = options.projectName ?? path.basename(projectDir).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
216
|
+
const baseUrl = options.baseUrl ?? "example.com";
|
|
217
|
+
const variant = options.variant ?? "website";
|
|
218
|
+
const language = options.language ?? "en";
|
|
219
|
+
const force = Boolean(options.force);
|
|
220
|
+
|
|
221
|
+
await ensureDirectory(path.join(projectDir, "app", "part"));
|
|
222
|
+
await ensureDirectory(path.join(projectDir, "app", variant));
|
|
223
|
+
await ensureDirectory(path.join(projectDir, "offline", "tests"));
|
|
224
|
+
await ensureDirectory(path.join(projectDir, "offline", "content"));
|
|
225
|
+
await ensureDirectory(path.join(projectDir, "offline", "schemas"));
|
|
226
|
+
await ensureDirectory(path.join(projectDir, ".vscode"));
|
|
227
|
+
|
|
228
|
+
const projectPackageJson = {
|
|
229
|
+
name: projectName,
|
|
230
|
+
private: true,
|
|
231
|
+
type: "module",
|
|
232
|
+
scripts: {
|
|
233
|
+
validate: "s3te validate",
|
|
234
|
+
render: "s3te render --env dev",
|
|
235
|
+
test: "s3te test"
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const config = {
|
|
240
|
+
$schema: "./offline/schemas/s3te.config.schema.json",
|
|
241
|
+
configVersion: 1,
|
|
242
|
+
project: {
|
|
243
|
+
name: projectName
|
|
244
|
+
},
|
|
245
|
+
rendering: {
|
|
246
|
+
outputDir: "offline/S3TELocal/preview"
|
|
247
|
+
},
|
|
248
|
+
environments: {
|
|
249
|
+
dev: {
|
|
250
|
+
awsRegion: "eu-central-1",
|
|
251
|
+
stackPrefix: "DEV",
|
|
252
|
+
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/replace-me"
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
variants: {
|
|
256
|
+
[variant]: {
|
|
257
|
+
sourceDir: `app/${variant}`,
|
|
258
|
+
partDir: "app/part",
|
|
259
|
+
defaultLanguage: language,
|
|
260
|
+
languages: {
|
|
261
|
+
[language]: {
|
|
262
|
+
baseUrl,
|
|
263
|
+
cloudFrontAliases: [baseUrl],
|
|
264
|
+
webinyLocale: language
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
await writeProjectFile(path.join(projectDir, "package.json"), JSON.stringify(projectPackageJson, null, 2) + "\n", force);
|
|
272
|
+
await writeProjectFile(path.join(projectDir, "s3te.config.json"), JSON.stringify(config, null, 2) + "\n", force);
|
|
273
|
+
await writeProjectFile(path.join(projectDir, "offline", "schemas", "s3te.config.schema.json"), JSON.stringify(schemaTemplate(), null, 2) + "\n", force);
|
|
274
|
+
await writeProjectFile(path.join(projectDir, "app", "part", "head.part"), "<meta charset='utf-8'>\n<title>My S3TE Site</title>\n", force);
|
|
275
|
+
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);
|
|
276
|
+
await writeProjectFile(path.join(projectDir, "offline", "content", `${language}.json`), "[]\n", force);
|
|
277
|
+
await writeProjectFile(path.join(projectDir, ".vscode", "extensions.json"), JSON.stringify({
|
|
278
|
+
recommendations: [
|
|
279
|
+
"redhat.vscode-yaml",
|
|
280
|
+
"amazonwebservices.aws-toolkit-vscode"
|
|
281
|
+
]
|
|
282
|
+
}, null, 2) + "\n", force);
|
|
283
|
+
await writeProjectFile(path.join(projectDir, "offline", "tests", "project.test.mjs"), "import test from 'node:test';\nimport assert from 'node:assert/strict';\n\ntest('placeholder project test', () => {\n assert.equal(1, 1);\n});\n", force);
|
|
284
|
+
|
|
285
|
+
return { projectName, variant, language };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function renderProject(projectDir, config, options = {}) {
|
|
289
|
+
const templateRepository = new FileSystemTemplateRepository(projectDir, config);
|
|
290
|
+
const contentRepository = await loadLocalContent(projectDir, config);
|
|
291
|
+
const outputRoot = path.join(projectDir, options.outputDir ?? config.rendering.outputDir);
|
|
292
|
+
const templateEntries = [];
|
|
293
|
+
|
|
294
|
+
for (const variantName of Object.keys(config.variants)) {
|
|
295
|
+
templateEntries.push(...await templateRepository.listVariantEntries(variantName));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const targets = createManualRenderTargets({
|
|
299
|
+
config,
|
|
300
|
+
templateEntries,
|
|
301
|
+
environment: options.environment,
|
|
302
|
+
variant: options.variant,
|
|
303
|
+
language: options.language,
|
|
304
|
+
entry: options.entry
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const { statePath, state } = await loadRenderState(projectDir, options.environment);
|
|
308
|
+
if (!options.entry) {
|
|
309
|
+
await removeDirectory(path.join(outputRoot, options.environment));
|
|
310
|
+
state.templates = {};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const renderedArtifacts = [];
|
|
314
|
+
const deletedArtifacts = [];
|
|
315
|
+
const warnings = [];
|
|
316
|
+
|
|
317
|
+
for (const target of targets) {
|
|
318
|
+
const results = await renderSourceTemplate({
|
|
319
|
+
config,
|
|
320
|
+
templateRepository,
|
|
321
|
+
contentRepository,
|
|
322
|
+
environment: target.environment,
|
|
323
|
+
variantName: target.variant,
|
|
324
|
+
languageCode: target.language,
|
|
325
|
+
sourceKey: target.sourceKey
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const renderedOutputKeys = new Set();
|
|
329
|
+
for (const result of results) {
|
|
330
|
+
warnings.push(...result.warnings);
|
|
331
|
+
const targetPath = path.join(outputRoot, target.environment, target.variant, target.language, result.artifact.outputKey);
|
|
332
|
+
await writeTextFile(targetPath, String(result.artifact.body));
|
|
333
|
+
renderedArtifacts.push(normalizePath(path.relative(projectDir, targetPath)));
|
|
334
|
+
renderedOutputKeys.add(result.artifact.outputKey);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const stateKey = renderStateKey(target);
|
|
338
|
+
const previousOutputs = state.templates[stateKey] ?? [];
|
|
339
|
+
for (const previousOutput of previousOutputs) {
|
|
340
|
+
if (renderedOutputKeys.has(previousOutput)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const previousPath = path.join(outputRoot, target.environment, target.variant, target.language, previousOutput);
|
|
344
|
+
await fs.rm(previousPath, { force: true });
|
|
345
|
+
deletedArtifacts.push(normalizePath(path.relative(projectDir, previousPath)));
|
|
346
|
+
}
|
|
347
|
+
state.templates[stateKey] = [...renderedOutputKeys].sort();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const variantName of options.variant ? [options.variant] : Object.keys(config.variants)) {
|
|
351
|
+
const variantConfig = config.variants[variantName];
|
|
352
|
+
const variantEntries = await templateRepository.listVariantEntries(variantName);
|
|
353
|
+
for (const entry of variantEntries) {
|
|
354
|
+
if (isRenderableKey(config, entry.key)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const suffix = entry.key.slice(variantName.length + 1);
|
|
359
|
+
const sourcePath = path.join(projectDir, variantConfig.sourceDir, suffix);
|
|
360
|
+
for (const languageCode of options.language ? [options.language] : Object.keys(variantConfig.languages)) {
|
|
361
|
+
const targetPath = path.join(outputRoot, options.environment, variantName, languageCode, suffix);
|
|
362
|
+
await copyFile(sourcePath, targetPath);
|
|
363
|
+
renderedArtifacts.push(normalizePath(path.relative(projectDir, targetPath)));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await saveRenderState(statePath, state);
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
outputDir: normalizePath(path.relative(projectDir, outputRoot)),
|
|
372
|
+
renderedArtifacts,
|
|
373
|
+
deletedArtifacts,
|
|
374
|
+
warnings
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function runProjectTests(projectDir) {
|
|
379
|
+
const testsDir = await fileExists(path.join(projectDir, "offline", "tests"))
|
|
380
|
+
? "offline/tests"
|
|
381
|
+
: "tests";
|
|
382
|
+
return new Promise((resolve) => {
|
|
383
|
+
const child = spawn(process.execPath, ["--test", testsDir], {
|
|
384
|
+
cwd: projectDir,
|
|
385
|
+
stdio: "inherit"
|
|
386
|
+
});
|
|
387
|
+
child.on("close", (code) => {
|
|
388
|
+
resolve(code ?? 1);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function packageProject(projectDir, config, options = {}) {
|
|
394
|
+
return packageAwsProject({
|
|
395
|
+
projectDir,
|
|
396
|
+
config,
|
|
397
|
+
environment: options.environment,
|
|
398
|
+
outDir: options.outDir,
|
|
399
|
+
clean: Boolean(options.clean),
|
|
400
|
+
features: options.features ?? []
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export async function deployProject(projectDir, config, options = {}) {
|
|
405
|
+
return deployAwsProject({
|
|
406
|
+
projectDir,
|
|
407
|
+
config,
|
|
408
|
+
environment: options.environment,
|
|
409
|
+
packageDir: options.packageDir,
|
|
410
|
+
features: options.features ?? [],
|
|
411
|
+
profile: options.profile,
|
|
412
|
+
plan: Boolean(options.plan),
|
|
413
|
+
noSync: Boolean(options.noSync),
|
|
414
|
+
stdio: options.stdio ?? "pipe"
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function doctorProject(projectDir, configPath, options = {}) {
|
|
419
|
+
const checks = [];
|
|
420
|
+
const majorVersion = Number(process.versions.node.split(".")[0]);
|
|
421
|
+
|
|
422
|
+
checks.push({
|
|
423
|
+
name: "node",
|
|
424
|
+
ok: majorVersion >= 20,
|
|
425
|
+
message: `Node version ${process.versions.node}`
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
await fs.stat(configPath);
|
|
430
|
+
checks.push({ name: "config", ok: true, message: "s3te.config.json found" });
|
|
431
|
+
} catch {
|
|
432
|
+
checks.push({ name: "config", ok: false, message: "s3te.config.json missing" });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await fs.access(projectDir, fs.constants.W_OK);
|
|
437
|
+
checks.push({ name: "write", ok: true, message: "Project is writable" });
|
|
438
|
+
} catch {
|
|
439
|
+
checks.push({ name: "write", ok: false, message: "Project is not writable" });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
await ensureAwsCliAvailable({ cwd: projectDir });
|
|
444
|
+
checks.push({ name: "aws-cli", ok: true, message: "AWS CLI available" });
|
|
445
|
+
} catch (error) {
|
|
446
|
+
checks.push({ name: "aws-cli", ok: false, message: error.message });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (options.environment && options.config) {
|
|
450
|
+
try {
|
|
451
|
+
await ensureAwsCredentials({
|
|
452
|
+
region: options.config.environments[options.environment].awsRegion,
|
|
453
|
+
profile: options.profile,
|
|
454
|
+
cwd: projectDir
|
|
455
|
+
});
|
|
456
|
+
checks.push({ name: "aws-auth", ok: true, message: `AWS credentials valid for ${options.environment}` });
|
|
457
|
+
} catch (error) {
|
|
458
|
+
checks.push({ name: "aws-auth", ok: false, message: error.message });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return checks;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function migrateProject(configPath, rawConfig, writeChanges) {
|
|
466
|
+
const options = typeof writeChanges === "object" && writeChanges !== null
|
|
467
|
+
? writeChanges
|
|
468
|
+
: { writeChanges };
|
|
469
|
+
const nextConfig = {
|
|
470
|
+
...rawConfig,
|
|
471
|
+
configVersion: rawConfig.configVersion ?? 1
|
|
472
|
+
};
|
|
473
|
+
const changes = [];
|
|
474
|
+
|
|
475
|
+
const enableWebiny = Boolean(options.enableWebiny);
|
|
476
|
+
const disableWebiny = Boolean(options.disableWebiny);
|
|
477
|
+
const webinySourceTable = options.webinySourceTable ? String(options.webinySourceTable).trim() : "";
|
|
478
|
+
const webinyTenant = options.webinyTenant ? String(options.webinyTenant).trim() : "";
|
|
479
|
+
const webinyModels = normalizeStringList(options.webinyModels);
|
|
480
|
+
|
|
481
|
+
if (enableWebiny && disableWebiny) {
|
|
482
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", "migrate does not allow --enable-webiny and --disable-webiny at the same time.");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const touchesWebiny = enableWebiny || disableWebiny || Boolean(webinySourceTable) || webinyModels.length > 0;
|
|
486
|
+
if (touchesWebiny) {
|
|
487
|
+
const existingIntegrations = nextConfig.integrations ?? {};
|
|
488
|
+
const existingWebiny = existingIntegrations.webiny ?? {};
|
|
489
|
+
const existingModels = normalizeStringList(existingWebiny.relevantModels ?? ["staticContent", "staticCodeContent"]);
|
|
490
|
+
const shouldEnableWebiny = disableWebiny
|
|
491
|
+
? false
|
|
492
|
+
: (enableWebiny || Boolean(webinySourceTable) || webinyModels.length > 0
|
|
493
|
+
? true
|
|
494
|
+
: Boolean(existingWebiny.enabled));
|
|
495
|
+
const nextSourceTableName = webinySourceTable || existingWebiny.sourceTableName || "";
|
|
496
|
+
|
|
497
|
+
if (shouldEnableWebiny && !nextSourceTableName) {
|
|
498
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", "Enabling Webiny requires --webiny-source-table <table> or an existing integrations.webiny.sourceTableName.");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
nextConfig.integrations = {
|
|
502
|
+
...existingIntegrations,
|
|
503
|
+
webiny: {
|
|
504
|
+
enabled: shouldEnableWebiny,
|
|
505
|
+
sourceTableName: nextSourceTableName || undefined,
|
|
506
|
+
mirrorTableName: existingWebiny.mirrorTableName ?? "{stackPrefix}_s3te_content_{project}",
|
|
507
|
+
tenant: webinyTenant || existingWebiny.tenant || undefined,
|
|
508
|
+
relevantModels: normalizeStringList([
|
|
509
|
+
...(existingModels.length > 0 ? existingModels : ["staticContent", "staticCodeContent"]),
|
|
510
|
+
...webinyModels
|
|
511
|
+
])
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
changes.push(shouldEnableWebiny ? "Enabled Webiny integration." : "Disabled Webiny integration.");
|
|
516
|
+
if (webinySourceTable) {
|
|
517
|
+
changes.push(`Set Webiny source table to ${webinySourceTable}.`);
|
|
518
|
+
}
|
|
519
|
+
if (webinyTenant) {
|
|
520
|
+
changes.push(`Set Webiny tenant to ${webinyTenant}.`);
|
|
521
|
+
}
|
|
522
|
+
if (webinyModels.length > 0) {
|
|
523
|
+
changes.push(`Added Webiny models: ${webinyModels.join(", ")}.`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (options.writeChanges) {
|
|
528
|
+
await writeTextFile(configPath, JSON.stringify(nextConfig, null, 2) + "\n");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
config: nextConfig,
|
|
533
|
+
changes
|
|
534
|
+
};
|
|
535
|
+
}
|