aws-cdk-conf-n-tags 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +297 -0
- package/dist/index.cjs +275 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +210 -0
- package/dist/index.d.ts +210 -0
- package/dist/index.js +231 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-2026, Eduard Moskvin <ed@moskvin.ca>
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# aws-cdk-conf-n-tags
|
|
2
|
+
|
|
3
|
+
Per-environment configuration loading and stack tagging for **AWS CDK** apps.
|
|
4
|
+
|
|
5
|
+
- **One YAML file per environment**, selected with `--context env=<name>`.
|
|
6
|
+
- **Schema-validated** with [zod](https://zod.dev) — typos and bad values fail loudly
|
|
7
|
+
at synth time, and the validated config is fully typed.
|
|
8
|
+
- **Account/region guard** — refuses to synth an environment against the wrong AWS
|
|
9
|
+
account or region.
|
|
10
|
+
- **Immutable config** — the loaded values are frozen; this is a config object, not a
|
|
11
|
+
place to stash resource references.
|
|
12
|
+
- **Centralized tagging** — apply arbitrary tags plus auto `CreatedBy` / `WorkDir`
|
|
13
|
+
tags to every resource in a stack.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
This package is used inside an AWS CDK app. If you don't have one yet, scaffold it first
|
|
18
|
+
with the CDK CLI in an empty directory:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
mkdir my-infra && cd my-infra
|
|
22
|
+
cdk init app --language typescript
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then, **from inside that CDK app directory** (where `package.json` and `cdk.json` live),
|
|
26
|
+
install this package:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install aws-cdk-conf-n-tags zod
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> This both adds the entries to your `package.json` `dependencies` and installs them, in
|
|
33
|
+
> one step (modern npm writes to `package.json` by default — no `--save` needed). If you
|
|
34
|
+
> prefer, you can instead add the dependencies to `package.json` by hand and then run
|
|
35
|
+
> `npm install` with no arguments.
|
|
36
|
+
|
|
37
|
+
`aws-cdk-lib` (`^2`), `constructs` (`^10`), and `zod` (`^3`) are **peer dependencies**.
|
|
38
|
+
Your CDK app already has the first two; install `zod` yourself — you import it to declare
|
|
39
|
+
your schema, and a single shared `zod` instance is required for validation to work
|
|
40
|
+
(a second copy would break the internal schema checks).
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
### 1. Declare your schema
|
|
45
|
+
|
|
46
|
+
Extend `BaseConfigSchema` (which already covers `awsAccount`, `awsRegion`, `tags`, and
|
|
47
|
+
the tag-name overrides) with the parameters your stack needs:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// config-schema.ts
|
|
51
|
+
import { BaseConfigSchema } from 'aws-cdk-conf-n-tags';
|
|
52
|
+
import { z } from 'zod';
|
|
53
|
+
|
|
54
|
+
export const ConfigSchema = BaseConfigSchema.extend({
|
|
55
|
+
vpcId: z.string(),
|
|
56
|
+
asgMinCapacity: z.number().int().default(1),
|
|
57
|
+
asgMaxCapacity: z.number().int().default(1),
|
|
58
|
+
ec2InstanceType: z.string().default('t4g.micro'),
|
|
59
|
+
auroraEngineVersion: z.enum(['5.7', '8.0']).default('5.7'),
|
|
60
|
+
serverlessMinCapacity: z.number().min(0.5).default(1),
|
|
61
|
+
certificateArns: z.array(z.string()).optional(),
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Load and use it in your stack
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { Stack, type StackProps } from 'aws-cdk-lib';
|
|
69
|
+
import { Vpc } from 'aws-cdk-lib/aws-ec2';
|
|
70
|
+
import type { Construct } from 'constructs';
|
|
71
|
+
import { Config } from 'aws-cdk-conf-n-tags';
|
|
72
|
+
import { ConfigSchema } from './config-schema.js';
|
|
73
|
+
import { MyAsg } from './my-asg.js'; // your own construct
|
|
74
|
+
|
|
75
|
+
export class MyStack extends Stack {
|
|
76
|
+
constructor(scope: Construct, id: string, props?: StackProps) {
|
|
77
|
+
super(scope, id, props);
|
|
78
|
+
|
|
79
|
+
// Resolves env from `--context env=...`, reads config/<env>.yaml,
|
|
80
|
+
// validates it, and checks the account/region. `conf.values` is typed + frozen.
|
|
81
|
+
const conf = Config.load(this, ConfigSchema);
|
|
82
|
+
|
|
83
|
+
// Tag every resource in the stack (config tags + auto CreatedBy/WorkDir).
|
|
84
|
+
conf.tagStack(this);
|
|
85
|
+
|
|
86
|
+
// Pass config values to constructs explicitly.
|
|
87
|
+
const vpc = Vpc.fromLookup(this, 'vpc', { vpcId: conf.values.vpcId });
|
|
88
|
+
new MyAsg(this, 'asg', { vpc, min: conf.values.asgMinCapacity });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Add a config file per environment
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
# config/production.yaml
|
|
97
|
+
awsAccount: "012345678901" # quote it — preserves leading zeros
|
|
98
|
+
awsRegion: us-west-2
|
|
99
|
+
|
|
100
|
+
vpcId: vpc-0abc123def4567890
|
|
101
|
+
asgMinCapacity: 2
|
|
102
|
+
auroraEngineVersion: "8.0"
|
|
103
|
+
|
|
104
|
+
tags:
|
|
105
|
+
Product: my-product
|
|
106
|
+
Cluster: web-001
|
|
107
|
+
Env: Production
|
|
108
|
+
RetentionDays: 28 # non-string values are coerced to strings when tagging
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Deploy with:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
cdk deploy --context env=production
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Using from JavaScript
|
|
118
|
+
|
|
119
|
+
The package ships both ESM and CommonJS builds, so it works in JavaScript CDK apps
|
|
120
|
+
(`cdk init app --language javascript`) just as well as TypeScript ones. The API is
|
|
121
|
+
identical — you only give up the *compile-time* layer (`z.infer` types and `tsc`
|
|
122
|
+
checking). **Runtime validation still catches typos, bad types, and out-of-range values
|
|
123
|
+
and applies defaults**, because that is zod behavior, not TypeScript.
|
|
124
|
+
|
|
125
|
+
ESM JavaScript (a project with `"type": "module"` in its `package.json`):
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
import { Stack } from 'aws-cdk-lib';
|
|
129
|
+
import { Config, BaseConfigSchema } from 'aws-cdk-conf-n-tags';
|
|
130
|
+
import { z } from 'zod';
|
|
131
|
+
|
|
132
|
+
const ConfigSchema = BaseConfigSchema.extend({
|
|
133
|
+
vpcId: z.string(),
|
|
134
|
+
asgMinCapacity: z.number().int().default(1),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export class MyStack extends Stack {
|
|
138
|
+
constructor(scope, id, props) {
|
|
139
|
+
super(scope, id, props);
|
|
140
|
+
|
|
141
|
+
const conf = Config.load(this, ConfigSchema);
|
|
142
|
+
conf.tagStack(this);
|
|
143
|
+
// conf.values.vpcId, conf.values.asgMinCapacity, ...
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
CommonJS JavaScript (the default `cdk init --language javascript` template) is the same,
|
|
149
|
+
with `require`:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
const { Config, BaseConfigSchema } = require('aws-cdk-conf-n-tags');
|
|
153
|
+
const { z } = require('zod');
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
> Tip: even in `.js` files, editors like VS Code read the package's bundled type
|
|
157
|
+
> declarations and offer autocomplete and hover docs on `conf.values`.
|
|
158
|
+
|
|
159
|
+
## How it behaves
|
|
160
|
+
|
|
161
|
+
- **Environment selection** — `Config.load` reads the env name from CDK context
|
|
162
|
+
(`--context env=<name>`; the key is configurable). It then loads
|
|
163
|
+
`config/<env>.yaml` relative to the current working directory.
|
|
164
|
+
- **Validation** — unknown keys are **rejected** by default (`unknownKeys: 'strict'`),
|
|
165
|
+
so a mistyped key like `asgMinCapcity` errors instead of silently using a default.
|
|
166
|
+
Types, enums, numeric ranges, and `required`/`default` are all enforced by your
|
|
167
|
+
schema. Switch to `'strip'` or `'passthrough'` if you need leniency.
|
|
168
|
+
- **Account/region guard** — `awsAccount` / `awsRegion` in the file are checked against
|
|
169
|
+
`CDK_DEFAULT_ACCOUNT` / `CDK_DEFAULT_REGION` (override via options, or disable with
|
|
170
|
+
`verifyAccountRegion: false`).
|
|
171
|
+
- **Immutability** — `conf.values` is `Object.freeze`'d, so it can't double as a mutable bag for created resources. Share those via explicit args
|
|
172
|
+
or the [`StackResources`](#sharing-resources-between-constructs) helper instead.
|
|
173
|
+
|
|
174
|
+
## Sharing resources between constructs
|
|
175
|
+
|
|
176
|
+
Because `conf.values` is frozen, it can't accumulate created resources the way a single
|
|
177
|
+
mutable config object used to. To hand resources (VPC, database, …) from one construct to
|
|
178
|
+
the next, you have two options.
|
|
179
|
+
|
|
180
|
+
**Explicit props (most type-safe).** Give each construct exactly what it needs:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const vpc = Vpc.fromLookup(this, 'vpc', { vpcId: conf.values.vpcId });
|
|
184
|
+
const db = new MyDb(this, 'db', { vpc, conf });
|
|
185
|
+
new MyBackup(this, 'backup', { dbCluster: db.cluster });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**`StackResources` (one bag, fail-loud).** If you prefer threading a single object
|
|
189
|
+
through every construct, use the exported `StackResources` container. Unlike a plain
|
|
190
|
+
object, reading a resource that hasn't been created yet throws a clear error instead of
|
|
191
|
+
yielding `undefined` — valuable in JavaScript stacks where there's no compiler to catch it:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
import { Config, StackResources } from 'aws-cdk-conf-n-tags';
|
|
195
|
+
|
|
196
|
+
const res = new StackResources();
|
|
197
|
+
res.set('vpc', Vpc.fromLookup(this, 'vpc', { vpcId: conf.values.vpcId }));
|
|
198
|
+
const db = res.set('db', new MyDb(this, 'db', { conf, res })); // set() returns the value
|
|
199
|
+
|
|
200
|
+
new MyBackup(this, 'backup', { conf, res }); // internally reads res.get('db').cluster
|
|
201
|
+
|
|
202
|
+
// res.get('cache') before the cache is created throws:
|
|
203
|
+
// Resource "cache" was requested before it was created. Check the order ...
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`set(key, value)` stores and returns the value; `get(key)` returns it or throws;
|
|
207
|
+
`has(key)` checks; `keys()` lists what's been set. In TypeScript, parameterize it for
|
|
208
|
+
typed keys and values: `new StackResources<{ vpc: IVpc; db: MyDb }>()`.
|
|
209
|
+
|
|
210
|
+
## API
|
|
211
|
+
|
|
212
|
+
### `Config.load(scope, schema, options?)`
|
|
213
|
+
|
|
214
|
+
Returns a `Config` with:
|
|
215
|
+
|
|
216
|
+
| Member | Description |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `conf.values` | The validated, frozen config (typed as `z.infer<typeof schema>`). |
|
|
219
|
+
| `conf.environment` | The resolved environment name. |
|
|
220
|
+
| `conf.path` | Absolute path of the file that was loaded. |
|
|
221
|
+
| `conf.tagStack(scope, options?)` | Tag a stack using this config's `tags` + auto-tags. |
|
|
222
|
+
|
|
223
|
+
`options` (all optional):
|
|
224
|
+
|
|
225
|
+
| Option | Default | Description |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `contextKey` | `'env'` | CDK context key holding the environment name. |
|
|
228
|
+
| `environment` | — | Provide the env name explicitly (skips context). |
|
|
229
|
+
| `configDir` | `'config'` | Directory holding `<env>.yaml`, relative to `cwd`. |
|
|
230
|
+
| `configPath` | — | Explicit file path (overrides `configDir`). |
|
|
231
|
+
| `cwd` | `process.cwd()` | Base directory for relative paths. |
|
|
232
|
+
| `unknownKeys` | `'strict'` | `'strict'` \| `'strip'` \| `'passthrough'`. |
|
|
233
|
+
| `verifyAccountRegion` | `true` | Cross-check account/region. |
|
|
234
|
+
| `account` / `region` | `CDK_DEFAULT_*` | Expected account/region. |
|
|
235
|
+
|
|
236
|
+
### `tagStack(scope, options?)`
|
|
237
|
+
|
|
238
|
+
Standalone tagging (no `Config` needed). Applies the working-directory tag, the
|
|
239
|
+
created-by tag, then the explicit `tags` map, to every taggable resource in `scope`.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { tagStack } from 'aws-cdk-conf-n-tags';
|
|
243
|
+
|
|
244
|
+
tagStack(stack, {
|
|
245
|
+
tags: { Product: 'my-product', Env: 'Production' },
|
|
246
|
+
auto: { workDir: false }, // disable individual auto-tags
|
|
247
|
+
tagNames: { createdBy: 'Blame' }, // rename auto-tag keys
|
|
248
|
+
providers: { createdBy: () => 'ci-bot' }, // override how a value is computed
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
By default `CreatedBy` is derived from `git config user.name` / `user.email` (falling
|
|
253
|
+
back to `$USER`), and `WorkDir` is `process.cwd()`.
|
|
254
|
+
|
|
255
|
+
### `StackResources<T>`
|
|
256
|
+
|
|
257
|
+
A fail-loud container for resources created during synthesis — see
|
|
258
|
+
[Sharing resources between constructs](#sharing-resources-between-constructs).
|
|
259
|
+
|
|
260
|
+
| Method | Description |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `set(key, value)` | Store a resource and return it. |
|
|
263
|
+
| `get(key)` | Return it, or throw if it was never set. |
|
|
264
|
+
| `has(key)` | Whether a resource has been set. |
|
|
265
|
+
| `keys()` | The keys set so far. |
|
|
266
|
+
|
|
267
|
+
### `loadConfig(schema, options)`
|
|
268
|
+
|
|
269
|
+
The validation core without any CDK scope — useful for tests or non-CDK tooling.
|
|
270
|
+
Returns `{ values, path }`. See `LoadOptions` for the option shape.
|
|
271
|
+
|
|
272
|
+
## Migrating from the original `Config` class
|
|
273
|
+
|
|
274
|
+
If you used the original JavaScript `Config` (which flattened every YAML key onto the
|
|
275
|
+
instance and doubled as a mutable dependency bag):
|
|
276
|
+
|
|
277
|
+
- Read config via `conf.values.<key>` instead of `conf.<key>`.
|
|
278
|
+
- Declare a zod schema instead of relying on scattered `|| default` fallbacks.
|
|
279
|
+
- Stop writing resources back onto the config object (`conf.vpc = vpc`); pass them to
|
|
280
|
+
constructs explicitly.
|
|
281
|
+
- `setTags(stack)` → `conf.tagStack(stack)`.
|
|
282
|
+
|
|
283
|
+
## Development
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
npm install
|
|
287
|
+
npm run build # tsup → dist (ESM + CJS + d.ts/d.cts)
|
|
288
|
+
npm test # vitest
|
|
289
|
+
npm run typecheck # tsc --noEmit
|
|
290
|
+
npm run check:exports # publint + attw
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Run a single test: `npx vitest run test/loader.test.ts` (or `-t "<name>"`).
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
BSD-3-Clause © Eduard Moskvin
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BaseConfigSchema: () => BaseConfigSchema,
|
|
34
|
+
Config: () => Config,
|
|
35
|
+
StackResources: () => StackResources,
|
|
36
|
+
gitCreatedBy: () => gitCreatedBy,
|
|
37
|
+
loadConfig: () => loadConfig,
|
|
38
|
+
resolveConfigPath: () => resolveConfigPath,
|
|
39
|
+
tagStack: () => tagStack,
|
|
40
|
+
tagValueSchema: () => tagValueSchema
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(index_exports);
|
|
43
|
+
|
|
44
|
+
// src/config.ts
|
|
45
|
+
var import_zod2 = require("zod");
|
|
46
|
+
|
|
47
|
+
// src/loader.ts
|
|
48
|
+
var import_node_fs = require("fs");
|
|
49
|
+
var import_node_path = require("path");
|
|
50
|
+
var import_yaml = __toESM(require("yaml"), 1);
|
|
51
|
+
var import_zod = require("zod");
|
|
52
|
+
function resolveConfigPath(opts) {
|
|
53
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
54
|
+
if (opts.configPath) {
|
|
55
|
+
return (0, import_node_path.isAbsolute)(opts.configPath) ? opts.configPath : (0, import_node_path.join)(cwd, opts.configPath);
|
|
56
|
+
}
|
|
57
|
+
return (0, import_node_path.join)(cwd, opts.configDir ?? "config", `${opts.environment}.yaml`);
|
|
58
|
+
}
|
|
59
|
+
function withUnknownKeys(schema, policy) {
|
|
60
|
+
if (policy === "strip") return schema;
|
|
61
|
+
if (schema instanceof import_zod.z.ZodObject) {
|
|
62
|
+
return policy === "strict" ? schema.strict() : schema.passthrough();
|
|
63
|
+
}
|
|
64
|
+
if (schema instanceof import_zod.z.ZodEffects) {
|
|
65
|
+
const inner = withUnknownKeys(schema.innerType(), policy);
|
|
66
|
+
return new import_zod.z.ZodEffects({ ...schema._def, schema: inner });
|
|
67
|
+
}
|
|
68
|
+
return schema;
|
|
69
|
+
}
|
|
70
|
+
function verifyAccountRegion(values, options, path) {
|
|
71
|
+
const account = options.account ?? process.env.CDK_DEFAULT_ACCOUNT;
|
|
72
|
+
const region = options.region ?? process.env.CDK_DEFAULT_REGION;
|
|
73
|
+
if (account && typeof values.awsAccount === "string" && values.awsAccount !== account) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`AWS account mismatch for environment "${options.environment}": ${path} expects "${values.awsAccount}" but the active credentials resolve to "${account}". Check ${path} and your AWS CLI settings.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (region && typeof values.awsRegion === "string" && values.awsRegion !== region) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`AWS region mismatch for environment "${options.environment}": ${path} expects "${values.awsRegion}" but the active credentials resolve to "${region}". Check ${path} and your AWS CLI settings.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function loadConfig(schema, options) {
|
|
85
|
+
const path = resolveConfigPath(options);
|
|
86
|
+
let raw;
|
|
87
|
+
try {
|
|
88
|
+
raw = (0, import_node_fs.readFileSync)(path, "utf8");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Could not read config file for environment "${options.environment}" at ${path}: ${err.message}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = import_yaml.default.parse(raw) ?? {};
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Could not parse YAML config for environment "${options.environment}" at ${path}: ${err.message}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const result = withUnknownKeys(schema, options.unknownKeys ?? "strict").safeParse(parsed);
|
|
103
|
+
if (!result.success) {
|
|
104
|
+
const details = result.error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
105
|
+
throw new Error(`Invalid configuration in ${path}:
|
|
106
|
+
${details}`);
|
|
107
|
+
}
|
|
108
|
+
if (options.verifyAccountRegion !== false) {
|
|
109
|
+
verifyAccountRegion(result.data, options, path);
|
|
110
|
+
}
|
|
111
|
+
return { values: result.data, path };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/tags.ts
|
|
115
|
+
var import_node_child_process = require("child_process");
|
|
116
|
+
var import_aws_cdk_lib = require("aws-cdk-lib");
|
|
117
|
+
var DEFAULT_CREATED_BY_TAG = "CreatedBy";
|
|
118
|
+
var DEFAULT_WORK_DIR_TAG = "WorkDir";
|
|
119
|
+
function gitCreatedBy() {
|
|
120
|
+
let name = "";
|
|
121
|
+
let email = "";
|
|
122
|
+
try {
|
|
123
|
+
name = (0, import_node_child_process.execSync)("git config user.name").toString().trim();
|
|
124
|
+
email = (0, import_node_child_process.execSync)("git config user.email").toString().trim();
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
const joined = name && email ? `${name} - ${email}` : name || email;
|
|
128
|
+
return joined || process.env.USER || void 0;
|
|
129
|
+
}
|
|
130
|
+
function tagStack(scope, options = {}) {
|
|
131
|
+
const { tags, auto = {}, tagNames = {}, providers = {} } = options;
|
|
132
|
+
const collected = [];
|
|
133
|
+
if (auto.workDir !== false) {
|
|
134
|
+
const value = (providers.workDir ?? (() => process.cwd()))();
|
|
135
|
+
if (value) collected.push({ key: tagNames.workDir ?? DEFAULT_WORK_DIR_TAG, value });
|
|
136
|
+
}
|
|
137
|
+
if (auto.createdBy !== false) {
|
|
138
|
+
const value = (providers.createdBy ?? gitCreatedBy)();
|
|
139
|
+
if (value) collected.push({ key: tagNames.createdBy ?? DEFAULT_CREATED_BY_TAG, value });
|
|
140
|
+
}
|
|
141
|
+
if (tags && typeof tags === "object" && !Array.isArray(tags)) {
|
|
142
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
143
|
+
if (value === void 0 || value === null) continue;
|
|
144
|
+
collected.push({ key, value: String(value) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const { key, value } of collected) {
|
|
148
|
+
import_aws_cdk_lib.Tags.of(scope).add(key, value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/config.ts
|
|
153
|
+
function deepFreeze(value) {
|
|
154
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
155
|
+
Object.freeze(value);
|
|
156
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function resolveEnvironment(scope, options) {
|
|
161
|
+
if (options.environment) return options.environment;
|
|
162
|
+
const key = options.contextKey ?? "env";
|
|
163
|
+
const fromContext = scope.node.tryGetContext(key);
|
|
164
|
+
if (typeof fromContext !== "string" || fromContext.length === 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Deployment environment not set. Pass it with \`--context ${key}=<environment>\` or via the \`environment\` option.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return fromContext;
|
|
170
|
+
}
|
|
171
|
+
var Config = class _Config {
|
|
172
|
+
/** The environment name (e.g. from `--context env=...`). */
|
|
173
|
+
environment;
|
|
174
|
+
/** The validated, frozen config values. */
|
|
175
|
+
values;
|
|
176
|
+
/** Absolute path of the config file that was loaded. */
|
|
177
|
+
path;
|
|
178
|
+
constructor(environment, values, path) {
|
|
179
|
+
this.environment = environment;
|
|
180
|
+
this.values = deepFreeze(values);
|
|
181
|
+
this.path = path;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the environment from `scope`'s context, then load and validate its
|
|
185
|
+
* config against `schema` (which should extend {@link BaseConfigSchema}).
|
|
186
|
+
*/
|
|
187
|
+
static load(scope, schema, options = {}) {
|
|
188
|
+
const environment = resolveEnvironment(scope, options);
|
|
189
|
+
const { values, path } = loadConfig(schema, {
|
|
190
|
+
environment,
|
|
191
|
+
configDir: options.configDir,
|
|
192
|
+
configPath: options.configPath,
|
|
193
|
+
cwd: options.cwd,
|
|
194
|
+
unknownKeys: options.unknownKeys,
|
|
195
|
+
verifyAccountRegion: options.verifyAccountRegion,
|
|
196
|
+
account: options.account,
|
|
197
|
+
region: options.region
|
|
198
|
+
});
|
|
199
|
+
return new _Config(environment, values, path);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Tag every taggable resource in `scope` (the stack) using this config's `tags`
|
|
203
|
+
* and the configured auto-tag key names. Pass `options` to toggle auto-tags or
|
|
204
|
+
* override providers.
|
|
205
|
+
*/
|
|
206
|
+
tagStack(scope, options = {}) {
|
|
207
|
+
const base = this.values;
|
|
208
|
+
tagStack(scope, {
|
|
209
|
+
...options,
|
|
210
|
+
// Merge per-key so a partial override (e.g. only `tags.Extra` or only
|
|
211
|
+
// `tagNames.createdBy`) layers on top of the config instead of replacing
|
|
212
|
+
// the whole map and silently dropping the config's other values.
|
|
213
|
+
tags: { ...base.tags, ...options.tags },
|
|
214
|
+
tagNames: {
|
|
215
|
+
createdBy: options.tagNames?.createdBy ?? base.createdByTagName,
|
|
216
|
+
workDir: options.tagNames?.workDir ?? base.cwdTagName
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/resources.ts
|
|
223
|
+
var StackResources = class {
|
|
224
|
+
#refs = /* @__PURE__ */ new Map();
|
|
225
|
+
/** Store a resource and return it, so it can be captured inline. */
|
|
226
|
+
set(key, value) {
|
|
227
|
+
this.#refs.set(key, value);
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
/** Retrieve a resource, throwing a clear error if it was never set. */
|
|
231
|
+
get(key) {
|
|
232
|
+
if (!this.#refs.has(key)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Resource "${String(key)}" was requested before it was created. Check the order constructs are created in the stack.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return this.#refs.get(key);
|
|
238
|
+
}
|
|
239
|
+
/** Whether a resource has been set. */
|
|
240
|
+
has(key) {
|
|
241
|
+
return this.#refs.has(key);
|
|
242
|
+
}
|
|
243
|
+
/** The keys set so far. */
|
|
244
|
+
keys() {
|
|
245
|
+
return [...this.#refs.keys()];
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/schema.ts
|
|
250
|
+
var import_zod3 = require("zod");
|
|
251
|
+
var tagValueSchema = import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number(), import_zod3.z.boolean()]);
|
|
252
|
+
var BaseConfigSchema = import_zod3.z.object({
|
|
253
|
+
/** Target AWS account ID. Quote it in YAML (`"012345678901"`) to preserve leading zeros. */
|
|
254
|
+
awsAccount: import_zod3.z.string().min(1),
|
|
255
|
+
/** Target AWS region, e.g. `us-east-1`. */
|
|
256
|
+
awsRegion: import_zod3.z.string().min(1),
|
|
257
|
+
/** Arbitrary tags applied to every resource in the stack. */
|
|
258
|
+
tags: import_zod3.z.record(import_zod3.z.string(), tagValueSchema).optional(),
|
|
259
|
+
/** Tag key used for the auto-generated "created by" tag. */
|
|
260
|
+
createdByTagName: import_zod3.z.string().default("CreatedBy"),
|
|
261
|
+
/** Tag key used for the auto-generated "working directory" tag. */
|
|
262
|
+
cwdTagName: import_zod3.z.string().default("WorkDir")
|
|
263
|
+
});
|
|
264
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
265
|
+
0 && (module.exports = {
|
|
266
|
+
BaseConfigSchema,
|
|
267
|
+
Config,
|
|
268
|
+
StackResources,
|
|
269
|
+
gitCreatedBy,
|
|
270
|
+
loadConfig,
|
|
271
|
+
resolveConfigPath,
|
|
272
|
+
tagStack,
|
|
273
|
+
tagValueSchema
|
|
274
|
+
});
|
|
275
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/loader.ts","../src/tags.ts","../src/resources.ts","../src/schema.ts"],"sourcesContent":["export { Config, type ConfigOptions } from './config.js';\nexport { StackResources } from './resources.js';\nexport { BaseConfigSchema, tagValueSchema, type BaseConfig } from './schema.js';\nexport {\n loadConfig,\n resolveConfigPath,\n type LoadOptions,\n type UnknownKeys,\n} from './loader.js';\nexport {\n tagStack,\n gitCreatedBy,\n type TagStackOptions,\n type TagValue,\n} from './tags.js';\n","import { z } from 'zod';\nimport type { IConstruct } from 'constructs';\nimport { loadConfig, type UnknownKeys } from './loader.js';\nimport { tagStack as applyStackTags, type TagStackOptions } from './tags.js';\nimport type { BaseConfig } from './schema.js';\n\n/** Options for {@link Config.load}. */\nexport interface ConfigOptions {\n /** CDK context key holding the environment name. Default: `env`. */\n contextKey?: string;\n /** Provide the environment name explicitly instead of reading it from context. */\n environment?: string;\n /** Directory holding `<environment>.yaml`, relative to `cwd`. Default: `config`. */\n configDir?: string;\n /** Explicit config file path (absolute, or relative to `cwd`). Overrides `configDir`. */\n configPath?: string;\n /** Base directory for relative paths. Default: `process.cwd()`. */\n cwd?: string;\n /** Unknown-key policy. Default: `strict` (reject mistyped keys). */\n unknownKeys?: UnknownKeys;\n /** Cross-check `awsAccount`/`awsRegion` against the active credentials. Default: `true`. */\n verifyAccountRegion?: boolean;\n /** Expected account. Default: `process.env.CDK_DEFAULT_ACCOUNT`. */\n account?: string;\n /** Expected region. Default: `process.env.CDK_DEFAULT_REGION`. */\n region?: string;\n}\n\n/** Recursively freeze an object and everything reachable from it. */\nfunction deepFreeze<T>(value: T): T {\n if (value && typeof value === 'object' && !Object.isFrozen(value)) {\n Object.freeze(value);\n for (const nested of Object.values(value)) deepFreeze(nested);\n }\n return value;\n}\n\nfunction resolveEnvironment(scope: IConstruct, options: ConfigOptions): string {\n if (options.environment) return options.environment;\n const key = options.contextKey ?? 'env';\n const fromContext: unknown = scope.node.tryGetContext(key);\n if (typeof fromContext !== 'string' || fromContext.length === 0) {\n throw new Error(\n `Deployment environment not set. Pass it with \\`--context ${key}=<environment>\\` ` +\n `or via the \\`environment\\` option.`,\n );\n }\n return fromContext;\n}\n\n/**\n * Loaded, validated, immutable per-environment configuration.\n *\n * Build it with {@link Config.load}, which resolves the environment from CDK\n * context, reads and validates `config/<env>.yaml`, and (by default) verifies the\n * target account/region. The validated values are frozen — this object is **not**\n * a place to stash resource references; pass those between constructs explicitly.\n */\nexport class Config<V extends BaseConfig> {\n /** The environment name (e.g. from `--context env=...`). */\n readonly environment: string;\n /** The validated, frozen config values. */\n readonly values: Readonly<V>;\n /** Absolute path of the config file that was loaded. */\n readonly path: string;\n\n private constructor(environment: string, values: V, path: string) {\n this.environment = environment;\n this.values = deepFreeze(values);\n this.path = path;\n }\n\n /**\n * Resolve the environment from `scope`'s context, then load and validate its\n * config against `schema` (which should extend {@link BaseConfigSchema}).\n */\n static load<S extends z.ZodTypeAny>(\n scope: IConstruct,\n schema: S,\n options: ConfigOptions = {},\n ): Config<z.infer<S> & BaseConfig> {\n const environment = resolveEnvironment(scope, options);\n const { values, path } = loadConfig(schema, {\n environment,\n configDir: options.configDir,\n configPath: options.configPath,\n cwd: options.cwd,\n unknownKeys: options.unknownKeys,\n verifyAccountRegion: options.verifyAccountRegion,\n account: options.account,\n region: options.region,\n });\n return new Config(environment, values as z.infer<S> & BaseConfig, path);\n }\n\n /**\n * Tag every taggable resource in `scope` (the stack) using this config's `tags`\n * and the configured auto-tag key names. Pass `options` to toggle auto-tags or\n * override providers.\n */\n tagStack(scope: IConstruct, options: TagStackOptions = {}): void {\n const base = this.values as BaseConfig;\n applyStackTags(scope, {\n ...options,\n // Merge per-key so a partial override (e.g. only `tags.Extra` or only\n // `tagNames.createdBy`) layers on top of the config instead of replacing\n // the whole map and silently dropping the config's other values.\n tags: { ...base.tags, ...options.tags },\n tagNames: {\n createdBy: options.tagNames?.createdBy ?? base.createdByTagName,\n workDir: options.tagNames?.workDir ?? base.cwdTagName,\n },\n });\n }\n}\n","import { readFileSync } from 'node:fs';\nimport { isAbsolute, join } from 'node:path';\nimport YAML from 'yaml';\nimport { z } from 'zod';\n\n/**\n * How unknown (typically mistyped) keys in the config file are handled:\n * - `strict` — reject them with an error (default; catches typos loudly);\n * - `strip` — silently drop them (zod's default object behavior);\n * - `passthrough` — keep them on the result without validation.\n */\nexport type UnknownKeys = 'strict' | 'strip' | 'passthrough';\n\n/** Inputs for {@link loadConfig}. */\nexport interface LoadOptions {\n /** Environment name, used to resolve the config file and for error messages. */\n environment: string;\n /** Directory holding `<environment>.yaml`, relative to `cwd`. Default: `config`. */\n configDir?: string;\n /** Explicit config file path (absolute, or relative to `cwd`). Overrides `configDir`. */\n configPath?: string;\n /** Base directory for relative paths. Default: `process.cwd()`. */\n cwd?: string;\n /** Unknown-key policy. Default: `strict`. */\n unknownKeys?: UnknownKeys;\n /** Cross-check `awsAccount`/`awsRegion` against the active credentials. Default: `true`. */\n verifyAccountRegion?: boolean;\n /** Expected account. Default: `process.env.CDK_DEFAULT_ACCOUNT`. */\n account?: string;\n /** Expected region. Default: `process.env.CDK_DEFAULT_REGION`. */\n region?: string;\n}\n\n/** Resolve the absolute path of the config file for an environment. */\nexport function resolveConfigPath(opts: {\n environment: string;\n configDir?: string;\n configPath?: string;\n cwd?: string;\n}): string {\n const cwd = opts.cwd ?? process.cwd();\n if (opts.configPath) {\n return isAbsolute(opts.configPath) ? opts.configPath : join(cwd, opts.configPath);\n }\n return join(cwd, opts.configDir ?? 'config', `${opts.environment}.yaml`);\n}\n\nfunction withUnknownKeys<S extends z.ZodTypeAny>(schema: S, policy: UnknownKeys): S {\n if (policy === 'strip') return schema; // zod's default object behavior\n if (schema instanceof z.ZodObject) {\n return (policy === 'strict' ? schema.strict() : schema.passthrough()) as unknown as S;\n }\n // Unwrap `.refine()` / `.superRefine()` / `.transform()` so the policy still\n // reaches the underlying object schema. Without this, a refined schema would\n // silently fall through to zod's default strip and never reject typos.\n if (schema instanceof z.ZodEffects) {\n const inner = withUnknownKeys(schema.innerType(), policy);\n return new z.ZodEffects({ ...schema._def, schema: inner }) as unknown as S;\n }\n return schema;\n}\n\nfunction verifyAccountRegion(\n values: { awsAccount?: unknown; awsRegion?: unknown },\n options: LoadOptions,\n path: string,\n): void {\n const account = options.account ?? process.env.CDK_DEFAULT_ACCOUNT;\n const region = options.region ?? process.env.CDK_DEFAULT_REGION;\n\n // Only compare when there is a reference to compare against; an unset\n // CDK_DEFAULT_ACCOUNT/REGION (common in `cdk synth` and unit tests) means we\n // cannot verify, not that the config is wrong.\n if (account && typeof values.awsAccount === 'string' && values.awsAccount !== account) {\n throw new Error(\n `AWS account mismatch for environment \"${options.environment}\": ${path} expects ` +\n `\"${values.awsAccount}\" but the active credentials resolve to \"${account}\". ` +\n `Check ${path} and your AWS CLI settings.`,\n );\n }\n if (region && typeof values.awsRegion === 'string' && values.awsRegion !== region) {\n throw new Error(\n `AWS region mismatch for environment \"${options.environment}\": ${path} expects ` +\n `\"${values.awsRegion}\" but the active credentials resolve to \"${region}\". ` +\n `Check ${path} and your AWS CLI settings.`,\n );\n }\n}\n\n/**\n * Read, parse, and validate the YAML config for an environment.\n *\n * Standalone (no CDK scope needed) so it can be unit-tested and reused outside a\n * stack. {@link Config.load} wraps this with environment resolution from context.\n */\nexport function loadConfig<S extends z.ZodTypeAny>(\n schema: S,\n options: LoadOptions,\n): { values: z.infer<S>; path: string } {\n const path = resolveConfigPath(options);\n\n let raw: string;\n try {\n raw = readFileSync(path, 'utf8');\n } catch (err) {\n throw new Error(\n `Could not read config file for environment \"${options.environment}\" at ${path}: ` +\n `${(err as Error).message}`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = YAML.parse(raw) ?? {};\n } catch (err) {\n throw new Error(\n `Could not parse YAML config for environment \"${options.environment}\" at ${path}: ` +\n `${(err as Error).message}`,\n );\n }\n\n const result = withUnknownKeys(schema, options.unknownKeys ?? 'strict').safeParse(parsed);\n if (!result.success) {\n const details = result.error.issues\n .map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`)\n .join('\\n');\n throw new Error(`Invalid configuration in ${path}:\\n${details}`);\n }\n\n if (options.verifyAccountRegion !== false) {\n verifyAccountRegion(result.data as Record<string, unknown>, options, path);\n }\n\n return { values: result.data as z.infer<S>, path };\n}\n","import { execSync } from 'node:child_process';\nimport { Tags } from 'aws-cdk-lib';\nimport type { IConstruct } from 'constructs';\n\n/** A tag value as authored in config; coerced to a string when applied. */\nexport type TagValue = string | number | boolean;\n\n/** Options for {@link tagStack}. */\nexport interface TagStackOptions {\n /** Arbitrary key/value tags to apply (e.g. from config `tags:`). */\n tags?: Record<string, TagValue>;\n /** Toggle the auto-generated tags. Both default to `true`. */\n auto?: {\n /** Add a \"created by\" tag derived from the git/OS user. */\n createdBy?: boolean;\n /** Add a \"working directory\" tag with the current working directory. */\n workDir?: boolean;\n };\n /** Override the keys used for the auto-generated tags. */\n tagNames?: {\n createdBy?: string;\n workDir?: string;\n };\n /**\n * Override how the auto-tag values are computed. Injectable so tests don't\n * shell out to git and stacks can supply their own provenance.\n */\n providers?: {\n createdBy?: () => string | undefined;\n workDir?: () => string | undefined;\n };\n}\n\nconst DEFAULT_CREATED_BY_TAG = 'CreatedBy';\nconst DEFAULT_WORK_DIR_TAG = 'WorkDir';\n\n/**\n * Default \"created by\" provider: the git `user.name - user.email` identity,\n * falling back to `$USER`. Returns `undefined` if nothing is available.\n */\nexport function gitCreatedBy(): string | undefined {\n let name = '';\n let email = '';\n try {\n name = execSync('git config user.name').toString().trim();\n email = execSync('git config user.email').toString().trim();\n } catch {\n // No git identity available; fall back below.\n }\n const joined = name && email ? `${name} - ${email}` : name || email;\n return joined || process.env.USER || undefined;\n}\n\n/**\n * Apply tags to every taggable resource in `scope` (typically a Stack) via CDK's\n * tag aspect. Merges, in order: the working-directory tag, the created-by tag,\n * then the explicit `tags` map. Non-string tag values are coerced with `String()`.\n */\nexport function tagStack(scope: IConstruct, options: TagStackOptions = {}): void {\n const { tags, auto = {}, tagNames = {}, providers = {} } = options;\n const collected: Array<{ key: string; value: string }> = [];\n\n if (auto.workDir !== false) {\n const value = (providers.workDir ?? (() => process.cwd()))();\n if (value) collected.push({ key: tagNames.workDir ?? DEFAULT_WORK_DIR_TAG, value });\n }\n\n if (auto.createdBy !== false) {\n const value = (providers.createdBy ?? gitCreatedBy)();\n if (value) collected.push({ key: tagNames.createdBy ?? DEFAULT_CREATED_BY_TAG, value });\n }\n\n if (tags && typeof tags === 'object' && !Array.isArray(tags)) {\n for (const [key, value] of Object.entries(tags)) {\n if (value === undefined || value === null) continue;\n collected.push({ key, value: String(value) });\n }\n }\n\n for (const { key, value } of collected) {\n Tags.of(scope).add(key, value);\n }\n}\n","/**\n * A small, mutable container for resources created during stack synthesis, kept\n * separate from the frozen {@link Config} values.\n *\n * It lets you thread a single object through your constructs (the convenience of a\n * shared dependency bag) without the classic footgun: reading a resource that has\n * not been created yet throws a clear error instead of silently returning\n * `undefined`. That matters most in JavaScript stacks, where there is no compiler to\n * catch a missing reference.\n *\n * ```ts\n * const res = new StackResources<{ vpc: IVpc; db: MyDb }>();\n * res.set('vpc', Vpc.fromLookup(this, 'vpc', { vpcId: conf.values.vpcId }));\n * const db = res.set('db', new MyDb(this, 'db', { conf, res }));\n * // later: res.get('db').cluster — throws if 'db' was never set\n * ```\n */\nexport class StackResources<T extends Record<string, unknown> = Record<string, unknown>> {\n #refs = new Map<keyof T, T[keyof T]>();\n\n /** Store a resource and return it, so it can be captured inline. */\n set<K extends keyof T>(key: K, value: T[K]): T[K] {\n this.#refs.set(key, value);\n return value;\n }\n\n /** Retrieve a resource, throwing a clear error if it was never set. */\n get<K extends keyof T>(key: K): T[K] {\n if (!this.#refs.has(key)) {\n throw new Error(\n `Resource \"${String(key)}\" was requested before it was created. ` +\n `Check the order constructs are created in the stack.`,\n );\n }\n return this.#refs.get(key) as T[K];\n }\n\n /** Whether a resource has been set. */\n has<K extends keyof T>(key: K): boolean {\n return this.#refs.has(key);\n }\n\n /** The keys set so far. */\n keys(): Array<keyof T> {\n return [...this.#refs.keys()];\n }\n}\n","import { z } from 'zod';\n\n/**\n * A single tag value as it may appear in YAML. CloudFormation tag values must\n * ultimately be strings; numbers and booleans are coerced at tag-apply time\n * (see {@link tagStack}) so config authors can write `Env: 1` or `Managed: true`.\n */\nexport const tagValueSchema = z.union([z.string(), z.number(), z.boolean()]);\n\n/**\n * The keys every environment config shares. Consumers build their own schema by\n * extending this with the parameters their stack needs:\n *\n * ```ts\n * const ConfigSchema = BaseConfigSchema.extend({\n * vpcId: z.string(),\n * asgMinCapacity: z.number().int().default(1),\n * });\n * ```\n */\nexport const BaseConfigSchema = z.object({\n /** Target AWS account ID. Quote it in YAML (`\"012345678901\"`) to preserve leading zeros. */\n awsAccount: z.string().min(1),\n /** Target AWS region, e.g. `us-east-1`. */\n awsRegion: z.string().min(1),\n /** Arbitrary tags applied to every resource in the stack. */\n tags: z.record(z.string(), tagValueSchema).optional(),\n /** Tag key used for the auto-generated \"created by\" tag. */\n createdByTagName: z.string().default('CreatedBy'),\n /** Tag key used for the auto-generated \"working directory\" tag. */\n cwdTagName: z.string().default('WorkDir'),\n});\n\n/** The validated shape produced by {@link BaseConfigSchema}. */\nexport type BaseConfig = z.infer<typeof BaseConfigSchema>;\n"],"mappings":"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","names":["import_zod","YAML","import_zod"]}
|