@codedrifters/constructs 0.0.60 → 0.0.62
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 +3 -453
- package/lib/index.d.mts +19 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +4 -1
- package/lib/index.mjs.map +1 -1
- package/lib/static-hosting.viewer-request-handler.d.mts +39 -17
- package/lib/static-hosting.viewer-request-handler.d.ts +39 -17
- package/lib/static-hosting.viewer-request-handler.js +19 -6
- package/lib/static-hosting.viewer-request-handler.js.map +1 -1
- package/lib/static-hosting.viewer-request-handler.mjs +17 -5
- package/lib/static-hosting.viewer-request-handler.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,457 +1,7 @@
|
|
|
1
1
|
# @codedrifters/constructs
|
|
2
2
|
|
|
3
|
-
A collection of reusable AWS CDK constructs
|
|
3
|
+
A collection of reusable AWS CDK constructs for common infrastructure patterns. Pre-configured, secure, and production-ready building blocks for static hosting (`StaticHosting`, `StaticContent`), S3 (`PrivateBucket`), and more.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Documentation:** See the [@codedrifters/constructs docs](../../../docs/src/content/docs/packages/@codedrifters/constructs/index.md) in the monorepo's Starlight site.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- [Installation](#installation)
|
|
9
|
-
- [Constructs](#constructs)
|
|
10
|
-
- [S3 Constructs](#s3-constructs)
|
|
11
|
-
- [PrivateBucket](#privatebucket)
|
|
12
|
-
- [Static Hosting Constructs](#static-hosting-constructs)
|
|
13
|
-
- [StaticHosting](#statichosting)
|
|
14
|
-
- [StaticContent](#staticcontent)
|
|
15
|
-
- [Complete Example](#complete-example)
|
|
16
|
-
- [API Reference](#api-reference)
|
|
17
|
-
- [Further Documentation](#further-documentation)
|
|
18
|
-
|
|
19
|
-
## What are AWS CDK Constructs?
|
|
20
|
-
|
|
21
|
-
AWS CDK (Cloud Development Kit) constructs are reusable cloud components that encapsulate AWS resources and their configuration. Think of them as building blocks that combine multiple AWS services into higher-level abstractions.
|
|
22
|
-
|
|
23
|
-
For example, instead of manually configuring an S3 bucket, CloudFront distribution, Route53 records, and SSL certificates separately, a construct can combine all of these into a single "StaticHosting" construct that you can use with just a few lines of code.
|
|
24
|
-
|
|
25
|
-
**Key Benefits:**
|
|
26
|
-
- **Reusability**: Write once, use everywhere
|
|
27
|
-
- **Consistency**: Enforce best practices and security defaults
|
|
28
|
-
- **Simplicity**: Complex infrastructure becomes simple API calls
|
|
29
|
-
- **Type Safety**: Full TypeScript support with IntelliSense
|
|
30
|
-
|
|
31
|
-
For more information about AWS CDK constructs, see the [AWS CDK Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html).
|
|
32
|
-
|
|
33
|
-
## Installation
|
|
34
|
-
|
|
35
|
-
Add the package to your CDK project using Configulator/Projen configuration. This ensures consistent dependency management across your project.
|
|
36
|
-
|
|
37
|
-
> **Note:** Always configure dependencies through Projen configuration files, never by manually running `npm install`, `pnpm add`, or `yarn add`. Manual installation will create conflicts with Projen-managed files.
|
|
38
|
-
|
|
39
|
-
### In a Monorepo (Recommended)
|
|
40
|
-
|
|
41
|
-
If you're using `@codedrifters/configulator` in a monorepo, add the package as a dependency in your sub-project configuration:
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { TypeScriptProject } from '@codedrifters/configulator';
|
|
45
|
-
import { MonorepoProject } from '@codedrifters/configulator';
|
|
46
|
-
|
|
47
|
-
const myCdkProject = new TypeScriptProject({
|
|
48
|
-
name: 'my-cdk-project',
|
|
49
|
-
packageName: '@myorg/my-cdk-project',
|
|
50
|
-
outdir: 'packages/my-cdk-project',
|
|
51
|
-
parent: root, // Your MonorepoProject instance
|
|
52
|
-
|
|
53
|
-
deps: [
|
|
54
|
-
'@codedrifters/constructs',
|
|
55
|
-
],
|
|
56
|
-
|
|
57
|
-
devDeps: [
|
|
58
|
-
'aws-cdk-lib@catalog:', // Catalog versions are pre-configured
|
|
59
|
-
'constructs@catalog:',
|
|
60
|
-
],
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### In a Standalone CDK Project
|
|
65
|
-
|
|
66
|
-
If you're using AWS CDK directly (not via Configulator), add the package to your `deps` array:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { awscdk } from 'projen';
|
|
70
|
-
|
|
71
|
-
const project = new awscdk.AwsCdkTypeScriptApp({
|
|
72
|
-
name: 'my-cdk-app',
|
|
73
|
-
|
|
74
|
-
deps: [
|
|
75
|
-
'@codedrifters/constructs',
|
|
76
|
-
],
|
|
77
|
-
|
|
78
|
-
devDeps: [
|
|
79
|
-
'aws-cdk-lib',
|
|
80
|
-
'constructs',
|
|
81
|
-
],
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### After Adding to Configuration
|
|
86
|
-
|
|
87
|
-
After updating your projenrc configuration file, run:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
npx projen
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
This will update your `package.json` and install the dependencies.
|
|
94
|
-
|
|
95
|
-
### Peer Dependencies
|
|
96
|
-
|
|
97
|
-
This package requires the following peer dependencies:
|
|
98
|
-
|
|
99
|
-
- `aws-cdk-lib` - AWS CDK construct library
|
|
100
|
-
- `constructs` - Core constructs library
|
|
101
|
-
|
|
102
|
-
These should be added as dev dependencies in your project configuration. If you're using `@codedrifters/configulator`, the catalog versions (`@catalog:`) are automatically configured in the root `MonorepoProject`.
|
|
103
|
-
|
|
104
|
-
## Constructs
|
|
105
|
-
|
|
106
|
-
### S3 Constructs
|
|
107
|
-
|
|
108
|
-
#### PrivateBucket
|
|
109
|
-
|
|
110
|
-
A secure S3 bucket with sensible security defaults. This construct extends AWS CDK's `Bucket` construct with security best practices applied by default.
|
|
111
|
-
|
|
112
|
-
**Security Defaults:**
|
|
113
|
-
- Public access is blocked (`BlockPublicAccess.BLOCK_ALL`)
|
|
114
|
-
- SSL/TLS is enforced for all requests
|
|
115
|
-
- Bucket owner enforced object ownership
|
|
116
|
-
- Configurable removal policy (defaults to `RETAIN`)
|
|
117
|
-
|
|
118
|
-
**Basic Usage:**
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
import { PrivateBucket } from '@codedrifters/constructs';
|
|
122
|
-
import { Stack } from 'aws-cdk-lib';
|
|
123
|
-
import { Construct } from 'constructs';
|
|
124
|
-
|
|
125
|
-
const stack = new Stack(app, 'MyStack');
|
|
126
|
-
|
|
127
|
-
// Create a private bucket with default security settings
|
|
128
|
-
const bucket = new PrivateBucket(stack, 'MyPrivateBucket');
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**With Custom Properties:**
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
import { RemovalPolicy } from 'aws-cdk-lib';
|
|
135
|
-
import { PrivateBucket } from '@codedrifters/constructs';
|
|
136
|
-
|
|
137
|
-
const bucket = new PrivateBucket(stack, 'MyPrivateBucket', {
|
|
138
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
139
|
-
autoDeleteObjects: true,
|
|
140
|
-
versioned: true,
|
|
141
|
-
});
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
**Note:** The security settings (public access blocked, SSL enforced, etc.) cannot be overridden - they are always applied to ensure bucket security.
|
|
145
|
-
|
|
146
|
-
### Static Hosting Constructs
|
|
147
|
-
|
|
148
|
-
#### StaticHosting
|
|
149
|
-
|
|
150
|
-
A complete static website hosting solution that creates and configures:
|
|
151
|
-
|
|
152
|
-
- **S3 Bucket**: Private bucket for storing static files (using `PrivateBucket`)
|
|
153
|
-
- **CloudFront Distribution**: Global CDN for fast content delivery
|
|
154
|
-
- **SSL Certificate**: Wildcard certificate via AWS Certificate Manager
|
|
155
|
-
- **Route53 Records**: DNS entries for the domain and wildcard subdomains
|
|
156
|
-
- **Lambda@Edge Function**: Viewer request handler for path rewriting
|
|
157
|
-
- **SSM Parameters**: Stores bucket ARN, distribution domain, and distribution ID for later use
|
|
158
|
-
|
|
159
|
-
**Features:**
|
|
160
|
-
- Automatic wildcard SSL certificate generation
|
|
161
|
-
- Support for custom domains with automatic DNS configuration
|
|
162
|
-
- Lambda@Edge function for intelligent path rewriting
|
|
163
|
-
- Conservative caching policy (60s default TTL)
|
|
164
|
-
- Stores configuration in SSM Parameter Store for use by `StaticContent`
|
|
165
|
-
|
|
166
|
-
**Basic Usage (CloudFront Domain Only):**
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
import { StaticHosting } from '@codedrifters/constructs';
|
|
170
|
-
import { Stack } from 'aws-cdk-lib';
|
|
171
|
-
|
|
172
|
-
const hostingStack = new Stack(app, 'HostingStack', { env });
|
|
173
|
-
|
|
174
|
-
const hosting = new StaticHosting(hostingStack, 'static-hosting', {
|
|
175
|
-
description: 'My static website',
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**With Custom Domain:**
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
import { StaticHosting } from '@codedrifters/constructs';
|
|
183
|
-
import { Stack, RemovalPolicy } from 'aws-cdk-lib';
|
|
184
|
-
|
|
185
|
-
const hostingStack = new Stack(app, 'HostingStack', { env });
|
|
186
|
-
|
|
187
|
-
const hosting = new StaticHosting(hostingStack, 'static-hosting', {
|
|
188
|
-
description: 'My static website',
|
|
189
|
-
staticDomainProps: {
|
|
190
|
-
baseDomain: 'example.com',
|
|
191
|
-
hostedZoneAttributes: {
|
|
192
|
-
hostedZoneId: 'Z1234567890ABC',
|
|
193
|
-
zoneName: 'example.com',
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
privateBucketProps: {
|
|
197
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
198
|
-
autoDeleteObjects: true,
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**Properties:**
|
|
204
|
-
|
|
205
|
-
| Property | Type | Default | Description |
|
|
206
|
-
|----------|------|---------|-------------|
|
|
207
|
-
| `description` | `string` | `undefined` | Short description for traceability |
|
|
208
|
-
| `staticDomainProps` | `StaticDomainProps` | `undefined` | Domain configuration (optional) |
|
|
209
|
-
| `bucketArnParamName` | `string` | `"/STATIC_WEBSITE/BUCKET_ARN"` | SSM parameter name for bucket ARN |
|
|
210
|
-
| `distributionDomainParamName` | `string` | `"/STATIC_WEBSITE/DISTRIBUTION_DOMAIN"` | SSM parameter name for distribution domain |
|
|
211
|
-
| `distributionIDParamName` | `string` | `"/STATIC_WEBSITE/DISTRIBUTION_ID"` | SSM parameter name for distribution ID |
|
|
212
|
-
| `privateBucketProps` | `PrivateBucketProps` | `undefined` | Props to pass to the S3 bucket |
|
|
213
|
-
|
|
214
|
-
**StaticDomainProps:**
|
|
215
|
-
|
|
216
|
-
| Property | Type | Description |
|
|
217
|
-
|----------|------|-------------|
|
|
218
|
-
| `baseDomain` | `string` | The base domain (e.g., `example.com`) |
|
|
219
|
-
| `hostedZoneAttributes` | `HostedZoneAttributes` | Hosted zone ID and zone name |
|
|
220
|
-
|
|
221
|
-
**Outputs:**
|
|
222
|
-
|
|
223
|
-
The construct exposes:
|
|
224
|
-
- `fullDomain: string` - The full domain name (custom domain if provided, otherwise CloudFront domain)
|
|
225
|
-
|
|
226
|
-
**SSM Parameters Created:**
|
|
227
|
-
|
|
228
|
-
The construct automatically creates SSM parameters that can be referenced by `StaticContent`:
|
|
229
|
-
- Bucket ARN (default: `/STATIC_WEBSITE/BUCKET_ARN`)
|
|
230
|
-
- CloudFront Distribution Domain (default: `/STATIC_WEBSITE/DISTRIBUTION_DOMAIN`)
|
|
231
|
-
- CloudFront Distribution ID (default: `/STATIC_WEBSITE/DISTRIBUTION_ID`)
|
|
232
|
-
|
|
233
|
-
#### StaticContent
|
|
234
|
-
|
|
235
|
-
Deploys static content from a local directory to an S3 bucket. This construct is designed to work with `StaticHosting` and supports branch-based deployment paths for PR and feature branch previews.
|
|
236
|
-
|
|
237
|
-
**Features:**
|
|
238
|
-
- Deploys files from a local directory to S3
|
|
239
|
-
- Supports branch-based path prefixes (e.g., `feature-123.example.com/`)
|
|
240
|
-
- Automatically retrieves bucket ARN from SSM Parameter Store
|
|
241
|
-
- Configurable destination directory within the bucket
|
|
242
|
-
|
|
243
|
-
**How Branch-Based Deployment Works:**
|
|
244
|
-
|
|
245
|
-
The construct uses the current git branch name to create unique deployment paths. This allows multiple branches to deploy to the same bucket without conflicts:
|
|
246
|
-
|
|
247
|
-
```
|
|
248
|
-
S3 Bucket Structure:
|
|
249
|
-
├── example.com/ → Production/main branch
|
|
250
|
-
├── feature-123.example.com/ → Feature branch
|
|
251
|
-
├── pr-456.example.com/ → Pull request
|
|
252
|
-
└── stage.example.com/ → Staging branch
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
**Basic Usage:**
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
import { StaticContent, StaticHosting } from '@codedrifters/constructs';
|
|
259
|
-
import { Stack } from 'aws-cdk-lib';
|
|
260
|
-
|
|
261
|
-
// First, create the hosting infrastructure
|
|
262
|
-
const hostingStack = new Stack(app, 'HostingStack', { env });
|
|
263
|
-
const hosting = new StaticHosting(hostingStack, 'static-hosting', {
|
|
264
|
-
staticDomainProps: {
|
|
265
|
-
baseDomain: 'example.com',
|
|
266
|
-
hostedZoneAttributes: {
|
|
267
|
-
hostedZoneId: 'Z1234567890ABC',
|
|
268
|
-
zoneName: 'example.com',
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Then, deploy content in a separate stack
|
|
274
|
-
const contentStack = new Stack(app, 'ContentStack', { env });
|
|
275
|
-
contentStack.node.addDependency(hostingStack);
|
|
276
|
-
|
|
277
|
-
new StaticContent(contentStack, 'static-content', {
|
|
278
|
-
contentSourceDirectory: 'dist', // Local directory with built files
|
|
279
|
-
contentDestinationDirectory: '/', // Deploy to root of bucket path
|
|
280
|
-
fullDomain: hosting.fullDomain, // Use domain from StaticHosting
|
|
281
|
-
});
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
**With Custom Subdomain:**
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
new StaticContent(contentStack, 'static-content', {
|
|
288
|
-
contentSourceDirectory: 'dist',
|
|
289
|
-
contentDestinationDirectory: '/',
|
|
290
|
-
fullDomain: hosting.fullDomain,
|
|
291
|
-
subDomain: 'staging', // Override git branch detection
|
|
292
|
-
bucketArnParamName: '/STATIC_WEBSITE/BUCKET_ARN', // Custom param name
|
|
293
|
-
});
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**Properties:**
|
|
297
|
-
|
|
298
|
-
| Property | Type | Default | Description |
|
|
299
|
-
|----------|------|---------|-------------|
|
|
300
|
-
| `contentSourceDirectory` | `string` | **Required** | Absolute path to directory containing files to deploy |
|
|
301
|
-
| `contentDestinationDirectory` | `string` | **Required** | Directory within bucket to place content (should start with `/`) |
|
|
302
|
-
| `fullDomain` | `string` | **Required** | Full domain name (from `StaticHosting.fullDomain`) |
|
|
303
|
-
| `subDomain` | `string` | Git branch name | Subdomain prefix (defaults to current git branch) |
|
|
304
|
-
| `bucketArnParamName` | `string` | `"/STATIC_WEBSITE/BUCKET_ARN"` | SSM parameter name for bucket ARN |
|
|
305
|
-
|
|
306
|
-
**Path Construction:**
|
|
307
|
-
|
|
308
|
-
The construct creates a unique path prefix using: `{subDomain}.{fullDomain}`
|
|
309
|
-
|
|
310
|
-
For example:
|
|
311
|
-
- Branch: `feature-123`, Domain: `example.com` → Path: `feature-123.example.com/`
|
|
312
|
-
- Branch: `main`, Domain: `example.com` → Path: `main.example.com/` (or just `example.com/` if subdomain is empty)
|
|
313
|
-
|
|
314
|
-
## Complete Example
|
|
315
|
-
|
|
316
|
-
Here's a complete example showing how to use `StaticHosting` and `StaticContent` together:
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
import { StaticContent, StaticHosting } from '@codedrifters/constructs';
|
|
320
|
-
import { App, RemovalPolicy, Stack } from 'aws-cdk-lib';
|
|
321
|
-
|
|
322
|
-
const app = new App();
|
|
323
|
-
|
|
324
|
-
const env = {
|
|
325
|
-
account: '123456789012',
|
|
326
|
-
region: 'us-east-1',
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
const baseDomain = 'example.com';
|
|
330
|
-
|
|
331
|
-
/*******************************************************************************
|
|
332
|
-
*
|
|
333
|
-
* Step 1: Create the hosting infrastructure
|
|
334
|
-
*
|
|
335
|
-
* This creates the S3 bucket, CloudFront distribution, SSL certificate,
|
|
336
|
-
* and DNS records.
|
|
337
|
-
*
|
|
338
|
-
******************************************************************************/
|
|
339
|
-
|
|
340
|
-
const hostingStack = new Stack(
|
|
341
|
-
app,
|
|
342
|
-
`static-hosting-dev-${env.account}-${env.region}`,
|
|
343
|
-
{ env }
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
const hosting = new StaticHosting(hostingStack, 'static-hosting', {
|
|
347
|
-
description: 'My static website',
|
|
348
|
-
privateBucketProps: {
|
|
349
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
350
|
-
autoDeleteObjects: true,
|
|
351
|
-
},
|
|
352
|
-
staticDomainProps: {
|
|
353
|
-
baseDomain,
|
|
354
|
-
hostedZoneAttributes: {
|
|
355
|
-
hostedZoneId: 'Z1234567890ABC',
|
|
356
|
-
zoneName: baseDomain,
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
/*******************************************************************************
|
|
362
|
-
*
|
|
363
|
-
* Step 2: Deploy static content
|
|
364
|
-
*
|
|
365
|
-
* This deploys files from a local directory to the S3 bucket created above.
|
|
366
|
-
* The content stack must depend on the hosting stack to ensure the bucket
|
|
367
|
-
* exists before deployment.
|
|
368
|
-
*
|
|
369
|
-
******************************************************************************/
|
|
370
|
-
|
|
371
|
-
const contentStack = new Stack(
|
|
372
|
-
app,
|
|
373
|
-
`static-content-dev-${env.account}-${env.region}`,
|
|
374
|
-
{ env }
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
// Ensure hosting stack is created first
|
|
378
|
-
contentStack.node.addDependency(hostingStack);
|
|
379
|
-
|
|
380
|
-
new StaticContent(contentStack, 'static-content', {
|
|
381
|
-
contentSourceDirectory: 'src/website', // Path to your built website files
|
|
382
|
-
contentDestinationDirectory: '/', // Deploy to root
|
|
383
|
-
fullDomain: hosting.fullDomain, // Use the domain from StaticHosting
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
app.synth();
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
**Deployment Flow:**
|
|
390
|
-
|
|
391
|
-
1. Deploy the hosting stack first: `cdk deploy static-hosting-dev-*`
|
|
392
|
-
2. This creates the S3 bucket, CloudFront distribution, SSL certificate, and DNS records
|
|
393
|
-
3. Deploy the content stack: `cdk deploy static-content-dev-*`
|
|
394
|
-
4. This uploads your static files to the S3 bucket
|
|
395
|
-
5. Your website is now live at your domain!
|
|
396
|
-
|
|
397
|
-
**Branch-Based Deployments:**
|
|
398
|
-
|
|
399
|
-
When deploying from different git branches, the `StaticContent` construct automatically uses the branch name as a subdomain prefix. This allows you to have:
|
|
400
|
-
- `main` branch → `example.com`
|
|
401
|
-
- `feature-123` branch → `feature-123.example.com`
|
|
402
|
-
- `pr-456` branch → `pr-456.example.com`
|
|
403
|
-
|
|
404
|
-
All using the same S3 bucket and CloudFront distribution!
|
|
405
|
-
|
|
406
|
-
## API Reference
|
|
407
|
-
|
|
408
|
-
### Exports
|
|
409
|
-
|
|
410
|
-
The package exports the following:
|
|
411
|
-
|
|
412
|
-
**Constructs:**
|
|
413
|
-
- `PrivateBucket` - Secure S3 bucket
|
|
414
|
-
- `StaticHosting` - Complete static hosting solution
|
|
415
|
-
- `StaticContent` - Static content deployment
|
|
416
|
-
|
|
417
|
-
### Type Definitions
|
|
418
|
-
|
|
419
|
-
**PrivateBucketProps**
|
|
420
|
-
- Extends `BucketProps` from `aws-cdk-lib/aws-s3`
|
|
421
|
-
- All standard S3 bucket properties are supported
|
|
422
|
-
- Security settings are enforced and cannot be overridden
|
|
423
|
-
|
|
424
|
-
**StaticHostingProps**
|
|
425
|
-
- Extends `StackProps` from `aws-cdk-lib`
|
|
426
|
-
- See [StaticHosting](#statichosting) section for full property list
|
|
427
|
-
|
|
428
|
-
**StaticContentProps**
|
|
429
|
-
- See [StaticContent](#staticcontent) section for full property list
|
|
430
|
-
|
|
431
|
-
**StaticDomainProps**
|
|
432
|
-
- `baseDomain: string` - Base domain name
|
|
433
|
-
- `hostedZoneAttributes: HostedZoneAttributes` - Route53 hosted zone attributes
|
|
434
|
-
|
|
435
|
-
## Further Documentation
|
|
436
|
-
|
|
437
|
-
### AWS CDK Resources
|
|
438
|
-
|
|
439
|
-
- [AWS CDK Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/home.html) - Comprehensive guide to AWS CDK
|
|
440
|
-
- [AWS CDK API Reference](https://docs.aws.amazon.com/cdk/api/v2/) - Complete API documentation
|
|
441
|
-
- [AWS CDK Construct Library](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html) - All available constructs
|
|
442
|
-
|
|
443
|
-
### AWS Service Documentation
|
|
444
|
-
|
|
445
|
-
- [Amazon S3 Documentation](https://docs.aws.amazon.com/s3/) - S3 service documentation
|
|
446
|
-
- [Amazon CloudFront Documentation](https://docs.aws.amazon.com/cloudfront/) - CloudFront CDN documentation
|
|
447
|
-
- [AWS Certificate Manager](https://docs.aws.amazon.com/acm/) - SSL/TLS certificate management
|
|
448
|
-
- [Amazon Route53](https://docs.aws.amazon.com/route53/) - DNS service documentation
|
|
449
|
-
|
|
450
|
-
### Package Information
|
|
451
|
-
|
|
452
|
-
- [NPM Package](https://www.npmjs.com/package/@codedrifters/constructs) - View on NPM
|
|
453
|
-
- [GitHub Repository](https://github.com/codedrifters/packages) - Source code
|
|
454
|
-
|
|
455
|
-
---
|
|
456
|
-
|
|
457
|
-
**Note:** This package is designed for use with AWS CDK v2. Make sure you're using compatible versions of `aws-cdk-lib` and `constructs`.
|
|
7
|
+
**NPM:** [@codedrifters/constructs](https://www.npmjs.com/package/@codedrifters/constructs)
|
package/lib/index.d.mts
CHANGED
|
@@ -2,6 +2,8 @@ import { Bucket, BucketProps } from 'aws-cdk-lib/aws-s3';
|
|
|
2
2
|
import { Construct } from 'constructs';
|
|
3
3
|
import { StackProps } from 'aws-cdk-lib';
|
|
4
4
|
import { HostedZoneAttributes } from 'aws-cdk-lib/aws-route53';
|
|
5
|
+
import { HostingMode } from './static-hosting.viewer-request-handler.mjs';
|
|
6
|
+
import 'aws-lambda';
|
|
5
7
|
|
|
6
8
|
interface PrivateBucketProps extends BucketProps {
|
|
7
9
|
}
|
|
@@ -95,6 +97,23 @@ interface StaticHostingProps extends StackProps {
|
|
|
95
97
|
* Props to pass to the private S3 bucket.
|
|
96
98
|
*/
|
|
97
99
|
readonly privateBucketProps?: PrivateBucketProps;
|
|
100
|
+
/**
|
|
101
|
+
* Selects how path-like URIs are rewritten by the viewer-request
|
|
102
|
+
* Lambda@Edge handler.
|
|
103
|
+
*
|
|
104
|
+
* - `spa` (default): path-like URIs rewrite to `/index.html` so a
|
|
105
|
+
* single-page app can serve its one root index and let the client-side
|
|
106
|
+
* router handle the path.
|
|
107
|
+
* - `static`: path-like URIs append `/index.html` (e.g. `/docs` →
|
|
108
|
+
* `/docs/index.html`) so multi-page static sites can serve distinct
|
|
109
|
+
* HTML per path.
|
|
110
|
+
*
|
|
111
|
+
* Multi-tenant domain-folder prepending runs after the rewrite in both
|
|
112
|
+
* modes and is unaffected by this prop.
|
|
113
|
+
*
|
|
114
|
+
* @default "spa"
|
|
115
|
+
*/
|
|
116
|
+
readonly hostingMode?: HostingMode;
|
|
98
117
|
}
|
|
99
118
|
declare class StaticHosting extends Construct {
|
|
100
119
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -3,6 +3,18 @@ import { Construct } from 'constructs';
|
|
|
3
3
|
import { StackProps } from 'aws-cdk-lib';
|
|
4
4
|
import { HostedZoneAttributes } from 'aws-cdk-lib/aws-route53';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Hosting mode controls how path-like URIs get a default document.
|
|
8
|
+
*
|
|
9
|
+
* - `spa`: path-like URIs (e.g. `/dashboard`, `/patients/123`) rewrite to
|
|
10
|
+
* `/index.html` so the single-page app's root index is served and the
|
|
11
|
+
* client-side router handles the path.
|
|
12
|
+
* - `static`: path-like URIs append `/index.html` (e.g. `/docs` becomes
|
|
13
|
+
* `/docs/index.html`) so multi-page static sites can serve distinct
|
|
14
|
+
* HTML per path.
|
|
15
|
+
*/
|
|
16
|
+
type HostingMode = "spa" | "static";
|
|
17
|
+
|
|
6
18
|
interface PrivateBucketProps extends BucketProps {
|
|
7
19
|
}
|
|
8
20
|
declare class PrivateBucket extends Bucket {
|
|
@@ -95,6 +107,23 @@ interface StaticHostingProps extends StackProps {
|
|
|
95
107
|
* Props to pass to the private S3 bucket.
|
|
96
108
|
*/
|
|
97
109
|
readonly privateBucketProps?: PrivateBucketProps;
|
|
110
|
+
/**
|
|
111
|
+
* Selects how path-like URIs are rewritten by the viewer-request
|
|
112
|
+
* Lambda@Edge handler.
|
|
113
|
+
*
|
|
114
|
+
* - `spa` (default): path-like URIs rewrite to `/index.html` so a
|
|
115
|
+
* single-page app can serve its one root index and let the client-side
|
|
116
|
+
* router handle the path.
|
|
117
|
+
* - `static`: path-like URIs append `/index.html` (e.g. `/docs` →
|
|
118
|
+
* `/docs/index.html`) so multi-page static sites can serve distinct
|
|
119
|
+
* HTML per path.
|
|
120
|
+
*
|
|
121
|
+
* Multi-tenant domain-folder prepending runs after the rewrite in both
|
|
122
|
+
* modes and is unaffected by this prop.
|
|
123
|
+
*
|
|
124
|
+
* @default "spa"
|
|
125
|
+
*/
|
|
126
|
+
readonly hostingMode?: HostingMode;
|
|
98
127
|
}
|
|
99
128
|
declare class StaticHosting extends Construct {
|
|
100
129
|
/**
|
package/lib/index.js
CHANGED
|
@@ -260,11 +260,13 @@ var StaticHosting = class extends import_constructs2.Construct {
|
|
|
260
260
|
distributionDomainParamName,
|
|
261
261
|
distributionIDParamName,
|
|
262
262
|
staticDomainProps,
|
|
263
|
-
privateBucketProps
|
|
263
|
+
privateBucketProps,
|
|
264
|
+
hostingMode
|
|
264
265
|
} = {
|
|
265
266
|
bucketArnParamName: "/STATIC_WEBSITE/BUCKET_ARN",
|
|
266
267
|
distributionDomainParamName: "/STATIC_WEBSITE/DISTRIBUTION_DOMAIN",
|
|
267
268
|
distributionIDParamName: "/STATIC_WEBSITE/DISTRIBUTION_ID",
|
|
269
|
+
hostingMode: "spa",
|
|
268
270
|
...props
|
|
269
271
|
};
|
|
270
272
|
const { baseDomain, hostedZoneAttributes } = staticDomainProps ?? {};
|
|
@@ -301,6 +303,7 @@ var StaticHosting = class extends import_constructs2.Construct {
|
|
|
301
303
|
const handlerEntry = fs.existsSync(handlerJs) ? handlerJs : handlerTs;
|
|
302
304
|
const handler = new import_aws_lambda_nodejs.NodejsFunction(this, "viewer-request-handler", {
|
|
303
305
|
entry: handlerEntry,
|
|
306
|
+
handler: hostingMode === "static" ? "staticHandler" : "spaHandler",
|
|
304
307
|
memorySize: 128,
|
|
305
308
|
runtime: import_aws_lambda.Runtime.NODEJS_24_X,
|
|
306
309
|
logGroup: new import_aws_logs.LogGroup(this, "viewer-request-handler-log-group", {
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../utils/src/aws/aws-types.ts","../../utils/src/git/git-utils.ts","../../utils/src/string/string-utils.ts","../../utils/src/index.ts","../src/index.ts","../src/s3/private-bucket.ts","../src/static-hosting/static-content.ts","../src/static-hosting/static-hosting.ts"],"sourcesContent":["/**\n * Stage Types\n *\n * What stage of deployment is this? Dev, staging, or prod?\n */\nexport const AWS_STAGE_TYPE = {\n /**\n * Development environment, typically used for testing and development.\n */\n DEV: \"dev\",\n\n /**\n * Staging environment, used for pre-production testing.\n */\n STAGE: \"stage\",\n\n /**\n * Production environment, used for live deployments.\n */\n PROD: \"prod\",\n} as const;\n\n/**\n * Above const as a type.\n */\nexport type AwsStageType = (typeof AWS_STAGE_TYPE)[keyof typeof AWS_STAGE_TYPE];\n\n/**\n * Deployment target role: whether an (account, region) is the primary or\n * secondary deployment target (e.g. primary vs replica region).\n */\nexport const DEPLOYMENT_TARGET_ROLE = {\n /**\n * Account and region that represents the primary region for this service.\n * For example, the base DynamoDB Region for global tables.\n */\n PRIMARY: \"primary\",\n /**\n * Account and region that represents a secondary region for this service.\n * For example, a replica region for a global DynamoDB table.\n */\n SECONDARY: \"secondary\",\n} as const;\n\n/**\n * Type for deployment target role values.\n */\nexport type DeploymentTargetRoleType =\n (typeof DEPLOYMENT_TARGET_ROLE)[keyof typeof DEPLOYMENT_TARGET_ROLE];\n\n/**\n * Environment types (primary/secondary).\n *\n * @deprecated Use {@link DEPLOYMENT_TARGET_ROLE} instead. This constant is maintained for backward compatibility.\n */\nexport const AWS_ENVIRONMENT_TYPE = DEPLOYMENT_TARGET_ROLE;\n\n/**\n * Type for environment type values.\n *\n * @deprecated Use {@link DeploymentTargetRoleType} instead. This type is maintained for backward compatibility.\n */\nexport type AwsEnvironmentType = DeploymentTargetRoleType;\n","import { execSync } from \"node:child_process\";\n\n/**\n * Returns the current full git branch name\n *\n * ie: feature/1234 returns feature/1234\n *\n */\nexport const findGitBranch = (): string => {\n return execSync(\"git rev-parse --abbrev-ref HEAD\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\");\n};\n\nexport const findGitRepoName = (): string => {\n /**\n * When running in github actions this will be populated.\n */\n if (process.env.GITHUB_REPOSITORY) {\n return process.env.GITHUB_REPOSITORY;\n }\n\n /**\n * locally, we need to extract the repo name from the git config.\n */\n const remote = execSync(\"git config --get remote.origin.url\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\")\n .trim();\n\n const match = remote.match(/[:\\/]([^/]+\\/[^/]+?)(?:\\.git)?$/);\n const repoName = match ? match[1] : \"error-repo-name\";\n\n return repoName;\n};\n","import * as crypto from \"node:crypto\";\n\n/**\n *\n * @param inString string to hash\n * @param trimLength trim to this length (defaults to 999 chars)\n * @returns\n */\nexport const hashString = (inString: string, trimLength: number = 999) => {\n return crypto\n .createHash(\"sha256\")\n .update(inString)\n .digest(\"hex\")\n .substring(0, trimLength);\n};\n\n/**\n *\n * @param inputString string to truncate\n * @param maxLength max length of this string\n * @returns trimmed string\n */\nexport const trimStringLength = (inputString: string, maxLength: number) => {\n return inputString.length < maxLength\n ? inputString\n : inputString.substring(0, maxLength);\n};\n","export * from \"./aws/aws-types\";\nexport * from \"./git/git-utils\";\nexport * from \"./string/string-utils\";\n","export * from \"./s3\";\nexport * from \"./static-hosting\";\n","import { RemovalPolicy } from \"aws-cdk-lib\";\nimport {\n BlockPublicAccess,\n Bucket,\n BucketProps,\n ObjectOwnership,\n} from \"aws-cdk-lib/aws-s3\";\nimport { Construct } from \"constructs\";\n\nexport interface PrivateBucketProps extends BucketProps {}\n\nexport class PrivateBucket extends Bucket {\n constructor(scope: Construct, id: string, props: PrivateBucketProps = {}) {\n const defaultProps = {\n removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,\n autoDeleteObjects: props.removalPolicy === RemovalPolicy.DESTROY,\n };\n\n const requiredProps = {\n publicReadAccess: false,\n blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n enforceSSL: true,\n objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,\n };\n\n super(scope, id, { ...defaultProps, ...props, ...requiredProps });\n }\n}\n","// eslint-disable-next-line import/no-extraneous-dependencies\nimport { findGitBranch } from \"@codedrifters/utils\";\nimport { Bucket } from \"aws-cdk-lib/aws-s3\";\nimport { BucketDeployment, Source } from \"aws-cdk-lib/aws-s3-deployment\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { paramCase } from \"change-case\";\nimport { Construct } from \"constructs\";\n\n/*******************************************************************************\n *\n * STATIC CONTENT UPLOADER\n *\n * This construct uploads a directory of content from a local location into S3.\n *\n * To support PR and branch specific builds, each S3 bucket can store content\n * for multiple domains and builds, using the following format:\n *\n * S3-bucket/domain/*\n *\n * A bucket used to store content for stage.openhi.org might have the\n * following directory structure (all in the same bucket).\n *\n * /stage.openhi.org/* -> serves content to stage.openhi.org\n * /feature-7.stage.openhi.org/* -> serves content to feature-7.stage.openhi.org\n * /pr-123.stage.openhi.org/* -> serves content to pr-123.stage.openhi.org\n *\n ******************************************************************************/\n\nexport interface StaticContentProps {\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Absolute path to directory containing content for the website.\n */\n readonly contentSourceDirectory: string;\n\n /**\n * Directory to place content into. Should start with a slash.\n * Example: '/widget'\n */\n readonly contentDestinationDirectory: string;\n\n /**\n * The sub domain prefix (ie: images)\n *\n * @default git branch name\n */\n readonly subDomain?: string;\n\n /**\n * The full domain (ie: staging.codedrifters.com)\n */\n readonly fullDomain: string;\n}\n\nexport class StaticContent extends Construct {\n constructor(scope: Construct, id: string, props: StaticContentProps) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n contentSourceDirectory,\n contentDestinationDirectory,\n subDomain,\n fullDomain,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n subDomain: findGitBranch(),\n ...props,\n };\n\n /***************************************************************************\n *\n * Import and build some values from Param Store during deployment.\n *\n **************************************************************************/\n\n const keyPrefix = [paramCase(subDomain), fullDomain].join(\".\");\n\n const bucketArn = StringParameter.valueForStringParameter(\n this,\n bucketArnParamName,\n );\n const bucket = Bucket.fromBucketArn(this, \"bucket\", bucketArn);\n\n /***************************************************************************\n *\n * Gather the sources we'll be deploying. We need to have an empty source\n * for tests since it will change all the time and bork up the test\n * snapshots if we don't.\n *\n **************************************************************************/\n\n const isTestEnv = process.env.VITEST === \"true\";\n const sources = isTestEnv ? [] : [Source.asset(contentSourceDirectory)];\n\n new BucketDeployment(this, \"deploy\", {\n sources,\n destinationBucket: bucket,\n retainOnDelete: false,\n destinationKeyPrefix: `${keyPrefix}${contentDestinationDirectory}`,\n });\n }\n}\n","import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { Duration, StackProps } from \"aws-cdk-lib\";\nimport {\n Certificate,\n CertificateValidation,\n} from \"aws-cdk-lib/aws-certificatemanager\";\nimport {\n AccessLevel,\n AllowedMethods,\n CacheCookieBehavior,\n CacheHeaderBehavior,\n CachePolicy,\n CacheQueryStringBehavior,\n Distribution,\n LambdaEdgeEventType,\n S3OriginAccessControl,\n Signing,\n ViewerProtocolPolicy,\n} from \"aws-cdk-lib/aws-cloudfront\";\nimport { S3BucketOrigin } from \"aws-cdk-lib/aws-cloudfront-origins\";\nimport { Runtime } from \"aws-cdk-lib/aws-lambda\";\nimport { NodejsFunction } from \"aws-cdk-lib/aws-lambda-nodejs\";\nimport { LogGroup, RetentionDays } from \"aws-cdk-lib/aws-logs\";\nimport {\n ARecord,\n HostedZone,\n HostedZoneAttributes,\n IHostedZone,\n RecordTarget,\n} from \"aws-cdk-lib/aws-route53\";\nimport { CloudFrontTarget } from \"aws-cdk-lib/aws-route53-targets\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { Construct } from \"constructs\";\nimport { PrivateBucket, PrivateBucketProps } from \"../s3/private-bucket\";\n\nexport interface StaticDomainProps {\n /**\n * The base domain (ie: codedrifters.com)\n */\n readonly baseDomain: string;\n\n /**\n * Hosted zone ID for the base domain.\n */\n readonly hostedZoneAttributes: HostedZoneAttributes;\n}\n\nexport interface StaticHostingProps extends StackProps {\n /**\n * Short description used in various places for traceability.\n */\n readonly description?: string;\n\n /**\n * Values used to connect a domain name to the cloudfront distribution. If not\n * supplied, cloudfront doesn't use a custom domain.\n */\n readonly staticDomainProps?: StaticDomainProps;\n\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution Domain Name.\n */\n readonly distributionDomainParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution ID.\n */\n readonly distributionIDParamName?: string;\n\n /**\n * Props to pass to the private S3 bucket.\n */\n readonly privateBucketProps?: PrivateBucketProps;\n}\n\nexport class StaticHosting extends Construct {\n /**\n * Full domain name used as basis for hosting.\n */\n public readonly fullDomain: string;\n\n constructor(scope: Construct, id: string, props: StaticHostingProps = {}) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n distributionDomainParamName,\n distributionIDParamName,\n staticDomainProps,\n privateBucketProps,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n distributionDomainParamName: \"/STATIC_WEBSITE/DISTRIBUTION_DOMAIN\",\n distributionIDParamName: \"/STATIC_WEBSITE/DISTRIBUTION_ID\",\n ...props,\n };\n\n const { baseDomain, hostedZoneAttributes } = staticDomainProps ?? {};\n\n /***************************************************************************\n *\n * PRIVATE BUCKET\n *\n * A bucket to store the files within.\n * Save ARN for later deploys.\n *\n **************************************************************************/\n\n const bucket = new PrivateBucket(\n this,\n \"static-hosting-bucket\",\n privateBucketProps,\n );\n\n /***************************************************************************\n *\n * DNS & Wildcard Certificate\n *\n * If a zone Id as passed in, find the hosted zone and create a wildcard\n * certificate for the domain.\n *\n **************************************************************************/\n\n let zone: IHostedZone | undefined;\n let certificate: Certificate | undefined;\n\n if (hostedZoneAttributes && baseDomain) {\n zone = HostedZone.fromHostedZoneAttributes(\n this,\n \"zone\",\n hostedZoneAttributes,\n );\n certificate = new Certificate(this, \"wildcard-certificate\", {\n domainName: `*.${baseDomain}`,\n subjectAlternativeNames: [baseDomain],\n validation: CertificateValidation.fromDnsMultiZone({\n [`*.${baseDomain}`]: zone,\n [baseDomain]: zone,\n }),\n });\n }\n\n /******************************************************************************\n *\n * LAMBDA@EDGE FUNCTION\n *\n * This handles rewriting the path from domain name.\n *\n *****************************************************************************/\n\n // Explicit entry required: when omitted, NodejsFunction infers the path from the\n // call site (the built lib/index.js), so it looks for index.viewer-request-handler.js\n // in the package. That file is only emitted if we add it as a separate tsup entry.\n // Use .js when present (built package); fall back to .ts for tests running from source.\n const handlerJs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.js\",\n );\n const handlerTs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.ts\",\n );\n const handlerEntry = fs.existsSync(handlerJs) ? handlerJs : handlerTs;\n\n const handler = new NodejsFunction(this, \"viewer-request-handler\", {\n entry: handlerEntry,\n memorySize: 128,\n runtime: Runtime.NODEJS_24_X,\n logGroup: new LogGroup(this, \"viewer-request-handler-log-group\", {\n retention: RetentionDays.ONE_MONTH,\n }),\n });\n\n /******************************************************************************\n *\n * CLOUDFRONT CONFIG\n *\n * Setup a CloudFront Distribution for the bucket.\n *\n *****************************************************************************/\n\n const cachePolicy = new CachePolicy(this, \"cloudfront-policy\", {\n comment: \"Relatively conservative TTL policy.\",\n maxTtl: Duration.seconds(300),\n minTtl: Duration.seconds(0),\n defaultTtl: Duration.seconds(60),\n headerBehavior: CacheHeaderBehavior.none(),\n queryStringBehavior: CacheQueryStringBehavior.none(),\n cookieBehavior: CacheCookieBehavior.none(),\n enableAcceptEncodingGzip: true,\n enableAcceptEncodingBrotli: true,\n });\n\n const oac = new S3OriginAccessControl(this, \"MyOAC\", {\n signing: Signing.SIGV4_NO_OVERRIDE,\n });\n const origin = S3BucketOrigin.withOriginAccessControl(bucket, {\n originAccessControl: oac,\n originAccessLevels: [AccessLevel.READ],\n });\n\n const distribution = new Distribution(this, \"cloudfront-distribution\", {\n comment: `Distribution for ${props.description ?? id}`,\n\n /**\n * Only if domain was supplied\n */\n ...(certificate && baseDomain\n ? {\n certificate,\n domainNames: [baseDomain, `*.${baseDomain}`],\n }\n : {}),\n\n defaultBehavior: {\n origin,\n viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n cachePolicy,\n allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,\n edgeLambdas: [\n {\n functionVersion: handler.currentVersion,\n eventType: LambdaEdgeEventType.VIEWER_REQUEST,\n },\n ],\n },\n defaultRootObject: \"index.html\",\n });\n\n /**\n * We finally have enough information to set the full domain.\n */\n this.fullDomain =\n certificate && baseDomain ? baseDomain : distribution.domainName;\n\n /***************************************************************************\n *\n * DNS ENTRY\n *\n * Link cloudfront to both the root fulldomain and all possible subdomains.\n *\n **************************************************************************/\n\n if (zone) {\n new ARecord(this, \"root-dns-entry\", {\n zone,\n recordName: baseDomain ? baseDomain : \"\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n\n new ARecord(this, \"wc-dns-entry\", {\n zone,\n recordName: baseDomain ? `*.${baseDomain}` : \"*\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n }\n\n /***************************************************************************\n *\n * EXPORTS\n *\n * Used by content uploader later.\n *\n **************************************************************************/\n\n new StringParameter(this, \"dist-domain\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionDomainParamName,\n stringValue: distribution.domainName,\n });\n\n new StringParameter(this, \"dist-id\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionIDParamName,\n stringValue: distribution.distributionId,\n });\n\n new StringParameter(this, \"bucket-arn\", {\n description: `GENERATED DO NOT CHANGE - S3 Bucket ARN for (${props.description ?? id}).`,\n parameterName: bucketArnParamName,\n stringValue: bucket.bucketArn,\n });\n }\n}\n"],"mappings":"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","names":["exports","findGitBranch","exports","exports","exports","import_aws_s3","import_aws_cdk_lib","import_aws_ssm","import_constructs"]}
|
|
1
|
+
{"version":3,"sources":["../../utils/src/aws/aws-types.ts","../../utils/src/git/git-utils.ts","../../utils/src/string/string-utils.ts","../../utils/src/index.ts","../src/index.ts","../src/s3/private-bucket.ts","../src/static-hosting/static-content.ts","../src/static-hosting/static-hosting.ts"],"sourcesContent":["/**\n * Stage Types\n *\n * What stage of deployment is this? Dev, staging, or prod?\n */\nexport const AWS_STAGE_TYPE = {\n /**\n * Development environment, typically used for testing and development.\n */\n DEV: \"dev\",\n\n /**\n * Staging environment, used for pre-production testing.\n */\n STAGE: \"stage\",\n\n /**\n * Production environment, used for live deployments.\n */\n PROD: \"prod\",\n} as const;\n\n/**\n * Above const as a type.\n */\nexport type AwsStageType = (typeof AWS_STAGE_TYPE)[keyof typeof AWS_STAGE_TYPE];\n\n/**\n * Deployment target role: whether an (account, region) is the primary or\n * secondary deployment target (e.g. primary vs replica region).\n */\nexport const DEPLOYMENT_TARGET_ROLE = {\n /**\n * Account and region that represents the primary region for this service.\n * For example, the base DynamoDB Region for global tables.\n */\n PRIMARY: \"primary\",\n /**\n * Account and region that represents a secondary region for this service.\n * For example, a replica region for a global DynamoDB table.\n */\n SECONDARY: \"secondary\",\n} as const;\n\n/**\n * Type for deployment target role values.\n */\nexport type DeploymentTargetRoleType =\n (typeof DEPLOYMENT_TARGET_ROLE)[keyof typeof DEPLOYMENT_TARGET_ROLE];\n\n/**\n * Environment types (primary/secondary).\n *\n * @deprecated Use {@link DEPLOYMENT_TARGET_ROLE} instead. This constant is maintained for backward compatibility.\n */\nexport const AWS_ENVIRONMENT_TYPE = DEPLOYMENT_TARGET_ROLE;\n\n/**\n * Type for environment type values.\n *\n * @deprecated Use {@link DeploymentTargetRoleType} instead. This type is maintained for backward compatibility.\n */\nexport type AwsEnvironmentType = DeploymentTargetRoleType;\n","import { execSync } from \"node:child_process\";\n\n/**\n * Returns the current full git branch name\n *\n * ie: feature/1234 returns feature/1234\n *\n */\nexport const findGitBranch = (): string => {\n return execSync(\"git rev-parse --abbrev-ref HEAD\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\");\n};\n\nexport const findGitRepoName = (): string => {\n /**\n * When running in github actions this will be populated.\n */\n if (process.env.GITHUB_REPOSITORY) {\n return process.env.GITHUB_REPOSITORY;\n }\n\n /**\n * locally, we need to extract the repo name from the git config.\n */\n const remote = execSync(\"git config --get remote.origin.url\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\")\n .trim();\n\n const match = remote.match(/[:\\/]([^/]+\\/[^/]+?)(?:\\.git)?$/);\n const repoName = match ? match[1] : \"error-repo-name\";\n\n return repoName;\n};\n","import * as crypto from \"node:crypto\";\n\n/**\n *\n * @param inString string to hash\n * @param trimLength trim to this length (defaults to 999 chars)\n * @returns\n */\nexport const hashString = (inString: string, trimLength: number = 999) => {\n return crypto\n .createHash(\"sha256\")\n .update(inString)\n .digest(\"hex\")\n .substring(0, trimLength);\n};\n\n/**\n *\n * @param inputString string to truncate\n * @param maxLength max length of this string\n * @returns trimmed string\n */\nexport const trimStringLength = (inputString: string, maxLength: number) => {\n return inputString.length < maxLength\n ? inputString\n : inputString.substring(0, maxLength);\n};\n","export * from \"./aws/aws-types\";\nexport * from \"./git/git-utils\";\nexport * from \"./string/string-utils\";\n","export * from \"./s3\";\nexport * from \"./static-hosting\";\n","import { RemovalPolicy } from \"aws-cdk-lib\";\nimport {\n BlockPublicAccess,\n Bucket,\n BucketProps,\n ObjectOwnership,\n} from \"aws-cdk-lib/aws-s3\";\nimport { Construct } from \"constructs\";\n\nexport interface PrivateBucketProps extends BucketProps {}\n\nexport class PrivateBucket extends Bucket {\n constructor(scope: Construct, id: string, props: PrivateBucketProps = {}) {\n const defaultProps = {\n removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,\n autoDeleteObjects: props.removalPolicy === RemovalPolicy.DESTROY,\n };\n\n const requiredProps = {\n publicReadAccess: false,\n blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n enforceSSL: true,\n objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,\n };\n\n super(scope, id, { ...defaultProps, ...props, ...requiredProps });\n }\n}\n","// eslint-disable-next-line import/no-extraneous-dependencies\nimport { findGitBranch } from \"@codedrifters/utils\";\nimport { Bucket } from \"aws-cdk-lib/aws-s3\";\nimport { BucketDeployment, Source } from \"aws-cdk-lib/aws-s3-deployment\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { paramCase } from \"change-case\";\nimport { Construct } from \"constructs\";\n\n/*******************************************************************************\n *\n * STATIC CONTENT UPLOADER\n *\n * This construct uploads a directory of content from a local location into S3.\n *\n * To support PR and branch specific builds, each S3 bucket can store content\n * for multiple domains and builds, using the following format:\n *\n * S3-bucket/domain/*\n *\n * A bucket used to store content for stage.openhi.org might have the\n * following directory structure (all in the same bucket).\n *\n * /stage.openhi.org/* -> serves content to stage.openhi.org\n * /feature-7.stage.openhi.org/* -> serves content to feature-7.stage.openhi.org\n * /pr-123.stage.openhi.org/* -> serves content to pr-123.stage.openhi.org\n *\n ******************************************************************************/\n\nexport interface StaticContentProps {\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Absolute path to directory containing content for the website.\n */\n readonly contentSourceDirectory: string;\n\n /**\n * Directory to place content into. Should start with a slash.\n * Example: '/widget'\n */\n readonly contentDestinationDirectory: string;\n\n /**\n * The sub domain prefix (ie: images)\n *\n * @default git branch name\n */\n readonly subDomain?: string;\n\n /**\n * The full domain (ie: staging.codedrifters.com)\n */\n readonly fullDomain: string;\n}\n\nexport class StaticContent extends Construct {\n constructor(scope: Construct, id: string, props: StaticContentProps) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n contentSourceDirectory,\n contentDestinationDirectory,\n subDomain,\n fullDomain,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n subDomain: findGitBranch(),\n ...props,\n };\n\n /***************************************************************************\n *\n * Import and build some values from Param Store during deployment.\n *\n **************************************************************************/\n\n const keyPrefix = [paramCase(subDomain), fullDomain].join(\".\");\n\n const bucketArn = StringParameter.valueForStringParameter(\n this,\n bucketArnParamName,\n );\n const bucket = Bucket.fromBucketArn(this, \"bucket\", bucketArn);\n\n /***************************************************************************\n *\n * Gather the sources we'll be deploying. We need to have an empty source\n * for tests since it will change all the time and bork up the test\n * snapshots if we don't.\n *\n **************************************************************************/\n\n const isTestEnv = process.env.VITEST === \"true\";\n const sources = isTestEnv ? [] : [Source.asset(contentSourceDirectory)];\n\n new BucketDeployment(this, \"deploy\", {\n sources,\n destinationBucket: bucket,\n retainOnDelete: false,\n destinationKeyPrefix: `${keyPrefix}${contentDestinationDirectory}`,\n });\n }\n}\n","import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { Duration, StackProps } from \"aws-cdk-lib\";\nimport {\n Certificate,\n CertificateValidation,\n} from \"aws-cdk-lib/aws-certificatemanager\";\nimport {\n AccessLevel,\n AllowedMethods,\n CacheCookieBehavior,\n CacheHeaderBehavior,\n CachePolicy,\n CacheQueryStringBehavior,\n Distribution,\n LambdaEdgeEventType,\n S3OriginAccessControl,\n Signing,\n ViewerProtocolPolicy,\n} from \"aws-cdk-lib/aws-cloudfront\";\nimport { S3BucketOrigin } from \"aws-cdk-lib/aws-cloudfront-origins\";\nimport { Runtime } from \"aws-cdk-lib/aws-lambda\";\nimport { NodejsFunction } from \"aws-cdk-lib/aws-lambda-nodejs\";\nimport { LogGroup, RetentionDays } from \"aws-cdk-lib/aws-logs\";\nimport {\n ARecord,\n HostedZone,\n HostedZoneAttributes,\n IHostedZone,\n RecordTarget,\n} from \"aws-cdk-lib/aws-route53\";\nimport { CloudFrontTarget } from \"aws-cdk-lib/aws-route53-targets\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { Construct } from \"constructs\";\nimport type { HostingMode } from \"./static-hosting.viewer-request-handler\";\nimport { PrivateBucket, PrivateBucketProps } from \"../s3/private-bucket\";\n\nexport interface StaticDomainProps {\n /**\n * The base domain (ie: codedrifters.com)\n */\n readonly baseDomain: string;\n\n /**\n * Hosted zone ID for the base domain.\n */\n readonly hostedZoneAttributes: HostedZoneAttributes;\n}\n\nexport interface StaticHostingProps extends StackProps {\n /**\n * Short description used in various places for traceability.\n */\n readonly description?: string;\n\n /**\n * Values used to connect a domain name to the cloudfront distribution. If not\n * supplied, cloudfront doesn't use a custom domain.\n */\n readonly staticDomainProps?: StaticDomainProps;\n\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution Domain Name.\n */\n readonly distributionDomainParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution ID.\n */\n readonly distributionIDParamName?: string;\n\n /**\n * Props to pass to the private S3 bucket.\n */\n readonly privateBucketProps?: PrivateBucketProps;\n\n /**\n * Selects how path-like URIs are rewritten by the viewer-request\n * Lambda@Edge handler.\n *\n * - `spa` (default): path-like URIs rewrite to `/index.html` so a\n * single-page app can serve its one root index and let the client-side\n * router handle the path.\n * - `static`: path-like URIs append `/index.html` (e.g. `/docs` →\n * `/docs/index.html`) so multi-page static sites can serve distinct\n * HTML per path.\n *\n * Multi-tenant domain-folder prepending runs after the rewrite in both\n * modes and is unaffected by this prop.\n *\n * @default \"spa\"\n */\n readonly hostingMode?: HostingMode;\n}\n\nexport class StaticHosting extends Construct {\n /**\n * Full domain name used as basis for hosting.\n */\n public readonly fullDomain: string;\n\n constructor(scope: Construct, id: string, props: StaticHostingProps = {}) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n distributionDomainParamName,\n distributionIDParamName,\n staticDomainProps,\n privateBucketProps,\n hostingMode,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n distributionDomainParamName: \"/STATIC_WEBSITE/DISTRIBUTION_DOMAIN\",\n distributionIDParamName: \"/STATIC_WEBSITE/DISTRIBUTION_ID\",\n hostingMode: \"spa\" as HostingMode,\n ...props,\n };\n\n const { baseDomain, hostedZoneAttributes } = staticDomainProps ?? {};\n\n /***************************************************************************\n *\n * PRIVATE BUCKET\n *\n * A bucket to store the files within.\n * Save ARN for later deploys.\n *\n **************************************************************************/\n\n const bucket = new PrivateBucket(\n this,\n \"static-hosting-bucket\",\n privateBucketProps,\n );\n\n /***************************************************************************\n *\n * DNS & Wildcard Certificate\n *\n * If a zone Id as passed in, find the hosted zone and create a wildcard\n * certificate for the domain.\n *\n **************************************************************************/\n\n let zone: IHostedZone | undefined;\n let certificate: Certificate | undefined;\n\n if (hostedZoneAttributes && baseDomain) {\n zone = HostedZone.fromHostedZoneAttributes(\n this,\n \"zone\",\n hostedZoneAttributes,\n );\n certificate = new Certificate(this, \"wildcard-certificate\", {\n domainName: `*.${baseDomain}`,\n subjectAlternativeNames: [baseDomain],\n validation: CertificateValidation.fromDnsMultiZone({\n [`*.${baseDomain}`]: zone,\n [baseDomain]: zone,\n }),\n });\n }\n\n /******************************************************************************\n *\n * LAMBDA@EDGE FUNCTION\n *\n * This handles rewriting the path from domain name.\n *\n *****************************************************************************/\n\n // Explicit entry required: when omitted, NodejsFunction infers the path from the\n // call site (the built lib/index.js), so it looks for index.viewer-request-handler.js\n // in the package. That file is only emitted if we add it as a separate tsup entry.\n // Use .js when present (built package); fall back to .ts for tests running from source.\n const handlerJs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.js\",\n );\n const handlerTs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.ts\",\n );\n const handlerEntry = fs.existsSync(handlerJs) ? handlerJs : handlerTs;\n\n const handler = new NodejsFunction(this, \"viewer-request-handler\", {\n entry: handlerEntry,\n handler: hostingMode === \"static\" ? \"staticHandler\" : \"spaHandler\",\n memorySize: 128,\n runtime: Runtime.NODEJS_24_X,\n logGroup: new LogGroup(this, \"viewer-request-handler-log-group\", {\n retention: RetentionDays.ONE_MONTH,\n }),\n });\n\n /******************************************************************************\n *\n * CLOUDFRONT CONFIG\n *\n * Setup a CloudFront Distribution for the bucket.\n *\n *****************************************************************************/\n\n const cachePolicy = new CachePolicy(this, \"cloudfront-policy\", {\n comment: \"Relatively conservative TTL policy.\",\n maxTtl: Duration.seconds(300),\n minTtl: Duration.seconds(0),\n defaultTtl: Duration.seconds(60),\n headerBehavior: CacheHeaderBehavior.none(),\n queryStringBehavior: CacheQueryStringBehavior.none(),\n cookieBehavior: CacheCookieBehavior.none(),\n enableAcceptEncodingGzip: true,\n enableAcceptEncodingBrotli: true,\n });\n\n const oac = new S3OriginAccessControl(this, \"MyOAC\", {\n signing: Signing.SIGV4_NO_OVERRIDE,\n });\n const origin = S3BucketOrigin.withOriginAccessControl(bucket, {\n originAccessControl: oac,\n originAccessLevels: [AccessLevel.READ],\n });\n\n const distribution = new Distribution(this, \"cloudfront-distribution\", {\n comment: `Distribution for ${props.description ?? id}`,\n\n /**\n * Only if domain was supplied\n */\n ...(certificate && baseDomain\n ? {\n certificate,\n domainNames: [baseDomain, `*.${baseDomain}`],\n }\n : {}),\n\n defaultBehavior: {\n origin,\n viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n cachePolicy,\n allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,\n edgeLambdas: [\n {\n functionVersion: handler.currentVersion,\n eventType: LambdaEdgeEventType.VIEWER_REQUEST,\n },\n ],\n },\n defaultRootObject: \"index.html\",\n });\n\n /**\n * We finally have enough information to set the full domain.\n */\n this.fullDomain =\n certificate && baseDomain ? baseDomain : distribution.domainName;\n\n /***************************************************************************\n *\n * DNS ENTRY\n *\n * Link cloudfront to both the root fulldomain and all possible subdomains.\n *\n **************************************************************************/\n\n if (zone) {\n new ARecord(this, \"root-dns-entry\", {\n zone,\n recordName: baseDomain ? baseDomain : \"\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n\n new ARecord(this, \"wc-dns-entry\", {\n zone,\n recordName: baseDomain ? `*.${baseDomain}` : \"*\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n }\n\n /***************************************************************************\n *\n * EXPORTS\n *\n * Used by content uploader later.\n *\n **************************************************************************/\n\n new StringParameter(this, \"dist-domain\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionDomainParamName,\n stringValue: distribution.domainName,\n });\n\n new StringParameter(this, \"dist-id\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionIDParamName,\n stringValue: distribution.distributionId,\n });\n\n new StringParameter(this, \"bucket-arn\", {\n description: `GENERATED DO NOT CHANGE - S3 Bucket ARN for (${props.description ?? id}).`,\n parameterName: bucketArnParamName,\n stringValue: bucket.bucketArn,\n });\n }\n}\n"],"mappings":"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","names":["exports","findGitBranch","exports","exports","exports","import_aws_s3","import_aws_cdk_lib","import_aws_ssm","import_constructs"]}
|
package/lib/index.mjs
CHANGED
|
@@ -248,11 +248,13 @@ var StaticHosting = class extends Construct2 {
|
|
|
248
248
|
distributionDomainParamName,
|
|
249
249
|
distributionIDParamName,
|
|
250
250
|
staticDomainProps,
|
|
251
|
-
privateBucketProps
|
|
251
|
+
privateBucketProps,
|
|
252
|
+
hostingMode
|
|
252
253
|
} = {
|
|
253
254
|
bucketArnParamName: "/STATIC_WEBSITE/BUCKET_ARN",
|
|
254
255
|
distributionDomainParamName: "/STATIC_WEBSITE/DISTRIBUTION_DOMAIN",
|
|
255
256
|
distributionIDParamName: "/STATIC_WEBSITE/DISTRIBUTION_ID",
|
|
257
|
+
hostingMode: "spa",
|
|
256
258
|
...props
|
|
257
259
|
};
|
|
258
260
|
const { baseDomain, hostedZoneAttributes } = staticDomainProps ?? {};
|
|
@@ -289,6 +291,7 @@ var StaticHosting = class extends Construct2 {
|
|
|
289
291
|
const handlerEntry = fs.existsSync(handlerJs) ? handlerJs : handlerTs;
|
|
290
292
|
const handler = new NodejsFunction(this, "viewer-request-handler", {
|
|
291
293
|
entry: handlerEntry,
|
|
294
|
+
handler: hostingMode === "static" ? "staticHandler" : "spaHandler",
|
|
292
295
|
memorySize: 128,
|
|
293
296
|
runtime: Runtime.NODEJS_24_X,
|
|
294
297
|
logGroup: new LogGroup(this, "viewer-request-handler-log-group", {
|
package/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../utils/src/aws/aws-types.ts","../../utils/src/git/git-utils.ts","../../utils/src/string/string-utils.ts","../../utils/src/index.ts","../src/s3/private-bucket.ts","../src/static-hosting/static-content.ts","../src/static-hosting/static-hosting.ts"],"sourcesContent":["/**\n * Stage Types\n *\n * What stage of deployment is this? Dev, staging, or prod?\n */\nexport const AWS_STAGE_TYPE = {\n /**\n * Development environment, typically used for testing and development.\n */\n DEV: \"dev\",\n\n /**\n * Staging environment, used for pre-production testing.\n */\n STAGE: \"stage\",\n\n /**\n * Production environment, used for live deployments.\n */\n PROD: \"prod\",\n} as const;\n\n/**\n * Above const as a type.\n */\nexport type AwsStageType = (typeof AWS_STAGE_TYPE)[keyof typeof AWS_STAGE_TYPE];\n\n/**\n * Deployment target role: whether an (account, region) is the primary or\n * secondary deployment target (e.g. primary vs replica region).\n */\nexport const DEPLOYMENT_TARGET_ROLE = {\n /**\n * Account and region that represents the primary region for this service.\n * For example, the base DynamoDB Region for global tables.\n */\n PRIMARY: \"primary\",\n /**\n * Account and region that represents a secondary region for this service.\n * For example, a replica region for a global DynamoDB table.\n */\n SECONDARY: \"secondary\",\n} as const;\n\n/**\n * Type for deployment target role values.\n */\nexport type DeploymentTargetRoleType =\n (typeof DEPLOYMENT_TARGET_ROLE)[keyof typeof DEPLOYMENT_TARGET_ROLE];\n\n/**\n * Environment types (primary/secondary).\n *\n * @deprecated Use {@link DEPLOYMENT_TARGET_ROLE} instead. This constant is maintained for backward compatibility.\n */\nexport const AWS_ENVIRONMENT_TYPE = DEPLOYMENT_TARGET_ROLE;\n\n/**\n * Type for environment type values.\n *\n * @deprecated Use {@link DeploymentTargetRoleType} instead. This type is maintained for backward compatibility.\n */\nexport type AwsEnvironmentType = DeploymentTargetRoleType;\n","import { execSync } from \"node:child_process\";\n\n/**\n * Returns the current full git branch name\n *\n * ie: feature/1234 returns feature/1234\n *\n */\nexport const findGitBranch = (): string => {\n return execSync(\"git rev-parse --abbrev-ref HEAD\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\");\n};\n\nexport const findGitRepoName = (): string => {\n /**\n * When running in github actions this will be populated.\n */\n if (process.env.GITHUB_REPOSITORY) {\n return process.env.GITHUB_REPOSITORY;\n }\n\n /**\n * locally, we need to extract the repo name from the git config.\n */\n const remote = execSync(\"git config --get remote.origin.url\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\")\n .trim();\n\n const match = remote.match(/[:\\/]([^/]+\\/[^/]+?)(?:\\.git)?$/);\n const repoName = match ? match[1] : \"error-repo-name\";\n\n return repoName;\n};\n","import * as crypto from \"node:crypto\";\n\n/**\n *\n * @param inString string to hash\n * @param trimLength trim to this length (defaults to 999 chars)\n * @returns\n */\nexport const hashString = (inString: string, trimLength: number = 999) => {\n return crypto\n .createHash(\"sha256\")\n .update(inString)\n .digest(\"hex\")\n .substring(0, trimLength);\n};\n\n/**\n *\n * @param inputString string to truncate\n * @param maxLength max length of this string\n * @returns trimmed string\n */\nexport const trimStringLength = (inputString: string, maxLength: number) => {\n return inputString.length < maxLength\n ? inputString\n : inputString.substring(0, maxLength);\n};\n","export * from \"./aws/aws-types\";\nexport * from \"./git/git-utils\";\nexport * from \"./string/string-utils\";\n","import { RemovalPolicy } from \"aws-cdk-lib\";\nimport {\n BlockPublicAccess,\n Bucket,\n BucketProps,\n ObjectOwnership,\n} from \"aws-cdk-lib/aws-s3\";\nimport { Construct } from \"constructs\";\n\nexport interface PrivateBucketProps extends BucketProps {}\n\nexport class PrivateBucket extends Bucket {\n constructor(scope: Construct, id: string, props: PrivateBucketProps = {}) {\n const defaultProps = {\n removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,\n autoDeleteObjects: props.removalPolicy === RemovalPolicy.DESTROY,\n };\n\n const requiredProps = {\n publicReadAccess: false,\n blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n enforceSSL: true,\n objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,\n };\n\n super(scope, id, { ...defaultProps, ...props, ...requiredProps });\n }\n}\n","// eslint-disable-next-line import/no-extraneous-dependencies\nimport { findGitBranch } from \"@codedrifters/utils\";\nimport { Bucket } from \"aws-cdk-lib/aws-s3\";\nimport { BucketDeployment, Source } from \"aws-cdk-lib/aws-s3-deployment\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { paramCase } from \"change-case\";\nimport { Construct } from \"constructs\";\n\n/*******************************************************************************\n *\n * STATIC CONTENT UPLOADER\n *\n * This construct uploads a directory of content from a local location into S3.\n *\n * To support PR and branch specific builds, each S3 bucket can store content\n * for multiple domains and builds, using the following format:\n *\n * S3-bucket/domain/*\n *\n * A bucket used to store content for stage.openhi.org might have the\n * following directory structure (all in the same bucket).\n *\n * /stage.openhi.org/* -> serves content to stage.openhi.org\n * /feature-7.stage.openhi.org/* -> serves content to feature-7.stage.openhi.org\n * /pr-123.stage.openhi.org/* -> serves content to pr-123.stage.openhi.org\n *\n ******************************************************************************/\n\nexport interface StaticContentProps {\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Absolute path to directory containing content for the website.\n */\n readonly contentSourceDirectory: string;\n\n /**\n * Directory to place content into. Should start with a slash.\n * Example: '/widget'\n */\n readonly contentDestinationDirectory: string;\n\n /**\n * The sub domain prefix (ie: images)\n *\n * @default git branch name\n */\n readonly subDomain?: string;\n\n /**\n * The full domain (ie: staging.codedrifters.com)\n */\n readonly fullDomain: string;\n}\n\nexport class StaticContent extends Construct {\n constructor(scope: Construct, id: string, props: StaticContentProps) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n contentSourceDirectory,\n contentDestinationDirectory,\n subDomain,\n fullDomain,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n subDomain: findGitBranch(),\n ...props,\n };\n\n /***************************************************************************\n *\n * Import and build some values from Param Store during deployment.\n *\n **************************************************************************/\n\n const keyPrefix = [paramCase(subDomain), fullDomain].join(\".\");\n\n const bucketArn = StringParameter.valueForStringParameter(\n this,\n bucketArnParamName,\n );\n const bucket = Bucket.fromBucketArn(this, \"bucket\", bucketArn);\n\n /***************************************************************************\n *\n * Gather the sources we'll be deploying. We need to have an empty source\n * for tests since it will change all the time and bork up the test\n * snapshots if we don't.\n *\n **************************************************************************/\n\n const isTestEnv = process.env.VITEST === \"true\";\n const sources = isTestEnv ? [] : [Source.asset(contentSourceDirectory)];\n\n new BucketDeployment(this, \"deploy\", {\n sources,\n destinationBucket: bucket,\n retainOnDelete: false,\n destinationKeyPrefix: `${keyPrefix}${contentDestinationDirectory}`,\n });\n }\n}\n","import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { Duration, StackProps } from \"aws-cdk-lib\";\nimport {\n Certificate,\n CertificateValidation,\n} from \"aws-cdk-lib/aws-certificatemanager\";\nimport {\n AccessLevel,\n AllowedMethods,\n CacheCookieBehavior,\n CacheHeaderBehavior,\n CachePolicy,\n CacheQueryStringBehavior,\n Distribution,\n LambdaEdgeEventType,\n S3OriginAccessControl,\n Signing,\n ViewerProtocolPolicy,\n} from \"aws-cdk-lib/aws-cloudfront\";\nimport { S3BucketOrigin } from \"aws-cdk-lib/aws-cloudfront-origins\";\nimport { Runtime } from \"aws-cdk-lib/aws-lambda\";\nimport { NodejsFunction } from \"aws-cdk-lib/aws-lambda-nodejs\";\nimport { LogGroup, RetentionDays } from \"aws-cdk-lib/aws-logs\";\nimport {\n ARecord,\n HostedZone,\n HostedZoneAttributes,\n IHostedZone,\n RecordTarget,\n} from \"aws-cdk-lib/aws-route53\";\nimport { CloudFrontTarget } from \"aws-cdk-lib/aws-route53-targets\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { Construct } from \"constructs\";\nimport { PrivateBucket, PrivateBucketProps } from \"../s3/private-bucket\";\n\nexport interface StaticDomainProps {\n /**\n * The base domain (ie: codedrifters.com)\n */\n readonly baseDomain: string;\n\n /**\n * Hosted zone ID for the base domain.\n */\n readonly hostedZoneAttributes: HostedZoneAttributes;\n}\n\nexport interface StaticHostingProps extends StackProps {\n /**\n * Short description used in various places for traceability.\n */\n readonly description?: string;\n\n /**\n * Values used to connect a domain name to the cloudfront distribution. If not\n * supplied, cloudfront doesn't use a custom domain.\n */\n readonly staticDomainProps?: StaticDomainProps;\n\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution Domain Name.\n */\n readonly distributionDomainParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution ID.\n */\n readonly distributionIDParamName?: string;\n\n /**\n * Props to pass to the private S3 bucket.\n */\n readonly privateBucketProps?: PrivateBucketProps;\n}\n\nexport class StaticHosting extends Construct {\n /**\n * Full domain name used as basis for hosting.\n */\n public readonly fullDomain: string;\n\n constructor(scope: Construct, id: string, props: StaticHostingProps = {}) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n distributionDomainParamName,\n distributionIDParamName,\n staticDomainProps,\n privateBucketProps,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n distributionDomainParamName: \"/STATIC_WEBSITE/DISTRIBUTION_DOMAIN\",\n distributionIDParamName: \"/STATIC_WEBSITE/DISTRIBUTION_ID\",\n ...props,\n };\n\n const { baseDomain, hostedZoneAttributes } = staticDomainProps ?? {};\n\n /***************************************************************************\n *\n * PRIVATE BUCKET\n *\n * A bucket to store the files within.\n * Save ARN for later deploys.\n *\n **************************************************************************/\n\n const bucket = new PrivateBucket(\n this,\n \"static-hosting-bucket\",\n privateBucketProps,\n );\n\n /***************************************************************************\n *\n * DNS & Wildcard Certificate\n *\n * If a zone Id as passed in, find the hosted zone and create a wildcard\n * certificate for the domain.\n *\n **************************************************************************/\n\n let zone: IHostedZone | undefined;\n let certificate: Certificate | undefined;\n\n if (hostedZoneAttributes && baseDomain) {\n zone = HostedZone.fromHostedZoneAttributes(\n this,\n \"zone\",\n hostedZoneAttributes,\n );\n certificate = new Certificate(this, \"wildcard-certificate\", {\n domainName: `*.${baseDomain}`,\n subjectAlternativeNames: [baseDomain],\n validation: CertificateValidation.fromDnsMultiZone({\n [`*.${baseDomain}`]: zone,\n [baseDomain]: zone,\n }),\n });\n }\n\n /******************************************************************************\n *\n * LAMBDA@EDGE FUNCTION\n *\n * This handles rewriting the path from domain name.\n *\n *****************************************************************************/\n\n // Explicit entry required: when omitted, NodejsFunction infers the path from the\n // call site (the built lib/index.js), so it looks for index.viewer-request-handler.js\n // in the package. That file is only emitted if we add it as a separate tsup entry.\n // Use .js when present (built package); fall back to .ts for tests running from source.\n const handlerJs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.js\",\n );\n const handlerTs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.ts\",\n );\n const handlerEntry = fs.existsSync(handlerJs) ? handlerJs : handlerTs;\n\n const handler = new NodejsFunction(this, \"viewer-request-handler\", {\n entry: handlerEntry,\n memorySize: 128,\n runtime: Runtime.NODEJS_24_X,\n logGroup: new LogGroup(this, \"viewer-request-handler-log-group\", {\n retention: RetentionDays.ONE_MONTH,\n }),\n });\n\n /******************************************************************************\n *\n * CLOUDFRONT CONFIG\n *\n * Setup a CloudFront Distribution for the bucket.\n *\n *****************************************************************************/\n\n const cachePolicy = new CachePolicy(this, \"cloudfront-policy\", {\n comment: \"Relatively conservative TTL policy.\",\n maxTtl: Duration.seconds(300),\n minTtl: Duration.seconds(0),\n defaultTtl: Duration.seconds(60),\n headerBehavior: CacheHeaderBehavior.none(),\n queryStringBehavior: CacheQueryStringBehavior.none(),\n cookieBehavior: CacheCookieBehavior.none(),\n enableAcceptEncodingGzip: true,\n enableAcceptEncodingBrotli: true,\n });\n\n const oac = new S3OriginAccessControl(this, \"MyOAC\", {\n signing: Signing.SIGV4_NO_OVERRIDE,\n });\n const origin = S3BucketOrigin.withOriginAccessControl(bucket, {\n originAccessControl: oac,\n originAccessLevels: [AccessLevel.READ],\n });\n\n const distribution = new Distribution(this, \"cloudfront-distribution\", {\n comment: `Distribution for ${props.description ?? id}`,\n\n /**\n * Only if domain was supplied\n */\n ...(certificate && baseDomain\n ? {\n certificate,\n domainNames: [baseDomain, `*.${baseDomain}`],\n }\n : {}),\n\n defaultBehavior: {\n origin,\n viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n cachePolicy,\n allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,\n edgeLambdas: [\n {\n functionVersion: handler.currentVersion,\n eventType: LambdaEdgeEventType.VIEWER_REQUEST,\n },\n ],\n },\n defaultRootObject: \"index.html\",\n });\n\n /**\n * We finally have enough information to set the full domain.\n */\n this.fullDomain =\n certificate && baseDomain ? baseDomain : distribution.domainName;\n\n /***************************************************************************\n *\n * DNS ENTRY\n *\n * Link cloudfront to both the root fulldomain and all possible subdomains.\n *\n **************************************************************************/\n\n if (zone) {\n new ARecord(this, \"root-dns-entry\", {\n zone,\n recordName: baseDomain ? baseDomain : \"\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n\n new ARecord(this, \"wc-dns-entry\", {\n zone,\n recordName: baseDomain ? `*.${baseDomain}` : \"*\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n }\n\n /***************************************************************************\n *\n * EXPORTS\n *\n * Used by content uploader later.\n *\n **************************************************************************/\n\n new StringParameter(this, \"dist-domain\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionDomainParamName,\n stringValue: distribution.domainName,\n });\n\n new StringParameter(this, \"dist-id\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionIDParamName,\n stringValue: distribution.distributionId,\n });\n\n new StringParameter(this, \"bucket-arn\", {\n description: `GENERATED DO NOT CHANGE - S3 Bucket ARN for (${props.description ?? id}).`,\n parameterName: bucketArnParamName,\n stringValue: bucket.bucketArn,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;AAKa,YAAA,iBAAiB;;;;MAI5B,KAAK;;;;MAKL,OAAO;;;;MAKP,MAAM;;AAYK,YAAA,yBAAyB;;;;;MAKpC,SAAS;;;;;MAKT,WAAW;;AAcA,YAAA,uBAAuB,QAAA;;;;;;;;;;ACvDpC,QAAA,uBAAA,UAAA,eAAA;AAQO,QAAMA,iBAAgB,MAAa;AACxC,cAAO,GAAA,qBAAA,UAAS,iCAAiC,EAC9C,SAAS,MAAM,EACf,QAAQ,cAAc,EAAE;IAC7B;AAJa,YAAA,gBAAaA;AAMnB,QAAM,kBAAkB,MAAa;AAI1C,UAAI,QAAQ,IAAI,mBAAmB;AACjC,eAAO,QAAQ,IAAI;MACrB;AAKA,YAAM,UAAS,GAAA,qBAAA,UAAS,oCAAoC,EACzD,SAAS,MAAM,EACf,QAAQ,cAAc,EAAE,EACxB,KAAI;AAEP,YAAM,QAAQ,OAAO,MAAM,iCAAiC;AAC5D,YAAM,WAAW,QAAQ,MAAM,CAAC,IAAI;AAEpC,aAAO;IACT;AApBa,YAAA,kBAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACd5B,QAAA,SAAA,aAAA,UAAA,QAAA,CAAA;AAQO,QAAM,aAAa,CAAC,UAAkB,aAAqB,QAAO;AACvE,aAAO,OACJ,WAAW,QAAQ,EACnB,OAAO,QAAQ,EACf,OAAO,KAAK,EACZ,UAAU,GAAG,UAAU;IAC5B;AANa,YAAA,aAAU;AAchB,QAAM,mBAAmB,CAAC,aAAqB,cAAqB;AACzE,aAAO,YAAY,SAAS,YACxB,cACA,YAAY,UAAU,GAAG,SAAS;IACxC;AAJa,YAAA,mBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;ACtB7B,iBAAA,qBAAA,OAAA;AACA,iBAAA,qBAAA,OAAA;AACA,iBAAA,wBAAA,OAAA;;;;;ACFA,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAKA,IAAM,gBAAN,cAA4B,OAAO;AAAA,EACxC,YAAY,OAAkB,IAAY,QAA4B,CAAC,GAAG;AACxE,UAAM,eAAe;AAAA,MACnB,eAAe,MAAM,iBAAiB,cAAc;AAAA,MACpD,mBAAmB,MAAM,kBAAkB,cAAc;AAAA,IAC3D;AAEA,UAAM,gBAAgB;AAAA,MACpB,kBAAkB;AAAA,MAClB,mBAAmB,kBAAkB;AAAA,MACrC,YAAY;AAAA,MACZ,iBAAiB,gBAAgB;AAAA,IACnC;AAEA,UAAM,OAAO,IAAI,EAAE,GAAG,cAAc,GAAG,OAAO,GAAG,cAAc,CAAC;AAAA,EAClE;AACF;;;AC1BA,mBAA8B;AAC9B,SAAS,UAAAC,eAAc;AACvB,SAAS,kBAAkB,cAAc;AACzC,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAqDnB,IAAM,gBAAN,cAA4B,UAAU;AAAA,EAC3C,YAAY,OAAkB,IAAY,OAA2B;AACnE,UAAM,OAAO,EAAE;AAUf,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,IAAI;AAAA,MACF,oBAAoB;AAAA,MACpB,eAAW,4BAAc;AAAA,MACzB,GAAG;AAAA,IACL;AAQA,UAAM,YAAY,CAAC,UAAU,SAAS,GAAG,UAAU,EAAE,KAAK,GAAG;AAE7D,UAAM,YAAY,gBAAgB;AAAA,MAChC;AAAA,MACA;AAAA,IACF;AACA,UAAM,SAASA,QAAO,cAAc,MAAM,UAAU,SAAS;AAU7D,UAAM,YAAY,QAAQ,IAAI,WAAW;AACzC,UAAM,UAAU,YAAY,CAAC,IAAI,CAAC,OAAO,MAAM,sBAAsB,CAAC;AAEtE,QAAI,iBAAiB,MAAM,UAAU;AAAA,MACnC;AAAA,MACA,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,sBAAsB,GAAG,SAAS,GAAG,2BAA2B;AAAA,IAClE,CAAC;AAAA,EACH;AACF;;;ACnHA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,gBAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAC/B,SAAS,UAAU,qBAAqB;AACxC;AAAA,EACE;AAAA,EACA;AAAA,EAGA;AAAA,OACK;AACP,SAAS,wBAAwB;AACjC,SAAS,mBAAAC,wBAAuB;AAChC,SAAS,aAAAC,kBAAiB;AAiDnB,IAAM,gBAAN,cAA4BC,WAAU;AAAA,EAM3C,YAAY,OAAkB,IAAY,QAA4B,CAAC,GAAG;AACxE,UAAM,OAAO,EAAE;AAUf,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,IAAI;AAAA,MACF,oBAAoB;AAAA,MACpB,6BAA6B;AAAA,MAC7B,yBAAyB;AAAA,MACzB,GAAG;AAAA,IACL;AAEA,UAAM,EAAE,YAAY,qBAAqB,IAAI,qBAAqB,CAAC;AAWnE,UAAM,SAAS,IAAI;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAWA,QAAI;AACJ,QAAI;AAEJ,QAAI,wBAAwB,YAAY;AACtC,aAAO,WAAW;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,oBAAc,IAAI,YAAY,MAAM,wBAAwB;AAAA,QAC1D,YAAY,KAAK,UAAU;AAAA,QAC3B,yBAAyB,CAAC,UAAU;AAAA,QACpC,YAAY,sBAAsB,iBAAiB;AAAA,UACjD,CAAC,KAAK,UAAU,EAAE,GAAG;AAAA,UACrB,CAAC,UAAU,GAAG;AAAA,QAChB,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAcA,UAAM,YAAiB;AAAA,MACrB;AAAA,MACA;AAAA,IACF;AACA,UAAM,YAAiB;AAAA,MACrB;AAAA,MACA;AAAA,IACF;AACA,UAAM,eAAkB,cAAW,SAAS,IAAI,YAAY;AAE5D,UAAM,UAAU,IAAI,eAAe,MAAM,0BAA0B;AAAA,MACjE,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,SAAS,QAAQ;AAAA,MACjB,UAAU,IAAI,SAAS,MAAM,oCAAoC;AAAA,QAC/D,WAAW,cAAc;AAAA,MAC3B,CAAC;AAAA,IACH,CAAC;AAUD,UAAM,cAAc,IAAI,YAAY,MAAM,qBAAqB;AAAA,MAC7D,SAAS;AAAA,MACT,QAAQ,SAAS,QAAQ,GAAG;AAAA,MAC5B,QAAQ,SAAS,QAAQ,CAAC;AAAA,MAC1B,YAAY,SAAS,QAAQ,EAAE;AAAA,MAC/B,gBAAgB,oBAAoB,KAAK;AAAA,MACzC,qBAAqB,yBAAyB,KAAK;AAAA,MACnD,gBAAgB,oBAAoB,KAAK;AAAA,MACzC,0BAA0B;AAAA,MAC1B,4BAA4B;AAAA,IAC9B,CAAC;AAED,UAAM,MAAM,IAAI,sBAAsB,MAAM,SAAS;AAAA,MACnD,SAAS,QAAQ;AAAA,IACnB,CAAC;AACD,UAAM,SAAS,eAAe,wBAAwB,QAAQ;AAAA,MAC5D,qBAAqB;AAAA,MACrB,oBAAoB,CAAC,YAAY,IAAI;AAAA,IACvC,CAAC;AAED,UAAM,eAAe,IAAI,aAAa,MAAM,2BAA2B;AAAA,MACrE,SAAS,oBAAoB,MAAM,eAAe,EAAE;AAAA;AAAA;AAAA;AAAA,MAKpD,GAAI,eAAe,aACf;AAAA,QACE;AAAA,QACA,aAAa,CAAC,YAAY,KAAK,UAAU,EAAE;AAAA,MAC7C,IACA,CAAC;AAAA,MAEL,iBAAiB;AAAA,QACf;AAAA,QACA,sBAAsB,qBAAqB;AAAA,QAC3C;AAAA,QACA,gBAAgB,eAAe;AAAA,QAC/B,aAAa;AAAA,UACX;AAAA,YACE,iBAAiB,QAAQ;AAAA,YACzB,WAAW,oBAAoB;AAAA,UACjC;AAAA,QACF;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,IACrB,CAAC;AAKD,SAAK,aACH,eAAe,aAAa,aAAa,aAAa;AAUxD,QAAI,MAAM;AACR,UAAI,QAAQ,MAAM,kBAAkB;AAAA,QAClC;AAAA,QACA,YAAY,aAAa,aAAa;AAAA,QACtC,QAAQ,aAAa,UAAU,IAAI,iBAAiB,YAAY,CAAC;AAAA,MACnE,CAAC;AAED,UAAI,QAAQ,MAAM,gBAAgB;AAAA,QAChC;AAAA,QACA,YAAY,aAAa,KAAK,UAAU,KAAK;AAAA,QAC7C,QAAQ,aAAa,UAAU,IAAI,iBAAiB,YAAY,CAAC;AAAA,MACnE,CAAC;AAAA,IACH;AAUA,QAAIC,iBAAgB,MAAM,eAAe;AAAA,MACvC,aAAa,8DAA8D,MAAM,eAAe,EAAE;AAAA,MAClG,eAAe;AAAA,MACf,aAAa,aAAa;AAAA,IAC5B,CAAC;AAED,QAAIA,iBAAgB,MAAM,WAAW;AAAA,MACnC,aAAa,8DAA8D,MAAM,eAAe,EAAE;AAAA,MAClG,eAAe;AAAA,MACf,aAAa,aAAa;AAAA,IAC5B,CAAC;AAED,QAAIA,iBAAgB,MAAM,cAAc;AAAA,MACtC,aAAa,gDAAgD,MAAM,eAAe,EAAE;AAAA,MACpF,eAAe;AAAA,MACf,aAAa,OAAO;AAAA,IACtB,CAAC;AAAA,EACH;AACF;","names":["findGitBranch","Bucket","StringParameter","Construct","Construct","StringParameter"]}
|
|
1
|
+
{"version":3,"sources":["../../utils/src/aws/aws-types.ts","../../utils/src/git/git-utils.ts","../../utils/src/string/string-utils.ts","../../utils/src/index.ts","../src/s3/private-bucket.ts","../src/static-hosting/static-content.ts","../src/static-hosting/static-hosting.ts"],"sourcesContent":["/**\n * Stage Types\n *\n * What stage of deployment is this? Dev, staging, or prod?\n */\nexport const AWS_STAGE_TYPE = {\n /**\n * Development environment, typically used for testing and development.\n */\n DEV: \"dev\",\n\n /**\n * Staging environment, used for pre-production testing.\n */\n STAGE: \"stage\",\n\n /**\n * Production environment, used for live deployments.\n */\n PROD: \"prod\",\n} as const;\n\n/**\n * Above const as a type.\n */\nexport type AwsStageType = (typeof AWS_STAGE_TYPE)[keyof typeof AWS_STAGE_TYPE];\n\n/**\n * Deployment target role: whether an (account, region) is the primary or\n * secondary deployment target (e.g. primary vs replica region).\n */\nexport const DEPLOYMENT_TARGET_ROLE = {\n /**\n * Account and region that represents the primary region for this service.\n * For example, the base DynamoDB Region for global tables.\n */\n PRIMARY: \"primary\",\n /**\n * Account and region that represents a secondary region for this service.\n * For example, a replica region for a global DynamoDB table.\n */\n SECONDARY: \"secondary\",\n} as const;\n\n/**\n * Type for deployment target role values.\n */\nexport type DeploymentTargetRoleType =\n (typeof DEPLOYMENT_TARGET_ROLE)[keyof typeof DEPLOYMENT_TARGET_ROLE];\n\n/**\n * Environment types (primary/secondary).\n *\n * @deprecated Use {@link DEPLOYMENT_TARGET_ROLE} instead. This constant is maintained for backward compatibility.\n */\nexport const AWS_ENVIRONMENT_TYPE = DEPLOYMENT_TARGET_ROLE;\n\n/**\n * Type for environment type values.\n *\n * @deprecated Use {@link DeploymentTargetRoleType} instead. This type is maintained for backward compatibility.\n */\nexport type AwsEnvironmentType = DeploymentTargetRoleType;\n","import { execSync } from \"node:child_process\";\n\n/**\n * Returns the current full git branch name\n *\n * ie: feature/1234 returns feature/1234\n *\n */\nexport const findGitBranch = (): string => {\n return execSync(\"git rev-parse --abbrev-ref HEAD\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\");\n};\n\nexport const findGitRepoName = (): string => {\n /**\n * When running in github actions this will be populated.\n */\n if (process.env.GITHUB_REPOSITORY) {\n return process.env.GITHUB_REPOSITORY;\n }\n\n /**\n * locally, we need to extract the repo name from the git config.\n */\n const remote = execSync(\"git config --get remote.origin.url\")\n .toString(\"utf8\")\n .replace(/[\\n\\r\\s]+$/, \"\")\n .trim();\n\n const match = remote.match(/[:\\/]([^/]+\\/[^/]+?)(?:\\.git)?$/);\n const repoName = match ? match[1] : \"error-repo-name\";\n\n return repoName;\n};\n","import * as crypto from \"node:crypto\";\n\n/**\n *\n * @param inString string to hash\n * @param trimLength trim to this length (defaults to 999 chars)\n * @returns\n */\nexport const hashString = (inString: string, trimLength: number = 999) => {\n return crypto\n .createHash(\"sha256\")\n .update(inString)\n .digest(\"hex\")\n .substring(0, trimLength);\n};\n\n/**\n *\n * @param inputString string to truncate\n * @param maxLength max length of this string\n * @returns trimmed string\n */\nexport const trimStringLength = (inputString: string, maxLength: number) => {\n return inputString.length < maxLength\n ? inputString\n : inputString.substring(0, maxLength);\n};\n","export * from \"./aws/aws-types\";\nexport * from \"./git/git-utils\";\nexport * from \"./string/string-utils\";\n","import { RemovalPolicy } from \"aws-cdk-lib\";\nimport {\n BlockPublicAccess,\n Bucket,\n BucketProps,\n ObjectOwnership,\n} from \"aws-cdk-lib/aws-s3\";\nimport { Construct } from \"constructs\";\n\nexport interface PrivateBucketProps extends BucketProps {}\n\nexport class PrivateBucket extends Bucket {\n constructor(scope: Construct, id: string, props: PrivateBucketProps = {}) {\n const defaultProps = {\n removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,\n autoDeleteObjects: props.removalPolicy === RemovalPolicy.DESTROY,\n };\n\n const requiredProps = {\n publicReadAccess: false,\n blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n enforceSSL: true,\n objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,\n };\n\n super(scope, id, { ...defaultProps, ...props, ...requiredProps });\n }\n}\n","// eslint-disable-next-line import/no-extraneous-dependencies\nimport { findGitBranch } from \"@codedrifters/utils\";\nimport { Bucket } from \"aws-cdk-lib/aws-s3\";\nimport { BucketDeployment, Source } from \"aws-cdk-lib/aws-s3-deployment\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { paramCase } from \"change-case\";\nimport { Construct } from \"constructs\";\n\n/*******************************************************************************\n *\n * STATIC CONTENT UPLOADER\n *\n * This construct uploads a directory of content from a local location into S3.\n *\n * To support PR and branch specific builds, each S3 bucket can store content\n * for multiple domains and builds, using the following format:\n *\n * S3-bucket/domain/*\n *\n * A bucket used to store content for stage.openhi.org might have the\n * following directory structure (all in the same bucket).\n *\n * /stage.openhi.org/* -> serves content to stage.openhi.org\n * /feature-7.stage.openhi.org/* -> serves content to feature-7.stage.openhi.org\n * /pr-123.stage.openhi.org/* -> serves content to pr-123.stage.openhi.org\n *\n ******************************************************************************/\n\nexport interface StaticContentProps {\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Absolute path to directory containing content for the website.\n */\n readonly contentSourceDirectory: string;\n\n /**\n * Directory to place content into. Should start with a slash.\n * Example: '/widget'\n */\n readonly contentDestinationDirectory: string;\n\n /**\n * The sub domain prefix (ie: images)\n *\n * @default git branch name\n */\n readonly subDomain?: string;\n\n /**\n * The full domain (ie: staging.codedrifters.com)\n */\n readonly fullDomain: string;\n}\n\nexport class StaticContent extends Construct {\n constructor(scope: Construct, id: string, props: StaticContentProps) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n contentSourceDirectory,\n contentDestinationDirectory,\n subDomain,\n fullDomain,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n subDomain: findGitBranch(),\n ...props,\n };\n\n /***************************************************************************\n *\n * Import and build some values from Param Store during deployment.\n *\n **************************************************************************/\n\n const keyPrefix = [paramCase(subDomain), fullDomain].join(\".\");\n\n const bucketArn = StringParameter.valueForStringParameter(\n this,\n bucketArnParamName,\n );\n const bucket = Bucket.fromBucketArn(this, \"bucket\", bucketArn);\n\n /***************************************************************************\n *\n * Gather the sources we'll be deploying. We need to have an empty source\n * for tests since it will change all the time and bork up the test\n * snapshots if we don't.\n *\n **************************************************************************/\n\n const isTestEnv = process.env.VITEST === \"true\";\n const sources = isTestEnv ? [] : [Source.asset(contentSourceDirectory)];\n\n new BucketDeployment(this, \"deploy\", {\n sources,\n destinationBucket: bucket,\n retainOnDelete: false,\n destinationKeyPrefix: `${keyPrefix}${contentDestinationDirectory}`,\n });\n }\n}\n","import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { Duration, StackProps } from \"aws-cdk-lib\";\nimport {\n Certificate,\n CertificateValidation,\n} from \"aws-cdk-lib/aws-certificatemanager\";\nimport {\n AccessLevel,\n AllowedMethods,\n CacheCookieBehavior,\n CacheHeaderBehavior,\n CachePolicy,\n CacheQueryStringBehavior,\n Distribution,\n LambdaEdgeEventType,\n S3OriginAccessControl,\n Signing,\n ViewerProtocolPolicy,\n} from \"aws-cdk-lib/aws-cloudfront\";\nimport { S3BucketOrigin } from \"aws-cdk-lib/aws-cloudfront-origins\";\nimport { Runtime } from \"aws-cdk-lib/aws-lambda\";\nimport { NodejsFunction } from \"aws-cdk-lib/aws-lambda-nodejs\";\nimport { LogGroup, RetentionDays } from \"aws-cdk-lib/aws-logs\";\nimport {\n ARecord,\n HostedZone,\n HostedZoneAttributes,\n IHostedZone,\n RecordTarget,\n} from \"aws-cdk-lib/aws-route53\";\nimport { CloudFrontTarget } from \"aws-cdk-lib/aws-route53-targets\";\nimport { StringParameter } from \"aws-cdk-lib/aws-ssm\";\nimport { Construct } from \"constructs\";\nimport type { HostingMode } from \"./static-hosting.viewer-request-handler\";\nimport { PrivateBucket, PrivateBucketProps } from \"../s3/private-bucket\";\n\nexport interface StaticDomainProps {\n /**\n * The base domain (ie: codedrifters.com)\n */\n readonly baseDomain: string;\n\n /**\n * Hosted zone ID for the base domain.\n */\n readonly hostedZoneAttributes: HostedZoneAttributes;\n}\n\nexport interface StaticHostingProps extends StackProps {\n /**\n * Short description used in various places for traceability.\n */\n readonly description?: string;\n\n /**\n * Values used to connect a domain name to the cloudfront distribution. If not\n * supplied, cloudfront doesn't use a custom domain.\n */\n readonly staticDomainProps?: StaticDomainProps;\n\n /**\n * Parameter name to use when storing the static hosting bucket's ARN.\n * This is needed in other later steps when deploying hosted content to S3.\n */\n readonly bucketArnParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution Domain Name.\n */\n readonly distributionDomainParamName?: string;\n\n /**\n * Parameter name to use when storing the CloudFront Distribution ID.\n */\n readonly distributionIDParamName?: string;\n\n /**\n * Props to pass to the private S3 bucket.\n */\n readonly privateBucketProps?: PrivateBucketProps;\n\n /**\n * Selects how path-like URIs are rewritten by the viewer-request\n * Lambda@Edge handler.\n *\n * - `spa` (default): path-like URIs rewrite to `/index.html` so a\n * single-page app can serve its one root index and let the client-side\n * router handle the path.\n * - `static`: path-like URIs append `/index.html` (e.g. `/docs` →\n * `/docs/index.html`) so multi-page static sites can serve distinct\n * HTML per path.\n *\n * Multi-tenant domain-folder prepending runs after the rewrite in both\n * modes and is unaffected by this prop.\n *\n * @default \"spa\"\n */\n readonly hostingMode?: HostingMode;\n}\n\nexport class StaticHosting extends Construct {\n /**\n * Full domain name used as basis for hosting.\n */\n public readonly fullDomain: string;\n\n constructor(scope: Construct, id: string, props: StaticHostingProps = {}) {\n super(scope, id);\n\n /***************************************************************************\n *\n * Initial Setup\n *\n * Set some defaults, build domain information.\n *\n **************************************************************************/\n\n const {\n bucketArnParamName,\n distributionDomainParamName,\n distributionIDParamName,\n staticDomainProps,\n privateBucketProps,\n hostingMode,\n } = {\n bucketArnParamName: \"/STATIC_WEBSITE/BUCKET_ARN\",\n distributionDomainParamName: \"/STATIC_WEBSITE/DISTRIBUTION_DOMAIN\",\n distributionIDParamName: \"/STATIC_WEBSITE/DISTRIBUTION_ID\",\n hostingMode: \"spa\" as HostingMode,\n ...props,\n };\n\n const { baseDomain, hostedZoneAttributes } = staticDomainProps ?? {};\n\n /***************************************************************************\n *\n * PRIVATE BUCKET\n *\n * A bucket to store the files within.\n * Save ARN for later deploys.\n *\n **************************************************************************/\n\n const bucket = new PrivateBucket(\n this,\n \"static-hosting-bucket\",\n privateBucketProps,\n );\n\n /***************************************************************************\n *\n * DNS & Wildcard Certificate\n *\n * If a zone Id as passed in, find the hosted zone and create a wildcard\n * certificate for the domain.\n *\n **************************************************************************/\n\n let zone: IHostedZone | undefined;\n let certificate: Certificate | undefined;\n\n if (hostedZoneAttributes && baseDomain) {\n zone = HostedZone.fromHostedZoneAttributes(\n this,\n \"zone\",\n hostedZoneAttributes,\n );\n certificate = new Certificate(this, \"wildcard-certificate\", {\n domainName: `*.${baseDomain}`,\n subjectAlternativeNames: [baseDomain],\n validation: CertificateValidation.fromDnsMultiZone({\n [`*.${baseDomain}`]: zone,\n [baseDomain]: zone,\n }),\n });\n }\n\n /******************************************************************************\n *\n * LAMBDA@EDGE FUNCTION\n *\n * This handles rewriting the path from domain name.\n *\n *****************************************************************************/\n\n // Explicit entry required: when omitted, NodejsFunction infers the path from the\n // call site (the built lib/index.js), so it looks for index.viewer-request-handler.js\n // in the package. That file is only emitted if we add it as a separate tsup entry.\n // Use .js when present (built package); fall back to .ts for tests running from source.\n const handlerJs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.js\",\n );\n const handlerTs = path.join(\n __dirname,\n \"static-hosting.viewer-request-handler.ts\",\n );\n const handlerEntry = fs.existsSync(handlerJs) ? handlerJs : handlerTs;\n\n const handler = new NodejsFunction(this, \"viewer-request-handler\", {\n entry: handlerEntry,\n handler: hostingMode === \"static\" ? \"staticHandler\" : \"spaHandler\",\n memorySize: 128,\n runtime: Runtime.NODEJS_24_X,\n logGroup: new LogGroup(this, \"viewer-request-handler-log-group\", {\n retention: RetentionDays.ONE_MONTH,\n }),\n });\n\n /******************************************************************************\n *\n * CLOUDFRONT CONFIG\n *\n * Setup a CloudFront Distribution for the bucket.\n *\n *****************************************************************************/\n\n const cachePolicy = new CachePolicy(this, \"cloudfront-policy\", {\n comment: \"Relatively conservative TTL policy.\",\n maxTtl: Duration.seconds(300),\n minTtl: Duration.seconds(0),\n defaultTtl: Duration.seconds(60),\n headerBehavior: CacheHeaderBehavior.none(),\n queryStringBehavior: CacheQueryStringBehavior.none(),\n cookieBehavior: CacheCookieBehavior.none(),\n enableAcceptEncodingGzip: true,\n enableAcceptEncodingBrotli: true,\n });\n\n const oac = new S3OriginAccessControl(this, \"MyOAC\", {\n signing: Signing.SIGV4_NO_OVERRIDE,\n });\n const origin = S3BucketOrigin.withOriginAccessControl(bucket, {\n originAccessControl: oac,\n originAccessLevels: [AccessLevel.READ],\n });\n\n const distribution = new Distribution(this, \"cloudfront-distribution\", {\n comment: `Distribution for ${props.description ?? id}`,\n\n /**\n * Only if domain was supplied\n */\n ...(certificate && baseDomain\n ? {\n certificate,\n domainNames: [baseDomain, `*.${baseDomain}`],\n }\n : {}),\n\n defaultBehavior: {\n origin,\n viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n cachePolicy,\n allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,\n edgeLambdas: [\n {\n functionVersion: handler.currentVersion,\n eventType: LambdaEdgeEventType.VIEWER_REQUEST,\n },\n ],\n },\n defaultRootObject: \"index.html\",\n });\n\n /**\n * We finally have enough information to set the full domain.\n */\n this.fullDomain =\n certificate && baseDomain ? baseDomain : distribution.domainName;\n\n /***************************************************************************\n *\n * DNS ENTRY\n *\n * Link cloudfront to both the root fulldomain and all possible subdomains.\n *\n **************************************************************************/\n\n if (zone) {\n new ARecord(this, \"root-dns-entry\", {\n zone,\n recordName: baseDomain ? baseDomain : \"\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n\n new ARecord(this, \"wc-dns-entry\", {\n zone,\n recordName: baseDomain ? `*.${baseDomain}` : \"*\",\n target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)),\n });\n }\n\n /***************************************************************************\n *\n * EXPORTS\n *\n * Used by content uploader later.\n *\n **************************************************************************/\n\n new StringParameter(this, \"dist-domain\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionDomainParamName,\n stringValue: distribution.domainName,\n });\n\n new StringParameter(this, \"dist-id\", {\n description: `GENERATED DO NOT CHANGE - CloudFront Distribution Details (${props.description ?? id}).`,\n parameterName: distributionIDParamName,\n stringValue: distribution.distributionId,\n });\n\n new StringParameter(this, \"bucket-arn\", {\n description: `GENERATED DO NOT CHANGE - S3 Bucket ARN for (${props.description ?? id}).`,\n parameterName: bucketArnParamName,\n stringValue: bucket.bucketArn,\n });\n }\n}\n"],"mappings":"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","names":["findGitBranch","Bucket","StringParameter","Construct","Construct","StringParameter"]}
|
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
import { CloudFrontRequest, CloudFrontRequestEvent } from 'aws-lambda';
|
|
2
2
|
|
|
3
|
-
declare const handler: (event: CloudFrontRequestEvent) => Promise<CloudFrontRequest>;
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
* to make testing easier.
|
|
4
|
+
* Hosting mode controls how path-like URIs get a default document.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* - `spa`: path-like URIs (e.g. `/dashboard`, `/patients/123`) rewrite to
|
|
7
|
+
* `/index.html` so the single-page app's root index is served and the
|
|
8
|
+
* client-side router handles the path.
|
|
9
|
+
* - `static`: path-like URIs append `/index.html` (e.g. `/docs` becomes
|
|
10
|
+
* `/docs/index.html`) so multi-page static sites can serve distinct
|
|
11
|
+
* HTML per path.
|
|
12
|
+
*/
|
|
13
|
+
type HostingMode = "spa" | "static";
|
|
14
|
+
/**
|
|
15
|
+
* Viewer Request Handler for SPA mode.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* - Logs the request
|
|
18
|
+
* - Rewrites path-like URIs to `/index.html`
|
|
19
|
+
* - Prepends the Host header as a folder for multi-tenant routing
|
|
16
20
|
*/
|
|
17
|
-
declare const
|
|
21
|
+
declare const spaHandler: (event: CloudFrontRequestEvent) => Promise<CloudFrontRequest>;
|
|
18
22
|
/**
|
|
19
|
-
*
|
|
20
|
-
* to make testing easier.
|
|
23
|
+
* Viewer Request Handler for static (non-SPA) mode.
|
|
21
24
|
*
|
|
22
|
-
*
|
|
25
|
+
* - Logs the request
|
|
26
|
+
* - Rewrites path-like URIs to `<path>/index.html`
|
|
27
|
+
* - Prepends the Host header as a folder for multi-tenant routing
|
|
28
|
+
*/
|
|
29
|
+
declare const staticHandler: (event: CloudFrontRequestEvent) => Promise<CloudFrontRequest>;
|
|
30
|
+
/**
|
|
31
|
+
* Adds a default document to the request URI based on the hosting mode.
|
|
23
32
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
33
|
+
* In both modes:
|
|
34
|
+
* - `/` and `/index.html` become `/index.html`
|
|
35
|
+
* - URIs whose last segment contains a dot are treated as static files and
|
|
36
|
+
* left unchanged.
|
|
37
|
+
*
|
|
38
|
+
* In `spa` mode, all other path-like URIs rewrite to `/index.html`.
|
|
39
|
+
* In `static` mode, path-like URIs append `/index.html` (trailing slash
|
|
40
|
+
* preserved as a single slash): `/docs` and `/docs/` both become
|
|
41
|
+
* `/docs/index.html`.
|
|
42
|
+
*/
|
|
43
|
+
declare const addDefaultDocument: (request: CloudFrontRequest, mode: HostingMode) => CloudFrontRequest;
|
|
44
|
+
/**
|
|
45
|
+
* Prepends the Host header as a folder to the request URI. Used for
|
|
46
|
+
* multi-tenant routing where each domain maps to a folder in the S3
|
|
47
|
+
* bucket (e.g. `example.com/foo` → `/example.com/foo`).
|
|
26
48
|
*/
|
|
27
49
|
declare const addDomainFolder: (request: CloudFrontRequest) => CloudFrontRequest;
|
|
28
50
|
|
|
29
|
-
export { addDefaultDocument, addDomainFolder,
|
|
51
|
+
export { type HostingMode, addDefaultDocument, addDomainFolder, spaHandler, staticHandler };
|
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
import { CloudFrontRequest, CloudFrontRequestEvent } from 'aws-lambda';
|
|
2
2
|
|
|
3
|
-
declare const handler: (event: CloudFrontRequestEvent) => Promise<CloudFrontRequest>;
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
* to make testing easier.
|
|
4
|
+
* Hosting mode controls how path-like URIs get a default document.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* - `spa`: path-like URIs (e.g. `/dashboard`, `/patients/123`) rewrite to
|
|
7
|
+
* `/index.html` so the single-page app's root index is served and the
|
|
8
|
+
* client-side router handles the path.
|
|
9
|
+
* - `static`: path-like URIs append `/index.html` (e.g. `/docs` becomes
|
|
10
|
+
* `/docs/index.html`) so multi-page static sites can serve distinct
|
|
11
|
+
* HTML per path.
|
|
12
|
+
*/
|
|
13
|
+
type HostingMode = "spa" | "static";
|
|
14
|
+
/**
|
|
15
|
+
* Viewer Request Handler for SPA mode.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* - Logs the request
|
|
18
|
+
* - Rewrites path-like URIs to `/index.html`
|
|
19
|
+
* - Prepends the Host header as a folder for multi-tenant routing
|
|
16
20
|
*/
|
|
17
|
-
declare const
|
|
21
|
+
declare const spaHandler: (event: CloudFrontRequestEvent) => Promise<CloudFrontRequest>;
|
|
18
22
|
/**
|
|
19
|
-
*
|
|
20
|
-
* to make testing easier.
|
|
23
|
+
* Viewer Request Handler for static (non-SPA) mode.
|
|
21
24
|
*
|
|
22
|
-
*
|
|
25
|
+
* - Logs the request
|
|
26
|
+
* - Rewrites path-like URIs to `<path>/index.html`
|
|
27
|
+
* - Prepends the Host header as a folder for multi-tenant routing
|
|
28
|
+
*/
|
|
29
|
+
declare const staticHandler: (event: CloudFrontRequestEvent) => Promise<CloudFrontRequest>;
|
|
30
|
+
/**
|
|
31
|
+
* Adds a default document to the request URI based on the hosting mode.
|
|
23
32
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
33
|
+
* In both modes:
|
|
34
|
+
* - `/` and `/index.html` become `/index.html`
|
|
35
|
+
* - URIs whose last segment contains a dot are treated as static files and
|
|
36
|
+
* left unchanged.
|
|
37
|
+
*
|
|
38
|
+
* In `spa` mode, all other path-like URIs rewrite to `/index.html`.
|
|
39
|
+
* In `static` mode, path-like URIs append `/index.html` (trailing slash
|
|
40
|
+
* preserved as a single slash): `/docs` and `/docs/` both become
|
|
41
|
+
* `/docs/index.html`.
|
|
42
|
+
*/
|
|
43
|
+
declare const addDefaultDocument: (request: CloudFrontRequest, mode: HostingMode) => CloudFrontRequest;
|
|
44
|
+
/**
|
|
45
|
+
* Prepends the Host header as a folder to the request URI. Used for
|
|
46
|
+
* multi-tenant routing where each domain maps to a folder in the S3
|
|
47
|
+
* bucket (e.g. `example.com/foo` → `/example.com/foo`).
|
|
26
48
|
*/
|
|
27
49
|
declare const addDomainFolder: (request: CloudFrontRequest) => CloudFrontRequest;
|
|
28
50
|
|
|
29
|
-
export { addDefaultDocument, addDomainFolder,
|
|
51
|
+
export { type HostingMode, addDefaultDocument, addDomainFolder, spaHandler, staticHandler };
|
|
@@ -22,23 +22,30 @@ var static_hosting_viewer_request_handler_exports = {};
|
|
|
22
22
|
__export(static_hosting_viewer_request_handler_exports, {
|
|
23
23
|
addDefaultDocument: () => addDefaultDocument,
|
|
24
24
|
addDomainFolder: () => addDomainFolder,
|
|
25
|
-
|
|
25
|
+
spaHandler: () => spaHandler,
|
|
26
|
+
staticHandler: () => staticHandler
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(static_hosting_viewer_request_handler_exports);
|
|
28
29
|
var isTestEnv = process.env.VITEST === "true";
|
|
29
|
-
var
|
|
30
|
+
var spaHandler = async (event) => {
|
|
31
|
+
return runHandler(event, "spa");
|
|
32
|
+
};
|
|
33
|
+
var staticHandler = async (event) => {
|
|
34
|
+
return runHandler(event, "static");
|
|
35
|
+
};
|
|
36
|
+
var runHandler = async (event, mode) => {
|
|
30
37
|
if (!isTestEnv) {
|
|
31
38
|
console.log("Request Event: ", JSON.stringify(event, null, 2));
|
|
32
39
|
}
|
|
33
40
|
let request = event.Records[0].cf.request;
|
|
34
|
-
request = addDefaultDocument(request);
|
|
41
|
+
request = addDefaultDocument(request, mode);
|
|
35
42
|
request = addDomainFolder(request);
|
|
36
43
|
if (!isTestEnv) {
|
|
37
44
|
console.log("Resulting Request: ", JSON.stringify(request, null, 2));
|
|
38
45
|
}
|
|
39
46
|
return request;
|
|
40
47
|
};
|
|
41
|
-
var addDefaultDocument = (request) => {
|
|
48
|
+
var addDefaultDocument = (request, mode) => {
|
|
42
49
|
if (request.uri === "/" || request.uri === "/index.html") {
|
|
43
50
|
request.uri = "/index.html";
|
|
44
51
|
return request;
|
|
@@ -49,7 +56,12 @@ var addDefaultDocument = (request) => {
|
|
|
49
56
|
if (looksLikeStaticFile) {
|
|
50
57
|
return request;
|
|
51
58
|
}
|
|
52
|
-
|
|
59
|
+
if (mode === "spa") {
|
|
60
|
+
request.uri = "/index.html";
|
|
61
|
+
return request;
|
|
62
|
+
}
|
|
63
|
+
const normalized = request.uri.endsWith("/") ? request.uri.slice(0, -1) : request.uri;
|
|
64
|
+
request.uri = `${normalized}/index.html`;
|
|
53
65
|
return request;
|
|
54
66
|
};
|
|
55
67
|
var addDomainFolder = (request) => {
|
|
@@ -61,6 +73,7 @@ var addDomainFolder = (request) => {
|
|
|
61
73
|
0 && (module.exports = {
|
|
62
74
|
addDefaultDocument,
|
|
63
75
|
addDomainFolder,
|
|
64
|
-
|
|
76
|
+
spaHandler,
|
|
77
|
+
staticHandler
|
|
65
78
|
});
|
|
66
79
|
//# sourceMappingURL=static-hosting.viewer-request-handler.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/static-hosting/static-hosting.viewer-request-handler.ts"],"sourcesContent":["import { CloudFrontRequest, CloudFrontRequestEvent } from \"aws-lambda\";\n\n/**\n
|
|
1
|
+
{"version":3,"sources":["../src/static-hosting/static-hosting.viewer-request-handler.ts"],"sourcesContent":["import { CloudFrontRequest, CloudFrontRequestEvent } from \"aws-lambda\";\n\n/**\n * Hosting mode controls how path-like URIs get a default document.\n *\n * - `spa`: path-like URIs (e.g. `/dashboard`, `/patients/123`) rewrite to\n * `/index.html` so the single-page app's root index is served and the\n * client-side router handles the path.\n * - `static`: path-like URIs append `/index.html` (e.g. `/docs` becomes\n * `/docs/index.html`) so multi-page static sites can serve distinct\n * HTML per path.\n */\nexport type HostingMode = \"spa\" | \"static\";\n\nconst isTestEnv = process.env.VITEST === \"true\";\n\n/**\n * Viewer Request Handler for SPA mode.\n *\n * - Logs the request\n * - Rewrites path-like URIs to `/index.html`\n * - Prepends the Host header as a folder for multi-tenant routing\n */\nexport const spaHandler = async (\n event: CloudFrontRequestEvent,\n): Promise<CloudFrontRequest> => {\n return runHandler(event, \"spa\");\n};\n\n/**\n * Viewer Request Handler for static (non-SPA) mode.\n *\n * - Logs the request\n * - Rewrites path-like URIs to `<path>/index.html`\n * - Prepends the Host header as a folder for multi-tenant routing\n */\nexport const staticHandler = async (\n event: CloudFrontRequestEvent,\n): Promise<CloudFrontRequest> => {\n return runHandler(event, \"static\");\n};\n\nconst runHandler = async (\n event: CloudFrontRequestEvent,\n mode: HostingMode,\n): Promise<CloudFrontRequest> => {\n if (!isTestEnv) {\n console.log(\"Request Event: \", JSON.stringify(event, null, 2));\n }\n\n let request = event.Records[0].cf.request;\n\n // add index if needed\n request = addDefaultDocument(request, mode);\n\n // prepend folder with domain\n request = addDomainFolder(request);\n\n if (!isTestEnv) {\n console.log(\"Resulting Request: \", JSON.stringify(request, null, 2));\n }\n\n return request;\n};\n\n/**\n * Adds a default document to the request URI based on the hosting mode.\n *\n * In both modes:\n * - `/` and `/index.html` become `/index.html`\n * - URIs whose last segment contains a dot are treated as static files and\n * left unchanged.\n *\n * In `spa` mode, all other path-like URIs rewrite to `/index.html`.\n * In `static` mode, path-like URIs append `/index.html` (trailing slash\n * preserved as a single slash): `/docs` and `/docs/` both become\n * `/docs/index.html`.\n */\nexport const addDefaultDocument = (\n request: CloudFrontRequest,\n mode: HostingMode,\n) => {\n if (request.uri === \"/\" || request.uri === \"/index.html\") {\n request.uri = \"/index.html\";\n return request;\n }\n\n const segments = request.uri.split(\"/\").filter(Boolean);\n const lastSegment = segments[segments.length - 1] ?? \"\";\n const looksLikeStaticFile = lastSegment.includes(\".\");\n\n if (looksLikeStaticFile) {\n return request;\n }\n\n if (mode === \"spa\") {\n request.uri = \"/index.html\";\n return request;\n }\n\n // static mode: append /index.html to the path, collapsing any trailing slash\n const normalized = request.uri.endsWith(\"/\")\n ? request.uri.slice(0, -1)\n : request.uri;\n request.uri = `${normalized}/index.html`;\n return request;\n};\n\n/**\n * Prepends the Host header as a folder to the request URI. Used for\n * multi-tenant routing where each domain maps to a folder in the S3\n * bucket (e.g. `example.com/foo` → `/example.com/foo`).\n */\nexport const addDomainFolder = (request: CloudFrontRequest) => {\n const hostHeader = request.headers.host[0].value;\n request.uri = `/${hostHeader}${request.uri}`;\n return request;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,IAAM,YAAY,QAAQ,IAAI,WAAW;AASlC,IAAM,aAAa,OACxB,UAC+B;AAC/B,SAAO,WAAW,OAAO,KAAK;AAChC;AASO,IAAM,gBAAgB,OAC3B,UAC+B;AAC/B,SAAO,WAAW,OAAO,QAAQ;AACnC;AAEA,IAAM,aAAa,OACjB,OACA,SAC+B;AAC/B,MAAI,CAAC,WAAW;AACd,YAAQ,IAAI,mBAAmB,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,EAC/D;AAEA,MAAI,UAAU,MAAM,QAAQ,CAAC,EAAE,GAAG;AAGlC,YAAU,mBAAmB,SAAS,IAAI;AAG1C,YAAU,gBAAgB,OAAO;AAEjC,MAAI,CAAC,WAAW;AACd,YAAQ,IAAI,uBAAuB,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,EACrE;AAEA,SAAO;AACT;AAeO,IAAM,qBAAqB,CAChC,SACA,SACG;AACH,MAAI,QAAQ,QAAQ,OAAO,QAAQ,QAAQ,eAAe;AACxD,YAAQ,MAAM;AACd,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,QAAQ,IAAI,MAAM,GAAG,EAAE,OAAO,OAAO;AACtD,QAAM,cAAc,SAAS,SAAS,SAAS,CAAC,KAAK;AACrD,QAAM,sBAAsB,YAAY,SAAS,GAAG;AAEpD,MAAI,qBAAqB;AACvB,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,OAAO;AAClB,YAAQ,MAAM;AACd,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,QAAQ,IAAI,SAAS,GAAG,IACvC,QAAQ,IAAI,MAAM,GAAG,EAAE,IACvB,QAAQ;AACZ,UAAQ,MAAM,GAAG,UAAU;AAC3B,SAAO;AACT;AAOO,IAAM,kBAAkB,CAAC,YAA+B;AAC7D,QAAM,aAAa,QAAQ,QAAQ,KAAK,CAAC,EAAE;AAC3C,UAAQ,MAAM,IAAI,UAAU,GAAG,QAAQ,GAAG;AAC1C,SAAO;AACT;","names":[]}
|
|
@@ -2,19 +2,25 @@ import "./chunk-LZOMFHX3.mjs";
|
|
|
2
2
|
|
|
3
3
|
// src/static-hosting/static-hosting.viewer-request-handler.ts
|
|
4
4
|
var isTestEnv = process.env.VITEST === "true";
|
|
5
|
-
var
|
|
5
|
+
var spaHandler = async (event) => {
|
|
6
|
+
return runHandler(event, "spa");
|
|
7
|
+
};
|
|
8
|
+
var staticHandler = async (event) => {
|
|
9
|
+
return runHandler(event, "static");
|
|
10
|
+
};
|
|
11
|
+
var runHandler = async (event, mode) => {
|
|
6
12
|
if (!isTestEnv) {
|
|
7
13
|
console.log("Request Event: ", JSON.stringify(event, null, 2));
|
|
8
14
|
}
|
|
9
15
|
let request = event.Records[0].cf.request;
|
|
10
|
-
request = addDefaultDocument(request);
|
|
16
|
+
request = addDefaultDocument(request, mode);
|
|
11
17
|
request = addDomainFolder(request);
|
|
12
18
|
if (!isTestEnv) {
|
|
13
19
|
console.log("Resulting Request: ", JSON.stringify(request, null, 2));
|
|
14
20
|
}
|
|
15
21
|
return request;
|
|
16
22
|
};
|
|
17
|
-
var addDefaultDocument = (request) => {
|
|
23
|
+
var addDefaultDocument = (request, mode) => {
|
|
18
24
|
if (request.uri === "/" || request.uri === "/index.html") {
|
|
19
25
|
request.uri = "/index.html";
|
|
20
26
|
return request;
|
|
@@ -25,7 +31,12 @@ var addDefaultDocument = (request) => {
|
|
|
25
31
|
if (looksLikeStaticFile) {
|
|
26
32
|
return request;
|
|
27
33
|
}
|
|
28
|
-
|
|
34
|
+
if (mode === "spa") {
|
|
35
|
+
request.uri = "/index.html";
|
|
36
|
+
return request;
|
|
37
|
+
}
|
|
38
|
+
const normalized = request.uri.endsWith("/") ? request.uri.slice(0, -1) : request.uri;
|
|
39
|
+
request.uri = `${normalized}/index.html`;
|
|
29
40
|
return request;
|
|
30
41
|
};
|
|
31
42
|
var addDomainFolder = (request) => {
|
|
@@ -36,6 +47,7 @@ var addDomainFolder = (request) => {
|
|
|
36
47
|
export {
|
|
37
48
|
addDefaultDocument,
|
|
38
49
|
addDomainFolder,
|
|
39
|
-
|
|
50
|
+
spaHandler,
|
|
51
|
+
staticHandler
|
|
40
52
|
};
|
|
41
53
|
//# sourceMappingURL=static-hosting.viewer-request-handler.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/static-hosting/static-hosting.viewer-request-handler.ts"],"sourcesContent":["import { CloudFrontRequest, CloudFrontRequestEvent } from \"aws-lambda\";\n\n/**\n
|
|
1
|
+
{"version":3,"sources":["../src/static-hosting/static-hosting.viewer-request-handler.ts"],"sourcesContent":["import { CloudFrontRequest, CloudFrontRequestEvent } from \"aws-lambda\";\n\n/**\n * Hosting mode controls how path-like URIs get a default document.\n *\n * - `spa`: path-like URIs (e.g. `/dashboard`, `/patients/123`) rewrite to\n * `/index.html` so the single-page app's root index is served and the\n * client-side router handles the path.\n * - `static`: path-like URIs append `/index.html` (e.g. `/docs` becomes\n * `/docs/index.html`) so multi-page static sites can serve distinct\n * HTML per path.\n */\nexport type HostingMode = \"spa\" | \"static\";\n\nconst isTestEnv = process.env.VITEST === \"true\";\n\n/**\n * Viewer Request Handler for SPA mode.\n *\n * - Logs the request\n * - Rewrites path-like URIs to `/index.html`\n * - Prepends the Host header as a folder for multi-tenant routing\n */\nexport const spaHandler = async (\n event: CloudFrontRequestEvent,\n): Promise<CloudFrontRequest> => {\n return runHandler(event, \"spa\");\n};\n\n/**\n * Viewer Request Handler for static (non-SPA) mode.\n *\n * - Logs the request\n * - Rewrites path-like URIs to `<path>/index.html`\n * - Prepends the Host header as a folder for multi-tenant routing\n */\nexport const staticHandler = async (\n event: CloudFrontRequestEvent,\n): Promise<CloudFrontRequest> => {\n return runHandler(event, \"static\");\n};\n\nconst runHandler = async (\n event: CloudFrontRequestEvent,\n mode: HostingMode,\n): Promise<CloudFrontRequest> => {\n if (!isTestEnv) {\n console.log(\"Request Event: \", JSON.stringify(event, null, 2));\n }\n\n let request = event.Records[0].cf.request;\n\n // add index if needed\n request = addDefaultDocument(request, mode);\n\n // prepend folder with domain\n request = addDomainFolder(request);\n\n if (!isTestEnv) {\n console.log(\"Resulting Request: \", JSON.stringify(request, null, 2));\n }\n\n return request;\n};\n\n/**\n * Adds a default document to the request URI based on the hosting mode.\n *\n * In both modes:\n * - `/` and `/index.html` become `/index.html`\n * - URIs whose last segment contains a dot are treated as static files and\n * left unchanged.\n *\n * In `spa` mode, all other path-like URIs rewrite to `/index.html`.\n * In `static` mode, path-like URIs append `/index.html` (trailing slash\n * preserved as a single slash): `/docs` and `/docs/` both become\n * `/docs/index.html`.\n */\nexport const addDefaultDocument = (\n request: CloudFrontRequest,\n mode: HostingMode,\n) => {\n if (request.uri === \"/\" || request.uri === \"/index.html\") {\n request.uri = \"/index.html\";\n return request;\n }\n\n const segments = request.uri.split(\"/\").filter(Boolean);\n const lastSegment = segments[segments.length - 1] ?? \"\";\n const looksLikeStaticFile = lastSegment.includes(\".\");\n\n if (looksLikeStaticFile) {\n return request;\n }\n\n if (mode === \"spa\") {\n request.uri = \"/index.html\";\n return request;\n }\n\n // static mode: append /index.html to the path, collapsing any trailing slash\n const normalized = request.uri.endsWith(\"/\")\n ? request.uri.slice(0, -1)\n : request.uri;\n request.uri = `${normalized}/index.html`;\n return request;\n};\n\n/**\n * Prepends the Host header as a folder to the request URI. Used for\n * multi-tenant routing where each domain maps to a folder in the S3\n * bucket (e.g. `example.com/foo` → `/example.com/foo`).\n */\nexport const addDomainFolder = (request: CloudFrontRequest) => {\n const hostHeader = request.headers.host[0].value;\n request.uri = `/${hostHeader}${request.uri}`;\n return request;\n};\n"],"mappings":";;;AAcA,IAAM,YAAY,QAAQ,IAAI,WAAW;AASlC,IAAM,aAAa,OACxB,UAC+B;AAC/B,SAAO,WAAW,OAAO,KAAK;AAChC;AASO,IAAM,gBAAgB,OAC3B,UAC+B;AAC/B,SAAO,WAAW,OAAO,QAAQ;AACnC;AAEA,IAAM,aAAa,OACjB,OACA,SAC+B;AAC/B,MAAI,CAAC,WAAW;AACd,YAAQ,IAAI,mBAAmB,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,EAC/D;AAEA,MAAI,UAAU,MAAM,QAAQ,CAAC,EAAE,GAAG;AAGlC,YAAU,mBAAmB,SAAS,IAAI;AAG1C,YAAU,gBAAgB,OAAO;AAEjC,MAAI,CAAC,WAAW;AACd,YAAQ,IAAI,uBAAuB,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,EACrE;AAEA,SAAO;AACT;AAeO,IAAM,qBAAqB,CAChC,SACA,SACG;AACH,MAAI,QAAQ,QAAQ,OAAO,QAAQ,QAAQ,eAAe;AACxD,YAAQ,MAAM;AACd,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,QAAQ,IAAI,MAAM,GAAG,EAAE,OAAO,OAAO;AACtD,QAAM,cAAc,SAAS,SAAS,SAAS,CAAC,KAAK;AACrD,QAAM,sBAAsB,YAAY,SAAS,GAAG;AAEpD,MAAI,qBAAqB;AACvB,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,OAAO;AAClB,YAAQ,MAAM;AACd,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,QAAQ,IAAI,SAAS,GAAG,IACvC,QAAQ,IAAI,MAAM,GAAG,EAAE,IACvB,QAAQ;AACZ,UAAQ,MAAM,GAAG,UAAU;AAC3B,SAAO;AACT;AAOO,IAAM,kBAAkB,CAAC,YAA+B;AAC7D,QAAM,aAAa,QAAQ,QAAQ,KAAK,CAAC,EAAE;AAC3C,UAAQ,MAAM,IAAI,UAAU,GAAG,QAAQ,GAAG;AAC1C,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"eslint-plugin-import": "^2.32.0",
|
|
24
24
|
"eslint-plugin-prettier": "^5.5.5",
|
|
25
25
|
"prettier": "^3.8.3",
|
|
26
|
-
"rollup": "^4.60.
|
|
26
|
+
"rollup": "^4.60.2",
|
|
27
27
|
"rollup-plugin-dts": "^6.4.1",
|
|
28
28
|
"tsup": "^8.5.1",
|
|
29
29
|
"typescript": "^5.9.3",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"main": "lib/index.js",
|
|
50
50
|
"license": "MIT",
|
|
51
|
-
"version": "0.0.
|
|
51
|
+
"version": "0.0.62",
|
|
52
52
|
"types": "lib/index.d.ts",
|
|
53
53
|
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
|
|
54
54
|
"scripts": {
|