{"_id":"@arts-n-crafts/cdk-composition","_rev":"6-2e9e7e037369139dcd501504af90630c","name":"@arts-n-crafts/cdk-composition","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@arts-n-crafts/cdk-composition","version":"0.1.0","keywords":["aws-cdk","cdk","typescript","composition","constructs","patterns"],"license":"MIT","_id":"@arts-n-crafts/cdk-composition@0.1.0","maintainers":[{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"}],"homepage":"https://github.com/jvhellemondt/cdk-composition#readme","bugs":{"url":"https://github.com/jvhellemondt/cdk-composition/issues"},"dist":{"shasum":"19e31de0f6fc9e8cd4336e7fd9fc4f4891d01372","tarball":"https://registry.npmjs.org/@arts-n-crafts/cdk-composition/-/cdk-composition-0.1.0.tgz","fileCount":7,"integrity":"sha512-7Tkc6HRYNEK65I6nBr30aLwEU9llsM7Swa/pHvNbvfxflM1qVmZ5K5u/L/i2E6eDiL9xz7elr+sulNCLlnTI9w==","signatures":[{"sig":"MEQCIDAuJg1JKRQmqFObEbmMgeTZ29mn8KkluGtkM8N7xM/YAiAUfO4cX3Azc6EKrvM2hzaWeH8ZA/f9xzg4Qvd7M8w/qg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":58787},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"bun":">=1.0.0","node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"gitHead":"ad7ece54dc1169ca220122f684f66138e5484e96","scripts":{"lint":"oxlint src test","test":"bun test --timeout 30000","build":"tsup","format":"oxfmt src test","prepack":"bun run build","test:watch":"bun test --watch --timeout 30000","format:check":"oxfmt --check src test"},"_npmUser":{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"},"repository":{"url":"git+https://github.com/jvhellemondt/cdk-composition.git","type":"git"},"_npmVersion":"11.17.0","description":"Higher-level composition patterns for AWS CDK TypeScript","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.3.14","devDependencies":{"tsup":"^8.5.1","oxfmt":"^0.60.0","oxlint":"^1.75.0","aws-cdk":"^2.262.0","bun-types":"^1.3.14","constructs":"^10.3.0","typescript":"^5.9.2","aws-cdk-lib":"^2.262.0","aws-cdk-local":"^2.0.0"},"peerDependencies":{"constructs":"^10.0.0","aws-cdk-lib":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cdk-composition_0.1.0_1787674933996_0.7769566295907269","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@arts-n-crafts/cdk-composition","version":"0.1.1","keywords":["aws-cdk","cdk","typescript","composition","constructs","patterns"],"license":"MIT","_id":"@arts-n-crafts/cdk-composition@0.1.1","maintainers":[{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"}],"homepage":"https://github.com/jvhellemondt/cdk-composition#readme","bugs":{"url":"https://github.com/jvhellemondt/cdk-composition/issues"},"dist":{"shasum":"e08d6db107895077942093c026f0817ecbf13cbd","tarball":"https://registry.npmjs.org/@arts-n-crafts/cdk-composition/-/cdk-composition-0.1.1.tgz","fileCount":7,"integrity":"sha512-k7o6nTcKX1VNCwAN4vSL2rvyufe0lxEonnYrrhv+A8x9d29c5G4R30BsZ0dAxSASU/AIaOxFn4ENHDTwLQ9Hmg==","signatures":[{"sig":"MEUCIQCaP42iV3s0hDYNIpQewXYalz9Lco3KnT3dpf29qNrxYAIgZa8s9Il6R2ZvLiNyHX7FbxbgprjWmpyYXiywnFa85JM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@arts-n-crafts%2fcdk-composition@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":58562},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"bun":">=1.0.0","node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"gitHead":"584d773dafd8e8951b7879745e0854ba2fa9f3a2","scripts":{"lint":"oxlint src test","test":"bun test --timeout 30000","build":"tsup","format":"oxfmt src test","prepack":"bun run build","test:watch":"bun test --watch --timeout 30000","format:check":"oxfmt --check src test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da99e393-714b-42f0-8a3d-8c53c71104f8"}},"repository":{"url":"git+https://github.com/jvhellemondt/cdk-composition.git","type":"git"},"_npmVersion":"11.17.0","description":"Higher-level composition patterns for AWS CDK TypeScript","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.3.14","devDependencies":{"tsup":"^8.5.1","oxfmt":"^0.60.0","oxlint":"^1.75.0","aws-cdk":"^2.262.0","bun-types":"^1.3.14","constructs":"^10.3.0","typescript":"^5.9.2","aws-cdk-lib":"^2.262.0","aws-cdk-local":"^2.0.0"},"peerDependencies":{"constructs":"^10.0.0","aws-cdk-lib":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cdk-composition_0.1.1_1787677277139_0.24028236654144308","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@arts-n-crafts/cdk-composition","version":"0.1.2","keywords":["aws-cdk","cdk","typescript","composition","constructs","patterns"],"license":"MIT","_id":"@arts-n-crafts/cdk-composition@0.1.2","maintainers":[{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"}],"homepage":"https://github.com/jvhellemondt/cdk-composition#readme","bugs":{"url":"https://github.com/jvhellemondt/cdk-composition/issues"},"dist":{"shasum":"e74009ab337cd12fd6da30ca53e4044cbc718497","tarball":"https://registry.npmjs.org/@arts-n-crafts/cdk-composition/-/cdk-composition-0.1.2.tgz","fileCount":7,"integrity":"sha512-glfYxiLmdOoSsnf4xTQUtUBXJ56H0o6bEBZ6ICDWTtO6NA4Pfey/uUps50gZOUOz/NwpOFy3pGKJcYc5fKcwIg==","signatures":[{"sig":"MEUCIHGdwKAc9RE+cYWFPDQ0wR1MVLmFEwIx/HOBtM9lmh3+AiEA3GI7TguZBcAcrClOSHPy4VPgML4I2cZXxKY1y9jnLjY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@arts-n-crafts%2fcdk-composition@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":59334},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"bun":">=1.0.0","node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"gitHead":"3f28b295eff8dcdf26bb911769a0e99028d4bfd0","scripts":{"lint":"oxlint src test","test":"bun test --timeout 30000","build":"tsup","format":"oxfmt src test","prepack":"bun run build","test:watch":"bun test --watch --timeout 30000","format:check":"oxfmt --check src test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da99e393-714b-42f0-8a3d-8c53c71104f8"}},"repository":{"url":"git+https://github.com/jvhellemondt/cdk-composition.git","type":"git"},"_npmVersion":"11.17.0","description":"Higher-level composition patterns for AWS CDK TypeScript","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.3.14","devDependencies":{"tsup":"^8.5.1","oxfmt":"^0.60.0","oxlint":"^1.75.0","aws-cdk":"^2.262.0","bun-types":"^1.3.14","constructs":"^10.3.0","typescript":"^5.9.2","aws-cdk-lib":"^2.262.0","aws-cdk-local":"^2.0.0"},"peerDependencies":{"constructs":"^10.0.0","aws-cdk-lib":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cdk-composition_0.1.2_1787682267040_0.6799373507787709","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@arts-n-crafts/cdk-composition","version":"0.1.3","keywords":["aws-cdk","cdk","typescript","composition","constructs","patterns"],"license":"MIT","_id":"@arts-n-crafts/cdk-composition@0.1.3","maintainers":[{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"}],"homepage":"https://github.com/jvhellemondt/cdk-composition#readme","bugs":{"url":"https://github.com/jvhellemondt/cdk-composition/issues"},"dist":{"shasum":"948a5fd99cc1b546329ca3a75d3800948ff5de87","tarball":"https://registry.npmjs.org/@arts-n-crafts/cdk-composition/-/cdk-composition-0.1.3.tgz","fileCount":7,"integrity":"sha512-pnKYcdgIPJ+T2kgDM7uSPtJa8ameAGzTgCmlUZXeYYZbd3XhqIglGH5DLuiZNw3h1rF24BHSuAYTYfvrxPjC9A==","signatures":[{"sig":"MEYCIQC7Vkg/i50CEjFb3ROKfKs8wOzioxwv4pi2terel0rAbgIhAPXNiEd13noTE1azBsoz+5fof1dhe/yZKmnvvKtAvW/8","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@arts-n-crafts%2fcdk-composition@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":59895},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"bun":">=1.0.0","node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"gitHead":"316e419b4d37a65ee7ab9b36d42178359fbf1bb4","scripts":{"lint":"oxlint src test","test":"bun test --timeout 30000","build":"tsup","format":"oxfmt src test","prepack":"bun run build","test:watch":"bun test --watch --timeout 30000","format:check":"oxfmt --check src test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da99e393-714b-42f0-8a3d-8c53c71104f8"}},"repository":{"url":"git+https://github.com/jvhellemondt/cdk-composition.git","type":"git"},"_npmVersion":"11.17.0","description":"Higher-level composition patterns for AWS CDK TypeScript","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.3.14","devDependencies":{"tsup":"^8.5.1","oxfmt":"^0.60.0","oxlint":"^1.75.0","aws-cdk":"^2.262.0","bun-types":"^1.3.14","constructs":"^10.3.0","typescript":"^5.9.2","aws-cdk-lib":"^2.262.0","aws-cdk-local":"^2.0.0"},"peerDependencies":{"constructs":"^10.0.0","aws-cdk-lib":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cdk-composition_0.1.3_1787684824037_0.18200525338639761","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@arts-n-crafts/cdk-composition","version":"0.1.4","keywords":["aws-cdk","cdk","typescript","composition","constructs","patterns"],"license":"MIT","_id":"@arts-n-crafts/cdk-composition@0.1.4","maintainers":[{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"}],"homepage":"https://github.com/jvhellemondt/cdk-composition#readme","bugs":{"url":"https://github.com/jvhellemondt/cdk-composition/issues"},"dist":{"shasum":"45837bc9b0bf431992d89ca829600a21aca7469a","tarball":"https://registry.npmjs.org/@arts-n-crafts/cdk-composition/-/cdk-composition-0.1.4.tgz","fileCount":7,"integrity":"sha512-jv7qx9xtmE0iqpTaZEXmfISjbpT0ckq31U0tp2cNcpOKoRszMcbNV79OOE3B/aiKPY0fW0dvsImGBNT7HBt1DQ==","signatures":[{"sig":"MEUCIBDvL4QAlR6z3EjfORdvGcCHvdNr8R1VEpYqfnxsNoQ+AiEA8LtBgaATddU3kYAQoAKkHxUpS/NeXtu/Zt1oHsd7pyo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@arts-n-crafts%2fcdk-composition@0.1.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":59895},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"bun":">=1.0.0","node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"gitHead":"02ea9da6023c074cd94ab3299224e909b84bcf3b","scripts":{"lint":"oxlint src test","test":"bun test --timeout 30000","build":"tsup","format":"oxfmt src test","prepack":"bun run build","test:watch":"bun test --watch --timeout 30000","format:check":"oxfmt --check src test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:da99e393-714b-42f0-8a3d-8c53c71104f8"}},"repository":{"url":"git+https://github.com/jvhellemondt/cdk-composition.git","type":"git"},"_npmVersion":"11.17.0","description":"Higher-level composition patterns for AWS CDK TypeScript","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.3.14","devDependencies":{"tsup":"^8.5.1","oxfmt":"^0.60.0","oxlint":"^1.75.0","aws-cdk":"^2.262.0","bun-types":"^1.3.14","constructs":"^10.3.0","typescript":"^5.9.2","aws-cdk-lib":"^2.262.0","aws-cdk-local":"^2.0.0"},"peerDependencies":{"constructs":"^10.0.0","aws-cdk-lib":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/cdk-composition_0.1.4_1787684871916_0.30074604855980636","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@arts-n-crafts/cdk-composition@0.2.0","bugs":{"url":"https://github.com/jvhellemondt/cdk-composition/issues"},"dist":{"shasum":"33cdc4a2ca2ebffcc27ef2c8c2a5320fc020f52a","tarball":"https://registry.npmjs.org/@arts-n-crafts/cdk-composition/-/cdk-composition-0.2.0.tgz","fileCount":7,"integrity":"sha512-HQ2v2xBoNZC0FSJpMAOSj9OGzd/1xd5wqSOcxLWYFcI8O1qg92QuuVd8GM00PkWMcPulfSfqlXBOv9Twj9p/pQ==","signatures":[{"sig":"MEQCIDGZcaAggme5SnMrQdiCtOt9Monjv4sUrfiLTEGWyizhAiB6ayRrI22Oi12JAJdvLPn3k16e4DHohNTUBH2OcEyesA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDRzdeNBhKcvySaEZ/xiWnh4FfnAcK5IrdgIziKlx0Y4wIgOUSBMBLDdWIgP1Z1X9BBspQb4fHUBP70SnVUNGSzAuU="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@arts-n-crafts%2fcdk-composition@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":67956},"main":"./dist/index.js","name":"@arts-n-crafts/cdk-composition","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"bun":">=1.0.0","node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./package.json":"./package.json"},"gitHead":"a1018c06bff2288710b1ff29437c1f63d81e6f9e","license":"MIT","scripts":{"lint":"oxlint src test","test":"bun test --timeout 30000","build":"tsup","format":"oxfmt src test","prepack":"bun run build","test:watch":"bun test --watch --timeout 30000","format:check":"oxfmt --check src test"},"version":"0.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"da99e393-714b-42f0-8a3d-8c53c71104f8"}},"homepage":"https://github.com/jvhellemondt/cdk-composition#readme","keywords":["aws-cdk","cdk","typescript","composition","constructs","patterns"],"repository":{"url":"git+https://github.com/jvhellemondt/cdk-composition.git","type":"git"},"_npmVersion":"11.19.0","description":"Higher-level composition patterns for AWS CDK TypeScript","directories":{},"maintainers":[{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"}],"_nodeVersion":"24.21.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.3.14","devDependencies":{"tsup":"^8.5.1","oxfmt":"^0.60.0","oxlint":"^1.75.0","aws-cdk":"^2.262.0","bun-types":"^1.3.14","constructs":"^10.3.0","typescript":"^5.9.2","aws-cdk-lib":"^2.262.0","aws-cdk-local":"^2.0.0"},"peerDependencies":{"constructs":"^10.0.0","aws-cdk-lib":"^2.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cdk-composition_0.2.0_1790768821871_0.7653859924041657"}}},"time":{"created":"2026-08-25T16:22:13.720Z","modified":"2026-09-30T11:47:02.345Z","0.1.0":"2026-08-25T16:22:14.143Z","0.1.1":"2026-08-25T17:01:17.274Z","0.1.2":"2026-08-25T18:24:27.183Z","0.1.3":"2026-08-25T19:07:04.226Z","0.1.4":"2026-08-25T19:07:52.056Z","0.2.0":"2026-09-30T11:47:01.959Z"},"bugs":{"url":"https://github.com/jvhellemondt/cdk-composition/issues"},"license":"MIT","homepage":"https://github.com/jvhellemondt/cdk-composition#readme","keywords":["aws-cdk","cdk","typescript","composition","constructs","patterns"],"repository":{"url":"git+https://github.com/jvhellemondt/cdk-composition.git","type":"git"},"description":"Higher-level composition patterns for AWS CDK TypeScript","maintainers":[{"name":"jvhellemondt","email":"jens.vanhellemondt@gmail.com"}],"readme":"# cdk-composition\n\nHigher-level composition patterns for AWS CDK TypeScript.\n\n---\n\n> *The core tension: reuse encourages centralisation, but centralisation creates coupling.*\n>\n> Custom L3 constructs start focused but accrete unrelated capabilities over time — the path of least resistance is always \"add another prop.\" Changing any part of the construct risks regressions across every consumer. `cdk-composition` replaces the monolithic construct with a flat list of CDK classes and named, typed **traits**. Adding a capability means adding a trait. A change that alters the intent of an existing trait means writing a new one — the old trait stays valid for every consumer that still wants the original behaviour.\n>\n> — [ADR-0001](docs/0001-composition-with-pluggable-traits.md)\n\n---\n\n## Installation\n\n```sh\nnpm install @arts-n-crafts/cdk-composition\n# or\nbun add @arts-n-crafts/cdk-composition\n```\n\n`aws-cdk-lib` and `constructs` are peer dependencies — they are not bundled.\n\n## Concepts\n\nTraits are **named, typed values defined outside the composition**. They live in a shared file or library, carry a descriptive name, and describe a single concern. A `compose()` call is then a readable manifest — which constructs belong together and which named capabilities each carries — with no configuration detail buried inside it.\n\n`buildConstruct()` and `buildFlat()` materialise the composition in two phases:\n\n1. **Instantiation** — an entry is created the first time something asks for it. A property function resolving a sibling is what causes that sibling to be created, so the order emerges from the references traits actually make; entries nothing resolved are created by a final sweep in declaration order.\n2. **Deferred traits** — method and action traits run once every construct exists, in declaration order.\n\nDeclaration order is therefore presentation, not build order: order the `.and()` chain for reading. A property trait may resolve any sibling, whether it is declared before or after. The one unsatisfiable case is two entries whose property traits resolve *each other*, which building reports as a cycle naming the path — see [ADR-0002](docs/0002-demand-driven-instantiation.md).\n\nBuilding returns each construct under its own id, plus the same constructs typed and in declaration order, so nothing needs to be looked up afterwards:\n\n```ts\nconst { fn, queue } = compose(Function, [nodeRuntime], \"fn\")\n  .and(Queue, [], \"queue\")\n  .buildConstruct(this, \"Worker\");\n\nqueue.grantSendMessages(fn); // fully typed\n\n// Or positionally, when the ids don't matter:\nconst { constructs: [handler, jobs] } = compose(Function, [nodeRuntime]).and(Queue).buildConstruct(this, \"Jobs\");\n```\n\nEntries are named by their class name, with a numeric suffix on repeats (`Queue`, `Queue1`, …). Pass a third argument to `compose`/`and` to set the id yourself — worth doing when you want a stable, meaningful logical id, or a name to destructure the build result by:\n\n```ts\nconst { Inbox, Outbox } = compose(Queue, [], \"Inbox\").and(Queue, [], \"Outbox\").buildConstruct(this, \"Mail\");\n```\n\nA defaulted id keys the entry at runtime too, but only ids written as literals are visible to the compiler — see [Why `get` is only sometimes typed](#why-get-is-only-sometimes-typed), which applies to these names for the same reason. `root`, `constructs` and `resources` are the build result's own members, so they are rejected as ids.\n\n---\n\n## Traits\n\n### PropertyTrait\n\nMerges configuration into a construct's props before it is instantiated.\n\n`value` is a plain object for static configuration, or a function when a prop needs to reference a sibling construct. The function may resolve any sibling regardless of where it sits in the chain: the composition creates whatever the trait asks for on the spot. A trait's requirement is therefore \"the composition holds a `Queue`\", never \"a `Queue` is declared after me\" — so traits stay portable between compositions.\n\nUse the function form for anything stateful (`Code.fromAsset`, for instance). The object form is shared across every build, and CDK rejects a second binding.\n\nProperty traits merge left-to-right, later traits winning. Plain objects merge deeply, so separate traits can each contribute part of a nested prop; arrays and class instances are replaced outright.\n\n```ts\n// traits/queue.ts\nimport { Duration } from \"aws-cdk-lib\";\nimport { type PropertyTrait } from \"@arts-n-crafts/cdk-composition\";\n\nexport const thirtySecondVisibility: PropertyTrait = {\n  name: \"visibility-30s\",\n  type: \"property\",\n  value: { visibilityTimeout: Duration.seconds(30) },\n};\n\n// Function form — resolving the sibling is what creates it, wherever it sits in the chain\nexport const withDeadLetterQueue: PropertyTrait = {\n  name: \"dead-letter-queue\",\n  type: \"property\",\n  value: (r) => ({ deadLetterQueue: { queue: r.of(Queue), maxReceiveCount: 3 } }),\n};\n```\n\n```ts\n// stack.ts\ncompose(Function, [withDeadLetterQueue])\n  .and(Queue, [thirtySecondVisibility])\n  .buildConstruct(this, \"Worker\");\n```\n\nApply a base trait and override specific keys with a more specific one — no subclassing required.\n\nNote that props are checked individually but not collectively: because each trait contributes a `Partial`, a composition that never supplies a *required* prop still compiles and fails at synth with CDK's own error.\n\n---\n\n### MethodTrait\n\nCalls a named method on the construct after all siblings are instantiated.\n\nUse this for configuration that requires a method call rather than props — lifecycle rules, event source mappings, policy grants. Both the method name and its arguments are checked against the construct, and `args` receives a lookup covering every sibling.\n\nOverloaded methods resolve to their last overload; TypeScript exposes only that one.\n\n```ts\n// traits/bucket.ts\nimport { Duration } from \"aws-cdk-lib\";\nimport { type MethodTrait } from \"@arts-n-crafts/cdk-composition\";\nimport { Queue } from \"aws-cdk-lib/aws-sqs\";\nimport { SqsEventSource } from \"aws-cdk-lib/aws-lambda-event-sources\";\n\nexport const ninetyDayExpiry: MethodTrait = {\n  name: \"lifecycle-90d-expiry\",\n  type: \"method\",\n  args: (_r) => [{ expiration: Duration.days(90) }],\n};\n\n// traits/lambda.ts\nexport const withSqsEventSource = (batchSize = 10): MethodTrait => ({\n  name: \"sqs-event-source\",\n  type: \"method\",\n  args: (r) => [new SqsEventSource(r.of(Queue), { batchSize })],\n});\n```\n\n```ts\n// stack.ts\ncompose(Bucket, [ninetyDayExpiry]).buildConstruct(this, \"Archive\");\n\ncompose(Function, [withSqsEventSource(5)])\n  .and(Queue)\n  .buildConstruct(this, \"Worker\");\n```\n\nMethod traits are applied in declaration order, after every construct in the composition exists.\n\n---\n\n### ActionTrait\n\nRuns arbitrary logic against the construct after all siblings are instantiated.\n\nUse this for cross-composition wiring that cannot be expressed as a method call. The `run` function receives the construct itself, so `Stack.of(construct).node.findAll()` can locate any construct in the broader CDK tree — including shared infrastructure from a different composition.\n\n```ts\n// traits/api.ts\nimport { Stack } from \"aws-cdk-lib\";\nimport { Function } from \"aws-cdk-lib/aws-lambda\";\nimport { HttpApi, HttpMethod } from \"aws-cdk-lib/aws-apigatewayv2\";\nimport { HttpLambdaIntegration } from \"aws-cdk-lib/aws-apigatewayv2-integrations\";\nimport { type ActionTrait } from \"@arts-n-crafts/cdk-composition\";\n\nexport const httpRoute = (path: string, method: HttpMethod): ActionTrait<Function> => ({\n  name: `http-route-${method.toLowerCase()}-${path}`,\n  type: \"action\",\n  run: (fn, _r) => {\n    const api = Stack.of(fn).node\n      .findAll()\n      .find((c): c is HttpApi => c instanceof HttpApi);\n    api?.addRoutes({\n      path,\n      methods: [method],\n      integration: new HttpLambdaIntegration(path, fn),\n    });\n  },\n});\n```\n\n```ts\n// stack.ts — each route is its own composition; all wire to the same shared HttpApi\ncompose(Function, [nodeRuntime, httpRoute(\"/orders\", HttpMethod.POST)]).buildConstruct(this, \"CreateOrder\");\ncompose(Function, [nodeRuntime, httpRoute(\"/orders\", HttpMethod.GET)]).buildConstruct(this, \"ListOrders\");\ncompose(Function, [nodeRuntime, httpRoute(\"/orders/:id\", HttpMethod.DELETE)]).buildConstruct(this, \"DeleteOrder\");\n```\n\nBecause `httpRoute` finds the `HttpApi` via the CDK tree rather than through the resources map, each composition stays self-contained. No shared state, no cross-composition imports, no ordering dependencies.\n\n---\n\n## Full example\n\nA queue worker that uses all three trait types. Traits are defined in a shared file; the composition itself is a one-screen manifest.\n\n```ts\n// traits/worker.ts\nimport { Duration, Stack } from \"aws-cdk-lib\";\nimport { Runtime, Function } from \"aws-cdk-lib/aws-lambda\";\nimport { SqsEventSource } from \"aws-cdk-lib/aws-lambda-event-sources\";\nimport { Queue } from \"aws-cdk-lib/aws-sqs\";\nimport { HttpApi, HttpMethod } from \"aws-cdk-lib/aws-apigatewayv2\";\nimport { HttpLambdaIntegration } from \"aws-cdk-lib/aws-apigatewayv2-integrations\";\nimport { type PropertyTrait, type MethodTrait, type ActionTrait } from \"@arts-n-crafts/cdk-composition\";\n\nexport const nodeRuntime: PropertyTrait = {\n  name: \"runtime\",\n  type: \"property\",\n  value: { runtime: Runtime.NODEJS_24_X, memorySize: 512, timeout: Duration.seconds(30) },\n};\n\n// Function form — resolving the Queue is what creates it, so this works\n// wherever the Queue sits in the chain.\nexport const withDeadLetterQueue: PropertyTrait = {\n  name: \"dead-letter-queue\",\n  type: \"property\",\n  value: (r) => ({ deadLetterQueue: { queue: r.of(Queue), maxReceiveCount: 3 } }),\n};\n\nexport const withSqsEventSource = (batchSize = 10): MethodTrait<Function> => ({\n  name: \"sqs-event-source\",\n  type: \"method\",\n  args: (r) => [new SqsEventSource(r.of(Queue), { batchSize })],\n});\n\nexport const statusRoute = (path: string): ActionTrait<Function> => ({\n  name: `status-route-${path}`,\n  type: \"action\",\n  run: (fn, _r) => {\n    const api = Stack.of(fn).node\n      .findAll()\n      .find((c): c is HttpApi => c instanceof HttpApi);\n    api?.addRoutes({\n      path,\n      methods: [HttpMethod.GET],\n      integration: new HttpLambdaIntegration(path, fn),\n    });\n  },\n});\n\nexport const workerVisibility: PropertyTrait = {\n  name: \"visibility-30s\",\n  type: \"property\",\n  value: { visibilityTimeout: Duration.seconds(30) },\n};\n```\n\n```ts\n// stack.ts\nimport { compose } from \"@arts-n-crafts/cdk-composition\";\nimport {\n  nodeRuntime,\n  withDeadLetterQueue,\n  withSqsEventSource,\n  statusRoute,\n  workerVisibility,\n} from \"./traits/worker\";\n\ncompose(Function, [nodeRuntime, withDeadLetterQueue, withSqsEventSource(), statusRoute(\"/worker/status\")])\n  .and(Queue, [workerVisibility])\n  .buildConstruct(this, \"Worker\");\n```\n\nThe stack file says what exists and what it can do. The trait file says how each capability is implemented. Neither knows about the other's internals.\n\n---\n\n## API\n\n### `compose(ctor, traits?, id?)`\n\nStarts a new `Composition` with one entry. `id` defaults to the construct's class name.\n\n### `Composition.and(ctor, traits?, id?)`\n\nAppends a sibling entry. Returns a **new** `Composition` — the original is unchanged.\n\n### `Composition.buildConstruct(scope, id)`\n\nMaterialises the composition inside a `Construct` of its own, created in `scope` under `id`. The entries' ids only need to be unique within the composition — the usual choice inside a stack.\n\n### `Composition.buildFlat(scope)`\n\nMaterialises the composition directly in `scope`, without a construct of its own. Stacks need this to keep their names, since only a direct child of an `App` or `Stage` is named after its id:\n\n```ts\ncompose(CoreStack, [...], 'Core').and(EdgeStack, [...], 'Edge').buildFlat(app);\n```\n\nThe entries share `scope` with everything else in it, so their ids must be unique there. A construct's logical ids change when it moves between `buildFlat` and `buildConstruct`.\n\nBoth return the root, the constructs, a lookup, and each construct under its own id:\n\n| Member | Returns |\n|--------|---------|\n| `root` | Where the entries were created: the wrapping construct, or `scope` itself for `buildFlat`. |\n| `constructs` | The created constructs as a typed tuple, in declaration order. |\n| `resources` | A `Resources` lookup over the same constructs. |\n| *`<id>`* | The construct created under that id — typed for every id the composition declared literally. |\n\n`root`, `constructs` and `resources` cannot be used as entry ids; building throws if one is.\n\n### `Composition.build(scope, id?)` — deprecated\n\nCalls `buildConstruct(scope, id)` when given an `id` and `buildFlat(scope)` without one. Use either directly, so the choice between them is visible.\n\n### `Resources`\n\nPassed to trait callbacks and returned by `buildConstruct()` and `buildFlat()`.\n\n| Member | Returns |\n|--------|---------|\n| `of(Class)` | The single construct of that class. Throws if absent or ambiguous. |\n| `all(Class)` | Every construct of that class, in declaration order. |\n| `get(id)` | The construct under that id. Typed as that entry's class — and never `undefined` — for an id the composition declared literally; `Construct \\| undefined` otherwise. |\n| `get(id, Class)` | The same, narrowed to `Class` by an `instanceof` check. A different class reads as `undefined`, same as a missing id. |\n| `has(id)` | Whether an id exists. |\n| `values()` | Every construct created so far. |\n\n#### Why `get` is only sometimes typed\n\nA composition tracks the ids it was given, so building can hand them back typed:\n\n```ts\nconst { resources } = compose(HttpApi, [], \"Api\").and(LogGroup, [], \"AccessLogs\").buildConstruct(this, \"Gateway\");\n\nresources.get(\"Api\").apiEndpoint;      // HttpApi — no `?` needed, buildConstruct() created it\nresources.get(\"AccessLogs\").logGroupArn; // LogGroup\nresources.get(\"Nope\");                 // Construct | undefined\n```\n\nTwo cases stay untyped — and, for the same reason, absent from the build result's named entries — because the id is genuinely not knowable at compile time:\n\n- **A defaulted id.** It is derived from the class name at runtime; `ctor.name` is `string` for every class, so the type system cannot read `\"Queue\"` out of `typeof Queue`.\n- **A `string` variable as the id.** Nothing to bind.\n\nTrait callbacks also receive the untyped lookup. A trait is written alongside its own entry, before the rest of the chain exists to be inferred from, so there are no ids to bind against. Inside a trait, reach for `of(Class)`: it is typed, and it throws with an explanatory message — `No Bucket in this composition.` — rather than yielding `undefined`.\n\n### Trait types\n\n| Type | Purpose | Runs |\n|------|---------|------|\n| `PropertyTrait<Props>` | Merges props before instantiation. `value` is a plain object or `(resources) => object`. | Phase 1, when the entry is first resolved |\n| `MethodTrait<Construct>` | Calls a method. Name and arguments are both checked. | Phase 2, declaration order |\n| `ActionTrait<Construct>` | Runs arbitrary logic. `run` receives the concrete construct and the lookup. | Phase 2, declaration order |\n\nAll traits carry a `name` field. It has no functional effect — it documents intent and is the unit of granularity at code review.\n\n---\n\n## Releasing\n\nThe package is published to npm as [`@arts-n-crafts/cdk-composition`](https://www.npmjs.com/package/@arts-n-crafts/cdk-composition) by `.github/workflows/publish.yml`.\n\n`bun run build` runs [tsup](https://tsup.egoist.dev), which produces `dist/`: a CommonJS bundle (`index.js`), an ESM bundle (`index.mjs`) and a single bundled declaration file per format. `aws-cdk-lib` and `constructs` stay external. Only `dist/` is published — `src/` and the tests are not.\n\nDeclarations are bundled rather than emitted file-by-file, so the shipped `.d.ts` has no relative imports and resolves under every `moduleResolution` setting. `src/` therefore keeps plain extensionless specifiers, with no build concern leaking into it.\n\nTo cut a release:\n\nRun the **Publish** workflow from the Actions tab and pick a bump — `patch`, `minor` or `major`. That is the whole procedure: nothing is bumped or tagged by hand.\n\nThe workflow applies the bump, refuses to continue if the resulting tag already exists or the version is already on npm, runs lint, the tests and the build, pushes the bump to `main`, publishes, and creates the `vX.Y.Z` tag and the GitHub release with generated notes.\n\nThe order of those last three steps is deliberate. The bump is pushed *before* the publish, so a push that fails — a protected branch, another commit landing first — leaves nothing published and `main` untouched; the reverse order can put a version on npm that the repository has no record of, which cannot be undone. The release is created *after* the publish, so a failed publish never leaves a release pointing at a version npm does not have.\n\nIf a run pushes its bump and then fails to publish, re-run with bump `none`: it publishes the version already in `package.json` rather than bumping again.\n\nTick **dry run** to take the same pipeline as far as `npm publish --dry-run` and stop before pushing, publishing or tagging.\n\n### Trusted publishing\n\nThe workflow authenticates to npm with [trusted publishing](https://docs.npmjs.com/trusted-publishers) — GitHub's OIDC token is exchanged for short-lived credentials, so there is no `NPM_TOKEN` secret to store or rotate. That is why the job requests `id-token: write`, runs on a GitHub-hosted runner, and runs on Node 24 (trusted publishing needs Node >= 22.14.0 and npm >= 11.5.1, and Node 24 bundles npm 11.17).\n\nThe trusted publisher is already configured, against this repository and the `publish.yml` workflow filename. Renaming that workflow file breaks the match and publishes start failing — the configuration on npmjs.com has to be updated to the new name at the same time.\n\nnpm can only attach a trusted publisher to a package that already exists, so v0.1.0 was published once with a short-lived token, which has since been revoked. That bootstrap is not repeatable and not needed again.\n\nReleases go through the workflow with no credentials in the repository. npm attaches a provenance attestation automatically on a trusted publish, which is why `publishConfig` does not set `provenance` — that flag makes `npm publish` fail anywhere it cannot generate provenance, including a local run.\n","readmeFilename":"README.md"}