@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.
- 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 +46 -9
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +66 -67
- package/dist/cdk/blocks-backend.test.js +114 -56
- package/dist/cdk/blocks-defaults.d.ts +70 -0
- package/dist/cdk/blocks-defaults.d.ts.map +1 -0
- package/dist/cdk/blocks-defaults.js +27 -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 +24 -0
- package/dist/cdk/blocks-stack.test.js +74 -31
- 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/compute/default-compute-factory.d.ts +33 -0
- package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
- package/dist/cdk/compute/default-compute-factory.js +3 -0
- package/dist/cdk/index.d.ts +92 -12
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +141 -25
- package/dist/cdk/internal.d.ts +25 -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/hosting-secrets.d.ts +48 -0
- package/dist/hosting-secrets.d.ts.map +1 -0
- package/dist/hosting-secrets.js +57 -0
- package/dist/hosting-secrets.test.d.ts +2 -0
- package/dist/hosting-secrets.test.d.ts.map +1 -0
- package/dist/hosting-secrets.test.js +81 -0
- package/dist/hosting.d.ts +88 -6
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +83 -24
- package/dist/hosting.test.js +158 -10
- package/dist/index.cdk.d.ts +13 -11
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +13 -8
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/lambda-handler.test.js +66 -0
- package/dist/pipeline/index.d.ts +24 -2
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +41 -1
- package/dist/pipeline.test.d.ts +2 -0
- package/dist/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline.test.js +47 -0
- package/dist/rpc.d.ts +21 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +46 -3
- package/dist/rpc.test.js +58 -1
- package/dist/scripts/config.d.ts +15 -0
- package/dist/scripts/config.d.ts.map +1 -0
- package/dist/scripts/config.js +29 -0
- package/dist/scripts/deploy.d.ts.map +1 -1
- package/dist/scripts/deploy.js +4 -0
- package/dist/scripts/dev-server.d.ts +8 -0
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +31 -0
- package/dist/scripts/generate-client.d.ts.map +1 -1
- package/dist/scripts/generate-client.js +9 -4
- package/dist/scripts/generate-client.test.js +19 -4
- package/dist/scripts/index.d.ts +13 -10
- package/dist/scripts/index.d.ts.map +1 -1
- package/dist/scripts/index.js +11 -8
- package/dist/scripts/preflight-credentials.d.ts +30 -0
- package/dist/scripts/preflight-credentials.d.ts.map +1 -0
- package/dist/scripts/preflight-credentials.js +108 -0
- package/dist/scripts/preflight-credentials.test.d.ts +2 -0
- package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
- package/dist/scripts/preflight-credentials.test.js +77 -0
- package/dist/scripts/sandbox-args.test.d.ts +2 -0
- package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
- package/dist/scripts/sandbox-args.test.js +77 -0
- package/dist/scripts/sandbox.d.ts +52 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +92 -15
- package/dist/scripts/secret.d.ts +15 -0
- package/dist/scripts/secret.d.ts.map +1 -0
- package/dist/scripts/secret.js +29 -0
- package/dist/scripts/secret.test.d.ts +2 -0
- package/dist/scripts/secret.test.d.ts.map +1 -0
- package/dist/scripts/secret.test.js +28 -0
- package/dist/scripts/telemetry.test.js +2 -0
- package/dist/scripts/typegen.d.ts +15 -0
- package/dist/scripts/typegen.d.ts.map +1 -0
- package/dist/scripts/typegen.js +16 -0
- package/dist/secret-naming.d.ts +58 -0
- package/dist/secret-naming.d.ts.map +1 -0
- package/dist/secret-naming.js +67 -0
- package/dist/secret-naming.test.d.ts +2 -0
- package/dist/secret-naming.test.d.ts.map +1 -0
- package/dist/secret-naming.test.js +40 -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 +7 -1
- package/src/cdk/__fixtures__/import-meta-handler.ts +16 -0
- package/src/cdk/blocks-backend.test.ts +308 -237
- package/src/cdk/blocks-backend.ts +98 -77
- package/src/cdk/blocks-defaults.test.ts +27 -0
- package/src/cdk/blocks-defaults.ts +75 -0
- package/src/cdk/blocks-stack.test.ts +160 -110
- 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/compute/default-compute-factory.ts +37 -0
- package/src/cdk/index.ts +178 -27
- package/src/cdk/internal.ts +30 -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/hosting-secrets.test.ts +106 -0
- package/src/hosting-secrets.ts +106 -0
- package/src/hosting.test.ts +244 -63
- package/src/hosting.ts +180 -68
- package/src/index.cdk.ts +79 -36
- package/src/index.ts +35 -16
- package/src/lambda-handler.test.ts +74 -0
- package/src/pipeline/index.ts +64 -11
- package/src/pipeline.test.ts +55 -0
- package/src/rpc.test.ts +66 -1
- package/src/rpc.ts +60 -3
- package/src/scripts/config.ts +35 -0
- package/src/scripts/deploy.ts +5 -0
- package/src/scripts/dev-server.ts +40 -0
- package/src/scripts/generate-client.test.ts +43 -8
- package/src/scripts/generate-client.ts +70 -65
- package/src/scripts/index.ts +20 -17
- package/src/scripts/preflight-credentials.test.ts +110 -0
- package/src/scripts/preflight-credentials.ts +131 -0
- package/src/scripts/sandbox-args.test.ts +92 -0
- package/src/scripts/sandbox.ts +102 -15
- package/src/scripts/secret.test.ts +35 -0
- package/src/scripts/secret.ts +35 -0
- package/src/scripts/telemetry.test.ts +2 -0
- package/src/scripts/typegen.ts +18 -0
- package/src/secret-naming.test.ts +59 -0
- package/src/secret-naming.ts +99 -0
- package/src/telemetry/environment.ts +3 -1
- package/src/telemetry/telemetry.test.ts +12 -0
- package/src/version.ts +1 -1
package/src/pipeline/index.ts
CHANGED
|
@@ -1,16 +1,69 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { __PIPELINE_STAGE_SCOPE__, DeployStage, Pipeline as LeafPipeline } from '@aws-blocks/pipeline';
|
|
5
|
+
import type {
|
|
6
|
+
BranchConfig,
|
|
7
|
+
DeployStageProps,
|
|
8
|
+
PipelineProps,
|
|
9
|
+
PipelineSourceConfig,
|
|
10
|
+
PipelineStageConfig,
|
|
11
|
+
PipelineSynthConfig,
|
|
8
12
|
} from '@aws-blocks/pipeline';
|
|
13
|
+
import type { Construct } from 'constructs';
|
|
14
|
+
import { blocksStoreConfig } from '../secret-naming.js';
|
|
15
|
+
|
|
16
|
+
export { __PIPELINE_STAGE_SCOPE__, DeployStage };
|
|
9
17
|
export type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
18
|
+
BranchConfig,
|
|
19
|
+
DeployStageProps,
|
|
20
|
+
PipelineProps,
|
|
21
|
+
PipelineSourceConfig,
|
|
22
|
+
PipelineStageConfig,
|
|
23
|
+
PipelineSynthConfig,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Merge the Blocks `/blocks/*` namespace as the DEFAULT store config, so a
|
|
28
|
+
* Blocks pipeline resolves `connectionArn` / `buildSecrets` from the same place
|
|
29
|
+
* the Blocks CLI (`npm run secret` / `npm run config`) writes. Per-kind and
|
|
30
|
+
* shallow: any field the caller sets (a custom `prefix`, a `stage`, a
|
|
31
|
+
* `cacheTtlSeconds`) wins over the Blocks default.
|
|
32
|
+
*/
|
|
33
|
+
function withBlocksDefaults<TConfig>(props: PipelineProps<TConfig>): PipelineProps<TConfig> {
|
|
34
|
+
const blocks = blocksStoreConfig();
|
|
35
|
+
return {
|
|
36
|
+
...props,
|
|
37
|
+
secretStore: { ...blocks.secretStore, ...props.secretStore },
|
|
38
|
+
configStore: { ...blocks.configStore, ...props.configStore },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Blocks `Pipeline` — the framework-neutral `@aws-blocks/pipeline` construct with
|
|
44
|
+
* the Blocks value namespace pinned. It defaults `secretStore` / `configStore` to
|
|
45
|
+
* `/blocks/secrets` and `/blocks/config` (the prefixes the Blocks `secret` /
|
|
46
|
+
* `config` CLIs write), mirroring how the Blocks `Hosting` block pins the same
|
|
47
|
+
* namespace — so a value set with `npm run secret`/`config` is the value the
|
|
48
|
+
* pipeline reads. Pass `secretStore` / `configStore` to override.
|
|
49
|
+
*
|
|
50
|
+
* @see {@link https://github.com/aws-devtools-labs/aws-blocks/blob/main/packages/pipeline/README.md}
|
|
51
|
+
*/
|
|
52
|
+
export class Pipeline<TConfig = Record<string, unknown>> extends LeafPipeline<TConfig> {
|
|
53
|
+
constructor(scope: Construct, id: string, props: PipelineProps<TConfig>) {
|
|
54
|
+
super(scope, id, withBlocksDefaults(props));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Async constructor — required when `source.connectionArn` is a `config()`
|
|
59
|
+
* marker (resolved at synth time). Pins the Blocks namespace, then delegates to
|
|
60
|
+
* the leaf {@link LeafPipeline.create}.
|
|
61
|
+
*/
|
|
62
|
+
static async create<TConfig = Record<string, unknown>>(
|
|
63
|
+
scope: Construct,
|
|
64
|
+
id: string,
|
|
65
|
+
props: PipelineProps<TConfig>,
|
|
66
|
+
): Promise<LeafPipeline<TConfig>> {
|
|
67
|
+
return LeafPipeline.create(scope, id, withBlocksDefaults(props));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { afterEach, describe, it } from 'node:test';
|
|
6
|
+
import { _setSynthSecretFetcher } from '@aws-blocks/hosting/constructs';
|
|
7
|
+
import { config } from '@aws-blocks/hosting';
|
|
8
|
+
import type * as cdk from 'aws-cdk-lib';
|
|
9
|
+
import { App, Stack } from 'aws-cdk-lib';
|
|
10
|
+
import { Pipeline } from './pipeline/index.js';
|
|
11
|
+
import type { PipelineStageConfig } from './pipeline/index.js';
|
|
12
|
+
|
|
13
|
+
const MOCK_CONNECTION_ARN = 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id';
|
|
14
|
+
|
|
15
|
+
function minimalStageFactory(scope: cdk.Stage, stageConfig: PipelineStageConfig): void {
|
|
16
|
+
new Stack(scope, 'AppStack', { env: stageConfig.env });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// B2: the core Pipeline wrapper must resolve values from the SAME namespace the
|
|
20
|
+
// Blocks CLI writes (`/blocks/*`), not the leaf-neutral `/hosting/*` — otherwise
|
|
21
|
+
// `npm run config -- set CONNECTION_ARN` writes one name and the deploy reads
|
|
22
|
+
// another. The leaf default is `/hosting/config`; the wrapper must pin `/blocks/config`.
|
|
23
|
+
void describe('core Pipeline pins the Blocks namespace', () => {
|
|
24
|
+
afterEach(() => _setSynthSecretFetcher(null));
|
|
25
|
+
|
|
26
|
+
void it('resolves a config() connectionArn under /blocks/config by default', async () => {
|
|
27
|
+
const seen: string[] = [];
|
|
28
|
+
_setSynthSecretFetcher(async (locator: string) => {
|
|
29
|
+
seen.push(locator);
|
|
30
|
+
return MOCK_CONNECTION_ARN;
|
|
31
|
+
});
|
|
32
|
+
await Pipeline.create(new App(), 'BlocksPipeline', {
|
|
33
|
+
source: { repo: 'my-org/my-app', connectionArn: config('CONNECTION_ARN') },
|
|
34
|
+
branches: [{ branch: 'main', stages: [{ name: 'prod' }] }],
|
|
35
|
+
stageFactory: minimalStageFactory,
|
|
36
|
+
});
|
|
37
|
+
// The Blocks CLI writes /blocks/config/CONNECTION_ARN; the wrapper must read it there.
|
|
38
|
+
assert.deepStrictEqual(seen, ['/blocks/config/CONNECTION_ARN']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
void it('lets the caller override the pinned prefix', async () => {
|
|
42
|
+
const seen: string[] = [];
|
|
43
|
+
_setSynthSecretFetcher(async (locator: string) => {
|
|
44
|
+
seen.push(locator);
|
|
45
|
+
return MOCK_CONNECTION_ARN;
|
|
46
|
+
});
|
|
47
|
+
await Pipeline.create(new App(), 'OverridePipeline', {
|
|
48
|
+
source: { repo: 'my-org/my-app', connectionArn: config('CONNECTION_ARN') },
|
|
49
|
+
branches: [{ branch: 'main', stages: [{ name: 'prod' }] }],
|
|
50
|
+
stageFactory: minimalStageFactory,
|
|
51
|
+
configStore: { prefix: '/myapp/config' },
|
|
52
|
+
});
|
|
53
|
+
assert.deepStrictEqual(seen, ['/myapp/config/CONNECTION_ARN']);
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/rpc.test.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { describe, it } from 'node:test';
|
|
5
5
|
import assert from 'node:assert';
|
|
6
|
-
import { decodeRpcResponse, errorResponseFromCatch, parseRpcRequest, RpcErrorCode } from './rpc.js';
|
|
6
|
+
import { decodeRpcResponse, errorResponseFromCatch, parseRpcRequest, RpcErrorCode, MAX_RPC_BODY_BYTES } from './rpc.js';
|
|
7
7
|
import { ApiError, isBlocksError } from './errors.js';
|
|
8
8
|
|
|
9
9
|
describe('-32600 Invalid Request error shape', () => {
|
|
@@ -97,6 +97,28 @@ describe('params decoding', () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
describe('-32602 Invalid Params validation', () => {
|
|
101
|
+
for (const params of ['abc', 42, true, false, null]) {
|
|
102
|
+
it(`rejects ${JSON.stringify(params)} params`, () => {
|
|
103
|
+
const result = parseRpcRequest(JSON.stringify({
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
method: 'api.echo',
|
|
106
|
+
params,
|
|
107
|
+
id: 'request-1',
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
assert.strictEqual(result.ok, false);
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
const response = JSON.parse(result.response);
|
|
113
|
+
assert.strictEqual(response.error.code, RpcErrorCode.InvalidParams);
|
|
114
|
+
assert.strictEqual(response.error.data.name, 'InvalidParams');
|
|
115
|
+
assert.ok(response.error.message.includes('expected an array or object'));
|
|
116
|
+
assert.strictEqual(response.id, 'request-1');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
100
122
|
describe('batch requests (top-level JSON array body)', () => {
|
|
101
123
|
it('rejects an array body as Invalid Request with a null id', () => {
|
|
102
124
|
const result = parseRpcRequest(JSON.stringify([
|
|
@@ -166,3 +188,46 @@ describe('ApiError status ↔ JSON-RPC error code', () => {
|
|
|
166
188
|
);
|
|
167
189
|
});
|
|
168
190
|
});
|
|
191
|
+
|
|
192
|
+
describe('request body size limit', () => {
|
|
193
|
+
it('accepts a normal-sized body', () => {
|
|
194
|
+
const result = parseRpcRequest(JSON.stringify({ jsonrpc: '2.0', method: 'ns.method', params: [], id: 1 }));
|
|
195
|
+
assert.strictEqual(result.ok, true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('rejects a body larger than MAX_RPC_BODY_BYTES with a PayloadTooLarge error', () => {
|
|
199
|
+
// A JSON string just over the limit (a single huge param value).
|
|
200
|
+
const huge = 'x'.repeat(MAX_RPC_BODY_BYTES + 1);
|
|
201
|
+
const body = JSON.stringify({ jsonrpc: '2.0', method: 'ns.method', params: [huge], id: 1 });
|
|
202
|
+
assert.ok(Buffer.byteLength(body, 'utf8') > MAX_RPC_BODY_BYTES);
|
|
203
|
+
|
|
204
|
+
const result = parseRpcRequest(body);
|
|
205
|
+
assert.strictEqual(result.ok, false);
|
|
206
|
+
if (result.ok) return; // narrow
|
|
207
|
+
const parsed = JSON.parse(result.response);
|
|
208
|
+
// Positive HTTP status (413), not a reserved -32xxx — so decodeRpcResponse
|
|
209
|
+
// surfaces it to the client as ApiError.status === 413.
|
|
210
|
+
assert.strictEqual(parsed.error.code, 413);
|
|
211
|
+
assert.strictEqual(parsed.error.data?.name, 'PayloadTooLarge');
|
|
212
|
+
assert.match(parsed.error.message, /exceeds/);
|
|
213
|
+
assert.match(parsed.error.message, /10 MiB/); // human-readable size crosses the wire
|
|
214
|
+
|
|
215
|
+
// Client round-trip: decodeRpcResponse surfaces it as ApiError.status 413
|
|
216
|
+
// (not 500), so consumer `e.status === 413` handling works.
|
|
217
|
+
assert.throws(
|
|
218
|
+
() => decodeRpcResponse(parsed),
|
|
219
|
+
(e: unknown) => e instanceof ApiError && e.status === 413 && isBlocksError(e, 'PayloadTooLarge'),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('rejects an oversized body before attempting to parse it (invalid JSON still 413s, not a ParseError)', () => {
|
|
224
|
+
// Oversized AND not valid JSON — the size guard must win, proving the body
|
|
225
|
+
// is rejected before the (expensive) parse + before any handler/DB touch.
|
|
226
|
+
const oversizedGarbage = 'x'.repeat(MAX_RPC_BODY_BYTES + 1);
|
|
227
|
+
const result = parseRpcRequest(oversizedGarbage);
|
|
228
|
+
assert.strictEqual(result.ok, false);
|
|
229
|
+
if (result.ok) return;
|
|
230
|
+
const parsed = JSON.parse(result.response);
|
|
231
|
+
assert.strictEqual(parsed.error.data?.name, 'PayloadTooLarge');
|
|
232
|
+
});
|
|
233
|
+
});
|
package/src/rpc.ts
CHANGED
|
@@ -32,6 +32,28 @@ export type RpcParseResult =
|
|
|
32
32
|
|
|
33
33
|
const VERSION = '2.0' as const;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Maximum accepted request-body size, in bytes (10 MiB).
|
|
37
|
+
*
|
|
38
|
+
* Matches the payload limit API Gateway enforces in production (~10 MB for a
|
|
39
|
+
* REST API). In prod an oversized body is rejected by API Gateway at the edge —
|
|
40
|
+
* before the Lambda is invoked — so this guard's rejection path effectively
|
|
41
|
+
* runs on the dev/mock server, where there is no edge to stop it: an oversized
|
|
42
|
+
* body would otherwise buffer and wedge the local database (e.g. PGlite). By
|
|
43
|
+
* enforcing the *same* limit locally, the dev server rejects the same oversized
|
|
44
|
+
* body prod would 413 at the edge, instead of failing only in one environment.
|
|
45
|
+
* Enforced in `parseRpcRequest`, the single choke point both the Lambda handler
|
|
46
|
+
* and the dev server route through.
|
|
47
|
+
*
|
|
48
|
+
* Note: this value is the Lambda + API Gateway limit. It lives here (in the
|
|
49
|
+
* compute-agnostic parser) because Lambda is the only compute path today. When
|
|
50
|
+
* a non-API-Gateway compute (e.g. container/ALB) is introduced, this should
|
|
51
|
+
* become compute-aware — sourced from / overridden by the compute layer (via
|
|
52
|
+
* the Compute `setEnv` config hook) rather than a fixed constant in `core` —
|
|
53
|
+
* since an ALB has a different payload limit. Tracked with the multi-compute work.
|
|
54
|
+
*/
|
|
55
|
+
export const MAX_RPC_BODY_BYTES = 10 * 1024 * 1024;
|
|
56
|
+
|
|
35
57
|
/** Reserved JSON-RPC error codes. */
|
|
36
58
|
export const RpcErrorCode = {
|
|
37
59
|
ParseError: -32700,
|
|
@@ -89,6 +111,25 @@ export function decodeRpcResponse(body: unknown): unknown {
|
|
|
89
111
|
* anything about the JSON-RPC spec.
|
|
90
112
|
*/
|
|
91
113
|
export function parseRpcRequest(bodyText: string): RpcParseResult {
|
|
114
|
+
// Reject oversized bodies before parsing or dispatch. See MAX_RPC_BODY_BYTES
|
|
115
|
+
// for the limit and its rationale.
|
|
116
|
+
if (Buffer.byteLength(bodyText, 'utf8') > MAX_RPC_BODY_BYTES) {
|
|
117
|
+
// Emit the real HTTP status (413) as the error code, not a reserved -32xxx:
|
|
118
|
+
// `decodeRpcResponse` maps a positive code straight to `ApiError.status`
|
|
119
|
+
// (reserved codes collapse to 500), mirroring the 504 handler-timeout path,
|
|
120
|
+
// so a caller's `e.status === 413` works. `name` (which crosses the wire,
|
|
121
|
+
// not the code) lets callers match with `isBlocksError(e, 'PayloadTooLarge')`.
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
response: errorResponse(
|
|
125
|
+
413,
|
|
126
|
+
`Request body exceeds the ${MAX_RPC_BODY_BYTES} byte (${MAX_RPC_BODY_BYTES / (1024 * 1024)} MiB) limit`,
|
|
127
|
+
null,
|
|
128
|
+
{ name: 'PayloadTooLarge' },
|
|
129
|
+
),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
92
133
|
let parsed: any;
|
|
93
134
|
try {
|
|
94
135
|
parsed = JSON.parse(bodyText || '{}');
|
|
@@ -118,15 +159,31 @@ export function parseRpcRequest(bodyText: string): RpcParseResult {
|
|
|
118
159
|
};
|
|
119
160
|
}
|
|
120
161
|
|
|
162
|
+
const hasParams = Object.hasOwn(parsed, 'params');
|
|
163
|
+
if (hasParams && (parsed.params === null || typeof parsed.params !== 'object')) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
response: errorResponse(
|
|
167
|
+
RpcErrorCode.InvalidParams,
|
|
168
|
+
'Invalid params: expected an array or object',
|
|
169
|
+
id,
|
|
170
|
+
{ name: 'InvalidParams' },
|
|
171
|
+
),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let args: unknown[] = [];
|
|
176
|
+
if (hasParams) {
|
|
177
|
+
args = Array.isArray(parsed.params) ? parsed.params : Object.values(parsed.params);
|
|
178
|
+
}
|
|
179
|
+
|
|
121
180
|
return {
|
|
122
181
|
ok: true,
|
|
123
182
|
request: {
|
|
124
183
|
apiNamespace: parsed.method.substring(0, dotIndex),
|
|
125
184
|
method: parsed.method.substring(dotIndex + 1),
|
|
126
185
|
// JSON-RPC 2.0 §4.2: params may be an array (positional) or object (named).
|
|
127
|
-
args
|
|
128
|
-
? parsed.params
|
|
129
|
-
: Object.values(parsed.params ?? {}),
|
|
186
|
+
args,
|
|
130
187
|
id,
|
|
131
188
|
},
|
|
132
189
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `blocks config` — manage a Blocks app's non-sensitive **config** values (SSM
|
|
6
|
+
* Parameter Store), pinned to the Blocks `/blocks/<stackId>/config` namespace
|
|
7
|
+
* (scoped to the app's stable `stackId` from `.blocks/config.json`). Thin wrapper
|
|
8
|
+
* over the shared `@aws-blocks/hosting` CLI with the kind fixed to `config`. The
|
|
9
|
+
* `secret` counterpart is `./secret.ts`.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { listValues, removeValue, runValueCli, setValue } from '@aws-blocks/hosting/scripts';
|
|
15
|
+
import { blocksConfigPrefix } from '../secret-naming.js';
|
|
16
|
+
|
|
17
|
+
/** Set (create or overwrite) a Blocks config value. */
|
|
18
|
+
export function setConfig(key: string, value: string, opts: { stage?: string } = {}): Promise<void> {
|
|
19
|
+
return setValue('config', key, value, { prefix: blocksConfigPrefix(), ...opts });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** List Blocks config keys (names only). */
|
|
23
|
+
export function listConfig(opts: { stage?: string } = {}): Promise<string[]> {
|
|
24
|
+
return listValues('config', { prefix: blocksConfigPrefix(), ...opts });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Remove a Blocks config value. */
|
|
28
|
+
export function removeConfig(key: string, opts: { stage?: string } = {}): Promise<boolean> {
|
|
29
|
+
return removeValue('config', key, { prefix: blocksConfigPrefix(), ...opts });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** CLI dispatcher for `blocks config <set|list|remove> …`. */
|
|
33
|
+
export function runConfigCli(argv: string[]): Promise<void> {
|
|
34
|
+
return runValueCli(argv, { kind: 'config', prefix: blocksConfigPrefix(), label: 'blocks config' });
|
|
35
|
+
}
|
package/src/scripts/deploy.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
6
6
|
import { join, resolve, dirname } from 'node:path';
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
import { ensureSecrets, loadProductionEnv } from './ensure-secrets.js';
|
|
9
|
+
import { assertAwsCredentials } from './preflight-credentials.js';
|
|
9
10
|
import { applyExternalMigrations } from './external-migrations-step.js';
|
|
10
11
|
import { trackCommand } from '../telemetry/trackCommand.js';
|
|
11
12
|
import { getCdkTelemetryEnv } from './cdk-telemetry-env.js';
|
|
@@ -25,6 +26,10 @@ export async function deploy(options: DeployOptions) {
|
|
|
25
26
|
|
|
26
27
|
process.env.BLOCKS_STAGE = 'production';
|
|
27
28
|
|
|
29
|
+
// Fail fast if AWS credentials are missing/expired, before generating the
|
|
30
|
+
// client and spending time in synth only to hit an opaque CDK credential error.
|
|
31
|
+
await assertAwsCredentials('deploy');
|
|
32
|
+
|
|
28
33
|
// Provision secrets for production. projectRoot must match the root cdk
|
|
29
34
|
// synth uses (passed as --context below) so the written parameter name
|
|
30
35
|
// equals the one the app resolves at synth.
|
|
@@ -105,6 +105,39 @@ export interface DevServerOptions {
|
|
|
105
105
|
frontendCommand?: string;
|
|
106
106
|
/** Port the frontend dev server listens on. Default: 3100. */
|
|
107
107
|
frontendPort?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Watch `secret()` / `config()` calls and regenerate the type-safe key
|
|
110
|
+
* augmentation (so `getSecret`/`getConfig` autocomplete and reject typos) as you
|
|
111
|
+
* edit — all under this one `npm run dev`, no second command. Auto-detected: a
|
|
112
|
+
* no-op unless the app actually declares a `secret()`/`config()`, and never fatal
|
|
113
|
+
* (a failure only logs a warning). Set `false` to disable. Default: enabled.
|
|
114
|
+
*/
|
|
115
|
+
typegen?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Bootstrap the type-safe `getSecret`/`getConfig` key generation for the dev
|
|
120
|
+
* session. Auto-detected and non-fatal: scans the app for `secret()`/`config()`
|
|
121
|
+
* calls and, only if it finds any, generates the augmentation `.d.ts` and starts a
|
|
122
|
+
* watcher that regenerates on save — so a single `npm run dev` gives type-safe keys
|
|
123
|
+
* with no second command. Returns a `stop()` (or `undefined` when the app declares
|
|
124
|
+
* no secrets, typegen is unavailable, or it is disabled). The watcher is `unref`'d
|
|
125
|
+
* so it can never block the dev server's shutdown.
|
|
126
|
+
*/
|
|
127
|
+
async function startTypegenWatch(): Promise<(() => void) | undefined> {
|
|
128
|
+
try {
|
|
129
|
+
const { scanValueKeys, watchHostingValues } = await import('@aws-blocks/hosting/scripts');
|
|
130
|
+
const scan = await scanValueKeys();
|
|
131
|
+
if (scan.secretKeys.length === 0 && scan.configKeys.length === 0) {
|
|
132
|
+
return undefined; // app doesn't use secret()/config() → nothing to type, skip silently
|
|
133
|
+
}
|
|
134
|
+
console.log('🔑 Type-safe secret()/config() keys — watching for changes...');
|
|
135
|
+
return await watchHostingValues({ unref: true });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Never break the dev server over typegen (e.g. `typescript` not installed).
|
|
138
|
+
console.warn(`⚠️ hosting-typegen skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
108
141
|
}
|
|
109
142
|
|
|
110
143
|
/**
|
|
@@ -953,6 +986,12 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
953
986
|
await writeClientCode(resolvedPath, clientPath);
|
|
954
987
|
}
|
|
955
988
|
|
|
989
|
+
// Type-safe getSecret/getConfig: generate + watch the key augmentation so the
|
|
990
|
+
// getters autocomplete and reject typos, regenerating on save — all under this
|
|
991
|
+
// one `npm run dev`. Auto-detected (no-op unless the app uses secret()/config())
|
|
992
|
+
// and non-fatal.
|
|
993
|
+
const stopTypegen = options.typegen === false ? undefined : await startTypegenWatch();
|
|
994
|
+
|
|
956
995
|
// ── Startup reclaim ──────────────────────────────────────────────────────
|
|
957
996
|
// Free any port left bound by a crashed / SIGKILL'd predecessor before we bind
|
|
958
997
|
// the front door or spawn the `--strictPort` frontend. tsx-watch only gives the
|
|
@@ -1039,6 +1078,7 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
1039
1078
|
console.log('\nShutting down...');
|
|
1040
1079
|
|
|
1041
1080
|
if (respawnTimer) { clearTimeout(respawnTimer); respawnTimer = null; }
|
|
1081
|
+
stopTypegen?.(); // tear down the typegen watcher (unref'd, but close it cleanly)
|
|
1042
1082
|
// Detach our own listeners so repeated signals can't pile up handlers.
|
|
1043
1083
|
for (const sig of signals) process.removeListener(sig, cleanup);
|
|
1044
1084
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
5
4
|
import assert from 'node:assert';
|
|
5
|
+
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
6
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
6
9
|
import { generateClientCode } from './generate-client.js';
|
|
7
|
-
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { tmpdir } from 'os';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Regression test: generateClientCode collects middleware from whatever
|
|
@@ -35,9 +35,12 @@ describe('generateClientCode — middleware collection', () => {
|
|
|
35
35
|
it('includes middleware specifiers registered by the backend', async () => {
|
|
36
36
|
// Create a minimal backend that registers a middleware specifier
|
|
37
37
|
const backendPath = join(tmpDir, 'backend.mjs');
|
|
38
|
-
writeFileSync(
|
|
38
|
+
writeFileSync(
|
|
39
|
+
backendPath,
|
|
40
|
+
`
|
|
39
41
|
globalThis.__BLOCKS_CLIENT_MIDDLEWARE__?.push('@aws-blocks/bb-realtime/aws-middleware');
|
|
40
|
-
|
|
42
|
+
`,
|
|
43
|
+
);
|
|
41
44
|
|
|
42
45
|
const code = await generateClientCode(backendPath);
|
|
43
46
|
assert.ok(
|
|
@@ -52,9 +55,12 @@ describe('generateClientCode — middleware collection', () => {
|
|
|
52
55
|
|
|
53
56
|
it('includes mock-middleware when backend registers it (local dev mode)', async () => {
|
|
54
57
|
const backendPath = join(tmpDir, 'backend.mjs');
|
|
55
|
-
writeFileSync(
|
|
58
|
+
writeFileSync(
|
|
59
|
+
backendPath,
|
|
60
|
+
`
|
|
56
61
|
globalThis.__BLOCKS_CLIENT_MIDDLEWARE__?.push('@aws-blocks/bb-realtime/mock-middleware');
|
|
57
|
-
|
|
62
|
+
`,
|
|
63
|
+
);
|
|
58
64
|
|
|
59
65
|
const code = await generateClientCode(backendPath);
|
|
60
66
|
assert.ok(
|
|
@@ -66,4 +72,33 @@ describe('generateClientCode — middleware collection', () => {
|
|
|
66
72
|
'Generated client must NOT include aws-middleware in local dev mode',
|
|
67
73
|
);
|
|
68
74
|
});
|
|
75
|
+
|
|
76
|
+
it('skips secret()/config() markers — they are values, not API namespaces', async () => {
|
|
77
|
+
// A secret()/config() marker is branded with Symbol.for('@aws-blocks/hosting.ManagedValue');
|
|
78
|
+
// build one inline so the fixture needs no imports. A real export still gets a proxy.
|
|
79
|
+
const backendPath = join(tmpDir, 'backend.mjs');
|
|
80
|
+
writeFileSync(
|
|
81
|
+
backendPath,
|
|
82
|
+
`
|
|
83
|
+
const BRAND = Symbol.for('@aws-blocks/hosting.ManagedValue');
|
|
84
|
+
export const stripeKey = { [BRAND]: true, key: 'STRIPE_KEY', kind: 'secret' };
|
|
85
|
+
export const featureFlags = { [BRAND]: true, key: 'FEATURE_FLAGS', kind: 'config' };
|
|
86
|
+
export function realApi() {}
|
|
87
|
+
`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const code = await generateClientCode(backendPath);
|
|
91
|
+
assert.ok(
|
|
92
|
+
!code.includes("__BLOCKS_ApiNamespaceClient__('stripeKey')"),
|
|
93
|
+
'a secret() marker must not become an HTTP namespace',
|
|
94
|
+
);
|
|
95
|
+
assert.ok(
|
|
96
|
+
!code.includes("__BLOCKS_ApiNamespaceClient__('featureFlags')"),
|
|
97
|
+
'a config() marker must not become an HTTP namespace',
|
|
98
|
+
);
|
|
99
|
+
assert.ok(
|
|
100
|
+
code.includes("export const realApi = __BLOCKS_ApiNamespaceClient__('realApi')"),
|
|
101
|
+
'a real (non-marker) export still becomes a namespace',
|
|
102
|
+
);
|
|
103
|
+
});
|
|
69
104
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { isManagedValue } from '@aws-blocks/hosting';
|
|
5
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
7
8
|
import { API_NAMESPACE_MARKER } from '../api.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -15,72 +16,76 @@ import { API_NAMESPACE_MARKER } from '../api.js';
|
|
|
15
16
|
* import statements + ApiNamespace proxy exports.
|
|
16
17
|
*/
|
|
17
18
|
export async function generateClientCode(foundationPath: string): Promise<string> {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
19
|
+
// Set up global collector if not already present (dev server may have already done this)
|
|
20
|
+
const collectorAlreadyActive = Array.isArray((globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__);
|
|
21
|
+
if (!collectorAlreadyActive) {
|
|
22
|
+
(globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__ = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const backend = await import(pathToFileURL(foundationPath).href);
|
|
26
|
+
|
|
27
|
+
// Read collected middleware and clean up
|
|
28
|
+
const middleware: string[] = (globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__;
|
|
29
|
+
delete (globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__;
|
|
30
|
+
|
|
31
|
+
const imports = new Set<string>();
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
const namespaceNames: string[] = [];
|
|
34
|
+
|
|
35
|
+
imports.add(`import { ApiNamespaceClient as __BLOCKS_ApiNamespaceClient__ } from '@aws-blocks/blocks/client';`);
|
|
36
|
+
|
|
37
|
+
for (const specifier of middleware) {
|
|
38
|
+
imports.add(`import '${specifier}';`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Emit ApiNamespace proxy exports
|
|
42
|
+
for (const [name, value] of Object.entries(backend)) {
|
|
43
|
+
if (name.startsWith('_')) continue;
|
|
44
|
+
|
|
45
|
+
const isApiNamespace = typeof (value as any)?.[API_NAMESPACE_MARKER] === 'string';
|
|
46
|
+
if (isApiNamespace) {
|
|
47
|
+
lines.push(`export const ${name} = __BLOCKS_ApiNamespaceClient__('${name}');`);
|
|
48
|
+
namespaceNames.push(name);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: any exported function/object that isn't a Scope gets a proxy.
|
|
53
|
+
const isScope = typeof (value as any)?.id === 'string' && typeof (value as any)?.fullId === 'string';
|
|
54
|
+
if (isScope) continue;
|
|
55
|
+
|
|
56
|
+
// A secret()/config() marker is a deferred value, not an API — skip it, or
|
|
57
|
+
// `export const k = secret('K')` in index.ts would become a bogus HTTP namespace.
|
|
58
|
+
if (isManagedValue(value)) continue;
|
|
59
|
+
|
|
60
|
+
// Fallback: any exported function/object gets an ApiNamespace proxy.
|
|
61
|
+
if ((typeof value === 'function' || typeof value === 'object') && value !== null) {
|
|
62
|
+
lines.push(`export const ${name} = __BLOCKS_ApiNamespaceClient__('${name}');`);
|
|
63
|
+
namespaceNames.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Emit generateClient — creates new ApiNamespaceClient instances with the given url
|
|
68
|
+
const nsEntries = namespaceNames.map((n) => ` ${n}: __BLOCKS_ApiNamespaceClient__('${n}', config),`).join('\n');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(`export const generateClient = (config) => ({\n${nsEntries}\n});`);
|
|
71
|
+
|
|
72
|
+
const header = [
|
|
73
|
+
'// ============================================================',
|
|
74
|
+
'// AUTO-GENERATED FILE — DO NOT EDIT',
|
|
75
|
+
'//',
|
|
76
|
+
'// This file is generated by the Blocks dev server / build process.',
|
|
77
|
+
'// Any manual changes will be overwritten on the next build.',
|
|
78
|
+
'// ============================================================',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
return [...header, '', ...imports, '', ...lines].join('\n');
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/**
|
|
80
85
|
* Generate client code and write it to the aws-blocks subpackage.
|
|
81
86
|
*/
|
|
82
87
|
export async function writeClientCode(foundationPath: string, outputPath: string): Promise<void> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
const code = await generateClientCode(foundationPath);
|
|
89
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
90
|
+
writeFileSync(outputPath, code);
|
|
86
91
|
}
|