@kensio/yulin 1.21.6 → 1.21.8
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 +22 -2
- package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-creator.js +1 -0
- package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.d.ts +4 -0
- package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.js +26 -1
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-creator.js +3 -1
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.d.ts +0 -7
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.js +0 -20
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.d.ts +13 -0
- package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.js +59 -0
- package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.d.ts +1 -1
- package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.js +1 -1
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +1 -1
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.d.ts +3 -2
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.js +25 -3
- package/dist/service/ses/command/authorize/sim-ses-authorizer.d.ts +9 -0
- package/dist/service/ses/command/authorize/sim-ses-authorizer.js +18 -2
- package/dist/service/ses/command/send/sim-ses-send-email.js +2 -1
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-creator.js +1 -0
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.d.ts +9 -4
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.js +18 -6
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.d.ts +10 -0
- package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.js +16 -1
- package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-creator.js +1 -1
- package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.d.ts +11 -6
- package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.js +18 -9
- package/docs/README.md +40 -4
- package/docs/ai-skill/README.md +57 -54
- package/docs/cli/README.md +84 -94
- package/docs/factories/README.md +42 -54
- package/docs/lint/README.md +41 -67
- package/docs/non-aws-dependencies/README.md +72 -168
- package/docs/sdk/README.md +109 -95
- package/docs/serve/README.md +192 -898
- package/docs/services/acm/README.md +24 -40
- package/docs/services/apigateway/README.md +52 -71
- package/docs/services/apigatewayv2/README.md +55 -74
- package/docs/services/athena/README.md +17 -26
- package/docs/services/backup/README.md +29 -39
- package/docs/services/bedrock/README.md +38 -52
- package/docs/services/cloudformation/README.md +43 -55
- package/docs/services/cloudfront/README.md +69 -95
- package/docs/services/cloudwatch/README.md +40 -54
- package/docs/services/cognito/README.md +30 -45
- package/docs/services/dynamodb/README.md +34 -51
- package/docs/services/ecr/README.md +36 -77
- package/docs/services/ecs/README.md +26 -46
- package/docs/services/elbv2/README.md +19 -29
- package/docs/services/eventbridge/README.md +24 -20
- package/docs/services/firehose/README.md +24 -32
- package/docs/services/glue/README.md +41 -78
- package/docs/services/iam/README.md +13 -15
- package/docs/services/kinesis/README.md +53 -93
- package/docs/services/kms/README.md +22 -32
- package/docs/services/lambda/README.md +60 -80
- package/docs/services/logs/README.md +41 -50
- package/docs/services/organizations/README.md +50 -85
- package/docs/services/personalize/README.md +28 -44
- package/docs/services/rekognition/README.md +26 -38
- package/docs/services/route53/README.md +17 -17
- package/docs/services/s3/README.md +47 -51
- package/docs/services/scheduler/README.md +41 -52
- package/docs/services/secretsmanager/README.md +27 -42
- package/docs/services/ses/README.md +24 -34
- package/docs/services/sns/README.md +31 -33
- package/docs/services/sqs/README.md +14 -14
- package/docs/services/ssm/README.md +19 -21
- package/docs/services/stepfunctions/README.md +18 -20
- package/docs/services/sts/README.md +32 -45
- package/docs/services/wafv2/README.md +12 -17
- package/docs/terraform/README.md +108 -126
- package/docs/testing/README.md +228 -0
- package/docs/time/README.md +87 -117
- package/llms.txt +2 -1
- package/package.json +1 -1
package/docs/serve/README.md
CHANGED
|
@@ -1,87 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Serve Yulin on localhost
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
reaches it over HTTP. The same request path is also available in the process with no port at all,
|
|
5
|
-
which is the form a test usually wants.
|
|
3
|
+
Yulin can handle HTTP and DNS requests either in process or through local network ports.
|
|
6
4
|
|
|
7
|
-
##
|
|
5
|
+
## Start a local server
|
|
8
6
|
|
|
9
|
-
`serveSimAws`
|
|
7
|
+
Use `serveSimAws` when a browser, command-line program, or application in another process needs to
|
|
8
|
+
reach the simulation:
|
|
10
9
|
|
|
11
10
|
```typescript sim-serve-localhost
|
|
12
11
|
/**
|
|
13
|
-
* Serving a simulated environment on
|
|
12
|
+
* Serving a simulated AWS environment on localhost.
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { SimAws } from "@kensio/yulin";
|
|
17
16
|
import { serveSimAws } from "@kensio/yulin/serve";
|
|
18
17
|
|
|
19
18
|
const simAws = new SimAws();
|
|
20
|
-
const
|
|
19
|
+
const server = await serveSimAws({ simAws, port: 8787 });
|
|
21
20
|
|
|
22
|
-
console.log(
|
|
21
|
+
console.log(server.port); // "8787"
|
|
22
|
+
console.log(server.hostname); // "sim-aws.localhost"
|
|
23
23
|
|
|
24
|
-
await
|
|
24
|
+
await server.close();
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
Omit `port` to let the operating system choose a free port. Read the selected value from
|
|
28
|
+
`server.port`.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
The server keeps simulated service hostnames in the URL. `server.localUrl(...)` converts a URL
|
|
31
|
+
returned by Yulin into an address served by this server:
|
|
32
32
|
|
|
33
|
-
```typescript
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
import { CreateBucketCommand } from "@aws-sdk/client-s3";
|
|
39
|
-
import { SimAws } from "@kensio/yulin";
|
|
40
|
-
import { serveSimAws } from "@kensio/yulin/serve";
|
|
41
|
-
|
|
42
|
-
const simAws = new SimAws();
|
|
43
|
-
const srv = await serveSimAws({ simAws });
|
|
44
|
-
|
|
45
|
-
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
|
|
46
|
-
|
|
47
|
-
const websiteUrl = simAws.s3().getBucketWebsiteUrl("foo-site");
|
|
48
|
-
console.log(srv.localUrl(websiteUrl).toString());
|
|
49
|
-
// http://foo-site.s3-website.us-east-1.sim-aws.localhost:<srv.port>/
|
|
50
|
-
// with whatever port this run took, since none was pinned.
|
|
51
|
-
|
|
52
|
-
await srv.close();
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Redirects
|
|
56
|
-
|
|
57
|
-
A simulated service redirects to the hostname it would use against real AWS. Served on localhost that
|
|
58
|
-
address reaches the public internet, and a browser following it leaves the simulation behind.
|
|
59
|
-
|
|
60
|
-
A `Location` header naming a hostname the simulation serves is put into its localhost form on the way
|
|
61
|
-
out of the local server. A sign-in page behind `www.example.com` redirecting to its user pool domain
|
|
62
|
-
answers with an address the browser can open:
|
|
33
|
+
```typescript
|
|
34
|
+
const localUrl = server.localUrl(
|
|
35
|
+
"https://my-site.s3-website.eu-west-2.sim-aws.localhost/index.html",
|
|
36
|
+
);
|
|
63
37
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Location: http://auth.example.com.sim-aws.localhost:8787/oauth2/authorize?client_id=1a2b3c
|
|
38
|
+
console.log(localUrl.toString());
|
|
39
|
+
// http://my-site.s3-website.eu-west-2.sim-aws.localhost:8787/index.html
|
|
67
40
|
```
|
|
68
41
|
|
|
69
|
-
The
|
|
70
|
-
|
|
71
|
-
at is passed on as the service wrote it. So is a relative one, since that is already relative to the
|
|
72
|
-
address the client arrived at.
|
|
42
|
+
The server also rewrites an absolute `Location` response header when it points to a hostname in the
|
|
43
|
+
simulation. Relative redirects and redirects to unknown hosts are left unchanged.
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
test using it sees the `Location` the service issued and asserts on the production hostname.
|
|
45
|
+
## Send HTTP requests in process
|
|
76
46
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`SimAwsHttp` is the same request path with no socket under it. It takes a Fetch API `Request` and
|
|
80
|
-
answers with a `Response`, in the process that built the environment:
|
|
47
|
+
Use `SimAwsHttp` for tests and other callers in the same process. It accepts the same inputs as
|
|
48
|
+
`fetch`, but it opens no socket:
|
|
81
49
|
|
|
82
50
|
```typescript sim-serve-in-process-request
|
|
83
51
|
/**
|
|
84
|
-
* Requesting a simulated S3 website
|
|
52
|
+
* Requesting a simulated S3 website without starting a server.
|
|
85
53
|
*/
|
|
86
54
|
|
|
87
55
|
import {
|
|
@@ -95,95 +63,69 @@ import { SimAws } from "@kensio/yulin";
|
|
|
95
63
|
import { SimAwsHttp } from "@kensio/yulin/serve";
|
|
96
64
|
|
|
97
65
|
const simAws = new SimAws();
|
|
98
|
-
const
|
|
99
|
-
const
|
|
66
|
+
const http = new SimAwsHttp({ simAws });
|
|
67
|
+
const s3 = simAws.region("eu-west-2").s3();
|
|
100
68
|
|
|
101
|
-
await
|
|
102
|
-
await
|
|
69
|
+
await s3.createBucket(new CreateBucketCommand({ Bucket: "site" }));
|
|
70
|
+
await s3.putObject(
|
|
103
71
|
new PutObjectCommand({
|
|
104
|
-
Bucket: "
|
|
72
|
+
Bucket: "site",
|
|
105
73
|
Key: "index.html",
|
|
106
|
-
Body: "<h1>Hello
|
|
74
|
+
Body: "<h1>Hello</h1>",
|
|
107
75
|
ContentType: "text/html; charset=utf-8",
|
|
108
76
|
}),
|
|
109
77
|
);
|
|
110
|
-
await
|
|
78
|
+
await s3.putBucketWebsite(
|
|
111
79
|
new PutBucketWebsiteCommand({
|
|
112
|
-
Bucket: "
|
|
113
|
-
WebsiteConfiguration: {
|
|
114
|
-
IndexDocument: {
|
|
115
|
-
Suffix: "index.html",
|
|
116
|
-
},
|
|
117
|
-
},
|
|
80
|
+
Bucket: "site",
|
|
81
|
+
WebsiteConfiguration: { IndexDocument: { Suffix: "index.html" } },
|
|
118
82
|
}),
|
|
119
83
|
);
|
|
120
|
-
|
|
121
|
-
// A website endpoint serves only what the Bucket policy makes readable, and a
|
|
122
|
-
// public policy needs the Block Public Access opt-out first.
|
|
123
|
-
await simS3.putPublicAccessBlock(
|
|
84
|
+
await s3.putPublicAccessBlock(
|
|
124
85
|
new PutPublicAccessBlockCommand({
|
|
125
|
-
Bucket: "
|
|
86
|
+
Bucket: "site",
|
|
126
87
|
PublicAccessBlockConfiguration: {
|
|
127
88
|
BlockPublicAcls: true,
|
|
128
89
|
IgnorePublicAcls: true,
|
|
129
90
|
},
|
|
130
91
|
}),
|
|
131
92
|
);
|
|
132
|
-
await
|
|
93
|
+
await s3.putBucketPolicy(
|
|
133
94
|
new PutBucketPolicyCommand({
|
|
134
|
-
Bucket: "
|
|
95
|
+
Bucket: "site",
|
|
135
96
|
Policy: JSON.stringify({
|
|
136
97
|
Version: "2012-10-17",
|
|
137
98
|
Statement: {
|
|
138
99
|
Effect: "Allow",
|
|
139
100
|
Principal: "*",
|
|
140
101
|
Action: "s3:GetObject",
|
|
141
|
-
Resource: "arn:aws:s3:::
|
|
102
|
+
Resource: "arn:aws:s3:::site/*",
|
|
142
103
|
},
|
|
143
104
|
}),
|
|
144
105
|
}),
|
|
145
106
|
);
|
|
146
107
|
|
|
147
|
-
const response = await
|
|
108
|
+
const response = await http.fetch(s3.getBucketWebsiteUrl("site"));
|
|
148
109
|
|
|
149
110
|
console.log(response.status); // 200
|
|
150
|
-
console.log(await response.text()); // <h1>Hello
|
|
111
|
+
console.log(await response.text()); // <h1>Hello</h1>
|
|
151
112
|
```
|
|
152
113
|
|
|
153
|
-
`fetch(input, init)`
|
|
154
|
-
that is already built
|
|
155
|
-
|
|
156
|
-
With no port bound there is no URL to adapt. The website URL above is fetched as it is, where a
|
|
157
|
-
served environment needs `srv.localUrl(...)` to add the port it took. A hostname a simulated Route53
|
|
158
|
-
answers for is requested by its own name. A Distribution behind `www.example.com` is reached at
|
|
159
|
-
`https://www.example.com/`. An `https` URL needs no certificate set up for it, since there is no
|
|
160
|
-
connection to encrypt.
|
|
161
|
-
|
|
162
|
-
There is also no server to close, though `simAws.close()` still lets go of what the environment
|
|
163
|
-
itself is holding, such as a watched template file. See
|
|
164
|
-
[stopping and restarting](#stopping-and-restarting).
|
|
165
|
-
|
|
166
|
-
Live reload sits either side of this interface, outside it. A response from here never carries the
|
|
167
|
-
injected script, even in a process that served one elsewhere with live reload on.
|
|
114
|
+
`SimAwsHttp.fetch(input, init)` builds a Fetch API `Request` for you.
|
|
115
|
+
`SimAwsHttp.handleRequest(request)` accepts a request that is already built. Both methods use the
|
|
116
|
+
same routing, authentication, and service controllers as the local server.
|
|
168
117
|
|
|
169
|
-
|
|
118
|
+
`SimAwsHttp` uses simulated hostnames directly. HTTPS URLs also work because no network connection
|
|
119
|
+
or TLS handshake takes place.
|
|
170
120
|
|
|
171
|
-
|
|
172
|
-
have no port to collide over. With no socket listening there is no teardown to get wrong, and no
|
|
173
|
-
connection between the request and the service answering it.
|
|
174
|
-
- `serveSimAws` for anything outside the process, such as a browser, `curl`, or an SDK client
|
|
175
|
-
pointed at a local endpoint.
|
|
121
|
+
## Connect an AWS SDK client
|
|
176
122
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
## Pointing an AWS SDK or the CLI at the simulation
|
|
181
|
-
|
|
182
|
-
A served environment answers the general AWS service APIs on the same port it serves everything else. Give any AWS SDK, in any language, the server's own URL as its endpoint and it reaches the simulation.
|
|
123
|
+
The local server exposes supported AWS service APIs through one endpoint URL. The client still
|
|
124
|
+
needs a Region and credentials issued by simulated IAM:
|
|
183
125
|
|
|
184
126
|
```typescript sim-serve-aws-api-endpoint
|
|
185
127
|
/**
|
|
186
|
-
*
|
|
128
|
+
* Calling simulated DynamoDB through a local HTTP endpoint.
|
|
187
129
|
*/
|
|
188
130
|
|
|
189
131
|
import {
|
|
@@ -210,29 +152,30 @@ await simAws.dynamoDb().createTable(
|
|
|
210
152
|
}),
|
|
211
153
|
);
|
|
212
154
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await simIam.createUser(new CreateUserCommand({ UserName: "Widgets" }));
|
|
217
|
-
await simIam.putUserPolicy(
|
|
155
|
+
const iam = simAws.iam();
|
|
156
|
+
await iam.createUser(new CreateUserCommand({ UserName: "Operator" }));
|
|
157
|
+
await iam.putUserPolicy(
|
|
218
158
|
new PutUserPolicyCommand({
|
|
219
|
-
UserName: "
|
|
159
|
+
UserName: "Operator",
|
|
220
160
|
PolicyName: "WriteWidgets",
|
|
221
161
|
PolicyDocument: JSON.stringify({
|
|
222
162
|
Version: "2012-10-17",
|
|
223
|
-
Statement: {
|
|
163
|
+
Statement: {
|
|
164
|
+
Effect: "Allow",
|
|
165
|
+
Action: "dynamodb:PutItem",
|
|
166
|
+
Resource: "*",
|
|
167
|
+
},
|
|
224
168
|
}),
|
|
225
169
|
}),
|
|
226
170
|
);
|
|
227
|
-
const created = await
|
|
228
|
-
new CreateAccessKeyCommand({ UserName: "
|
|
171
|
+
const created = await iam.createAccessKey(
|
|
172
|
+
new CreateAccessKeyCommand({ UserName: "Operator" }),
|
|
229
173
|
);
|
|
230
174
|
|
|
231
|
-
const
|
|
232
|
-
|
|
175
|
+
const server = await serveSimAws({ simAws });
|
|
233
176
|
const client = new DynamoDBClient({
|
|
234
177
|
region: simAws.defaultRegionName,
|
|
235
|
-
endpoint: `http://localhost:${
|
|
178
|
+
endpoint: `http://localhost:${server.port}`,
|
|
236
179
|
credentials: {
|
|
237
180
|
accessKeyId: created.AccessKey.AccessKeyId,
|
|
238
181
|
secretAccessKey: created.AccessKey.SecretAccessKey,
|
|
@@ -242,885 +185,236 @@ const client = new DynamoDBClient({
|
|
|
242
185
|
await client.send(
|
|
243
186
|
new PutItemCommand({
|
|
244
187
|
TableName: "widgets",
|
|
245
|
-
Item: { id: { S: "
|
|
188
|
+
Item: { id: { S: "widget-1" } },
|
|
246
189
|
}),
|
|
247
190
|
);
|
|
248
191
|
|
|
249
|
-
await
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
One endpoint URL covers every service. A client sends `Host: localhost:<port>` whichever service it is talking to, which leaves no hostname to route on. Routing is on the service and Region named in the request's SigV4 credential scope, and a client cannot change either without invalidating its signature.
|
|
253
|
-
|
|
254
|
-
The same URL works for anything that speaks the AWS APIs, which includes the real `aws` CLI:
|
|
255
|
-
|
|
256
|
-
```bash
|
|
257
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
258
|
-
export AWS_ACCESS_KEY_ID=<key from simulated IAM>
|
|
259
|
-
export AWS_SECRET_ACCESS_KEY=<secret from simulated IAM>
|
|
260
|
-
aws dynamodb put-item --table-name widgets --item '{"id":{"S":"w1"}}'
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Who a served request is
|
|
264
|
-
|
|
265
|
-
Whoever signed it. The endpoint verifies the signature against simulated IAM and runs the operation as the principal behind the access key. An IAM policy applies exactly as it does in process, and an assumed-role session is authorized against the Role behind it.
|
|
266
|
-
|
|
267
|
-
A request carrying no signature is anonymous and reaches nothing. In process an omitted caller means "whoever owns this simulation", and over a port the same default would hand administrator rights to anyone who could reach it.
|
|
268
|
-
|
|
269
|
-
### Which services answer
|
|
270
|
-
|
|
271
|
-
S3, STS, IAM, ELBv2, SNS, CloudFormation, Lambda, and the services that speak the AWS JSON protocol. Those are DynamoDB, DynamoDB Streams, SQS, Cognito Identity Provider, EventBridge, ECS, SSM, ACM, CloudWatch, CloudWatch Logs, KMS, Secrets Manager and Rekognition.
|
|
272
|
-
|
|
273
|
-
A request to any other service is refused with `501 Not Implemented` and a body saying why. Every service is reachable in process through `SimAws` and through [SDK interception](https://yulinsim.dev/sdk/), whether or not it answers here.
|
|
274
|
-
|
|
275
|
-
CloudWatch's windowed reads, `GetMetricStatistics` and `GetMetricData`, are the exception among the operations those services implement. The JSON protocol carries a timestamp as epoch seconds, and the endpoint passes that number through as it arrives. The simulation is handed a number where it expects a date. Both reads answer in process and through SDK interception.
|
|
276
|
-
|
|
277
|
-
### Checking who the simulator thinks you are
|
|
278
|
-
|
|
279
|
-
`sts get-caller-identity` reports the principal behind the credentials that signed the request, which is the quickest way to confirm an endpoint and a set of credentials are wired up as expected:
|
|
280
|
-
|
|
281
|
-
```bash
|
|
282
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
283
|
-
aws sts get-caller-identity
|
|
284
|
-
{
|
|
285
|
-
"UserId": "AIDAM7J2TJYHV8BHEVIO",
|
|
286
|
-
"Account": "888888888888",
|
|
287
|
-
"Arn": "arn:aws:iam::888888888888:user/Widgets"
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
An assumed-role session reports its own session ARN, as it does in real AWS, and its user id joins the Role's id to the session name. A caller with no identity is refused, since there is nothing to answer with.
|
|
292
|
-
|
|
293
|
-
### Assuming a Role over the endpoint
|
|
294
|
-
|
|
295
|
-
`sts assume-role` answers with temporary credentials, and those credentials sign the requests that
|
|
296
|
-
follow:
|
|
297
|
-
|
|
298
|
-
```bash
|
|
299
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
300
|
-
aws sts assume-role --role-arn arn:aws:iam::888888888888:role/Reader --role-session-name probe
|
|
301
|
-
{
|
|
302
|
-
"Credentials": {
|
|
303
|
-
"AccessKeyId": "ASIAQ3JZQ6XKFPWLZ4TM",
|
|
304
|
-
"SecretAccessKey": "T4rBqYbLXKsJ0nZuV9dHc2Wm1PfAeR7gSjNyIvXo",
|
|
305
|
-
"SessionToken": "IQoJb3JpZ2luX2VjEHkaCXVzLWVhc3QtMSJHMEUCIQ",
|
|
306
|
-
"Expiration": "2026-08-18T21:00:00.000Z"
|
|
307
|
-
},
|
|
308
|
-
"AssumedRoleUser": {
|
|
309
|
-
"AssumedRoleId": "AROA5KQZH2XWNDLB7YTVR:probe",
|
|
310
|
-
"Arn": "arn:aws:sts::888888888888:assumed-role/Reader/probe"
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Hand the three credential values to an SDK client, or to `AWS_ACCESS_KEY_ID`,
|
|
316
|
-
`AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN`, and the whole assume-then-work sequence runs
|
|
317
|
-
against the endpoint URL. Simulated IAM authorizes each request against the Role behind the session,
|
|
318
|
-
and `get-caller-identity` reports the session ARN above.
|
|
319
|
-
|
|
320
|
-
`DurationSeconds` and `ExternalId` are read from the request as they are in process. The expiry is
|
|
321
|
-
stamped from the simulation's own clock. `simAws.clock().advanceBy({ hours: 2 })` takes a session
|
|
322
|
-
past it, and the credentials stop authenticating. An SDK refreshes an expired session by assuming
|
|
323
|
-
again.
|
|
324
|
-
|
|
325
|
-
A Role whose trust policy refuses the caller comes back as `AccessDenied`, under the name real STS
|
|
326
|
-
raises it.
|
|
327
|
-
|
|
328
|
-
`AssumeRole` and `GetCallerIdentity` are the two operations simulated STS implements, and both are
|
|
329
|
-
served. `AssumeRoleWithWebIdentity` and `GetSessionToken` are refused as `NotImplemented`.
|
|
330
|
-
|
|
331
|
-
### IAM over the endpoint
|
|
332
|
-
|
|
333
|
-
A served request runs as whoever signed it, and the credentials to sign one used to come from the
|
|
334
|
-
process that built the simulation. `aws iam` closes that circle. A container or a shell script
|
|
335
|
-
creates its own User over the endpoint, gives it a policy, asks for an access key and signs
|
|
336
|
-
everything after that with what it was answered:
|
|
337
|
-
|
|
338
|
-
```bash
|
|
339
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
340
|
-
aws iam create-user --user-name widgets
|
|
341
|
-
aws iam put-user-policy --user-name widgets --policy-name everything \
|
|
342
|
-
--policy-document '{"Version":"2012-10-17","Statement":{"Effect":"Allow","Action":"*","Resource":"*"}}'
|
|
343
|
-
aws iam create-access-key --user-name widgets
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
`create-access-key` answers with the secret, and nothing reports it again. The first request still
|
|
347
|
-
has to be signed by somebody. The [example above](#pointing-an-aws-sdk-or-the-cli-at-the-simulation)
|
|
348
|
-
builds one identity in process, and everything after it can be built over the port.
|
|
349
|
-
|
|
350
|
-
The fifteen operations simulated IAM implements are:
|
|
351
|
-
|
|
352
|
-
- **Users** — `CreateUser`, `CreateAccessKey`, `PutUserPolicy`
|
|
353
|
-
- **Roles** — `CreateRole`, `GetRole`, `ListRoles`, `DeleteRole`, `AttachRolePolicy`,
|
|
354
|
-
`DetachRolePolicy`, `PutRolePolicy`, `DeleteRolePolicy`
|
|
355
|
-
- **Managed policies** — `CreatePolicy`, `GetPolicy`, `ListPolicies`, `DeletePolicy`
|
|
356
|
-
|
|
357
|
-
Anything else is refused as `NotImplemented`, which an SDK raises under that name.
|
|
358
|
-
|
|
359
|
-
### ELBv2 over the endpoint
|
|
360
|
-
|
|
361
|
-
`aws elbv2` builds an Application Load Balancer in the simulation over the same endpoint URL:
|
|
362
|
-
|
|
363
|
-
```bash
|
|
364
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
365
|
-
aws elbv2 create-load-balancer --name shop-alb --subnets subnet-1 subnet-2
|
|
366
|
-
aws elbv2 create-target-group --name checkout-tg --target-type lambda
|
|
367
|
-
aws elbv2 create-listener --load-balancer-arn <arn> --protocol HTTP --port 80 \
|
|
368
|
-
--default-actions Type=forward,TargetGroupArn=<arn>
|
|
192
|
+
await server.close();
|
|
369
193
|
```
|
|
370
194
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
The twenty-two operations simulated ELBv2 implements are:
|
|
376
|
-
|
|
377
|
-
- **Load balancers** — `CreateLoadBalancer`, `DescribeLoadBalancers`, `DeleteLoadBalancer`
|
|
378
|
-
- **Target groups** — `CreateTargetGroup`, `DescribeTargetGroups`, `ModifyTargetGroup`,
|
|
379
|
-
`DeleteTargetGroup`
|
|
380
|
-
- **Targets** — `RegisterTargets`, `DeregisterTargets`, `DescribeTargetHealth`
|
|
381
|
-
- **Listeners** — `CreateListener`, `DescribeListeners`, `ModifyListener`, `DeleteListener`
|
|
382
|
-
- **Listener certificates** — `AddListenerCertificates`, `RemoveListenerCertificates`,
|
|
383
|
-
`DescribeListenerCertificates`
|
|
384
|
-
- **Rules** — `CreateRule`, `DescribeRules`, `ModifyRule`, `DeleteRule`, `SetRulePriorities`
|
|
385
|
-
|
|
386
|
-
A rule matches on `host-header` and `path-pattern`, in either the plain `Values` form or the
|
|
387
|
-
per-field configuration. A condition naming any other field is refused by name, as is an operation
|
|
388
|
-
outside the list above.
|
|
389
|
-
|
|
390
|
-
### S3 over the endpoint
|
|
391
|
-
|
|
392
|
-
`aws s3` and an `S3Client` reach simulated S3 through the same endpoint URL:
|
|
393
|
-
|
|
394
|
-
```bash
|
|
395
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
396
|
-
aws s3api create-bucket --bucket widgets
|
|
397
|
-
aws s3api put-object --bucket widgets --key one.txt --body ./one.txt
|
|
398
|
-
aws s3 ls s3://widgets/
|
|
399
|
-
```
|
|
195
|
+
The endpoint reads the service and Region from the request's SigV4 credential scope. Simulated IAM
|
|
196
|
+
verifies the signature and authorizes the principal that owns the access key. Temporary credentials
|
|
197
|
+
from simulated STS work in the same way.
|
|
400
198
|
|
|
401
|
-
An
|
|
199
|
+
An unsigned AWS API request contains no service identity and is refused. Unknown credentials,
|
|
200
|
+
expired sessions, bad signatures, and denied actions return AWS-shaped authentication or
|
|
201
|
+
authorization errors.
|
|
402
202
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
region: "us-east-1",
|
|
406
|
-
endpoint: `http://localhost:${srv.port}`,
|
|
407
|
-
forcePathStyle: true,
|
|
408
|
-
credentials,
|
|
409
|
-
});
|
|
410
|
-
```
|
|
203
|
+
Set `forcePathStyle: true` on an `S3Client` that uses the shared endpoint. The Bucket name must stay
|
|
204
|
+
in the request path because the endpoint hostname is `localhost`.
|
|
411
205
|
|
|
412
|
-
The
|
|
206
|
+
The [AWS CLI guide](https://yulinsim.dev/cli/) shows the equivalent environment variables and named
|
|
207
|
+
profile configuration.
|
|
413
208
|
|
|
414
|
-
|
|
209
|
+
## Query simulated DNS
|
|
415
210
|
|
|
416
|
-
|
|
211
|
+
`serveSimAws` starts a UDP DNS server beside the HTTP server. It usually uses the same port number,
|
|
212
|
+
but UDP may already be using that number. Always read `server.dnsPort`.
|
|
417
213
|
|
|
418
|
-
|
|
214
|
+
Point a DNS client at `127.0.0.1:<dnsPort>` to resolve records from simulated Route 53. Names that
|
|
215
|
+
route to an HTTP service resolve to `127.0.0.1`, where the local HTTP server is listening. The DNS
|
|
216
|
+
server also answers supported records such as TXT and NS records.
|
|
419
217
|
|
|
420
|
-
|
|
218
|
+
For example, with the DNS port set to `8787`:
|
|
421
219
|
|
|
422
220
|
```bash
|
|
423
|
-
|
|
424
|
-
aws sns create-topic --name orders
|
|
425
|
-
aws sns publish --topic-arn arn:aws:sns:us-east-1:888888888888:orders --message hello
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
A message published here fans out to the simulated Queues and Functions subscribed to the topic, the
|
|
429
|
-
same way an in-process publish does. Delivery runs on the background scheduler (as it does on real
|
|
430
|
-
SNS, after the publish has been answered), so a test reading the Queue afterwards waits on
|
|
431
|
-
`simAws.backgroundTasksComplete()` first.
|
|
432
|
-
|
|
433
|
-
The operations served are the sixteen simulated SNS implements:
|
|
434
|
-
|
|
435
|
-
- **Topics** — `CreateTopic`, `DeleteTopic`, `ListTopics`, `GetTopicAttributes`, `SetTopicAttributes`
|
|
436
|
-
- **Subscriptions** — `Subscribe`, `Unsubscribe`, `ListSubscriptions`, `ListSubscriptionsByTopic`,
|
|
437
|
-
`GetSubscriptionAttributes`, `SetSubscriptionAttributes`
|
|
438
|
-
- **Messages** — `Publish`, `PublishBatch`
|
|
439
|
-
- **SMS** — `CheckIfPhoneNumberIsOptedOut`, `ListPhoneNumbersOptedOut`, `OptInPhoneNumber`
|
|
440
|
-
|
|
441
|
-
Anything else is refused as `NotImplemented`, which an SDK raises under that name.
|
|
442
|
-
|
|
443
|
-
### CloudFormation over the endpoint
|
|
444
|
-
|
|
445
|
-
`aws cloudformation` deploys a template into the simulation over the same endpoint URL:
|
|
446
|
-
|
|
447
|
-
```bash
|
|
448
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
449
|
-
aws cloudformation create-stack --stack-name site --template-body file://template.json
|
|
450
|
-
aws cloudformation describe-stacks
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
The four operations simulated CloudFormation implements are `CreateStack`, `UpdateStack`,
|
|
454
|
-
`DeleteStack` and `DescribeStacks`. A deployment starts in the background and the call is answered
|
|
455
|
-
before the Resources exist, as real CloudFormation answers it. `describe-stacks` reports the status
|
|
456
|
-
it reached, and `waitForStackDeployComplete` waits for it in process.
|
|
457
|
-
|
|
458
|
-
### Lambda over the endpoint
|
|
459
|
-
|
|
460
|
-
`aws lambda` and a `LambdaClient` reach simulated Lambda through the same endpoint URL:
|
|
461
|
-
|
|
462
|
-
```bash
|
|
463
|
-
export AWS_ENDPOINT_URL=http://localhost:8787
|
|
464
|
-
aws lambda invoke --function-name orders --payload '{"id":1}' /tmp/out.json
|
|
465
|
-
cat /tmp/out.json
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
The function runs in the process serving it, and what its handler returned is written to the file.
|
|
469
|
-
A handler that throws answers `200` with `FunctionError` set to `Unhandled`, and the payload holds
|
|
470
|
-
the error it threw. `--invocation-type Event` answers `202` and runs the handler on the background
|
|
471
|
-
scheduler. A test that goes on to read what the function did waits on
|
|
472
|
-
`simAws.backgroundTasksComplete()` first.
|
|
473
|
-
|
|
474
|
-
A function created over the endpoint carries its code as a zip archive, which is the shape the
|
|
475
|
-
`Code.ZipFile` member travels in over HTTP:
|
|
476
|
-
|
|
477
|
-
```bash
|
|
478
|
-
aws lambda create-function --function-name orders \
|
|
479
|
-
--role arn:aws:iam::888888888888:role/OrdersRole \
|
|
480
|
-
--handler index.handler --runtime nodejs22.x \
|
|
481
|
-
--zip-file fileb://orders.zip
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
The operations served are nineteen of the ones simulated Lambda implements:
|
|
485
|
-
|
|
486
|
-
- **Functions** — `CreateFunction`, `GetFunction`, `UpdateFunctionCode`,
|
|
487
|
-
`UpdateFunctionConfiguration`, `ListFunctions`, `DeleteFunction`, `Invoke`
|
|
488
|
-
- **Function URLs** — `CreateFunctionUrlConfig`, `GetFunctionUrlConfig`,
|
|
489
|
-
`UpdateFunctionUrlConfig`, `DeleteFunctionUrlConfig`, `ListFunctionUrlConfigs`
|
|
490
|
-
- **Permissions** — `AddPermission`, `RemovePermission`, `GetPolicy`
|
|
491
|
-
- **Event source mappings** — `CreateEventSourceMapping`, `GetEventSourceMapping`,
|
|
492
|
-
`ListEventSourceMappings`, `DeleteEventSourceMapping`
|
|
493
|
-
|
|
494
|
-
The version and alias operations have no route here yet, and reach the simulation through `SimAws`
|
|
495
|
-
or SDK interception instead.
|
|
496
|
-
|
|
497
|
-
Anything else is refused as `NotImplemented`, which an SDK raises under that name. The refusal names
|
|
498
|
-
the path it arrived at. `aws lambda get-function-configuration` reports that
|
|
499
|
-
`GET /2015-03-31/functions/{name}/configuration` is unserved, and an unimplemented operation sharing
|
|
500
|
-
a method with one that is served gets the same answer.
|
|
501
|
-
|
|
502
|
-
Simulated Lambda also answers its own Function URL hostnames, covered above. That path is unchanged,
|
|
503
|
-
and it is what a browser and a webhook reach.
|
|
504
|
-
|
|
505
|
-
## Stopping and restarting
|
|
506
|
-
|
|
507
|
-
`close()` stops serving and lets go of everything Yulin was holding, leaving the process free to
|
|
508
|
-
exit. That is the HTTP port, the DNS port and the connections the server is holding, along with the
|
|
509
|
-
simulated environment it was serving with them. The environment covers the template files a
|
|
510
|
-
deployment is [watching](https://yulinsim.dev/services/cloudformation/#watching-a-template-file) and the
|
|
511
|
-
directories a [mount](https://yulinsim.dev/services/s3/#reloading-the-browser-when-the-directory-changes) is
|
|
512
|
-
watching, in whichever Account and Region each of them lives in. One call covers all of it. A script
|
|
513
|
-
that hangs on exit is not a hunt for the handle you missed.
|
|
514
|
-
|
|
515
|
-
It returns a promise that settles once the last thing the server had to say has gone. A script that
|
|
516
|
-
means to exit under its own steam has something to wait for. Yulin installs no signal handlers,
|
|
517
|
-
since a library taking over process signals gets in the way of whatever else the process is doing.
|
|
518
|
-
Call `close()` from your own handler:
|
|
519
|
-
|
|
520
|
-
```typescript sim-serve-shutdown
|
|
521
|
-
/**
|
|
522
|
-
* Closing a served environment when the process is asked to stop.
|
|
523
|
-
*/
|
|
524
|
-
|
|
525
|
-
import { serveSimAws } from "@kensio/yulin/serve";
|
|
526
|
-
|
|
527
|
-
const srv = await serveSimAws({ port: 8787 });
|
|
528
|
-
|
|
529
|
-
async function stopServing(): Promise<void> {
|
|
530
|
-
// Waiting means anything the server still had to say has gone before the
|
|
531
|
-
// process does.
|
|
532
|
-
await srv.close();
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
process.on("SIGTERM", () => {
|
|
536
|
-
void stopServing();
|
|
537
|
-
});
|
|
221
|
+
dig @127.0.0.1 -p 8787 www.example.test A
|
|
538
222
|
```
|
|
539
223
|
|
|
540
|
-
|
|
541
|
-
the handles that keep the process alive. Every simulated Bucket, Table and Stack is where it was,
|
|
542
|
-
and the environment goes on working. A script can close and carry on.
|
|
224
|
+
The DNS server uses UDP only. `server.close()` releases both the HTTP and DNS ports.
|
|
543
225
|
|
|
544
|
-
|
|
545
|
-
watches and mounted directory watches, and a test with one of those closes it in one line:
|
|
226
|
+
## Stop the server
|
|
546
227
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
* Letting go of what an unserved environment is holding.
|
|
550
|
-
*/
|
|
228
|
+
Always await `server.close()` when a script or test finishes. It closes active connections and any
|
|
229
|
+
file watchers owned by the `SimAws` instance. The simulated resources remain available in memory.
|
|
551
230
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const simAws = new SimAws();
|
|
555
|
-
|
|
556
|
-
await simAws.cloudFormation().deployTemplateFile({
|
|
557
|
-
templatePath: "cdk.out/TestStack.template.json",
|
|
558
|
-
watch: true,
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
// The Stack, and everything the template deployed, is still there afterwards.
|
|
562
|
-
await simAws.close();
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
### Asking for a signal handler
|
|
566
|
-
|
|
567
|
-
The handler above is yours to write, and that is the point. Your script decides what a signal means
|
|
568
|
-
and in what order things happen. A script that wants the usual behaviour can ask for it instead, and
|
|
569
|
-
gets the same close on `SIGINT` and `SIGTERM`:
|
|
231
|
+
Yulin does not install process signal handlers automatically. A development script can ask the
|
|
232
|
+
server to close on `SIGINT` and `SIGTERM`:
|
|
570
233
|
|
|
571
234
|
```typescript sim-serve-close-on-signal
|
|
572
235
|
/**
|
|
573
|
-
*
|
|
236
|
+
* Closing a local server when the process receives a termination signal.
|
|
574
237
|
*/
|
|
575
238
|
|
|
576
239
|
import { SimAws } from "@kensio/yulin";
|
|
577
240
|
import { serveSimAws } from "@kensio/yulin/serve";
|
|
578
241
|
|
|
579
242
|
const simAws = new SimAws();
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
// Build the simulated environment the pages are served from here.
|
|
243
|
+
const server = await serveSimAws({ simAws, port: 8787 });
|
|
583
244
|
|
|
584
|
-
|
|
585
|
-
// it serves, and the process then exits on its own.
|
|
586
|
-
const stopListening = srv.closeOnSignal();
|
|
245
|
+
const removeSignalHandlers = server.closeOnSignal();
|
|
587
246
|
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
247
|
+
// Call this only if the script later takes responsibility for signals itself.
|
|
248
|
+
removeSignalHandlers();
|
|
249
|
+
await server.close();
|
|
591
250
|
```
|
|
592
251
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
default and ends the process. Closing never exits the process itself. It lets go of what Yulin was
|
|
597
|
-
holding, and a process with no work left then exits on its own. `SimAws` has the same method, for an
|
|
598
|
-
unserved environment.
|
|
252
|
+
Pass `signals` to `closeOnSignal` to replace the default signal list. The returned function removes
|
|
253
|
+
the installed handlers. `SimAws.closeOnSignal()` provides the same behavior when no local server is
|
|
254
|
+
running.
|
|
599
255
|
|
|
600
|
-
A
|
|
601
|
-
|
|
602
|
-
|
|
256
|
+
A server waits for a pinned HTTP port for up to two seconds during startup. This allows a restarted
|
|
257
|
+
process to take the port after the previous process releases it. Yulin throws
|
|
258
|
+
`SimAwsLocalPortInUse` if the port stays occupied.
|
|
603
259
|
|
|
604
|
-
##
|
|
260
|
+
## Reload a browser after changes
|
|
605
261
|
|
|
606
|
-
|
|
607
|
-
only Yulin in its response path. Yulin is the one thing that can tell the browser to reload. Turning
|
|
608
|
-
`liveReload` on serves a reload channel and puts a small script into the HTML pages it serves to
|
|
609
|
-
browsers:
|
|
262
|
+
Set `liveReload: true` when the local server is serving HTML during development:
|
|
610
263
|
|
|
611
264
|
```typescript sim-serve-live-reload
|
|
612
265
|
/**
|
|
613
|
-
* Serving with
|
|
614
|
-
* restarts.
|
|
615
|
-
*/
|
|
616
|
-
|
|
617
|
-
import { SimAws } from "@kensio/yulin";
|
|
618
|
-
import { serveSimAws } from "@kensio/yulin/serve";
|
|
619
|
-
|
|
620
|
-
const simAws = new SimAws();
|
|
621
|
-
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
622
|
-
|
|
623
|
-
// Build the simulated environment the pages are served from here.
|
|
624
|
-
|
|
625
|
-
async function stopServing(): Promise<void> {
|
|
626
|
-
// Waiting means the browsers hear about the restart before the process goes.
|
|
627
|
-
await srv.close();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
process.on("SIGTERM", () => {
|
|
631
|
-
void stopServing();
|
|
632
|
-
});
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
It is off by default, and every response is then byte for byte what it would otherwise be.
|
|
636
|
-
|
|
637
|
-
### Reloading on a restart
|
|
638
|
-
|
|
639
|
-
Local development means restarting the process, because a changed setup script or Lambda handler
|
|
640
|
-
needs a fresh module graph. The script survives that on its own, with no supervisor process and no
|
|
641
|
-
shared state between the outgoing and incoming process.
|
|
642
|
-
|
|
643
|
-
The channel is Server-Sent Events, and the browser reconnects by itself. Each process has a boot id
|
|
644
|
-
and sends it to every page that connects. A page that reconnects and finds a different boot id knows
|
|
645
|
-
it is showing output from a process that has gone, and reloads. A blip on a still running process
|
|
646
|
-
hands back the same boot id, and the page carries on.
|
|
647
|
-
|
|
648
|
-
`close()` sends a `reloading` event before the connections go. A page then reads the gap it is about
|
|
649
|
-
to see as a restart, and not a server that has died. The page keeps its own appearance and gets a
|
|
650
|
-
`data-sim-aws-live-reload="reloading"` attribute on its `<html>` element, ready to style:
|
|
651
|
-
|
|
652
|
-
```css
|
|
653
|
-
html[data-sim-aws-live-reload="reloading"] {
|
|
654
|
-
opacity: 0.6;
|
|
655
|
-
}
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
The event has to reach the browser before the connection goes. `close()` sees the reload streams out
|
|
659
|
-
before it destroys anything else the server was holding, and its promise settles once they have
|
|
660
|
-
gone. A browser that has stopped answering is waited on for half a second and then dropped. A page
|
|
661
|
-
nobody is looking at cannot hold up a restart. Both ports are released before any of that waiting,
|
|
662
|
-
and a replacement process can take them straight away either way.
|
|
663
|
-
|
|
664
|
-
None of this needs a supervisor process. A dev script started from an IDE debugger gets browser
|
|
665
|
-
reload with the debugger attached throughout.
|
|
666
|
-
|
|
667
|
-
### Reloading without a restart
|
|
668
|
-
|
|
669
|
-
For a change that needs no restart, such as new content in a simulated Bucket, `reload()` reloads
|
|
670
|
-
every connected browser:
|
|
671
|
-
|
|
672
|
-
```typescript sim-serve-reload
|
|
673
|
-
/**
|
|
674
|
-
* Reloading connected browsers after changing simulated content in place.
|
|
675
|
-
*/
|
|
676
|
-
|
|
677
|
-
import { CreateBucketCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
678
|
-
import { SimAws } from "@kensio/yulin";
|
|
679
|
-
import { serveSimAws } from "@kensio/yulin/serve";
|
|
680
|
-
|
|
681
|
-
const simAws = new SimAws();
|
|
682
|
-
const srv = await serveSimAws({ simAws, liveReload: true });
|
|
683
|
-
|
|
684
|
-
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
|
|
685
|
-
await simAws.s3().putObject(
|
|
686
|
-
new PutObjectCommand({
|
|
687
|
-
Bucket: "foo-site",
|
|
688
|
-
Key: "index.html",
|
|
689
|
-
Body: "<h1>Changed</h1>",
|
|
690
|
-
ContentType: "text/html; charset=utf-8",
|
|
691
|
-
}),
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
srv.reload();
|
|
695
|
-
|
|
696
|
-
await srv.close();
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
`reload()` throws when live reload is off.
|
|
700
|
-
|
|
701
|
-
You can also hand the server to something that reloads for you, either a
|
|
702
|
-
[mounted directory](#reloading-when-a-build-changes-a-mounted-directory) or a
|
|
703
|
-
[watched template file](#answering-a-change-instead-of-restarting), both as `{ reload: srv }`. A
|
|
704
|
-
watched template file refuses a server it could never reload as it is handed over, ahead of the
|
|
705
|
-
first change.
|
|
706
|
-
|
|
707
|
-
### Reloading when a build changes a mounted directory
|
|
708
|
-
|
|
709
|
-
A Bucket mounted on a local directory is already reading the files a site generator writes. A
|
|
710
|
-
rebuild needs nothing copied into it. Hand the mount the server and it watches the directory and
|
|
711
|
-
reloads for you once the writes stop:
|
|
712
|
-
|
|
713
|
-
```typescript sim-serve-mount-reload
|
|
714
|
-
/**
|
|
715
|
-
* A built site the process reloads the browser for, rather than restarting for.
|
|
266
|
+
* Serving HTML with browser reload support.
|
|
716
267
|
*/
|
|
717
268
|
|
|
718
|
-
import path from "node:path";
|
|
719
|
-
|
|
720
|
-
import { CreateBucketCommand } from "@aws-sdk/client-s3";
|
|
721
269
|
import { SimAws } from "@kensio/yulin";
|
|
722
270
|
import { serveSimAws } from "@kensio/yulin/serve";
|
|
723
271
|
|
|
724
272
|
const simAws = new SimAws();
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "site" }));
|
|
728
|
-
|
|
729
|
-
simAws
|
|
730
|
-
.s3()
|
|
731
|
-
.mountBucketFilesystem("site", path.join(process.cwd(), "articles/public"), {
|
|
732
|
-
reload: srv,
|
|
733
|
-
});
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
One build is one reload, however many files it wrote. See
|
|
737
|
-
[filesystem-backed Bucket storage](https://yulinsim.dev/services/s3/#reloading-the-browser-when-the-directory-changes)
|
|
738
|
-
for the `settleMs` override and for stopping the watch.
|
|
739
|
-
|
|
740
|
-
### What gets the script
|
|
741
|
-
|
|
742
|
-
Yulin's own account of a request goes in headers and never in the body. A response keeps the shape
|
|
743
|
-
the real service returns. Live reload breaks that rule on purpose, and only for a page a browser is
|
|
744
|
-
about to render. A response gets the script only when all of this holds:
|
|
745
|
-
|
|
746
|
-
- the response media type is `text/html` exactly
|
|
747
|
-
- the request `accept` header asks for `text/html`
|
|
748
|
-
- the request carries no SigV4 signature, in either an `authorization` header or the query string of
|
|
749
|
-
a presigned URL, and no `x-amz-*` header
|
|
750
|
-
- the response has no `content-encoding`
|
|
751
|
-
- the response status is other than 206, and the request is not a `HEAD`
|
|
752
|
-
|
|
753
|
-
So an SDK `GetObject` for an HTML Object gets the stored bytes, byte for byte, while a browser
|
|
754
|
-
looking at the same Object through a website endpoint gets the script.
|
|
755
|
-
|
|
756
|
-
An injected response carries `x-sim-aws-live-reload: injected`. Its `content-length` is recomputed,
|
|
757
|
-
and `etag` and `last-modified` are dropped, since the bytes are no longer the ones those headers
|
|
758
|
-
describe. Its `cache-control` is set to `no-store`, replacing whatever the service said, because a
|
|
759
|
-
page held in the browser cache is a page live reload cannot reach. The script goes in before
|
|
760
|
-
`</body>`, or before `</html>` when there is no body element, or on the end when the HTML has no
|
|
761
|
-
such element.
|
|
762
|
-
|
|
763
|
-
### The reserved path
|
|
764
|
-
|
|
765
|
-
The reload channel is served at `/__sim-aws/live-reload` on every hostname the server answers on. A
|
|
766
|
-
page can ask for it relative to wherever it was served from. While live reload is on, no simulated
|
|
767
|
-
service can serve anything at that path.
|
|
768
|
-
|
|
769
|
-
## Messages on the console
|
|
770
|
-
|
|
771
|
-
A simulated Cognito user pool records the verification message it would have sent, simulated SNS
|
|
772
|
-
records the text message it would have texted, and simulated SES records the email it accepted.
|
|
773
|
-
Reading any of them back takes test code, or the pool's `/<userPoolId>/messages` listing. Either is
|
|
774
|
-
a detour when the sign-up form is open in a browser and the confirmation code is the one thing you
|
|
775
|
-
want. A served environment prints them as they are recorded:
|
|
776
|
-
|
|
777
|
-
```
|
|
778
|
-
sim Cognito eu-west-2_aBcDeFgHi: email to alice@example.com (SignUp)
|
|
779
|
-
Subject: Your verification code
|
|
780
|
-
Your confirmation code is 483920
|
|
781
|
-
sim SNS: SMS to +15550100
|
|
782
|
-
Your one-time code is 118221
|
|
783
|
-
sim SES: hello@example.com to alice@example.com
|
|
784
|
-
Subject: Welcome to Example
|
|
785
|
-
Text body:
|
|
786
|
-
Glad to have you here.
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
Nothing has to ask for that. Narrow it with `messageLogging`, which holds one property per kind of
|
|
790
|
-
message:
|
|
791
|
-
|
|
792
|
-
```typescript sim-serve-message-logging
|
|
793
|
-
/**
|
|
794
|
-
* Serving with the text messages left off, and the pool messages still
|
|
795
|
-
* printed.
|
|
796
|
-
*/
|
|
797
|
-
|
|
798
|
-
import { SimAws } from "@kensio/yulin";
|
|
799
|
-
import { serveSimAws } from "@kensio/yulin/serve";
|
|
800
|
-
|
|
801
|
-
const simAws = new SimAws();
|
|
802
|
-
const srv = await serveSimAws({
|
|
273
|
+
const server = await serveSimAws({
|
|
803
274
|
simAws,
|
|
804
275
|
port: 8787,
|
|
805
|
-
|
|
276
|
+
liveReload: true,
|
|
806
277
|
});
|
|
807
278
|
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
await srv.close();
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
`messageLogging: false` prints none of them.
|
|
814
|
-
|
|
815
|
-
An SMS the opt-out list stopped is printed with the suppression named. The publish succeeded, and a
|
|
816
|
-
handset that will never see the code is worth being told about rather than waited on:
|
|
279
|
+
// Reload connected browsers after changing simulated content in place.
|
|
280
|
+
server.reload();
|
|
817
281
|
|
|
818
|
-
|
|
819
|
-
sim SNS: SMS to +15550100 (suppressed, number opted out)
|
|
820
|
-
Your one-time code is 118221
|
|
282
|
+
await server.close();
|
|
821
283
|
```
|
|
822
284
|
|
|
823
|
-
|
|
285
|
+
The server injects a small client script into an HTML response when all of these conditions hold:
|
|
824
286
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
the
|
|
287
|
+
- the response media type is `text/html`
|
|
288
|
+
- the request accepts `text/html`
|
|
289
|
+
- the request is unsigned and has no `x-amz-*` header
|
|
290
|
+
- the response is unencoded, has a body, and is not a partial response
|
|
291
|
+
- the request method is not `HEAD`
|
|
830
292
|
|
|
831
|
-
|
|
832
|
-
sim
|
|
833
|
-
|
|
834
|
-
Template: password-reset {"code":"483920"}
|
|
835
|
-
Text body:
|
|
836
|
-
Follow this link to reset your password.
|
|
837
|
-
https://app.example.com/reset?token=abc123
|
|
838
|
-
HTML body: 4.1 kB, not printed
|
|
839
|
-
```
|
|
293
|
+
Signed SDK requests keep their original response bytes. An injected response has
|
|
294
|
+
`x-sim-aws-live-reload: injected`, uses `cache-control: no-store`, and drops validators that no
|
|
295
|
+
longer describe the changed body.
|
|
840
296
|
|
|
841
|
-
|
|
297
|
+
The reload channel uses `/__sim-aws/live-reload` on every served hostname. Calling `reload()` while
|
|
298
|
+
live reload is disabled throws an error.
|
|
842
299
|
|
|
843
|
-
|
|
844
|
-
Text body:
|
|
845
|
-
Here is your monthly statement in full.
|
|
846
|
-
... 4162 more characters, not printed
|
|
847
|
-
```
|
|
300
|
+
### Reload after an in-process update
|
|
848
301
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
See [Sending a pool's email through SES](https://yulinsim.dev/services/cognito/#sending-a-pools-email-through-ses).
|
|
302
|
+
Pass the server as a reload target when Yulin already watches the changed files. A mounted S3
|
|
303
|
+
directory accepts `{ reload: server }`. A watched CloudFormation template accepts the same target:
|
|
852
304
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
```typescript sim-serve-email-text-limit
|
|
305
|
+
```typescript sim-serve-watch-template
|
|
856
306
|
/**
|
|
857
|
-
*
|
|
307
|
+
* Updating a stack and reloading browsers when its template changes.
|
|
858
308
|
*/
|
|
859
309
|
|
|
860
310
|
import { SimAws } from "@kensio/yulin";
|
|
861
311
|
import { serveSimAws } from "@kensio/yulin/serve";
|
|
862
312
|
|
|
863
313
|
const simAws = new SimAws();
|
|
864
|
-
const
|
|
865
|
-
simAws,
|
|
866
|
-
port: 8787,
|
|
867
|
-
messageLogging: { emailTextLimit: 500 },
|
|
868
|
-
});
|
|
314
|
+
const server = await serveSimAws({ simAws, liveReload: true });
|
|
869
315
|
|
|
870
|
-
|
|
316
|
+
await simAws.cloudFormation().deployTemplateFile({
|
|
317
|
+
templatePath: "cdk.out/TestStack.template.json",
|
|
318
|
+
watch: { reload: server },
|
|
319
|
+
});
|
|
871
320
|
|
|
872
|
-
|
|
321
|
+
// Keep serving until the application shuts down.
|
|
873
322
|
```
|
|
874
323
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
`sentSmsMessages()` and `sentEmails()` are still where the whole history is.
|
|
324
|
+
Yulin waits for the stack update before reloading. A failed update leaves the current page and
|
|
325
|
+
resources in place.
|
|
878
326
|
|
|
879
|
-
|
|
327
|
+
### Restart after source changes
|
|
880
328
|
|
|
881
|
-
|
|
882
|
-
it. Run the dev script through it and a save is the whole loop:
|
|
329
|
+
Run a development command through `yulin watch` when source changes require a new process:
|
|
883
330
|
|
|
884
331
|
```bash
|
|
885
332
|
yulin watch -- tsx dev.ts
|
|
886
333
|
```
|
|
887
334
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
Restarting is the deliberate choice, over swapping code in place. A Lambda handler is a function
|
|
894
|
-
reference out of your own module graph, and a new process re-imports it with no module cache to
|
|
895
|
-
defeat. It also keeps simulated state the same as what a fresh test run sees, since seeding is part
|
|
896
|
-
of the setup script and runs again.
|
|
897
|
-
|
|
898
|
-
### What is watched
|
|
899
|
-
|
|
900
|
-
The working directory, minus the paths nobody edits by hand. Those are `node_modules`, `.git`,
|
|
901
|
-
`dist`, `coverage`, CDK asset directories, and the working files an editor writes around a save.
|
|
902
|
-
|
|
903
|
-
On top of that, Yulin names paths it is holding that the module graph never mentions. A directory
|
|
904
|
-
given to `mountBucketFilesystem` and a template given to `deployTemplateFile` are reported to the
|
|
905
|
-
supervisor as they are registered, and watched from then on, without appearing in any list. Editing
|
|
906
|
-
a file in a mounted directory or re-synthing a stack restarts the process.
|
|
907
|
-
|
|
908
|
-
A path the process is watching itself is the exception, and is
|
|
909
|
-
[left to the process reading it](#answering-a-change-instead-of-restarting). That covers a template
|
|
910
|
-
deployed with the `watch` option, and a directory mounted with somewhere to reload. So is any other
|
|
911
|
-
path the process [says it is holding](#holding-a-path-yourself).
|
|
335
|
+
The watcher restarts the command after files stop changing for 250 milliseconds. Use
|
|
336
|
+
`--settle=<milliseconds>` for builds that write files in longer bursts. It watches the working
|
|
337
|
+
directory and paths reported by Yulin, including mounted S3 directories and deployed template
|
|
338
|
+
files.
|
|
912
339
|
|
|
913
|
-
|
|
340
|
+
Paths handled in process are excluded from restart handling. This includes a template with a
|
|
341
|
+
`watch` option and a mounted directory with a reload target.
|
|
914
342
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
hundreds of files gets one restart, however many files it wrote.
|
|
343
|
+
`yulin watch` keeps running when the child command throws. The next file change starts it again. It
|
|
344
|
+
also detects a setup loop where each start writes the file that caused the restart.
|
|
918
345
|
|
|
919
|
-
|
|
920
|
-
events in waves. A build writing several thousand files was measured arriving as tens of waves up to
|
|
921
|
-
49ms apart, and a window anywhere near that turns one build into several restarts. A build that
|
|
922
|
-
pauses between its own phases, as a tool that resolves before it writes does, pauses for longer than
|
|
923
|
-
that again. 250ms clears the waves several times over and covers the shorter of those pauses, at the
|
|
924
|
-
cost of 250ms before a plain save is acted on.
|
|
925
|
-
|
|
926
|
-
A project whose build is unusual can say so on the command line:
|
|
346
|
+
Pass `--inspect=<port>` to add a Node inspector port to each child process:
|
|
927
347
|
|
|
928
348
|
```bash
|
|
929
|
-
yulin watch --
|
|
930
|
-
```
|
|
931
|
-
|
|
932
|
-
Writes that keep arriving push the wait back. A build that never goes quiet would otherwise hold the
|
|
933
|
-
restart off for as long as it ran, so a burst is acted on after five seconds however much is still
|
|
934
|
-
arriving, and the writes after that are a burst of their own. That is a backstop for a build that
|
|
935
|
-
writes continuously for minutes. An ordinary build never reaches it.
|
|
936
|
-
|
|
937
|
-
### Holding a path yourself
|
|
938
|
-
|
|
939
|
-
A process that is already watching a path and answering changes to it in place has nothing to gain
|
|
940
|
-
from a restart, and everything its simulation holds to lose. `simWatch.reportHeldPath(...)` says so,
|
|
941
|
-
and the supervisor leaves that path alone from then on:
|
|
942
|
-
|
|
943
|
-
```typescript sim-serve-hold-path
|
|
944
|
-
/**
|
|
945
|
-
* A mounted directory this process watches itself, reloading the browser
|
|
946
|
-
* rather than being restarted for it.
|
|
947
|
-
*/
|
|
948
|
-
|
|
949
|
-
import { watch } from "node:fs";
|
|
950
|
-
import path from "node:path";
|
|
951
|
-
|
|
952
|
-
import { CreateBucketCommand } from "@aws-sdk/client-s3";
|
|
953
|
-
import { SimAws } from "@kensio/yulin";
|
|
954
|
-
import { serveSimAws } from "@kensio/yulin/serve";
|
|
955
|
-
import { simWatch } from "@kensio/yulin/watch";
|
|
956
|
-
|
|
957
|
-
const built = path.join(process.cwd(), "public");
|
|
958
|
-
|
|
959
|
-
const simAws = new SimAws();
|
|
960
|
-
await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "site" }));
|
|
961
|
-
simAws.s3().mountBucketFilesystem("site", built);
|
|
962
|
-
|
|
963
|
-
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
964
|
-
|
|
965
|
-
simWatch.reportHeldPath(built);
|
|
966
|
-
|
|
967
|
-
watch(built, { recursive: true }, () => {
|
|
968
|
-
srv.reload();
|
|
969
|
-
});
|
|
349
|
+
yulin watch --inspect=9230 -- tsx dev.ts
|
|
970
350
|
```
|
|
971
351
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
beats having reported it. Being told a path is answered in the process is more specific than being
|
|
975
|
-
told it is worth watching, so holding wins even though `mountBucketFilesystem` reported the same
|
|
976
|
-
directory first.
|
|
977
|
-
|
|
978
|
-
A held path stays held only for the run that reported it. A run that exits releases the path, and
|
|
979
|
-
the supervisor watches it again until its replacement says otherwise.
|
|
980
|
-
|
|
981
|
-
`simWatch.onStopping(...)` is the other half, for a restart this process has no say in. It runs just
|
|
982
|
-
before the supervisor kills the process. Live reload uses that moment to tell browsers a reload is
|
|
983
|
-
coming, so a page comes back on its own. A hand-written reload channel wants the same warning.
|
|
352
|
+
The watcher does not run `cdk synth`. Run your own synth command and let Yulin react to the changed
|
|
353
|
+
template file.
|
|
984
354
|
|
|
985
|
-
|
|
986
|
-
run or a script launched from an IDE debugger, both are no-ops.
|
|
355
|
+
## Print simulated messages
|
|
987
356
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
the writes, so the example above is written for you. See
|
|
991
|
-
[reloading when a build changes a mounted directory](#reloading-when-a-build-changes-a-mounted-directory).
|
|
357
|
+
While the local server is running, it prints messages recorded by simulated Cognito, SNS, and SES.
|
|
358
|
+
This puts confirmation codes and local-development emails in the terminal.
|
|
992
359
|
|
|
993
|
-
|
|
360
|
+
Use `messageLogging` to disable a message kind or shorten the printed part of an email:
|
|
994
361
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
```typescript sim-serve-watch-template
|
|
362
|
+
```typescript sim-serve-message-logging
|
|
998
363
|
/**
|
|
999
|
-
*
|
|
364
|
+
* Disabling SNS message output and limiting printed email text.
|
|
1000
365
|
*/
|
|
1001
366
|
|
|
1002
367
|
import { SimAws } from "@kensio/yulin";
|
|
1003
368
|
import { serveSimAws } from "@kensio/yulin/serve";
|
|
1004
369
|
|
|
1005
370
|
const simAws = new SimAws();
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
reload: srv,
|
|
371
|
+
const server = await serveSimAws({
|
|
372
|
+
simAws,
|
|
373
|
+
messageLogging: {
|
|
374
|
+
sns: false,
|
|
375
|
+
emailTextLimit: 500,
|
|
1012
376
|
},
|
|
1013
377
|
});
|
|
1014
|
-
```
|
|
1015
378
|
|
|
1016
|
-
|
|
1017
|
-
and not the write. The page comes back on the resources the new template asked for, and an update
|
|
1018
|
-
that failed reloads nothing. Whatever the change left alone keeps what it holds in simulated S3,
|
|
1019
|
-
DynamoDB and SQS, where a restart would have taken all of it. The process names the template as one
|
|
1020
|
-
it is answering itself, and the supervisor takes it off its own list. See
|
|
1021
|
-
[watching a template file](https://yulinsim.dev/services/cloudformation/#watching-a-template-file) for what
|
|
1022
|
-
an update does to the resources.
|
|
1023
|
-
|
|
1024
|
-
A directory mounted with somewhere to reload is left alone the same way. The Bucket is reading the
|
|
1025
|
-
files either way, and a rebuild has nothing to redo. The browser is reloaded, and everything else
|
|
1026
|
-
the process is holding stays where it is. A restart would have taken the whole simulated environment
|
|
1027
|
-
for the sake of a page that changed. See
|
|
1028
|
-
[reloading when a build changes a mounted directory](#reloading-when-a-build-changes-a-mounted-directory).
|
|
1029
|
-
|
|
1030
|
-
A template synthesized against a real account sometimes needs adapting before Yulin will take it.
|
|
1031
|
-
`transform` is given the parsed template and answers with the one to deploy, on the deployment and
|
|
1032
|
-
again on every change. The file the supervisor leaves alone is still the one in `cdk.out`:
|
|
1033
|
-
|
|
1034
|
-
```typescript sim-serve-transform-template
|
|
1035
|
-
/**
|
|
1036
|
-
* Adapting a watched template, so the file being watched is the real one.
|
|
1037
|
-
*/
|
|
1038
|
-
|
|
1039
|
-
import { SimAws } from "@kensio/yulin";
|
|
1040
|
-
import { serveSimAws } from "@kensio/yulin/serve";
|
|
1041
|
-
|
|
1042
|
-
const simAws = new SimAws();
|
|
1043
|
-
const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
|
|
1044
|
-
|
|
1045
|
-
await simAws.cloudFormation().deployTemplateFile({
|
|
1046
|
-
templatePath: "cdk.out/TestStack.template.json",
|
|
1047
|
-
transform: (template) => ({
|
|
1048
|
-
...template,
|
|
1049
|
-
Resources: Object.fromEntries(
|
|
1050
|
-
Object.entries(template.Resources).filter(
|
|
1051
|
-
([logicalId]) => logicalId !== "SiteAliasRecord",
|
|
1052
|
-
),
|
|
1053
|
-
),
|
|
1054
|
-
}),
|
|
1055
|
-
watch: {
|
|
1056
|
-
onUpdated: () => {
|
|
1057
|
-
srv.reload();
|
|
1058
|
-
},
|
|
1059
|
-
},
|
|
1060
|
-
});
|
|
379
|
+
await server.close();
|
|
1061
380
|
```
|
|
1062
381
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
382
|
+
`messageLogging: false` disables all message output. An object leaves every omitted kind enabled.
|
|
383
|
+
Email output includes the sender, recipients, subject, and text body up to the configured limit.
|
|
384
|
+
HTML bodies are reported by size.
|
|
1066
385
|
|
|
1067
|
-
|
|
1068
|
-
|
|
386
|
+
Only messages recorded while the server is running are printed. The service-specific message
|
|
387
|
+
history remains available through its inspection methods.
|
|
1069
388
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
A setup script that throws leaves the watcher up. The error is on the terminal and the next save is
|
|
1073
|
-
the retry, with no watch to start over.
|
|
1074
|
-
|
|
1075
|
-
Setup that writes into a watched path restarts the process, which writes again, which restarts it.
|
|
1076
|
-
`yulin watch` refuses to run that loop. After a few restarts caused by the same file changing
|
|
1077
|
-
straight after startup, it stops and names the file. Write generated files outside the working
|
|
1078
|
-
directory, or into a directory the watch passes over.
|
|
1079
|
-
|
|
1080
|
-
### Debugging
|
|
1081
|
-
|
|
1082
|
-
A process started by `yulin watch` can be debugged. Attach the debugger to that process, and not to
|
|
1083
|
-
the supervisor, which has nothing worth stepping through. Pass an inspector flag to the watch and it
|
|
1084
|
-
reaches each run through `NODE_OPTIONS`, whether the command is `node` or something that spawns it:
|
|
1085
|
-
|
|
1086
|
-
```bash
|
|
1087
|
-
yulin watch --inspect=9230 -- tsx dev.ts
|
|
1088
|
-
```
|
|
389
|
+
## Available functionality
|
|
1089
390
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
391
|
+
- `serveSimAws` exposes simulated service hosts and supported AWS APIs over local HTTP.
|
|
392
|
+
- `SimAwsHttp` handles Fetch API requests in process.
|
|
393
|
+
- `SimAwsDns` handles DNS datagrams in process, and `SimAwsDnsServer` serves them over UDP.
|
|
394
|
+
- `server.localUrl(...)` converts simulated service URLs to the server's local port.
|
|
395
|
+
- `server.close()`, `server.closeOnSignal()`, and `simAws.close()` release network and file-watching
|
|
396
|
+
resources.
|
|
397
|
+
- Live reload supports explicit reloads, watched templates, mounted S3 directories, and process
|
|
398
|
+
restarts.
|
|
399
|
+
- The server logs simulated Cognito messages, SNS text messages, and SES emails by default.
|
|
400
|
+
- The shared AWS API endpoint serves S3, STS, IAM, ELBv2, SNS, CloudFormation, Lambda, and simulated
|
|
401
|
+
services that use the AWS JSON protocol.
|
|
1093
402
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
403
|
+
The AWS JSON services include DynamoDB, DynamoDB Streams, SQS, Cognito Identity Provider,
|
|
404
|
+
EventBridge, ECS, SSM, ACM, CloudWatch, CloudWatch Logs, KMS, Secrets Manager, and Rekognition. Each
|
|
405
|
+
service guide lists its supported commands.
|
|
1097
406
|
|
|
1098
407
|
## Limitations
|
|
1099
408
|
|
|
1100
|
-
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
-
|
|
1111
|
-
|
|
1112
|
-
- An open reload connection uses one of the browser's six connections per origin per tab.
|
|
1113
|
-
- There is no overlay for a reload that failed. The terminal has the error.
|
|
1114
|
-
- `yulin watch` does not re-synth CDK. `cdk watch` is a shortcut for `deploy --watch` against real
|
|
1115
|
-
AWS and there is no synth-only watch, so run your own synth and let the watch pick up its output.
|
|
1116
|
-
- A template updated in place still replaces a changed resource, so the objects in a bucket the
|
|
1117
|
-
change touches go with it. Only the resources the template left alone keep what they hold.
|
|
1118
|
-
- Lambda and CloudFront Function code is not swapped without a restart. A fresh process picks up an
|
|
1119
|
-
edited handler correctly, and an in-process swap would have to invalidate an ESM import subgraph
|
|
1120
|
-
that the language keeps private.
|
|
1121
|
-
- Simulated state is not carried across a restart. Seeding belongs in the setup script, so it runs
|
|
1122
|
-
again and local state stays the same as what tests and CI see.
|
|
1123
|
-
- The IDE run configurations for attaching a debugger to a watched process are not documented yet.
|
|
1124
|
-
- The served AWS service API covers S3, STS and the AWS JSON protocol services. A service speaking REST-JSON, or Query other than STS, is refused with `501 Not Implemented`.
|
|
1125
|
-
- Simulated STS implements `AssumeRole` and `GetCallerIdentity`, and serves both. `AssumeRoleWithWebIdentity` and `GetSessionToken` are refused as `NotImplemented`.
|
|
1126
|
-
- A served AWS API request is routed by its SigV4 credential scope. An unsigned one reaches nothing, whatever endpoint URL it used.
|
|
409
|
+
- The local server speaks HTTP only. `localUrl(...)` changes an HTTPS service URL to HTTP.
|
|
410
|
+
- Only the `Location` header is rewritten for local browsing. URLs in response bodies and cookie
|
|
411
|
+
domains remain unchanged.
|
|
412
|
+
- Live reload changes eligible HTML responses and is disabled by default. It decodes HTML as UTF-8.
|
|
413
|
+
- Live reload reserves `/__sim-aws/live-reload` and has no browser overlay for update failures.
|
|
414
|
+
- The DNS server uses UDP only.
|
|
415
|
+
- `yulin watch` does not preserve simulated state across a restart or reload changed Lambda and
|
|
416
|
+
CloudFront Function code inside the current process.
|
|
417
|
+
- The shared AWS API endpoint refuses unsupported protocols and operations with `501 Not
|
|
418
|
+
Implemented`.
|
|
419
|
+
- Some service functionality is available only through `SimAws` or SDK interception. Check the
|
|
420
|
+
relevant service guide before depending on access through the local endpoint.
|