@aws-blocks/core 0.1.18 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/cdk/__fixtures__/import-meta-handler.d.ts +6 -0
  2. package/dist/cdk/__fixtures__/import-meta-handler.d.ts.map +1 -0
  3. package/dist/cdk/__fixtures__/import-meta-handler.js +13 -0
  4. package/dist/cdk/blocks-backend.d.ts +46 -9
  5. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  6. package/dist/cdk/blocks-backend.js +66 -67
  7. package/dist/cdk/blocks-backend.test.js +114 -56
  8. package/dist/cdk/blocks-defaults.d.ts +70 -0
  9. package/dist/cdk/blocks-defaults.d.ts.map +1 -0
  10. package/dist/cdk/blocks-defaults.js +27 -0
  11. package/dist/cdk/blocks-defaults.test.d.ts +2 -0
  12. package/dist/cdk/blocks-defaults.test.d.ts.map +1 -0
  13. package/dist/cdk/blocks-defaults.test.js +24 -0
  14. package/dist/cdk/blocks-stack.test.js +74 -31
  15. package/dist/cdk/bundling.d.ts +42 -0
  16. package/dist/cdk/bundling.d.ts.map +1 -0
  17. package/dist/cdk/bundling.js +72 -0
  18. package/dist/cdk/bundling.test.d.ts +2 -0
  19. package/dist/cdk/bundling.test.d.ts.map +1 -0
  20. package/dist/cdk/bundling.test.js +80 -0
  21. package/dist/cdk/compute/compute.d.ts +33 -0
  22. package/dist/cdk/compute/compute.d.ts.map +1 -0
  23. package/dist/cdk/compute/compute.js +28 -0
  24. package/dist/cdk/compute/default-compute-factory.d.ts +33 -0
  25. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
  26. package/dist/cdk/compute/default-compute-factory.js +3 -0
  27. package/dist/cdk/index.d.ts +92 -12
  28. package/dist/cdk/index.d.ts.map +1 -1
  29. package/dist/cdk/index.js +141 -25
  30. package/dist/cdk/internal.d.ts +25 -0
  31. package/dist/cdk/internal.d.ts.map +1 -0
  32. package/dist/cdk/internal.js +27 -0
  33. package/dist/cdk/mixins.d.ts +18 -4
  34. package/dist/cdk/mixins.d.ts.map +1 -1
  35. package/dist/cdk/mixins.js +31 -7
  36. package/dist/cdk/mixins.test.js +45 -0
  37. package/dist/common/index.d.ts +9 -0
  38. package/dist/common/index.d.ts.map +1 -1
  39. package/dist/hosting-secrets.d.ts +48 -0
  40. package/dist/hosting-secrets.d.ts.map +1 -0
  41. package/dist/hosting-secrets.js +57 -0
  42. package/dist/hosting-secrets.test.d.ts +2 -0
  43. package/dist/hosting-secrets.test.d.ts.map +1 -0
  44. package/dist/hosting-secrets.test.js +81 -0
  45. package/dist/hosting.d.ts +88 -6
  46. package/dist/hosting.d.ts.map +1 -1
  47. package/dist/hosting.js +83 -24
  48. package/dist/hosting.test.js +158 -10
  49. package/dist/index.cdk.d.ts +13 -11
  50. package/dist/index.cdk.d.ts.map +1 -1
  51. package/dist/index.cdk.js +13 -8
  52. package/dist/index.d.ts +8 -7
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -5
  55. package/dist/lambda-handler.test.js +66 -0
  56. package/dist/pipeline/index.d.ts +24 -2
  57. package/dist/pipeline/index.d.ts.map +1 -1
  58. package/dist/pipeline/index.js +41 -1
  59. package/dist/pipeline.test.d.ts +2 -0
  60. package/dist/pipeline.test.d.ts.map +1 -0
  61. package/dist/pipeline.test.js +47 -0
  62. package/dist/rpc.d.ts +21 -0
  63. package/dist/rpc.d.ts.map +1 -1
  64. package/dist/rpc.js +46 -3
  65. package/dist/rpc.test.js +58 -1
  66. package/dist/scripts/config.d.ts +15 -0
  67. package/dist/scripts/config.d.ts.map +1 -0
  68. package/dist/scripts/config.js +29 -0
  69. package/dist/scripts/deploy.d.ts.map +1 -1
  70. package/dist/scripts/deploy.js +4 -0
  71. package/dist/scripts/dev-server.d.ts +8 -0
  72. package/dist/scripts/dev-server.d.ts.map +1 -1
  73. package/dist/scripts/dev-server.js +31 -0
  74. package/dist/scripts/generate-client.d.ts.map +1 -1
  75. package/dist/scripts/generate-client.js +9 -4
  76. package/dist/scripts/generate-client.test.js +19 -4
  77. package/dist/scripts/index.d.ts +13 -10
  78. package/dist/scripts/index.d.ts.map +1 -1
  79. package/dist/scripts/index.js +11 -8
  80. package/dist/scripts/preflight-credentials.d.ts +30 -0
  81. package/dist/scripts/preflight-credentials.d.ts.map +1 -0
  82. package/dist/scripts/preflight-credentials.js +108 -0
  83. package/dist/scripts/preflight-credentials.test.d.ts +2 -0
  84. package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
  85. package/dist/scripts/preflight-credentials.test.js +77 -0
  86. package/dist/scripts/sandbox-args.test.d.ts +2 -0
  87. package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
  88. package/dist/scripts/sandbox-args.test.js +77 -0
  89. package/dist/scripts/sandbox.d.ts +52 -0
  90. package/dist/scripts/sandbox.d.ts.map +1 -1
  91. package/dist/scripts/sandbox.js +92 -15
  92. package/dist/scripts/secret.d.ts +15 -0
  93. package/dist/scripts/secret.d.ts.map +1 -0
  94. package/dist/scripts/secret.js +29 -0
  95. package/dist/scripts/secret.test.d.ts +2 -0
  96. package/dist/scripts/secret.test.d.ts.map +1 -0
  97. package/dist/scripts/secret.test.js +28 -0
  98. package/dist/scripts/telemetry.test.js +2 -0
  99. package/dist/scripts/typegen.d.ts +15 -0
  100. package/dist/scripts/typegen.d.ts.map +1 -0
  101. package/dist/scripts/typegen.js +16 -0
  102. package/dist/secret-naming.d.ts +58 -0
  103. package/dist/secret-naming.d.ts.map +1 -0
  104. package/dist/secret-naming.js +67 -0
  105. package/dist/secret-naming.test.d.ts +2 -0
  106. package/dist/secret-naming.test.d.ts.map +1 -0
  107. package/dist/secret-naming.test.js +40 -0
  108. package/dist/telemetry/environment.d.ts.map +1 -1
  109. package/dist/telemetry/environment.js +3 -1
  110. package/dist/telemetry/telemetry.test.js +10 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.d.ts.map +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +7 -1
  115. package/src/cdk/__fixtures__/import-meta-handler.ts +16 -0
  116. package/src/cdk/blocks-backend.test.ts +308 -237
  117. package/src/cdk/blocks-backend.ts +98 -77
  118. package/src/cdk/blocks-defaults.test.ts +27 -0
  119. package/src/cdk/blocks-defaults.ts +75 -0
  120. package/src/cdk/blocks-stack.test.ts +160 -110
  121. package/src/cdk/bundling.test.ts +90 -0
  122. package/src/cdk/bundling.ts +76 -0
  123. package/src/cdk/compute/compute.ts +37 -0
  124. package/src/cdk/compute/default-compute-factory.ts +37 -0
  125. package/src/cdk/index.ts +178 -27
  126. package/src/cdk/internal.ts +30 -0
  127. package/src/cdk/mixins.test.ts +56 -1
  128. package/src/cdk/mixins.ts +32 -7
  129. package/src/common/index.ts +9 -0
  130. package/src/hosting-secrets.test.ts +106 -0
  131. package/src/hosting-secrets.ts +106 -0
  132. package/src/hosting.test.ts +244 -63
  133. package/src/hosting.ts +180 -68
  134. package/src/index.cdk.ts +79 -36
  135. package/src/index.ts +35 -16
  136. package/src/lambda-handler.test.ts +74 -0
  137. package/src/pipeline/index.ts +64 -11
  138. package/src/pipeline.test.ts +55 -0
  139. package/src/rpc.test.ts +66 -1
  140. package/src/rpc.ts +60 -3
  141. package/src/scripts/config.ts +35 -0
  142. package/src/scripts/deploy.ts +5 -0
  143. package/src/scripts/dev-server.ts +40 -0
  144. package/src/scripts/generate-client.test.ts +43 -8
  145. package/src/scripts/generate-client.ts +70 -65
  146. package/src/scripts/index.ts +20 -17
  147. package/src/scripts/preflight-credentials.test.ts +110 -0
  148. package/src/scripts/preflight-credentials.ts +131 -0
  149. package/src/scripts/sandbox-args.test.ts +92 -0
  150. package/src/scripts/sandbox.ts +102 -15
  151. package/src/scripts/secret.test.ts +35 -0
  152. package/src/scripts/secret.ts +35 -0
  153. package/src/scripts/telemetry.test.ts +2 -0
  154. package/src/scripts/typegen.ts +18 -0
  155. package/src/secret-naming.test.ts +59 -0
  156. package/src/secret-naming.ts +99 -0
  157. package/src/telemetry/environment.ts +3 -1
  158. package/src/telemetry/telemetry.test.ts +12 -0
  159. package/src/version.ts +1 -1
@@ -1,19 +1,64 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import { test, describe, before } from 'node:test';
5
4
  import assert from 'node:assert';
6
- import { fileURLToPath } from 'node:url';
7
5
  import { dirname, join } from 'node:path';
6
+ import { before, describe, test } from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
8
  import * as cdk from 'aws-cdk-lib';
9
- import { Template, Match } from 'aws-cdk-lib/assertions';
9
+ import { Match, Template } from 'aws-cdk-lib/assertions';
10
+ import * as apigateway from 'aws-cdk-lib/aws-apigateway';
10
11
  import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
12
+ import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
13
+ import type { Construct } from 'constructs';
14
+ import type { ScopeParent } from '../common/index.js';
15
+ import { BLOCKS_RPC_PREFIX } from '../constants.js';
11
16
  import { BlocksBackend } from './blocks-backend.js';
17
+ import { BlocksPresets } from './blocks-defaults.js';
18
+ import { Compute } from './compute/compute.js';
19
+ import type { DefaultComputeFactory } from './compute/default-compute-factory.js';
12
20
  import { Scope } from './index.js';
13
21
 
22
+ // A real app gets its default compute from @aws-blocks/bb-lambda-compute (via
23
+ // @aws-blocks/blocks), which core's own tests can't depend on. Use an
24
+ // equivalent inline stub: a Compute that owns a NodejsFunction + API Gateway,
25
+ // so create() can build the default and the handler/gateway/apiUrl accessors
26
+ // and synth-shape assertions have something real to resolve to. It is passed to
27
+ // each create() via the internal `defaultComputeFactory` option (see
28
+ // makeBackend), exactly as @aws-blocks/blocks injects LambdaCompute.
29
+ class StubLambdaCompute extends Compute {
30
+ readonly fn: lambda.NodejsFunction;
31
+ readonly apiGateway: apigateway.RestApi;
32
+ readonly apiUrl: string;
33
+
34
+ constructor(scope: ScopeParent, id: string) {
35
+ super(id, { parent: scope });
36
+ this.fn = new lambda.NodejsFunction(this, 'Handler', {
37
+ entry: this.backendHandlerPath,
38
+ runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
39
+ handler: 'handler',
40
+ role: this.executionRole,
41
+ environment: { BLOCKS_STACK_NAME: this.backendStackName },
42
+ bundling: { minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } },
43
+ });
44
+ this.apiGateway = new apigateway.RestApi(this, 'API', { restApiName: 'Blocks API' });
45
+ this.apiGateway.root.addProxy({
46
+ defaultIntegration: new apigateway.LambdaIntegration(this.fn),
47
+ anyMethod: true,
48
+ });
49
+ this.apiUrl = `${this.apiGateway.url}${BLOCKS_RPC_PREFIX.slice(1)}`;
50
+ }
51
+
52
+ setEnv(key: string, value: string): void {
53
+ this.fn.addEnvironment(key, value);
54
+ }
55
+ }
56
+
57
+ const stubComputeFactory: DefaultComputeFactory = (root) => new StubLambdaCompute(root as never, 'DefaultCompute');
58
+
14
59
  // Simulate the CDK condition being active (tests import CDK files directly)
15
60
  before(() => {
16
- process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
61
+ process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
17
62
  });
18
63
 
19
64
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -22,275 +67,301 @@ const sideEffectBackendPath = join(__dirname, '__fixtures__', 'side-effect-backe
22
67
  const factoryBackendPath = join(__dirname, '__fixtures__', 'factory-backend.js');
23
68
  const fullIdConstructBackendPath = join(__dirname, '__fixtures__', 'fullid-construct-backend.js');
24
69
  const EXECUTION_ROLE_MARKER_ACTION = 'blocks-test:MarkerAction';
70
+ const importMetaHandlerPath = join(__dirname, '__fixtures__', 'import-meta-handler.js');
25
71
 
26
- describe('ESM cache-busting (multi-stage)', () => {
27
- test('BlocksBackend.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
28
- const app = new cdk.App();
29
- const stack = new cdk.Stack(app, 'TestStack');
30
-
31
- const backend1 = await BlocksBackend.create(stack, 'Stage1', {
32
- backendHandlerPath: handlerPath,
33
- backendCDKPath: sideEffectBackendPath,
34
- });
35
-
36
- const backend2 = await BlocksBackend.create(stack, 'Stage2', {
37
- backendHandlerPath: handlerPath,
38
- backendCDKPath: sideEffectBackendPath,
39
- });
40
-
41
- const findMarker = (scope: cdk.aws_lambda_nodejs.NodejsFunction | any) =>
42
- scope.node.tryFindChild('SideEffectMarker');
72
+ // Wraps BlocksBackend.create, injecting the stub default-compute factory the way
73
+ // @aws-blocks/blocks injects LambdaCompute so tests don't repeat it 15 times.
74
+ const makeBackend = (scope: Construct, id: string, backendCDKPath: string) =>
75
+ BlocksBackend.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
43
76
 
44
- assert.ok(
45
- findMarker(backend1),
46
- 'Stage1 backend should have SideEffectMarker construct from module side effect',
47
- );
48
- assert.ok(
49
- findMarker(backend2),
50
- 'Stage2 backend should have SideEffectMarker construct from re-executed module',
51
- );
52
- });
77
+ describe('ESM cache-busting (multi-stage)', () => {
78
+ test('BlocksBackend.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
79
+ const app = new cdk.App();
80
+ const stack = new cdk.Stack(app, 'TestStack');
81
+
82
+ const backend1 = await makeBackend(stack, 'Stage1', sideEffectBackendPath);
83
+
84
+ const backend2 = await makeBackend(stack, 'Stage2', sideEffectBackendPath);
85
+
86
+ const findMarker = (scope: cdk.aws_lambda_nodejs.NodejsFunction | any) =>
87
+ scope.node.tryFindChild('SideEffectMarker');
88
+
89
+ assert.ok(
90
+ findMarker(backend1),
91
+ 'Stage1 backend should have SideEffectMarker construct from module side effect',
92
+ );
93
+ assert.ok(
94
+ findMarker(backend2),
95
+ 'Stage2 backend should have SideEffectMarker construct from re-executed module',
96
+ );
97
+ });
53
98
  });
54
99
 
55
100
  describe('synth shape (drop into existing stack)', () => {
56
- test('BlocksBackend lives inside the parent stack and synthesizes Lambda + API Gateway', async () => {
57
- const app = new cdk.App();
58
- const parent = new cdk.Stack(app, 'MyExistingStack');
59
-
60
- const backend = await BlocksBackend.create(parent, 'Blocks', {
61
- backendHandlerPath: handlerPath,
62
- backendCDKPath: sideEffectBackendPath,
63
- });
64
-
65
- // Public surface mirrors BlocksStack.
66
- assert.ok(backend.handler, 'BlocksBackend should expose .handler');
67
- assert.ok(backend.gateway, 'BlocksBackend should expose .gateway');
68
- assert.ok(backend.apiUrl, 'BlocksBackend should expose .apiUrl');
69
-
70
- // Synth produces the expected resources inside the parent stack —
71
- // no separate stack is created.
72
- const template = Template.fromStack(parent);
73
- template.hasResourceProperties('AWS::Lambda::Function', {});
74
- template.resourceCountIs('AWS::ApiGateway::RestApi', 1);
75
- });
76
-
77
- test('multiple BlocksBackends in the same parent stack do not collide', async () => {
78
- const app = new cdk.App();
79
- const parent = new cdk.Stack(app, 'MultiBackendStack');
80
-
81
- await BlocksBackend.create(parent, 'BackendA', {
82
- backendHandlerPath: handlerPath,
83
- backendCDKPath: sideEffectBackendPath,
84
- });
85
- await BlocksBackend.create(parent, 'BackendB', {
86
- backendHandlerPath: handlerPath,
87
- backendCDKPath: sideEffectBackendPath,
88
- });
89
-
90
- const template = Template.fromStack(parent);
91
- template.resourceCountIs('AWS::ApiGateway::RestApi', 2);
92
- });
101
+ test('BlocksBackend lives inside the parent stack and synthesizes Lambda + API Gateway', async () => {
102
+ const app = new cdk.App();
103
+ const parent = new cdk.Stack(app, 'MyExistingStack');
104
+
105
+ const backend = await makeBackend(parent, 'Blocks', sideEffectBackendPath);
106
+
107
+ // Public surface mirrors BlocksStack.
108
+ assert.ok(backend.handler, 'BlocksBackend should expose .handler');
109
+ assert.ok(backend.gateway, 'BlocksBackend should expose .gateway');
110
+ assert.ok(backend.apiUrl, 'BlocksBackend should expose .apiUrl');
111
+
112
+ // Synth produces the expected resources inside the parent stack —
113
+ // no separate stack is created.
114
+ const template = Template.fromStack(parent);
115
+ template.hasResourceProperties('AWS::Lambda::Function', {});
116
+ template.resourceCountIs('AWS::ApiGateway::RestApi', 1);
117
+ });
118
+
119
+ test('multiple BlocksBackends in the same parent stack do not collide', async () => {
120
+ const app = new cdk.App();
121
+ const parent = new cdk.Stack(app, 'MultiBackendStack');
122
+
123
+ await makeBackend(parent, 'BackendA', sideEffectBackendPath);
124
+ await makeBackend(parent, 'BackendB', sideEffectBackendPath);
125
+
126
+ const template = Template.fromStack(parent);
127
+ template.resourceCountIs('AWS::ApiGateway::RestApi', 2);
128
+ });
93
129
  });
94
130
 
95
131
  describe('shared execution role', () => {
96
- test('exposes executionRole on the backend', async () => {
97
- const app = new cdk.App();
98
- const parent = new cdk.Stack(app, 'RoleSurfaceStack');
99
-
100
- const backend = await BlocksBackend.create(parent, 'Blocks', {
101
- backendHandlerPath: handlerPath,
102
- backendCDKPath: sideEffectBackendPath,
103
- });
104
-
105
- assert.ok(backend.executionRole, 'BlocksBackend should expose .executionRole');
106
- });
107
-
108
- test('synth produces a Lambda-assumable role with basic execution, and the handler uses it', async () => {
109
- const app = new cdk.App();
110
- const parent = new cdk.Stack(app, 'RoleSynthStack');
111
-
112
- await BlocksBackend.create(parent, 'Blocks', {
113
- backendHandlerPath: handlerPath,
114
- backendCDKPath: sideEffectBackendPath,
115
- });
116
-
117
- const template = Template.fromStack(parent);
118
-
119
- // The shared role (logical id derived from the 'BlocksRole' construct id)
120
- // is assumable by Lambda and carries AWSLambdaBasicExecutionRole (so
121
- // CloudWatch Logs keep working after swapping off the auto-role). Other
122
- // roles exist (API Gateway CloudWatch role, config BucketDeployment role),
123
- // so we target ours by logical id.
124
- const roles = template.findResources('AWS::IAM::Role');
125
- const blocksRoleId = Object.keys(roles).find(k => k.includes('BlocksRole'));
126
- assert.ok(blocksRoleId, 'expected a role from the BlocksRole construct');
127
- const blocksRole = roles[blocksRoleId];
128
- assert.deepStrictEqual(blocksRole.Properties.AssumeRolePolicyDocument.Statement[0], {
129
- Action: 'sts:AssumeRole',
130
- Effect: 'Allow',
131
- Principal: { Service: 'lambda.amazonaws.com' },
132
- });
133
- assert.ok(
134
- JSON.stringify(blocksRole.Properties.ManagedPolicyArns ?? []).includes('AWSLambdaBasicExecutionRole'),
135
- 'BlocksRole should attach AWSLambdaBasicExecutionRole',
136
- );
137
-
138
- // The Blocks handler references the shared role, not an auto-generated one.
139
- template.hasResourceProperties('AWS::Lambda::Function', {
140
- Role: { 'Fn::GetAtt': [blocksRoleId, 'Arn'] },
141
- });
142
- });
132
+ test('exposes executionRole on the backend', async () => {
133
+ const app = new cdk.App();
134
+ const parent = new cdk.Stack(app, 'RoleSurfaceStack');
135
+
136
+ const backend = await makeBackend(parent, 'Blocks', sideEffectBackendPath);
137
+
138
+ assert.ok(backend.executionRole, 'BlocksBackend should expose .executionRole');
139
+ });
140
+
141
+ test('synth produces a Lambda-assumable role with basic execution, and the handler uses it', async () => {
142
+ const app = new cdk.App();
143
+ const parent = new cdk.Stack(app, 'RoleSynthStack');
144
+
145
+ await makeBackend(parent, 'Blocks', sideEffectBackendPath);
146
+
147
+ const template = Template.fromStack(parent);
148
+
149
+ // The shared role (logical id derived from the 'BlocksRole' construct id)
150
+ // is assumable by Lambda and carries AWSLambdaBasicExecutionRole (so
151
+ // CloudWatch Logs keep working after swapping off the auto-role). Other
152
+ // roles exist (API Gateway CloudWatch role, config BucketDeployment role),
153
+ // so we target ours by logical id.
154
+ const roles = template.findResources('AWS::IAM::Role');
155
+ const blocksRoleId = Object.keys(roles).find((k) => k.includes('BlocksRole'));
156
+ assert.ok(blocksRoleId, 'expected a role from the BlocksRole construct');
157
+ const blocksRole = roles[blocksRoleId];
158
+ assert.deepStrictEqual(blocksRole.Properties.AssumeRolePolicyDocument.Statement[0], {
159
+ Action: 'sts:AssumeRole',
160
+ Effect: 'Allow',
161
+ Principal: { Service: 'lambda.amazonaws.com' },
162
+ });
163
+ assert.ok(
164
+ JSON.stringify(blocksRole.Properties.ManagedPolicyArns ?? []).includes('AWSLambdaBasicExecutionRole'),
165
+ 'BlocksRole should attach AWSLambdaBasicExecutionRole',
166
+ );
167
+
168
+ // The Blocks handler references the shared role, not an auto-generated one.
169
+ template.hasResourceProperties('AWS::Lambda::Function', {
170
+ Role: { 'Fn::GetAtt': [blocksRoleId, 'Arn'] },
171
+ });
172
+ });
173
+
174
+ test('a nested block resolves executionRole via the construct-tree walk', async () => {
175
+ const app = new cdk.App();
176
+ const parent = new cdk.Stack(app, 'RoleResolveStack');
177
+
178
+ const backend = await makeBackend(parent, 'Blocks', sideEffectBackendPath);
179
+
180
+ // Build nested Scopes under the backend (outer → inner), the same shape a
181
+ // real Building Block tree has, and grant a uniquely-named marker action to
182
+ // `this.executionRole` from the innermost scope. If the getter's tree-walk
183
+ // failed, it would resolve the wrong role (or throw), and the marker would
184
+ // not land on the backend's shared role.
185
+ // `create()` sets globalThis.CURRENT_BLOCKS_STACK = backend, so a parent-less
186
+ // Scope attaches under the backend (the same way a real backend module's
187
+ // top-level blocks do); `inner` is then nested one level deeper.
188
+ const outer = new Scope('outer');
189
+ const inner = new Scope('inner', { parent: outer });
190
+
191
+ // Resolves to the backend's shared role from two levels deep.
192
+ assert.strictEqual(inner.executionRole, backend.executionRole);
193
+
194
+ inner.executionRole.addToPrincipalPolicy(
195
+ new PolicyStatement({ actions: [EXECUTION_ROLE_MARKER_ACTION], resources: ['*'] }),
196
+ );
197
+
198
+ // The grant lands on the shared role's default inline policy (AWS::IAM::Policy).
199
+ const template = Template.fromStack(parent);
200
+ template.hasResourceProperties('AWS::IAM::Policy', {
201
+ PolicyDocument: {
202
+ Statement: Match.arrayWith([Match.objectLike({ Action: EXECUTION_ROLE_MARKER_ACTION })]),
203
+ },
204
+ });
205
+ });
206
+ });
143
207
 
144
- test('a nested block resolves executionRole via the construct-tree walk', async () => {
208
+ describe('CJS bundle: import.meta.url in the handler is shimmed (no Lambda-load crash)', () => {
209
+ test('a handler that uses import.meta.url bundles successfully instead of throwing at load', async () => {
210
+ // The handler is bundled to CJS, where `import.meta` is empty. Left unshimmed,
211
+ // `fileURLToPath(import.meta.url)` compiles to `fileURLToPath(undefined)` and
212
+ // throws at Lambda load (esbuild only warns, so the broken bundle would deploy).
213
+ // blocksNodejsBundling shims import.meta.* to CommonJS equivalents, so bundling
214
+ // (which runs synchronously during construction) succeeds. The runtime behaviour
215
+ // of the emitted shim is verified directly in bundling.test.ts.
145
216
  const app = new cdk.App();
146
- const parent = new cdk.Stack(app, 'RoleResolveStack');
147
-
148
- const backend = await BlocksBackend.create(parent, 'Blocks', {
149
- backendHandlerPath: handlerPath,
150
- backendCDKPath: sideEffectBackendPath,
151
- });
152
-
153
- // Build nested Scopes under the backend (outer → inner), the same shape a
154
- // real Building Block tree has, and grant a uniquely-named marker action to
155
- // `this.executionRole` from the innermost scope. If the getter's tree-walk
156
- // failed, it would resolve the wrong role (or throw), and the marker would
157
- // not land on the backend's shared role.
158
- // `create()` sets globalThis.CURRENT_BLOCKS_STACK = backend, so a parent-less
159
- // Scope attaches under the backend (the same way a real backend module's
160
- // top-level blocks do); `inner` is then nested one level deeper.
161
- const outer = new Scope('outer');
162
- const inner = new Scope('inner', { parent: outer });
163
-
164
- // Resolves to the backend's shared role from two levels deep.
165
- assert.strictEqual(inner.executionRole, backend.executionRole);
166
-
167
- inner.executionRole.addToPrincipalPolicy(
168
- new PolicyStatement({ actions: [EXECUTION_ROLE_MARKER_ACTION], resources: ['*'] }),
217
+ const stack = new cdk.Stack(app, 'ImportMetaStack');
218
+
219
+ await assert.doesNotReject(() =>
220
+ BlocksBackend.create(stack, 'blocks', {
221
+ backendHandlerPath: importMetaHandlerPath,
222
+ backendCDKPath: sideEffectBackendPath,
223
+ defaults: BlocksPresets.production,
224
+ defaultComputeFactory: stubComputeFactory,
225
+ }),
169
226
  );
170
-
171
- // The grant lands on the shared role's default inline policy (AWS::IAM::Policy).
172
- const template = Template.fromStack(parent);
173
- template.hasResourceProperties('AWS::IAM::Policy', {
174
- PolicyDocument: {
175
- Statement: Match.arrayWith([Match.objectLike({ Action: EXECUTION_ROLE_MARKER_ACTION })]),
176
- },
177
- });
178
227
  });
179
228
  });
180
229
 
181
230
  describe('factory function support', () => {
182
- test('BlocksBackend.create() calls default export function with the backend instance', async () => {
183
- const app = new cdk.App();
184
- const stack = new cdk.Stack(app, 'FactoryTestStack');
231
+ test('BlocksBackend.create() calls default export function with the backend instance', async () => {
232
+ const app = new cdk.App();
233
+ const stack = new cdk.Stack(app, 'FactoryTestStack');
185
234
 
186
- const backend = await BlocksBackend.create(stack, 'FactoryStage', {
187
- backendHandlerPath: handlerPath,
188
- backendCDKPath: factoryBackendPath,
189
- });
235
+ const backend = await makeBackend(stack, 'FactoryStage', factoryBackendPath);
190
236
 
191
- const marker = backend.node.tryFindChild('FactoryMarker');
192
- assert.ok(marker, 'Factory function should have created FactoryMarker on the backend');
193
- });
237
+ const marker = backend.node.tryFindChild('FactoryMarker');
238
+ assert.ok(marker, 'Factory function should have created FactoryMarker on the backend');
239
+ });
194
240
  });
195
241
 
196
242
  describe('fullId is token-free (construct IDs / env-var keys)', () => {
197
- test('top-level stack: fullId is {stackName}-{id} and resolvable', async () => {
198
- const app = new cdk.App();
199
- const stack = new cdk.Stack(app, 'TopLevelStack');
200
-
201
- const backend = await BlocksBackend.create(stack, 'blocks', {
202
- backendHandlerPath: handlerPath,
203
- backendCDKPath: sideEffectBackendPath,
204
- });
205
-
206
- assert.strictEqual(backend.fullId, 'TopLevelStack-blocks');
207
- assert.ok(!cdk.Token.isUnresolved(backend.fullId), 'fullId must not contain a token');
208
- });
243
+ test('top-level stack: fullId is {stackName}-{id} and resolvable', async () => {
244
+ const app = new cdk.App();
245
+ const stack = new cdk.Stack(app, 'TopLevelStack');
246
+
247
+ const backend = await makeBackend(stack, 'blocks', sideEffectBackendPath);
248
+
249
+ assert.strictEqual(backend.fullId, 'TopLevelStack-blocks');
250
+ assert.ok(!cdk.Token.isUnresolved(backend.fullId), 'fullId must not contain a token');
251
+ });
252
+
253
+ test('nested stack: fullId stays token-free (regression for #714 / Amplify Gen2)', async () => {
254
+ // Amplify Gen2 wires Blocks into a NestedStack via backend.createStack('blocks').
255
+ // A NestedStack has a tokenized stackName that only resolves at deploy time —
256
+ // embedding it in fullId broke construct IDs with
257
+ // "ID components may not include unresolved tokens".
258
+ const app = new cdk.App();
259
+ const parent = new cdk.Stack(app, 'ParentStack');
260
+ const nested = new cdk.NestedStack(parent, 'blocks');
261
+
262
+ // Sanity: the nested stack's own name really is a token.
263
+ assert.ok(
264
+ cdk.Token.isUnresolved(nested.stackName),
265
+ 'precondition: NestedStack.stackName should be an unresolved token',
266
+ );
267
+
268
+ const backend = await makeBackend(nested, 'blocks', sideEffectBackendPath);
269
+
270
+ assert.ok(
271
+ !cdk.Token.isUnresolved(backend.fullId),
272
+ `fullId must be token-free inside a nested stack, got: ${backend.fullId}`,
273
+ );
274
+ // Falls back to the top-level (concrete) stack name, keeping uniqueness.
275
+ assert.strictEqual(backend.fullId, 'ParentStack-blocks');
276
+ });
277
+
278
+ test('child Scope can use fullId as a construct ID inside a nested stack', async () => {
279
+ // The fixture mirrors bb-distributed-data: it builds a construct whose ID is
280
+ // `${this.fullId}Marker`. If fullId carried a token, synth would throw.
281
+ const app = new cdk.App();
282
+ const parent = new cdk.Stack(app, 'Gen2ParentStack');
283
+ const nested = new cdk.NestedStack(parent, 'blocks');
284
+
285
+ const backend = await makeBackend(nested, 'blocks', fullIdConstructBackendPath);
286
+
287
+ // The construct ID is `${scope.fullId}Marker` → `ParentStack-blocks-blocks-dbMarker`.
288
+ const expectedId = `${backend.fullId}-dbMarker`;
289
+ assert.ok(nested.node.tryFindChild(expectedId), `expected a child construct with id "${expectedId}"`);
290
+
291
+ // Full synth must not throw on unresolved-token-in-construct-id.
292
+ assert.doesNotThrow(() => app.synth());
293
+ });
294
+
295
+ test('BLOCKS_STACK_NAME env var equals fullId (CDK-time ↔ runtime invariant)', async () => {
296
+ // Physical resource names (DynamoDB tables, DSQL env-var keys, IAM ARNs) are
297
+ // derived from fullId on BOTH sides: at synth via BlocksBackend.fullId, and at
298
+ // runtime via the root parent `{ id: process.env.BLOCKS_STACK_NAME }`. They MUST
299
+ // be byte-for-byte identical, otherwise the runtime looks up names/grants that
300
+ // were never created. BlocksBackend keeps them in sync by writing fullId into the
301
+ // handler's BLOCKS_STACK_NAME env var — assert that contract holds and is token-free.
302
+ const app = new cdk.App();
303
+ const parent = new cdk.Stack(app, 'InvariantParent');
304
+ const nested = new cdk.NestedStack(parent, 'blocks');
305
+
306
+ const backend = await makeBackend(nested, 'blocks', sideEffectBackendPath);
307
+
308
+ const template = Template.fromStack(nested);
309
+ const fns = template.findResources('AWS::Lambda::Function');
310
+ const envValues = Object.values(fns)
311
+ .map((fn: any) => fn.Properties?.Environment?.Variables?.BLOCKS_STACK_NAME)
312
+ .filter((v): v is string => typeof v === 'string');
313
+
314
+ assert.ok(envValues.length > 0, 'expected a Lambda with a BLOCKS_STACK_NAME env var');
315
+ for (const value of envValues) {
316
+ assert.strictEqual(
317
+ value,
318
+ backend.fullId,
319
+ 'BLOCKS_STACK_NAME must equal BlocksBackend.fullId so runtime names match synth',
320
+ );
321
+ assert.ok(!cdk.Token.isUnresolved(value), `BLOCKS_STACK_NAME must be token-free, got: ${value}`);
322
+ }
323
+ });
324
+ });
209
325
 
210
- test('nested stack: fullId stays token-free (regression for #714 / Amplify Gen2)', async () => {
211
- // Amplify Gen2 wires Blocks into a NestedStack via backend.createStack('blocks').
212
- // A NestedStack has a tokenized stackName that only resolves at deploy time —
213
- // embedding it in fullId broke construct IDs with
214
- // "ID components may not include unresolved tokens".
326
+ describe('infrastructure defaults (backend-anchored)', () => {
327
+ test('each backend exposes its own defaults', async () => {
215
328
  const app = new cdk.App();
216
- const parent = new cdk.Stack(app, 'ParentStack');
217
- const nested = new cdk.NestedStack(parent, 'blocks');
218
-
219
- // Sanity: the nested stack's own name really is a token.
220
- assert.ok(
221
- cdk.Token.isUnresolved(nested.stackName),
222
- 'precondition: NestedStack.stackName should be an unresolved token',
223
- );
329
+ const stack = new cdk.Stack(app, 'TwoBackendsStack');
224
330
 
225
- const backend = await BlocksBackend.create(nested, 'blocks', {
331
+ const a = await BlocksBackend.create(stack, 'A', {
226
332
  backendHandlerPath: handlerPath,
227
333
  backendCDKPath: sideEffectBackendPath,
334
+ defaults: BlocksPresets.production,
335
+ defaultComputeFactory: stubComputeFactory,
228
336
  });
229
-
230
- assert.ok(
231
- !cdk.Token.isUnresolved(backend.fullId),
232
- `fullId must be token-free inside a nested stack, got: ${backend.fullId}`,
233
- );
234
- // Falls back to the top-level (concrete) stack name, keeping uniqueness.
235
- assert.strictEqual(backend.fullId, 'ParentStack-blocks');
236
- });
237
-
238
- test('child Scope can use fullId as a construct ID inside a nested stack', async () => {
239
- // The fixture mirrors bb-distributed-data: it builds a construct whose ID is
240
- // `${this.fullId}Marker`. If fullId carried a token, synth would throw.
241
- const app = new cdk.App();
242
- const parent = new cdk.Stack(app, 'Gen2ParentStack');
243
- const nested = new cdk.NestedStack(parent, 'blocks');
244
-
245
- const backend = await BlocksBackend.create(nested, 'blocks', {
337
+ const b = await BlocksBackend.create(stack, 'B', {
246
338
  backendHandlerPath: handlerPath,
247
- backendCDKPath: fullIdConstructBackendPath,
339
+ backendCDKPath: sideEffectBackendPath,
340
+ defaults: BlocksPresets.sandbox,
341
+ defaultComputeFactory: stubComputeFactory,
248
342
  });
249
343
 
250
- // The construct ID is `${scope.fullId}Marker` `ParentStack-blocks-blocks-dbMarker`.
251
- const expectedId = `${backend.fullId}-dbMarker`;
252
- assert.ok(
253
- nested.node.tryFindChild(expectedId),
254
- `expected a child construct with id "${expectedId}"`,
255
- );
256
-
257
- // Full synth must not throw on unresolved-token-in-construct-id.
258
- assert.doesNotThrow(() => app.synth());
344
+ // Two backends in one stack must NOT clobber each other — defaults are
345
+ // anchored on the backend, not the shared stack.
346
+ assert.strictEqual(a.defaults, BlocksPresets.production);
347
+ assert.strictEqual(b.defaults, BlocksPresets.sandbox);
259
348
  });
260
349
 
261
- test('BLOCKS_STACK_NAME env var equals fullId (CDK-time runtime invariant)', async () => {
262
- // Physical resource names (DynamoDB tables, DSQL env-var keys, IAM ARNs) are
263
- // derived from fullId on BOTH sides: at synth via BlocksBackend.fullId, and at
264
- // runtime via the root parent `{ id: process.env.BLOCKS_STACK_NAME }`. They MUST
265
- // be byte-for-byte identical, otherwise the runtime looks up names/grants that
266
- // were never created. BlocksBackend keeps them in sync by writing fullId into the
267
- // handler's BLOCKS_STACK_NAME env var — assert that contract holds and is token-free.
350
+ test('a nested block resolves its owning backend defaults via the tree-walk', async () => {
268
351
  const app = new cdk.App();
269
- const parent = new cdk.Stack(app, 'InvariantParent');
270
- const nested = new cdk.NestedStack(parent, 'blocks');
352
+ const stack = new cdk.Stack(app, 'ResolveDefaultsStack');
271
353
 
272
- const backend = await BlocksBackend.create(nested, 'blocks', {
354
+ const backend = await BlocksBackend.create(stack, 'Blocks', {
273
355
  backendHandlerPath: handlerPath,
274
356
  backendCDKPath: sideEffectBackendPath,
357
+ defaults: BlocksPresets.sandbox,
358
+ defaultComputeFactory: stubComputeFactory,
275
359
  });
276
360
 
277
- const template = Template.fromStack(nested);
278
- const fns = template.findResources('AWS::Lambda::Function');
279
- const envValues = Object.values(fns)
280
- .map((fn: any) => fn.Properties?.Environment?.Variables?.BLOCKS_STACK_NAME)
281
- .filter((v): v is string => typeof v === 'string');
282
-
283
- assert.ok(envValues.length > 0, 'expected a Lambda with a BLOCKS_STACK_NAME env var');
284
- for (const value of envValues) {
285
- assert.strictEqual(
286
- value,
287
- backend.fullId,
288
- 'BLOCKS_STACK_NAME must equal BlocksBackend.fullId so runtime names match synth',
289
- );
290
- assert.ok(
291
- !cdk.Token.isUnresolved(value),
292
- `BLOCKS_STACK_NAME must be token-free, got: ${value}`,
293
- );
294
- }
361
+ // A Scope under the backend resolves scope.defaults by walking up to it.
362
+ const outer = new Scope('outer');
363
+ const inner = new Scope('inner', { parent: outer });
364
+ assert.strictEqual(inner.defaults, backend.defaults);
365
+ assert.strictEqual(inner.defaults, BlocksPresets.sandbox);
295
366
  });
296
367
  });