@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.
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ deployProject,
7
+ doctorProject,
8
+ loadResolvedConfig,
9
+ migrateProject,
10
+ packageProject,
11
+ renderProject,
12
+ runProjectTests,
13
+ scaffoldProject,
14
+ validateProject
15
+ } from "../src/project.mjs";
16
+
17
+ function setOption(options, key, value) {
18
+ if (options[key] === undefined) {
19
+ options[key] = value;
20
+ return;
21
+ }
22
+
23
+ if (Array.isArray(options[key])) {
24
+ options[key].push(value);
25
+ return;
26
+ }
27
+
28
+ options[key] = [options[key], value];
29
+ }
30
+
31
+ function parseArgs(argv) {
32
+ const [command = "help", ...rest] = argv;
33
+ const options = { _: [] };
34
+ for (let index = 0; index < rest.length; index += 1) {
35
+ const token = rest[index];
36
+ if (!token.startsWith("--")) {
37
+ options._.push(token);
38
+ continue;
39
+ }
40
+
41
+ const key = token.slice(2);
42
+ const next = rest[index + 1];
43
+ if (!next || next.startsWith("--")) {
44
+ setOption(options, key, true);
45
+ continue;
46
+ }
47
+
48
+ setOption(options, key, next);
49
+ index += 1;
50
+ }
51
+ return { command, options };
52
+ }
53
+
54
+ function asArray(value) {
55
+ if (value === undefined) {
56
+ return [];
57
+ }
58
+ return Array.isArray(value) ? value : [value];
59
+ }
60
+
61
+ function duration(startTime) {
62
+ return Date.now() - startTime;
63
+ }
64
+
65
+ function printJson(command, success, warnings, errors, startedAt, data = undefined) {
66
+ process.stdout.write(JSON.stringify({
67
+ command,
68
+ success,
69
+ durationMs: duration(startedAt),
70
+ warnings,
71
+ errors,
72
+ data
73
+ }, null, 2) + "\n");
74
+ }
75
+
76
+ function printHelp() {
77
+ process.stdout.write(
78
+ "Usage: s3te <command> [options]\n\n" +
79
+ "Commands:\n" +
80
+ " init\n" +
81
+ " validate\n" +
82
+ " render\n" +
83
+ " test\n" +
84
+ " package\n" +
85
+ " deploy\n" +
86
+ " doctor\n" +
87
+ " migrate\n"
88
+ );
89
+ }
90
+
91
+ async function loadConfigForCommand(projectDir, configOption) {
92
+ const configPath = path.resolve(projectDir, configOption ?? "s3te.config.json");
93
+ return { configPath, ...(await loadResolvedConfig(projectDir, configPath)) };
94
+ }
95
+
96
+ function warningsShouldFail(options, warnings) {
97
+ return Boolean(options["warnings-as-errors"]) && warnings.length > 0;
98
+ }
99
+
100
+ async function main() {
101
+ const startedAt = Date.now();
102
+ const { command, options } = parseArgs(process.argv.slice(2));
103
+ const cwd = path.resolve(options.cwd ?? process.cwd());
104
+ const wantsJson = Boolean(options.json);
105
+
106
+ if (command === "help" || command === "--help" || command === "-h") {
107
+ printHelp();
108
+ return;
109
+ }
110
+
111
+ if (command === "init") {
112
+ const projectDir = path.resolve(cwd, options.dir ?? ".");
113
+ const result = await scaffoldProject(projectDir, {
114
+ projectName: options["project-name"],
115
+ baseUrl: options["base-url"],
116
+ variant: asArray(options.variant)[0],
117
+ language: asArray(options.lang)[0],
118
+ force: Boolean(options.force)
119
+ });
120
+
121
+ if (wantsJson) {
122
+ printJson("init", true, [], [], startedAt, result);
123
+ return;
124
+ }
125
+
126
+ process.stdout.write(`Initialized project ${result.projectName} in ${projectDir}\n`);
127
+ return;
128
+ }
129
+
130
+ if (command === "validate") {
131
+ const loaded = await loadConfigForCommand(cwd, options.config);
132
+ if (!loaded.ok) {
133
+ if (wantsJson) {
134
+ printJson("validate", false, loaded.warnings, loaded.errors, startedAt);
135
+ } else {
136
+ for (const error of loaded.errors) {
137
+ process.stderr.write(`${error.code}: ${error.message}\n`);
138
+ }
139
+ }
140
+ process.exitCode = 2;
141
+ return;
142
+ }
143
+
144
+ const validation = await validateProject(cwd, loaded.config, {
145
+ environment: asArray(options.env)[0]
146
+ });
147
+ const success = validation.ok && !warningsShouldFail(options, validation.warnings);
148
+
149
+ if (wantsJson) {
150
+ printJson("validate", success, validation.warnings, validation.errors, startedAt, {
151
+ configPath: loaded.configPath,
152
+ checkedEnvironments: asArray(options.env).length > 0 ? asArray(options.env) : Object.keys(loaded.config.environments),
153
+ checkedTemplates: validation.checkedTemplates
154
+ });
155
+ process.exitCode = success ? 0 : 2;
156
+ return;
157
+ }
158
+
159
+ if (!success) {
160
+ for (const error of validation.errors) {
161
+ process.stderr.write(`${error.code}: ${error.message}\n`);
162
+ }
163
+ if (warningsShouldFail(options, validation.warnings)) {
164
+ for (const warning of validation.warnings) {
165
+ process.stderr.write(`${warning.code}: ${warning.message}\n`);
166
+ }
167
+ }
168
+ process.exitCode = 2;
169
+ return;
170
+ }
171
+
172
+ process.stdout.write(`Config and templates valid: ${loaded.configPath}\n`);
173
+ return;
174
+ }
175
+
176
+ if (command === "render") {
177
+ const loaded = await loadConfigForCommand(cwd, options.config);
178
+ if (!loaded.ok) {
179
+ if (wantsJson) {
180
+ printJson("render", false, loaded.warnings, loaded.errors, startedAt);
181
+ } else {
182
+ for (const error of loaded.errors) {
183
+ process.stderr.write(`${error.code}: ${error.message}\n`);
184
+ }
185
+ }
186
+ process.exitCode = 2;
187
+ return;
188
+ }
189
+
190
+ if (!options.env) {
191
+ process.stderr.write("render requires --env <name>\n");
192
+ process.exitCode = 1;
193
+ return;
194
+ }
195
+
196
+ const report = await renderProject(cwd, loaded.config, {
197
+ environment: asArray(options.env)[0],
198
+ variant: asArray(options.variant)[0],
199
+ language: asArray(options.lang)[0],
200
+ entry: options.entry,
201
+ outputDir: options["output-dir"]
202
+ });
203
+ const success = !warningsShouldFail(options, report.warnings);
204
+
205
+ if (options.stdout && report.renderedArtifacts.length === 1) {
206
+ const body = await fs.readFile(path.join(cwd, report.renderedArtifacts[0]), "utf8");
207
+ process.stdout.write(body);
208
+ process.exitCode = success ? 0 : 2;
209
+ return;
210
+ }
211
+
212
+ if (wantsJson) {
213
+ printJson("render", success, report.warnings, [], startedAt, report);
214
+ process.exitCode = success ? 0 : 2;
215
+ return;
216
+ }
217
+
218
+ process.stdout.write(`Rendered ${report.renderedArtifacts.length} artifact(s) into ${report.outputDir}\n`);
219
+ process.exitCode = success ? 0 : 2;
220
+ return;
221
+ }
222
+
223
+ if (command === "test") {
224
+ const loaded = await loadConfigForCommand(cwd, options.config);
225
+ if (!loaded.ok) {
226
+ if (wantsJson) {
227
+ printJson("test", false, loaded.warnings, loaded.errors, startedAt);
228
+ } else {
229
+ for (const error of loaded.errors) {
230
+ process.stderr.write(`${error.code}: ${error.message}\n`);
231
+ }
232
+ }
233
+ process.exitCode = 2;
234
+ return;
235
+ }
236
+
237
+ const validation = await validateProject(cwd, loaded.config, {
238
+ environment: asArray(options.env)[0]
239
+ });
240
+ if (!validation.ok) {
241
+ if (wantsJson) {
242
+ printJson("test", false, validation.warnings, validation.errors, startedAt);
243
+ }
244
+ process.exitCode = 2;
245
+ return;
246
+ }
247
+
248
+ const exitCode = await runProjectTests(cwd);
249
+ process.exitCode = exitCode;
250
+ return;
251
+ }
252
+
253
+ if (command === "package") {
254
+ const loaded = await loadConfigForCommand(cwd, options.config);
255
+ if (!loaded.ok) {
256
+ if (wantsJson) {
257
+ printJson("package", false, loaded.warnings, loaded.errors, startedAt);
258
+ }
259
+ process.exitCode = 2;
260
+ return;
261
+ }
262
+ if (!options.env) {
263
+ process.stderr.write("package requires --env <name>\n");
264
+ process.exitCode = 1;
265
+ return;
266
+ }
267
+
268
+ const report = await packageProject(cwd, loaded.config, {
269
+ environment: asArray(options.env)[0],
270
+ outDir: options["out-dir"],
271
+ clean: Boolean(options.clean),
272
+ features: asArray(options.feature)
273
+ });
274
+
275
+ if (wantsJson) {
276
+ printJson("package", true, [], [], startedAt, report);
277
+ return;
278
+ }
279
+
280
+ process.stdout.write(`Packaged deployment artifacts into ${report.packageDir}\n`);
281
+ return;
282
+ }
283
+
284
+ if (command === "deploy") {
285
+ const loaded = await loadConfigForCommand(cwd, options.config);
286
+ if (!loaded.ok) {
287
+ if (wantsJson) {
288
+ printJson("deploy", false, loaded.warnings, loaded.errors, startedAt);
289
+ }
290
+ process.exitCode = 2;
291
+ return;
292
+ }
293
+ if (!options.env) {
294
+ process.stderr.write("deploy requires --env <name>\n");
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+
299
+ const report = await deployProject(cwd, loaded.config, {
300
+ environment: asArray(options.env)[0],
301
+ packageDir: options["package-dir"],
302
+ features: asArray(options.feature),
303
+ profile: options.profile,
304
+ plan: Boolean(options.plan),
305
+ noSync: Boolean(options["no-sync"]),
306
+ stdio: wantsJson ? "pipe" : "inherit"
307
+ });
308
+
309
+ if (wantsJson) {
310
+ printJson("deploy", true, [], [], startedAt, report);
311
+ return;
312
+ }
313
+
314
+ process.stdout.write(`${options.plan ? "Prepared" : "Deployed"} stack ${report.stackName}\n`);
315
+ return;
316
+ }
317
+
318
+ if (command === "doctor") {
319
+ let loaded = null;
320
+ try {
321
+ loaded = await loadConfigForCommand(cwd, options.config);
322
+ } catch {
323
+ loaded = null;
324
+ }
325
+
326
+ const configPath = path.resolve(cwd, options.config ?? "s3te.config.json");
327
+ const checks = await doctorProject(cwd, configPath, {
328
+ environment: asArray(options.env)[0],
329
+ config: loaded?.config,
330
+ profile: options.profile
331
+ });
332
+ const success = checks.every((check) => check.ok);
333
+
334
+ if (wantsJson) {
335
+ printJson("doctor", success, [], [], startedAt, { checks });
336
+ process.exitCode = success ? 0 : 3;
337
+ return;
338
+ }
339
+
340
+ for (const check of checks) {
341
+ process.stdout.write(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.message}\n`);
342
+ }
343
+ process.exitCode = success ? 0 : 3;
344
+ return;
345
+ }
346
+
347
+ if (command === "migrate") {
348
+ const configPath = path.resolve(cwd, options.config ?? "s3te.config.json");
349
+ const rawConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
350
+ const migration = await migrateProject(configPath, rawConfig, {
351
+ writeChanges: Boolean(options.write) && !Boolean(options["dry-run"]),
352
+ enableWebiny: Boolean(options["enable-webiny"]),
353
+ disableWebiny: Boolean(options["disable-webiny"]),
354
+ webinySourceTable: options["webiny-source-table"],
355
+ webinyTenant: options["webiny-tenant"],
356
+ webinyModels: asArray(options["webiny-model"])
357
+ });
358
+ if (wantsJson) {
359
+ printJson("migrate", true, [], [], startedAt, {
360
+ configVersion: migration.config.configVersion,
361
+ wrote: Boolean(options.write) && !Boolean(options["dry-run"]),
362
+ changes: migration.changes
363
+ });
364
+ return;
365
+ }
366
+ process.stdout.write(options.write ? `Migrated ${configPath}\n` : `Migration preview for ${configPath}: configVersion=${migration.config.configVersion}\n`);
367
+ for (const change of migration.changes) {
368
+ process.stdout.write(`- ${change}\n`);
369
+ }
370
+ return;
371
+ }
372
+
373
+ printHelp();
374
+ process.exitCode = 1;
375
+ }
376
+
377
+ main().catch((error) => {
378
+ process.stderr.write(`${error.code ?? "ERROR"}: ${error.message}\n`);
379
+ if (error.details) {
380
+ process.stderr.write(`${JSON.stringify(error.details, null, 2)}\n`);
381
+ }
382
+ process.exitCode = error.code === "CONFIG_SCHEMA_ERROR" || error.code === "CONFIG_CONFLICT_ERROR" || error.code === "TEMPLATE_SYNTAX_ERROR" ? 2 : 1;
383
+ });
@@ -0,0 +1,221 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { applyContentQuery, getContentTypeForPath } from "../../core/src/index.mjs";
5
+
6
+ function normalizeKey(value) {
7
+ return String(value).replace(/\\/g, "/");
8
+ }
9
+
10
+ function normalizeLocale(value) {
11
+ return String(value).trim().toLowerCase();
12
+ }
13
+
14
+ function buildLocaleCandidates(language, languageLocaleMap) {
15
+ const requested = String(language ?? "").trim();
16
+ const configured = String(languageLocaleMap?.[requested] ?? requested).trim();
17
+ return [...new Set([configured, requested].filter(Boolean).map(normalizeLocale))];
18
+ }
19
+
20
+ function matchesRequestedLocale(item, language, languageLocaleMap) {
21
+ return localeMatchScore(item?.locale, language, languageLocaleMap) > 0;
22
+ }
23
+
24
+ function localeMatchScore(itemLocale, language, languageLocaleMap) {
25
+ if (!itemLocale) {
26
+ return 1;
27
+ }
28
+
29
+ const normalizedItemLocale = normalizeLocale(itemLocale);
30
+ const candidates = buildLocaleCandidates(language, languageLocaleMap);
31
+ if (candidates.includes(normalizedItemLocale)) {
32
+ return 3;
33
+ }
34
+
35
+ if (candidates.some((candidate) => candidate.length >= 2 && normalizedItemLocale.startsWith(`${candidate}-`))) {
36
+ return 2;
37
+ }
38
+
39
+ return 0;
40
+ }
41
+
42
+ function filterItemsByRequestedLocale(items, language, languageLocaleMap) {
43
+ const grouped = new Map();
44
+
45
+ for (const item of items) {
46
+ const groupKey = item.contentId ?? item.id ?? JSON.stringify(item);
47
+ if (!grouped.has(groupKey)) {
48
+ grouped.set(groupKey, []);
49
+ }
50
+ grouped.get(groupKey).push(item);
51
+ }
52
+
53
+ return [...grouped.values()].flatMap((groupItems) => {
54
+ const scored = groupItems
55
+ .map((item) => ({ item, score: localeMatchScore(item.locale, language, languageLocaleMap) }))
56
+ .filter((entry) => entry.score > 0);
57
+ if (scored.length === 0) {
58
+ return [];
59
+ }
60
+
61
+ const bestScore = Math.max(...scored.map((entry) => entry.score));
62
+ return scored.filter((entry) => entry.score === bestScore).map((entry) => entry.item);
63
+ });
64
+ }
65
+
66
+ async function walkFiles(rootDir, currentDir = rootDir) {
67
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
68
+ const files = [];
69
+ for (const entry of entries) {
70
+ const fullPath = path.join(currentDir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ files.push(...await walkFiles(rootDir, fullPath));
73
+ } else if (entry.isFile()) {
74
+ files.push(path.relative(rootDir, fullPath).replace(/\\/g, "/"));
75
+ }
76
+ }
77
+ return files;
78
+ }
79
+
80
+ export class FileSystemTemplateRepository {
81
+ constructor(projectDir, config) {
82
+ this.projectDir = projectDir;
83
+ this.config = config;
84
+ }
85
+
86
+ resolveKey(key) {
87
+ const normalized = normalizeKey(key);
88
+
89
+ for (const [variantName, variantConfig] of Object.entries(this.config.variants)) {
90
+ if (normalized === variantName || normalized.startsWith(`${variantName}/`)) {
91
+ const suffix = normalized === variantName ? "" : normalized.slice(variantName.length + 1);
92
+ return path.join(this.projectDir, variantConfig.sourceDir, suffix);
93
+ }
94
+ }
95
+
96
+ return path.join(this.projectDir, normalized);
97
+ }
98
+
99
+ async get(key) {
100
+ const filePath = this.resolveKey(key);
101
+ try {
102
+ const body = await fs.readFile(filePath);
103
+ return {
104
+ key: normalizeKey(key),
105
+ body: body.toString("utf8"),
106
+ contentType: getContentTypeForPath(filePath)
107
+ };
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ async listVariantEntries(variant) {
114
+ const variantConfig = this.config.variants[variant];
115
+ const sourceDir = path.join(this.projectDir, variantConfig.sourceDir);
116
+ const files = await walkFiles(sourceDir);
117
+ return files.map((relativePath) => ({
118
+ key: `${variant}/${relativePath}`.replace(/\\/g, "/"),
119
+ body: null,
120
+ contentType: getContentTypeForPath(relativePath)
121
+ }));
122
+ }
123
+
124
+ async exists(key) {
125
+ try {
126
+ await fs.stat(this.resolveKey(key));
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ }
133
+
134
+ export class FileSystemContentRepository {
135
+ constructor(itemsByLanguage, languageLocaleMap = {}) {
136
+ this.itemsByLanguage = itemsByLanguage;
137
+ this.languageLocaleMap = languageLocaleMap;
138
+ }
139
+
140
+ getItems(language) {
141
+ const items = this.itemsByLanguage[language] ?? this.itemsByLanguage.default ?? [];
142
+ return filterItemsByRequestedLocale(items, language, this.languageLocaleMap);
143
+ }
144
+
145
+ async getByContentId(contentId, language) {
146
+ return this.getItems(language).find((item) => item.contentId === contentId) ?? null;
147
+ }
148
+
149
+ async query(query, language) {
150
+ return applyContentQuery(this.getItems(language), query);
151
+ }
152
+ }
153
+
154
+ export async function loadLocalContent(projectDir, config) {
155
+ const contentDirectories = [
156
+ path.join(projectDir, "offline", "content"),
157
+ path.join(projectDir, "content")
158
+ ];
159
+ const itemsByLanguage = {};
160
+
161
+ try {
162
+ for (const contentDir of contentDirectories) {
163
+ let entries = [];
164
+ try {
165
+ entries = await fs.readdir(contentDir, { withFileTypes: true });
166
+ } catch {
167
+ continue;
168
+ }
169
+
170
+ for (const entry of entries) {
171
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
172
+ continue;
173
+ }
174
+ const raw = await fs.readFile(path.join(contentDir, entry.name), "utf8");
175
+ const parsed = JSON.parse(raw);
176
+ if (!Array.isArray(parsed)) {
177
+ continue;
178
+ }
179
+ const key = entry.name === "items.json" ? "default" : entry.name.slice(0, -".json".length);
180
+ itemsByLanguage[key] = parsed;
181
+ }
182
+
183
+ if (Object.keys(itemsByLanguage).length > 0) {
184
+ break;
185
+ }
186
+ }
187
+ } catch {
188
+ for (const variantConfig of Object.values(config.variants)) {
189
+ for (const languageCode of Object.keys(variantConfig.languages)) {
190
+ itemsByLanguage[languageCode] = [];
191
+ }
192
+ }
193
+ }
194
+
195
+ const languageLocaleMap = Object.fromEntries(Object.entries(config.variants).flatMap(([, variantConfig]) => (
196
+ Object.entries(variantConfig.languages).map(([languageCode, languageConfig]) => [
197
+ languageCode,
198
+ languageConfig.webinyLocale ?? languageCode
199
+ ])
200
+ )));
201
+
202
+ return new FileSystemContentRepository(itemsByLanguage, languageLocaleMap);
203
+ }
204
+
205
+ export async function ensureDirectory(targetDir) {
206
+ await fs.mkdir(targetDir, { recursive: true });
207
+ }
208
+
209
+ export async function writeTextFile(targetPath, body) {
210
+ await ensureDirectory(path.dirname(targetPath));
211
+ await fs.writeFile(targetPath, body, "utf8");
212
+ }
213
+
214
+ export async function copyFile(sourcePath, targetPath) {
215
+ await ensureDirectory(path.dirname(targetPath));
216
+ await fs.copyFile(sourcePath, targetPath);
217
+ }
218
+
219
+ export async function removeDirectory(targetDir) {
220
+ await fs.rm(targetDir, { recursive: true, force: true });
221
+ }