@projectdochelp/s3te 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- 1. [Motivation](#motivation)
10
- 2. [Support](#support)
11
- 3. [Concept](#concept)
12
- 4. [Installation (AWS)](#installation-aws)
13
- 5. [Installation (VSCode)](#installation-vscode)
14
- 6. [Installation (S3TE)](#installation-s3te)
15
- 7. [Usage](#usage)
16
- 8. [Optional: Webiny CMS](#optional-webiny-cms)
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
 
@@ -54,7 +59,9 @@ The website bucket contains the finished result that visitors actually receive t
54
59
  <details>
55
60
  <summary>What happens when I deploy?</summary>
56
61
 
57
- `s3te deploy` validates the project, packages the AWS runtime, creates or updates one persistent CloudFormation environment stack, creates one temporary CloudFormation deploy stack for packaging artifacts, synchronizes your current source files into the code bucket, and removes the temporary stack again after the real deploy run.
62
+ `s3te deploy` loads the validated project configuration, packages the AWS runtime, creates or updates one persistent CloudFormation environment stack, creates one temporary CloudFormation deploy stack for packaging artifacts, synchronizes your current source files into the code bucket, and removes the temporary stack again after the real deploy run.
63
+
64
+ That source sync is not limited to Lambda code. It includes your `.html`, `.part`, CSS, JavaScript, images and other project files so the running AWS stack can react to source changes inside the code bucket.
58
65
 
59
66
  The persistent environment stack contains the long-lived AWS resources such as buckets, Lambda functions, DynamoDB tables, CloudFront distributions and the runtime manifest parameter. The temporary deploy stack exists only so CloudFormation can consume the packaged Lambda artifacts cleanly.
60
67
 
@@ -152,7 +159,7 @@ With the local package installed, initialize the project like this:
152
159
  npx s3te init --project-name mywebsite --base-url example.com
153
160
  ```
154
161
 
155
- If `npm install` already created a minimal `package.json`, `s3te init` extends it with the missing S3TE defaults and scripts instead of failing.
162
+ 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
163
 
157
164
  If you want a one-shot scaffold without installing first, and `@projectdochelp/s3te` is already published on npm, this also works:
158
165
 
@@ -160,25 +167,15 @@ If you want a one-shot scaffold without installing first, and `@projectdochelp/s
160
167
  npx --package @projectdochelp/s3te s3te init --project-name mywebsite --base-url example.com
161
168
  ```
162
169
 
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
170
  The default scaffold creates:
177
171
 
178
172
  ```text
179
173
  mywebsite/
180
174
  package.json
181
175
  s3te.config.json
176
+ .github/
177
+ workflows/
178
+ s3te-sync.yml
182
179
  app/
183
180
  part/
184
181
  head.part
@@ -195,10 +192,12 @@ mywebsite/
195
192
  extensions.json
196
193
  ```
197
194
 
195
+ The generated `.github/workflows/s3te-sync.yml` is the default CI path for GitHub-based source publishing into the S3TE code bucket. It is scaffolded once and then left alone on later `s3te init` runs unless you use `--force`.
196
+
198
197
  </details>
199
198
 
200
199
  <details>
201
- <summary>5. Fill in the real AWS values in <code>s3te.config.json</code></summary>
200
+ <summary>4. Fill in the real AWS values in <code>s3te.config.json</code></summary>
202
201
 
203
202
  The most important fields for a first deployment are:
204
203
 
@@ -227,10 +226,12 @@ The most important fields for a first deployment are:
227
226
 
228
227
  `route53HostedZoneId` is optional. Leave it out if you want to manage DNS yourself.
229
228
 
229
+ 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>.`.
230
+
230
231
  </details>
231
232
 
232
233
  <details>
233
- <summary>6. Run the first local check and deploy</summary>
234
+ <summary>5. Run the first local check and deploy</summary>
234
235
 
235
236
  ```bash
236
237
  npx s3te validate
@@ -244,13 +245,15 @@ npx s3te deploy --env dev
244
245
 
245
246
  `deploy` creates or updates the persistent environment stack, uses a temporary deploy stack for packaged Lambda artifacts, synchronizes the source project into the code bucket, and removes the temporary stack again when the deploy finishes.
246
247
 
248
+ After the first successful deploy, use `s3te sync --env dev` for regular template, partial, asset and source updates when the infrastructure itself did not change.
249
+
247
250
  If you left `route53HostedZoneId` out of the config, the last DNS step stays manual: point your domain at the created CloudFront distribution after deploy.
248
251
 
249
252
  </details>
250
253
 
251
254
  ## Usage
252
255
 
253
- Once the project is installed, your everyday loop is deliberately small: edit templates, validate, render locally, then deploy.
256
+ Once the project is installed, your everyday loop splits into two paths: deploy when infrastructure changes, sync when only project sources changed.
254
257
 
255
258
  ### Daily Workflow
256
259
 
@@ -261,15 +264,24 @@ Once the project is installed, your everyday loop is deliberately small: edit te
261
264
  2. If you use content-driven tags without Webiny, edit `offline/content/en.json` or `offline/content/items.json`.
262
265
  3. Validate and render locally.
263
266
  4. Run your tests.
264
- 5. Deploy when the result looks right.
267
+ 5. Use `deploy` for the first installation or after infrastructure/config/runtime changes.
268
+ 6. Use `sync` for day-to-day source publishing into the code bucket.
265
269
 
266
270
  ```bash
267
271
  npx s3te validate
268
272
  npx s3te render --env dev
269
273
  npx s3te test
274
+ npx s3te sync --env dev
275
+ ```
276
+
277
+ Use a full deploy only when needed:
278
+
279
+ ```bash
270
280
  npx s3te deploy --env dev
271
281
  ```
272
282
 
283
+ Once Webiny is installed and the stack is deployed with Webiny enabled, CMS content changes are picked up in AWS through the DynamoDB stream integration. Those content changes do not require another `sync` or `deploy`.
284
+
273
285
  </details>
274
286
 
275
287
  ### CLI Commands
@@ -284,6 +296,7 @@ npx s3te deploy --env dev
284
296
  | `s3te render --env <name>` | Renders locally into `offline/S3TELocal/preview/<env>/...`. |
285
297
  | `s3te test` | Runs the project tests from `offline/tests/`. |
286
298
  | `s3te package --env <name>` | Builds the AWS deployment artifacts without deploying them yet. |
299
+ | `s3te sync --env <name>` | Uploads current project sources into the configured code buckets. |
287
300
  | `s3te doctor --env <name>` | Checks local machine and AWS access before deploy. |
288
301
  | `s3te deploy --env <name>` | Deploys or updates the AWS environment and syncs source files. |
289
302
  | `s3te migrate` | Updates older project configs and can retrofit Webiny into an existing S3TE project. |
@@ -378,6 +391,13 @@ npx s3te migrate --enable-webiny --webiny-source-table webiny-1234567 --webiny-t
378
391
 
379
392
  `staticContent` and `staticCodeContent` are kept automatically. Add `--webiny-model` once per custom model you want S3TE to mirror.
380
393
 
394
+ If different environments should read from different Webiny installations or tenants, run the migration per environment:
395
+
396
+ ```bash
397
+ npx s3te migrate --env test --enable-webiny --webiny-source-table webiny-test-1234567 --webiny-tenant preview --write
398
+ npx s3te migrate --env prod --enable-webiny --webiny-source-table webiny-live-1234567 --webiny-tenant root --write
399
+ ```
400
+
381
401
  4. Turn on DynamoDB Streams for the Webiny source table with `NEW_AND_OLD_IMAGES`.
382
402
  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
403
  6. If your Webiny installation hosts multiple tenants, keep `integrations.webiny.tenant` set so S3TE only mirrors the intended tenant.
@@ -393,7 +413,100 @@ npx s3te doctor --env prod
393
413
  npx s3te deploy --env prod
394
414
  ```
395
415
 
396
- That deploy updates the existing environment stack and adds the Webiny mirror resources to it. You do not need a fresh S3TE installation.
416
+ That deploy updates the existing environment stack and adds the Webiny mirror resources to it. You do not need a fresh S3TE installation. After that, Webiny content changes flow through the deployed AWS resources automatically; only template or asset changes still need `s3te sync --env <name>`.
417
+
418
+ </details>
419
+
420
+ <details>
421
+ <summary>GitHub Actions source publishing</summary>
422
+
423
+ If your team works through GitHub instead of running `s3te sync` locally, the scaffold already includes `.github/workflows/s3te-sync.yml`.
424
+
425
+ That workflow is meant for source publishing only:
426
+
427
+ - it validates the project
428
+ - it uploads `app/...` and `part/...` into the S3TE code bucket
429
+ - the resulting S3 events trigger the deployed Lambda pipeline in AWS
430
+
431
+ Use a full `deploy` only when the infrastructure, environment config, or runtime package changes.
432
+
433
+ Before the workflow can run, do this once:
434
+
435
+ 1. Run the first real `npx s3te deploy --env <name>` so the code bucket already exists.
436
+ 2. In AWS IAM, create an access key for a CI user that may sync only the S3TE code bucket for that environment.
437
+ 3. In GitHub open `Settings -> Secrets and variables -> Actions -> Secrets`.
438
+ 4. Add these repository secrets:
439
+ - `AWS_ACCESS_KEY_ID`
440
+ - `AWS_SECRET_ACCESS_KEY`
441
+ 5. Open `.github/workflows/s3te-sync.yml` and adjust:
442
+ - the branch under `on.push.branches`
443
+ - `aws-region`
444
+ - `npx s3te sync --env dev` to your target environment such as `prod` or `test`
445
+
446
+ Minimal IAM policy example for one code bucket:
447
+
448
+ ```json
449
+ {
450
+ "Version": "2012-10-17",
451
+ "Statement": [
452
+ {
453
+ "Effect": "Allow",
454
+ "Action": ["s3:ListBucket"],
455
+ "Resource": ["arn:aws:s3:::dev-website-code-mywebsite"]
456
+ },
457
+ {
458
+ "Effect": "Allow",
459
+ "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
460
+ "Resource": ["arn:aws:s3:::dev-website-code-mywebsite/*"]
461
+ }
462
+ ]
463
+ }
464
+ ```
465
+
466
+ For non-production environments or additional variants, use the derived code bucket names from your config, for example `test-website-code-mywebsite` or `app-code-mywebsite`.
467
+
468
+ The scaffolded workflow looks like this:
469
+
470
+ ```yaml
471
+ name: S3TE Sync
472
+ on:
473
+ workflow_dispatch:
474
+ push:
475
+ branches: ["main"]
476
+ paths:
477
+ - "app/**"
478
+ - "package.json"
479
+ - "package-lock.json"
480
+ - ".github/workflows/s3te-sync.yml"
481
+
482
+ jobs:
483
+ sync:
484
+ runs-on: ubuntu-latest
485
+ permissions:
486
+ contents: read
487
+ steps:
488
+ - uses: actions/checkout@v4
489
+ - uses: actions/setup-node@v4
490
+ with:
491
+ node-version: 22
492
+ cache: npm
493
+ - name: Install dependencies
494
+ shell: bash
495
+ run: |
496
+ if [ -f package-lock.json ]; then
497
+ npm ci
498
+ else
499
+ npm install
500
+ fi
501
+ - name: Configure AWS credentials
502
+ uses: aws-actions/configure-aws-credentials@v4
503
+ with:
504
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
505
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
506
+ aws-region: eu-central-1
507
+ - run: npx s3te validate
508
+ - run: npx s3te sync --env dev
509
+ ```
397
510
 
398
511
  </details>
399
512
 
@@ -411,7 +524,17 @@ Example config block:
411
524
  "sourceTableName": "webiny-1234567",
412
525
  "mirrorTableName": "{stackPrefix}_s3te_content_{project}",
413
526
  "tenant": "root",
414
- "relevantModels": ["article", "staticContent", "staticCodeContent"]
527
+ "relevantModels": ["article", "staticContent", "staticCodeContent"],
528
+ "environments": {
529
+ "test": {
530
+ "sourceTableName": "webiny-test-1234567",
531
+ "tenant": "preview"
532
+ },
533
+ "prod": {
534
+ "sourceTableName": "webiny-live-1234567",
535
+ "tenant": "root"
536
+ }
537
+ }
415
538
  }
416
539
  }
417
540
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectdochelp/s3te",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "CLI, render core, AWS adapter, and testkit for S3TemplateEngine projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,6 +10,7 @@ import { ensureAwsCliAvailable, ensureAwsCredentials, runAwsCli } from "./aws-cl
10
10
  import { resolveRequestedFeatures } from "./features.mjs";
11
11
  import { buildAwsRuntimeManifest, extractStackOutputsMap } from "./manifest.mjs";
12
12
  import { packageAwsProject } from "./package.mjs";
13
+ import { syncPreparedSources } from "./sync.mjs";
13
14
  import { buildTemporaryDeployStackTemplate } from "./template.mjs";
14
15
 
15
16
  function normalizeRelative(projectDir, targetPath) {
@@ -243,12 +244,12 @@ export async function deployAwsProject({
243
244
  }) {
244
245
  const runtimeConfig = buildEnvironmentRuntimeConfig(config, environment);
245
246
  const requestedFeatureSet = new Set(features);
246
- const featureSet = new Set(resolveRequestedFeatures(config, features));
247
+ const featureSet = new Set(resolveRequestedFeatures(config, features, environment));
247
248
  const stackName = resolveStackName(config, environment);
248
249
  const tempStackName = temporaryStackName(stackName);
249
250
  const runtimeManifestPath = path.join(projectDir, packageDir ?? path.join("offline", "IAAS", "package", environment), "runtime-manifest.json");
250
251
 
251
- if (requestedFeatureSet.has("webiny") && !config.integrations.webiny.enabled) {
252
+ if (requestedFeatureSet.has("webiny") && !runtimeConfig.integrations.webiny.enabled) {
252
253
  throw new S3teError("ADAPTER_ERROR", "Feature webiny was requested but is not enabled in s3te.config.json.");
253
254
  }
254
255
 
@@ -365,20 +366,15 @@ export async function deployAwsProject({
365
366
  stdio
366
367
  });
367
368
 
368
- const syncedCodeBuckets = [];
369
- if (!noSync) {
370
- for (const [variantName, variantConfig] of Object.entries(runtimeConfig.variants)) {
371
- const syncDir = path.join(projectDir, packaged.manifest.syncDirectories[variantName]);
372
- await runAwsCli(["s3", "sync", syncDir, `s3://${variantConfig.codeBucket}`], {
373
- region: runtimeConfig.awsRegion,
369
+ const syncedCodeBuckets = noSync
370
+ ? []
371
+ : (await syncPreparedSources({
372
+ projectDir,
373
+ runtimeConfig,
374
+ syncDirectories: packaged.manifest.syncDirectories,
374
375
  profile,
375
- cwd: projectDir,
376
- stdio,
377
- errorCode: "ADAPTER_ERROR"
378
- });
379
- syncedCodeBuckets.push(variantConfig.codeBucket);
380
- }
381
- }
376
+ stdio
377
+ })).syncedCodeBuckets;
382
378
 
383
379
  const distributions = [];
384
380
  for (const [variantName, variantConfig] of Object.entries(runtimeConfig.variants)) {
@@ -1,16 +1,29 @@
1
- export function getConfiguredFeatures(config) {
1
+ import { resolveEnvironmentWebinyIntegration } from "../../core/src/index.mjs";
2
+
3
+ export function getConfiguredFeatures(config, environment) {
2
4
  const features = [];
3
5
 
4
- if (config.integrations?.webiny?.enabled) {
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
  }
@@ -5,3 +5,4 @@ export { ensureAwsCliAvailable, ensureAwsCredentials, runAwsCli } from "./aws-cl
5
5
  export { getConfiguredFeatures, resolveRequestedFeatures } from "./features.mjs";
6
6
  export { packageAwsProject } from "./package.mjs";
7
7
  export { deployAwsProject } from "./deploy.mjs";
8
+ export { stageProjectSources, syncPreparedSources, syncAwsProject } from "./sync.mjs";
@@ -221,11 +221,13 @@ export async function packageAwsProject({
221
221
  clean = false,
222
222
  features = []
223
223
  }) {
224
- if (features.includes("webiny") && !config.integrations.webiny.enabled) {
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");
@@ -0,0 +1,155 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { buildEnvironmentRuntimeConfig } from "../../core/src/index.mjs";
5
+ import { ensureAwsCliAvailable, ensureAwsCredentials, runAwsCli } from "./aws-cli.mjs";
6
+
7
+ async function ensureDirectory(targetDir) {
8
+ await fs.mkdir(targetDir, { recursive: true });
9
+ }
10
+
11
+ async function removeDirectory(targetDir) {
12
+ await fs.rm(targetDir, { recursive: true, force: true });
13
+ }
14
+
15
+ async function listFiles(rootDir, currentDir = rootDir) {
16
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
17
+ const files = [];
18
+
19
+ for (const entry of entries) {
20
+ const fullPath = path.join(currentDir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ files.push(...await listFiles(rootDir, fullPath));
23
+ continue;
24
+ }
25
+
26
+ if (entry.isFile()) {
27
+ files.push(path.relative(rootDir, fullPath).replace(/\\/g, "/"));
28
+ }
29
+ }
30
+
31
+ return files.sort();
32
+ }
33
+
34
+ async function copyDirectory(sourceDir, targetDir) {
35
+ const files = await listFiles(sourceDir);
36
+ for (const relativePath of files) {
37
+ const sourcePath = path.join(sourceDir, relativePath);
38
+ const targetPath = path.join(targetDir, relativePath);
39
+ await ensureDirectory(path.dirname(targetPath));
40
+ await fs.copyFile(sourcePath, targetPath);
41
+ }
42
+ }
43
+
44
+ function normalizeRelative(projectDir, targetPath) {
45
+ return path.relative(projectDir, targetPath).replace(/\\/g, "/");
46
+ }
47
+
48
+ async function stageVariantSources(projectDir, runtimeConfig, variantName, syncRoot) {
49
+ const variantConfig = runtimeConfig.variants[variantName];
50
+ const variantRoot = path.join(syncRoot, variantName);
51
+ await removeDirectory(variantRoot);
52
+ await ensureDirectory(variantRoot);
53
+
54
+ await copyDirectory(path.join(projectDir, variantConfig.partDir), path.join(variantRoot, "part"));
55
+ await copyDirectory(path.join(projectDir, variantConfig.sourceDir), path.join(variantRoot, variantName));
56
+
57
+ return variantRoot;
58
+ }
59
+
60
+ export async function stageProjectSources({
61
+ projectDir,
62
+ config,
63
+ environment,
64
+ outDir
65
+ }) {
66
+ const runtimeConfig = buildEnvironmentRuntimeConfig(config, environment);
67
+ const syncRoot = outDir
68
+ ? path.join(projectDir, outDir)
69
+ : path.join(projectDir, "offline", "IAAS", "sync", environment);
70
+
71
+ await ensureDirectory(syncRoot);
72
+
73
+ const syncDirectories = {};
74
+ for (const variantName of Object.keys(runtimeConfig.variants)) {
75
+ const variantRoot = await stageVariantSources(projectDir, runtimeConfig, variantName, syncRoot);
76
+ syncDirectories[variantName] = normalizeRelative(projectDir, variantRoot);
77
+ }
78
+
79
+ return {
80
+ runtimeConfig,
81
+ syncRoot: normalizeRelative(projectDir, syncRoot),
82
+ syncDirectories
83
+ };
84
+ }
85
+
86
+ export async function syncPreparedSources({
87
+ projectDir,
88
+ runtimeConfig,
89
+ syncDirectories,
90
+ profile,
91
+ stdio = "pipe",
92
+ ensureAwsCliAvailableFn = ensureAwsCliAvailable,
93
+ ensureAwsCredentialsFn = ensureAwsCredentials,
94
+ runAwsCliFn = runAwsCli
95
+ }) {
96
+ await ensureAwsCliAvailableFn({ cwd: projectDir });
97
+ await ensureAwsCredentialsFn({
98
+ region: runtimeConfig.awsRegion,
99
+ profile,
100
+ cwd: projectDir
101
+ });
102
+
103
+ const syncedCodeBuckets = [];
104
+ for (const [variantName, variantConfig] of Object.entries(runtimeConfig.variants)) {
105
+ const syncDir = path.join(projectDir, syncDirectories[variantName]);
106
+ await runAwsCliFn(["s3", "sync", syncDir, `s3://${variantConfig.codeBucket}`, "--delete"], {
107
+ region: runtimeConfig.awsRegion,
108
+ profile,
109
+ cwd: projectDir,
110
+ stdio,
111
+ errorCode: "ADAPTER_ERROR"
112
+ });
113
+ syncedCodeBuckets.push(variantConfig.codeBucket);
114
+ }
115
+
116
+ return {
117
+ syncedCodeBuckets
118
+ };
119
+ }
120
+
121
+ export async function syncAwsProject({
122
+ projectDir,
123
+ config,
124
+ environment,
125
+ outDir,
126
+ profile,
127
+ stdio = "pipe",
128
+ ensureAwsCliAvailableFn = ensureAwsCliAvailable,
129
+ ensureAwsCredentialsFn = ensureAwsCredentials,
130
+ runAwsCliFn = runAwsCli
131
+ }) {
132
+ const prepared = await stageProjectSources({
133
+ projectDir,
134
+ config,
135
+ environment,
136
+ outDir
137
+ });
138
+
139
+ const synced = await syncPreparedSources({
140
+ projectDir,
141
+ runtimeConfig: prepared.runtimeConfig,
142
+ syncDirectories: prepared.syncDirectories,
143
+ profile,
144
+ stdio,
145
+ ensureAwsCliAvailableFn,
146
+ ensureAwsCredentialsFn,
147
+ runAwsCliFn
148
+ });
149
+
150
+ return {
151
+ syncRoot: prepared.syncRoot,
152
+ syncDirectories: prepared.syncDirectories,
153
+ syncedCodeBuckets: synced.syncedCodeBuckets
154
+ };
155
+ }
@@ -11,6 +11,7 @@ import {
11
11
  renderProject,
12
12
  runProjectTests,
13
13
  scaffoldProject,
14
+ syncProject,
14
15
  validateProject
15
16
  } from "../src/project.mjs";
16
17
 
@@ -82,6 +83,7 @@ function printHelp() {
82
83
  " render\n" +
83
84
  " test\n" +
84
85
  " package\n" +
86
+ " sync\n" +
85
87
  " deploy\n" +
86
88
  " doctor\n" +
87
89
  " migrate\n"
@@ -281,6 +283,37 @@ async function main() {
281
283
  return;
282
284
  }
283
285
 
286
+ if (command === "sync") {
287
+ const loaded = await loadConfigForCommand(cwd, options.config);
288
+ if (!loaded.ok) {
289
+ if (wantsJson) {
290
+ printJson("sync", false, loaded.warnings, loaded.errors, startedAt);
291
+ }
292
+ process.exitCode = 2;
293
+ return;
294
+ }
295
+ if (!options.env) {
296
+ process.stderr.write("sync requires --env <name>\n");
297
+ process.exitCode = 1;
298
+ return;
299
+ }
300
+
301
+ const report = await syncProject(cwd, loaded.config, {
302
+ environment: asArray(options.env)[0],
303
+ outDir: options["out-dir"],
304
+ profile: options.profile,
305
+ stdio: wantsJson ? "pipe" : "inherit"
306
+ });
307
+
308
+ if (wantsJson) {
309
+ printJson("sync", true, [], [], startedAt, report);
310
+ return;
311
+ }
312
+
313
+ process.stdout.write(`Synced project sources to ${report.syncedCodeBuckets.length} code bucket(s)\n`);
314
+ return;
315
+ }
316
+
284
317
  if (command === "deploy") {
285
318
  const loaded = await loadConfigForCommand(cwd, options.config);
286
319
  if (!loaded.ok) {
@@ -349,6 +382,7 @@ async function main() {
349
382
  const rawConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
350
383
  const migration = await migrateProject(configPath, rawConfig, {
351
384
  writeChanges: Boolean(options.write) && !Boolean(options["dry-run"]),
385
+ environment: asArray(options.env)[0],
352
386
  enableWebiny: Boolean(options["enable-webiny"]),
353
387
  disableWebiny: Boolean(options["disable-webiny"]),
354
388
  webinySourceTable: options["webiny-source-table"],