@liflig/cdk-snapshot 1.0.2 → 1.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/README.md +36 -16
- package/lib/assets.d.ts +11 -0
- package/lib/assets.js +39 -0
- package/lib/cjs/assets.d.ts +11 -0
- package/lib/cjs/assets.js +42 -0
- package/lib/cjs/index.d.ts +14 -0
- package/lib/cjs/index.js +25 -0
- package/lib/cjs/jest.d.ts +10 -0
- package/lib/cjs/jest.js +8 -0
- package/lib/cjs/matcher.d.ts +29 -0
- package/lib/cjs/matcher.js +43 -0
- package/lib/cjs/normalize.d.ts +16 -0
- package/lib/cjs/normalize.js +155 -0
- package/lib/cjs/options.d.ts +61 -0
- package/lib/cjs/options.js +2 -0
- package/lib/cjs/package.json +1 -0
- package/lib/cjs/placeholder.d.ts +9 -0
- package/lib/cjs/placeholder.js +18 -0
- package/lib/index.js +6 -1
- package/lib/normalize.d.ts +4 -1
- package/lib/normalize.js +16 -2
- package/lib/options.d.ts +12 -0
- package/package.json +20 -8
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
Snapshot testing for AWS CDK stacks. A stack is synthesized to CloudFormation and
|
|
9
|
-
normalized before it is snapshotted. The CDK bootstrap version is
|
|
9
|
+
normalized before it is snapshotted. The CDK bootstrap version is dropped by default.
|
|
10
10
|
Asset hashes, Lambda version suffixes and CDK Pipelines asset IDs change whenever
|
|
11
11
|
an asset's content does; the [options](#options) mask them, so a snapshot fails only
|
|
12
12
|
when the infrastructure itself changed.
|
|
@@ -25,7 +25,8 @@ npm install --save-dev @liflig/cdk-snapshot
|
|
|
25
25
|
pnpm add -D @liflig/cdk-snapshot
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
The package is ESM
|
|
28
|
+
The package is ESM, with a CommonJS build of the root and Jest entry points for
|
|
29
|
+
CommonJS Jest projects.
|
|
29
30
|
|
|
30
31
|
## Usage
|
|
31
32
|
|
|
@@ -96,16 +97,17 @@ test("my stack", () => {
|
|
|
96
97
|
});
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
Instead of importing it in each test file, the entry point can be listed once in
|
|
101
|
+
`setupFilesAfterEnv`.
|
|
102
|
+
|
|
103
|
+
A CommonJS test file, including one ts-jest or babel-jest compiles to CommonJS, loads the
|
|
104
|
+
CommonJS build and needs no configuration. An ESM test file loads the ESM build, and
|
|
105
|
+
needs Jest's ESM support enabled as any ESM test file does:
|
|
100
106
|
|
|
101
107
|
```sh
|
|
102
108
|
NODE_OPTIONS=--experimental-vm-modules jest
|
|
103
109
|
```
|
|
104
110
|
|
|
105
|
-
A CommonJS test file additionally needs Node 24.9 or later, where Jest can `require()` an
|
|
106
|
-
ESM package. Below that, the test file has to be ESM or go through a transform that
|
|
107
|
-
compiles the package to CommonJS.
|
|
108
|
-
|
|
109
111
|
The Jest entry point uses the global `expect`, so it throws on import if Jest is
|
|
110
112
|
configured with `injectGlobals: false`.
|
|
111
113
|
|
|
@@ -135,6 +137,7 @@ correct the count.
|
|
|
135
137
|
| Option | Type | Default | Effect |
|
|
136
138
|
| --- | --- | --- | --- |
|
|
137
139
|
| `ignoreAssets` | `boolean` | `false` | Replaces every `Code` property, every container definition's `Image` and the whole `Parameters` block with `Any<Object>` |
|
|
140
|
+
| `ignoreAssetHashes` | `boolean` | `false` | Replaces the hash of every asset in the app with `<ASSET_HASH>`, wherever it appears |
|
|
138
141
|
| `ignoreBootstrapVersion` | `boolean` | `true` | Drops the `BootstrapVersion` parameter and its check rule |
|
|
139
142
|
| `ignoreCurrentVersion` | `boolean` | `false` | Masks the content hash on Lambda `CurrentVersion` logical IDs and every reference to them |
|
|
140
143
|
| `ignoreMetadata` | `boolean` | `false` | Drops template and resource `Metadata` |
|
|
@@ -156,11 +159,29 @@ only if it matches both.
|
|
|
156
159
|
`Code.fromBucket` key or to a registry image tag such as `nginx:1.27` does not show.
|
|
157
160
|
- Assets outside Lambda `Code` and container images keep their hash: Lambda layers,
|
|
158
161
|
`BucketDeployment` sources, Step Functions and API Gateway definitions read from files,
|
|
159
|
-
and nested stack templates.
|
|
162
|
+
and nested stack templates. `ignoreAssetHashes` covers them.
|
|
160
163
|
- A function's `currentVersion` logical ID is a hash over its configuration, code
|
|
161
164
|
included, so a stack that uses it also needs `ignoreCurrentVersion` to stay stable.
|
|
162
165
|
- It does nothing to a template with no `Resources`.
|
|
163
166
|
|
|
167
|
+
`ignoreAssetHashes` is the precise alternative. It reads the hash of every file and
|
|
168
|
+
container image asset from the asset manifests the app synthesizes, CDK Pipelines stages
|
|
169
|
+
included, and replaces exactly those hashes wherever a string in the template holds one:
|
|
170
|
+
|
|
171
|
+
```diff
|
|
172
|
+
"Code": {
|
|
173
|
+
"S3Bucket": "cdk-hnb659fds-assets-112233445566-eu-west-1",
|
|
174
|
+
- "S3Key": "9b8fce7ae7f25ef82fdbaf6b72523b99d0875c0c9c826642819fb51f11d9b125.zip",
|
|
175
|
+
+ "S3Key": "<ASSET_HASH>.zip",
|
|
176
|
+
},
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Everything else stays visible: the stack's parameters, inline code, registry image tags
|
|
180
|
+
and any hash that belongs to no asset. A function using `currentVersion` still needs
|
|
181
|
+
`ignoreCurrentVersion`, and a CDK Pipeline still needs `ignorePipelineAssets` for its
|
|
182
|
+
destination suffixes, which are not asset hashes. The hashes come from the asset
|
|
183
|
+
manifests that CDK's default synthesizer writes.
|
|
184
|
+
|
|
164
185
|
`ignoreTags` drops the `Tags` property of each resource. Tags nested deeper stay, such as
|
|
165
186
|
those `Tags.of()` propagates into a launch template's `TagSpecifications`.
|
|
166
187
|
|
|
@@ -174,8 +195,8 @@ its own `expect`.
|
|
|
174
195
|
|
|
175
196
|
## How it works
|
|
176
197
|
|
|
177
|
-
Everything is built around one
|
|
178
|
-
|
|
198
|
+
Everything is built around one function, `cdkTemplate`, which synthesizes a stack and
|
|
199
|
+
hands the template to a pure normalizer. Each runner gets a thin adapter that wraps that function in
|
|
179
200
|
whatever the runner's own snapshot assertion looks like, so snapshots keep the naming and
|
|
180
201
|
format that runner already produces.
|
|
181
202
|
|
|
@@ -205,15 +226,17 @@ make ci # what the CI workflow runs: refuses a stale lockfile, fails on an
|
|
|
205
226
|
```
|
|
206
227
|
|
|
207
228
|
`make snapshots` regenerates the unit snapshots plus the shared fixture under all four
|
|
208
|
-
runners, which `test/compat.test.ts` then compares
|
|
229
|
+
runners, Jest once as ESM and once as CommonJS, which `test/compat.test.ts` then compares
|
|
230
|
+
against each other.
|
|
209
231
|
|
|
210
232
|
`make compat-check` runs only the four runners and fails if their snapshots changed. CI
|
|
211
233
|
runs it on the oldest Node that `engines` in `package.json` allows.
|
|
212
234
|
|
|
213
235
|
## Migrating from jest-cdk-snapshot
|
|
214
236
|
|
|
215
|
-
Change the import. Call sites and `.snap` files stay
|
|
216
|
-
defaults and the serialization all match
|
|
237
|
+
Change the import, or the `setupFilesAfterEnv` entry. Call sites and `.snap` files stay
|
|
238
|
+
as they are, since the options, their defaults and the serialization all match, and the
|
|
239
|
+
Jest configuration stays as it is, CommonJS or ESM.
|
|
217
240
|
|
|
218
241
|
```diff
|
|
219
242
|
-import "jest-cdk-snapshot"
|
|
@@ -224,9 +247,6 @@ Verified against two public CDK libraries, liflig-cdk and cdk-cloudfront-auth: e
|
|
|
224
247
|
existing snapshot passes under `jest --ci`, and a forced `--updateSnapshot` rewrites
|
|
225
248
|
nothing.
|
|
226
249
|
|
|
227
|
-
Jest now has to run with ESM support enabled, since this package is ESM — see
|
|
228
|
-
[Jest](#jest).
|
|
229
|
-
|
|
230
250
|
One option is gone. `yaml` is not supported, so a project snapshotting YAML has to
|
|
231
251
|
regenerate as JSON.
|
|
232
252
|
|
package/lib/assets.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Stack } from "aws-cdk-lib";
|
|
2
|
+
/**
|
|
3
|
+
* The IDs of every file and container image asset in the cloud assembly
|
|
4
|
+
* `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
|
|
5
|
+
* Under CDK's default synthesizer an asset's ID is the hash it is published
|
|
6
|
+
* under.
|
|
7
|
+
*
|
|
8
|
+
* Synthesis is cached per stage, so this reads the assembly the stack's
|
|
9
|
+
* template came from.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assetHashes(stack: Stack): Set<string>;
|
package/lib/assets.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { Stage } from "aws-cdk-lib";
|
|
3
|
+
/**
|
|
4
|
+
* The IDs of every file and container image asset in the cloud assembly
|
|
5
|
+
* `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
|
|
6
|
+
* Under CDK's default synthesizer an asset's ID is the hash it is published
|
|
7
|
+
* under.
|
|
8
|
+
*
|
|
9
|
+
* Synthesis is cached per stage, so this reads the assembly the stack's
|
|
10
|
+
* template came from.
|
|
11
|
+
*/
|
|
12
|
+
export function assetHashes(stack) {
|
|
13
|
+
const hashes = new Set();
|
|
14
|
+
const stage = Stage.of(stack);
|
|
15
|
+
if (stage)
|
|
16
|
+
collect(stage.synth(), hashes);
|
|
17
|
+
return hashes;
|
|
18
|
+
}
|
|
19
|
+
// Artifacts are told apart by manifest type rather than instanceof, which
|
|
20
|
+
// fails when the app was built with a different copy of aws-cdk-lib.
|
|
21
|
+
function collect(assembly, hashes) {
|
|
22
|
+
for (const artifact of assembly.artifacts) {
|
|
23
|
+
switch (artifact.manifest.type) {
|
|
24
|
+
case "cdk:asset-manifest": {
|
|
25
|
+
const { file } = artifact;
|
|
26
|
+
const { files = {}, dockerImages = {} } = JSON.parse(readFileSync(file, "utf8"));
|
|
27
|
+
for (const id of Object.keys({ ...files, ...dockerImages })) {
|
|
28
|
+
hashes.add(id);
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case "cdk:cloud-assembly": {
|
|
33
|
+
const { nestedAssembly } = artifact;
|
|
34
|
+
collect(nestedAssembly, hashes);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Stack } from "aws-cdk-lib";
|
|
2
|
+
/**
|
|
3
|
+
* The IDs of every file and container image asset in the cloud assembly
|
|
4
|
+
* `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
|
|
5
|
+
* Under CDK's default synthesizer an asset's ID is the hash it is published
|
|
6
|
+
* under.
|
|
7
|
+
*
|
|
8
|
+
* Synthesis is cached per stage, so this reads the assembly the stack's
|
|
9
|
+
* template came from.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assetHashes(stack: Stack): Set<string>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assetHashes = assetHashes;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const aws_cdk_lib_1 = require("aws-cdk-lib");
|
|
6
|
+
/**
|
|
7
|
+
* The IDs of every file and container image asset in the cloud assembly
|
|
8
|
+
* `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
|
|
9
|
+
* Under CDK's default synthesizer an asset's ID is the hash it is published
|
|
10
|
+
* under.
|
|
11
|
+
*
|
|
12
|
+
* Synthesis is cached per stage, so this reads the assembly the stack's
|
|
13
|
+
* template came from.
|
|
14
|
+
*/
|
|
15
|
+
function assetHashes(stack) {
|
|
16
|
+
const hashes = new Set();
|
|
17
|
+
const stage = aws_cdk_lib_1.Stage.of(stack);
|
|
18
|
+
if (stage)
|
|
19
|
+
collect(stage.synth(), hashes);
|
|
20
|
+
return hashes;
|
|
21
|
+
}
|
|
22
|
+
// Artifacts are told apart by manifest type rather than instanceof, which
|
|
23
|
+
// fails when the app was built with a different copy of aws-cdk-lib.
|
|
24
|
+
function collect(assembly, hashes) {
|
|
25
|
+
for (const artifact of assembly.artifacts) {
|
|
26
|
+
switch (artifact.manifest.type) {
|
|
27
|
+
case "cdk:asset-manifest": {
|
|
28
|
+
const { file } = artifact;
|
|
29
|
+
const { files = {}, dockerImages = {} } = JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
|
|
30
|
+
for (const id of Object.keys({ ...files, ...dockerImages })) {
|
|
31
|
+
hashes.add(id);
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "cdk:cloud-assembly": {
|
|
36
|
+
const { nestedAssembly } = artifact;
|
|
37
|
+
collect(nestedAssembly, hashes);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Stack } from "aws-cdk-lib";
|
|
2
|
+
import type { CdkTemplateOptions } from "./options.js";
|
|
3
|
+
export type { CdkSnapshotOptions, CdkTemplateOptions } from "./options.js";
|
|
4
|
+
export { anyObject } from "./placeholder.js";
|
|
5
|
+
/**
|
|
6
|
+
* Synthesizes `stack` to a CloudFormation template with deployment noise
|
|
7
|
+
* removed, ready to hand to a snapshot assertion.
|
|
8
|
+
*
|
|
9
|
+
* The stack is left untouched, so it can be synthesized again with different
|
|
10
|
+
* options.
|
|
11
|
+
*
|
|
12
|
+
* Bun users should import this from `@liflig/cdk-snapshot/bun` instead.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cdkTemplate(stack: Stack, options?: CdkTemplateOptions): Record<string, unknown>;
|
package/lib/cjs/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.anyObject = void 0;
|
|
4
|
+
exports.cdkTemplate = cdkTemplate;
|
|
5
|
+
const assertions_1 = require("aws-cdk-lib/assertions");
|
|
6
|
+
const assets_js_1 = require("./assets.js");
|
|
7
|
+
const normalize_js_1 = require("./normalize.js");
|
|
8
|
+
var placeholder_js_1 = require("./placeholder.js");
|
|
9
|
+
Object.defineProperty(exports, "anyObject", { enumerable: true, get: function () { return placeholder_js_1.anyObject; } });
|
|
10
|
+
/**
|
|
11
|
+
* Synthesizes `stack` to a CloudFormation template with deployment noise
|
|
12
|
+
* removed, ready to hand to a snapshot assertion.
|
|
13
|
+
*
|
|
14
|
+
* The stack is left untouched, so it can be synthesized again with different
|
|
15
|
+
* options.
|
|
16
|
+
*
|
|
17
|
+
* Bun users should import this from `@liflig/cdk-snapshot/bun` instead.
|
|
18
|
+
*/
|
|
19
|
+
function cdkTemplate(stack, options = {}) {
|
|
20
|
+
const template = assertions_1.Template.fromStack(stack).toJSON();
|
|
21
|
+
const hashes = options.ignoreAssetHashes
|
|
22
|
+
? (0, assets_js_1.assetHashes)(stack)
|
|
23
|
+
: new Set();
|
|
24
|
+
return (0, normalize_js_1.normalize)(template, options, hashes);
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CdkSnapshotOptions } from "./options.js";
|
|
2
|
+
export { cdkTemplate } from "./index.js";
|
|
3
|
+
export type { CdkSnapshotOptions, CdkTemplateOptions } from "./options.js";
|
|
4
|
+
declare global {
|
|
5
|
+
namespace jest {
|
|
6
|
+
interface Matchers<R, T = {}> {
|
|
7
|
+
toMatchCdkSnapshot(options?: CdkSnapshotOptions): R;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
package/lib/cjs/jest.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cdkTemplate = void 0;
|
|
4
|
+
const index_js_1 = require("./index.js");
|
|
5
|
+
const matcher_js_1 = require("./matcher.js");
|
|
6
|
+
var index_js_2 = require("./index.js");
|
|
7
|
+
Object.defineProperty(exports, "cdkTemplate", { enumerable: true, get: function () { return index_js_2.cdkTemplate; } });
|
|
8
|
+
(0, matcher_js_1.registerCdkMatcher)((0, matcher_js_1.requireExpect)("Jest", globalThis.expect), index_js_1.cdkTemplate);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Stack } from "aws-cdk-lib";
|
|
2
|
+
import type { CdkTemplateOptions } from "./options.js";
|
|
3
|
+
/** The part of a runner's `expect` this matcher relies on. */
|
|
4
|
+
export interface ExpectLike {
|
|
5
|
+
(actual: unknown): {
|
|
6
|
+
toMatchSnapshot(propertyMatchers?: Record<string, unknown>): void;
|
|
7
|
+
};
|
|
8
|
+
extend(matchers: Record<string, unknown>): void;
|
|
9
|
+
/** Jest and Vitest track assertion calls here; Bun has neither method. */
|
|
10
|
+
getState?(): {
|
|
11
|
+
assertionCalls: number;
|
|
12
|
+
};
|
|
13
|
+
setState?(state: {
|
|
14
|
+
assertionCalls: number;
|
|
15
|
+
}): void;
|
|
16
|
+
}
|
|
17
|
+
export type TemplateFn = (stack: Stack, options?: CdkTemplateOptions) => Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Registers `toMatchCdkSnapshot` on the runner's `expect`.
|
|
20
|
+
*
|
|
21
|
+
* The matcher delegates to the runner's own snapshot assertion, so snapshots
|
|
22
|
+
* keep the naming and format that runner already produces.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerCdkMatcher(expect: ExpectLike, cdkTemplate: TemplateFn): void;
|
|
25
|
+
/**
|
|
26
|
+
* Narrows the `expect` a runner injected to the shape the matcher needs, or
|
|
27
|
+
* explains what to do when the runner injected nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function requireExpect(runner: string, injected: unknown): ExpectLike;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCdkMatcher = registerCdkMatcher;
|
|
4
|
+
exports.requireExpect = requireExpect;
|
|
5
|
+
/**
|
|
6
|
+
* Registers `toMatchCdkSnapshot` on the runner's `expect`.
|
|
7
|
+
*
|
|
8
|
+
* The matcher delegates to the runner's own snapshot assertion, so snapshots
|
|
9
|
+
* keep the naming and format that runner already produces.
|
|
10
|
+
*/
|
|
11
|
+
function registerCdkMatcher(expect, cdkTemplate) {
|
|
12
|
+
expect.extend({
|
|
13
|
+
toMatchCdkSnapshot(received, options = {}) {
|
|
14
|
+
if (this?.isNot) {
|
|
15
|
+
throw new Error("toMatchCdkSnapshot cannot be negated with `.not`.");
|
|
16
|
+
}
|
|
17
|
+
const { propertyMatchers, ...templateOptions } = options;
|
|
18
|
+
const assertionCalls = expect.getState?.().assertionCalls;
|
|
19
|
+
const assertion = expect(cdkTemplate(received, templateOptions));
|
|
20
|
+
if (propertyMatchers) {
|
|
21
|
+
assertion.toMatchSnapshot(propertyMatchers);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
assertion.toMatchSnapshot();
|
|
25
|
+
}
|
|
26
|
+
// The nested snapshot assertion must not count towards `expect.assertions()`.
|
|
27
|
+
if (assertionCalls !== undefined)
|
|
28
|
+
expect.setState?.({ assertionCalls });
|
|
29
|
+
return { pass: true, message: () => "" };
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Narrows the `expect` a runner injected to the shape the matcher needs, or
|
|
35
|
+
* explains what to do when the runner injected nothing.
|
|
36
|
+
*/
|
|
37
|
+
function requireExpect(runner, injected) {
|
|
38
|
+
const candidate = injected;
|
|
39
|
+
if (typeof candidate?.extend !== "function") {
|
|
40
|
+
throw new Error(`@liflig/cdk-snapshot: ${runner} did not inject a global \`expect\`. Enable global injection, or use cdkTemplate() directly.`);
|
|
41
|
+
}
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CdkTemplateOptions } from "./options.js";
|
|
2
|
+
/** A synthesized CloudFormation template. */
|
|
3
|
+
export type Template = Record<string, any>;
|
|
4
|
+
/**
|
|
5
|
+
* Returns a copy of `template` with the configured normalizations applied. The
|
|
6
|
+
* argument is left untouched: `Template.fromStack` hands out the assembly's
|
|
7
|
+
* cached template object, so mutating it would corrupt every later assertion
|
|
8
|
+
* on the same stack.
|
|
9
|
+
*
|
|
10
|
+
* Step order is significant: earlier steps can remove structures that later
|
|
11
|
+
* ones inspect.
|
|
12
|
+
*
|
|
13
|
+
* `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
|
|
14
|
+
* masks, as read from the stack's cloud assembly.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalize(template: Template, options?: CdkTemplateOptions, assetHashes?: ReadonlySet<string>): Template;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalize = normalize;
|
|
4
|
+
const placeholder_js_1 = require("./placeholder.js");
|
|
5
|
+
const currentVersionRegex = /^(.+CurrentVersion[0-9A-F]{8})[0-9a-f]{32}$/;
|
|
6
|
+
const pipelineCdkAssetsRegex = /cdk-assets\s+--path\s+\\"([^\\/]+)\/.+?assets\.json\\"\s+--verbose\s+publish\s+\\"(.+?)\\"/g;
|
|
7
|
+
const assetDestinationRegex = /:(.*?)(?:-[0-9a-f]{8})?$/;
|
|
8
|
+
const assetHashRegex = /(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])/g;
|
|
9
|
+
const maskedVersionSuffix = "x".repeat(32);
|
|
10
|
+
const maskedAssetHash = "<ASSET_HASH>";
|
|
11
|
+
/**
|
|
12
|
+
* Returns a copy of `template` with the configured normalizations applied. The
|
|
13
|
+
* argument is left untouched: `Template.fromStack` hands out the assembly's
|
|
14
|
+
* cached template object, so mutating it would corrupt every later assertion
|
|
15
|
+
* on the same stack.
|
|
16
|
+
*
|
|
17
|
+
* Step order is significant: earlier steps can remove structures that later
|
|
18
|
+
* ones inspect.
|
|
19
|
+
*
|
|
20
|
+
* `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
|
|
21
|
+
* masks, as read from the stack's cloud assembly.
|
|
22
|
+
*/
|
|
23
|
+
function normalize(template, options = {}, assetHashes = new Set()) {
|
|
24
|
+
const { ignoreAssets = false, ignoreAssetHashes = false, ignoreBootstrapVersion = true, ignoreCurrentVersion = false, ignoreMetadata = false, ignoreTags = false, ignorePipelineAssets = false, subsetResourceTypes, subsetResourceKeys, assetPlaceholder = placeholder_js_1.anyObject, } = options;
|
|
25
|
+
const result = structuredClone(template);
|
|
26
|
+
if (ignoreBootstrapVersion)
|
|
27
|
+
stripBootstrapVersion(result);
|
|
28
|
+
if (ignoreAssets)
|
|
29
|
+
stripAssets(result, assetPlaceholder);
|
|
30
|
+
if (ignoreAssetHashes)
|
|
31
|
+
maskAssetHashes(result, assetHashes);
|
|
32
|
+
if (ignoreCurrentVersion)
|
|
33
|
+
maskCurrentVersions(result);
|
|
34
|
+
if (ignorePipelineAssets)
|
|
35
|
+
maskPipelineAssets(result);
|
|
36
|
+
if (subsetResourceTypes) {
|
|
37
|
+
keepResources(result, (_key, resource) => subsetResourceTypes.includes(resource?.Type));
|
|
38
|
+
}
|
|
39
|
+
if (subsetResourceKeys) {
|
|
40
|
+
keepResources(result, (key) => subsetResourceKeys.includes(key));
|
|
41
|
+
}
|
|
42
|
+
if (ignoreMetadata)
|
|
43
|
+
stripMetadata(result);
|
|
44
|
+
if (ignoreTags)
|
|
45
|
+
stripTags(result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function stripBootstrapVersion(template) {
|
|
49
|
+
const { Parameters, Rules } = template;
|
|
50
|
+
if (Parameters) {
|
|
51
|
+
delete Parameters.BootstrapVersion;
|
|
52
|
+
if (Object.keys(Parameters).length === 0)
|
|
53
|
+
delete template.Parameters;
|
|
54
|
+
}
|
|
55
|
+
if (Rules) {
|
|
56
|
+
delete Rules.CheckBootstrapVersion;
|
|
57
|
+
if (Object.keys(Rules).length === 0)
|
|
58
|
+
delete template.Rules;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function stripAssets(template, placeholder) {
|
|
62
|
+
if (!template.Resources)
|
|
63
|
+
return;
|
|
64
|
+
if (template.Parameters) {
|
|
65
|
+
template.Parameters = placeholder;
|
|
66
|
+
}
|
|
67
|
+
for (const resource of Object.values(template.Resources)) {
|
|
68
|
+
const properties = resource?.Properties;
|
|
69
|
+
if (!properties)
|
|
70
|
+
continue;
|
|
71
|
+
if (properties.Code) {
|
|
72
|
+
properties.Code = placeholder;
|
|
73
|
+
}
|
|
74
|
+
for (const definition of properties.ContainerDefinitions ?? []) {
|
|
75
|
+
definition.Image = placeholder;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Only a standalone 64-hex run is a candidate, so a longer hex string that
|
|
81
|
+
* happens to contain an asset hash is left intact.
|
|
82
|
+
*/
|
|
83
|
+
function maskAssetHashes(tree, hashes) {
|
|
84
|
+
transformStrings(tree, (value) => value.replace(assetHashRegex, (hash) => hashes.has(hash) ? maskedAssetHash : hash));
|
|
85
|
+
}
|
|
86
|
+
function maskCurrentVersions(tree) {
|
|
87
|
+
transformStrings(tree, (value) => {
|
|
88
|
+
const match = currentVersionRegex.exec(value);
|
|
89
|
+
return match ? `${match[1]}${maskedVersionSuffix}` : value;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* `cdk-assets ... publish "<hash>:<account>-<region>-<suffix>"` — the hash and
|
|
94
|
+
* the 8-hex suffix follow the asset's content, the account and region do not.
|
|
95
|
+
* CDK versions before the suffix emit `<hash>:<account>-<region>`.
|
|
96
|
+
*/
|
|
97
|
+
function maskPipelineAssets(tree) {
|
|
98
|
+
transformStrings(tree, (value) => value.replace(pipelineCdkAssetsRegex, (_match, assemblyDir, asset) => {
|
|
99
|
+
const destination = assetDestinationRegex.exec(asset)?.[1] || "<ASSET_ID>";
|
|
100
|
+
return `cdk-assets --path "<${assemblyDir}>" --verbose publish "${destination}"`;
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
/** Rewrites every string in `tree`, object keys included, in place. */
|
|
104
|
+
function transformStrings(tree, transform) {
|
|
105
|
+
if (tree == null || typeof tree !== "object")
|
|
106
|
+
return;
|
|
107
|
+
if (Array.isArray(tree)) {
|
|
108
|
+
for (let i = 0; i < tree.length; i++) {
|
|
109
|
+
const value = tree[i];
|
|
110
|
+
if (typeof value === "string") {
|
|
111
|
+
tree[i] = transform(value);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
transformStrings(value, transform);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const record = tree;
|
|
120
|
+
for (const [key, value] of Object.entries(record)) {
|
|
121
|
+
const newKey = transform(key);
|
|
122
|
+
if (newKey !== key) {
|
|
123
|
+
record[newKey] = value;
|
|
124
|
+
delete record[key];
|
|
125
|
+
}
|
|
126
|
+
if (typeof value === "string") {
|
|
127
|
+
record[newKey] = transform(value);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
transformStrings(value, transform);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function keepResources(template, keep) {
|
|
135
|
+
if (!template.Resources)
|
|
136
|
+
return;
|
|
137
|
+
for (const [key, resource] of Object.entries(template.Resources)) {
|
|
138
|
+
if (!keep(key, resource)) {
|
|
139
|
+
delete template.Resources[key];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function stripMetadata(template) {
|
|
144
|
+
delete template.Metadata;
|
|
145
|
+
for (const resource of Object.values(template.Resources ?? {})) {
|
|
146
|
+
delete resource?.Metadata;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function stripTags(template) {
|
|
150
|
+
for (const resource of Object.values(template.Resources ?? {})) {
|
|
151
|
+
const properties = resource?.Properties;
|
|
152
|
+
if (properties?.Tags)
|
|
153
|
+
delete properties.Tags;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface CdkTemplateOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Replace every resource's `Code` property, every container definition's
|
|
4
|
+
* `Image`, and the whole `Parameters` block with
|
|
5
|
+
* {@link CdkTemplateOptions.assetPlaceholder}.
|
|
6
|
+
*
|
|
7
|
+
* Assets elsewhere, such as Lambda layers, keep their hash. A function using
|
|
8
|
+
* `currentVersion` also needs
|
|
9
|
+
* {@link CdkTemplateOptions.ignoreCurrentVersion}, since the version's
|
|
10
|
+
* logical ID hashes the code.
|
|
11
|
+
*/
|
|
12
|
+
ignoreAssets?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Replace the hash of every asset in the app with `<ASSET_HASH>`, wherever
|
|
15
|
+
* a string in the template holds it: Lambda code and layers, container
|
|
16
|
+
* images, `BucketDeployment` sources, nested stack templates, CDK Pipelines
|
|
17
|
+
* commands. The rest of each value stays, and values that are not assets
|
|
18
|
+
* are untouched.
|
|
19
|
+
*
|
|
20
|
+
* The hashes are read from the asset manifests CDK's default synthesizer
|
|
21
|
+
* writes. A function using `currentVersion` also needs
|
|
22
|
+
* {@link CdkTemplateOptions.ignoreCurrentVersion}.
|
|
23
|
+
*/
|
|
24
|
+
ignoreAssetHashes?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Drop the CDK-managed `BootstrapVersion` parameter and its check rule.
|
|
27
|
+
* Defaults to `true`.
|
|
28
|
+
*/
|
|
29
|
+
ignoreBootstrapVersion?: boolean;
|
|
30
|
+
/** Mask the content hash suffix on Lambda `CurrentVersion` logical IDs. */
|
|
31
|
+
ignoreCurrentVersion?: boolean;
|
|
32
|
+
/** Drop template and resource `Metadata`. */
|
|
33
|
+
ignoreMetadata?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Drop each resource's `Tags` property. Tags nested deeper, such as a launch
|
|
36
|
+
* template's `TagSpecifications`, are kept.
|
|
37
|
+
*/
|
|
38
|
+
ignoreTags?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Mask asset paths, IDs and destination suffixes inside CDK Pipelines
|
|
41
|
+
* `cdk-assets` commands.
|
|
42
|
+
*/
|
|
43
|
+
ignorePipelineAssets?: boolean;
|
|
44
|
+
/** Keep only resources of these CloudFormation types. */
|
|
45
|
+
subsetResourceTypes?: string[];
|
|
46
|
+
/** Keep only resources with these logical IDs. */
|
|
47
|
+
subsetResourceKeys?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Token substituted for asset-derived values. Defaults to a matcher
|
|
50
|
+
* serializing as `Any<Object>`; the Bun entry point overrides it.
|
|
51
|
+
*/
|
|
52
|
+
assetPlaceholder?: unknown;
|
|
53
|
+
}
|
|
54
|
+
/** {@link CdkTemplateOptions} plus what only the snapshot matcher can apply. */
|
|
55
|
+
export interface CdkSnapshotOptions extends CdkTemplateOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Property matchers handed to the runner's snapshot assertion, for values
|
|
58
|
+
* the normalizations do not cover.
|
|
59
|
+
*/
|
|
60
|
+
propertyMatchers?: Record<string, unknown>;
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stand-in for asset-derived values, which change whenever an asset's content
|
|
3
|
+
* does.
|
|
4
|
+
* Serializes as `Any<Object>` so snapshots match across test runners.
|
|
5
|
+
*
|
|
6
|
+
* Bun accepts only matchers built by its own `expect`; the Bun entry point
|
|
7
|
+
* substitutes one.
|
|
8
|
+
*/
|
|
9
|
+
export declare const anyObject: unknown;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.anyObject = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Stand-in for asset-derived values, which change whenever an asset's content
|
|
6
|
+
* does.
|
|
7
|
+
* Serializes as `Any<Object>` so snapshots match across test runners.
|
|
8
|
+
*
|
|
9
|
+
* Bun accepts only matchers built by its own `expect`; the Bun entry point
|
|
10
|
+
* substitutes one.
|
|
11
|
+
*/
|
|
12
|
+
exports.anyObject = {
|
|
13
|
+
$$typeof: Symbol.for("jest.asymmetricMatcher"),
|
|
14
|
+
asymmetricMatch: (actual) => typeof actual === "object" && actual !== null,
|
|
15
|
+
toString: () => "Any",
|
|
16
|
+
getExpectedType: () => "Object",
|
|
17
|
+
toAsymmetricMatcher: () => "Any<Object>",
|
|
18
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Template } from "aws-cdk-lib/assertions";
|
|
2
|
+
import { assetHashes } from "./assets.js";
|
|
2
3
|
import { normalize } from "./normalize.js";
|
|
3
4
|
export { anyObject } from "./placeholder.js";
|
|
4
5
|
/**
|
|
@@ -11,5 +12,9 @@ export { anyObject } from "./placeholder.js";
|
|
|
11
12
|
* Bun users should import this from `@liflig/cdk-snapshot/bun` instead.
|
|
12
13
|
*/
|
|
13
14
|
export function cdkTemplate(stack, options = {}) {
|
|
14
|
-
|
|
15
|
+
const template = Template.fromStack(stack).toJSON();
|
|
16
|
+
const hashes = options.ignoreAssetHashes
|
|
17
|
+
? assetHashes(stack)
|
|
18
|
+
: new Set();
|
|
19
|
+
return normalize(template, options, hashes);
|
|
15
20
|
}
|
package/lib/normalize.d.ts
CHANGED
|
@@ -9,5 +9,8 @@ export type Template = Record<string, any>;
|
|
|
9
9
|
*
|
|
10
10
|
* Step order is significant: earlier steps can remove structures that later
|
|
11
11
|
* ones inspect.
|
|
12
|
+
*
|
|
13
|
+
* `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
|
|
14
|
+
* masks, as read from the stack's cloud assembly.
|
|
12
15
|
*/
|
|
13
|
-
export declare function normalize(template: Template, options?: CdkTemplateOptions): Template;
|
|
16
|
+
export declare function normalize(template: Template, options?: CdkTemplateOptions, assetHashes?: ReadonlySet<string>): Template;
|
package/lib/normalize.js
CHANGED
|
@@ -2,7 +2,9 @@ import { anyObject } from "./placeholder.js";
|
|
|
2
2
|
const currentVersionRegex = /^(.+CurrentVersion[0-9A-F]{8})[0-9a-f]{32}$/;
|
|
3
3
|
const pipelineCdkAssetsRegex = /cdk-assets\s+--path\s+\\"([^\\/]+)\/.+?assets\.json\\"\s+--verbose\s+publish\s+\\"(.+?)\\"/g;
|
|
4
4
|
const assetDestinationRegex = /:(.*?)(?:-[0-9a-f]{8})?$/;
|
|
5
|
+
const assetHashRegex = /(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])/g;
|
|
5
6
|
const maskedVersionSuffix = "x".repeat(32);
|
|
7
|
+
const maskedAssetHash = "<ASSET_HASH>";
|
|
6
8
|
/**
|
|
7
9
|
* Returns a copy of `template` with the configured normalizations applied. The
|
|
8
10
|
* argument is left untouched: `Template.fromStack` hands out the assembly's
|
|
@@ -11,14 +13,19 @@ const maskedVersionSuffix = "x".repeat(32);
|
|
|
11
13
|
*
|
|
12
14
|
* Step order is significant: earlier steps can remove structures that later
|
|
13
15
|
* ones inspect.
|
|
16
|
+
*
|
|
17
|
+
* `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
|
|
18
|
+
* masks, as read from the stack's cloud assembly.
|
|
14
19
|
*/
|
|
15
|
-
export function normalize(template, options = {}) {
|
|
16
|
-
const { ignoreAssets = false, ignoreBootstrapVersion = true, ignoreCurrentVersion = false, ignoreMetadata = false, ignoreTags = false, ignorePipelineAssets = false, subsetResourceTypes, subsetResourceKeys, assetPlaceholder = anyObject, } = options;
|
|
20
|
+
export function normalize(template, options = {}, assetHashes = new Set()) {
|
|
21
|
+
const { ignoreAssets = false, ignoreAssetHashes = false, ignoreBootstrapVersion = true, ignoreCurrentVersion = false, ignoreMetadata = false, ignoreTags = false, ignorePipelineAssets = false, subsetResourceTypes, subsetResourceKeys, assetPlaceholder = anyObject, } = options;
|
|
17
22
|
const result = structuredClone(template);
|
|
18
23
|
if (ignoreBootstrapVersion)
|
|
19
24
|
stripBootstrapVersion(result);
|
|
20
25
|
if (ignoreAssets)
|
|
21
26
|
stripAssets(result, assetPlaceholder);
|
|
27
|
+
if (ignoreAssetHashes)
|
|
28
|
+
maskAssetHashes(result, assetHashes);
|
|
22
29
|
if (ignoreCurrentVersion)
|
|
23
30
|
maskCurrentVersions(result);
|
|
24
31
|
if (ignorePipelineAssets)
|
|
@@ -66,6 +73,13 @@ function stripAssets(template, placeholder) {
|
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Only a standalone 64-hex run is a candidate, so a longer hex string that
|
|
78
|
+
* happens to contain an asset hash is left intact.
|
|
79
|
+
*/
|
|
80
|
+
function maskAssetHashes(tree, hashes) {
|
|
81
|
+
transformStrings(tree, (value) => value.replace(assetHashRegex, (hash) => hashes.has(hash) ? maskedAssetHash : hash));
|
|
82
|
+
}
|
|
69
83
|
function maskCurrentVersions(tree) {
|
|
70
84
|
transformStrings(tree, (value) => {
|
|
71
85
|
const match = currentVersionRegex.exec(value);
|
package/lib/options.d.ts
CHANGED
|
@@ -10,6 +10,18 @@ export interface CdkTemplateOptions {
|
|
|
10
10
|
* logical ID hashes the code.
|
|
11
11
|
*/
|
|
12
12
|
ignoreAssets?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Replace the hash of every asset in the app with `<ASSET_HASH>`, wherever
|
|
15
|
+
* a string in the template holds it: Lambda code and layers, container
|
|
16
|
+
* images, `BucketDeployment` sources, nested stack templates, CDK Pipelines
|
|
17
|
+
* commands. The rest of each value stays, and values that are not assets
|
|
18
|
+
* are untouched.
|
|
19
|
+
*
|
|
20
|
+
* The hashes are read from the asset manifests CDK's default synthesizer
|
|
21
|
+
* writes. A function using `currentVersion` also needs
|
|
22
|
+
* {@link CdkTemplateOptions.ignoreCurrentVersion}.
|
|
23
|
+
*/
|
|
24
|
+
ignoreAssetHashes?: boolean;
|
|
13
25
|
/**
|
|
14
26
|
* Drop the CDK-managed `BootstrapVersion` parameter and its check rule.
|
|
15
27
|
* Defaults to `true`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liflig/cdk-snapshot",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Normalizes synthesized AWS CDK stacks for snapshot testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,12 +28,18 @@
|
|
|
28
28
|
"bun",
|
|
29
29
|
"node-test"
|
|
30
30
|
],
|
|
31
|
-
"main": "./lib/index.js",
|
|
32
|
-
"types": "./lib/index.d.ts",
|
|
31
|
+
"main": "./lib/cjs/index.js",
|
|
32
|
+
"types": "./lib/cjs/index.d.ts",
|
|
33
33
|
"exports": {
|
|
34
34
|
".": {
|
|
35
|
-
"
|
|
36
|
-
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./lib/index.d.ts",
|
|
37
|
+
"default": "./lib/index.js"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"types": "./lib/cjs/index.d.ts",
|
|
41
|
+
"default": "./lib/cjs/index.js"
|
|
42
|
+
},
|
|
37
43
|
"default": "./lib/index.js"
|
|
38
44
|
},
|
|
39
45
|
"./bun": {
|
|
@@ -42,8 +48,14 @@
|
|
|
42
48
|
"default": "./lib/bun.js"
|
|
43
49
|
},
|
|
44
50
|
"./jest": {
|
|
45
|
-
"
|
|
46
|
-
|
|
51
|
+
"import": {
|
|
52
|
+
"types": "./lib/jest.d.ts",
|
|
53
|
+
"default": "./lib/jest.js"
|
|
54
|
+
},
|
|
55
|
+
"require": {
|
|
56
|
+
"types": "./lib/cjs/jest.d.ts",
|
|
57
|
+
"default": "./lib/cjs/jest.js"
|
|
58
|
+
},
|
|
47
59
|
"default": "./lib/jest.js"
|
|
48
60
|
},
|
|
49
61
|
"./node": {
|
|
@@ -62,7 +74,7 @@
|
|
|
62
74
|
"lib/**/*"
|
|
63
75
|
],
|
|
64
76
|
"scripts": {
|
|
65
|
-
"build": "tsc",
|
|
77
|
+
"build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > lib/cjs/package.json",
|
|
66
78
|
"test": "bun test ./test",
|
|
67
79
|
"snapshots": "bun test ./test --update-snapshots",
|
|
68
80
|
"typecheck": "tsc -p tsconfig.check.json",
|