diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..20a32b1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bullseye", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "nodeGypDependencies": true, + "version": "20.18.3", + "nvmVersion": "latest" + }, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install -g npm@8" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 08ed839..0000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -node_modules/**/*.js -dist -lib -tmp diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 5972865..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,41 +0,0 @@ -const prettier = require('./.prettierrc'); - -module.exports = { - extends: ['eslint-config-standard', 'plugin:prettier/recommended'], - rules: { - 'max-len': ['error', { code: prettier.printWidth, ignoreUrls: true }], // KEEP THIS IN SYNC - strict: 0, - 'arrow-parens': ['error', 'always'], - 'consistent-return': 0, - 'no-param-reassign': 0, - 'func-names': 0, - 'no-use-before-define': 0, - 'one-var': 0, - 'prefer-destructuring': 0, - 'no-template-curly-in-string': 0, - 'prefer-template': 0, - 'prefer-const': 0, - 'promise/avoid-new': 0, - 'promise/always-return': 0, - 'promise/no-nesting': 0, - 'promise/no-return-wrap': 0, - 'promise/no-callback-in-promise': 0, - 'promise/no-promise-in-callback': 0, - semi: ['error', 'always'], - // 'comma-dangle': ['error', 'always-multiline'], - }, - overrides: [ - { - files: ['t/**/*.js'], - plugins: ['mocha'], - env: { - mocha: true, - node: true, - }, - rules: { - 'mocha/valid-suite-description': 0, - 'mocha/valid-test-description': 0, - }, - }, - ], -}; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 66e9e33..2eac9a4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: monken +github: nmccready diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d1a7a95..35cda2d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,14 @@ updates: - package-ecosystem: "npm" directory: "/" # Location of package manifests schedule: - interval: "weekly" + interval: "daily" + cooldown: + default-days: 7 + groups: + all: + patterns: + - "*" + commit-message: # force conventional commits standard + prefix: fix + prefix-development: chore + include: scope diff --git a/.github/workflows/auto-merge-dependabot.yml_disable b/.github/workflows/auto-merge-dependabot.yml_disable new file mode 100644 index 0000000..030d6c6 --- /dev/null +++ b/.github/workflows/auto-merge-dependabot.yml_disable @@ -0,0 +1,13 @@ +name: Auto-merge Dependabot +on: pull_request + +jobs: + automerge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - uses: peter-evans/enable-pull-request-automerge@v8 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + pull-request-number: ${{ github.event.pull_request.number }} + merge-method: squash diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 179ead3..3de1eb6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,28 +1,28 @@ -# TODO: ENABLE / UNCOMMENT WHEN NPM_TOKEN IS SET in secrets REPO -# name: publish +name: publish -# on: -# push: -# tags: -# - "v*" +on: + push: + tags: + - "v*" -# jobs: -# tests: -# uses: ./.github/workflows/tests.yml -# publish-npm: -# needs: [tests] -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v4 -# - name: Use Node.js ${{ matrix.node-version }} -# uses: actions/setup-node@v3 -# with: -# node-version: '20.x' -# registry-url: 'https://registry.npmjs.org' -# - name: Publish to npm -# run: | -# npm install -# npm publish --access public -# env: -# NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -# #kick +jobs: + tests: + uses: ./.github/workflows/tests.yml + publish-npm: + needs: [tests] + runs-on: ubuntu-latest + permissions: + id-token: write # Required for OIDC + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + - name: Publish to npm + run: | # npm 11.15.1 for OIDC support + npm install -g npm@11 + npm ci + npm install + npm publish --access public diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa6751f..7591cba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,38 +1,61 @@ # TODO: ENABLE / UNCOMMENT WHEN NPM_TOKEN IS SET in secrets REPO -# name: release - -# on: -# push: -# branches: ["master"] -# tags-ignore: ['**'] - -# jobs: -# tests: -# uses: ./.github/workflows/tests.yml -# tag-release: -# runs-on: ubuntu-latest -# needs: [tests] -# steps: -# - uses: actions/checkout@v4 -# with: # important, must be defined on checkout to kick publish (defining in setup/node doesn't work) -# token: ${{ secrets.GH_TOKEN }} -# - name: Use Node.js ${{ matrix.node-version }} -# uses: actions/setup-node@v3 -# with: -# node-version: '20.x' -# # cache: "npm" # needs lockfile if enabled - -# - name: tag release -# run: | -# # ignore if commit message is chore(release): ... -# if [[ $(git log -1 --pretty=%B) =~ ^chore\(release\):.* ]]; then -# echo "Commit message starts with 'chore(release):', skipping release" -# exit 0 -# fi -# git config --local user.email "creadbot@github.com" -# git config --local user.name "creadbot_github" -# set -v -# npm install -# npx commit-and-tag-version -# git push -# git push --tags +name: release + +on: + push: + branches: ["master"] + paths-ignore: + - '.devcontainer/**' + tags-ignore: ['**'] + +jobs: + tests: + uses: ./.github/workflows/tests.yml + tag-release: + runs-on: ubuntu-latest + needs: [tests] + steps: + - uses: actions/checkout@v4 + with: + # important, must be defined on checkout to kick publish + token: ${{ secrets.GH_TOKEN }} + # Full history needed for conventional-changelog to detect breaking changes + fetch-depth: 0 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.x' + + - name: tag release + run: | + # ignore if commit message is chore(release): ... + if [[ $(git log -1 --pretty=%B) =~ ^chore\(release\):.* ]]; then + echo "Commit message starts with 'chore(release):', skipping release" + exit 0 + fi + + git config --local user.email "creadbot@github.com" + git config --local user.name "creadbot_github" + + npm install + + # Check for breaking changes in commits since last tag + # Look for feat!:, fix!:, or BREAKING CHANGE in commit messages + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$LAST_TAG" ]; then + RANGE="$LAST_TAG..HEAD" + else + RANGE="HEAD" + fi + + # Check all commits (not just first-parent) for breaking changes + if git log $RANGE --format="%B" | grep -qE "(^[a-z]+!:|BREAKING CHANGE:)"; then + echo "Breaking change detected, forcing major version bump" + npx commit-and-tag-version --release-as major + else + npx commit-and-tag-version + fi + + git push + git push --tags diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60b63e9..f6e4f3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,8 @@ name: tests on: workflow_call: + push: + branches: ["master"] pull_request: branches: ["master"] @@ -9,7 +11,7 @@ jobs: test: strategy: matrix: - node-version: ['18.x', '20.x', '22.x'] + node-version: ['20.x', '22.x', '24.x'] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index dbf2185..cf3ce67 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ tmp temp yarn.lock -package-lock.json +# package-lock.json # OIDC +dist/ diff --git a/.versionrc.json b/.versionrc.json new file mode 100644 index 0000000..f4ba5e9 --- /dev/null +++ b/.versionrc.json @@ -0,0 +1,13 @@ +{ + "types": [ + {"type": "feat", "section": "Features"}, + {"type": "fix", "section": "Bug Fixes"}, + {"type": "perf", "section": "Performance"}, + {"type": "refactor", "section": "Refactor"}, + {"type": "test", "section": "Tests"}, + {"type": "docs", "section": "Documentation"}, + {"type": "chore", "hidden": true} + ], + "commitUrlFormat": "https://github.com/brickhouse-tech/cfn-include/commit/{{hash}}", + "compareUrlFormat": "https://github.com/brickhouse-tech/cfn-include/compare/{{previousTag}}...{{currentTag}}" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b45278..5cf21c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,276 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. -## [2.0.0](https://github.com/monken/cfn-include/compare/v1.4.1...v2.0.0) (2024-08-24) +## [4.2.0](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.8...v4.2.0) (2026-02-18) + + +### Features + +* add template stats and oversized template warnings (Phase 1 of [#90](https://github.com/brickhouse-tech/cfn-include/issues/90)) ([9ec2a97](https://github.com/brickhouse-tech/cfn-include/commit/9ec2a9775a37fb3d4d5d767d902d2ee330181da3)) + + +### Refactor + +* export CFN limits as true constants, use in tests ([95dbb8b](https://github.com/brickhouse-tech/cfn-include/commit/95dbb8b6a86c3b87498dde187e92739bc6acf635)) + + +### Documentation + +* add AWS CloudFormation limits doc references to stats constants ([03e0a76](https://github.com/brickhouse-tech/cfn-include/commit/03e0a764005c8bf8189cc91f6e66fd9873e329ea)) +* add Phase 5 template stats doc, move completed phases to docs/completed/ ([141d950](https://github.com/brickhouse-tech/cfn-include/commit/141d950c2d88e5d4f4d3b3630276f57bc02e6223)) +* renumber phases — stats is Phase 4, CDK is Phase 5 ([957f370](https://github.com/brickhouse-tech/cfn-include/commit/957f3706d5c7797b12916754c73fb16d6d36bfa0)) + +## [4.1.8](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.7...v4.1.8) (2026-02-18) + +## [4.1.7](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.6...v4.1.7) (2026-02-17) + +## [4.1.6](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.5...v4.1.6) (2026-02-17) + + +### Bug Fixes + +* **deps:** bump the all group with 5 updates ([0a65846](https://github.com/brickhouse-tech/cfn-include/commit/0a658461a4c816f2ba14d33edfaf23fea3e14fb7)) + +## [4.1.5](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.4...v4.1.5) (2026-02-16) + + +### Bug Fixes + +* **deps:** bump the all group with 2 updates ([f1c45aa](https://github.com/brickhouse-tech/cfn-include/commit/f1c45aac6026ae3ebb8769943e671c63542282a1)) + +## [4.1.4](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.3...v4.1.4) (2026-02-15) + + +### Refactor + +* add recursion depth tracking and limit (MAX_RECURSE_DEPTH=100) ([05fba95](https://github.com/brickhouse-tech/cfn-include/commit/05fba95f2b50e4251d97d2bfbc15a1bed9a168e2)) +* extract all Fn:: handlers into modular files under src/lib/functions/ ([b70eccb](https://github.com/brickhouse-tech/cfn-include/commit/b70eccb22bffeb49492d05f6cd177d21a61b39ba)) + +## [4.1.3](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.2...v4.1.3) (2026-02-15) + +## [4.1.2](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.1...v4.1.2) (2026-02-14) + + +### Bug Fixes + +* **deps:** add globals and @eslint/js for eslint 10 ([3d6a39a](https://github.com/brickhouse-tech/cfn-include/commit/3d6a39a814827de1f751f9bcb3954d76b9052ed9)) +* **deps:** bump the all group across 1 directory with 7 updates ([ddd66b6](https://github.com/brickhouse-tech/cfn-include/commit/ddd66b676be60bf5e53a4431e03855e462351e26)) + +## [4.1.1](https://github.com/brickhouse-tech/cfn-include/compare/v4.1.0...v4.1.1) (2026-02-14) + +## [4.1.0](https://github.com/brickhouse-tech/cfn-include/compare/v4.0.1...v4.1.0) (2026-02-14) + + +### Features + +* migrate tests from Mocha to Vitest + TypeScript (Phase 3b) ([47ccd3d](https://github.com/brickhouse-tech/cfn-include/commit/47ccd3d385a24e7689e5971bcb711950e39bc645)) + +## [4.0.1](https://github.com/brickhouse-tech/cfn-include/compare/v4.0.0...v4.0.1) (2026-02-12) +### Bug Fixes + +* **deps:** bump the all group across 1 directory with 7 updates ([56989d3](https://github.com/brickhouse-tech/cfn-include/commit/56989d3f2de9c50bc5968b4cafcb059b887b96f4)) + +## [4.0.0](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.24...v4.0.0) (2026-02-08) + + +### ⚠ BREAKING CHANGES + +* **release:** Package is now ESM-only. CommonJS require() no longer works. + +- Fix version bump (should have been 3.0.0 due to ESM breaking change) +- Update CHANGELOG.md with Phase 1-3 changes +- Add .versionrc.json for commit-and-tag-version config + +* **release:** 3.0.0 ([3d0bf89](https://github.com/brickhouse-tech/cfn-include/commit/3d0bf89a39d73d0f9645f96b6d56bd3ec0dce6a0)) + + +### Bug Fixes + +* **ci:** detect breaking changes in merge commits ([09f848c](https://github.com/brickhouse-tech/cfn-include/commit/09f848c434d9b287283fd9c48ed8e48d19a3cbd1)) + +## [3.0.0](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.24...v3.0.0) (2026-02-08) + +### ⚠ BREAKING CHANGES + +* **ESM-only:** Package is now ESM-only. CommonJS `require()` no longer works. Use dynamic `import()` or migrate to ESM. + +### Features + +* **Phase 3a:** add TypeScript source with build pipeline ([4b576b7](https://github.com/brickhouse-tech/cfn-include/commit/4b576b7)) + - Create src/ directory structure (lib/, types/, lib/include/) + - Add comprehensive type definitions in src/types/ + - Convert 13 lib files to TypeScript + - Add main src/index.ts with all Fn:: handlers + - Configure TypeScript (strict mode, ES2022, NodeNext) + +## [2.1.24](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.23...v2.1.24) (2026-02-08) + +*Released in error - see 3.0.0* + +## [2.1.23](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.22...v2.1.23) (2026-02-08) + +*No notable changes* + +## [2.1.22](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.21...v2.1.22) (2026-02-08) + +### ⚠ BREAKING CHANGES + +* **ESM:** Package converted to ES Modules. CommonJS `require()` no longer works. + +### Features + +* convert to ES Modules (ESM) ([0ceb431](https://github.com/brickhouse-tech/cfn-include/commit/0ceb431)) + +### Bug Fixes + +* convert benchmark runner to ESM ([ba70ddb](https://github.com/brickhouse-tech/cfn-include/commit/ba70ddb)) +* rename config files to .cjs for ESM compatibility ([d77608a](https://github.com/brickhouse-tech/cfn-include/commit/d77608a)) + +## [2.1.21](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.20...v2.1.21) (2026-02-08) + +### Refactor + +* remove bluebird and path-parse dependencies ([9ae7a59](https://github.com/brickhouse-tech/cfn-include/commit/9ae7a59)) + +## [2.1.20](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.19...v2.1.20) (2026-02-08) + +### Performance + +* **Phase 1 optimizations:** + - async glob for Fn::Include ([6409511](https://github.com/brickhouse-tech/cfn-include/commit/6409511)) + - file content cache for Fn::Include ([0724a23](https://github.com/brickhouse-tech/cfn-include/commit/0724a23)) + - replace simple lodash calls with native alternatives ([0ad6df6](https://github.com/brickhouse-tech/cfn-include/commit/0ad6df6)) + - regex pre-compilation cache in replaceEnv ([34f2e93](https://github.com/brickhouse-tech/cfn-include/commit/34f2e93)) + - Object.create() for O(1) scope creation in Fn::Map ([22aae96](https://github.com/brickhouse-tech/cfn-include/commit/22aae96)) + +### Tests + +* add regression test suite for Phase 1 optimizations ([def9fd2](https://github.com/brickhouse-tech/cfn-include/commit/def9fd2)) + +## [2.1.19](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.18...v2.1.19) (2026-02-08) + +### Bug Fixes + +* lint errors in benchmark-runner.js (trailing commas) ([8257735](https://github.com/brickhouse-tech/cfn-include/commit/8257735)) + +### Documentation + +* clarify scope vs body cloning optimization strategy ([4c3ddc1](https://github.com/brickhouse-tech/cfn-include/commit/4c3ddc1)) +* add Phase 3 TypeScript Analysis ([3758d4d](https://github.com/brickhouse-tech/cfn-include/commit/3758d4d)) +* add Phase 4 CDK Integration Analysis ([38a7090](https://github.com/brickhouse-tech/cfn-include/commit/38a7090)) + +### Features + +* **benchmarks:** add Phase 1 performance analysis and benchmark suite ([7bb7670](https://github.com/brickhouse-tech/cfn-include/commit/7bb7670)) + +## [2.1.18](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.17...v2.1.18) (2026-02-04) + +*No notable changes* + +## [2.1.17](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.16...v2.1.17) (2026-02-04) + +### Bug Fixes + +* **deps:** bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 ([92f242e](https://github.com/brickhouse-tech/cfn-include/commit/92f242e)) +* **deps:** bump the all group with 2 updates ([c10adc7](https://github.com/brickhouse-tech/cfn-include/commit/c10adc7)) + +## [2.1.16](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.15...v2.1.16) (2026-01-30) + +### Security + +* CVE fast-xml-parse 5.3.4 override ([ca5c7cc](https://github.com/brickhouse-tech/cfn-include/commit/ca5c7cc)) + +## [2.1.15](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.14...v2.1.15) (2026-01-30) + +### Bug Fixes + +* **deps:** bump the all group with 3 updates ([f0c5bd2](https://github.com/brickhouse-tech/cfn-include/commit/f0c5bd2)) + +## [2.1.14](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.13...v2.1.14) (2026-01-27) + +### Bug Fixes + +* **deps:** bump @znemz/cft-utils from 0.1.30 to 0.1.31 in the all group ([06669aa](https://github.com/brickhouse-tech/cfn-include/commit/06669aa)) + +## [2.1.13](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.12...v2.1.13) (2026-01-22) + +### Bug Fixes + +* Fn::RefNow bug fixes ([f42119e](https://github.com/brickhouse-tech/cfn-include/commit/f42119e)) + +## [2.1.12](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.11...v2.1.12) (2026-01-22) + +### Features + +* RefNow LogicalId support ([2a01d82](https://github.com/brickhouse-tech/cfn-include/commit/2a01d82)) + +## [2.1.11](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.10...v2.1.11) (2026-01-22) + +### Features + +* refNowIgnoreMissing and refNowIgnores for cli for passthrough ([cfd4740](https://github.com/brickhouse-tech/cfn-include/commit/cfd4740)) + +## [2.1.10](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.9...v2.1.10) (2026-01-22) + +### Bug Fixes + +* **deps:** bump lodash from 4.17.21 to 4.17.23 ([f45ef98](https://github.com/brickhouse-tech/cfn-include/commit/f45ef98)) + +## [2.1.9](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.8...v2.1.9) (2026-01-17) + +### Features + +* Fn::RefNow ([81e6867](https://github.com/brickhouse-tech/cfn-include/commit/81e6867)) +* Fn::SubNow ([45a1cbc](https://github.com/brickhouse-tech/cfn-include/commit/45a1cbc)) + +## [2.1.8](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.7...v2.1.8) (2025-12-24) + +### Chore + +* npm publish OIDC ([bef7a21](https://github.com/brickhouse-tech/cfn-include/commit/bef7a21)) + +## [2.1.7](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.6...v2.1.7) (2025-12-12) + +### Bug Fixes + +* **deps:** bump the all group across 1 directory with 2 updates ([4d2266e](https://github.com/brickhouse-tech/cfn-include/commit/4d2266e)) + +## [2.1.6](https://github.com/brickhouse-tech/cfn-include/compare/v2.1.5...v2.1.6) (2025-11-21) + +### Bug Fixes + +* safeDump to dump ([98393d1](https://github.com/nmccready/cfn-include/commit/98393d1902a7ee681bc5bd214a244a65c0222bec)) + +## 2.1.5 (2025-11-18) + +## 2.1.4 (2025-11-06) + +## 2.1.3 (2025-09-01) + +## 2.1.2 (2025-05-12) + +## 2.1.1 (2025-02-26) + +## [2.1.0](https://github.com/brickhouse-tech/cfn-include/compare/v2.0.2...v2.1.0) (2025-01-21) + +### Features + +* env file support CFN_INCLUDE_(DO_ENV|DO_EVAL) ([8e6208d](https://github.com/nmccready/cfn-include/commit/8e6208d4762710268da2a2e011576f341a3986d3)) + +## [2.0.2](https://github.com/nmccready/cfn-include/compare/v2.0.1...v2.0.2) (2025-01-21) + +## [2.0.1](https://github.com/nmccready/cfn-include/compare/v2.0.0...v2.0.1) (2024-11-14) + +### Bug Fixes + +* dependency bump CVE serve ([eed7ac5](https://github.com/nmccready/cfn-include/commit/eed7ac5de3dbb5a0607d8966d1c220857b8cc636)) +* **handleIncludeBody:** loopTemplate pass on option doEval ([95dd1a0](https://github.com/nmccready/cfn-include/commit/95dd1a0059fbf4ac37e445cd407c5baec2c3792a)) +* scoped to @znemz/cfn-include to publish 2.0.0 ([492e479](https://github.com/nmccready/cfn-include/commit/492e479a8fa8c1e15a33ce3a7962a7cca5affb94)) + +## [2.0.0](https://github.com/monken/cfn-include/compare/v1.4.1...v2.0.0) (2024-08-24) + ### ⚠ BREAKING CHANGES * capital one features and more Fn::* @@ -15,7 +282,6 @@ All notable changes to this project will be documented in this file. See [commit * cli added --context to allow stdin to work with includes ([ee33ba9](https://github.com/monken/cfn-include/commit/ee33ba95bee24ce04b262001f05951947621b27d)) * cli added --context to allow stdin to work with includes ([7f6986f](https://github.com/monken/cfn-include/commit/7f6986fb34dad85c700ecccd70ec2f49895b2523)) - ### Bug Fixes * cve globby issue resolved via glob ([7e27d12](https://github.com/monken/cfn-include/commit/7e27d1272996ead317ab6448e672f4787a3d882b)) diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 0000000..5137ebc --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1 @@ +github: [nmccready] \ No newline at end of file diff --git a/README.md b/README.md index 2f34673..d8251e5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ For example, [`Fn::Include`](#fninclude) provides a convenient way to include fi - [Fn::Eval](#fneval) - [Fn::IfEval](#fnifeval) - [Fn::JoinNow](#fnjoinnow) + - [Fn::SubNow](#fsubnow) + - [Fn::RefNow](#frefnow) - [Fn::ApplyTags](#fnapplytags) - [Fn::Outputs](#fnoutputs) - [More Examples](#more-examples) @@ -58,7 +60,7 @@ Tag-based syntax is available in YAML templates. For example,`Fn::Include` becom You can either install `cfn-include` or use a web service to compile templates. ``` -npm install --global cfn-include +npm install --global @znemz/cfn-include ``` The web service can be called with your favorite CLI tool such as `curl`. @@ -91,7 +93,10 @@ Options: * `--enable` different options / toggles: ['env','eval'] [string] [choices: 'env','eval','env.eval' etc...] * `env` pre-process env vars and inject into templates as they are processed looks for $KEY or ${KEY} matches * `-i, --inject` JSON string payload to use for template injection. (Takes precedence over process.env (if enabled) injection and will be merged on top of process.env) -* `--doLog` console log out include options in recurse step. +* `--doLog` console log out include options in recurse step. Shows caller parameter to aid debugging nested function calls. +* `--ref-now-ignore-missing` do not fail if `Fn::RefNow` reference cannot be resolved; instead return in standard CloudFormation `Ref` syntax +* `--ref-now-ignores ` comma-separated list of reference names to ignore if not found (e.g., `OptionalRef1,OptionalRef2`) +* `--stats` report template statistics and CloudFormation limit warnings to stderr. Includes resource count, output count, template size (with % of CloudFormation limits), and a breakdown of resource types. Warnings are emitted when any metric reaches 80% of its limit. Output goes to stderr so it does not interfere with template output on stdout. `cfn-include` also accepts a template passed from stdin ``` @@ -123,7 +128,7 @@ This is what the `userdata.sh` looks like: ```bash cfn-include synopsis.json > output.template # you can also compile remote files -cfn-include https://raw.githubusercontent.com/monken/cfn-include/master/examples/synopsis.json > output.template +cfn-include https://raw.githubusercontent.com/nmccready/cfn-include/master/examples/synopsis.json > output.template ``` The output will be something like this: @@ -1071,6 +1076,211 @@ Fn::JoinNow: arn:aws:s3:::c1-acme-iam-cache-engine-${AWS::AccountId}-us-east-1$CFT_STACK_SUFFIX ``` +## Fn::SubNow + +`Fn::SubNow` performs immediate string substitution similar to AWS CloudFormation's `Fn::Sub`, but evaluates during template preprocessing rather than at stack creation time. It supports variable substitution using `${VariableName}` syntax and AWS pseudo-parameters. + +The function supports two input formats: + +**String format:** +```yaml +Fn::SubNow: "arn:aws:s3:::bucket-${BucketSuffix}" +``` + +**Array format with variables:** +```yaml +Fn::SubNow: + - "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${LogGroupName}" + - LogGroupName: /aws/lambda/my-function +``` + +**Supported AWS pseudo-parameters:** +- `${AWS::AccountId}` - AWS Account ID +- `${AWS::Region}` - AWS Region +- `${AWS::StackName}` - Stack name +- `${AWS::StackId}` - Stack ID +- `${AWS::Partition}` - AWS Partition (e.g., 'aws') +- `${AWS::URLSuffix}` - URL suffix (e.g., 'amazonaws.com') + +Variables can be provided via: +1. The `inject` option passed to `cfn-include` +2. Explicit variables in the array format (takes precedence over `inject`) +3. Environment variables when using the `doEnv` option + +**Example with environment variables:** +```yaml +BucketName: !SubNow "my-bucket-${Environment}-${AWS::Region}" +``` + +With `CFN_INCLUDE_DO_ENV=true` and environment variable `ENVIRONMENT=prod`, this resolves to: +```yaml +BucketName: "my-bucket-prod-us-east-1" +``` + +## Fn::RefNow + +`Fn::RefNow` resolves a reference immediately during template preprocessing, similar to AWS CloudFormation's `Fn::Ref` but evaluated at processing time rather than stack creation time. It resolves references to parameters, variables, and AWS pseudo-parameters. + +**Basic syntax:** +```yaml +BucketRef: + Fn::RefNow: BucketName +``` + +or using YAML tag syntax: +```yaml +BucketRef: !RefNow BucketName +``` + +**Supported AWS pseudo-parameters:** +- `AWS::AccountId` - AWS Account ID (environment: `AWS_ACCOUNT_ID` or `AWS_ACCOUNT_NUM`, fallback: `${AWS::AccountId}`) +- `AWS::Region` - AWS Region (environment: `AWS_REGION`, fallback: `${AWS::Region}`) +- `AWS::StackName` - Stack name (environment: `AWS_STACK_NAME`, fallback: `${AWS::StackName}`) +- `AWS::StackId` - Stack ID (environment: `AWS_STACK_ID`, fallback: `${AWS::StackId}`) +- `AWS::Partition` - AWS Partition (environment: `AWS_PARTITION`, default: `'aws'`) +- `AWS::URLSuffix` - URL suffix (environment: `AWS_URL_SUFFIX`, default: `'amazonaws.com'`) +- `AWS::NotificationARNs` - SNS topic ARNs for notifications (environment: `AWS_NOTIFICATION_ARNS`, fallback: `${AWS::NotificationARNs}`) + +**Reference resolution priority:** +1. AWS pseudo-parameters (with environment variable fallbacks) +2. Variables from the `inject` option +3. Variables from the current scope (useful with `Fn::Map`) + +**Reference indirection:** +If a resolved reference is a string, it will be treated as a reference name and resolved again. This enables reference chaining, useful when using `Fn::RefNow` with `Fn::Map`: + +```yaml +Fn::Map: + - [BucketVar1, BucketVar2] + - BucketName: + Fn::RefNow: _ +``` + +With `inject: { BucketVar1: "my-bucket-1", BucketVar2: "my-bucket-2" }`, this via + +`$ Bucket1=my-bucket-1 BucketVar2=my-bucket-2 cnf-include examples/refNow.yml --enable env,eval` via exports / env + +or + +`$ cnf-include examples/refNow.yml --enable env,eval --inject '{"BucketVar1":"my-bucket-1","BucketVar2":"my-bucket-2"}'` via inject + +resolves to: +```yaml +BucketVar1: my-bucket-1 +BucketVar2: my-bucket-2 +``` + +The `_` placeholder resolves to `"BucketVar1"` or `"BucketVar2"`, which are then resolved again to their actual values. + +**Example with injected variables:** +```yaml +Resources: + Bucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::RefNow: BucketName +``` + +With `inject: { BucketName: "my-app-bucket" }`, this resolves to: +```yaml +BucketName: "my-app-bucket" +``` + +**Example with AWS pseudo-parameters:** +```yaml +LogGroup: + Fn::RefNow: AWS::StackName +``` + +With `AWS_STACK_NAME=my-stack`, this resolves to: +```yaml +LogGroup: "my-stack" +``` + +**Unresolved AWS pseudo-parameters:** +If an AWS pseudo-parameter is not set via environment variables, it falls back to a placeholder string (e.g., `${AWS::AccountId}`). Only `AWS::Partition` and `AWS::URLSuffix` have hardcoded defaults since they are rarely environment-specific. + +**Error handling:** +If a reference cannot be resolved, `Fn::RefNow` will throw an error. Ensure all referenced parameters and variables are available via inject, scope, or environment variables. + +**Resolving LogicalResourceIds:** + +`Fn::RefNow` can also resolve references to CloudFormation Resource LogicalResourceIds, enabling you to construct ARNs or other resource-specific values during template preprocessing. When a reference matches a LogicalResourceId in the Resources section, `Fn::RefNow` will automatically generate the appropriate ARN based on the resource type and properties. + +**Supported Resource Types for ARN/Name Resolution:** + +- `AWS::IAM::ManagedPolicy` - Returns policy ARN (supports Path) +- `AWS::IAM::Role` - Returns role ARN (supports Path) +- `AWS::IAM::InstanceProfile` - Returns instance profile ARN (supports Path) +- `AWS::S3::Bucket` - Returns bucket ARN +- `AWS::Lambda::Function` - Returns function ARN +- `AWS::SQS::Queue` - Returns queue ARN +- `AWS::SNS::Topic` - Returns topic ARN +- `AWS::DynamoDB::Table` - Returns table ARN +- `AWS::RDS::DBInstance` - Returns DB instance ARN +- `AWS::SecretsManager::Secret` - Returns secret ARN +- `AWS::KMS::Key` - Returns key ARN + +Example with AWS::IAM::ManagedPolicy: + +```yaml +Resources: + ObjPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: teststack-CreateTestDBPolicy-16M23YE3CS700 + Path: /CRAP/ + + IAMRole: + Type: AWS::IAM::Role + Properties: + ManagedPolicyArns: + - Fn::RefNow: ObjPolicy # Resolves to: arn:aws:iam::${AWS_ACCOUNT_ID}:policy/CRAP/teststack-CreateTestDBPolicy-16M23YE3CS700 +``` + +**Returning Resource Names Instead of ARNs:** + +By default, `Fn::RefNow` returns the ARN for supported resource types. However, if the key name ends with `Name` (e.g., `RoleName`, `BucketName`, `FunctionName`), it automatically returns the resource name/identifier instead: + +```yaml +Resources: + MyRole: + Type: AWS::IAM::Role + Properties: + RoleName: MyExecutionRole + + RoleArn: + Fn::RefNow: MyRole # Returns: arn:aws:iam::${AWS_ACCOUNT_ID}:role/MyExecutionRole + + RoleName: + Fn::RefNow: MyRole # Returns: MyExecutionRole (because key ends with "Name") +``` + +This intuitive approach makes templates more readable and follows natural CloudFormation naming conventions. + +**CLI Options for Fn::RefNow:** + +The `cfn-include` command provides CLI options to control how unresolved references are handled: + +- `--ref-now-ignore-missing`: Do not fail if a `Fn::RefNow` reference cannot be resolved. Instead, the reference will be returned in AWS CloudFormation's standard `Ref` syntax (e.g., `{ Ref: 'UnresolvedRef' }`), allowing CloudFormation to resolve it at stack creation time. +- `--ref-now-ignores `: Comma-separated list of reference names to ignore if not found. These references will be returned in `Ref` syntax instead of throwing an error. + +**Example usage:** + +```bash +# Ignore all unresolved references +cfn-include template.yaml --ref-now-ignore-missing + +# Ignore specific reference names +cfn-include template.yaml --ref-now-ignores "OptionalRef1,OptionalRef2" + +# Combine both options +cfn-include template.yaml --ref-now-ignore-missing --ref-now-ignores "SpecificRef" +``` + +This is useful for templates that reference CloudFormation parameters or other resources that may not be available at template processing time but will be available at stack creation time. + ## Fn::ApplyTags See [ApplyTags test file](t/tests/applyTags.yml). @@ -1133,7 +1343,7 @@ Outputs: ## More Examples -See [/examples](https://github.com/monken/cfn-include/tree/master/examples) for templates that call an API Gateway endpoint to collect AMI IDs for all regions. There is also a good amount of [tests](https://github.com/monken/cfn-include/tree/master/t) that might be helpful. +See [/examples](https://github.com/nmccready/cfn-include/tree/master/examples) for templates that call an API Gateway endpoint to collect AMI IDs for all regions. There is also a good amount of [tests](https://github.com/nmccready/cfn-include/tree/master/t) that might be helpful. A common pattern is to process a template, validate it against the AWS [validate-template](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/validate-template.html) API, minimize it and upload the result to S3. You can do this with a single line of code: @@ -1163,8 +1373,124 @@ Options are query parameters. - `validate=false` do not validate template [true] +## Developer Documentation + +### Debug Logging with doLog + +The `cfn-include` preprocessor supports comprehensive debug logging for tracing template processing. When enabled, the `doLog` option logs all arguments at each recursion level in the template processing pipeline. + +#### Enabling doLog + +**CLI:** +```bash +cfn-include template.yaml --doLog +``` + +**Programmatic:** +```javascript +include({ + template: myTemplate, + url: 'file:///path/to/template.yaml', + doLog: true +}) +``` + +#### Understanding Caller Parameter + +The `recurse` function now includes a `caller` parameter to help identify which function triggered each recursion step. This is invaluable for debugging complex templates with nested function calls. The caller parameter provides a trace path like: + +- `recurse:isArray` - Processing an array +- `Fn::Map` - Inside an `Fn::Map` function +- `Fn::Include` - Inside an `Fn::Include` function +- `recurse:isPlainObject:end` - Final plain object processing +- `handleIncludeBody:json` - JSON body being processed + +When `doLog` is enabled, the console output will show the caller for each recursion: +```javascript +{ + base: {...}, + scope: {...}, + cft: {...}, + rootTemplate: {...}, + caller: "Fn::Map", + doEnv: false, + doEval: false, + ... +} +``` + +This makes it easy to trace execution flow through nested `Fn::Include`, `Fn::Map`, `Fn::RefNow`, and other functions. + +#### Example Debug Output + +```bash +$ cfn-include examples/base.template --doLog | head -50 +{ + base: { + protocol: 'file', + host: '/Users/SOME_USER/code', + path: '/examples/base.template' + }, + scope: {}, + cft: { AWSTemplateFormatVersion: '2010-09-09', ... }, + rootTemplate: { AWSTemplateFormatVersion: '2010-09-09', ... }, + caller: 'recurse:isPlainObject:end', + doEnv: false, + doEval: false, + inject: undefined, + doLog: true +} +``` + +### Fn::RefNow Improvements + +#### CLI Options for Reference Resolution + +Two new CLI options control how unresolved `Fn::RefNow` references are handled: + +- `--ref-now-ignore-missing`: Do not fail if a reference cannot be resolved. Instead, return the reference in CloudFormation's standard `Ref` syntax, allowing CloudFormation to resolve it at stack creation time. + +- `--ref-now-ignores `: Comma-separated list of specific reference names to ignore if not found. Useful for optional references. + +**Example usage:** +```bash +# Ignore all unresolved references +cfn-include template.yaml --ref-now-ignore-missing + +# Ignore specific references +cfn-include template.yaml --ref-now-ignores "OptionalParam,CustomRef" + +# Combine both +cfn-include template.yaml --ref-now-ignore-missing --ref-now-ignores "SpecificRef" +``` + +#### rootTemplate Parameter + +The `recurse` function now receives the complete `rootTemplate` for all recursion calls. This enables `Fn::RefNow` to resolve references to CloudFormation resources defined anywhere in the template, even when processing deeply nested includes or function results. + +### Fn::SubNow and Fn::JoinNow + +New intrinsic functions for immediate string substitution and joining: + +- `Fn::SubNow` - Performs immediate string substitution similar to `Fn::Sub`, but evaluates at template processing time +- `Fn::JoinNow` - Joins array elements into a string at template processing time + +See the main documentation sections above for detailed usage. + +### Template Processing Pipeline + +The template processing follows this call chain for better debugging: + +1. Entry point calls `recurse()` with `caller: undefined` +2. Array elements call `recurse()` with `caller: 'recurse:isArray'` +3. Each `Fn::*` function calls `recurse()` with `caller: 'Fn::FunctionName'` +4. Final plain object recursion uses `caller: 'recurse:isPlainObject:end'` +5. Include body processing uses `caller: 'handleIncludeBody:json'` + +When combined with `--doLog`, this provides complete visibility into how `cfn-include` processes your templates. + To compile the synopsis run the following command. ``` -curl -Ssf -XPOST https://api.netcubed.de/latest/template -d '{"Fn::Include":"https://raw.githubusercontent.com/monken/cfn-include/master/examples/synopsis.json"}' > output.template +curl -Ssf -XPOST https://api.netcubed.de/latest/template -d '{"Fn::Include":"https://raw.githubusercontent.com/nmccready/cfn-include/master/examples/synopsis.json"}' > output.template ``` diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..3128aee --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,49 @@ +# cfn-include Benchmark Suite + +Performance benchmarks for measuring template compilation times and memory usage. + +## Running Benchmarks + +```bash +# Basic run +node benchmarks/benchmark-runner.js + +# With GC exposed for accurate memory measurement +node --expose-gc benchmarks/benchmark-runner.js +``` + +## What's Measured + +1. **Simple Template** - Baseline with no custom Fn:: functions +2. **Fn::Map (10/100/1000 items)** - Tests scaling behavior +3. **Nested Fn::Map (3-deep)** - Tests recursion overhead +4. **Fn::Include chains (3/10-deep)** - Tests include resolution +5. **Glob operations (10/100 files)** - Tests file discovery +6. **Complex template** - Mixed real-world scenario + +## Output + +Results are printed to console and saved to `results.json`. + +## Fixtures + +The benchmark runner auto-generates fixtures in `fixtures/` directory: +- Simple templates +- Map templates with varying sizes +- Nested map templates +- Include chain templates +- Glob test files + +## Adding New Benchmarks + +Edit `benchmark-runner.js` and add a new `runBenchmark()` call: + +```javascript +results.push(await runBenchmark('My New Benchmark', async () => { + await include({ + template: myTemplate, + url: 'file:///path/to/template.json', + }); +})); +printResult(results[results.length - 1]); +``` diff --git a/benchmarks/baseline-before-quickwins.json b/benchmarks/baseline-before-quickwins.json new file mode 100644 index 0000000..e121b1d --- /dev/null +++ b/benchmarks/baseline-before-quickwins.json @@ -0,0 +1,78 @@ +{ + "timestamp": "2026-02-08T18:11:56.440Z", + "nodeVersion": "v22.22.0", + "platform": "darwin", + "arch": "arm64", + "results": [ + { + "name": "Simple Template (baseline)", + "avgMs": 0.3760584000000051, + "minMs": 0.16200000000000614, + "maxMs": 1.0092090000000127, + "memoryDeltaBytes": 303952 + }, + { + "name": "Fn::Map (10 items)", + "avgMs": 1.6977333999999928, + "minMs": 1.3721670000000046, + "maxMs": 2.4862079999999764, + "memoryDeltaBytes": -1763328 + }, + { + "name": "Fn::Map (100 items)", + "avgMs": 5.699275000000005, + "minMs": 3.221917000000019, + "maxMs": 11.950000000000017, + "memoryDeltaBytes": -5786384 + }, + { + "name": "Fn::Map (1000 items)", + "avgMs": 28.46569999999999, + "minMs": 27.931165999999962, + "maxMs": 29.007542, + "memoryDeltaBytes": 5221744 + }, + { + "name": "Nested Fn::Map (3-deep, 3x3x3=27 items)", + "avgMs": 3.7033664000000046, + "minMs": 3.3335409999999683, + "maxMs": 4.05479200000002, + "memoryDeltaBytes": 4920744 + }, + { + "name": "Fn::Include chain (3-deep)", + "avgMs": 0.626566600000001, + "minMs": 0.45583299999998417, + "maxMs": 0.9602500000000305, + "memoryDeltaBytes": -12612048 + }, + { + "name": "Fn::Include chain (10-deep)", + "avgMs": 1.9377081999999972, + "minMs": 1.8670000000000186, + "maxMs": 1.9903339999999616, + "memoryDeltaBytes": 2607624 + }, + { + "name": "Glob (10 files)", + "avgMs": 0.054025000000001454, + "minMs": 0.04825000000005275, + "maxMs": 0.06941699999998718, + "memoryDeltaBytes": 45664 + }, + { + "name": "Glob (100 files)", + "avgMs": 0.05366680000001907, + "minMs": 0.047584000000028936, + "maxMs": 0.06725000000000136, + "memoryDeltaBytes": 45736 + }, + { + "name": "Complex template (mixed features)", + "avgMs": 0.9749168000000055, + "minMs": 0.8508340000000203, + "maxMs": 1.1594589999999698, + "memoryDeltaBytes": 3533864 + } + ] +} \ No newline at end of file diff --git a/benchmarks/benchmark-runner.js b/benchmarks/benchmark-runner.js new file mode 100644 index 0000000..b20ab05 --- /dev/null +++ b/benchmarks/benchmark-runner.js @@ -0,0 +1,546 @@ +/** + * cfn-include Performance Benchmark Suite + * + * Measures: + * - Template compilation times for various complexity levels + * - Memory usage during compilation + * - Nested template performance (1-deep, 3-deep, 10-deep) + * - Fn::Map with varying array sizes (10, 100, 1000 items) + * - Glob operations with varying file counts + */ + +import { performance } from 'node:perf_hooks'; +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import include from '../dist/index.js'; +import * as yaml from '../dist/lib/yaml.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ANSI colors for output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + yellow: '\x1b[33m', + cyan: '\x1b[36m', + red: '\x1b[31m', +}; + +/** + * Format memory usage in human-readable format + */ +function formatMemory(bytes) { + const mb = bytes / (1024 * 1024); + return `${mb.toFixed(2)} MB`; +} + +/** + * Format duration in human-readable format + */ +function formatDuration(ms) { + if (ms < 1) return `${(ms * 1000).toFixed(2)}μs`; + if (ms < 1000) return `${ms.toFixed(2)}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +/** + * Get memory usage + */ +function getMemoryUsage() { + const usage = process.memoryUsage(); + return { + heapUsed: usage.heapUsed, + heapTotal: usage.heapTotal, + external: usage.external, + rss: usage.rss, + }; +} + +/** + * Run a benchmark with memory tracking + */ +async function runBenchmark(name, fn, iterations = 5) { + const results = []; + + // Warm up + await fn(); + + // Force GC if available + if (global.gc) global.gc(); + + const startMemory = getMemoryUsage(); + + for (let i = 0; i < iterations; i++) { + if (global.gc) global.gc(); + + const start = performance.now(); + await fn(); + const end = performance.now(); + + results.push(end - start); + } + + const endMemory = getMemoryUsage(); + + const avg = results.reduce((a, b) => a + b, 0) / results.length; + const min = Math.min(...results); + const max = Math.max(...results); + const memoryDelta = endMemory.heapUsed - startMemory.heapUsed; + + return { + name, + iterations, + avg, + min, + max, + memoryDelta, + startMemory: startMemory.heapUsed, + endMemory: endMemory.heapUsed, + }; +} + +/** + * Print benchmark result + */ +function printResult(result) { + console.log(`${colors.cyan}${result.name}${colors.reset}`); + console.log(` Iterations: ${result.iterations}`); + console.log(` Average: ${colors.green}${formatDuration(result.avg)}${colors.reset}`); + console.log(` Min: ${formatDuration(result.min)}`); + console.log(` Max: ${formatDuration(result.max)}`); + console.log(` Memory Delta: ${formatMemory(result.memoryDelta)}`); + console.log(); +} + +/** + * Generate fixture templates + */ +function generateFixtures() { + const fixturesDir = path.join(__dirname, 'fixtures'); + + // Simple template + const simple = { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Simple benchmark template', + Resources: { + MyBucket: { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'my-simple-bucket', + }, + }, + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'simple.json'), JSON.stringify(simple, null, 2)); + + // Template with Fn::Map (10 items) + const map10 = { + Resources: { + 'Fn::Merge': [ + { + 'Fn::Map': [ + Array.from({ length: 10 }, (_, i) => `item${i}`), + '$', + { + 'Bucket${$}': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: { 'Fn::Sub': 'bucket-${$}' }, + }, + }, + }, + ], + }, + ], + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'map-10.json'), JSON.stringify(map10, null, 2)); + + // Template with Fn::Map (100 items) + const map100 = { + Resources: { + 'Fn::Merge': [ + { + 'Fn::Map': [ + Array.from({ length: 100 }, (_, i) => `item${i}`), + '$', + { + 'Bucket${$}': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: { 'Fn::Sub': 'bucket-${$}' }, + }, + }, + }, + ], + }, + ], + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'map-100.json'), JSON.stringify(map100, null, 2)); + + // Template with Fn::Map (1000 items) + const map1000 = { + Resources: { + 'Fn::Merge': [ + { + 'Fn::Map': [ + Array.from({ length: 1000 }, (_, i) => `item${i}`), + '$', + { + 'Bucket${$}': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: { 'Fn::Sub': 'bucket-${$}' }, + }, + }, + }, + ], + }, + ], + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'map-1000.json'), JSON.stringify(map1000, null, 2)); + + // Nested Map template (3-deep) + const nestedMap3 = { + Resources: { + 'Fn::Merge': [ + { + 'Fn::Map': [ + ['a', 'b', 'c'], + 'outer', + { + 'Fn::Merge': [ + { + 'Fn::Map': [ + ['x', 'y', 'z'], + 'middle', + { + 'Fn::Merge': [ + { + 'Fn::Map': [ + [1, 2, 3], + 'inner', + { + 'Resource${outer}${middle}${inner}': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: { 'Fn::Sub': 'bucket-${outer}-${middle}-${inner}' }, + }, + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'nested-map-3.json'), JSON.stringify(nestedMap3, null, 2)); + + // Include chain base templates + const include1 = { + Level1: { + 'Fn::Include': 'file://' + path.join(fixturesDir, 'include-level2.json'), + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'include-level1.json'), JSON.stringify(include1, null, 2)); + + const include2 = { + Level2: { + 'Fn::Include': 'file://' + path.join(fixturesDir, 'include-level3.json'), + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'include-level2.json'), JSON.stringify(include2, null, 2)); + + const include3 = { + Level3: { + Value: 'deepest-level', + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'include-level3.json'), JSON.stringify(include3, null, 2)); + + // 10-deep include chain + for (let i = 1; i <= 10; i++) { + const content = + i < 10 + ? { [`Level${i}`]: { 'Fn::Include': `file://${path.join(fixturesDir, `deep-include-level${i + 1}.json`)}` } } + : { Level10: { Value: 'deepest' } }; + fs.writeFileSync(path.join(fixturesDir, `deep-include-level${i}.json`), JSON.stringify(content, null, 2)); + } + + // Create glob test files + const globDir = path.join(fixturesDir, 'glob-test'); + if (!fs.existsSync(globDir)) fs.mkdirSync(globDir, { recursive: true }); + + // 10 files for glob + for (let i = 0; i < 10; i++) { + fs.writeFileSync( + path.join(globDir, `resource-${i}.json`), + JSON.stringify({ [`Resource${i}`]: { Type: 'AWS::S3::Bucket' } }, null, 2), + ); + } + + // 100 files for glob + const globDir100 = path.join(fixturesDir, 'glob-test-100'); + if (!fs.existsSync(globDir100)) fs.mkdirSync(globDir100, { recursive: true }); + for (let i = 0; i < 100; i++) { + fs.writeFileSync( + path.join(globDir100, `resource-${i}.json`), + JSON.stringify({ [`Resource${i}`]: { Type: 'AWS::S3::Bucket' } }, null, 2), + ); + } + + // Complex template with multiple features + const complex = { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Complex benchmark template', + Parameters: { + Environment: { Type: 'String', Default: 'dev' }, + }, + Resources: { + 'Fn::Merge': [ + { + 'Fn::Map': [ + ['web', 'api', 'worker'], + 'service', + { + '${service}Bucket': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: { 'Fn::Sub': '${AWS::StackName}-${service}' }, + }, + }, + }, + ], + }, + { + 'Fn::Map': [ + { 'Fn::Sequence': [1, 5] }, + 'idx', + { + 'Lambda${idx}': { + Type: 'AWS::Lambda::Function', + Properties: { + FunctionName: { 'Fn::Sub': 'function-${idx}' }, + }, + }, + }, + ], + }, + ], + }, + Outputs: { + 'Fn::Merge': [ + { + 'Fn::Map': [ + ['web', 'api', 'worker'], + 'service', + { + '${service}Output': { + Value: { 'Fn::Ref': '${service}Bucket' }, + Export: { Name: { 'Fn::Sub': '${AWS::StackName}-${service}' } }, + }, + }, + ], + }, + ], + }, + }; + fs.writeFileSync(path.join(fixturesDir, 'complex.json'), JSON.stringify(complex, null, 2)); + + console.log(`${colors.green}✓ Generated fixture templates${colors.reset}\n`); +} + +/** + * Main benchmark suite + */ +async function main() { + console.log(`${colors.bright}cfn-include Performance Benchmark Suite${colors.reset}\n`); + console.log(`Node.js ${process.version}`); + console.log(`Platform: ${process.platform} ${process.arch}\n`); + + // Generate fixtures + generateFixtures(); + + const fixturesDir = path.join(__dirname, 'fixtures'); + const results = []; + + // 1. Simple template (baseline) + results.push( + await runBenchmark('Simple Template (baseline)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'simple.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'simple.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 2. Fn::Map with 10 items + results.push( + await runBenchmark('Fn::Map (10 items)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'map-10.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'map-10.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 3. Fn::Map with 100 items + results.push( + await runBenchmark('Fn::Map (100 items)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'map-100.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'map-100.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 4. Fn::Map with 1000 items + results.push( + await runBenchmark('Fn::Map (1000 items)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'map-1000.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'map-1000.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 5. Nested Map (3-deep) + results.push( + await runBenchmark('Nested Fn::Map (3-deep, 3x3x3=27 items)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'nested-map-3.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'nested-map-3.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 6. Include chain (3-deep) + results.push( + await runBenchmark('Fn::Include chain (3-deep)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'include-level1.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'include-level1.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 7. Include chain (10-deep) + results.push( + await runBenchmark('Fn::Include chain (10-deep)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'deep-include-level1.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'deep-include-level1.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 8. Glob with 10 files + const glob10Template = { + Resources: { + 'Fn::Merge': [ + { + 'Fn::Include': { + location: path.join(fixturesDir, 'glob-test', '*.json'), + isGlob: true, + }, + }, + ], + }, + }; + results.push( + await runBenchmark('Glob (10 files)', async () => { + await include({ + template: glob10Template, + url: `file://${fixturesDir}/glob-benchmark.json`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 9. Glob with 100 files + const glob100Template = { + Resources: { + 'Fn::Merge': [ + { + 'Fn::Include': { + location: path.join(fixturesDir, 'glob-test-100', '*.json'), + isGlob: true, + }, + }, + ], + }, + }; + results.push( + await runBenchmark('Glob (100 files)', async () => { + await include({ + template: glob100Template, + url: `file://${fixturesDir}/glob-benchmark.json`, + }); + }), + ); + printResult(results[results.length - 1]); + + // 10. Complex template + results.push( + await runBenchmark('Complex template (mixed features)', async () => { + await include({ + template: yaml.load(fs.readFileSync(path.join(fixturesDir, 'complex.json'), 'utf8')), + url: `file://${path.join(fixturesDir, 'complex.json')}`, + }); + }), + ); + printResult(results[results.length - 1]); + + // Summary + console.log(`${colors.bright}Summary${colors.reset}`); + console.log('─'.repeat(60)); + + const baselineAvg = results[0].avg; + for (const r of results) { + const ratio = r.avg / baselineAvg; + const ratioColor = ratio > 10 ? colors.red : ratio > 3 ? colors.yellow : colors.green; + console.log( + `${r.name.padEnd(40)} ${colors.green}${formatDuration(r.avg).padStart(12)}${colors.reset} ` + + `(${ratioColor}${ratio.toFixed(1)}x${colors.reset})`, + ); + } + + // Write results to JSON + const jsonResults = { + timestamp: new Date().toISOString(), + nodeVersion: process.version, + platform: process.platform, + arch: process.arch, + results: results.map((r) => ({ + name: r.name, + avgMs: r.avg, + minMs: r.min, + maxMs: r.max, + memoryDeltaBytes: r.memoryDelta, + })), + }; + + fs.writeFileSync(path.join(__dirname, 'results.json'), JSON.stringify(jsonResults, null, 2)); + console.log(`\n${colors.green}✓ Results saved to benchmarks/results.json${colors.reset}`); +} + +main().catch(console.error); diff --git a/benchmarks/fixtures/complex.json b/benchmarks/fixtures/complex.json new file mode 100644 index 0000000..816103e --- /dev/null +++ b/benchmarks/fixtures/complex.json @@ -0,0 +1,81 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Complex benchmark template", + "Parameters": { + "Environment": { + "Type": "String", + "Default": "dev" + } + }, + "Resources": { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + "web", + "api", + "worker" + ], + "service", + { + "${service}Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": "${AWS::StackName}-${service}" + } + } + } + } + ] + }, + { + "Fn::Map": [ + { + "Fn::Sequence": [ + 1, + 5 + ] + }, + "idx", + { + "Lambda${idx}": { + "Type": "AWS::Lambda::Function", + "Properties": { + "FunctionName": { + "Fn::Sub": "function-${idx}" + } + } + } + } + ] + } + ] + }, + "Outputs": { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + "web", + "api", + "worker" + ], + "service", + { + "${service}Output": { + "Value": { + "Fn::Ref": "${service}Bucket" + }, + "Export": { + "Name": { + "Fn::Sub": "${AWS::StackName}-${service}" + } + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level1.json b/benchmarks/fixtures/deep-include-level1.json new file mode 100644 index 0000000..edc6281 --- /dev/null +++ b/benchmarks/fixtures/deep-include-level1.json @@ -0,0 +1,5 @@ +{ + "Level1": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level2.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level10.json b/benchmarks/fixtures/deep-include-level10.json new file mode 100644 index 0000000..5e7d4b6 --- /dev/null +++ b/benchmarks/fixtures/deep-include-level10.json @@ -0,0 +1,5 @@ +{ + "Level10": { + "Value": "deepest" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level2.json b/benchmarks/fixtures/deep-include-level2.json new file mode 100644 index 0000000..1676c37 --- /dev/null +++ b/benchmarks/fixtures/deep-include-level2.json @@ -0,0 +1,5 @@ +{ + "Level2": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level3.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level3.json b/benchmarks/fixtures/deep-include-level3.json new file mode 100644 index 0000000..55db8a6 --- /dev/null +++ b/benchmarks/fixtures/deep-include-level3.json @@ -0,0 +1,5 @@ +{ + "Level3": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level4.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level4.json b/benchmarks/fixtures/deep-include-level4.json new file mode 100644 index 0000000..97d8505 --- /dev/null +++ b/benchmarks/fixtures/deep-include-level4.json @@ -0,0 +1,5 @@ +{ + "Level4": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level5.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level5.json b/benchmarks/fixtures/deep-include-level5.json new file mode 100644 index 0000000..c6f992f --- /dev/null +++ b/benchmarks/fixtures/deep-include-level5.json @@ -0,0 +1,5 @@ +{ + "Level5": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level6.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level6.json b/benchmarks/fixtures/deep-include-level6.json new file mode 100644 index 0000000..a0667ef --- /dev/null +++ b/benchmarks/fixtures/deep-include-level6.json @@ -0,0 +1,5 @@ +{ + "Level6": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level7.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level7.json b/benchmarks/fixtures/deep-include-level7.json new file mode 100644 index 0000000..4a70066 --- /dev/null +++ b/benchmarks/fixtures/deep-include-level7.json @@ -0,0 +1,5 @@ +{ + "Level7": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level8.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level8.json b/benchmarks/fixtures/deep-include-level8.json new file mode 100644 index 0000000..37e7f9f --- /dev/null +++ b/benchmarks/fixtures/deep-include-level8.json @@ -0,0 +1,5 @@ +{ + "Level8": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level9.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/deep-include-level9.json b/benchmarks/fixtures/deep-include-level9.json new file mode 100644 index 0000000..b79fe62 --- /dev/null +++ b/benchmarks/fixtures/deep-include-level9.json @@ -0,0 +1,5 @@ +{ + "Level9": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/deep-include-level10.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-0.json b/benchmarks/fixtures/glob-test-100/resource-0.json new file mode 100644 index 0000000..edcb99b --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-0.json @@ -0,0 +1,5 @@ +{ + "Resource0": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-1.json b/benchmarks/fixtures/glob-test-100/resource-1.json new file mode 100644 index 0000000..a2f802f --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-1.json @@ -0,0 +1,5 @@ +{ + "Resource1": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-10.json b/benchmarks/fixtures/glob-test-100/resource-10.json new file mode 100644 index 0000000..9616b71 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-10.json @@ -0,0 +1,5 @@ +{ + "Resource10": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-11.json b/benchmarks/fixtures/glob-test-100/resource-11.json new file mode 100644 index 0000000..0c96c65 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-11.json @@ -0,0 +1,5 @@ +{ + "Resource11": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-12.json b/benchmarks/fixtures/glob-test-100/resource-12.json new file mode 100644 index 0000000..3d2577d --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-12.json @@ -0,0 +1,5 @@ +{ + "Resource12": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-13.json b/benchmarks/fixtures/glob-test-100/resource-13.json new file mode 100644 index 0000000..eb99585 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-13.json @@ -0,0 +1,5 @@ +{ + "Resource13": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-14.json b/benchmarks/fixtures/glob-test-100/resource-14.json new file mode 100644 index 0000000..c860c97 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-14.json @@ -0,0 +1,5 @@ +{ + "Resource14": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-15.json b/benchmarks/fixtures/glob-test-100/resource-15.json new file mode 100644 index 0000000..221d0cd --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-15.json @@ -0,0 +1,5 @@ +{ + "Resource15": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-16.json b/benchmarks/fixtures/glob-test-100/resource-16.json new file mode 100644 index 0000000..ba5201c --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-16.json @@ -0,0 +1,5 @@ +{ + "Resource16": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-17.json b/benchmarks/fixtures/glob-test-100/resource-17.json new file mode 100644 index 0000000..58437ff --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-17.json @@ -0,0 +1,5 @@ +{ + "Resource17": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-18.json b/benchmarks/fixtures/glob-test-100/resource-18.json new file mode 100644 index 0000000..c5ede3f --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-18.json @@ -0,0 +1,5 @@ +{ + "Resource18": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-19.json b/benchmarks/fixtures/glob-test-100/resource-19.json new file mode 100644 index 0000000..4714999 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-19.json @@ -0,0 +1,5 @@ +{ + "Resource19": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-2.json b/benchmarks/fixtures/glob-test-100/resource-2.json new file mode 100644 index 0000000..a53fb32 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-2.json @@ -0,0 +1,5 @@ +{ + "Resource2": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-20.json b/benchmarks/fixtures/glob-test-100/resource-20.json new file mode 100644 index 0000000..f3dad97 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-20.json @@ -0,0 +1,5 @@ +{ + "Resource20": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-21.json b/benchmarks/fixtures/glob-test-100/resource-21.json new file mode 100644 index 0000000..7f5a5a4 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-21.json @@ -0,0 +1,5 @@ +{ + "Resource21": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-22.json b/benchmarks/fixtures/glob-test-100/resource-22.json new file mode 100644 index 0000000..bfda8dc --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-22.json @@ -0,0 +1,5 @@ +{ + "Resource22": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-23.json b/benchmarks/fixtures/glob-test-100/resource-23.json new file mode 100644 index 0000000..71ad4ca --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-23.json @@ -0,0 +1,5 @@ +{ + "Resource23": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-24.json b/benchmarks/fixtures/glob-test-100/resource-24.json new file mode 100644 index 0000000..8a2aacb --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-24.json @@ -0,0 +1,5 @@ +{ + "Resource24": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-25.json b/benchmarks/fixtures/glob-test-100/resource-25.json new file mode 100644 index 0000000..cfcd250 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-25.json @@ -0,0 +1,5 @@ +{ + "Resource25": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-26.json b/benchmarks/fixtures/glob-test-100/resource-26.json new file mode 100644 index 0000000..81f6338 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-26.json @@ -0,0 +1,5 @@ +{ + "Resource26": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-27.json b/benchmarks/fixtures/glob-test-100/resource-27.json new file mode 100644 index 0000000..eeaf686 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-27.json @@ -0,0 +1,5 @@ +{ + "Resource27": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-28.json b/benchmarks/fixtures/glob-test-100/resource-28.json new file mode 100644 index 0000000..686b974 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-28.json @@ -0,0 +1,5 @@ +{ + "Resource28": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-29.json b/benchmarks/fixtures/glob-test-100/resource-29.json new file mode 100644 index 0000000..1ef4ea9 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-29.json @@ -0,0 +1,5 @@ +{ + "Resource29": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-3.json b/benchmarks/fixtures/glob-test-100/resource-3.json new file mode 100644 index 0000000..a9cb4d6 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-3.json @@ -0,0 +1,5 @@ +{ + "Resource3": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-30.json b/benchmarks/fixtures/glob-test-100/resource-30.json new file mode 100644 index 0000000..d49ec1b --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-30.json @@ -0,0 +1,5 @@ +{ + "Resource30": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-31.json b/benchmarks/fixtures/glob-test-100/resource-31.json new file mode 100644 index 0000000..efc758c --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-31.json @@ -0,0 +1,5 @@ +{ + "Resource31": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-32.json b/benchmarks/fixtures/glob-test-100/resource-32.json new file mode 100644 index 0000000..ad760c0 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-32.json @@ -0,0 +1,5 @@ +{ + "Resource32": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-33.json b/benchmarks/fixtures/glob-test-100/resource-33.json new file mode 100644 index 0000000..076da15 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-33.json @@ -0,0 +1,5 @@ +{ + "Resource33": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-34.json b/benchmarks/fixtures/glob-test-100/resource-34.json new file mode 100644 index 0000000..511007b --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-34.json @@ -0,0 +1,5 @@ +{ + "Resource34": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-35.json b/benchmarks/fixtures/glob-test-100/resource-35.json new file mode 100644 index 0000000..c5b39d9 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-35.json @@ -0,0 +1,5 @@ +{ + "Resource35": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-36.json b/benchmarks/fixtures/glob-test-100/resource-36.json new file mode 100644 index 0000000..d0b55b0 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-36.json @@ -0,0 +1,5 @@ +{ + "Resource36": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-37.json b/benchmarks/fixtures/glob-test-100/resource-37.json new file mode 100644 index 0000000..7a31142 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-37.json @@ -0,0 +1,5 @@ +{ + "Resource37": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-38.json b/benchmarks/fixtures/glob-test-100/resource-38.json new file mode 100644 index 0000000..d03148c --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-38.json @@ -0,0 +1,5 @@ +{ + "Resource38": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-39.json b/benchmarks/fixtures/glob-test-100/resource-39.json new file mode 100644 index 0000000..f20a853 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-39.json @@ -0,0 +1,5 @@ +{ + "Resource39": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-4.json b/benchmarks/fixtures/glob-test-100/resource-4.json new file mode 100644 index 0000000..a2387b9 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-4.json @@ -0,0 +1,5 @@ +{ + "Resource4": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-40.json b/benchmarks/fixtures/glob-test-100/resource-40.json new file mode 100644 index 0000000..65a029f --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-40.json @@ -0,0 +1,5 @@ +{ + "Resource40": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-41.json b/benchmarks/fixtures/glob-test-100/resource-41.json new file mode 100644 index 0000000..bee71e2 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-41.json @@ -0,0 +1,5 @@ +{ + "Resource41": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-42.json b/benchmarks/fixtures/glob-test-100/resource-42.json new file mode 100644 index 0000000..e27b417 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-42.json @@ -0,0 +1,5 @@ +{ + "Resource42": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-43.json b/benchmarks/fixtures/glob-test-100/resource-43.json new file mode 100644 index 0000000..d2e5a7c --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-43.json @@ -0,0 +1,5 @@ +{ + "Resource43": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-44.json b/benchmarks/fixtures/glob-test-100/resource-44.json new file mode 100644 index 0000000..45fba5a --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-44.json @@ -0,0 +1,5 @@ +{ + "Resource44": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-45.json b/benchmarks/fixtures/glob-test-100/resource-45.json new file mode 100644 index 0000000..6d3aa22 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-45.json @@ -0,0 +1,5 @@ +{ + "Resource45": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-46.json b/benchmarks/fixtures/glob-test-100/resource-46.json new file mode 100644 index 0000000..6c4f182 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-46.json @@ -0,0 +1,5 @@ +{ + "Resource46": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-47.json b/benchmarks/fixtures/glob-test-100/resource-47.json new file mode 100644 index 0000000..9d04739 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-47.json @@ -0,0 +1,5 @@ +{ + "Resource47": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-48.json b/benchmarks/fixtures/glob-test-100/resource-48.json new file mode 100644 index 0000000..98a35de --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-48.json @@ -0,0 +1,5 @@ +{ + "Resource48": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-49.json b/benchmarks/fixtures/glob-test-100/resource-49.json new file mode 100644 index 0000000..8a82489 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-49.json @@ -0,0 +1,5 @@ +{ + "Resource49": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-5.json b/benchmarks/fixtures/glob-test-100/resource-5.json new file mode 100644 index 0000000..082da89 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-5.json @@ -0,0 +1,5 @@ +{ + "Resource5": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-50.json b/benchmarks/fixtures/glob-test-100/resource-50.json new file mode 100644 index 0000000..1089d60 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-50.json @@ -0,0 +1,5 @@ +{ + "Resource50": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-51.json b/benchmarks/fixtures/glob-test-100/resource-51.json new file mode 100644 index 0000000..db9dff3 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-51.json @@ -0,0 +1,5 @@ +{ + "Resource51": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-52.json b/benchmarks/fixtures/glob-test-100/resource-52.json new file mode 100644 index 0000000..519f277 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-52.json @@ -0,0 +1,5 @@ +{ + "Resource52": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-53.json b/benchmarks/fixtures/glob-test-100/resource-53.json new file mode 100644 index 0000000..cf394e8 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-53.json @@ -0,0 +1,5 @@ +{ + "Resource53": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-54.json b/benchmarks/fixtures/glob-test-100/resource-54.json new file mode 100644 index 0000000..592c2c0 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-54.json @@ -0,0 +1,5 @@ +{ + "Resource54": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-55.json b/benchmarks/fixtures/glob-test-100/resource-55.json new file mode 100644 index 0000000..75a1d66 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-55.json @@ -0,0 +1,5 @@ +{ + "Resource55": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-56.json b/benchmarks/fixtures/glob-test-100/resource-56.json new file mode 100644 index 0000000..79589a8 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-56.json @@ -0,0 +1,5 @@ +{ + "Resource56": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-57.json b/benchmarks/fixtures/glob-test-100/resource-57.json new file mode 100644 index 0000000..886f425 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-57.json @@ -0,0 +1,5 @@ +{ + "Resource57": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-58.json b/benchmarks/fixtures/glob-test-100/resource-58.json new file mode 100644 index 0000000..1a248f9 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-58.json @@ -0,0 +1,5 @@ +{ + "Resource58": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-59.json b/benchmarks/fixtures/glob-test-100/resource-59.json new file mode 100644 index 0000000..a0ab4fd --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-59.json @@ -0,0 +1,5 @@ +{ + "Resource59": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-6.json b/benchmarks/fixtures/glob-test-100/resource-6.json new file mode 100644 index 0000000..5f5a2ec --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-6.json @@ -0,0 +1,5 @@ +{ + "Resource6": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-60.json b/benchmarks/fixtures/glob-test-100/resource-60.json new file mode 100644 index 0000000..ed2d37d --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-60.json @@ -0,0 +1,5 @@ +{ + "Resource60": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-61.json b/benchmarks/fixtures/glob-test-100/resource-61.json new file mode 100644 index 0000000..eb408af --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-61.json @@ -0,0 +1,5 @@ +{ + "Resource61": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-62.json b/benchmarks/fixtures/glob-test-100/resource-62.json new file mode 100644 index 0000000..ba3296b --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-62.json @@ -0,0 +1,5 @@ +{ + "Resource62": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-63.json b/benchmarks/fixtures/glob-test-100/resource-63.json new file mode 100644 index 0000000..800a57a --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-63.json @@ -0,0 +1,5 @@ +{ + "Resource63": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-64.json b/benchmarks/fixtures/glob-test-100/resource-64.json new file mode 100644 index 0000000..008c1c3 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-64.json @@ -0,0 +1,5 @@ +{ + "Resource64": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-65.json b/benchmarks/fixtures/glob-test-100/resource-65.json new file mode 100644 index 0000000..b41670b --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-65.json @@ -0,0 +1,5 @@ +{ + "Resource65": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-66.json b/benchmarks/fixtures/glob-test-100/resource-66.json new file mode 100644 index 0000000..cfc0f1f --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-66.json @@ -0,0 +1,5 @@ +{ + "Resource66": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-67.json b/benchmarks/fixtures/glob-test-100/resource-67.json new file mode 100644 index 0000000..9e70bc2 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-67.json @@ -0,0 +1,5 @@ +{ + "Resource67": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-68.json b/benchmarks/fixtures/glob-test-100/resource-68.json new file mode 100644 index 0000000..2abef01 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-68.json @@ -0,0 +1,5 @@ +{ + "Resource68": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-69.json b/benchmarks/fixtures/glob-test-100/resource-69.json new file mode 100644 index 0000000..1f6074f --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-69.json @@ -0,0 +1,5 @@ +{ + "Resource69": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-7.json b/benchmarks/fixtures/glob-test-100/resource-7.json new file mode 100644 index 0000000..7d65200 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-7.json @@ -0,0 +1,5 @@ +{ + "Resource7": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-70.json b/benchmarks/fixtures/glob-test-100/resource-70.json new file mode 100644 index 0000000..156d19d --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-70.json @@ -0,0 +1,5 @@ +{ + "Resource70": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-71.json b/benchmarks/fixtures/glob-test-100/resource-71.json new file mode 100644 index 0000000..1cdf31e --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-71.json @@ -0,0 +1,5 @@ +{ + "Resource71": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-72.json b/benchmarks/fixtures/glob-test-100/resource-72.json new file mode 100644 index 0000000..65805fd --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-72.json @@ -0,0 +1,5 @@ +{ + "Resource72": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-73.json b/benchmarks/fixtures/glob-test-100/resource-73.json new file mode 100644 index 0000000..bd99791 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-73.json @@ -0,0 +1,5 @@ +{ + "Resource73": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-74.json b/benchmarks/fixtures/glob-test-100/resource-74.json new file mode 100644 index 0000000..9f7b2fb --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-74.json @@ -0,0 +1,5 @@ +{ + "Resource74": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-75.json b/benchmarks/fixtures/glob-test-100/resource-75.json new file mode 100644 index 0000000..a7f3643 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-75.json @@ -0,0 +1,5 @@ +{ + "Resource75": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-76.json b/benchmarks/fixtures/glob-test-100/resource-76.json new file mode 100644 index 0000000..9a47ef4 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-76.json @@ -0,0 +1,5 @@ +{ + "Resource76": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-77.json b/benchmarks/fixtures/glob-test-100/resource-77.json new file mode 100644 index 0000000..9c150a7 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-77.json @@ -0,0 +1,5 @@ +{ + "Resource77": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-78.json b/benchmarks/fixtures/glob-test-100/resource-78.json new file mode 100644 index 0000000..c47a9f1 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-78.json @@ -0,0 +1,5 @@ +{ + "Resource78": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-79.json b/benchmarks/fixtures/glob-test-100/resource-79.json new file mode 100644 index 0000000..14a3302 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-79.json @@ -0,0 +1,5 @@ +{ + "Resource79": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-8.json b/benchmarks/fixtures/glob-test-100/resource-8.json new file mode 100644 index 0000000..3ba9bca --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-8.json @@ -0,0 +1,5 @@ +{ + "Resource8": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-80.json b/benchmarks/fixtures/glob-test-100/resource-80.json new file mode 100644 index 0000000..12970e3 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-80.json @@ -0,0 +1,5 @@ +{ + "Resource80": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-81.json b/benchmarks/fixtures/glob-test-100/resource-81.json new file mode 100644 index 0000000..d4ff1fd --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-81.json @@ -0,0 +1,5 @@ +{ + "Resource81": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-82.json b/benchmarks/fixtures/glob-test-100/resource-82.json new file mode 100644 index 0000000..158f976 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-82.json @@ -0,0 +1,5 @@ +{ + "Resource82": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-83.json b/benchmarks/fixtures/glob-test-100/resource-83.json new file mode 100644 index 0000000..f80f327 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-83.json @@ -0,0 +1,5 @@ +{ + "Resource83": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-84.json b/benchmarks/fixtures/glob-test-100/resource-84.json new file mode 100644 index 0000000..09deef2 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-84.json @@ -0,0 +1,5 @@ +{ + "Resource84": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-85.json b/benchmarks/fixtures/glob-test-100/resource-85.json new file mode 100644 index 0000000..f3c69aa --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-85.json @@ -0,0 +1,5 @@ +{ + "Resource85": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-86.json b/benchmarks/fixtures/glob-test-100/resource-86.json new file mode 100644 index 0000000..01775f2 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-86.json @@ -0,0 +1,5 @@ +{ + "Resource86": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-87.json b/benchmarks/fixtures/glob-test-100/resource-87.json new file mode 100644 index 0000000..e2fc2ed --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-87.json @@ -0,0 +1,5 @@ +{ + "Resource87": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-88.json b/benchmarks/fixtures/glob-test-100/resource-88.json new file mode 100644 index 0000000..b23595e --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-88.json @@ -0,0 +1,5 @@ +{ + "Resource88": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-89.json b/benchmarks/fixtures/glob-test-100/resource-89.json new file mode 100644 index 0000000..0b51628 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-89.json @@ -0,0 +1,5 @@ +{ + "Resource89": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-9.json b/benchmarks/fixtures/glob-test-100/resource-9.json new file mode 100644 index 0000000..3c489fb --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-9.json @@ -0,0 +1,5 @@ +{ + "Resource9": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-90.json b/benchmarks/fixtures/glob-test-100/resource-90.json new file mode 100644 index 0000000..798f571 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-90.json @@ -0,0 +1,5 @@ +{ + "Resource90": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-91.json b/benchmarks/fixtures/glob-test-100/resource-91.json new file mode 100644 index 0000000..5b738de --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-91.json @@ -0,0 +1,5 @@ +{ + "Resource91": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-92.json b/benchmarks/fixtures/glob-test-100/resource-92.json new file mode 100644 index 0000000..a06141c --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-92.json @@ -0,0 +1,5 @@ +{ + "Resource92": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-93.json b/benchmarks/fixtures/glob-test-100/resource-93.json new file mode 100644 index 0000000..9c9b141 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-93.json @@ -0,0 +1,5 @@ +{ + "Resource93": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-94.json b/benchmarks/fixtures/glob-test-100/resource-94.json new file mode 100644 index 0000000..775050e --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-94.json @@ -0,0 +1,5 @@ +{ + "Resource94": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-95.json b/benchmarks/fixtures/glob-test-100/resource-95.json new file mode 100644 index 0000000..7edebf6 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-95.json @@ -0,0 +1,5 @@ +{ + "Resource95": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-96.json b/benchmarks/fixtures/glob-test-100/resource-96.json new file mode 100644 index 0000000..ed54991 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-96.json @@ -0,0 +1,5 @@ +{ + "Resource96": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-97.json b/benchmarks/fixtures/glob-test-100/resource-97.json new file mode 100644 index 0000000..229420f --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-97.json @@ -0,0 +1,5 @@ +{ + "Resource97": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-98.json b/benchmarks/fixtures/glob-test-100/resource-98.json new file mode 100644 index 0000000..e14ce42 --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-98.json @@ -0,0 +1,5 @@ +{ + "Resource98": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test-100/resource-99.json b/benchmarks/fixtures/glob-test-100/resource-99.json new file mode 100644 index 0000000..a15f96f --- /dev/null +++ b/benchmarks/fixtures/glob-test-100/resource-99.json @@ -0,0 +1,5 @@ +{ + "Resource99": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-0.json b/benchmarks/fixtures/glob-test/resource-0.json new file mode 100644 index 0000000..edcb99b --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-0.json @@ -0,0 +1,5 @@ +{ + "Resource0": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-1.json b/benchmarks/fixtures/glob-test/resource-1.json new file mode 100644 index 0000000..a2f802f --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-1.json @@ -0,0 +1,5 @@ +{ + "Resource1": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-2.json b/benchmarks/fixtures/glob-test/resource-2.json new file mode 100644 index 0000000..a53fb32 --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-2.json @@ -0,0 +1,5 @@ +{ + "Resource2": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-3.json b/benchmarks/fixtures/glob-test/resource-3.json new file mode 100644 index 0000000..a9cb4d6 --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-3.json @@ -0,0 +1,5 @@ +{ + "Resource3": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-4.json b/benchmarks/fixtures/glob-test/resource-4.json new file mode 100644 index 0000000..a2387b9 --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-4.json @@ -0,0 +1,5 @@ +{ + "Resource4": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-5.json b/benchmarks/fixtures/glob-test/resource-5.json new file mode 100644 index 0000000..082da89 --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-5.json @@ -0,0 +1,5 @@ +{ + "Resource5": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-6.json b/benchmarks/fixtures/glob-test/resource-6.json new file mode 100644 index 0000000..5f5a2ec --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-6.json @@ -0,0 +1,5 @@ +{ + "Resource6": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-7.json b/benchmarks/fixtures/glob-test/resource-7.json new file mode 100644 index 0000000..7d65200 --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-7.json @@ -0,0 +1,5 @@ +{ + "Resource7": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-8.json b/benchmarks/fixtures/glob-test/resource-8.json new file mode 100644 index 0000000..3ba9bca --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-8.json @@ -0,0 +1,5 @@ +{ + "Resource8": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/glob-test/resource-9.json b/benchmarks/fixtures/glob-test/resource-9.json new file mode 100644 index 0000000..3c489fb --- /dev/null +++ b/benchmarks/fixtures/glob-test/resource-9.json @@ -0,0 +1,5 @@ +{ + "Resource9": { + "Type": "AWS::S3::Bucket" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/include-level1.json b/benchmarks/fixtures/include-level1.json new file mode 100644 index 0000000..5a602f5 --- /dev/null +++ b/benchmarks/fixtures/include-level1.json @@ -0,0 +1,5 @@ +{ + "Level1": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/include-level2.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/include-level2.json b/benchmarks/fixtures/include-level2.json new file mode 100644 index 0000000..ab72fb7 --- /dev/null +++ b/benchmarks/fixtures/include-level2.json @@ -0,0 +1,5 @@ +{ + "Level2": { + "Fn::Include": "file:///Users/nem/code/clawd/cfn-include/benchmarks/fixtures/include-level3.json" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/include-level3.json b/benchmarks/fixtures/include-level3.json new file mode 100644 index 0000000..c31b20a --- /dev/null +++ b/benchmarks/fixtures/include-level3.json @@ -0,0 +1,5 @@ +{ + "Level3": { + "Value": "deepest-level" + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/map-10.json b/benchmarks/fixtures/map-10.json new file mode 100644 index 0000000..645cc5b --- /dev/null +++ b/benchmarks/fixtures/map-10.json @@ -0,0 +1,33 @@ +{ + "Resources": { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + "item0", + "item1", + "item2", + "item3", + "item4", + "item5", + "item6", + "item7", + "item8", + "item9" + ], + "$", + { + "Bucket${$}": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": "bucket-${$}" + } + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/map-100.json b/benchmarks/fixtures/map-100.json new file mode 100644 index 0000000..8467d23 --- /dev/null +++ b/benchmarks/fixtures/map-100.json @@ -0,0 +1,123 @@ +{ + "Resources": { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + "item0", + "item1", + "item2", + "item3", + "item4", + "item5", + "item6", + "item7", + "item8", + "item9", + "item10", + "item11", + "item12", + "item13", + "item14", + "item15", + "item16", + "item17", + "item18", + "item19", + "item20", + "item21", + "item22", + "item23", + "item24", + "item25", + "item26", + "item27", + "item28", + "item29", + "item30", + "item31", + "item32", + "item33", + "item34", + "item35", + "item36", + "item37", + "item38", + "item39", + "item40", + "item41", + "item42", + "item43", + "item44", + "item45", + "item46", + "item47", + "item48", + "item49", + "item50", + "item51", + "item52", + "item53", + "item54", + "item55", + "item56", + "item57", + "item58", + "item59", + "item60", + "item61", + "item62", + "item63", + "item64", + "item65", + "item66", + "item67", + "item68", + "item69", + "item70", + "item71", + "item72", + "item73", + "item74", + "item75", + "item76", + "item77", + "item78", + "item79", + "item80", + "item81", + "item82", + "item83", + "item84", + "item85", + "item86", + "item87", + "item88", + "item89", + "item90", + "item91", + "item92", + "item93", + "item94", + "item95", + "item96", + "item97", + "item98", + "item99" + ], + "$", + { + "Bucket${$}": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": "bucket-${$}" + } + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/map-1000.json b/benchmarks/fixtures/map-1000.json new file mode 100644 index 0000000..61eecb5 --- /dev/null +++ b/benchmarks/fixtures/map-1000.json @@ -0,0 +1,1023 @@ +{ + "Resources": { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + "item0", + "item1", + "item2", + "item3", + "item4", + "item5", + "item6", + "item7", + "item8", + "item9", + "item10", + "item11", + "item12", + "item13", + "item14", + "item15", + "item16", + "item17", + "item18", + "item19", + "item20", + "item21", + "item22", + "item23", + "item24", + "item25", + "item26", + "item27", + "item28", + "item29", + "item30", + "item31", + "item32", + "item33", + "item34", + "item35", + "item36", + "item37", + "item38", + "item39", + "item40", + "item41", + "item42", + "item43", + "item44", + "item45", + "item46", + "item47", + "item48", + "item49", + "item50", + "item51", + "item52", + "item53", + "item54", + "item55", + "item56", + "item57", + "item58", + "item59", + "item60", + "item61", + "item62", + "item63", + "item64", + "item65", + "item66", + "item67", + "item68", + "item69", + "item70", + "item71", + "item72", + "item73", + "item74", + "item75", + "item76", + "item77", + "item78", + "item79", + "item80", + "item81", + "item82", + "item83", + "item84", + "item85", + "item86", + "item87", + "item88", + "item89", + "item90", + "item91", + "item92", + "item93", + "item94", + "item95", + "item96", + "item97", + "item98", + "item99", + "item100", + "item101", + "item102", + "item103", + "item104", + "item105", + "item106", + "item107", + "item108", + "item109", + "item110", + "item111", + "item112", + "item113", + "item114", + "item115", + "item116", + "item117", + "item118", + "item119", + "item120", + "item121", + "item122", + "item123", + "item124", + "item125", + "item126", + "item127", + "item128", + "item129", + "item130", + "item131", + "item132", + "item133", + "item134", + "item135", + "item136", + "item137", + "item138", + "item139", + "item140", + "item141", + "item142", + "item143", + "item144", + "item145", + "item146", + "item147", + "item148", + "item149", + "item150", + "item151", + "item152", + "item153", + "item154", + "item155", + "item156", + "item157", + "item158", + "item159", + "item160", + "item161", + "item162", + "item163", + "item164", + "item165", + "item166", + "item167", + "item168", + "item169", + "item170", + "item171", + "item172", + "item173", + "item174", + "item175", + "item176", + "item177", + "item178", + "item179", + "item180", + "item181", + "item182", + "item183", + "item184", + "item185", + "item186", + "item187", + "item188", + "item189", + "item190", + "item191", + "item192", + "item193", + "item194", + "item195", + "item196", + "item197", + "item198", + "item199", + "item200", + "item201", + "item202", + "item203", + "item204", + "item205", + "item206", + "item207", + "item208", + "item209", + "item210", + "item211", + "item212", + "item213", + "item214", + "item215", + "item216", + "item217", + "item218", + "item219", + "item220", + "item221", + "item222", + "item223", + "item224", + "item225", + "item226", + "item227", + "item228", + "item229", + "item230", + "item231", + "item232", + "item233", + "item234", + "item235", + "item236", + "item237", + "item238", + "item239", + "item240", + "item241", + "item242", + "item243", + "item244", + "item245", + "item246", + "item247", + "item248", + "item249", + "item250", + "item251", + "item252", + "item253", + "item254", + "item255", + "item256", + "item257", + "item258", + "item259", + "item260", + "item261", + "item262", + "item263", + "item264", + "item265", + "item266", + "item267", + "item268", + "item269", + "item270", + "item271", + "item272", + "item273", + "item274", + "item275", + "item276", + "item277", + "item278", + "item279", + "item280", + "item281", + "item282", + "item283", + "item284", + "item285", + "item286", + "item287", + "item288", + "item289", + "item290", + "item291", + "item292", + "item293", + "item294", + "item295", + "item296", + "item297", + "item298", + "item299", + "item300", + "item301", + "item302", + "item303", + "item304", + "item305", + "item306", + "item307", + "item308", + "item309", + "item310", + "item311", + "item312", + "item313", + "item314", + "item315", + "item316", + "item317", + "item318", + "item319", + "item320", + "item321", + "item322", + "item323", + "item324", + "item325", + "item326", + "item327", + "item328", + "item329", + "item330", + "item331", + "item332", + "item333", + "item334", + "item335", + "item336", + "item337", + "item338", + "item339", + "item340", + "item341", + "item342", + "item343", + "item344", + "item345", + "item346", + "item347", + "item348", + "item349", + "item350", + "item351", + "item352", + "item353", + "item354", + "item355", + "item356", + "item357", + "item358", + "item359", + "item360", + "item361", + "item362", + "item363", + "item364", + "item365", + "item366", + "item367", + "item368", + "item369", + "item370", + "item371", + "item372", + "item373", + "item374", + "item375", + "item376", + "item377", + "item378", + "item379", + "item380", + "item381", + "item382", + "item383", + "item384", + "item385", + "item386", + "item387", + "item388", + "item389", + "item390", + "item391", + "item392", + "item393", + "item394", + "item395", + "item396", + "item397", + "item398", + "item399", + "item400", + "item401", + "item402", + "item403", + "item404", + "item405", + "item406", + "item407", + "item408", + "item409", + "item410", + "item411", + "item412", + "item413", + "item414", + "item415", + "item416", + "item417", + "item418", + "item419", + "item420", + "item421", + "item422", + "item423", + "item424", + "item425", + "item426", + "item427", + "item428", + "item429", + "item430", + "item431", + "item432", + "item433", + "item434", + "item435", + "item436", + "item437", + "item438", + "item439", + "item440", + "item441", + "item442", + "item443", + "item444", + "item445", + "item446", + "item447", + "item448", + "item449", + "item450", + "item451", + "item452", + "item453", + "item454", + "item455", + "item456", + "item457", + "item458", + "item459", + "item460", + "item461", + "item462", + "item463", + "item464", + "item465", + "item466", + "item467", + "item468", + "item469", + "item470", + "item471", + "item472", + "item473", + "item474", + "item475", + "item476", + "item477", + "item478", + "item479", + "item480", + "item481", + "item482", + "item483", + "item484", + "item485", + "item486", + "item487", + "item488", + "item489", + "item490", + "item491", + "item492", + "item493", + "item494", + "item495", + "item496", + "item497", + "item498", + "item499", + "item500", + "item501", + "item502", + "item503", + "item504", + "item505", + "item506", + "item507", + "item508", + "item509", + "item510", + "item511", + "item512", + "item513", + "item514", + "item515", + "item516", + "item517", + "item518", + "item519", + "item520", + "item521", + "item522", + "item523", + "item524", + "item525", + "item526", + "item527", + "item528", + "item529", + "item530", + "item531", + "item532", + "item533", + "item534", + "item535", + "item536", + "item537", + "item538", + "item539", + "item540", + "item541", + "item542", + "item543", + "item544", + "item545", + "item546", + "item547", + "item548", + "item549", + "item550", + "item551", + "item552", + "item553", + "item554", + "item555", + "item556", + "item557", + "item558", + "item559", + "item560", + "item561", + "item562", + "item563", + "item564", + "item565", + "item566", + "item567", + "item568", + "item569", + "item570", + "item571", + "item572", + "item573", + "item574", + "item575", + "item576", + "item577", + "item578", + "item579", + "item580", + "item581", + "item582", + "item583", + "item584", + "item585", + "item586", + "item587", + "item588", + "item589", + "item590", + "item591", + "item592", + "item593", + "item594", + "item595", + "item596", + "item597", + "item598", + "item599", + "item600", + "item601", + "item602", + "item603", + "item604", + "item605", + "item606", + "item607", + "item608", + "item609", + "item610", + "item611", + "item612", + "item613", + "item614", + "item615", + "item616", + "item617", + "item618", + "item619", + "item620", + "item621", + "item622", + "item623", + "item624", + "item625", + "item626", + "item627", + "item628", + "item629", + "item630", + "item631", + "item632", + "item633", + "item634", + "item635", + "item636", + "item637", + "item638", + "item639", + "item640", + "item641", + "item642", + "item643", + "item644", + "item645", + "item646", + "item647", + "item648", + "item649", + "item650", + "item651", + "item652", + "item653", + "item654", + "item655", + "item656", + "item657", + "item658", + "item659", + "item660", + "item661", + "item662", + "item663", + "item664", + "item665", + "item666", + "item667", + "item668", + "item669", + "item670", + "item671", + "item672", + "item673", + "item674", + "item675", + "item676", + "item677", + "item678", + "item679", + "item680", + "item681", + "item682", + "item683", + "item684", + "item685", + "item686", + "item687", + "item688", + "item689", + "item690", + "item691", + "item692", + "item693", + "item694", + "item695", + "item696", + "item697", + "item698", + "item699", + "item700", + "item701", + "item702", + "item703", + "item704", + "item705", + "item706", + "item707", + "item708", + "item709", + "item710", + "item711", + "item712", + "item713", + "item714", + "item715", + "item716", + "item717", + "item718", + "item719", + "item720", + "item721", + "item722", + "item723", + "item724", + "item725", + "item726", + "item727", + "item728", + "item729", + "item730", + "item731", + "item732", + "item733", + "item734", + "item735", + "item736", + "item737", + "item738", + "item739", + "item740", + "item741", + "item742", + "item743", + "item744", + "item745", + "item746", + "item747", + "item748", + "item749", + "item750", + "item751", + "item752", + "item753", + "item754", + "item755", + "item756", + "item757", + "item758", + "item759", + "item760", + "item761", + "item762", + "item763", + "item764", + "item765", + "item766", + "item767", + "item768", + "item769", + "item770", + "item771", + "item772", + "item773", + "item774", + "item775", + "item776", + "item777", + "item778", + "item779", + "item780", + "item781", + "item782", + "item783", + "item784", + "item785", + "item786", + "item787", + "item788", + "item789", + "item790", + "item791", + "item792", + "item793", + "item794", + "item795", + "item796", + "item797", + "item798", + "item799", + "item800", + "item801", + "item802", + "item803", + "item804", + "item805", + "item806", + "item807", + "item808", + "item809", + "item810", + "item811", + "item812", + "item813", + "item814", + "item815", + "item816", + "item817", + "item818", + "item819", + "item820", + "item821", + "item822", + "item823", + "item824", + "item825", + "item826", + "item827", + "item828", + "item829", + "item830", + "item831", + "item832", + "item833", + "item834", + "item835", + "item836", + "item837", + "item838", + "item839", + "item840", + "item841", + "item842", + "item843", + "item844", + "item845", + "item846", + "item847", + "item848", + "item849", + "item850", + "item851", + "item852", + "item853", + "item854", + "item855", + "item856", + "item857", + "item858", + "item859", + "item860", + "item861", + "item862", + "item863", + "item864", + "item865", + "item866", + "item867", + "item868", + "item869", + "item870", + "item871", + "item872", + "item873", + "item874", + "item875", + "item876", + "item877", + "item878", + "item879", + "item880", + "item881", + "item882", + "item883", + "item884", + "item885", + "item886", + "item887", + "item888", + "item889", + "item890", + "item891", + "item892", + "item893", + "item894", + "item895", + "item896", + "item897", + "item898", + "item899", + "item900", + "item901", + "item902", + "item903", + "item904", + "item905", + "item906", + "item907", + "item908", + "item909", + "item910", + "item911", + "item912", + "item913", + "item914", + "item915", + "item916", + "item917", + "item918", + "item919", + "item920", + "item921", + "item922", + "item923", + "item924", + "item925", + "item926", + "item927", + "item928", + "item929", + "item930", + "item931", + "item932", + "item933", + "item934", + "item935", + "item936", + "item937", + "item938", + "item939", + "item940", + "item941", + "item942", + "item943", + "item944", + "item945", + "item946", + "item947", + "item948", + "item949", + "item950", + "item951", + "item952", + "item953", + "item954", + "item955", + "item956", + "item957", + "item958", + "item959", + "item960", + "item961", + "item962", + "item963", + "item964", + "item965", + "item966", + "item967", + "item968", + "item969", + "item970", + "item971", + "item972", + "item973", + "item974", + "item975", + "item976", + "item977", + "item978", + "item979", + "item980", + "item981", + "item982", + "item983", + "item984", + "item985", + "item986", + "item987", + "item988", + "item989", + "item990", + "item991", + "item992", + "item993", + "item994", + "item995", + "item996", + "item997", + "item998", + "item999" + ], + "$", + { + "Bucket${$}": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": "bucket-${$}" + } + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/nested-map-3.json b/benchmarks/fixtures/nested-map-3.json new file mode 100644 index 0000000..549bb14 --- /dev/null +++ b/benchmarks/fixtures/nested-map-3.json @@ -0,0 +1,54 @@ +{ + "Resources": { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + "a", + "b", + "c" + ], + "outer", + { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + "x", + "y", + "z" + ], + "middle", + { + "Fn::Merge": [ + { + "Fn::Map": [ + [ + 1, + 2, + 3 + ], + "inner", + { + "Resource${outer}${middle}${inner}": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": "bucket-${outer}-${middle}-${inner}" + } + } + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/benchmarks/fixtures/simple.json b/benchmarks/fixtures/simple.json new file mode 100644 index 0000000..6c40648 --- /dev/null +++ b/benchmarks/fixtures/simple.json @@ -0,0 +1,12 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Simple benchmark template", + "Resources": { + "MyBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "my-simple-bucket" + } + } + } +} \ No newline at end of file diff --git a/benchmarks/results.json b/benchmarks/results.json new file mode 100644 index 0000000..e7ca6a5 --- /dev/null +++ b/benchmarks/results.json @@ -0,0 +1,78 @@ +{ + "timestamp": "2026-02-08T21:30:17.706Z", + "nodeVersion": "v22.22.0", + "platform": "darwin", + "arch": "arm64", + "results": [ + { + "name": "Simple Template (baseline)", + "avgMs": 0.1845002000000079, + "minMs": 0.06662500000001614, + "maxMs": 0.5414580000000058, + "memoryDeltaBytes": 232672 + }, + { + "name": "Fn::Map (10 items)", + "avgMs": 0.6623668000000009, + "minMs": 0.4798330000000135, + "maxMs": 1.0785419999999988, + "memoryDeltaBytes": -2764688 + }, + { + "name": "Fn::Map (100 items)", + "avgMs": 3.284158400000007, + "minMs": 2.569041999999996, + "maxMs": 4.212250000000012, + "memoryDeltaBytes": -1487784 + }, + { + "name": "Fn::Map (1000 items)", + "avgMs": 24.232258399999996, + "minMs": 23.28054199999997, + "maxMs": 25.349625000000003, + "memoryDeltaBytes": 4106544 + }, + { + "name": "Nested Fn::Map (3-deep, 3x3x3=27 items)", + "avgMs": 2.2009915999999747, + "minMs": 1.9771669999999517, + "maxMs": 2.525457999999958, + "memoryDeltaBytes": -12797680 + }, + { + "name": "Fn::Include chain (3-deep)", + "avgMs": 0.186258399999997, + "minMs": 0.11037499999997635, + "maxMs": 0.2673340000000053, + "memoryDeltaBytes": 446288 + }, + { + "name": "Fn::Include chain (10-deep)", + "avgMs": 0.40524180000001025, + "minMs": 0.38433400000002393, + "maxMs": 0.45150000000001, + "memoryDeltaBytes": 2360632 + }, + { + "name": "Glob (10 files)", + "avgMs": 0.006899799999996503, + "minMs": 0.004416999999989457, + "maxMs": 0.013374999999996362, + "memoryDeltaBytes": 42712 + }, + { + "name": "Glob (100 files)", + "avgMs": 0.006166600000005929, + "minMs": 0.004166000000054737, + "maxMs": 0.01208299999996143, + "memoryDeltaBytes": 42712 + }, + { + "name": "Complex template (mixed features)", + "avgMs": 0.4172166000000175, + "minMs": 0.3972090000000321, + "maxMs": 0.43558300000000827, + "memoryDeltaBytes": 3683552 + } + ] +} \ No newline at end of file diff --git a/bin/cfn b/bin/cfn new file mode 100755 index 0000000..91cee2a --- /dev/null +++ b/bin/cfn @@ -0,0 +1,86 @@ +#!/bin/bash +# cfn - Execute CloudFormation templates with cfn-include preprocessing +# Makes CloudFormation templates directly executable via shebang +set -eo pipefail + +TEMPLATE="$1" +shift + +if [ -z "$TEMPLATE" ]; then + echo "Usage: cfn [options]" >&2 + echo "" >&2 + echo "Options:" >&2 + echo " --deploy Deploy to AWS after processing" >&2 + echo " --stack-name Override stack name (default: filename)" >&2 + echo " --profile AWS profile to use" >&2 + echo " --region AWS region" >&2 + echo "" >&2 + echo "Environment:" >&2 + echo " STACK_NAME Override stack name" >&2 + echo " AWS_PROFILE AWS profile" >&2 + echo " AWS_REGION AWS region" >&2 + exit 1 +fi + +# Parse arguments +DEPLOY=false +STACK_NAME="" +AWS_ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --deploy) + DEPLOY=true + shift + ;; + --stack-name) + STACK_NAME="$2" + shift 2 + ;; + --profile) + AWS_ARGS+=(--profile "$2") + shift 2 + ;; + --region) + AWS_ARGS+=(--region "$2") + shift 2 + ;; + *) + # Pass through to cfn-include + CFN_ARGS+=("$1") + shift + ;; + esac +done + +# Process with cfn-include +OUTPUT=$(mktemp -t cfn-exec.XXXXXX.json) +trap "rm -f '$OUTPUT'" EXIT + +if ! cfn-include "$TEMPLATE" -o "$OUTPUT" "${CFN_ARGS[@]}"; then + echo "❌ cfn-include processing failed" >&2 + exit 1 +fi + +# If not deploying, just print processed template +if [ "$DEPLOY" = false ]; then + cat "$OUTPUT" + exit 0 +fi + +# Determine stack name +if [ -z "$STACK_NAME" ]; then + STACK_NAME=$(basename "$TEMPLATE" .yaml) + STACK_NAME=$(basename "$STACK_NAME" .yml) +fi + +echo "🚀 Deploying stack: $STACK_NAME" >&2 + +# Deploy to AWS +aws cloudformation deploy \ + --template-file "$OUTPUT" \ + --stack-name "$STACK_NAME" \ + --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ + "${AWS_ARGS[@]}" + +echo "✅ Stack deployed: $STACK_NAME" >&2 diff --git a/bin/cli.js b/bin/cli.js deleted file mode 100755 index 3ee9d93..0000000 --- a/bin/cli.js +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable global-require, no-console */ -const exec = require('child_process').execSync; -const path = require('path'); -const _ = require('lodash'); -const pathParse = require('path-parse'); -const yargs = require('yargs'); - -const include = require('../index'); -const yaml = require('../lib/yaml'); -const Client = require('../lib/cfnclient'); -const pkg = require('../package.json'); -const replaceEnv = require('../lib/replaceEnv'); - -yargs.version(false); - -const { env } = process; -// eslint-disable-next-line global-require -const opts = yargs - .command('$0 [path] [options]', pkg.description, (y) => - y.positional('path', { - positional: true, - desc: 'location of template. Either path to a local file, URL or file on an S3 bucket (e.g. s3://bucket-name/example.template)', - required: false, - }) - ) - .options({ - minimize: { - desc: 'minimize JSON output', - default: false, - boolean: true, - alias: 'm', - }, - metadata: { - desc: 'add build metadata to output', - default: false, - boolean: true, - }, - validate: { - desc: 'validate compiled template', - default: false, - boolean: true, - alias: 't', - }, - yaml: { - desc: 'output yaml instead of json', - default: false, - boolean: true, - alias: 'y', - }, - lineWidth: { - desc: 'output yaml line width', - default: 200, - number: true, - alias: 'l', - }, - bucket: { - desc: 'bucket name required for templates larger than 50k', - }, - context: { - desc: - // eslint-disable-next-line max-len - 'template full path. only utilized for stdin when the template is piped to this script', - required: false, - string: true, - }, - prefix: { - desc: 'prefix for templates uploaded to the bucket', - default: 'cfn-include', - }, - enable: { - string: true, - desc: `enable different options: ['env','eval'] or a combination of both via comma.`, - choices: ['', 'env', 'env,eval', 'eval,env', 'eval'], // '' hack - default: '', - }, - inject: { - alias: 'i', - string: true, - // eslint-disable-next-line max-len - desc: `JSON string payload to use for template injection.`, - coerce: (valStr) => JSON.parse(valStr), - }, - doLog: { - boolean: true, - // eslint-disable-next-line max-len - desc: `console log out include options in recurse step`, - }, - version: { - boolean: true, - desc: 'print version and exit', - callback() { - console.log(pkg.version); - process.exit(0); - }, - }, - }) - .parse(); - -// make enable an array -opts.enable = opts.enable.split(','); - -let promise; -if (opts.path) { - let location; - const protocol = opts.path.match(/^\w+:\/\//); - if (protocol) location = opts.path; - else if (pathParse(opts.path).root) location = `file://${opts.path}`; - else location = `file://${path.join(process.cwd(), opts.path)}`; - promise = include({ - url: location, - doEnv: opts.enable.includes('env'), - doEval: opts.enable.includes('eval'), - inject: opts.inject, - doLog: opts.doLog, - }); -} else { - promise = new Promise((resolve, reject) => { - process.stdin.setEncoding('utf8'); - const rawData = []; - process.stdin.on('data', (chunk) => rawData.push(chunk)); - process.stdin.on('error', (err) => reject(err)); - process.stdin.on('end', () => resolve(rawData.join(''))); - }).then((template) => { - if (template.length === 0) { - console.error('empty template received from stdin'); - process.exit(1); - } - - const location = opts.context - ? path.resolve(opts.context) - : path.join(process.cwd(), 'template.yml'); - - template = opts.enable.includes('env') ? replaceEnv(template) : template; - - return include({ - template: yaml.load(template), - url: `file://${location}`, - doEnv: opts.enable.includes('env'), - doEval: opts.enable.includes('eval'), - inject: opts.inject, - doLog: opts.doLog, - }).catch((err) => console.error(err)); - }); -} - -promise - .then(function (template) { - if (opts.metadata) { - let stdout; - try { - stdout = exec('git log -n 1 --pretty=%H', { - stdio: [0, 'pipe', 'ignore'], - }) - .toString() - .trim(); - } catch (e) { - // eslint-disable-next-line no-empty - } - _.defaultsDeep(template, { - Metadata: { - CfnInclude: { - GitCommit: stdout, - BuildDate: new Date().toISOString(), - }, - }, - }); - } - if (opts.validate) { - const cfn = new Client({ - region: env.AWS_REGION || env.AWS_DEFAULT_REGION || 'us-east-1', - bucket: opts.bucket, - prefix: opts.prefix, - }); - return cfn.validateTemplate(JSON.stringify(template)).then(() => template); - } - return template; - }) - .then((template) => { - console.log( - opts.yaml - ? yaml.dump(template, opts) - : JSON.stringify(template, null, opts.minimize ? null : 2) - ); - }) - .catch(function (err) { - if (typeof err.toString === 'function') console.error(err.toString()); - else console.error(err); - console.log(err.stack); - process.exit(1); - }); diff --git a/bin/yaml b/bin/yaml new file mode 120000 index 0000000..9e78ee7 --- /dev/null +++ b/bin/yaml @@ -0,0 +1 @@ +cfn \ No newline at end of file diff --git a/bin/yml b/bin/yml new file mode 120000 index 0000000..9e78ee7 --- /dev/null +++ b/bin/yml @@ -0,0 +1 @@ +cfn \ No newline at end of file diff --git a/commitlint.config.js b/commitlint.config.cjs similarity index 58% rename from commitlint.config.js rename to commitlint.config.cjs index f8e4a00..02c84a8 100644 --- a/commitlint.config.js +++ b/commitlint.config.cjs @@ -2,5 +2,6 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'body-max-line-length': [2, 'always', 200], + 'subject-case': [0, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], }, }; diff --git a/docs/PHASE4-TEMPLATE-STATS-AUTOSPLIT.md b/docs/PHASE4-TEMPLATE-STATS-AUTOSPLIT.md new file mode 100644 index 0000000..26cb3f2 --- /dev/null +++ b/docs/PHASE4-TEMPLATE-STATS-AUTOSPLIT.md @@ -0,0 +1,78 @@ +# Phase 4: Template Stats & Auto-Split Oversized Stacks + +**Date:** February 2026 +**Issue:** [#90](https://github.com/brickhouse-tech/cfn-include/issues/90) +**Status:** Phase 1 In Progress + +--- + +## Problem + +CloudFormation has hard limits that teams inevitably hit: + +- **500 resources** per stack +- **200 outputs** per stack +- **1 MB** (1,048,576 bytes) template body size + +When a stack outgrows these limits, teams must manually split stacks, wire cross-stack references, and maintain deployment ordering. This is tedious, error-prone, and blocks iteration. + +## Phased Approach + +### Phase 1: Detection & Warnings ✅ (PR #93) + +- `--stats` CLI flag reports template statistics to stderr after processing +- Resource count, output count, and template size with % of CloudFormation limits +- Breakdown of resource types and counts +- **80% threshold warnings** — automatically warns when any metric reaches 80% of its limit +- All output goes to stderr so it doesn't interfere with template output on stdout + +**Usage:** + +```bash +# Process template and show stats on stderr +cfn-include template.yml --stats + +# Stats don't break piping +cfn-include template.yml --stats > output.json +``` + +**Files:** + +- `src/lib/stats.ts` — `computeStats()`, `checkThresholds()`, `formatStatsReport()` +- `src/cli.ts` — `--stats` flag integration +- `test/stats.test.ts` — unit tests + +### Phase 2: Suggestions (Planned) + +- Analyze resource dependency graph +- Suggest natural split boundaries (e.g., networking vs compute vs data) +- Output a report: "Split these N resource groups into separate stacks" + +### Phase 3: Auto-Split (Planned) + +- `cfn-include --auto-split` generates: + - Multiple child stack templates + - Cross-stack `Fn::ImportValue` / `Export` references auto-wired + - Parent/orchestrator stack with `AWS::CloudFormation::Stack` nested references + - Correct dependency ordering + +### Phase 4: Managed Multi-Stack (Planned) + +- Track split stacks as a "stack group" +- Deploy/update/rollback as a unit +- Drift detection across the group + +## Why This Matters + +- **Unique capability** — no other CFN preprocessor does this +- **Real pain point** — every team hits the 500 resource wall eventually +- **cfn-include is already positioned** — `Fn::Eval` (executable YAML) + includes + auto-split = the complete CFN toolchain +- ~4,600 downloads/week already + +## CloudFormation Limits Reference + +| Metric | Limit | Warning Threshold (80%) | +|--------|-------|------------------------| +| Resources per stack | 500 | 400 | +| Outputs per stack | 200 | 160 | +| Template body size | 1 MB (1,048,576 bytes) | ~800 KB | diff --git a/docs/PHASE5-CDK-INTEGRATION-ANALYSIS.md b/docs/PHASE5-CDK-INTEGRATION-ANALYSIS.md new file mode 100644 index 0000000..3361b8b --- /dev/null +++ b/docs/PHASE5-CDK-INTEGRATION-ANALYSIS.md @@ -0,0 +1,635 @@ +# Phase 5: AWS CDK Import/Eject Functionality Analysis + +**Date:** February 8, 2026 +**Author:** TARS (nmccready-tars) +**Status:** Research & Design Complete + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [AWS CDK CfnInclude Module Research](#2-aws-cdk-cfninclude-module-research) +3. [Existing Tools Analysis](#3-existing-tools-analysis) +4. [EJECT Functionality Design (cfn-include → CDK)](#4-eject-functionality-design-cfn-include--cdk) +5. [IMPORT Functionality Design (CDK → cfn-include)](#5-import-functionality-design-cdk--cfn-include) +6. [CLI Design](#6-cli-design) +7. [Challenges and Limitations](#7-challenges-and-limitations) +8. [Implementation Roadmap](#8-implementation-roadmap) +9. [Appendix: cfn-include Function Mapping](#appendix-cfn-include-function-mapping) + +--- + +## 1. Executive Summary + +This document analyzes the feasibility and design of bidirectional integration between `cfn-include` (a CloudFormation template preprocessor) and AWS CDK (Cloud Development Kit). The goal is to enable: + +1. **EJECT** - Convert cfn-include templates to AWS CDK TypeScript/Python code +2. **IMPORT** - Convert CDK-synthesized CloudFormation templates back to optimized cfn-include templates + +### Key Findings + +- **EJECT is feasible** but requires a two-phase approach: first resolve all custom `Fn::*` functions, then convert to CDK +- **IMPORT has limited utility** - CDK templates are already optimized for CDK workflows; reverse engineering patterns is complex +- **Existing tools exist** - `cdk-from-cfn` handles the CloudFormation → CDK conversion well +- **Fn::Eval is non-portable** - Arbitrary JavaScript cannot be safely converted to CDK + +### Recommended Approach + +Implement EJECT as a wrapper around existing tools (`cdk-from-cfn`) with a pre-processing step that resolves cfn-include's custom functions. IMPORT should focus on pattern detection for migration assistance rather than full conversion. + +--- + +## 2. AWS CDK CfnInclude Module Research + +### 2.1 What is @aws-cdk/cloudformation-include? + +The `cloudformation-include` module (now `aws-cdk-lib/cloudformation-include`) allows importing existing CloudFormation templates into CDK applications. It provides: + +```typescript +import * as cfn_inc from 'aws-cdk-lib/cloudformation-include'; + +const cfnTemplate = new cfn_inc.CfnInclude(this, 'Template', { + templateFile: 'my-template.json', +}); +``` + +### 2.2 Key Capabilities + +| Feature | Description | +|---------|-------------| +| **Template Import** | Import JSON/YAML CloudFormation templates | +| **Resource Access** | Get L1 constructs via `getResource('LogicalId')` | +| **L1 → L2 Conversion** | Convert with `fromCfn*()` methods (e.g., `Key.fromCfnKey()`) | +| **Parameter Replacement** | Replace parameters with build-time values | +| **Nested Stacks** | Support for `AWS::CloudFormation::Stack` resources | +| **Preserve Logical IDs** | Option to keep original IDs or rename with CDK algorithm | + +### 2.3 Construct Levels in CDK + +| Level | Description | Example | +|-------|-------------|---------| +| **L1 (CfnXxx)** | 1:1 mapping to CloudFormation resources | `CfnBucket` | +| **L2 (High-Level)** | Abstractions with sensible defaults | `Bucket` | +| **L3 (Patterns)** | Multi-resource patterns | `ApplicationLoadBalancedFargateService` | + +**cfn-include templates produce L1 constructs when imported via CfnInclude**, since they're raw CloudFormation. + +### 2.4 Converting L1 to L2 + +Two methods: + +**Method 1: `fromCfn*()` methods (Preferred)** +```typescript +const cfnKey = cfnTemplate.getResource('Key') as kms.CfnKey; +const key = kms.Key.fromCfnKey(cfnKey); // Mutable L2 +``` + +**Method 2: `fromXxxArn/Name()` methods (Fallback)** +```typescript +const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; +const bucket = s3.Bucket.fromBucketName(this, 'L2Bucket', cfnBucket.ref); // Immutable +``` + +### 2.5 Limitations of CfnInclude + +- Does not execute CloudFormation transforms +- Cannot handle cycles between resources (unless `allowCyclicalReferences: true`) +- Resources using complex `Fn::If` may not convert to L2 +- Custom resources return as generic `CfnResource` + +--- + +## 3. Existing Tools Analysis + +### 3.1 cdk-from-cfn + +**Repository:** `cdklabs/cdk-from-cfn` +**Type:** Rust CLI with WASM bindings (npm package available) + +**Features:** +- Converts CloudFormation JSON/YAML to CDK code +- Supports: TypeScript, Python, Java, Go, C# +- Generates either `Stack` or `Construct` classes +- Handles most intrinsic functions + +**Supported Intrinsic Functions:** +- ✅ `Fn::FindInMap`, `Fn::Join`, `Fn::Sub`, `Ref` +- ✅ `Fn::And`, `Fn::Equals`, `Fn::If`, `Fn::Not`, `Fn::Or` +- ✅ `Fn::GetAtt`, `Fn::Base64`, `Fn::ImportValue`, `Fn::Select` +- ✅ `Fn::GetAZs`, `Fn::Cidr` +- ❌ SSM/SecretsManager dynamic references +- ❌ Create policies + +**Usage:** +```bash +# CLI +cdk-from-cfn template.json output.ts --language typescript --stack-name MyStack + +# Node.js +import * as cdk_from_cfn from 'cdk-from-cfn'; +const cdkCode = cdk_from_cfn.transmute(template, 'typescript', 'MyStack'); +``` + +### 3.2 AWS CloudFormation Registry + +The CDK uses CloudFormation schema definitions from the registry to generate L1 constructs. This is why `cdk-from-cfn` can accurately map resources. + +### 3.3 Other Tools + +| Tool | Description | Status | +|------|-------------|--------| +| **former2** | Generates IaC from existing AWS resources | Alternative approach | +| **AWS CDK Migrate** | Official tool for importing existing stacks | Stack-focused, not template-focused | + +--- + +## 4. EJECT Functionality Design (cfn-include → CDK) + +### 4.1 Overview + +Convert a cfn-include template (with custom `Fn::*` functions) to AWS CDK code. + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ cfn-include │────▶│ Standard CFN │────▶│ CDK Code │ +│ Template │ │ Template │ │ (TS/Python) │ +│ (Fn::Include, │ │ (Resolved) │ │ │ +│ Fn::Map, etc) │ │ │ │ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + Phase 1 Phase 2 Phase 3 + (cfn-include) (cdk-from-cfn) (Code Generation) +``` + +### 4.2 Phase 1: Resolve cfn-include Functions + +Use the existing `cfn-include` CLI to resolve all custom functions: + +```bash +cfn-include input.yml --enable env,eval > resolved.json +``` + +This handles: +- `Fn::Include` → File contents inlined +- `Fn::Map` → Array expanded +- `Fn::Flatten` → Arrays flattened +- `Fn::Merge` → Objects merged +- `Fn::RefNow` → References resolved +- etc. + +### 4.3 Phase 2: Convert to CDK + +Pass the resolved CloudFormation template to `cdk-from-cfn`: + +```typescript +import * as cdk_from_cfn from 'cdk-from-cfn'; +import * as cfnInclude from '@znemz/cfn-include'; + +async function eject(templatePath: string, language: string) { + // Phase 1: Resolve cfn-include functions + const resolved = await cfnInclude({ + url: `file://${templatePath}`, + doEnv: true, + doEval: true, + }); + + // Phase 2: Convert to CDK + const cdkCode = cdk_from_cfn.transmute( + JSON.stringify(resolved), + language, + 'GeneratedStack' + ); + + return cdkCode; +} +``` + +### 4.4 Phase 3: Output Structure + +Generate a complete CDK application structure: + +``` +output/ +├── package.json +├── tsconfig.json +├── cdk.json +├── lib/ +│ └── generated-stack.ts # Generated CDK code +├── bin/ +│ └── app.ts # CDK app entry point +└── README.md # Migration notes +``` + +### 4.5 Preserving Intent with Comments + +Since cfn-include functions carry semantic meaning that's lost in resolution, we should: + +1. **Parse original template** to detect cfn-include function usage +2. **Generate comments** in output explaining original patterns + +```typescript +// Originally: Fn::Map over [80, 443] with template for each port +const securityGroupIngress80 = new ec2.CfnSecurityGroupIngress(this, 'Ingress80', { + // ... +}); +const securityGroupIngress443 = new ec2.CfnSecurityGroupIngress(this, 'Ingress443', { + // ... +}); +``` + +### 4.6 Handling Special Cases + +#### Fn::Eval (JavaScript Evaluation) +```yaml +# NOT CONVERTIBLE - Contains arbitrary JS +Fn::Eval: + state: [1, 2, 3] + script: "state.map(v => v * 2);" +``` +**Strategy:** Warn user, output as comment with manual intervention required. + +#### Fn::Include with dynamic paths +```yaml +# Dynamic path based on environment +Fn::Include: "${ENVIRONMENT}/config.yml" +``` +**Strategy:** Resolve at eject time with specific environment, document the source. + +#### Fn::IfEval (Conditional JS) +```yaml +Fn::IfEval: + evalCond: "('${ENV}' === 'prod')" + truthy: { ... } + falsy: { ... } +``` +**Strategy:** Evaluate at eject time, document the condition that was used. + +--- + +## 5. IMPORT Functionality Design (CDK → cfn-include) + +### 5.1 Overview + +Convert a CDK-synthesized CloudFormation template back to an optimized cfn-include template. + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ CDK Stack │────▶│ cdk synth │────▶│ cfn-include │ +│ (TypeScript) │ │ output.json │ │ optimized.yml │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + Pattern Detection + - Repeated resources + - Similar structures + - Extractable modules +``` + +### 5.2 Pattern Detection Algorithms + +#### 5.2.1 Repeated Resource Pattern → Fn::Map + +Detect resources with similar structure that differ only in specific values: + +```json +// Input (synthesized CFN) +{ + "SubnetA": { "Type": "AWS::EC2::Subnet", "Properties": { "AvailabilityZone": "us-east-1a" } }, + "SubnetB": { "Type": "AWS::EC2::Subnet", "Properties": { "AvailabilityZone": "us-east-1b" } }, + "SubnetC": { "Type": "AWS::EC2::Subnet", "Properties": { "AvailabilityZone": "us-east-1c" } } +} +``` + +```yaml +# Output (cfn-include) +Fn::Merge: + Fn::Map: + - [a, b, c] + - AZ + - Subnet${AZ}: + Type: AWS::EC2::Subnet + Properties: + AvailabilityZone: !Sub us-east-1${AZ} +``` + +**Algorithm:** +1. Group resources by Type +2. Compare property structures (ignoring values) +3. Identify varying fields +4. Check if variations follow a pattern (sequential, from list, etc.) +5. Generate `Fn::Map` if pattern detected + +#### 5.2.2 Similar Structure Detection → Fn::Include + +Identify resource blocks that could be extracted to reusable includes: + +```json +// Input: Multiple Lambda functions with similar config +{ + "Function1": { "Type": "AWS::Lambda::Function", "Properties": { "Runtime": "nodejs18.x", "Handler": "index.handler", "MemorySize": 256 } }, + "Function2": { "Type": "AWS::Lambda::Function", "Properties": { "Runtime": "nodejs18.x", "Handler": "index.handler", "MemorySize": 256 } } +} +``` + +```yaml +# Output: Extracted include +# includes/lambda-defaults.yml +Type: AWS::Lambda::Function +Properties: + Runtime: nodejs18.x + Handler: index.handler + MemorySize: 256 + +# main.yml +Function1: + Fn::DeepMerge: + - Fn::Include: includes/lambda-defaults.yml + - Properties: + FunctionName: function-1 +Function2: + Fn::DeepMerge: + - Fn::Include: includes/lambda-defaults.yml + - Properties: + FunctionName: function-2 +``` + +### 5.3 Optimization Strategies + +| Strategy | Description | Trigger | +|----------|-------------|---------| +| **Fn::Map extraction** | Convert repeated resources to map | 3+ similar resources | +| **Fn::Include extraction** | Extract common patterns to files | Repeated structures | +| **Fn::Merge usage** | Combine base + overrides | Inheritance patterns | +| **Fn::Sequence generation** | Detect sequential patterns | A, B, C or 1, 2, 3 patterns | + +### 5.4 Limitations of IMPORT + +1. **Loss of L2 abstraction** - CDK synthesizes to L1, can't recover L2 intent +2. **CDK metadata** - Contains CDK-specific constructs that don't reverse well +3. **Token resolution** - CDK tokens are resolved, original references lost +4. **Code generation impossible** - Can't regenerate original TypeScript/Python + +### 5.5 Recommended Use Cases for IMPORT + +- **Migration assistance** - Help identify patterns when migrating away from CDK +- **Template optimization** - Reduce template size by extracting patterns +- **Documentation** - Generate simplified cfn-include views of complex CDK stacks + +--- + +## 6. CLI Design + +### 6.1 EJECT Command + +```bash +cfn-include eject