@aws-blocks/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +12 -0
  2. package/dist/cdk/blocks-backend.d.ts +4 -1
  3. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  4. package/dist/cdk/blocks-backend.js +56 -10
  5. package/dist/cdk/blocks-backend.test.js +71 -1
  6. package/dist/cdk/blocks-defaults.d.ts +11 -0
  7. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  8. package/dist/cdk/blocks-stack.test.js +23 -3
  9. package/dist/cdk/compute/compute.d.ts +80 -2
  10. package/dist/cdk/compute/compute.d.ts.map +1 -1
  11. package/dist/cdk/compute/compute.js +57 -3
  12. package/dist/cdk/config-registry.test.js +12 -0
  13. package/dist/cdk/dashboard-registry.d.ts +41 -0
  14. package/dist/cdk/dashboard-registry.d.ts.map +1 -0
  15. package/dist/cdk/dashboard-registry.js +61 -0
  16. package/dist/cdk/index.d.ts +67 -11
  17. package/dist/cdk/index.d.ts.map +1 -1
  18. package/dist/cdk/index.js +100 -18
  19. package/dist/cdk/internal.d.ts +3 -1
  20. package/dist/cdk/internal.d.ts.map +1 -1
  21. package/dist/cdk/internal.js +4 -1
  22. package/dist/cdk/tracer-registry.d.ts +31 -0
  23. package/dist/cdk/tracer-registry.d.ts.map +1 -0
  24. package/dist/cdk/tracer-registry.js +49 -0
  25. package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
  26. package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
  27. package/dist/cdk/vpc-requirements-registry.js +46 -0
  28. package/dist/cdk/vpc-types.d.ts +151 -0
  29. package/dist/cdk/vpc-types.d.ts.map +1 -0
  30. package/dist/cdk/vpc-types.js +3 -0
  31. package/dist/cdk/vpc.d.ts +59 -0
  32. package/dist/cdk/vpc.d.ts.map +1 -0
  33. package/dist/cdk/vpc.js +298 -0
  34. package/dist/cdk/vpc.test.d.ts +2 -0
  35. package/dist/cdk/vpc.test.d.ts.map +1 -0
  36. package/dist/cdk/vpc.test.js +285 -0
  37. package/dist/errors.d.ts +5 -0
  38. package/dist/errors.d.ts.map +1 -1
  39. package/dist/errors.js +5 -0
  40. package/dist/hosting.d.ts.map +1 -1
  41. package/dist/hosting.js +2 -0
  42. package/dist/hosting.test.js +39 -1
  43. package/dist/index.cdk.d.ts +2 -1
  44. package/dist/index.cdk.d.ts.map +1 -1
  45. package/dist/index.cdk.js +1 -1
  46. package/dist/lambda-handler.js +9 -2
  47. package/dist/lambda-handler.test.js +61 -1
  48. package/dist/raw-route.d.ts +15 -1
  49. package/dist/raw-route.d.ts.map +1 -1
  50. package/dist/raw-route.js +96 -12
  51. package/dist/raw-route.test.js +332 -1
  52. package/dist/scripts/dev-server.d.ts.map +1 -1
  53. package/dist/scripts/dev-server.js +11 -0
  54. package/dist/scripts/extract-ts-types.d.ts.map +1 -1
  55. package/dist/scripts/extract-ts-types.js +107 -23
  56. package/dist/scripts/extract-ts-types.test.js +225 -26
  57. package/dist/scripts/generate-spec.d.ts.map +1 -1
  58. package/dist/scripts/generate-spec.js +14 -5
  59. package/dist/scripts/generate-spec.test.js +93 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +8 -1
  63. package/src/cdk/blocks-backend.test.ts +144 -60
  64. package/src/cdk/blocks-backend.ts +298 -239
  65. package/src/cdk/blocks-defaults.ts +12 -0
  66. package/src/cdk/blocks-stack.test.ts +32 -13
  67. package/src/cdk/compute/compute.ts +105 -3
  68. package/src/cdk/config-registry.test.ts +14 -0
  69. package/src/cdk/dashboard-registry.ts +68 -0
  70. package/src/cdk/index.ts +426 -298
  71. package/src/cdk/internal.ts +6 -2
  72. package/src/cdk/tracer-registry.ts +54 -0
  73. package/src/cdk/vpc-requirements-registry.ts +63 -0
  74. package/src/cdk/vpc-types.ts +158 -0
  75. package/src/cdk/vpc.test.ts +348 -0
  76. package/src/cdk/vpc.ts +336 -0
  77. package/src/errors.ts +5 -0
  78. package/src/hosting.test.ts +59 -1
  79. package/src/hosting.ts +3 -0
  80. package/src/index.cdk.ts +7 -0
  81. package/src/lambda-handler.test.ts +79 -1
  82. package/src/lambda-handler.ts +11 -2
  83. package/src/raw-route.test.ts +427 -1
  84. package/src/raw-route.ts +125 -12
  85. package/src/scripts/dev-server.ts +12 -1
  86. package/src/scripts/extract-ts-types.test.ts +228 -26
  87. package/src/scripts/extract-ts-types.ts +104 -20
  88. package/src/scripts/generate-spec.test.ts +101 -0
  89. package/src/scripts/generate-spec.ts +15 -5
  90. package/src/version.ts +1 -1
@@ -15,6 +15,7 @@ import { join, dirname } from 'path';
15
15
  import { tmpdir } from 'os';
16
16
  import { fileURLToPath, pathToFileURL } from 'url';
17
17
  import { generateSpec } from './generate-spec.js';
18
+ import { ApiNamespace } from '../api.js';
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
  // `import.meta.url` lives in dist/scripts/, so `..` is dist/ and the built
20
21
  // `api.js` is next to it. Re-using the real ApiNamespace ensures the marker
@@ -86,3 +87,95 @@ describe('generateSpec — @blocksSkipCodegen', () => {
86
87
  }
87
88
  });
88
89
  });
90
+ describe('generateSpec — namespaces sharing a method name (regression: #445)', () => {
91
+ it('emits each namespace its OWN schema for a same-named method', async () => {
92
+ const dir = join(tmpdir(), `blocks-spec-test-${Date.now()}-collision`);
93
+ mkdirSync(dir, { recursive: true });
94
+ writeTsconfig(dir);
95
+ // Typed foundation for the TS type extractor. `create` has DIFFERENT params
96
+ // in each namespace; binding names (widgets/subscriptions) match the runtime
97
+ // module's export names below.
98
+ writeFileSync(join(dir, 'index.ts'), `
99
+ interface ApiNamespaceConstructor { new <T>(scope: any, name: string, handler: (ctx: any) => T): T; }
100
+ const ApiNamespace: ApiNamespaceConstructor = class {} as any;
101
+
102
+ export const widgets = new ApiNamespace(null, 'widgets', () => ({
103
+ async create(label: string): Promise<{ widgetId: string }> { return { widgetId: 'w' }; },
104
+ }));
105
+ export const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({
106
+ async create(topicCount: number): Promise<{ subId: number }> { return { subId: 1 }; },
107
+ }));
108
+ `);
109
+ // Runtime module for namespace discovery (types come from the .ts above via
110
+ // the extractor). Use the real ApiNamespace so the marker symbol matches.
111
+ const widgets = new ApiNamespace(null, 'widgets', () => ({ async create(_label) { return { widgetId: 'w' }; } }));
112
+ const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({ async create(_topicCount) { return { subId: 1 }; } }));
113
+ const loader = async () => ({ widgets, subscriptions });
114
+ try {
115
+ const doc = await generateSpec(join(dir, 'index.ts'), loader);
116
+ const byName = new Map(doc.methods.map((m) => [m.name, m]));
117
+ const w = byName.get('widgets.create');
118
+ const s = byName.get('subscriptions.create');
119
+ assert.ok(w && s, 'both namespace-qualified methods should be present');
120
+ // Each keeps its OWN param — no cross-assignment.
121
+ assert.strictEqual(w.params[0]?.name, 'label');
122
+ assert.strictEqual(w.params[0]?.schema?.type, 'string');
123
+ assert.strictEqual(s.params[0]?.name, 'topicCount');
124
+ assert.strictEqual(s.params[0]?.schema?.type, 'number');
125
+ }
126
+ finally {
127
+ rmSync(dir, { recursive: true, force: true });
128
+ }
129
+ });
130
+ });
131
+ describe('generateSpec — namespace returned by a factory, then destructured (regression: #444)', () => {
132
+ it('emits real schemas (not unknown) for a factory/destructured namespace', async () => {
133
+ const dir = join(tmpdir(), `blocks-spec-test-${Date.now()}-factory`);
134
+ mkdirSync(dir, { recursive: true });
135
+ writeTsconfig(dir);
136
+ // Typed foundation: one directly-exported namespace and one built inside a
137
+ // factory, returned as a property, destructured, and re-exported. Both use
138
+ // `create` with DIFFERENT params so a regression would cross-assign or emit
139
+ // unknown.
140
+ writeFileSync(join(dir, 'index.ts'), `
141
+ interface ApiNamespaceConstructor { new <T>(scope: any, name: string, handler: (ctx: any) => T): T; }
142
+ const ApiNamespace: ApiNamespaceConstructor = class {} as any;
143
+ class Scope { constructor(id: string) {} }
144
+
145
+ class Factory {
146
+ constructor(private readonly scope: Scope) {}
147
+ build() {
148
+ return {
149
+ subscriptions: new ApiNamespace(this.scope, 'subscriptions', () => ({
150
+ async create(topicCount: number): Promise<{ subId: number }> { return { subId: 1 }; },
151
+ })),
152
+ };
153
+ }
154
+ }
155
+
156
+ const scope = new Scope('app');
157
+ export const widgets = new ApiNamespace(scope, 'widgets', () => ({
158
+ async create(label: string): Promise<{ widgetId: string }> { return { widgetId: 'w' }; },
159
+ }));
160
+ const { subscriptions } = new Factory(scope).build();
161
+ export { subscriptions };
162
+ `);
163
+ const widgets = new ApiNamespace(null, 'widgets', () => ({ async create(_label) { return { widgetId: 'w' }; } }));
164
+ const subscriptions = new ApiNamespace(null, 'subscriptions', () => ({ async create(_topicCount) { return { subId: 1 }; } }));
165
+ const loader = async () => ({ widgets, subscriptions });
166
+ try {
167
+ const doc = await generateSpec(join(dir, 'index.ts'), loader);
168
+ const byName = new Map(doc.methods.map((m) => [m.name, m]));
169
+ const w = byName.get('widgets.create');
170
+ const s = byName.get('subscriptions.create');
171
+ assert.ok(w && s, 'both qualified methods present');
172
+ // The factory-returned namespace keeps its own param, not `unknown` and
173
+ // not the direct namespace's `string`.
174
+ assert.strictEqual(s.params[0]?.schema?.type, 'number');
175
+ assert.strictEqual(w.params[0]?.schema?.type, 'string');
176
+ }
177
+ finally {
178
+ rmSync(dir, { recursive: true, force: true });
179
+ }
180
+ });
181
+ });
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const CORE_VERSION = "0.4.0";
1
+ export declare const CORE_VERSION = "0.5.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
- export const CORE_VERSION = '0.4.0';
2
+ export const CORE_VERSION = '0.5.0';
package/package.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "name": "@aws-blocks/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
+ "keywords": [
5
+ "aws-blocks",
6
+ "framework",
7
+ "typescript",
8
+ "backend",
9
+ "primitives"
10
+ ],
4
11
  "repository": {
5
12
  "type": "git",
6
13
  "url": "git+https://github.com/aws-devtools-labs/aws-blocks.git",
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import * as cdk from 'aws-cdk-lib';
9
9
  import { Match, Template } from 'aws-cdk-lib/assertions';
10
10
  import * as apigateway from 'aws-cdk-lib/aws-apigateway';
11
+ import type { IWidget } from 'aws-cdk-lib/aws-cloudwatch';
11
12
  import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
12
13
  import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
13
14
  import type { Construct } from 'constructs';
@@ -18,6 +19,7 @@ import { BlocksPresets } from './blocks-defaults.js';
18
19
  import { Compute } from './compute/compute.js';
19
20
  import type { DefaultComputeFactory } from './compute/default-compute-factory.js';
20
21
  import { Scope } from './index.js';
22
+ import { getVpcContext } from './vpc.js';
21
23
 
22
24
  // A real app gets its default compute from @aws-blocks/bb-lambda-compute (via
23
25
  // @aws-blocks/blocks), which core's own tests can't depend on. Use an
@@ -37,6 +39,11 @@ class StubLambdaCompute extends Compute {
37
39
  this.logGroup = new cdk.aws_logs.LogGroup(this, 'HandlerLogGroup', {
38
40
  removalPolicy: cdk.RemovalPolicy.DESTROY,
39
41
  });
42
+ // Discover the shared VPC context from the owning stack/backend, mirroring
43
+ // LambdaCompute — so the core-side VPC placement plumbing (initializeVpc →
44
+ // getVpcContext) is exercised by these tests without depending on
45
+ // @aws-blocks/bb-lambda-compute.
46
+ const vpcContext = getVpcContext(this);
40
47
  this.fn = new lambda.NodejsFunction(this, 'Handler', {
41
48
  entry: this.backendHandlerPath,
42
49
  runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
@@ -45,6 +52,13 @@ class StubLambdaCompute extends Compute {
45
52
  logGroup: this.logGroup,
46
53
  environment: { BLOCKS_STACK_NAME: this.backendStackName },
47
54
  bundling: { minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } },
55
+ ...(vpcContext
56
+ ? {
57
+ vpc: vpcContext.vpc,
58
+ vpcSubnets: vpcContext.computeSubnets,
59
+ securityGroups: [vpcContext.computeSecurityGroup],
60
+ }
61
+ : {}),
48
62
  });
49
63
  this.apiGateway = new apigateway.RestApi(this, 'API', { restApiName: 'Blocks API' });
50
64
  this.apiGateway.root.addProxy({
@@ -57,6 +71,17 @@ class StubLambdaCompute extends Compute {
57
71
  setEnv(key: string, value: string): void {
58
72
  this.fn.addEnvironment(key, value);
59
73
  }
74
+
75
+ protected applyTracing(): void {}
76
+ protected healthWidgets(_region: string): IWidget[][] {
77
+ return [];
78
+ }
79
+ protected loggingWidgets(_region: string): IWidget[][] {
80
+ return [];
81
+ }
82
+ protected tracingWidgets(_region: string): IWidget[][] {
83
+ return [];
84
+ }
60
85
  }
61
86
 
62
87
  const stubComputeFactory: DefaultComputeFactory = (root) => new StubLambdaCompute(root as never, 'DefaultCompute');
@@ -77,7 +102,12 @@ const importMetaHandlerPath = join(__dirname, '__fixtures__', 'import-meta-handl
77
102
  // Wraps BlocksBackend.create, injecting the stub default-compute factory the way
78
103
  // @aws-blocks/blocks injects LambdaCompute — so tests don't repeat it 15 times.
79
104
  const makeBackend = (scope: Construct, id: string, backendCDKPath: string) =>
80
- BlocksBackend.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
105
+ BlocksBackend.create(scope, id, {
106
+ backendHandlerPath: handlerPath,
107
+ backendCDKPath,
108
+ defaults: BlocksPresets.production,
109
+ defaultComputeFactory: stubComputeFactory,
110
+ });
81
111
 
82
112
  describe('ESM cache-busting (multi-stage)', () => {
83
113
  test('BlocksBackend.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
@@ -230,25 +260,25 @@ describe('shared execution role', () => {
230
260
  });
231
261
 
232
262
  describe('CJS bundle: import.meta.url in the handler is shimmed (no Lambda-load crash)', () => {
233
- test('a handler that uses import.meta.url bundles successfully instead of throwing at load', async () => {
234
- // The handler is bundled to CJS, where `import.meta` is empty. Left unshimmed,
235
- // `fileURLToPath(import.meta.url)` compiles to `fileURLToPath(undefined)` and
236
- // throws at Lambda load (esbuild only warns, so the broken bundle would deploy).
237
- // blocksNodejsBundling shims import.meta.* to CommonJS equivalents, so bundling
238
- // (which runs synchronously during construction) succeeds. The runtime behaviour
239
- // of the emitted shim is verified directly in bundling.test.ts.
240
- const app = new cdk.App();
241
- const stack = new cdk.Stack(app, 'ImportMetaStack');
242
-
243
- await assert.doesNotReject(() =>
244
- BlocksBackend.create(stack, 'blocks', {
245
- backendHandlerPath: importMetaHandlerPath,
246
- backendCDKPath: sideEffectBackendPath,
247
- defaults: BlocksPresets.production,
248
- defaultComputeFactory: stubComputeFactory,
249
- }),
250
- );
251
- });
263
+ test('a handler that uses import.meta.url bundles successfully instead of throwing at load', async () => {
264
+ // The handler is bundled to CJS, where `import.meta` is empty. Left unshimmed,
265
+ // `fileURLToPath(import.meta.url)` compiles to `fileURLToPath(undefined)` and
266
+ // throws at Lambda load (esbuild only warns, so the broken bundle would deploy).
267
+ // blocksNodejsBundling shims import.meta.* to CommonJS equivalents, so bundling
268
+ // (which runs synchronously during construction) succeeds. The runtime behaviour
269
+ // of the emitted shim is verified directly in bundling.test.ts.
270
+ const app = new cdk.App();
271
+ const stack = new cdk.Stack(app, 'ImportMetaStack');
272
+
273
+ await assert.doesNotReject(() =>
274
+ BlocksBackend.create(stack, 'blocks', {
275
+ backendHandlerPath: importMetaHandlerPath,
276
+ backendCDKPath: sideEffectBackendPath,
277
+ defaults: BlocksPresets.production,
278
+ defaultComputeFactory: stubComputeFactory,
279
+ }),
280
+ );
281
+ });
252
282
  });
253
283
 
254
284
  describe('factory function support', () => {
@@ -348,44 +378,98 @@ describe('fullId is token-free (construct IDs / env-var keys)', () => {
348
378
  });
349
379
 
350
380
  describe('infrastructure defaults (backend-anchored)', () => {
351
- test('each backend exposes its own defaults', async () => {
352
- const app = new cdk.App();
353
- const stack = new cdk.Stack(app, 'TwoBackendsStack');
354
-
355
- const a = await BlocksBackend.create(stack, 'A', {
356
- backendHandlerPath: handlerPath,
357
- backendCDKPath: sideEffectBackendPath,
358
- defaults: BlocksPresets.production,
359
- defaultComputeFactory: stubComputeFactory,
360
- });
361
- const b = await BlocksBackend.create(stack, 'B', {
362
- backendHandlerPath: handlerPath,
363
- backendCDKPath: sideEffectBackendPath,
364
- defaults: BlocksPresets.sandbox,
365
- defaultComputeFactory: stubComputeFactory,
366
- });
367
-
368
- // Two backends in one stack must NOT clobber each other — defaults are
369
- // anchored on the backend, not the shared stack.
370
- assert.strictEqual(a.defaults, BlocksPresets.production);
371
- assert.strictEqual(b.defaults, BlocksPresets.sandbox);
372
- });
373
-
374
- test('a nested block resolves its owning backend defaults via the tree-walk', async () => {
375
- const app = new cdk.App();
376
- const stack = new cdk.Stack(app, 'ResolveDefaultsStack');
377
-
378
- const backend = await BlocksBackend.create(stack, 'Blocks', {
379
- backendHandlerPath: handlerPath,
380
- backendCDKPath: sideEffectBackendPath,
381
- defaults: BlocksPresets.sandbox,
382
- defaultComputeFactory: stubComputeFactory,
383
- });
384
-
385
- // A Scope under the backend resolves scope.defaults by walking up to it.
386
- const outer = new Scope('outer');
387
- const inner = new Scope('inner', { parent: outer });
388
- assert.strictEqual(inner.defaults, backend.defaults);
389
- assert.strictEqual(inner.defaults, BlocksPresets.sandbox);
390
- });
381
+ test('each backend exposes its own defaults', async () => {
382
+ const app = new cdk.App();
383
+ const stack = new cdk.Stack(app, 'TwoBackendsStack');
384
+
385
+ const a = await BlocksBackend.create(stack, 'A', {
386
+ backendHandlerPath: handlerPath,
387
+ backendCDKPath: sideEffectBackendPath,
388
+ defaults: BlocksPresets.production,
389
+ defaultComputeFactory: stubComputeFactory,
390
+ });
391
+ const b = await BlocksBackend.create(stack, 'B', {
392
+ backendHandlerPath: handlerPath,
393
+ backendCDKPath: sideEffectBackendPath,
394
+ defaults: BlocksPresets.sandbox,
395
+ defaultComputeFactory: stubComputeFactory,
396
+ });
397
+
398
+ // Two backends in one stack must NOT clobber each other — defaults are
399
+ // anchored on the backend, not the shared stack.
400
+ assert.strictEqual(a.defaults, BlocksPresets.production);
401
+ assert.strictEqual(b.defaults, BlocksPresets.sandbox);
402
+ });
403
+
404
+ test('a nested block resolves its owning backend defaults via the tree-walk', async () => {
405
+ const app = new cdk.App();
406
+ const stack = new cdk.Stack(app, 'ResolveDefaultsStack');
407
+
408
+ const backend = await BlocksBackend.create(stack, 'Blocks', {
409
+ backendHandlerPath: handlerPath,
410
+ backendCDKPath: sideEffectBackendPath,
411
+ defaults: BlocksPresets.sandbox,
412
+ defaultComputeFactory: stubComputeFactory,
413
+ });
414
+
415
+ // A Scope under the backend resolves scope.defaults by walking up to it.
416
+ const outer = new Scope('outer');
417
+ const inner = new Scope('inner', { parent: outer });
418
+ assert.strictEqual(inner.defaults, backend.defaults);
419
+ assert.strictEqual(inner.defaults, BlocksPresets.sandbox);
420
+ });
421
+ });
422
+
423
+ describe('VPC placement', () => {
424
+ test('places the handler Lambda in the VPC with a security group when vpc is provided', async () => {
425
+ const app = new cdk.App();
426
+ const parent = new cdk.Stack(app, 'VpcParent', {
427
+ env: { account: '123456789012', region: 'us-east-1' },
428
+ });
429
+ const vpc = new cdk.aws_ec2.Vpc(parent, 'AppVpc', { maxAzs: 2, natGateways: 1 });
430
+
431
+ await BlocksBackend.create(parent, 'Blocks', {
432
+ backendHandlerPath: handlerPath,
433
+ backendCDKPath: sideEffectBackendPath,
434
+ defaults: { ...BlocksPresets.production, vpc: { network: vpc } },
435
+ defaultComputeFactory: stubComputeFactory,
436
+ });
437
+
438
+ const template = Template.fromStack(parent);
439
+ // The handler Lambda is VPC-attached: it has a VpcConfig with subnets + SGs.
440
+ template.hasResourceProperties('AWS::Lambda::Function', {
441
+ VpcConfig: Match.objectLike({
442
+ SubnetIds: Match.anyValue(),
443
+ SecurityGroupIds: Match.anyValue(),
444
+ }),
445
+ });
446
+ // The VPC-access managed policy is attached to the execution role.
447
+ const policies = template.findResources('AWS::IAM::Role');
448
+ const hasVpcManagedPolicy = Object.values(policies).some((role: any) =>
449
+ JSON.stringify(role.Properties?.ManagedPolicyArns ?? []).includes('AWSLambdaVPCAccessExecutionRole'),
450
+ );
451
+ assert.ok(hasVpcManagedPolicy, 'execution role should have the VPC access managed policy');
452
+ });
453
+
454
+ test('no VpcConfig on the handler when vpc is omitted', async () => {
455
+ const app = new cdk.App();
456
+ const parent = new cdk.Stack(app, 'NoVpcParent');
457
+
458
+ await BlocksBackend.create(parent, 'Blocks', {
459
+ backendHandlerPath: handlerPath,
460
+ backendCDKPath: sideEffectBackendPath,
461
+ defaults: BlocksPresets.production,
462
+ defaultComputeFactory: stubComputeFactory,
463
+ });
464
+
465
+ const template = Template.fromStack(parent);
466
+ const fns = template.findResources('AWS::Lambda::Function');
467
+ for (const fn of Object.values(fns)) {
468
+ assert.strictEqual(
469
+ (fn as any).Properties?.VpcConfig,
470
+ undefined,
471
+ 'handler must not have VpcConfig when no VPC is configured',
472
+ );
473
+ }
474
+ });
391
475
  });