@composurecdk/core 0.5.1 → 0.7.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/builder.d.ts CHANGED
@@ -9,6 +9,31 @@ type Constructor<T> = new () => T;
9
9
  interface ObjectWithProps<Props extends object> {
10
10
  props: Partial<Props>;
11
11
  }
12
+ /**
13
+ * Optional hook a builder class can implement to clone non-`props` state
14
+ * during {@link IBuilder.copy}.
15
+ *
16
+ * The default `.copy()` shallow-clones `props` and wraps a fresh instance.
17
+ * State stored outside `props` (private fields, internal accumulators) is
18
+ * invisible to that default. A class with such state defines a method
19
+ * keyed by this symbol to copy it onto the new instance.
20
+ *
21
+ * See ADR-0005 for the full protocol, including how decorator layers
22
+ * participate.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * class StackBuilder {
27
+ * props: Partial<StackProps> = {};
28
+ * readonly #tags: [string, string][] = [];
29
+ *
30
+ * [COPY_STATE](target: StackBuilder): void {
31
+ * target.#tags.push(...this.#tags);
32
+ * }
33
+ * }
34
+ * ```
35
+ */
36
+ export declare const COPY_STATE: unique symbol;
12
37
  /**
13
38
  * A fluent builder interface generated from a props type and a target class.
14
39
  *
@@ -20,6 +45,9 @@ interface ObjectWithProps<Props extends object> {
20
45
  * replaced to return the builder. All other members of `T` pass through as-is,
21
46
  * allowing methods like `build()` to be called directly on the builder.
22
47
  *
48
+ * Every builder also exposes {@link IBuilder.copy | `.copy()`}, which returns
49
+ * an independent builder with the same configured state.
50
+ *
23
51
  * @typeParam Props - The configurable properties.
24
52
  * @typeParam T - The target class the builder wraps.
25
53
  */
@@ -27,11 +55,37 @@ export type IBuilder<Props extends object, T> = {
27
55
  [K in keyof Props]-?: ((arg: Props[K]) => IBuilder<Props, T>) & (() => Props[K]);
28
56
  } & {
29
57
  [K in keyof T]: T[K] extends (...args: infer A) => T ? (...args: A) => IBuilder<Props, T> : T[K];
58
+ } & {
59
+ /**
60
+ * Returns an independent builder with the same configured props and any
61
+ * state that the underlying class copies via {@link COPY_STATE}.
62
+ *
63
+ * Mutations to the returned builder do not affect the original, and
64
+ * vice versa.
65
+ *
66
+ * `props` is shallow-cloned (`{ ...this.props }`). Top-level keys are
67
+ * independent; nested object references (CDK constructs, IRoles, IVpcs,
68
+ * etc.) are shared by design — they are construct identities, not
69
+ * configuration data. Builders with internal lists/maps/sets that should
70
+ * be deep-cloned implement {@link COPY_STATE}.
71
+ *
72
+ * Use cases:
73
+ * - **Variant authoring** — derive multiple builders from a shared base
74
+ * (`const us = base.copy().region("us-east-1")`).
75
+ * - **Strategy hand-off snapshot** — pass an isolated builder to a stack
76
+ * strategy (`singleStack(base.copy())`) so later mutations to the
77
+ * original don't leak into the strategy's lazy `build()`.
78
+ *
79
+ * See ADR-0005 for the design rationale.
80
+ */
81
+ copy(): IBuilder<Props, T>;
30
82
  };
31
83
  /**
32
84
  * Creates a fluent builder wrapping an instance of `T`.
33
85
  *
34
86
  * The builder is backed by a {@link Proxy} that intercepts property access:
87
+ * - For `copy`: returns a function that produces an independent builder with
88
+ * the same configured state (see {@link IBuilder.copy}).
35
89
  * - For keys in `Props`: returns a getter/setter function. When called with a
36
90
  * value, it sets the prop and returns the builder. When called with no args,
37
91
  * it returns the current value.
@@ -39,8 +93,11 @@ export type IBuilder<Props extends object, T> = {
39
93
  * - For all other members: delegates directly to the underlying instance.
40
94
  *
41
95
  * @param constructor - The class to instantiate and wrap.
42
- * @returns A fluent {@link IBuilder} wrapping a new instance of `T`.
96
+ * @param instance - Optional pre-existing instance to wrap. Defaults to
97
+ * `new constructor()`. Used internally by {@link IBuilder.copy} to wrap a
98
+ * freshly cloned instance without re-running the constructor for new state.
99
+ * @returns A fluent {@link IBuilder} wrapping `instance`.
43
100
  */
44
- export declare function Builder<Props extends object, T extends ObjectWithProps<Props>>(constructor: Constructor<T>): IBuilder<Props, T>;
101
+ export declare function Builder<Props extends object, T extends ObjectWithProps<Props>>(constructor: Constructor<T>, instance?: T): IBuilder<Props, T>;
45
102
  export {};
46
103
  //# sourceMappingURL=builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,KAAK,WAAW,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC;AAElC;;;GAGG;AACH,UAAU,eAAe,CAAC,KAAK,SAAS,MAAM;IAC5C,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,EAAE,CAAC,IAAI;KAC7C,CAAC,IAAI,MAAM,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;CACjF,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACjG,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,KAAK,SAAS,MAAM,EAAE,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,EAC5E,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,GAC1B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAoCpB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,KAAK,WAAW,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC;AAElC;;;GAGG;AACH,UAAU,eAAe,CAAC,KAAK,SAAS,MAAM;IAC5C,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,UAAU,eAA+C,CAAC;AAEvE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,EAAE,CAAC,IAAI;KAC7C,CAAC,IAAI,MAAM,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;CACjF,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACjG,GAAG;IACF;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;CAC5B,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,KAAK,SAAS,MAAM,EAAE,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,EAC5E,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,EAC3B,QAAQ,GAAE,CAAqB,GAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CA+CpB"}
package/dist/builder.js CHANGED
@@ -1,7 +1,34 @@
1
+ /**
2
+ * Optional hook a builder class can implement to clone non-`props` state
3
+ * during {@link IBuilder.copy}.
4
+ *
5
+ * The default `.copy()` shallow-clones `props` and wraps a fresh instance.
6
+ * State stored outside `props` (private fields, internal accumulators) is
7
+ * invisible to that default. A class with such state defines a method
8
+ * keyed by this symbol to copy it onto the new instance.
9
+ *
10
+ * See ADR-0005 for the full protocol, including how decorator layers
11
+ * participate.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * class StackBuilder {
16
+ * props: Partial<StackProps> = {};
17
+ * readonly #tags: [string, string][] = [];
18
+ *
19
+ * [COPY_STATE](target: StackBuilder): void {
20
+ * target.#tags.push(...this.#tags);
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+ export const COPY_STATE = Symbol.for("composurecdk.builder.copyState");
1
26
  /**
2
27
  * Creates a fluent builder wrapping an instance of `T`.
3
28
  *
4
29
  * The builder is backed by a {@link Proxy} that intercepts property access:
30
+ * - For `copy`: returns a function that produces an independent builder with
31
+ * the same configured state (see {@link IBuilder.copy}).
5
32
  * - For keys in `Props`: returns a getter/setter function. When called with a
6
33
  * value, it sets the prop and returns the builder. When called with no args,
7
34
  * it returns the current value.
@@ -9,16 +36,29 @@
9
36
  * - For all other members: delegates directly to the underlying instance.
10
37
  *
11
38
  * @param constructor - The class to instantiate and wrap.
12
- * @returns A fluent {@link IBuilder} wrapping a new instance of `T`.
39
+ * @param instance - Optional pre-existing instance to wrap. Defaults to
40
+ * `new constructor()`. Used internally by {@link IBuilder.copy} to wrap a
41
+ * freshly cloned instance without re-running the constructor for new state.
42
+ * @returns A fluent {@link IBuilder} wrapping `instance`.
13
43
  */
14
- export function Builder(constructor) {
15
- const instance = new constructor();
44
+ export function Builder(constructor, instance = new constructor()) {
16
45
  const methods = new Set(Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).filter((key) => key !== "constructor" && typeof instance[key] === "function"));
17
46
  const proxy = new Proxy(instance, {
18
47
  get(target, prop) {
19
48
  if (typeof prop === "symbol") {
20
49
  return Reflect.get(target, prop);
21
50
  }
51
+ if (prop === "copy") {
52
+ return () => {
53
+ const next = new constructor();
54
+ next.props = { ...target.props };
55
+ const hook = target[COPY_STATE];
56
+ if (typeof hook === "function") {
57
+ hook.call(target, next);
58
+ }
59
+ return Builder(constructor, next);
60
+ };
61
+ }
22
62
  // Props getter/setter
23
63
  if (!methods.has(prop)) {
24
64
  return (...args) => {
@@ -1 +1 @@
1
- {"version":3,"file":"builder.js","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAiCA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,OAAO,CACrB,WAA2B;IAE3B,MAAM,QAAQ,GAAG,IAAI,WAAW,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAChE,CAAC,GAAG,EAAE,EAAE,CACN,GAAG,KAAK,aAAa,IAAI,OAAQ,QAAoC,CAAC,GAAG,CAAC,KAAK,UAAU,CAC5F,CACF,CAAC;IAEF,MAAM,KAAK,GAAuB,IAAI,KAAK,CAAC,QAAQ,EAAE;QACpD,GAAG,CAAC,MAAS,EAAE,IAAqB;YAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAY,CAAC;YAC9C,CAAC;YAED,sBAAsB;YACtB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvB,OAAO,CAAC,GAAG,IAAe,EAAE,EAAE;oBAC5B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBACtB,OAAO,MAAM,CAAC,KAAK,CAAC,IAAmB,CAAC,CAAC;oBAC3C,CAAC;oBACD,MAAM,CAAC,KAAK,CAAC,IAAmB,CAAC,GAAG,IAAI,CAAC,CAAC,CAAuB,CAAC;oBAClE,OAAO,KAAK,CAAC;gBACf,CAAC,CAAC;YACJ,CAAC;YAED,gEAAgE;YAChE,MAAM,MAAM,GAAI,MAAkC,CAAC,IAAI,CAAiC,CAAC;YACzF,OAAO,CAAC,GAAG,IAAe,EAAE,EAAE;gBAC5B,MAAM,MAAM,GAAY,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBACnD,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;YAC5C,CAAC,CAAC;QACJ,CAAC;KACF,CAAuB,CAAC;IAEzB,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"builder.js","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAiDvE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,OAAO,CACrB,WAA2B,EAC3B,WAAc,IAAI,WAAW,EAAE;IAE/B,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAChE,CAAC,GAAG,EAAE,EAAE,CACN,GAAG,KAAK,aAAa,IAAI,OAAQ,QAAoC,CAAC,GAAG,CAAC,KAAK,UAAU,CAC5F,CACF,CAAC;IAEF,MAAM,KAAK,GAAuB,IAAI,KAAK,CAAC,QAAQ,EAAE;QACpD,GAAG,CAAC,MAAS,EAAE,IAAqB;YAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAY,CAAC;YAC9C,CAAC;YAED,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;gBACpB,OAAO,GAAG,EAAE;oBACV,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC;oBAC/B,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;oBACjC,MAAM,IAAI,GAAI,MAA6C,CAAC,UAAU,CAAC,CAAC;oBACxE,IAAI,OAAO,IAAI,KAAK,UAAU,EAAE,CAAC;wBAC9B,IAA0B,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;oBACjD,CAAC;oBACD,OAAO,OAAO,CAAW,WAAW,EAAE,IAAI,CAAC,CAAC;gBAC9C,CAAC,CAAC;YACJ,CAAC;YAED,sBAAsB;YACtB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvB,OAAO,CAAC,GAAG,IAAe,EAAE,EAAE;oBAC5B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBACtB,OAAO,MAAM,CAAC,KAAK,CAAC,IAAmB,CAAC,CAAC;oBAC3C,CAAC;oBACD,MAAM,CAAC,KAAK,CAAC,IAAmB,CAAC,GAAG,IAAI,CAAC,CAAC,CAAuB,CAAC;oBAClE,OAAO,KAAK,CAAC;gBACf,CAAC,CAAC;YACJ,CAAC;YAED,gEAAgE;YAChE,MAAM,MAAM,GAAI,MAAkC,CAAC,IAAI,CAAiC,CAAC;YACzF,OAAO,CAAC,GAAG,IAAe,EAAE,EAAE;gBAC5B,MAAM,MAAM,GAAY,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBACnD,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;YAC5C,CAAC,CAAC;QACJ,CAAC;KACF,CAAuB,CAAC;IAEzB,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -10,7 +10,7 @@
10
10
  * These helpers consolidate that sanitization in one place so every builder
11
11
  * in the monorepo applies the same constraints.
12
12
  */
13
- // eslint-disable-next-line no-control-regex
13
+ // eslint-disable-next-line no-control-regex -- the regex must literally match control characters to strip them from construct IDs
14
14
  const UNSAFE = /[/\x00-\x1f\x7f]/g;
15
15
  /**
16
16
  * Return a construct-ID-safe copy of `raw` by replacing unsafe characters
@@ -1 +1 @@
1
- {"version":3,"file":"construct-id.js","sourceRoot":"","sources":["../src/construct-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,4CAA4C;AAC5C,MAAM,MAAM,GAAG,mBAAmB,CAAC;AAEnC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW,CAAC,GAAG,KAAqD;IAClF,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SACjE,GAAG,CAAC,mBAAmB,CAAC;SACxB,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"construct-id.js","sourceRoot":"","sources":["../src/construct-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,kIAAkI;AAClI,MAAM,MAAM,GAAG,mBAAmB,CAAC;AAEnC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW,CAAC,GAAG,KAAqD;IAClF,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SACjE,GAAG,CAAC,mBAAmB,CAAC;SACxB,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Builder, type IBuilder } from "./builder.js";
1
+ export { Builder, COPY_STATE, type IBuilder } from "./builder.js";
2
2
  export { constructId, sanitizeConstructId } from "./construct-id.js";
3
3
  export { compose, type ComposedSystem, type ConfiguredSystem, type AfterBuildHook, } from "./compose.js";
4
4
  export { CyclicDependencyError } from "./cyclic-dependency-error.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AACrE,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,WAAW,EACX,aAAa,GACd,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AACrE,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,WAAW,EACX,aAAa,GACd,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Builder } from "./builder.js";
1
+ export { Builder, COPY_STATE } from "./builder.js";
2
2
  export { constructId, sanitizeConstructId } from "./construct-id.js";
3
3
  export { compose, } from "./compose.js";
4
4
  export { CyclicDependencyError } from "./cyclic-dependency-error.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAiB,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EACL,OAAO,GAIR,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAErE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAmB,MAAM,UAAU,CAAC;AACrE,OAAO,EAGL,WAAW,EACX,aAAa,GACd,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAiB,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EACL,OAAO,GAIR,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAErE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAmB,MAAM,UAAU,CAAC;AACrE,OAAO,EAGL,WAAW,EACX,aAAa,GACd,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Asserts that {@link IBuilder.copy | `.copy()`} returns an independent
3
+ * builder that preserves non-`props` state set via {@link COPY_STATE}.
4
+ *
5
+ * The helper is the standard way per-package tests verify each accumulator
6
+ * a builder holds outside `props`. It executes the following sequence and
7
+ * asserts the two invariants implied by ADR-0005:
8
+ *
9
+ * 1. Build a **baseline**: `factory()` then `configure()`.
10
+ * 2. Build the **copy**: `factory()`, `configure()`, then `.copy()`.
11
+ * Apply `mutate()` to the original *after* the copy is taken.
12
+ * 3. Build the **original** (now carrying both `configure` and `mutate`).
13
+ *
14
+ * Note that the baseline is constructed via `factory()` rather than via
15
+ * `.copy()` — using `.copy()` would make the helper unable to detect a
16
+ * broken `[COPY_STATE]`, since both the "baseline" and the "copy" would
17
+ * drop the same state and still match.
18
+ *
19
+ * Then:
20
+ *
21
+ * - `inspect(copyResult)` must deep-equal `inspect(baselineResult)` —
22
+ * the copy preserved exactly the state that `configure` set up.
23
+ * A failure here means `[COPY_STATE]` is missing, incomplete, or buggy.
24
+ * - `inspect(originalResult)` must deep-not-equal `inspect(baselineResult)` —
25
+ * `mutate` actually changed inspectable state. Without this sanity
26
+ * check, a no-op `mutate` would let the first assertion pass
27
+ * trivially, hiding an isolation bug.
28
+ *
29
+ * @param args.factory - Returns a fresh, unconfigured builder. Called
30
+ * twice — the helper builds two independent instances so that the
31
+ * build calls don't share construct scopes and so the baseline does
32
+ * not depend on `.copy()`.
33
+ * @param args.configure - Applies the state under test (e.g. adds a
34
+ * `customAlarm`, configures `#vpc`, appends a `subscription`). Called
35
+ * on the baseline and on the original.
36
+ * @param args.mutate - Applied to the original *after* the copy is taken.
37
+ * Must change something the `inspect` callback can see — typically a
38
+ * second `configure`-shaped call that adds another item to the
39
+ * accumulator under test.
40
+ * @param args.build - Calls `.build(scope, id)` against a fresh CDK
41
+ * scope. Three separate scopes are required (one per build) — return a
42
+ * freshly constructed scope each call. Reusing a scope across calls
43
+ * surfaces as a CDK duplicate-id error rather than a silent leak.
44
+ * @param args.inspect - Extracts the slice of the build result whose
45
+ * shape depends on the state under test (e.g.
46
+ * `result => Object.keys(result.alarms)`).
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * import { App, Stack } from "aws-cdk-lib";
51
+ * import { assertCopyPreservesState } from "@composurecdk/core/testing";
52
+ *
53
+ * assertCopyPreservesState({
54
+ * factory: () => createCertificateBuilder().domainName("example.com"),
55
+ * configure: (b) => b.customAlarm({ id: "FirstAlarm", ... }),
56
+ * mutate: (b) => b.customAlarm({ id: "SecondAlarm", ... }),
57
+ * build: (b) => b.build(new Stack(new App(), "S"), "Cert"),
58
+ * inspect: (r) => Object.keys(r.alarms).sort(),
59
+ * });
60
+ * ```
61
+ */
62
+ export declare function assertCopyPreservesState<B extends {
63
+ copy(): B;
64
+ }, R>(args: {
65
+ factory: () => B;
66
+ configure: (builder: B) => void;
67
+ mutate: (builder: B) => void;
68
+ build: (builder: B) => R;
69
+ inspect: (result: R) => unknown;
70
+ }): void;
71
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS;IAAE,IAAI,IAAI,CAAC,CAAA;CAAE,EAAE,CAAC,EAAE,IAAI,EAAE;IACzE,OAAO,EAAE,MAAM,CAAC,CAAC;IACjB,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACzB,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;CACjC,GAAG,IAAI,CAsCP"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Asserts that {@link IBuilder.copy | `.copy()`} returns an independent
3
+ * builder that preserves non-`props` state set via {@link COPY_STATE}.
4
+ *
5
+ * The helper is the standard way per-package tests verify each accumulator
6
+ * a builder holds outside `props`. It executes the following sequence and
7
+ * asserts the two invariants implied by ADR-0005:
8
+ *
9
+ * 1. Build a **baseline**: `factory()` then `configure()`.
10
+ * 2. Build the **copy**: `factory()`, `configure()`, then `.copy()`.
11
+ * Apply `mutate()` to the original *after* the copy is taken.
12
+ * 3. Build the **original** (now carrying both `configure` and `mutate`).
13
+ *
14
+ * Note that the baseline is constructed via `factory()` rather than via
15
+ * `.copy()` — using `.copy()` would make the helper unable to detect a
16
+ * broken `[COPY_STATE]`, since both the "baseline" and the "copy" would
17
+ * drop the same state and still match.
18
+ *
19
+ * Then:
20
+ *
21
+ * - `inspect(copyResult)` must deep-equal `inspect(baselineResult)` —
22
+ * the copy preserved exactly the state that `configure` set up.
23
+ * A failure here means `[COPY_STATE]` is missing, incomplete, or buggy.
24
+ * - `inspect(originalResult)` must deep-not-equal `inspect(baselineResult)` —
25
+ * `mutate` actually changed inspectable state. Without this sanity
26
+ * check, a no-op `mutate` would let the first assertion pass
27
+ * trivially, hiding an isolation bug.
28
+ *
29
+ * @param args.factory - Returns a fresh, unconfigured builder. Called
30
+ * twice — the helper builds two independent instances so that the
31
+ * build calls don't share construct scopes and so the baseline does
32
+ * not depend on `.copy()`.
33
+ * @param args.configure - Applies the state under test (e.g. adds a
34
+ * `customAlarm`, configures `#vpc`, appends a `subscription`). Called
35
+ * on the baseline and on the original.
36
+ * @param args.mutate - Applied to the original *after* the copy is taken.
37
+ * Must change something the `inspect` callback can see — typically a
38
+ * second `configure`-shaped call that adds another item to the
39
+ * accumulator under test.
40
+ * @param args.build - Calls `.build(scope, id)` against a fresh CDK
41
+ * scope. Three separate scopes are required (one per build) — return a
42
+ * freshly constructed scope each call. Reusing a scope across calls
43
+ * surfaces as a CDK duplicate-id error rather than a silent leak.
44
+ * @param args.inspect - Extracts the slice of the build result whose
45
+ * shape depends on the state under test (e.g.
46
+ * `result => Object.keys(result.alarms)`).
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * import { App, Stack } from "aws-cdk-lib";
51
+ * import { assertCopyPreservesState } from "@composurecdk/core/testing";
52
+ *
53
+ * assertCopyPreservesState({
54
+ * factory: () => createCertificateBuilder().domainName("example.com"),
55
+ * configure: (b) => b.customAlarm({ id: "FirstAlarm", ... }),
56
+ * mutate: (b) => b.customAlarm({ id: "SecondAlarm", ... }),
57
+ * build: (b) => b.build(new Stack(new App(), "S"), "Cert"),
58
+ * inspect: (r) => Object.keys(r.alarms).sort(),
59
+ * });
60
+ * ```
61
+ */
62
+ export function assertCopyPreservesState(args) {
63
+ const { factory, configure, mutate, build, inspect } = args;
64
+ const baseline = factory();
65
+ configure(baseline);
66
+ const baselineState = inspect(build(baseline));
67
+ const original = factory();
68
+ configure(original);
69
+ if (typeof original.copy !== "function") {
70
+ throw new Error("assertCopyPreservesState: builder returned by `factory` has no `.copy()` method. " +
71
+ "Pass a builder produced by `Builder()` / `taggedBuilder()` from `@composurecdk/core` / `@composurecdk/cloudformation`.");
72
+ }
73
+ const copy = original.copy();
74
+ mutate(original);
75
+ const originalState = inspect(build(original));
76
+ const copyState = inspect(build(copy));
77
+ if (deepEqual(originalState, baselineState)) {
78
+ throw new Error("assertCopyPreservesState: `mutate` did not change inspectable state on the original — " +
79
+ "the test cannot detect a leak through `.copy()`. Make `mutate` and `inspect` cover " +
80
+ "the same accumulator.\n" +
81
+ ` baseline: ${format(baselineState)}\n` +
82
+ ` original: ${format(originalState)}`);
83
+ }
84
+ if (!deepEqual(copyState, baselineState)) {
85
+ throw new Error("assertCopyPreservesState: `.copy()` did not preserve state set by `configure`. " +
86
+ "The builder is missing, incomplete, or has a buggy `[COPY_STATE]` hook (see ADR-0005).\n" +
87
+ ` baseline: ${format(baselineState)}\n` +
88
+ ` copy: ${format(copyState)}`);
89
+ }
90
+ }
91
+ function deepEqual(a, b) {
92
+ if (Object.is(a, b))
93
+ return true;
94
+ if (typeof a !== "object" || a === null)
95
+ return false;
96
+ if (typeof b !== "object" || b === null)
97
+ return false;
98
+ if (Array.isArray(a)) {
99
+ if (!Array.isArray(b) || a.length !== b.length)
100
+ return false;
101
+ return a.every((item, i) => deepEqual(item, b[i]));
102
+ }
103
+ if (Array.isArray(b))
104
+ return false;
105
+ const aKeys = Object.keys(a);
106
+ const bKeys = Object.keys(b);
107
+ if (aKeys.length !== bKeys.length)
108
+ return false;
109
+ const bRec = b;
110
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bRec, k) &&
111
+ deepEqual(a[k], bRec[k]));
112
+ }
113
+ function format(value) {
114
+ try {
115
+ return JSON.stringify(value);
116
+ }
117
+ catch {
118
+ return String(value);
119
+ }
120
+ }
121
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@composurecdk/core",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Composable CDK component system — lifecycle, dependency resolution, and builder pattern",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,10 @@
13
13
  ".": {
14
14
  "import": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts"
16
+ },
17
+ "./testing": {
18
+ "import": "./dist/testing.js",
19
+ "types": "./dist/testing.d.ts"
16
20
  }
17
21
  },
18
22
  "files": [