@aws-blocks/core 0.1.17 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdk/__fixtures__/import-meta-handler.d.ts +6 -0
- package/dist/cdk/__fixtures__/import-meta-handler.d.ts.map +1 -0
- package/dist/cdk/__fixtures__/import-meta-handler.js +13 -0
- package/dist/cdk/blocks-backend.d.ts +15 -0
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +41 -3
- package/dist/cdk/blocks-backend.test.js +137 -1
- package/dist/cdk/blocks-defaults.d.ts +44 -0
- package/dist/cdk/blocks-defaults.d.ts.map +1 -0
- package/dist/cdk/blocks-defaults.js +23 -0
- package/dist/cdk/blocks-defaults.test.d.ts +2 -0
- package/dist/cdk/blocks-defaults.test.d.ts.map +1 -0
- package/dist/cdk/blocks-defaults.test.js +22 -0
- package/dist/cdk/blocks-stack.test.js +39 -1
- package/dist/cdk/bundling.d.ts +42 -0
- package/dist/cdk/bundling.d.ts.map +1 -0
- package/dist/cdk/bundling.js +72 -0
- package/dist/cdk/bundling.test.d.ts +2 -0
- package/dist/cdk/bundling.test.d.ts.map +1 -0
- package/dist/cdk/bundling.test.js +80 -0
- package/dist/cdk/compute/compute.d.ts +33 -0
- package/dist/cdk/compute/compute.d.ts.map +1 -0
- package/dist/cdk/compute/compute.js +28 -0
- package/dist/cdk/index.d.ts +52 -0
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +95 -5
- package/dist/cdk/internal.d.ts +24 -0
- package/dist/cdk/internal.d.ts.map +1 -0
- package/dist/cdk/internal.js +27 -0
- package/dist/cdk/mixins.d.ts +18 -4
- package/dist/cdk/mixins.d.ts.map +1 -1
- package/dist/cdk/mixins.js +31 -7
- package/dist/cdk/mixins.test.js +45 -0
- package/dist/common/index.d.ts +9 -0
- package/dist/common/index.d.ts.map +1 -1
- package/dist/index.cdk.d.ts +2 -1
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +2 -1
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +12 -3
- package/dist/rpc.test.js +20 -0
- package/dist/scripts/telemetry.test.js +2 -0
- package/dist/telemetry/environment.d.ts.map +1 -1
- package/dist/telemetry/environment.js +3 -1
- package/dist/telemetry/telemetry.test.js +10 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
- package/src/cdk/__fixtures__/import-meta-handler.ts +16 -0
- package/src/cdk/blocks-backend.test.ts +166 -1
- package/src/cdk/blocks-backend.ts +55 -3
- package/src/cdk/blocks-defaults.test.ts +25 -0
- package/src/cdk/blocks-defaults.ts +49 -0
- package/src/cdk/blocks-stack.test.ts +48 -1
- package/src/cdk/bundling.test.ts +90 -0
- package/src/cdk/bundling.ts +76 -0
- package/src/cdk/compute/compute.ts +37 -0
- package/src/cdk/index.ts +105 -5
- package/src/cdk/internal.ts +29 -0
- package/src/cdk/mixins.test.ts +56 -1
- package/src/cdk/mixins.ts +32 -7
- package/src/common/index.ts +9 -0
- package/src/index.cdk.ts +5 -1
- package/src/rpc.test.ts +22 -0
- package/src/rpc.ts +19 -3
- package/src/scripts/telemetry.test.ts +2 -0
- package/src/telemetry/environment.ts +3 -1
- package/src/telemetry/telemetry.test.ts +12 -0
- package/src/version.ts +1 -1
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
import * as cdk from 'aws-cdk-lib';
|
|
5
5
|
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
6
6
|
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
7
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
7
8
|
import { CfnGroup } from 'aws-cdk-lib/aws-resourcegroups';
|
|
8
9
|
import { Construct } from 'constructs';
|
|
9
10
|
import { pathToFileURL } from 'node:url';
|
|
10
11
|
import { DEFAULT_NODE_RUNTIME } from './node-version.js';
|
|
12
|
+
import { blocksNodejsBundling } from './bundling.js';
|
|
11
13
|
import { addBlocksStackMetadata } from './stack-metadata.js';
|
|
12
14
|
import { finalizeConfigRegistry, registerConfig } from './config-registry.js';
|
|
15
|
+
import type { BlocksDefaults } from './blocks-defaults.js';
|
|
13
16
|
import { BLOCKS_NAMESPACE, BLOCKS_RPC_PREFIX } from '../constants.js';
|
|
14
17
|
import { registerBuiltinRoutes } from '../builtin-routes.js';
|
|
15
18
|
|
|
@@ -44,14 +47,50 @@ export function assertCdkConditionActive(): void {
|
|
|
44
47
|
export interface BlocksBackendProps {
|
|
45
48
|
backendHandlerPath: string;
|
|
46
49
|
backendCDKPath: string;
|
|
50
|
+
/**
|
|
51
|
+
* Stack-wide infrastructure defaults applied to every Building Block (removal
|
|
52
|
+
* policy, deletion protection, …). See {@link BlocksDefaults}. Start from
|
|
53
|
+
* `BlocksPresets.sandbox` or `BlocksPresets.production` and override
|
|
54
|
+
* individual fields as needed. A per-block option always wins over the
|
|
55
|
+
* corresponding stack default.
|
|
56
|
+
*/
|
|
57
|
+
defaults: BlocksDefaults;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/** Shared infra setup — creates Lambda + API Gateway on the given scope. */
|
|
50
61
|
export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id?: string) {
|
|
62
|
+
// Fail fast with an actionable message at the create() call site if `defaults`
|
|
63
|
+
// is missing (e.g. a plain-JS caller, `as any`, or a dynamically-built props
|
|
64
|
+
// object) — otherwise the first Building Block to read `scope.defaults` throws
|
|
65
|
+
// a cryptic `Cannot read properties of undefined (reading 'removalPolicy')`.
|
|
66
|
+
if (!props.defaults) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'BlocksStack/BlocksBackend requires a `defaults` field. Pass a posture from ' +
|
|
69
|
+
'`@aws-blocks/core/cdk` — typically `defaults: sandboxMode ? BlocksPresets.sandbox : BlocksPresets.production`.',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Shared execution role ──────────────────────────────────────────────
|
|
74
|
+
// A single IAM role that every Building Block grants to. Provisioned here so
|
|
75
|
+
// it exists before the backend module is imported (Building Blocks reach it
|
|
76
|
+
// via `scope.executionRole`). Block grants sit on the role's default (inline)
|
|
77
|
+
// policy. AWSLambdaBasicExecutionRole is attached so the handler retains
|
|
78
|
+
// CloudWatch Logs permissions.
|
|
79
|
+
const executionRole = new iam.Role(scope, 'BlocksRole', {
|
|
80
|
+
// CompositePrincipal (rather than a bare ServicePrincipal) so additional
|
|
81
|
+
// compute types can assume this same shared role as they are introduced
|
|
82
|
+
// (e.g. ECS tasks via ecs-tasks.amazonaws.com), by adding principals here.
|
|
83
|
+
assumedBy: new iam.CompositePrincipal(new iam.ServicePrincipal('lambda.amazonaws.com')),
|
|
84
|
+
managedPolicies: [
|
|
85
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
51
89
|
const handler = new lambda.NodejsFunction(scope, 'Handler', {
|
|
52
90
|
entry: props.backendHandlerPath,
|
|
53
91
|
runtime: DEFAULT_NODE_RUNTIME,
|
|
54
92
|
handler: 'handler',
|
|
93
|
+
role: executionRole,
|
|
55
94
|
memorySize: 2048,
|
|
56
95
|
timeout: cdk.Duration.seconds(60 * 15),
|
|
57
96
|
environment: {
|
|
@@ -67,10 +106,13 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
67
106
|
*/
|
|
68
107
|
BLOCKS_STACK_NAME: id ?? cdk.Stack.of(scope).stackName,
|
|
69
108
|
},
|
|
70
|
-
|
|
109
|
+
// blocksNodejsBundling shims import.meta.* to CommonJS equivalents so a
|
|
110
|
+
// CJS-bundled `fileURLToPath(import.meta.url)` resolves instead of throwing at
|
|
111
|
+
// Lambda load. See ./bundling.ts.
|
|
112
|
+
bundling: blocksNodejsBundling({
|
|
71
113
|
minify: true,
|
|
72
114
|
esbuildArgs: { '--conditions': 'aws-runtime' },
|
|
73
|
-
},
|
|
115
|
+
}),
|
|
74
116
|
});
|
|
75
117
|
|
|
76
118
|
// In sandbox mode, allow localhost origins so the local dev frontend can
|
|
@@ -152,7 +194,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
152
194
|
|
|
153
195
|
registerBuiltinRoutes();
|
|
154
196
|
|
|
155
|
-
return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}
|
|
197
|
+
return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}`, executionRole };
|
|
156
198
|
}
|
|
157
199
|
|
|
158
200
|
/**
|
|
@@ -177,6 +219,10 @@ export class BlocksBackend extends Construct {
|
|
|
177
219
|
public readonly gateway: apigateway.RestApi;
|
|
178
220
|
public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
179
221
|
public readonly backendHandlerPath: string;
|
|
222
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
223
|
+
public readonly executionRole: iam.IRole;
|
|
224
|
+
/** Infrastructure defaults for Building Blocks created under this backend. */
|
|
225
|
+
public readonly defaults: BlocksDefaults;
|
|
180
226
|
|
|
181
227
|
/**
|
|
182
228
|
* The fullId used by child Scopes to compute their env var names,
|
|
@@ -217,10 +263,16 @@ export class BlocksBackend extends Construct {
|
|
|
217
263
|
// Expose self to Building Blocks at CDK time
|
|
218
264
|
(globalThis as any).CURRENT_BLOCKS_STACK = this;
|
|
219
265
|
|
|
266
|
+
// Store defaults on the backend (not the stack) so several BlocksBackends
|
|
267
|
+
// in one stack each keep their own posture; Building Blocks resolve them by
|
|
268
|
+
// walking up to their owning backend (see Scope.defaults).
|
|
269
|
+
this.defaults = props.defaults;
|
|
270
|
+
|
|
220
271
|
const infra = setupBlocksInfra(this, props, id);
|
|
221
272
|
this.handler = infra.handler;
|
|
222
273
|
this.gateway = infra.gateway;
|
|
223
274
|
this.apiUrl = infra.apiUrl;
|
|
275
|
+
this.executionRole = infra.executionRole;
|
|
224
276
|
|
|
225
277
|
// Override BLOCKS_STACK_NAME to include the parent stack name so runtime
|
|
226
278
|
// resource lookups (DynamoDB table names) match the CDK-time fullId
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BlocksPresets carry the expected posture. Resolution/anchoring (a block
|
|
6
|
+
* reading its owning backend's defaults, and two backends in one stack keeping
|
|
7
|
+
* separate postures) is covered in blocks-backend.test.ts, which has the
|
|
8
|
+
* fixtures to construct real backends.
|
|
9
|
+
*/
|
|
10
|
+
import { test, describe } from 'node:test';
|
|
11
|
+
import assert from 'node:assert';
|
|
12
|
+
import { RemovalPolicy } from 'aws-cdk-lib';
|
|
13
|
+
import { BlocksPresets } from './blocks-defaults.js';
|
|
14
|
+
|
|
15
|
+
describe('BlocksPresets', () => {
|
|
16
|
+
test('sandbox is disposable: DESTROY + deletion protection off', () => {
|
|
17
|
+
assert.strictEqual(BlocksPresets.sandbox.removalPolicy, RemovalPolicy.DESTROY);
|
|
18
|
+
assert.strictEqual(BlocksPresets.sandbox.deletionProtection, false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('production is durable: RETAIN + deletion protection on', () => {
|
|
22
|
+
assert.strictEqual(BlocksPresets.production.removalPolicy, RemovalPolicy.RETAIN);
|
|
23
|
+
assert.strictEqual(BlocksPresets.production.deletionProtection, true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { RemovalPolicy } from 'aws-cdk-lib';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default values for every Amazon-authored Building Block created within a
|
|
8
|
+
* `BlocksStack` or `BlocksBackend`. A per-block option always overrides the
|
|
9
|
+
* corresponding default (`option ?? scope.defaults.field`).
|
|
10
|
+
*
|
|
11
|
+
* Start from {@link BlocksPresets} and override individual fields as needed.
|
|
12
|
+
*/
|
|
13
|
+
export interface BlocksDefaults {
|
|
14
|
+
/**
|
|
15
|
+
* What happens to a stateful resource (table, bucket, user pool) when the
|
|
16
|
+
* stack is deleted, via CDK/CloudFormation: `RETAIN` keeps the resource (and
|
|
17
|
+
* its data) behind; `DESTROY` tears it down. Applies only to CDK/CloudFormation
|
|
18
|
+
* deletions.
|
|
19
|
+
*/
|
|
20
|
+
removalPolicy: RemovalPolicy;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether to block deletion of a stateful resource. Unlike `removalPolicy`,
|
|
24
|
+
* this guards against deletion through **any** path — CDK/CloudFormation as
|
|
25
|
+
* well as the CLI, API, and AWS console — until it is turned off.
|
|
26
|
+
*/
|
|
27
|
+
deletionProtection: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Prepared starting points for {@link BlocksDefaults}. Pick one and override
|
|
32
|
+
* individual fields with a spread:
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* defaults: { ...BlocksPresets.production, deletionProtection: false }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const BlocksPresets = {
|
|
39
|
+
/** Disposable development stacks: tear down cleanly, no delete guard. */
|
|
40
|
+
sandbox: {
|
|
41
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
42
|
+
deletionProtection: false,
|
|
43
|
+
},
|
|
44
|
+
/** Durable, protected posture for permanent deployments. */
|
|
45
|
+
production: {
|
|
46
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
47
|
+
deletionProtection: true,
|
|
48
|
+
},
|
|
49
|
+
} satisfies Record<string, BlocksDefaults>;
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import * as cdk from 'aws-cdk-lib';
|
|
9
9
|
import { BlocksBackend } from './blocks-backend.js';
|
|
10
|
-
import { BlocksStack } from './index.js';
|
|
10
|
+
import { BlocksStack, BlocksPresets, Scope } from './index.js';
|
|
11
11
|
|
|
12
12
|
// Simulate the CDK condition being active (tests import CDK files directly)
|
|
13
13
|
before(() => {
|
|
@@ -26,11 +26,13 @@ describe('ESM cache-busting (multi-stage)', () => {
|
|
|
26
26
|
const stack1 = await BlocksStack.create(app, 'PipelineStage1', {
|
|
27
27
|
backendHandlerPath: handlerPath,
|
|
28
28
|
backendCDKPath: sideEffectBackendPath,
|
|
29
|
+
defaults: BlocksPresets.production,
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
const stack2 = await BlocksStack.create(app, 'PipelineStage2', {
|
|
32
33
|
backendHandlerPath: handlerPath,
|
|
33
34
|
backendCDKPath: sideEffectBackendPath,
|
|
35
|
+
defaults: BlocksPresets.production,
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
const findMarker = (scope: any) => scope.node.tryFindChild('SideEffectMarker');
|
|
@@ -53,6 +55,7 @@ describe('factory function support', () => {
|
|
|
53
55
|
const stack = await BlocksStack.create(app, 'FactoryBlocksStack', {
|
|
54
56
|
backendHandlerPath: handlerPath,
|
|
55
57
|
backendCDKPath: factoryBackendPath,
|
|
58
|
+
defaults: BlocksPresets.production,
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
const marker = stack.node.tryFindChild('FactoryMarker');
|
|
@@ -68,6 +71,7 @@ describe('legacy side-effect mode (no default export)', () => {
|
|
|
68
71
|
const backend = await BlocksBackend.create(stack, 'LegacyStage', {
|
|
69
72
|
backendHandlerPath: handlerPath,
|
|
70
73
|
backendCDKPath: sideEffectBackendPath,
|
|
74
|
+
defaults: BlocksPresets.production,
|
|
71
75
|
});
|
|
72
76
|
|
|
73
77
|
const marker = backend.node.tryFindChild('SideEffectMarker');
|
|
@@ -78,6 +82,48 @@ describe('legacy side-effect mode (no default export)', () => {
|
|
|
78
82
|
});
|
|
79
83
|
});
|
|
80
84
|
|
|
85
|
+
describe('shared execution role (BlocksStack)', () => {
|
|
86
|
+
// The role synth shape and the Scope.executionRole tree-walk are shared code
|
|
87
|
+
// (setupBlocksInfra + the getter), covered in blocks-backend.test.ts. The only
|
|
88
|
+
// BlocksStack-specific behavior is that its own constructor wires
|
|
89
|
+
// executionRole — a separate code path from BlocksBackend's constructor.
|
|
90
|
+
test('BlocksStack wires executionRole via its constructor', async () => {
|
|
91
|
+
const app = new cdk.App();
|
|
92
|
+
const stack = await BlocksStack.create(app, 'StackRoleStack', {
|
|
93
|
+
backendHandlerPath: handlerPath,
|
|
94
|
+
backendCDKPath: sideEffectBackendPath,
|
|
95
|
+
defaults: BlocksPresets.production,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert.ok(stack.executionRole, 'BlocksStack should expose a populated .executionRole');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('executionRole globalThis fallback', () => {
|
|
103
|
+
test('resolves via globalThis.CURRENT_BLOCKS_STACK when no owner is in the tree', async () => {
|
|
104
|
+
const app = new cdk.App();
|
|
105
|
+
const stack = await BlocksStack.create(app, 'FallbackStack', {
|
|
106
|
+
backendHandlerPath: handlerPath,
|
|
107
|
+
backendCDKPath: sideEffectBackendPath,
|
|
108
|
+
defaults: BlocksPresets.production,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// A Scope whose construct-tree ancestry has no BlocksStack/BlocksBackend
|
|
112
|
+
// (parented under a plain cdk.Stack) exhausts the tree-walk and falls back
|
|
113
|
+
// to globalThis.CURRENT_BLOCKS_STACK. The `as any` is test plumbing — a
|
|
114
|
+
// plain Stack isn't a ScopeParent, but it IS a valid Construct parent.
|
|
115
|
+
const plainStack = new cdk.Stack(app, 'PlainStack');
|
|
116
|
+
(globalThis as any).CURRENT_BLOCKS_STACK = stack;
|
|
117
|
+
const orphan = new Scope('orphan', { parent: plainStack as any });
|
|
118
|
+
|
|
119
|
+
assert.strictEqual(
|
|
120
|
+
orphan.executionRole,
|
|
121
|
+
stack.executionRole,
|
|
122
|
+
'fallback resolves to the ambient stack role',
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
81
127
|
describe('assertCdkConditionActive', () => {
|
|
82
128
|
test('BlocksStack.create() throws when --conditions=cdk is missing', async () => {
|
|
83
129
|
const origNodeOptions = process.env.NODE_OPTIONS;
|
|
@@ -92,6 +138,7 @@ describe('assertCdkConditionActive', () => {
|
|
|
92
138
|
BlocksStack.create(app, 'MissingConditionStack', {
|
|
93
139
|
backendHandlerPath: handlerPath,
|
|
94
140
|
backendCDKPath: sideEffectBackendPath,
|
|
141
|
+
defaults: BlocksPresets.production,
|
|
95
142
|
}),
|
|
96
143
|
(err: Error) => {
|
|
97
144
|
assert.ok(err.message.includes('Missing --conditions=cdk'), `Expected condition error, got: ${err.message}`);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { test, describe } from 'node:test';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { build } from 'esbuild';
|
|
12
|
+
import { OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
13
|
+
import { blocksNodejsBundling } from './bundling.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const importMetaFixture = join(__dirname, '__fixtures__', 'import-meta-handler.js');
|
|
17
|
+
const requireCjs = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
describe('blocksNodejsBundling', () => {
|
|
20
|
+
test('injects the import.meta.* CJS shim for the default (CJS) output', () => {
|
|
21
|
+
const out = blocksNodejsBundling({ minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } });
|
|
22
|
+
|
|
23
|
+
// Caller options are preserved.
|
|
24
|
+
assert.equal(out.minify, true);
|
|
25
|
+
assert.equal(out.esbuildArgs?.['--conditions'], 'aws-runtime');
|
|
26
|
+
|
|
27
|
+
// All three import.meta path properties are substituted.
|
|
28
|
+
assert.ok(out.esbuildArgs?.['--define:import.meta.url']);
|
|
29
|
+
assert.ok(out.esbuildArgs?.['--define:import.meta.dirname']);
|
|
30
|
+
assert.ok(out.esbuildArgs?.['--define:import.meta.filename']);
|
|
31
|
+
|
|
32
|
+
// The banner defines the substituted identifiers via CommonJS primitives.
|
|
33
|
+
assert.match(out.banner ?? '', /pathToFileURL\(__filename\)/);
|
|
34
|
+
assert.match(out.banner ?? '', /__dirname/);
|
|
35
|
+
assert.match(out.banner ?? '', /__filename/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('leaves ESM output untouched (import.meta works natively there)', () => {
|
|
39
|
+
const input = { format: OutputFormat.ESM, esbuildArgs: { '--conditions': 'aws-runtime' } };
|
|
40
|
+
const out = blocksNodejsBundling(input);
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(out, input);
|
|
43
|
+
assert.equal(out.esbuildArgs?.['--define:import.meta.url'], undefined);
|
|
44
|
+
assert.equal(out.banner, undefined);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('prepends the shim while keeping a caller-supplied banner', () => {
|
|
48
|
+
const out = blocksNodejsBundling({ banner: '// caller banner' });
|
|
49
|
+
assert.match(out.banner ?? '', /pathToFileURL\(__filename\)/);
|
|
50
|
+
assert.ok((out.banner ?? '').includes('// caller banner'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('a CJS bundle built with the shim resolves import.meta.url at load (no crash)', async () => {
|
|
54
|
+
// Bundle the fixture exactly as NodejsFunction would: apply the helper's `banner`
|
|
55
|
+
// and its `--define:import.meta.*` esbuildArgs. Without the shim this fixture's
|
|
56
|
+
// top-level `fileURLToPath(import.meta.url)` becomes `fileURLToPath(undefined)`
|
|
57
|
+
// and throws when the module is loaded.
|
|
58
|
+
const opts = blocksNodejsBundling({ minify: true });
|
|
59
|
+
const define: Record<string, string> = {};
|
|
60
|
+
for (const [key, value] of Object.entries(opts.esbuildArgs ?? {})) {
|
|
61
|
+
const m = key.match(/^--define:(.+)$/);
|
|
62
|
+
if (m) define[m[1]] = String(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tmp = mkdtempSync(join(tmpdir(), 'bb-shim-'));
|
|
66
|
+
const outfile = join(tmp, 'out.cjs');
|
|
67
|
+
try {
|
|
68
|
+
await build({
|
|
69
|
+
entryPoints: [importMetaFixture],
|
|
70
|
+
bundle: true,
|
|
71
|
+
platform: 'node',
|
|
72
|
+
format: 'cjs',
|
|
73
|
+
minify: true,
|
|
74
|
+
banner: { js: opts.banner ?? '' },
|
|
75
|
+
define,
|
|
76
|
+
outfile,
|
|
77
|
+
logLevel: 'silent',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Loading the bundle must not throw, and import.meta.url must resolve to a real
|
|
81
|
+
// (file-URL-derived) path rather than being undefined.
|
|
82
|
+
const mod = requireCjs(outfile);
|
|
83
|
+
assert.equal(typeof mod.moduleDir, 'string');
|
|
84
|
+
assert.ok(mod.moduleDir.length > 0, 'moduleDir should resolve to a non-empty path');
|
|
85
|
+
assert.equal(typeof mod.handler, 'function');
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { OutputFormat, type BundlingOptions } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
5
|
+
|
|
6
|
+
/** Banner-defined identifiers the shim substitutes `import.meta.*` with (CJS only). */
|
|
7
|
+
const IMPORT_META_SHIM = {
|
|
8
|
+
url: '__blocksImportMetaUrl',
|
|
9
|
+
dirname: '__blocksImportMetaDirname',
|
|
10
|
+
filename: '__blocksImportMetaFilename',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrap a `NodejsFunction` `bundling` config with the framework's hardened esbuild
|
|
15
|
+
* defaults, so every Lambda the framework bundles behaves consistently.
|
|
16
|
+
*
|
|
17
|
+
* **What it fixes.** `NodejsFunction` bundles to **CommonJS**, where `import.meta` is
|
|
18
|
+
* empty. Any bundled code that does `fileURLToPath(import.meta.url)` (a customer
|
|
19
|
+
* handler, a Building Block's `aws-runtime` code, or a dependency) would otherwise
|
|
20
|
+
* become `fileURLToPath(undefined)` and throw at Lambda load — esbuild only *warns*
|
|
21
|
+
* (`empty-import-meta`), so the broken bundle deploys and 502s on first invocation.
|
|
22
|
+
*
|
|
23
|
+
* **How.** For CJS output this shims `import.meta.url` / `import.meta.dirname` /
|
|
24
|
+
* `import.meta.filename` to their CommonJS equivalents (`pathToFileURL(__filename)`,
|
|
25
|
+
* `__dirname`, `__filename`) via an esbuild `--define` + `banner`. This is the same
|
|
26
|
+
* approach esbuild blesses (defining `import.meta` also suppresses the warning) and
|
|
27
|
+
* that Rollup applies by default, so:
|
|
28
|
+
* - a handler that reads `import.meta.url` no longer crashes at load, and
|
|
29
|
+
* - a bundled dependency that merely *contains* `import.meta` (even in dead code) no
|
|
30
|
+
* longer trips a build failure.
|
|
31
|
+
*
|
|
32
|
+
* The value resolves to the **bundled output file** (esbuild flattens the module tree),
|
|
33
|
+
* which is correct for the common cases — a value computed at synth (e.g. a
|
|
34
|
+
* `migrationsPath`) or dead interop fallbacks — but note it does not point at your
|
|
35
|
+
* source layout. Runtime code that must read a file relative to itself should not rely
|
|
36
|
+
* on `import.meta.url` inside a bundle; resolve such paths at synth time or ship the
|
|
37
|
+
* file as an asset. ESM output (`OutputFormat.ESM`) supports `import.meta` natively and
|
|
38
|
+
* is left untouched.
|
|
39
|
+
*
|
|
40
|
+
* All other options (`minify`, `commandHooks`, `externalModules`, other `esbuildArgs`
|
|
41
|
+
* such as `--conditions`, and any caller `banner`) are preserved.
|
|
42
|
+
*
|
|
43
|
+
* @param options - The site-specific `NodejsFunction` bundling options (optional).
|
|
44
|
+
* @returns The same options with the CJS `import.meta` shim merged in.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* new lambda.NodejsFunction(scope, 'Handler', {
|
|
48
|
+
* entry,
|
|
49
|
+
* bundling: blocksNodejsBundling({ minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } }),
|
|
50
|
+
* });
|
|
51
|
+
*/
|
|
52
|
+
export function blocksNodejsBundling(options: BundlingOptions = {}): BundlingOptions {
|
|
53
|
+
// ESM output has real `import.meta` — nothing to shim, and `require` in the banner
|
|
54
|
+
// wouldn't resolve. Only the CommonJS bundle needs the shim.
|
|
55
|
+
if (options.format === OutputFormat.ESM) return options;
|
|
56
|
+
|
|
57
|
+
const shimBanner = [
|
|
58
|
+
`const ${IMPORT_META_SHIM.url}=require('url').pathToFileURL(__filename).href;`,
|
|
59
|
+
`const ${IMPORT_META_SHIM.dirname}=__dirname;`,
|
|
60
|
+
`const ${IMPORT_META_SHIM.filename}=__filename;`,
|
|
61
|
+
].join('');
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...options,
|
|
65
|
+
// Prepend the shim definitions; keep any caller-supplied banner after them.
|
|
66
|
+
banner: options.banner ? `${shimBanner}\n${options.banner}` : shimBanner,
|
|
67
|
+
esbuildArgs: {
|
|
68
|
+
...options.esbuildArgs,
|
|
69
|
+
// Substitute import.meta.* with the banner identifiers. Also suppresses esbuild's
|
|
70
|
+
// empty-import-meta warning, so import.meta anywhere in the graph is safe.
|
|
71
|
+
'--define:import.meta.url': IMPORT_META_SHIM.url,
|
|
72
|
+
'--define:import.meta.dirname': IMPORT_META_SHIM.dirname,
|
|
73
|
+
'--define:import.meta.filename': IMPORT_META_SHIM.filename,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Scope } from '../index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base class for a Blocks *compute* — a runtime that executes handler code
|
|
8
|
+
* (Lambda today; containers later). A compute owns the physical function/service
|
|
9
|
+
* plus its ingress, and receives config via {@link setEnv}.
|
|
10
|
+
*
|
|
11
|
+
* The backend entry and stack name a compute needs are inherited from
|
|
12
|
+
* {@link Scope} (`backendHandlerPath` / `backendStackName`), which resolve them
|
|
13
|
+
* from the owning BlocksStack/BlocksBackend — never caller-supplied, so every
|
|
14
|
+
* compute in an app runs the same backend and agrees on the resource-name
|
|
15
|
+
* namespace.
|
|
16
|
+
*
|
|
17
|
+
* The abstract base lives in core (a framework primitive); concrete computes
|
|
18
|
+
* live in their own packages (e.g. `LambdaCompute` in `@aws-blocks/bb-lambda-compute`).
|
|
19
|
+
*
|
|
20
|
+
* @internal Not exported from the package's public entry points. Customers
|
|
21
|
+
* cannot instantiate a compute until the customer-facing surface exists.
|
|
22
|
+
*/
|
|
23
|
+
export abstract class Compute extends Scope {
|
|
24
|
+
/**
|
|
25
|
+
* API namespaces assigned to run on this compute — recorded so request
|
|
26
|
+
* routing can map a namespace to the compute that hosts it. Currently
|
|
27
|
+
* unpopulated (no compute assignment surface yet).
|
|
28
|
+
*/
|
|
29
|
+
readonly namespaces: string[] = [];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inject a runtime configuration value (an environment variable) into this
|
|
33
|
+
* compute. The framework calls this instead of `handler.addEnvironment()`
|
|
34
|
+
* directly so config targets the right compute.
|
|
35
|
+
*/
|
|
36
|
+
abstract setEnv(key: string, value: string): void;
|
|
37
|
+
}
|
package/src/cdk/index.ts
CHANGED
|
@@ -15,11 +15,17 @@ import {
|
|
|
15
15
|
import { setupBlocksInfra, BlocksBackend, assertCdkConditionActive } from './blocks-backend.js';
|
|
16
16
|
import { addBlocksStackMetadata } from './stack-metadata.js';
|
|
17
17
|
import { finalizeConfigRegistry } from './config-registry.js';
|
|
18
|
+
import { type BlocksDefaults, BlocksPresets } from './blocks-defaults.js';
|
|
18
19
|
|
|
19
20
|
export { BlocksBackend, type BlocksBackendProps } from './blocks-backend.js';
|
|
20
21
|
export { DEFAULT_NODE_RUNTIME } from './node-version.js';
|
|
22
|
+
export { blocksNodejsBundling } from './bundling.js';
|
|
21
23
|
export { SandboxDisableDeletionProtection } from './mixins.js';
|
|
22
24
|
export { registerConfig, finalizeConfigRegistry } from './config-registry.js';
|
|
25
|
+
export {
|
|
26
|
+
type BlocksDefaults,
|
|
27
|
+
BlocksPresets,
|
|
28
|
+
} from './blocks-defaults.js';
|
|
23
29
|
export { synthGuard } from './synth-guard.js';
|
|
24
30
|
export type { ScopeOptions } from '../index.js';
|
|
25
31
|
export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
@@ -30,11 +36,16 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
|
30
36
|
public readonly gateway: cdk.aws_apigateway.RestApi;
|
|
31
37
|
public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
32
38
|
public readonly backendHandlerPath: string;
|
|
39
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
40
|
+
public readonly executionRole: cdk.aws_iam.IRole;
|
|
41
|
+
/** Infrastructure defaults for Building Blocks created under this stack. */
|
|
42
|
+
public readonly defaults: BlocksDefaults;
|
|
33
43
|
|
|
34
44
|
private constructor(scope: Construct, id: string, props: BlocksStackProps) {
|
|
35
45
|
super(scope, id, props);
|
|
36
46
|
this.id = id;
|
|
37
47
|
this.backendHandlerPath = props.backendHandlerPath;
|
|
48
|
+
this.defaults = props.defaults;
|
|
38
49
|
|
|
39
50
|
// Set globalThis so Building Blocks attach directly to this stack
|
|
40
51
|
(globalThis as any).CURRENT_BLOCKS_STACK = this;
|
|
@@ -43,6 +54,7 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
|
43
54
|
this.handler = infra.handler;
|
|
44
55
|
this.gateway = infra.gateway;
|
|
45
56
|
this.apiUrl = infra.apiUrl;
|
|
57
|
+
this.executionRole = infra.executionRole;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
static async create(scope: Construct, id: string, props: BlocksStackProps) {
|
|
@@ -83,30 +95,118 @@ export class Scope extends Construct {
|
|
|
83
95
|
readonly bbName?: string;
|
|
84
96
|
readonly bbVersion?: string;
|
|
85
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The owning stack/backend (the root of the Blocks construct tree), resolved
|
|
100
|
+
* once at construction: the nearest BlocksStack/BlocksBackend up the construct
|
|
101
|
+
* tree, or the ambient `globalThis.CURRENT_BLOCKS_STACK` fallback. All
|
|
102
|
+
* root-derived accessors below read from this instead of each repeating the
|
|
103
|
+
* tree walk.
|
|
104
|
+
*/
|
|
105
|
+
private readonly root: BlocksStack | BlocksBackend;
|
|
106
|
+
|
|
86
107
|
constructor(id: string, options?: ScopeOptions) {
|
|
87
108
|
const parent = options?.parent || (globalThis as any).CURRENT_BLOCKS_STACK;
|
|
88
109
|
super(parent, id);
|
|
89
110
|
this.id = id;
|
|
90
111
|
this.parent = parent;
|
|
112
|
+
this.root = this.resolveRoot();
|
|
91
113
|
}
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Walk up the construct tree to the nearest owning BlocksStack/BlocksBackend;
|
|
117
|
+
* fall back to the ambient `globalThis.CURRENT_BLOCKS_STACK`. Called once from
|
|
118
|
+
* the constructor; the result is cached in {@link root}.
|
|
119
|
+
*/
|
|
120
|
+
private resolveRoot(): BlocksStack | BlocksBackend {
|
|
95
121
|
let current: Construct = this;
|
|
96
122
|
while (current.node.scope) {
|
|
97
123
|
current = current.node.scope as Construct;
|
|
98
124
|
if (current instanceof BlocksStack || current instanceof BlocksBackend) {
|
|
99
|
-
return current
|
|
125
|
+
return current;
|
|
100
126
|
}
|
|
101
127
|
}
|
|
102
|
-
// Fallback to
|
|
103
|
-
|
|
128
|
+
// Fallback to the ambient stack. In production this is always a real
|
|
129
|
+
// BlocksStack/BlocksBackend; the cast also admits the test doubles that set
|
|
130
|
+
// globalThis.CURRENT_BLOCKS_STACK to a stub exposing the same surface.
|
|
131
|
+
return (globalThis as any).CURRENT_BLOCKS_STACK as BlocksStack | BlocksBackend;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get handler() {
|
|
135
|
+
return this.root.handler;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The shared IAM role assumed by all Blocks compute. Building Blocks grant
|
|
140
|
+
* their permissions to this role; CDK's `grant*()` / `addToPrincipalPolicy()`
|
|
141
|
+
* route those grants to the role's default (inline) policy.
|
|
142
|
+
*/
|
|
143
|
+
get executionRole(): cdk.aws_iam.IRole {
|
|
144
|
+
return this.root.executionRole;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The backend entry file the owning BlocksStack/BlocksBackend runs — the
|
|
149
|
+
* single handler entry shared across the whole app.
|
|
150
|
+
*/
|
|
151
|
+
get backendHandlerPath(): string {
|
|
152
|
+
return this.root.backendHandlerPath;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The owning stack/backend's token-free root identity. This is the value the
|
|
157
|
+
* runtime receives as `BLOCKS_STACK_NAME` and rebuilds `fullId` from, so
|
|
158
|
+
* physical resource names (DynamoDB tables, env-var keys, IAM ARNs) derived
|
|
159
|
+
* from `fullId` match byte-for-byte between synth and runtime — otherwise the
|
|
160
|
+
* runtime looks up names that were never created. `BlocksBackend` exposes this
|
|
161
|
+
* as `fullId` ({@link BlocksBackend.fullId}); `BlocksStack` as `id`.
|
|
162
|
+
*/
|
|
163
|
+
get backendStackName(): string {
|
|
164
|
+
const name = this.root instanceof BlocksBackend ? this.root.fullId : this.root.id;
|
|
165
|
+
if (!name) {
|
|
166
|
+
throw new Error('Owning Blocks stack/backend has no id to derive BLOCKS_STACK_NAME');
|
|
167
|
+
}
|
|
168
|
+
return name;
|
|
104
169
|
}
|
|
105
170
|
|
|
106
171
|
get fullId(): string {
|
|
107
172
|
return computeScopeFullId(this);
|
|
108
173
|
}
|
|
109
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The stack-wide infrastructure {@link BlocksDefaults} registered by
|
|
177
|
+
* `BlocksStack.create` / `BlocksBackend.create`. Read these in a Building
|
|
178
|
+
* Block's CDK constructor to resolve a durability value, letting a per-block
|
|
179
|
+
* option override:
|
|
180
|
+
*
|
|
181
|
+
* ```ts
|
|
182
|
+
* const removalPolicy = options?.removalPolicy ?? this.defaults.removalPolicy;
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
get defaults(): BlocksDefaults {
|
|
186
|
+
// Resolve the same way as handler/executionRole: walk up to the owning
|
|
187
|
+
// BlocksStack/BlocksBackend and read its defaults, so several backends in
|
|
188
|
+
// one stack each keep their own posture. Falls back to the ambient stack,
|
|
189
|
+
// then to the production preset when none was registered.
|
|
190
|
+
let current: Construct = this;
|
|
191
|
+
while (current.node.scope) {
|
|
192
|
+
current = current.node.scope as Construct;
|
|
193
|
+
if (current instanceof BlocksStack || current instanceof BlocksBackend) {
|
|
194
|
+
return current.defaults;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const ambient = ((globalThis as any).CURRENT_BLOCKS_STACK as { defaults?: BlocksDefaults } | undefined)?.defaults;
|
|
198
|
+
if (ambient) return ambient;
|
|
199
|
+
// No owning BlocksStack/BlocksBackend in the tree and none ambient — this is
|
|
200
|
+
// usually a deliberate test stub, but could be a real misconfiguration (a
|
|
201
|
+
// block built outside any Blocks backend). Fall back to the safe production
|
|
202
|
+
// posture, and log so it's debuggable if it fires unexpectedly.
|
|
203
|
+
console.warn(
|
|
204
|
+
`[Blocks] Scope "${this.id}" resolved infrastructure defaults with no owning ` +
|
|
205
|
+
'BlocksStack/BlocksBackend in scope; falling back to BlocksPresets.production.',
|
|
206
|
+
);
|
|
207
|
+
return BlocksPresets.production;
|
|
208
|
+
}
|
|
209
|
+
|
|
110
210
|
protected buildUserAgentChain(): [string, string][] {
|
|
111
211
|
return [];
|
|
112
212
|
}
|