@projectdochelp/s3te 3.2.2 → 3.3.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/README.md +414 -38
- package/package.json +1 -1
- package/packages/aws-adapter/src/deploy.mjs +118 -8
- package/packages/aws-adapter/src/index.mjs +1 -1
- package/packages/aws-adapter/src/package.mjs +8 -1
- package/packages/aws-adapter/src/template.mjs +70 -52
- package/packages/cli/bin/s3te.mjs +25 -16
- package/packages/cli/src/project.mjs +103 -100
- package/packages/core/src/config.mjs +4 -0
- package/packages/core/src/index.mjs +1 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ This README is the user guide for the rewrite generation. The deeper implementat
|
|
|
17
17
|
- [Usage](#usage)
|
|
18
18
|
- [Daily Workflow](#daily-workflow)
|
|
19
19
|
- [CLI Commands](#cli-commands)
|
|
20
|
-
|
|
20
|
+
- [Template Commands](#template-commands)
|
|
21
21
|
- [Optional: Sitemap](#optional-sitemap)
|
|
22
22
|
- [Optional: Webiny CMS](#optional-webiny-cms)
|
|
23
23
|
|
|
@@ -66,7 +66,7 @@ That source sync is not limited to Lambda code. It includes your `.html`, `.part
|
|
|
66
66
|
|
|
67
67
|
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.
|
|
68
68
|
|
|
69
|
-
If optional runtime features
|
|
69
|
+
If optional runtime features are enabled in `s3te.config.json`, S3TE extends the deployed AWS runtime accordingly. `sitemap` is added to the main environment stack. Webiny is deployed as a separate option stack so retrofitting CMS support does not require CloudFront resources to move with it.
|
|
70
70
|
|
|
71
71
|
</details>
|
|
72
72
|
|
|
@@ -484,50 +484,142 @@ Once Webiny is installed and the stack is deployed with Webiny enabled, CMS cont
|
|
|
484
484
|
| `s3te sync --env <name>` | Uploads current project sources into the configured code buckets. |
|
|
485
485
|
| `s3te doctor --env <name>` | Checks local machine and AWS access before deploy. |
|
|
486
486
|
| `s3te deploy --env <name>` | Deploys or updates the AWS environment and syncs source files. |
|
|
487
|
-
| `s3te
|
|
487
|
+
| `s3te option <webiny|sitemap>` | Writes or updates optional feature configuration such as Webiny or sitemap support in an existing S3TE project. |
|
|
488
488
|
|
|
489
489
|
</details>
|
|
490
490
|
|
|
491
|
-
|
|
491
|
+
## Template Commands
|
|
492
492
|
|
|
493
|
-
|
|
493
|
+
S3TE uses literal HTML-like tags inside your `.html` and `.part` files. The tags are case-sensitive, always lowercase, and never use attributes. JSON-based commands must contain valid JSON and reject unknown properties.
|
|
494
|
+
|
|
495
|
+
The commands below are grouped by purpose:
|
|
496
|
+
|
|
497
|
+
- `Core Features` work in every S3TE project.
|
|
498
|
+
- `Webiny Features` are the content-driven commands. Despite the name, they also work with local content files under `offline/content/` when you are not using Webiny yet.
|
|
499
|
+
|
|
500
|
+
### Core Features
|
|
494
501
|
|
|
495
502
|
<details>
|
|
496
|
-
<summary><code><part></code> - reuse a partial file</summary>
|
|
503
|
+
<summary><code><part></code> - reuse a partial file from <code>partDir</code></summary>
|
|
504
|
+
|
|
505
|
+
**Action**
|
|
506
|
+
|
|
507
|
+
Loads another template fragment from the current variant's `partDir`, renders it recursively, and inserts the result at the current position.
|
|
508
|
+
|
|
509
|
+
**Syntax**
|
|
497
510
|
|
|
498
511
|
```html
|
|
499
512
|
<part>head.part</part>
|
|
500
513
|
```
|
|
501
514
|
|
|
515
|
+
The payload must be a relative path inside `partDir`. Leading `/` and `..` are invalid.
|
|
516
|
+
|
|
517
|
+
**Example**
|
|
518
|
+
|
|
519
|
+
```html
|
|
520
|
+
<head>
|
|
521
|
+
<part>head.part</part>
|
|
522
|
+
</head>
|
|
523
|
+
```
|
|
524
|
+
|
|
502
525
|
</details>
|
|
503
526
|
|
|
504
527
|
<details>
|
|
505
528
|
<summary><code><if></code> - render inline HTML only when a condition matches</summary>
|
|
506
529
|
|
|
530
|
+
**Action**
|
|
531
|
+
|
|
532
|
+
Evaluates one inline JSON rule and renders its `template` only when the rule matches the current render target.
|
|
533
|
+
|
|
534
|
+
**Syntax**
|
|
535
|
+
|
|
507
536
|
```html
|
|
508
537
|
<if>{
|
|
509
538
|
"env": "prod",
|
|
539
|
+
"file": "index.html",
|
|
540
|
+
"not": false,
|
|
510
541
|
"template": "<meta name='robots' content='all'>"
|
|
511
542
|
}</if>
|
|
512
543
|
```
|
|
513
544
|
|
|
545
|
+
Supported JSON properties:
|
|
546
|
+
|
|
547
|
+
- `env` optional: matches the current environment name case-insensitively
|
|
548
|
+
- `file` optional: matches the current output filename, for example `index.html`
|
|
549
|
+
- `not` optional: inverts the final result when `true`
|
|
550
|
+
- `template` required: inline HTML to render when the rule matches
|
|
551
|
+
|
|
552
|
+
If both `env` and `file` are present, both must match.
|
|
553
|
+
|
|
554
|
+
If both conditions are omitted, the tag behaves like an inline template include and always renders its `template`.
|
|
555
|
+
|
|
556
|
+
**Example**
|
|
557
|
+
|
|
558
|
+
```html
|
|
559
|
+
<if>{
|
|
560
|
+
"env": "prod",
|
|
561
|
+
"template": "<meta name='robots' content='all'>"
|
|
562
|
+
}</if>
|
|
563
|
+
<if>{
|
|
564
|
+
"env": "test",
|
|
565
|
+
"template": "<meta name='robots' content='noindex'>"
|
|
566
|
+
}</if>
|
|
567
|
+
```
|
|
568
|
+
|
|
514
569
|
</details>
|
|
515
570
|
|
|
516
571
|
<details>
|
|
517
572
|
<summary><code><fileattribute></code> - print metadata of the current output file</summary>
|
|
518
573
|
|
|
574
|
+
**Action**
|
|
575
|
+
|
|
576
|
+
Prints metadata of the file currently being rendered.
|
|
577
|
+
|
|
578
|
+
**Syntax**
|
|
579
|
+
|
|
519
580
|
```html
|
|
520
581
|
<fileattribute>filename</fileattribute>
|
|
521
582
|
```
|
|
522
583
|
|
|
584
|
+
Currently supported values:
|
|
585
|
+
|
|
586
|
+
- `filename`: the output key relative to the target bucket, for example `news/article-one.html`
|
|
587
|
+
|
|
588
|
+
**Example**
|
|
589
|
+
|
|
590
|
+
```html
|
|
591
|
+
<link rel="canonical" href="https://<lang>baseurl</lang>/<fileattribute>filename</fileattribute>">
|
|
592
|
+
```
|
|
593
|
+
|
|
523
594
|
</details>
|
|
524
595
|
|
|
525
596
|
<details>
|
|
526
|
-
<summary><code><lang></code> - print
|
|
597
|
+
<summary><code><lang></code> - print current language metadata</summary>
|
|
598
|
+
|
|
599
|
+
**Action**
|
|
600
|
+
|
|
601
|
+
Prints language-related metadata of the current render target.
|
|
602
|
+
|
|
603
|
+
**Syntax**
|
|
604
|
+
|
|
605
|
+
```html
|
|
606
|
+
<lang>2</lang>
|
|
607
|
+
<lang>baseurl</lang>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Currently supported values:
|
|
611
|
+
|
|
612
|
+
- `2`: the current language code such as `en` or `de`
|
|
613
|
+
- `baseurl`: the resolved base hostname for the current language and environment
|
|
614
|
+
|
|
615
|
+
**Example**
|
|
527
616
|
|
|
528
617
|
```html
|
|
529
618
|
<html lang="<lang>2</lang>">
|
|
530
|
-
<
|
|
619
|
+
<head>
|
|
620
|
+
<link rel="canonical" href="https://<lang>baseurl</lang>">
|
|
621
|
+
</head>
|
|
622
|
+
</html>
|
|
531
623
|
```
|
|
532
624
|
|
|
533
625
|
</details>
|
|
@@ -535,6 +627,12 @@ These are the core S3TE commands you will use even in a plain HTML-only project.
|
|
|
535
627
|
<details>
|
|
536
628
|
<summary><code><switchlang></code> - choose inline content by language</summary>
|
|
537
629
|
|
|
630
|
+
**Action**
|
|
631
|
+
|
|
632
|
+
Selects the block whose tag name matches the current language and renders only that block.
|
|
633
|
+
|
|
634
|
+
**Syntax**
|
|
635
|
+
|
|
538
636
|
```html
|
|
539
637
|
<switchlang>
|
|
540
638
|
<de>Willkommen</de>
|
|
@@ -542,9 +640,270 @@ These are the core S3TE commands you will use even in a plain HTML-only project.
|
|
|
542
640
|
</switchlang>
|
|
543
641
|
```
|
|
544
642
|
|
|
643
|
+
There is no fallback to `defaultLanguage`. If the current language block is missing, S3TE renders an empty string and records a warning.
|
|
644
|
+
|
|
645
|
+
**Example**
|
|
646
|
+
|
|
647
|
+
```html
|
|
648
|
+
<p>
|
|
649
|
+
<switchlang>
|
|
650
|
+
<de>Dein ultimatives Website-Werkzeug</de>
|
|
651
|
+
<en>Your ultimate website tool</en>
|
|
652
|
+
</switchlang>
|
|
653
|
+
</p>
|
|
654
|
+
```
|
|
655
|
+
|
|
545
656
|
</details>
|
|
546
657
|
|
|
547
|
-
|
|
658
|
+
### Webiny Features
|
|
659
|
+
|
|
660
|
+
These commands read from the resolved content repository. With Webiny enabled, that means mirrored Webiny content. Without Webiny, the same commands can read from local JSON files under `offline/content/`.
|
|
661
|
+
|
|
662
|
+
<details>
|
|
663
|
+
<summary><code><dbpart></code> - insert one content fragment by <code>contentId</code></summary>
|
|
664
|
+
|
|
665
|
+
**Action**
|
|
666
|
+
|
|
667
|
+
Loads a single content item by `contentId` and inserts its content fragment.
|
|
668
|
+
|
|
669
|
+
S3TE first tries the language-specific field `content<lang>`, for example `contentde`, and falls back to `content` if the language-specific field does not exist.
|
|
670
|
+
|
|
671
|
+
**Syntax**
|
|
672
|
+
|
|
673
|
+
```html
|
|
674
|
+
<dbpart>impressum</dbpart>
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
The payload is the content ID, not the internal database record ID.
|
|
678
|
+
|
|
679
|
+
**Example**
|
|
680
|
+
|
|
681
|
+
```html
|
|
682
|
+
<body>
|
|
683
|
+
<dbpart>impressum</dbpart>
|
|
684
|
+
</body>
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
</details>
|
|
688
|
+
|
|
689
|
+
<details>
|
|
690
|
+
<summary><code><dbmulti></code> - render one inline template for multiple content items</summary>
|
|
691
|
+
|
|
692
|
+
**Action**
|
|
693
|
+
|
|
694
|
+
Queries matching content items and renders the given inline template once for each result.
|
|
695
|
+
|
|
696
|
+
**Syntax**
|
|
697
|
+
|
|
698
|
+
```html
|
|
699
|
+
<dbmulti>{
|
|
700
|
+
"filter": [
|
|
701
|
+
{"forWebsite": {"BOOL": true}}
|
|
702
|
+
],
|
|
703
|
+
"filtertype": "equals",
|
|
704
|
+
"limit": 3,
|
|
705
|
+
"template": "<article><h2><dbitem>headline</dbitem></h2></article>"
|
|
706
|
+
}</dbmulti>
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Supported JSON properties:
|
|
710
|
+
|
|
711
|
+
- `filter` required: array of legacy DynamoDB-style filter clauses
|
|
712
|
+
- `filtertype` optional: `equals` or `contains`, default is `equals`
|
|
713
|
+
- `limit` optional: maximum number of items to render
|
|
714
|
+
- `template` required: inline template rendered once per match
|
|
715
|
+
|
|
716
|
+
Filter notes:
|
|
717
|
+
|
|
718
|
+
- every filter clause contains exactly one field
|
|
719
|
+
- multiple clauses are combined with logical `AND`
|
|
720
|
+
- `__typename` matches the content model, for example `article`
|
|
721
|
+
- supported legacy value wrappers are `S`, `N`, `BOOL`, `NULL`, and `L`
|
|
722
|
+
- results are sorted deterministically; numeric `order` comes first, then `contentId`, then `id`
|
|
723
|
+
|
|
724
|
+
**Example**
|
|
725
|
+
|
|
726
|
+
```html
|
|
727
|
+
<dbmulti>{
|
|
728
|
+
"filter": [
|
|
729
|
+
{"__typename": {"S": "article"}},
|
|
730
|
+
{"forWebsite": {"BOOL": true}}
|
|
731
|
+
],
|
|
732
|
+
"limit": 3,
|
|
733
|
+
"template": "<a href='article-<dbitem>slug</dbitem>.html'><h2><dbitem>headline</dbitem></h2></a>"
|
|
734
|
+
}</dbmulti>
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
</details>
|
|
738
|
+
|
|
739
|
+
<details>
|
|
740
|
+
<summary><code><dbmultifile></code> - generate one output file per content item</summary>
|
|
741
|
+
|
|
742
|
+
**Action**
|
|
743
|
+
|
|
744
|
+
Turns one source template into multiple output files. The content items are selected by filter, and each item produces one rendered file.
|
|
745
|
+
|
|
746
|
+
**Syntax**
|
|
747
|
+
|
|
748
|
+
```html
|
|
749
|
+
<dbmultifile>{
|
|
750
|
+
"filenamesuffix": "slug",
|
|
751
|
+
"filter": [
|
|
752
|
+
{"__typename": {"S": "article"}}
|
|
753
|
+
],
|
|
754
|
+
"limit": 10
|
|
755
|
+
}</dbmultifile>
|
|
756
|
+
<!doctype html>
|
|
757
|
+
<html>
|
|
758
|
+
<body>
|
|
759
|
+
<h1><dbmultifileitem>headline</dbmultifileitem></h1>
|
|
760
|
+
</body>
|
|
761
|
+
</html>
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Supported JSON properties:
|
|
765
|
+
|
|
766
|
+
- `filenamesuffix` required: field whose value becomes the filename suffix
|
|
767
|
+
- `filter` required: array of legacy DynamoDB-style filter clauses
|
|
768
|
+
- `filtertype` optional: `equals` or `contains`, default is `equals`
|
|
769
|
+
- `limit` optional: maximum number of files to generate
|
|
770
|
+
|
|
771
|
+
Rules:
|
|
772
|
+
|
|
773
|
+
- `dbmultifile` must be the first non-whitespace construct in the file
|
|
774
|
+
- the control block itself is not part of the output
|
|
775
|
+
- generated filenames follow the pattern `<basename>-<suffix>.<ext>`
|
|
776
|
+
- the suffix must not be empty and must not contain `/`, `\\`, or `:`
|
|
777
|
+
- suffixes must be unique within that template
|
|
778
|
+
|
|
779
|
+
**Example**
|
|
780
|
+
|
|
781
|
+
If the source file is `article.html` and the current item has `"slug": "first-article"`, the generated output becomes `article-first-article.html`.
|
|
782
|
+
|
|
783
|
+
```html
|
|
784
|
+
<dbmultifile>{
|
|
785
|
+
"filenamesuffix": "slug",
|
|
786
|
+
"filter": [
|
|
787
|
+
{"__typename": {"S": "article"}}
|
|
788
|
+
]
|
|
789
|
+
}</dbmultifile>
|
|
790
|
+
<article>
|
|
791
|
+
<h1><dbmultifileitem>headline</dbmultifileitem></h1>
|
|
792
|
+
</article>
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
</details>
|
|
796
|
+
|
|
797
|
+
<details>
|
|
798
|
+
<summary><code><dbitem></code> - print one field of the current content item</summary>
|
|
799
|
+
|
|
800
|
+
**Action**
|
|
801
|
+
|
|
802
|
+
Reads one field from the current content item. This works inside `dbmulti` templates and inside `dbmultifile` bodies.
|
|
803
|
+
|
|
804
|
+
**Syntax**
|
|
805
|
+
|
|
806
|
+
```html
|
|
807
|
+
<dbitem>headline</dbitem>
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
Special field names:
|
|
811
|
+
|
|
812
|
+
- `__typename`
|
|
813
|
+
- `contentId`
|
|
814
|
+
- `id`
|
|
815
|
+
- `locale`
|
|
816
|
+
- `tenant`
|
|
817
|
+
- `_version`
|
|
818
|
+
- `_lastChangedAt`
|
|
819
|
+
|
|
820
|
+
For the field name `content`, S3TE again prefers `content<lang>` over `content`.
|
|
821
|
+
|
|
822
|
+
If the field value is a string array, S3TE serializes it as concatenated HTML links.
|
|
823
|
+
|
|
824
|
+
**Example**
|
|
825
|
+
|
|
826
|
+
```html
|
|
827
|
+
<dbmulti>{
|
|
828
|
+
"filter": [{"__typename": {"S": "article"}}],
|
|
829
|
+
"template": "<article><h2><dbitem>headline</dbitem></h2><div><dbitem>content</dbitem></div></article>"
|
|
830
|
+
}</dbmulti>
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
</details>
|
|
834
|
+
|
|
835
|
+
<details>
|
|
836
|
+
<summary><code><dbmultifileitem></code> - print or transform fields inside a <code>dbmultifile</code> body</summary>
|
|
837
|
+
|
|
838
|
+
**Action**
|
|
839
|
+
|
|
840
|
+
Reads one field from the current content item and can apply one transformation mode. It is primarily meant for `dbmultifile` bodies, but works wherever a current content item exists.
|
|
841
|
+
|
|
842
|
+
**Syntax**
|
|
843
|
+
|
|
844
|
+
Simple field output:
|
|
845
|
+
|
|
846
|
+
```html
|
|
847
|
+
<dbmultifileitem>headline</dbmultifileitem>
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
JSON command mode:
|
|
851
|
+
|
|
852
|
+
```html
|
|
853
|
+
<dbmultifileitem>{"field":"content","limit":160}</dbmultifileitem>
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
Supported JSON properties:
|
|
857
|
+
|
|
858
|
+
- `field` required
|
|
859
|
+
- `limit` optional: truncate text to a maximum length and append `...`
|
|
860
|
+
- `limitlow` optional: choose a random length between `limitlow` and `limit`
|
|
861
|
+
- `format` optional: currently only `date`
|
|
862
|
+
- `locale` optional: used with `format: "date"`
|
|
863
|
+
- `divideattag` optional: cut a section out of the field value
|
|
864
|
+
- `startnumber` optional: 1-based occurrence number for the divide start
|
|
865
|
+
- `endnumber` optional: 1-based occurrence number for the divide end
|
|
866
|
+
|
|
867
|
+
Only one transform mode is allowed at a time:
|
|
868
|
+
|
|
869
|
+
- limit mode: `limit` with optional `limitlow`
|
|
870
|
+
- date mode: `format: "date"`
|
|
871
|
+
- divide mode: `divideattag`
|
|
872
|
+
|
|
873
|
+
Date mode formats `de` as `dd.mm.yyyy`. All other locales currently format as `mm/dd/yyyy`.
|
|
874
|
+
|
|
875
|
+
**Examples**
|
|
876
|
+
|
|
877
|
+
Simple field output:
|
|
878
|
+
|
|
879
|
+
```html
|
|
880
|
+
<dbmultifileitem>headline</dbmultifileitem>
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
Truncated teaser text:
|
|
884
|
+
|
|
885
|
+
```html
|
|
886
|
+
<dbmultifileitem>{"field":"content","limit":160}</dbmultifileitem>
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
Date formatting:
|
|
890
|
+
|
|
891
|
+
```html
|
|
892
|
+
<dbmultifileitem>{"field":"publishedAt","format":"date","locale":"de"}</dbmultifileitem>
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Extract one section from a larger HTML field:
|
|
896
|
+
|
|
897
|
+
```html
|
|
898
|
+
<dbmultifileitem>{
|
|
899
|
+
"field":"content",
|
|
900
|
+
"divideattag":"<h2>",
|
|
901
|
+
"startnumber":2,
|
|
902
|
+
"endnumber":3
|
|
903
|
+
}</dbmultifileitem>
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
</details>
|
|
548
907
|
|
|
549
908
|
## Optional: Sitemap
|
|
550
909
|
|
|
@@ -570,11 +929,11 @@ Add this block to `s3te.config.json`:
|
|
|
570
929
|
|
|
571
930
|
The top-level `enabled` acts as the default. `integrations.sitemap.environments.<env>.enabled` can override that for a single environment.
|
|
572
931
|
|
|
573
|
-
If you prefer the CLI path, this does the same
|
|
932
|
+
If you prefer the CLI path, this does the same option update:
|
|
574
933
|
|
|
575
934
|
```bash
|
|
576
|
-
npx s3te
|
|
577
|
-
npx s3te
|
|
935
|
+
npx s3te option sitemap --enable --write
|
|
936
|
+
npx s3te option sitemap --env test --enable --write
|
|
578
937
|
```
|
|
579
938
|
|
|
580
939
|
After enabling or disabling `sitemap`, redeploy the affected environment once:
|
|
@@ -609,6 +968,8 @@ You do not need Webiny to use S3TE. Start with plain HTML first. Add Webiny only
|
|
|
609
968
|
|
|
610
969
|
The supported target for this optional path is Webiny 6.x on its standard AWS deployment model.
|
|
611
970
|
|
|
971
|
+
Important for the Webiny path: S3TE does not turn on DynamoDB Streams on your Webiny table for you. You must enable the stream manually on the Webiny source table. S3TE uses that stream as the trigger source for CMS-driven rerendering.
|
|
972
|
+
|
|
612
973
|

|
|
613
974
|
|
|
614
975
|
<details>
|
|
@@ -625,44 +986,70 @@ This section assumes that S3TE is already installed and deployed. The S3TE-speci
|
|
|
625
986
|
|
|
626
987
|
1. Install Webiny in AWS and finish the Webiny setup first.
|
|
627
988
|
2. Find the Webiny DynamoDB table that contains the CMS entries you want S3TE to mirror.
|
|
628
|
-
3.
|
|
989
|
+
3. Manually enable DynamoDB Streams on that Webiny table before the first S3TE deploy with Webiny enabled.
|
|
990
|
+
Use `NEW_AND_OLD_IMAGES`.
|
|
991
|
+
Without that stream, `s3te deploy --env <name>` cannot wire the Webiny trigger and fails because the table has no `LatestStreamArn`.
|
|
992
|
+
4. Write the Webiny option into your existing S3TE config:
|
|
629
993
|
|
|
630
994
|
```bash
|
|
631
|
-
npx s3te
|
|
995
|
+
npx s3te option webiny --enable --source-table webiny-1234567 --tenant root --model article --write
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
`staticContent` and `staticCodeContent` are kept automatically. Add `--model` once per custom model you want S3TE to mirror.
|
|
999
|
+
|
|
1000
|
+
`--model article` means:
|
|
1001
|
+
|
|
1002
|
+
- `article` is the technical Webiny model ID, not the human-readable label shown in the CMS UI.
|
|
1003
|
+
- S3TE adds that model ID to `integrations.webiny.relevantModels` in `s3te.config.json`.
|
|
1004
|
+
- Only Webiny stream records whose model is listed in `relevantModels` are mirrored into the S3TE content table and can trigger rerendering.
|
|
1005
|
+
- If you omit `--model`, only the built-in defaults `staticContent` and `staticCodeContent` are mirrored.
|
|
1006
|
+
- You can pass the flag multiple times for multiple models, for example `--model article --model news --model event`.
|
|
1007
|
+
|
|
1008
|
+
That makes the option example above equivalent to a config that contains:
|
|
1009
|
+
|
|
1010
|
+
```json
|
|
1011
|
+
"relevantModels": ["article", "staticContent", "staticCodeContent"]
|
|
632
1012
|
```
|
|
633
1013
|
|
|
634
|
-
|
|
1014
|
+
Use this for every Webiny model whose entries should be available to S3TE template commands like `dbitem`, `dbmulti`, `dbmultifile`, `dbmultifileitem`, or `dbpart`.
|
|
635
1015
|
|
|
636
|
-
If different environments should read from different Webiny installations or tenants, run the
|
|
1016
|
+
If different environments should read from different Webiny installations or tenants, run the option command per environment:
|
|
637
1017
|
|
|
638
1018
|
```bash
|
|
639
|
-
npx s3te
|
|
640
|
-
npx s3te
|
|
1019
|
+
npx s3te option webiny --env test --enable --source-table webiny-test-1234567 --tenant preview --write
|
|
1020
|
+
npx s3te option webiny --env prod --enable --source-table webiny-live-1234567 --tenant root --write
|
|
641
1021
|
```
|
|
642
1022
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1023
|
+
5. Verify again that DynamoDB Streams are enabled on the Webiny source table with `NEW_AND_OLD_IMAGES`.
|
|
1024
|
+
You enable this manually in the AWS console on the Webiny DynamoDB table under `Exports and streams`.
|
|
1025
|
+
S3TE creates the Lambda event source mapping during deploy, but it does not create or enable the table stream itself.
|
|
1026
|
+
6. 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" }`.
|
|
1027
|
+
7. If your Webiny installation hosts multiple tenants, keep `integrations.webiny.tenant` set so S3TE only mirrors the intended tenant.
|
|
1028
|
+
8. Check the project again:
|
|
647
1029
|
|
|
648
1030
|
```bash
|
|
649
1031
|
npx s3te doctor --env prod
|
|
650
1032
|
```
|
|
651
1033
|
|
|
652
|
-
|
|
1034
|
+
9. Redeploy the existing S3TE environment:
|
|
653
1035
|
|
|
654
1036
|
```bash
|
|
655
1037
|
npx s3te deploy --env prod
|
|
656
1038
|
```
|
|
657
1039
|
|
|
658
|
-
That deploy updates the existing environment stack and
|
|
1040
|
+
That deploy updates the existing environment stack and, when Webiny is enabled, also deploys the separate Webiny option stack for `content-mirror` and its DynamoDB stream mapping. 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>`.
|
|
1041
|
+
|
|
1042
|
+
Manual versus automatic responsibilities in this step:
|
|
1043
|
+
|
|
1044
|
+
- Manual: enable DynamoDB Streams on the Webiny source table
|
|
1045
|
+
- Automatic during `s3te deploy`: read `LatestStreamArn`, create the Lambda event source mapping, and deploy the S3TE Webiny mirror Lambda in the separate Webiny option stack
|
|
659
1046
|
|
|
660
1047
|
</details>
|
|
661
1048
|
|
|
662
1049
|
<details>
|
|
663
|
-
<summary>What the
|
|
1050
|
+
<summary>What the option command changes</summary>
|
|
664
1051
|
|
|
665
|
-
The
|
|
1052
|
+
The option command writes or updates the `integrations.webiny` block in `s3te.config.json`. A typical result looks like this:
|
|
666
1053
|
|
|
667
1054
|
Example config block:
|
|
668
1055
|
|
|
@@ -702,15 +1089,4 @@ For localized Webiny projects, the language block can also carry the mapping exp
|
|
|
702
1089
|
|
|
703
1090
|
</details>
|
|
704
1091
|
|
|
705
|
-
|
|
706
|
-
<summary>Content template commands</summary>
|
|
707
|
-
|
|
708
|
-
These commands are useful both with Webiny and with local JSON content files:
|
|
709
|
-
|
|
710
|
-
- `dbpart`
|
|
711
|
-
- `dbmulti`
|
|
712
|
-
- `dbmultifile`
|
|
713
|
-
- `dbitem`
|
|
714
|
-
- `dbmultifileitem`
|
|
715
|
-
|
|
716
|
-
</details>
|
|
1092
|
+
The content-driven tags are documented in [Template Commands](#template-commands), section [Webiny Features](#webiny-features).
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
buildEnvironmentRuntimeConfig,
|
|
6
|
+
resolveOptionStackName,
|
|
6
7
|
resolveStackName,
|
|
7
8
|
S3teError
|
|
8
9
|
} from "../../core/src/index.mjs";
|
|
@@ -34,6 +35,15 @@ function temporaryStackName(stackName) {
|
|
|
34
35
|
return `${stackName}-deploy-temp`;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
function stackDoesNotExist(error) {
|
|
39
|
+
const detailsText = [
|
|
40
|
+
error?.message,
|
|
41
|
+
error?.details?.stderr,
|
|
42
|
+
error?.details?.stdout
|
|
43
|
+
].filter(Boolean).join("\n");
|
|
44
|
+
return /does not exist/i.test(detailsText) || /Stack with id .* does not exist/i.test(detailsText);
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
async function uploadArtifact({ bucketName, key, bodyPath, region, profile, cwd }) {
|
|
38
48
|
await runAwsCli(["s3api", "put-object", "--bucket", bucketName, "--key", key, "--body", bodyPath], {
|
|
39
49
|
region,
|
|
@@ -53,6 +63,18 @@ async function describeStack({ stackName, region, profile, cwd }) {
|
|
|
53
63
|
return JSON.parse(describedStack.stdout).Stacks?.[0];
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
async function stackExists({ stackName, region, profile, cwd }) {
|
|
67
|
+
try {
|
|
68
|
+
await describeStack({ stackName, region, profile, cwd });
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (stackDoesNotExist(error)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
async function describeStackEvents({ stackName, region, profile, cwd }) {
|
|
57
79
|
const describedEvents = await runAwsCli(["cloudformation", "describe-stack-events", "--stack-name", stackName, "--output", "json"], {
|
|
58
80
|
region,
|
|
@@ -331,11 +353,37 @@ async function cleanupTemporaryArtifactsStack({
|
|
|
331
353
|
});
|
|
332
354
|
}
|
|
333
355
|
|
|
356
|
+
async function deleteCloudFormationStackIfExists({
|
|
357
|
+
stackName,
|
|
358
|
+
region,
|
|
359
|
+
profile,
|
|
360
|
+
cwd
|
|
361
|
+
}) {
|
|
362
|
+
if (!(await stackExists({ stackName, region, profile, cwd }))) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
await runAwsCli(["cloudformation", "delete-stack", "--stack-name", stackName], {
|
|
367
|
+
region,
|
|
368
|
+
profile,
|
|
369
|
+
cwd,
|
|
370
|
+
errorCode: "ADAPTER_ERROR"
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await runAwsCli(["cloudformation", "wait", "stack-delete-complete", "--stack-name", stackName], {
|
|
374
|
+
region,
|
|
375
|
+
profile,
|
|
376
|
+
cwd,
|
|
377
|
+
errorCode: "ADAPTER_ERROR"
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
334
383
|
function buildEnvironmentStackParameters({
|
|
335
384
|
artifactBucket,
|
|
336
385
|
uploadedArtifacts,
|
|
337
|
-
runtimeManifestValue
|
|
338
|
-
webinyStreamArn = ""
|
|
386
|
+
runtimeManifestValue
|
|
339
387
|
}) {
|
|
340
388
|
return [
|
|
341
389
|
`ArtifactBucket=${artifactBucket}`,
|
|
@@ -343,9 +391,19 @@ function buildEnvironmentStackParameters({
|
|
|
343
391
|
`RenderWorkerArtifactKey=${uploadedArtifacts.renderWorker}`,
|
|
344
392
|
`InvalidationSchedulerArtifactKey=${uploadedArtifacts.invalidationScheduler}`,
|
|
345
393
|
`InvalidationExecutorArtifactKey=${uploadedArtifacts.invalidationExecutor}`,
|
|
346
|
-
`ContentMirrorArtifactKey=${uploadedArtifacts.contentMirror}`,
|
|
347
394
|
`SitemapUpdaterArtifactKey=${uploadedArtifacts.sitemapUpdater}`,
|
|
348
|
-
`RuntimeManifestValue=${runtimeManifestValue}
|
|
395
|
+
`RuntimeManifestValue=${runtimeManifestValue}`
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function buildWebinyStackParameters({
|
|
400
|
+
artifactBucket,
|
|
401
|
+
uploadedArtifacts,
|
|
402
|
+
webinyStreamArn
|
|
403
|
+
}) {
|
|
404
|
+
return [
|
|
405
|
+
`ArtifactBucket=${artifactBucket}`,
|
|
406
|
+
`ContentMirrorArtifactKey=${uploadedArtifacts.contentMirror}`,
|
|
349
407
|
`WebinySourceTableStreamArn=${webinyStreamArn}`
|
|
350
408
|
];
|
|
351
409
|
}
|
|
@@ -365,6 +423,7 @@ export async function deployAwsProject({
|
|
|
365
423
|
const requestedFeatureSet = new Set(features);
|
|
366
424
|
const featureSet = new Set(resolveRequestedFeatures(config, features, environment));
|
|
367
425
|
const stackName = resolveStackName(config, environment);
|
|
426
|
+
const webinyStackName = resolveOptionStackName(config, environment, "webiny");
|
|
368
427
|
const tempStackName = temporaryStackName(stackName);
|
|
369
428
|
const runtimeManifestPath = path.join(projectDir, packageDir ?? path.join("offline", "IAAS", "package", environment), "runtime-manifest.json");
|
|
370
429
|
|
|
@@ -439,16 +498,36 @@ export async function deployAwsProject({
|
|
|
439
498
|
parameterOverrides: buildEnvironmentStackParameters({
|
|
440
499
|
artifactBucket: tempStack.artifactBucket,
|
|
441
500
|
uploadedArtifacts,
|
|
442
|
-
runtimeManifestValue: "{}"
|
|
443
|
-
webinyStreamArn
|
|
501
|
+
runtimeManifestValue: "{}"
|
|
444
502
|
}),
|
|
445
503
|
noExecute: plan,
|
|
446
504
|
stdio
|
|
447
505
|
});
|
|
448
506
|
|
|
449
507
|
if (plan) {
|
|
508
|
+
if (featureSet.has("webiny")) {
|
|
509
|
+
if (!packaged.manifest.webinyCloudFormationTemplate) {
|
|
510
|
+
throw new S3teError("ADAPTER_ERROR", "Package manifest is missing the Webiny option template.");
|
|
511
|
+
}
|
|
512
|
+
await deployCloudFormationStack({
|
|
513
|
+
stackName: webinyStackName,
|
|
514
|
+
templatePath: path.join(projectDir, packaged.manifest.webinyCloudFormationTemplate),
|
|
515
|
+
region: runtimeConfig.awsRegion,
|
|
516
|
+
profile,
|
|
517
|
+
cwd: projectDir,
|
|
518
|
+
capabilities: ["CAPABILITY_NAMED_IAM"],
|
|
519
|
+
parameterOverrides: buildWebinyStackParameters({
|
|
520
|
+
artifactBucket: tempStack.artifactBucket,
|
|
521
|
+
uploadedArtifacts,
|
|
522
|
+
webinyStreamArn
|
|
523
|
+
}),
|
|
524
|
+
noExecute: true,
|
|
525
|
+
stdio
|
|
526
|
+
});
|
|
527
|
+
}
|
|
450
528
|
return {
|
|
451
529
|
stackName,
|
|
530
|
+
optionalStacks: featureSet.has("webiny") ? [webinyStackName] : [],
|
|
452
531
|
packageDir: packaged.manifest.packageDir,
|
|
453
532
|
runtimeManifestPath: normalizeRelative(projectDir, runtimeManifestPath),
|
|
454
533
|
syncedCodeBuckets: [],
|
|
@@ -482,12 +561,41 @@ export async function deployAwsProject({
|
|
|
482
561
|
parameterOverrides: buildEnvironmentStackParameters({
|
|
483
562
|
artifactBucket: tempStack.artifactBucket,
|
|
484
563
|
uploadedArtifacts,
|
|
485
|
-
runtimeManifestValue: JSON.stringify(runtimeManifest)
|
|
486
|
-
webinyStreamArn
|
|
564
|
+
runtimeManifestValue: JSON.stringify(runtimeManifest)
|
|
487
565
|
}),
|
|
488
566
|
stdio
|
|
489
567
|
});
|
|
490
568
|
|
|
569
|
+
let deployedOptionalStacks = [];
|
|
570
|
+
let removedOptionalStacks = [];
|
|
571
|
+
if (featureSet.has("webiny")) {
|
|
572
|
+
if (!packaged.manifest.webinyCloudFormationTemplate) {
|
|
573
|
+
throw new S3teError("ADAPTER_ERROR", "Package manifest is missing the Webiny option template.");
|
|
574
|
+
}
|
|
575
|
+
await deployCloudFormationStack({
|
|
576
|
+
stackName: webinyStackName,
|
|
577
|
+
templatePath: path.join(projectDir, packaged.manifest.webinyCloudFormationTemplate),
|
|
578
|
+
region: runtimeConfig.awsRegion,
|
|
579
|
+
profile,
|
|
580
|
+
cwd: projectDir,
|
|
581
|
+
capabilities: ["CAPABILITY_NAMED_IAM"],
|
|
582
|
+
parameterOverrides: buildWebinyStackParameters({
|
|
583
|
+
artifactBucket: tempStack.artifactBucket,
|
|
584
|
+
uploadedArtifacts,
|
|
585
|
+
webinyStreamArn
|
|
586
|
+
}),
|
|
587
|
+
stdio
|
|
588
|
+
});
|
|
589
|
+
deployedOptionalStacks = [webinyStackName];
|
|
590
|
+
} else if (await deleteCloudFormationStackIfExists({
|
|
591
|
+
stackName: webinyStackName,
|
|
592
|
+
region: runtimeConfig.awsRegion,
|
|
593
|
+
profile,
|
|
594
|
+
cwd: projectDir
|
|
595
|
+
})) {
|
|
596
|
+
removedOptionalStacks = [webinyStackName];
|
|
597
|
+
}
|
|
598
|
+
|
|
491
599
|
const syncedCodeBuckets = noSync
|
|
492
600
|
? []
|
|
493
601
|
: (await syncPreparedSources({
|
|
@@ -511,6 +619,8 @@ export async function deployAwsProject({
|
|
|
511
619
|
|
|
512
620
|
return {
|
|
513
621
|
stackName,
|
|
622
|
+
optionalStacks: deployedOptionalStacks,
|
|
623
|
+
removedOptionalStacks,
|
|
514
624
|
packageDir: packaged.manifest.packageDir,
|
|
515
625
|
runtimeManifestPath: normalizeRelative(projectDir, runtimeManifestPath),
|
|
516
626
|
syncedCodeBuckets,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { writeZipArchive } from "./zip.mjs";
|
|
2
|
-
export { buildCloudFormationTemplate, buildTemporaryDeployStackTemplate } from "./template.mjs";
|
|
2
|
+
export { buildCloudFormationTemplate, buildTemporaryDeployStackTemplate, buildWebinyCloudFormationTemplate } from "./template.mjs";
|
|
3
3
|
export { buildAwsRuntimeManifest, extractStackOutputsMap } from "./manifest.mjs";
|
|
4
4
|
export { ensureAwsCliAvailable, ensureAwsCredentials, runAwsCli } from "./aws-cli.mjs";
|
|
5
5
|
export { getConfiguredFeatures, resolveRequestedFeatures } from "./features.mjs";
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
import { buildAwsRuntimeManifest } from "./manifest.mjs";
|
|
13
13
|
import { resolveRequestedFeatures } from "./features.mjs";
|
|
14
|
-
import { buildCloudFormationTemplate } from "./template.mjs";
|
|
14
|
+
import { buildCloudFormationTemplate, buildWebinyCloudFormationTemplate } from "./template.mjs";
|
|
15
15
|
import { writeZipArchive } from "./zip.mjs";
|
|
16
16
|
|
|
17
17
|
const ZIP_DATE = new Date("2020-01-01T00:00:00.000Z");
|
|
@@ -242,6 +242,7 @@ export async function packageAwsProject({
|
|
|
242
242
|
|
|
243
243
|
const lambdaDir = path.join(packageDir, "lambda");
|
|
244
244
|
const templatePath = path.join(packageDir, "cloudformation.template.json");
|
|
245
|
+
const webinyTemplatePath = path.join(packageDir, "cloudformation.webiny.template.json");
|
|
245
246
|
const packagingManifestPath = path.join(packageDir, "manifest.json");
|
|
246
247
|
const runtimeManifestSeedPath = path.join(packageDir, "runtime-manifest.base.json");
|
|
247
248
|
const lambdaEntries = await collectLambdaArchiveEntries();
|
|
@@ -292,6 +293,9 @@ export async function packageAwsProject({
|
|
|
292
293
|
const runtimeManifestSeed = buildAwsRuntimeManifest({ config, environment });
|
|
293
294
|
|
|
294
295
|
await writeJsonFile(templatePath, template);
|
|
296
|
+
if (resolvedFeatures.includes("webiny") && runtimeConfig.integrations.webiny.enabled) {
|
|
297
|
+
await writeJsonFile(webinyTemplatePath, buildWebinyCloudFormationTemplate({ config, environment }));
|
|
298
|
+
}
|
|
295
299
|
await writeJsonFile(runtimeManifestSeedPath, runtimeManifestSeed);
|
|
296
300
|
|
|
297
301
|
const manifest = {
|
|
@@ -302,6 +306,9 @@ export async function packageAwsProject({
|
|
|
302
306
|
runtimeParameterName: runtimeConfig.runtimeParameterName,
|
|
303
307
|
packageDir: normalizeRelative(projectDir, packageDir),
|
|
304
308
|
cloudFormationTemplate: normalizeRelative(projectDir, templatePath),
|
|
309
|
+
...(resolvedFeatures.includes("webiny") && runtimeConfig.integrations.webiny.enabled
|
|
310
|
+
? { webinyCloudFormationTemplate: normalizeRelative(projectDir, webinyTemplatePath) }
|
|
311
|
+
: {}),
|
|
305
312
|
runtimeManifestSeed: normalizeRelative(projectDir, runtimeManifestSeedPath),
|
|
306
313
|
lambdaArtifacts: Object.fromEntries(Object.entries(lambdaArtifacts).map(([name, artifact]) => [
|
|
307
314
|
name,
|
|
@@ -70,6 +70,17 @@ function lambdaRuntimeProperties(runtimeConfig, roleRef, name, keyParameter, han
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function buildFunctionNames(runtimeConfig) {
|
|
74
|
+
return {
|
|
75
|
+
sourceDispatcher: `${runtimeConfig.stackPrefix}_s3te_source_dispatcher`,
|
|
76
|
+
renderWorker: `${runtimeConfig.stackPrefix}_s3te_render_worker`,
|
|
77
|
+
invalidationScheduler: `${runtimeConfig.stackPrefix}_s3te_invalidation_scheduler`,
|
|
78
|
+
invalidationExecutor: `${runtimeConfig.stackPrefix}_s3te_invalidation_executor`,
|
|
79
|
+
contentMirror: `${runtimeConfig.stackPrefix}_s3te_content_mirror`,
|
|
80
|
+
sitemapUpdater: `${runtimeConfig.stackPrefix}_s3te_sitemap_updater`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
function createExecutionRole(roleName) {
|
|
74
85
|
return {
|
|
75
86
|
Type: "AWS::IAM::Role",
|
|
@@ -132,14 +143,7 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
132
143
|
const outputs = {};
|
|
133
144
|
const featureSet = new Set(features);
|
|
134
145
|
|
|
135
|
-
const functionNames =
|
|
136
|
-
sourceDispatcher: `${runtimeConfig.stackPrefix}_s3te_source_dispatcher`,
|
|
137
|
-
renderWorker: `${runtimeConfig.stackPrefix}_s3te_render_worker`,
|
|
138
|
-
invalidationScheduler: `${runtimeConfig.stackPrefix}_s3te_invalidation_scheduler`,
|
|
139
|
-
invalidationExecutor: `${runtimeConfig.stackPrefix}_s3te_invalidation_executor`,
|
|
140
|
-
contentMirror: `${runtimeConfig.stackPrefix}_s3te_content_mirror`,
|
|
141
|
-
sitemapUpdater: `${runtimeConfig.stackPrefix}_s3te_sitemap_updater`
|
|
142
|
-
};
|
|
146
|
+
const functionNames = buildFunctionNames(runtimeConfig);
|
|
143
147
|
|
|
144
148
|
const parameters = {
|
|
145
149
|
ArtifactBucket: {
|
|
@@ -157,10 +161,6 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
157
161
|
InvalidationExecutorArtifactKey: {
|
|
158
162
|
Type: "String"
|
|
159
163
|
},
|
|
160
|
-
ContentMirrorArtifactKey: {
|
|
161
|
-
Type: "String",
|
|
162
|
-
Default: ""
|
|
163
|
-
},
|
|
164
164
|
SitemapUpdaterArtifactKey: {
|
|
165
165
|
Type: "String",
|
|
166
166
|
Default: ""
|
|
@@ -168,10 +168,6 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
168
168
|
RuntimeManifestValue: {
|
|
169
169
|
Type: "String",
|
|
170
170
|
Default: "{}"
|
|
171
|
-
},
|
|
172
|
-
WebinySourceTableStreamArn: {
|
|
173
|
-
Type: "String",
|
|
174
|
-
Default: ""
|
|
175
171
|
}
|
|
176
172
|
};
|
|
177
173
|
|
|
@@ -361,39 +357,6 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
361
357
|
}
|
|
362
358
|
);
|
|
363
359
|
|
|
364
|
-
if (featureSet.has("webiny") && runtimeConfig.integrations.webiny.enabled) {
|
|
365
|
-
resources.ContentMirror = lambdaRuntimeProperties(
|
|
366
|
-
runtimeConfig,
|
|
367
|
-
"ExecutionRole",
|
|
368
|
-
functionNames.contentMirror,
|
|
369
|
-
"ContentMirrorArtifactKey",
|
|
370
|
-
"content-mirror",
|
|
371
|
-
{
|
|
372
|
-
Timeout: 300,
|
|
373
|
-
MemorySize: 512,
|
|
374
|
-
Environment: {
|
|
375
|
-
Variables: {
|
|
376
|
-
S3TE_ENVIRONMENT: environment,
|
|
377
|
-
S3TE_CONTENT_TABLE: runtimeConfig.tables.content,
|
|
378
|
-
S3TE_RELEVANT_MODELS: runtimeConfig.integrations.webiny.relevantModels.join(","),
|
|
379
|
-
S3TE_WEBINY_TENANT: runtimeConfig.integrations.webiny.tenant ?? "",
|
|
380
|
-
S3TE_RENDER_WORKER_NAME: functionNames.renderWorker
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
resources.ContentMirrorEventSourceMapping = {
|
|
387
|
-
Type: "AWS::Lambda::EventSourceMapping",
|
|
388
|
-
Properties: {
|
|
389
|
-
BatchSize: 10,
|
|
390
|
-
StartingPosition: "LATEST",
|
|
391
|
-
EventSourceArn: { Ref: "WebinySourceTableStreamArn" },
|
|
392
|
-
FunctionName: { Ref: "ContentMirror" }
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
360
|
if (featureSet.has("sitemap") && runtimeConfig.integrations.sitemap.enabled) {
|
|
398
361
|
resources.SitemapUpdater = lambdaRuntimeProperties(
|
|
399
362
|
runtimeConfig,
|
|
@@ -426,9 +389,6 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
426
389
|
outputs.InvalidationSchedulerFunctionName = { Value: functionNames.invalidationScheduler };
|
|
427
390
|
outputs.InvalidationExecutorFunctionName = { Value: functionNames.invalidationExecutor };
|
|
428
391
|
|
|
429
|
-
if (resources.ContentMirror) {
|
|
430
|
-
outputs.ContentMirrorFunctionName = { Value: functionNames.contentMirror };
|
|
431
|
-
}
|
|
432
392
|
if (resources.SitemapUpdater) {
|
|
433
393
|
outputs.SitemapUpdaterFunctionName = { Value: functionNames.sitemapUpdater };
|
|
434
394
|
}
|
|
@@ -620,6 +580,64 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
620
580
|
};
|
|
621
581
|
}
|
|
622
582
|
|
|
583
|
+
export function buildWebinyCloudFormationTemplate({ config, environment }) {
|
|
584
|
+
const runtimeConfig = buildEnvironmentRuntimeConfig(config, environment);
|
|
585
|
+
const functionNames = buildFunctionNames(runtimeConfig);
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
589
|
+
Description: `S3TE Webiny option stack for ${config.project.name} (${environment})`,
|
|
590
|
+
Parameters: {
|
|
591
|
+
ArtifactBucket: {
|
|
592
|
+
Type: "String"
|
|
593
|
+
},
|
|
594
|
+
ContentMirrorArtifactKey: {
|
|
595
|
+
Type: "String"
|
|
596
|
+
},
|
|
597
|
+
WebinySourceTableStreamArn: {
|
|
598
|
+
Type: "String"
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
Resources: {
|
|
602
|
+
ExecutionRole: createExecutionRole(`${runtimeConfig.stackPrefix}_s3te_webiny_lambda_runtime`),
|
|
603
|
+
ContentMirror: lambdaRuntimeProperties(
|
|
604
|
+
runtimeConfig,
|
|
605
|
+
"ExecutionRole",
|
|
606
|
+
functionNames.contentMirror,
|
|
607
|
+
"ContentMirrorArtifactKey",
|
|
608
|
+
"content-mirror",
|
|
609
|
+
{
|
|
610
|
+
Timeout: 300,
|
|
611
|
+
MemorySize: 512,
|
|
612
|
+
Environment: {
|
|
613
|
+
Variables: {
|
|
614
|
+
S3TE_ENVIRONMENT: environment,
|
|
615
|
+
S3TE_CONTENT_TABLE: runtimeConfig.tables.content,
|
|
616
|
+
S3TE_RELEVANT_MODELS: runtimeConfig.integrations.webiny.relevantModels.join(","),
|
|
617
|
+
S3TE_WEBINY_TENANT: runtimeConfig.integrations.webiny.tenant ?? "",
|
|
618
|
+
S3TE_RENDER_WORKER_NAME: functionNames.renderWorker
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
),
|
|
623
|
+
ContentMirrorEventSourceMapping: {
|
|
624
|
+
Type: "AWS::Lambda::EventSourceMapping",
|
|
625
|
+
Properties: {
|
|
626
|
+
BatchSize: 10,
|
|
627
|
+
StartingPosition: "LATEST",
|
|
628
|
+
EventSourceArn: { Ref: "WebinySourceTableStreamArn" },
|
|
629
|
+
FunctionName: { Ref: "ContentMirror" }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
Outputs: {
|
|
634
|
+
ContentMirrorFunctionName: {
|
|
635
|
+
Value: functionNames.contentMirror
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
623
641
|
export function buildTemporaryDeployStackTemplate() {
|
|
624
642
|
return {
|
|
625
643
|
AWSTemplateFormatVersion: "2010-09-09",
|
|
@@ -3,10 +3,10 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
configureProjectOption,
|
|
6
7
|
deployProject,
|
|
7
8
|
doctorProject,
|
|
8
9
|
loadResolvedConfig,
|
|
9
|
-
migrateProject,
|
|
10
10
|
packageProject,
|
|
11
11
|
renderProject,
|
|
12
12
|
runProjectTests,
|
|
@@ -86,7 +86,7 @@ function printHelp() {
|
|
|
86
86
|
" sync\n" +
|
|
87
87
|
" deploy\n" +
|
|
88
88
|
" doctor\n" +
|
|
89
|
-
"
|
|
89
|
+
" option <webiny|sitemap>\n"
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -377,30 +377,39 @@ async function main() {
|
|
|
377
377
|
return;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
if (command === "
|
|
380
|
+
if (command === "option") {
|
|
381
|
+
const optionName = String(options._[0] ?? "").trim().toLowerCase();
|
|
382
|
+
if (!optionName) {
|
|
383
|
+
process.stderr.write("option requires a name such as webiny or sitemap\n");
|
|
384
|
+
process.exitCode = 1;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
381
387
|
const configPath = path.resolve(cwd, options.config ?? "s3te.config.json");
|
|
382
388
|
const rawConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
383
|
-
const
|
|
389
|
+
const optionResult = await configureProjectOption(configPath, rawConfig, {
|
|
390
|
+
optionName,
|
|
384
391
|
writeChanges: Boolean(options.write) && !Boolean(options["dry-run"]),
|
|
385
392
|
environment: asArray(options.env)[0],
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
webinyTenant: options["webiny-tenant"],
|
|
392
|
-
webinyModels: asArray(options["webiny-model"])
|
|
393
|
+
enable: Boolean(options.enable),
|
|
394
|
+
disable: Boolean(options.disable),
|
|
395
|
+
sourceTable: options["source-table"],
|
|
396
|
+
tenant: options.tenant,
|
|
397
|
+
models: asArray(options.model)
|
|
393
398
|
});
|
|
394
399
|
if (wantsJson) {
|
|
395
|
-
printJson(
|
|
396
|
-
configVersion:
|
|
400
|
+
printJson(`option ${optionName}`, true, [], [], startedAt, {
|
|
401
|
+
configVersion: optionResult.config.configVersion,
|
|
397
402
|
wrote: Boolean(options.write) && !Boolean(options["dry-run"]),
|
|
398
|
-
changes:
|
|
403
|
+
changes: optionResult.changes
|
|
399
404
|
});
|
|
400
405
|
return;
|
|
401
406
|
}
|
|
402
|
-
process.stdout.write(
|
|
403
|
-
|
|
407
|
+
process.stdout.write(
|
|
408
|
+
options.write
|
|
409
|
+
? `Updated option ${optionName} in ${configPath}\n`
|
|
410
|
+
: `Option preview for ${optionName} in ${configPath}: configVersion=${optionResult.config.configVersion}\n`
|
|
411
|
+
);
|
|
412
|
+
for (const change of optionResult.changes) {
|
|
404
413
|
process.stdout.write(`- ${change}\n`);
|
|
405
414
|
}
|
|
406
415
|
return;
|
|
@@ -991,128 +991,129 @@ export async function doctorProject(projectDir, configPath, options = {}) {
|
|
|
991
991
|
return checks;
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
-
export async function
|
|
995
|
-
const options = typeof
|
|
996
|
-
?
|
|
997
|
-
: { writeChanges };
|
|
994
|
+
export async function configureProjectOption(configPath, rawConfig, optionConfiguration) {
|
|
995
|
+
const options = typeof optionConfiguration === "object" && optionConfiguration !== null
|
|
996
|
+
? optionConfiguration
|
|
997
|
+
: { writeChanges: optionConfiguration };
|
|
998
|
+
const optionName = String(options.optionName ?? "").trim().toLowerCase();
|
|
999
|
+
const targetEnvironment = options.environment ? String(options.environment).trim() : "";
|
|
998
1000
|
const nextConfig = {
|
|
999
1001
|
...rawConfig,
|
|
1000
1002
|
configVersion: rawConfig.configVersion ?? 1
|
|
1001
1003
|
};
|
|
1002
1004
|
const changes = [];
|
|
1003
1005
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const enableSitemap = Boolean(options.enableSitemap);
|
|
1007
|
-
const disableSitemap = Boolean(options.disableSitemap);
|
|
1008
|
-
const targetEnvironment = options.environment ? String(options.environment).trim() : "";
|
|
1009
|
-
const webinySourceTable = options.webinySourceTable ? String(options.webinySourceTable).trim() : "";
|
|
1010
|
-
const webinyTenant = options.webinyTenant ? String(options.webinyTenant).trim() : "";
|
|
1011
|
-
const webinyModels = normalizeStringList(options.webinyModels);
|
|
1012
|
-
|
|
1013
|
-
if (enableWebiny && disableWebiny) {
|
|
1014
|
-
throw new S3teError("CONFIG_CONFLICT_ERROR", "migrate does not allow --enable-webiny and --disable-webiny at the same time.");
|
|
1006
|
+
if (!optionName) {
|
|
1007
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", "option requires an optionName such as webiny or sitemap.");
|
|
1015
1008
|
}
|
|
1016
|
-
if (
|
|
1017
|
-
throw new S3teError("CONFIG_CONFLICT_ERROR",
|
|
1009
|
+
if (!["webiny", "sitemap"].includes(optionName)) {
|
|
1010
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", `Unknown option ${optionName}. Supported options: webiny, sitemap.`);
|
|
1011
|
+
}
|
|
1012
|
+
if (targetEnvironment && !nextConfig.environments?.[targetEnvironment]) {
|
|
1013
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", `Unknown environment for option ${optionName}: ${targetEnvironment}.`);
|
|
1018
1014
|
}
|
|
1019
1015
|
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1016
|
+
const enable = Boolean(options.enable);
|
|
1017
|
+
const disable = Boolean(options.disable);
|
|
1018
|
+
if (enable && disable) {
|
|
1019
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", `option ${optionName} does not allow --enable and --disable at the same time.`);
|
|
1020
|
+
}
|
|
1025
1021
|
|
|
1026
|
-
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
??
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
: existingWebiny.enabled));
|
|
1044
|
-
const nextSourceTableName = webinySourceTable
|
|
1045
|
-
|| existingTargetWebiny.sourceTableName
|
|
1046
|
-
|| (targetEnvironment ? existingWebiny.sourceTableName : "")
|
|
1047
|
-
|| "";
|
|
1048
|
-
|
|
1049
|
-
if (shouldEnableWebiny && !nextSourceTableName) {
|
|
1050
|
-
throw new S3teError(
|
|
1051
|
-
"CONFIG_CONFLICT_ERROR",
|
|
1052
|
-
targetEnvironment
|
|
1053
|
-
? `Enabling Webiny for environment ${targetEnvironment} requires --webiny-source-table <table> or an existing sourceTableName.`
|
|
1054
|
-
: "Enabling Webiny requires --webiny-source-table <table> or an existing integrations.webiny.sourceTableName."
|
|
1022
|
+
if (optionName === "webiny") {
|
|
1023
|
+
const webinySourceTable = options.sourceTable ? String(options.sourceTable).trim() : "";
|
|
1024
|
+
const webinyTenant = options.tenant ? String(options.tenant).trim() : "";
|
|
1025
|
+
const webinyModels = normalizeStringList(options.models);
|
|
1026
|
+
const touchesWebiny = enable || disable || Boolean(webinySourceTable) || Boolean(webinyTenant) || webinyModels.length > 0;
|
|
1027
|
+
|
|
1028
|
+
if (touchesWebiny) {
|
|
1029
|
+
const existingIntegrations = nextConfig.integrations ?? {};
|
|
1030
|
+
const existingWebiny = existingIntegrations.webiny ?? {};
|
|
1031
|
+
const existingEnvironmentOverrides = existingWebiny.environments ?? {};
|
|
1032
|
+
const existingTargetWebiny = targetEnvironment
|
|
1033
|
+
? (existingEnvironmentOverrides[targetEnvironment] ?? {})
|
|
1034
|
+
: existingWebiny;
|
|
1035
|
+
const inheritedModels = normalizeStringList(
|
|
1036
|
+
existingTargetWebiny.relevantModels
|
|
1037
|
+
?? (targetEnvironment ? existingWebiny.relevantModels : undefined)
|
|
1038
|
+
?? ["staticContent", "staticCodeContent"]
|
|
1055
1039
|
);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1040
|
+
const shouldEnableWebiny = disable
|
|
1041
|
+
? false
|
|
1042
|
+
: (enable || Boolean(webinySourceTable) || webinyModels.length > 0
|
|
1043
|
+
? true
|
|
1044
|
+
: Boolean(targetEnvironment
|
|
1045
|
+
? (existingTargetWebiny.enabled ?? existingWebiny.enabled)
|
|
1046
|
+
: existingWebiny.enabled));
|
|
1047
|
+
const nextSourceTableName = webinySourceTable
|
|
1048
|
+
|| existingTargetWebiny.sourceTableName
|
|
1049
|
+
|| (targetEnvironment ? existingWebiny.sourceTableName : "")
|
|
1050
|
+
|| "";
|
|
1051
|
+
|
|
1052
|
+
if (shouldEnableWebiny && !nextSourceTableName) {
|
|
1053
|
+
throw new S3teError(
|
|
1054
|
+
"CONFIG_CONFLICT_ERROR",
|
|
1055
|
+
targetEnvironment
|
|
1056
|
+
? `Enabling Webiny for environment ${targetEnvironment} requires --source-table <table> or an existing sourceTableName.`
|
|
1057
|
+
: "Enabling Webiny requires --source-table <table> or an existing integrations.webiny.sourceTableName."
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1070
1060
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1061
|
+
const nextWebinyConfig = {
|
|
1062
|
+
enabled: shouldEnableWebiny,
|
|
1063
|
+
sourceTableName: nextSourceTableName || undefined,
|
|
1064
|
+
mirrorTableName: existingTargetWebiny.mirrorTableName
|
|
1065
|
+
?? (targetEnvironment ? existingWebiny.mirrorTableName : undefined)
|
|
1066
|
+
?? "{stackPrefix}_s3te_content_{project}",
|
|
1067
|
+
tenant: webinyTenant || existingTargetWebiny.tenant || (targetEnvironment ? existingWebiny.tenant : undefined) || undefined,
|
|
1068
|
+
relevantModels: normalizeStringList([
|
|
1069
|
+
...(inheritedModels.length > 0 ? inheritedModels : ["staticContent", "staticCodeContent"]),
|
|
1070
|
+
...webinyModels
|
|
1071
|
+
])
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
nextConfig.integrations = {
|
|
1075
|
+
...existingIntegrations,
|
|
1076
|
+
webiny: targetEnvironment
|
|
1077
|
+
? {
|
|
1078
|
+
...existingWebiny,
|
|
1079
|
+
environments: {
|
|
1080
|
+
...existingEnvironmentOverrides,
|
|
1081
|
+
[targetEnvironment]: nextWebinyConfig
|
|
1082
|
+
}
|
|
1079
1083
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1084
|
+
: {
|
|
1085
|
+
...existingWebiny,
|
|
1086
|
+
...nextWebinyConfig,
|
|
1087
|
+
...(Object.keys(existingEnvironmentOverrides).length > 0
|
|
1088
|
+
? { environments: existingEnvironmentOverrides }
|
|
1089
|
+
: {})
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1087
1092
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1093
|
+
const scopeLabel = targetEnvironment ? ` for environment ${targetEnvironment}` : "";
|
|
1094
|
+
changes.push(shouldEnableWebiny ? `Enabled Webiny option${scopeLabel}.` : `Disabled Webiny option${scopeLabel}.`);
|
|
1095
|
+
if (webinySourceTable) {
|
|
1096
|
+
changes.push(`Set Webiny source table${scopeLabel} to ${webinySourceTable}.`);
|
|
1097
|
+
}
|
|
1098
|
+
if (webinyTenant) {
|
|
1099
|
+
changes.push(`Set Webiny tenant${scopeLabel} to ${webinyTenant}.`);
|
|
1100
|
+
}
|
|
1101
|
+
if (webinyModels.length > 0) {
|
|
1102
|
+
changes.push(`Added Webiny models${scopeLabel}: ${webinyModels.join(", ")}.`);
|
|
1103
|
+
}
|
|
1098
1104
|
}
|
|
1099
1105
|
}
|
|
1100
1106
|
|
|
1101
|
-
|
|
1102
|
-
if (touchesSitemap) {
|
|
1103
|
-
if (targetEnvironment && !nextConfig.environments?.[targetEnvironment]) {
|
|
1104
|
-
throw new S3teError("CONFIG_CONFLICT_ERROR", `Unknown environment for migrate: ${targetEnvironment}.`);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
+
if (optionName === "sitemap") {
|
|
1107
1108
|
const existingIntegrations = nextConfig.integrations ?? {};
|
|
1108
1109
|
const existingSitemap = existingIntegrations.sitemap ?? {};
|
|
1109
1110
|
const existingEnvironmentOverrides = existingSitemap.environments ?? {};
|
|
1110
1111
|
const existingTargetSitemap = targetEnvironment
|
|
1111
1112
|
? (existingEnvironmentOverrides[targetEnvironment] ?? {})
|
|
1112
1113
|
: existingSitemap;
|
|
1113
|
-
const nextEnabled =
|
|
1114
|
+
const nextEnabled = disable
|
|
1114
1115
|
? false
|
|
1115
|
-
: (
|
|
1116
|
+
: (enable || Boolean(targetEnvironment
|
|
1116
1117
|
? (existingTargetSitemap.enabled ?? existingSitemap.enabled)
|
|
1117
1118
|
: existingSitemap.enabled));
|
|
1118
1119
|
|
|
@@ -1132,12 +1133,14 @@ export async function migrateProject(configPath, rawConfig, writeChanges) {
|
|
|
1132
1133
|
: {
|
|
1133
1134
|
...existingSitemap,
|
|
1134
1135
|
enabled: nextEnabled,
|
|
1135
|
-
|
|
1136
|
+
...(Object.keys(existingEnvironmentOverrides).length > 0
|
|
1137
|
+
? { environments: existingEnvironmentOverrides }
|
|
1138
|
+
: {})
|
|
1136
1139
|
}
|
|
1137
1140
|
};
|
|
1138
1141
|
|
|
1139
1142
|
const scopeLabel = targetEnvironment ? ` for environment ${targetEnvironment}` : "";
|
|
1140
|
-
changes.push(nextEnabled ? `Enabled sitemap
|
|
1143
|
+
changes.push(nextEnabled ? `Enabled sitemap option${scopeLabel}.` : `Disabled sitemap option${scopeLabel}.`);
|
|
1141
1144
|
}
|
|
1142
1145
|
|
|
1143
1146
|
if (options.writeChanges) {
|
|
@@ -383,6 +383,10 @@ export function resolveStackName(config, environmentName) {
|
|
|
383
383
|
return `${config.environments[environmentName].stackPrefix}-s3te-${config.project.name}`;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
export function resolveOptionStackName(config, environmentName, optionName) {
|
|
387
|
+
return `${resolveStackName(config, environmentName)}-${String(optionName).trim().toLowerCase()}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
386
390
|
export function buildEnvironmentRuntimeConfig(config, environmentName, stackOutputs = {}) {
|
|
387
391
|
const environmentConfig = config.environments[environmentName];
|
|
388
392
|
const webinyConfig = resolveEnvironmentWebinyIntegration(config, environmentName);
|