├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .node-version ├── .npmignore ├── .nvmrc ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── LICENSE ├── README.md ├── images ├── logo.png ├── logo.svg ├── wordmark-dark.png ├── wordmark-dark.svg ├── wordmark-dynamic.png ├── wordmark-dynamic.svg ├── wordmark-light.png ├── wordmark-light.svg ├── wordmark.png └── wordmark.svg ├── package.json ├── src ├── builder │ ├── index.ts │ └── struct.ts ├── index.ts ├── private │ ├── assembly.ts │ ├── index.ts │ └── utils.ts ├── projen │ ├── index.ts │ ├── projen-struct.ts │ └── ts-interface.ts └── renderer │ ├── index.ts │ └── typescript.ts ├── test ├── builder │ ├── __snapshots__ │ │ └── struct.test.ts.snap │ └── struct.test.ts ├── projen │ ├── __snapshots__ │ │ ├── projen-struct.test.ts.snap │ │ └── ts-interface.test.ts.snap │ ├── projen-struct.test.ts │ └── ts-interface.test.ts └── renderer │ ├── __snapshots__ │ └── typescript.test.ts.snap │ └── typescript.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import", 11 | "@stylistic" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module", 17 | "project": "./tsconfig.dev.json" 18 | }, 19 | "extends": [ 20 | "plugin:import/typescript" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "*.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage", 43 | "!.projenrc.ts", 44 | "!projenrc/**/*.ts" 45 | ], 46 | "rules": { 47 | "@stylistic/indent": [ 48 | "error", 49 | 2 50 | ], 51 | "@stylistic/quotes": [ 52 | "error", 53 | "single", 54 | { 55 | "avoidEscape": true 56 | } 57 | ], 58 | "@stylistic/comma-dangle": [ 59 | "error", 60 | "always-multiline" 61 | ], 62 | "@stylistic/comma-spacing": [ 63 | "error", 64 | { 65 | "before": false, 66 | "after": true 67 | } 68 | ], 69 | "@stylistic/no-multi-spaces": [ 70 | "error", 71 | { 72 | "ignoreEOLComments": false 73 | } 74 | ], 75 | "@stylistic/array-bracket-spacing": [ 76 | "error", 77 | "never" 78 | ], 79 | "@stylistic/array-bracket-newline": [ 80 | "error", 81 | "consistent" 82 | ], 83 | "@stylistic/object-curly-spacing": [ 84 | "error", 85 | "always" 86 | ], 87 | "@stylistic/object-curly-newline": [ 88 | "error", 89 | { 90 | "multiline": true, 91 | "consistent": true 92 | } 93 | ], 94 | "@stylistic/object-property-newline": [ 95 | "error", 96 | { 97 | "allowAllPropertiesOnSameLine": true 98 | } 99 | ], 100 | "@stylistic/keyword-spacing": [ 101 | "error" 102 | ], 103 | "@stylistic/brace-style": [ 104 | "error", 105 | "1tbs", 106 | { 107 | "allowSingleLine": true 108 | } 109 | ], 110 | "@stylistic/space-before-blocks": [ 111 | "error" 112 | ], 113 | "@stylistic/member-delimiter-style": [ 114 | "error" 115 | ], 116 | "@stylistic/semi": [ 117 | "error", 118 | "always" 119 | ], 120 | "@stylistic/max-len": [ 121 | "error", 122 | { 123 | "code": 150, 124 | "ignoreUrls": true, 125 | "ignoreStrings": true, 126 | "ignoreTemplateLiterals": true, 127 | "ignoreComments": true, 128 | "ignoreRegExpLiterals": true 129 | } 130 | ], 131 | "@stylistic/quote-props": [ 132 | "error", 133 | "consistent-as-needed" 134 | ], 135 | "@stylistic/key-spacing": [ 136 | "error" 137 | ], 138 | "@stylistic/no-multiple-empty-lines": [ 139 | "error" 140 | ], 141 | "@stylistic/no-trailing-spaces": [ 142 | "error" 143 | ], 144 | "curly": [ 145 | "error", 146 | "multi-line", 147 | "consistent" 148 | ], 149 | "@typescript-eslint/no-require-imports": "error", 150 | "import/no-extraneous-dependencies": [ 151 | "error", 152 | { 153 | "devDependencies": [ 154 | "**/test/**", 155 | "**/build-tools/**", 156 | ".projenrc.ts", 157 | "projenrc/**/*.ts" 158 | ], 159 | "optionalDependencies": false, 160 | "peerDependencies": true 161 | } 162 | ], 163 | "import/no-unresolved": [ 164 | "error" 165 | ], 166 | "import/order": [ 167 | "warn", 168 | { 169 | "groups": [ 170 | "builtin", 171 | "external" 172 | ], 173 | "alphabetize": { 174 | "order": "asc", 175 | "caseInsensitive": true 176 | } 177 | } 178 | ], 179 | "import/no-duplicates": [ 180 | "error" 181 | ], 182 | "no-shadow": [ 183 | "off" 184 | ], 185 | "@typescript-eslint/no-shadow": "error", 186 | "@typescript-eslint/no-floating-promises": "error", 187 | "no-return-await": [ 188 | "off" 189 | ], 190 | "@typescript-eslint/return-await": "error", 191 | "dot-notation": [ 192 | "error" 193 | ], 194 | "no-bitwise": [ 195 | "error" 196 | ], 197 | "@typescript-eslint/member-ordering": [ 198 | "error", 199 | { 200 | "default": [ 201 | "public-static-field", 202 | "public-static-method", 203 | "protected-static-field", 204 | "protected-static-method", 205 | "private-static-field", 206 | "private-static-method", 207 | "field", 208 | "constructor", 209 | "method" 210 | ] 211 | } 212 | ], 213 | "eol-last": [ 214 | "error", 215 | "always" 216 | ], 217 | "space-in-parens": [ 218 | "error", 219 | "never" 220 | ] 221 | }, 222 | "overrides": [ 223 | { 224 | "files": [ 225 | ".projenrc.ts" 226 | ], 227 | "rules": { 228 | "@typescript-eslint/no-require-imports": "off", 229 | "import/no-extraneous-dependencies": "off" 230 | } 231 | } 232 | ] 233 | } 234 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/auto-approve.yml linguist-generated 9 | /.github/workflows/build.yml linguist-generated 10 | /.github/workflows/pull-request-lint.yml linguist-generated 11 | /.github/workflows/release.yml linguist-generated 12 | /.github/workflows/upgrade-main.yml linguist-generated 13 | /.gitignore linguist-generated 14 | /.mergify.yml linguist-generated 15 | /.node-version linguist-generated 16 | /.npmignore linguist-generated 17 | /.nvmrc linguist-generated 18 | /.projen/** linguist-generated 19 | /.projen/deps.json linguist-generated 20 | /.projen/files.json linguist-generated 21 | /.projen/tasks.json linguist-generated 22 | /images/logo.svg linguist-generated 23 | /images/wordmark-dark.svg linguist-generated 24 | /images/wordmark-dynamic.svg linguist-generated 25 | /images/wordmark-light.svg linguist-generated 26 | /images/wordmark.svg linguist-generated 27 | /LICENSE linguist-generated 28 | /package.json linguist-generated 29 | /tsconfig.dev.json linguist-generated 30 | /tsconfig.json linguist-generated 31 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'projen-builder[bot]' || github.event.pull_request.user.login == 'mrgrain') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Install dependencies 23 | run: yarn install --check-files 24 | - name: build 25 | run: npx projen build 26 | - name: Find mutations 27 | id: self_mutation 28 | run: |- 29 | git add . 30 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 31 | working-directory: ./ 32 | - name: Upload patch 33 | if: steps.self_mutation.outputs.self_mutation_happened 34 | uses: actions/upload-artifact@v4.4.0 35 | with: 36 | name: repo.patch 37 | path: repo.patch 38 | overwrite: true 39 | - name: Fail build on mutation 40 | if: steps.self_mutation.outputs.self_mutation_happened 41 | run: |- 42 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 43 | cat repo.patch 44 | exit 1 45 | self-mutation: 46 | needs: build 47 | runs-on: ubuntu-latest 48 | permissions: 49 | contents: write 50 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 51 | steps: 52 | - name: Generate token 53 | id: generate_token 54 | uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 55 | with: 56 | app-id: ${{ secrets.PROJEN_APP_ID }} 57 | private-key: ${{ secrets.PROJEN_APP_PRIVATE_KEY }} 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | token: ${{ steps.generate_token.outputs.token }} 62 | ref: ${{ github.event.pull_request.head.ref }} 63 | repository: ${{ github.event.pull_request.head.repo.full_name }} 64 | - name: Download patch 65 | uses: actions/download-artifact@v4 66 | with: 67 | name: repo.patch 68 | path: ${{ runner.temp }} 69 | - name: Apply patch 70 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 71 | - name: Set git identity 72 | run: |- 73 | git config user.name "github-actions" 74 | git config user.email "github-actions@github.com" 75 | - name: Push changes 76 | env: 77 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 78 | run: |- 79 | git add . 80 | git commit -s -m "chore: self mutation" 81 | git push origin HEAD:$PULL_REQUEST_REF 82 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | chore 28 | ci 29 | docs 30 | feat 31 | fix 32 | revert 33 | requireScope: false 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Install dependencies 32 | run: yarn install --check-files --frozen-lockfile 33 | - name: release 34 | run: npx projen release 35 | - name: Check if version has already been tagged 36 | id: check_tag_exists 37 | run: |- 38 | TAG=$(cat dist/releasetag.txt) 39 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 40 | cat $GITHUB_OUTPUT 41 | - name: Check for new commits 42 | id: git_remote 43 | run: |- 44 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 45 | cat $GITHUB_OUTPUT 46 | - name: Backup artifact permissions 47 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 48 | run: cd dist && getfacl -R . > permissions-backup.acl 49 | continue-on-error: true 50 | - name: Upload artifact 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | uses: actions/upload-artifact@v4.4.0 53 | with: 54 | name: build-artifact 55 | path: dist 56 | overwrite: true 57 | release_github: 58 | name: Publish to GitHub Releases 59 | needs: 60 | - release 61 | - release_npm 62 | runs-on: ubuntu-latest 63 | permissions: 64 | contents: write 65 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 66 | steps: 67 | - uses: actions/setup-node@v4 68 | with: 69 | node-version: lts/* 70 | - name: Download build artifacts 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: build-artifact 74 | path: dist 75 | - name: Restore build artifact permissions 76 | run: cd dist && setfacl --restore=permissions-backup.acl 77 | continue-on-error: true 78 | - name: Release 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_SHA 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 82 | release_npm: 83 | name: Publish to npm 84 | needs: release 85 | runs-on: ubuntu-latest 86 | permissions: 87 | id-token: write 88 | contents: read 89 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 90 | steps: 91 | - uses: actions/setup-node@v4 92 | with: 93 | node-version: lts/* 94 | - name: Download build artifacts 95 | uses: actions/download-artifact@v4 96 | with: 97 | name: build-artifact 98 | path: dist 99 | - name: Restore build artifact permissions 100 | run: cd dist && setfacl --restore=permissions-backup.acl 101 | continue-on-error: true 102 | - name: Release 103 | env: 104 | NPM_DIST_TAG: latest 105 | NPM_REGISTRY: registry.npmjs.org 106 | NPM_CONFIG_PROVENANCE: "true" 107 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 108 | run: npx -p publib@latest publib-npm 109 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Install dependencies 22 | run: yarn install --check-files --frozen-lockfile 23 | - name: Upgrade dependencies 24 | run: npx projen upgrade 25 | - name: Find mutations 26 | id: create_patch 27 | run: |- 28 | git add . 29 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 30 | working-directory: ./ 31 | - name: Upload patch 32 | if: steps.create_patch.outputs.patch_created 33 | uses: actions/upload-artifact@v4.4.0 34 | with: 35 | name: repo.patch 36 | path: repo.patch 37 | overwrite: true 38 | pr: 39 | name: Create Pull Request 40 | needs: upgrade 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | if: ${{ needs.upgrade.outputs.patch_created }} 45 | steps: 46 | - name: Generate token 47 | id: generate_token 48 | uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 49 | with: 50 | app-id: ${{ secrets.PROJEN_APP_ID }} 51 | private-key: ${{ secrets.PROJEN_APP_PRIVATE_KEY }} 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | with: 55 | ref: main 56 | - name: Download patch 57 | uses: actions/download-artifact@v4 58 | with: 59 | name: repo.patch 60 | path: ${{ runner.temp }} 61 | - name: Apply patch 62 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 63 | - name: Set git identity 64 | run: |- 65 | git config user.name "github-actions" 66 | git config user.email "github-actions@github.com" 67 | - name: Create Pull Request 68 | id: create-pr 69 | uses: peter-evans/create-pull-request@v6 70 | with: 71 | token: ${{ steps.generate_token.outputs.token }} 72 | commit-message: |- 73 | chore(deps): upgrade dependencies 74 | 75 | Upgrades project dependencies. See details in [workflow run]. 76 | 77 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 78 | 79 | ------ 80 | 81 | *Automatically created by projen via the "upgrade-main" workflow* 82 | branch: github-actions/upgrade-main 83 | title: "chore(deps): upgrade dependencies" 84 | labels: auto-approve 85 | body: |- 86 | Upgrades project dependencies. See details in [workflow run]. 87 | 88 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 89 | 90 | ------ 91 | 92 | *Automatically created by projen via the "upgrade-main" workflow* 93 | author: github-actions 94 | committer: github-actions 95 | signoff: true 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | /test-reports/ 35 | junit.xml 36 | /coverage/ 37 | !/.github/workflows/build.yml 38 | /dist/changelog.md 39 | /dist/version.txt 40 | !/.github/workflows/release.yml 41 | !/.mergify.yml 42 | !/.github/workflows/upgrade-main.yml 43 | !/.github/pull_request_template.md 44 | !/test/ 45 | !/tsconfig.json 46 | !/tsconfig.dev.json 47 | !/src/ 48 | /lib 49 | /dist/ 50 | !/.eslintrc.json 51 | !/images/logo.svg 52 | !/images/wordmark.svg 53 | !/images/wordmark-dark.svg 54 | !/images/wordmark-light.svg 55 | !/images/wordmark-dynamic.svg 56 | !/.nvmrc 57 | !/.node-version 58 | !/.projenrc.ts 59 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | merge_method: squash 11 | commit_message_template: |- 12 | {{ title }} (#{{ number }}) 13 | 14 | {{ body }} 15 | pull_request_rules: 16 | - name: Automatic merge on approval and successful build 17 | actions: 18 | delete_head_branch: {} 19 | queue: 20 | name: default 21 | conditions: 22 | - "#approved-reviews-by>=1" 23 | - -label~=(do-not-merge) 24 | - status-success=build 25 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | .projenrc.ts 3 | projenrc 4 | .gitattributes 5 | /.projen/ 6 | /test-reports/ 7 | junit.xml 8 | /coverage/ 9 | permissions-backup.acl 10 | /dist/changelog.md 11 | /dist/version.txt 12 | /.mergify.yml 13 | /test/ 14 | /tsconfig.dev.json 15 | /src/ 16 | !/lib/ 17 | !/lib/**/*.js 18 | !/lib/**/*.d.ts 19 | dist 20 | /tsconfig.json 21 | /.github/ 22 | /.vscode/ 23 | /.idea/ 24 | /.projenrc.js 25 | tsconfig.tsbuildinfo 26 | /.eslintrc.json 27 | images/logo.svg 28 | images/logo.png 29 | images/wordmark.svg 30 | images/wordmark.png 31 | images/wordmark-dark.svg 32 | images/wordmark-dark.png 33 | images/wordmark-light.svg 34 | images/wordmark-light.png 35 | images/wordmark-dynamic.svg 36 | images/wordmark-dynamic.png 37 | .nvmrc 38 | .node-version 39 | /.gitattributes 40 | /.projenrc.ts 41 | /projenrc 42 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@stylistic/eslint-plugin", 5 | "version": "^2", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@types/jest", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@types/node", 14 | "type": "build" 15 | }, 16 | { 17 | "name": "@typescript-eslint/eslint-plugin", 18 | "version": "^8", 19 | "type": "build" 20 | }, 21 | { 22 | "name": "@typescript-eslint/parser", 23 | "version": "^8", 24 | "type": "build" 25 | }, 26 | { 27 | "name": "commit-and-tag-version", 28 | "version": "^12", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "constructs", 33 | "version": "^10.0.0", 34 | "type": "build" 35 | }, 36 | { 37 | "name": "eslint-import-resolver-typescript", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint-plugin-import", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "eslint", 46 | "version": "^9", 47 | "type": "build" 48 | }, 49 | { 50 | "name": "jest", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "jest-junit", 55 | "version": "^16", 56 | "type": "build" 57 | }, 58 | { 59 | "name": "mrpj", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "projen", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "ts-jest", 68 | "type": "build" 69 | }, 70 | { 71 | "name": "ts-node", 72 | "type": "build" 73 | }, 74 | { 75 | "name": "typescript", 76 | "type": "build" 77 | }, 78 | { 79 | "name": "projen", 80 | "version": "x.x.x", 81 | "type": "peer" 82 | }, 83 | { 84 | "name": "@jsii/spec", 85 | "type": "runtime" 86 | } 87 | ], 88 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 89 | } 90 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/build.yml", 8 | ".github/workflows/pull-request-lint.yml", 9 | ".github/workflows/release.yml", 10 | ".github/workflows/upgrade-main.yml", 11 | ".gitignore", 12 | ".mergify.yml", 13 | ".node-version", 14 | ".npmignore", 15 | ".nvmrc", 16 | ".projen/deps.json", 17 | ".projen/files.json", 18 | ".projen/tasks.json", 19 | "images/logo.svg", 20 | "images/wordmark-dark.svg", 21 | "images/wordmark-dynamic.svg", 22 | "images/wordmark-light.svg", 23 | "images/wordmark.svg", 24 | "LICENSE", 25 | "tsconfig.dev.json", 26 | "tsconfig.json" 27 | ], 28 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 29 | } 30 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 37 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(chore|feat|fix|revert){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\" -- ." 38 | }, 39 | "steps": [ 40 | { 41 | "builtin": "release/bump-version" 42 | } 43 | ], 44 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 45 | }, 46 | "clobber": { 47 | "name": "clobber", 48 | "description": "hard resets to HEAD of origin and cleans the local repo", 49 | "env": { 50 | "BRANCH": "$(git branch --show-current)" 51 | }, 52 | "steps": [ 53 | { 54 | "exec": "git checkout -b scratch", 55 | "name": "save current HEAD in \"scratch\" branch" 56 | }, 57 | { 58 | "exec": "git checkout $BRANCH" 59 | }, 60 | { 61 | "exec": "git fetch origin", 62 | "name": "fetch latest changes from origin" 63 | }, 64 | { 65 | "exec": "git reset --hard origin/$BRANCH", 66 | "name": "hard reset to origin commit" 67 | }, 68 | { 69 | "exec": "git clean -fdx", 70 | "name": "clean all untracked files" 71 | }, 72 | { 73 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 74 | } 75 | ], 76 | "condition": "git diff --exit-code > /dev/null" 77 | }, 78 | "compile": { 79 | "name": "compile", 80 | "description": "Only compile", 81 | "steps": [ 82 | { 83 | "exec": "tsc --build" 84 | } 85 | ] 86 | }, 87 | "default": { 88 | "name": "default", 89 | "description": "Synthesize project files", 90 | "steps": [ 91 | { 92 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 93 | } 94 | ] 95 | }, 96 | "eject": { 97 | "name": "eject", 98 | "description": "Remove projen from the project", 99 | "env": { 100 | "PROJEN_EJECTING": "true" 101 | }, 102 | "steps": [ 103 | { 104 | "spawn": "default" 105 | } 106 | ] 107 | }, 108 | "eslint": { 109 | "name": "eslint", 110 | "description": "Runs eslint against the codebase", 111 | "env": { 112 | "ESLINT_USE_FLAT_CONFIG": "false" 113 | }, 114 | "steps": [ 115 | { 116 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", 117 | "receiveArgs": true 118 | } 119 | ] 120 | }, 121 | "install": { 122 | "name": "install", 123 | "description": "Install project dependencies and update lockfile (non-frozen)", 124 | "steps": [ 125 | { 126 | "exec": "yarn install --check-files" 127 | } 128 | ] 129 | }, 130 | "install:ci": { 131 | "name": "install:ci", 132 | "description": "Install project dependencies using frozen lockfile", 133 | "steps": [ 134 | { 135 | "exec": "yarn install --check-files --frozen-lockfile" 136 | } 137 | ] 138 | }, 139 | "logo": { 140 | "name": "logo", 141 | "steps": [ 142 | { 143 | "exec": "rsvg-convert -h 1024 images/logo.svg > images/logo.png" 144 | }, 145 | { 146 | "exec": "rsvg-convert -h 1024 images/wordmark.svg > images/wordmark.png" 147 | }, 148 | { 149 | "exec": "rsvg-convert -h 1024 images/wordmark-dark.svg > images/wordmark-dark.png" 150 | }, 151 | { 152 | "exec": "rsvg-convert -h 1024 images/wordmark-light.svg > images/wordmark-light.png" 153 | }, 154 | { 155 | "exec": "rsvg-convert -h 1024 images/wordmark-dynamic.svg > images/wordmark-dynamic.png" 156 | } 157 | ] 158 | }, 159 | "package": { 160 | "name": "package", 161 | "description": "Creates the distribution package", 162 | "steps": [ 163 | { 164 | "exec": "mkdir -p dist/js" 165 | }, 166 | { 167 | "exec": "npm pack --pack-destination dist/js" 168 | } 169 | ] 170 | }, 171 | "post-compile": { 172 | "name": "post-compile", 173 | "description": "Runs after successful compilation" 174 | }, 175 | "post-upgrade": { 176 | "name": "post-upgrade", 177 | "description": "Runs after upgrading dependencies" 178 | }, 179 | "pre-compile": { 180 | "name": "pre-compile", 181 | "description": "Prepare the project for compilation" 182 | }, 183 | "release": { 184 | "name": "release", 185 | "description": "Prepare a release from \"main\" branch", 186 | "env": { 187 | "RELEASE": "true" 188 | }, 189 | "steps": [ 190 | { 191 | "exec": "rm -fr dist" 192 | }, 193 | { 194 | "spawn": "bump" 195 | }, 196 | { 197 | "spawn": "build" 198 | }, 199 | { 200 | "spawn": "unbump" 201 | }, 202 | { 203 | "exec": "git diff --ignore-space-at-eol --exit-code" 204 | } 205 | ] 206 | }, 207 | "test": { 208 | "name": "test", 209 | "description": "Run tests", 210 | "steps": [ 211 | { 212 | "exec": "jest --passWithNoTests --updateSnapshot", 213 | "receiveArgs": true 214 | }, 215 | { 216 | "spawn": "eslint" 217 | } 218 | ] 219 | }, 220 | "test:watch": { 221 | "name": "test:watch", 222 | "description": "Run jest in watch mode", 223 | "steps": [ 224 | { 225 | "exec": "jest --watch" 226 | } 227 | ] 228 | }, 229 | "unbump": { 230 | "name": "unbump", 231 | "description": "Restores version to 0.0.0", 232 | "env": { 233 | "OUTFILE": "package.json", 234 | "CHANGELOG": "dist/changelog.md", 235 | "BUMPFILE": "dist/version.txt", 236 | "RELEASETAG": "dist/releasetag.txt", 237 | "RELEASE_TAG_PREFIX": "", 238 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 239 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(chore|feat|fix|revert){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\" -- ." 240 | }, 241 | "steps": [ 242 | { 243 | "builtin": "release/reset-version" 244 | } 245 | ] 246 | }, 247 | "upgrade": { 248 | "name": "upgrade", 249 | "description": "upgrade dependencies", 250 | "env": { 251 | "CI": "0" 252 | }, 253 | "steps": [ 254 | { 255 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=prod,dev,optional --filter=@jsii/spec,@types/jest,@types/node,eslint-import-resolver-typescript,eslint-plugin-import,jest,mrpj,projen,ts-jest,ts-node,typescript" 256 | }, 257 | { 258 | "exec": "yarn install --check-files" 259 | }, 260 | { 261 | "exec": "yarn upgrade @jsii/spec @stylistic/eslint-plugin @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version constructs eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit mrpj projen ts-jest ts-node typescript" 262 | }, 263 | { 264 | "exec": "npx projen" 265 | }, 266 | { 267 | "spawn": "post-upgrade" 268 | } 269 | ] 270 | }, 271 | "watch": { 272 | "name": "watch", 273 | "description": "Watch & compile in the background", 274 | "steps": [ 275 | { 276 | "exec": "tsc --build -w" 277 | } 278 | ] 279 | } 280 | }, 281 | "env": { 282 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 283 | }, 284 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 285 | } 286 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { TypeScriptProject, logo } from 'mrpj'; 2 | const project = new TypeScriptProject({ 3 | repo: 'mrgrain/jsii-struct-builder', 4 | description: 'Build jsii structs with ease', 5 | authorName: 'Momo Kornher', 6 | authorUrl: 'https://moritzkornher.de', 7 | license: 'Apache-2.0', 8 | 9 | // Release & Automation 10 | release: true, 11 | automationAppName: 'projen-builder', 12 | 13 | // Dependencies 14 | deps: ['@jsii/spec'], 15 | peerDeps: ['projen@x.x.x'], 16 | peerDependencyOptions: { 17 | pinnedDevDependency: false, 18 | }, 19 | 20 | // Node & TypeScript config 21 | tsconfig: { 22 | compilerOptions: { 23 | lib: ['es2022'], 24 | }, 25 | }, 26 | 27 | // Marketing 28 | logo: logo.Logo.forProjen('images/logo.svg', { 29 | tapeColor: '#e9f1f1', 30 | outlineColor: '#1f3043', 31 | topColor: '#d89751', 32 | frontColor: '#c97d2c', 33 | iconTransform: 'translate(24.5 20) scale(.3)', 34 | icon: '', 35 | }), 36 | wordmarkOptions: { 37 | text: 'jsii-struct-builder', 38 | textPosition: { 39 | dx: 30, 40 | dy: 10, 41 | }, 42 | size: { 43 | height: 180, 44 | width: 960, 45 | }, 46 | }, 47 | }); 48 | 49 | project.package.addEngine('node', '>=18'); 50 | 51 | project.synth(); 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![jsii-struct-builder](./images/wordmark-dynamic.svg) 2 | 3 | Build jsii structs with ease. 4 | 5 | Jsii doesn't support TypeScript [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html) like `Partial` or `Omit`, making it difficult to re-use existing [struct interfaces](https://aws.github.io/jsii/specification/2-type-system/#structs). 6 | With this package, you can work around that limitation and create brand new struct interfaces based on the jsii specification of any existing structs, their parents, and your custom specification. 7 | 8 | From jsii's perspective, these structs are completely new types. 9 | From a maintainer's perspective, they require the same minimal effort as utility types do. 10 | Everybody wins! 11 | 12 | ## Usage 13 | 14 | Install with: 15 | 16 | ```console 17 | npm install --save-dev @mrgrain/jsii-struct-builder 18 | ``` 19 | 20 | Or add `@mrgrain/jsii-struct-builder` to the `devDeps` in your project in your `.projenrc.ts` file, and run `npx projen` to install it. 21 | 22 | Then add a `new ProjenStruct` in your `.projenrc.ts` file, passing a [TypeScript project](https://projen.io/typescript.html) as the first parameter. 23 | See the sections below for more usage details. 24 | 25 | If you're not using [`projen`](https://projen.io/), see [Use without `projen`](#use-without-projen). 26 | 27 | ### Requirements 28 | 29 | ```txt 30 | Node.js >= 18 31 | projen >= 0.65.0 32 | ``` 33 | 34 | ### Create from an existing Struct 35 | 36 | Use the jsii FQN to mix in an existing struct. 37 | Use `omit()` to remove any properties you are not interested in. 38 | 39 | ```ts 40 | new ProjenStruct(project, { name: 'MyProjectOptions' }) 41 | .mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')) 42 | .omit('sampleCode', 'projenrcTs', 'projenrcTsOptions'); 43 | ``` 44 | 45 | ### Adding new Properties 46 | 47 | New properties can be added with a `@jsii/spec` definition. 48 | Complex types can be used and will be imported using their FQN. 49 | Any existing properties of the same name will be replaced. 50 | 51 | ```ts 52 | new ProjenStruct(project, { name: 'MyProjectOptions' }) 53 | .mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')) 54 | .add( 55 | { 56 | name: 'booleanSetting', 57 | type: { primitive: jsii.PrimitiveType.Boolean }, 58 | }, 59 | { 60 | name: 'complexSetting', 61 | type: { fqn: 'my_project.SomeEnum' }, // my_project is the "name" in your projen project 62 | } 63 | ); 64 | ``` 65 | 66 | ### Updating existing Properties 67 | 68 | Existing properties can be updated. 69 | The provided partial `@jsii/spec` definition will be deep merged with the existing spec. 70 | 71 | A convenience `rename()` method is provided. 72 | An `update()` of the `name` field has the same effect and can be combined with other updates. 73 | 74 | ```ts 75 | new ProjenStruct(project, { name: 'MyProjectOptions'}) 76 | .mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')) 77 | 78 | // Update a property 79 | .update('typescriptVersion', { optional: false }) 80 | // Nested values can be updated 81 | .update('sampleCode', { 82 | docs: { 83 | summary: 'New summary', 84 | default: 'false', 85 | } 86 | } 87 | ) 88 | 89 | // Rename a property 90 | .rename('homepage', 'website'}) 91 | // ...this also does a rename 92 | .update('eslint', { 93 | name: 'lint', 94 | optional: false, 95 | }); 96 | ``` 97 | 98 | ### Updating multiple properties 99 | 100 | A callback function can be passed to `updateEvery()` to update multiple properties at a time. 101 | 102 | Use `updateAll()` to uniformly update all properties. 103 | A convenience `allOptional()` method is provided to make all properties optional. 104 | 105 | ```js 106 | new ProjenStruct(project, { name: 'MyProjectOptions'}) 107 | .mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')) 108 | 109 | // Use a callback to make conditional updates 110 | .updateEvery((property) => { 111 | if (!property.optional) { 112 | return { 113 | docs: { 114 | remarks: 'This property is required.', 115 | }, 116 | }; 117 | } 118 | return {}; 119 | }) 120 | 121 | // Apply an update to all properties 122 | .updateAll({ 123 | immutable: true, 124 | }) 125 | 126 | // Mark all properties as optional 127 | .allOptional(); 128 | ``` 129 | 130 | ### Replacing properties 131 | 132 | Existing properties can be replaced with a new `@jsii/spec` definition. 133 | If a different `name` is provided, the property is also renamed. 134 | 135 | A callback function can be passed to `map()` to map every property to a new `@jsii/spec` definition. 136 | 137 | ```ts 138 | new ProjenStruct(project, { name: 'MyProjectOptions' }) 139 | .mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')) 140 | 141 | // Replace a property with an entirely new definition 142 | .replace('autoApproveOptions', { 143 | name: 'autoApproveOptions', 144 | type: { fqn: 'my_project.AutoApproveOptions' }, // my_project is the "name" in your projen project 145 | docs: { 146 | summary: 'Configure the auto-approval workflow.' 147 | } 148 | }) 149 | 150 | // Passing a new name, will also rename the property 151 | .replace('autoMergeOptions', { 152 | name: 'mergeFlowOptions', 153 | type: { fqn: 'my_project.MergeFlowOptions' }, // my_project is the "name" in your projen project 154 | }) 155 | 156 | // Use a callback to map every property to a new definition 157 | .map((property) => { 158 | if (property.protected) { 159 | return { 160 | ...property, 161 | protected: false, 162 | docs: { 163 | custom: { 164 | 'internal': 'true', 165 | } 166 | } 167 | } 168 | } 169 | return property; 170 | }); 171 | ``` 172 | 173 | ### Filter properties 174 | 175 | Arbitrary predicate functions can be used to filter properties. 176 | Only properties that meet the condition are kept. 177 | 178 | Use `omit()` and `only()` for easy name based filtering. 179 | A convenience `withoutDeprecated()` method is also provided. 180 | 181 | ```ts 182 | new ProjenStruct(project, { name: 'MyProjectOptions' }) 183 | .mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')) 184 | 185 | // Keep properties using arbitrary filters 186 | .filter((prop) => !prop.optional) 187 | 188 | // Keep or omit properties by name 189 | .only('projenrcTs', 'projenrcTsOptions') 190 | .omit('sampleCode') 191 | 192 | // Remove all deprecated properties 193 | .withoutDeprecated(); 194 | ``` 195 | 196 | ### AWS CDK properties 197 | 198 | A common use-case of this project is to expose arbitrary overrides in CDK constructs. 199 | For example, you may want to provide common AWS Lambda configuration, but allow a consuming user to override any arbitrary property. 200 | 201 | To accomplish this, first create the new struct in your `.projenrc.ts` file. 202 | 203 | ```ts 204 | import { ProjenStruct, Struct } from '@mrgrain/jsii-struct-builder'; 205 | import { awscdk } from 'projen'; 206 | 207 | const project = new awscdk.AwsCdkConstructLibrary({ 208 | // your config - see https://projen.io/awscdk-construct.html 209 | }); 210 | 211 | new ProjenStruct(project, { name: 'MyFunctionProps' }) 212 | .mixin(Struct.fromFqn('aws-cdk-lib.aws_lambda.FunctionProps')) 213 | .withoutDeprecated() 214 | .allOptional() 215 | .omit('code'); // our construct always provides the code 216 | ``` 217 | 218 | Then use the new struct in your CDK construct. 219 | 220 | ```ts 221 | // lib/MyFunction.ts 222 | import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; 223 | import { Construct } from 'constructs'; 224 | import { join } from 'path'; 225 | import { MyFunctionProps } from './MyFunctionProps'; 226 | 227 | export class MyFunction extends Construct { 228 | constructor(scope: Construct, id: string, props: MyFunctionProps = {}) { 229 | super(scope, id); 230 | 231 | new Function(this, 'Function', { 232 | // sensible defaults 233 | runtime: Runtime.NODEJS_18_X, 234 | handler: 'index.handler', 235 | // user provided props 236 | ...props, 237 | // always force `code` from our construct 238 | code: Code.fromAsset(join(__dirname, 'lambda-handler')), 239 | }); 240 | } 241 | } 242 | ``` 243 | 244 | ### Complex types 245 | 246 | More complex jsii types can be used as well. They can be expressed like this: 247 | 248 | ```ts 249 | import { PrimitiveType, CollectionKind } from '@jsii/spec'; 250 | import { ProjenStruct, Struct } from '@mrgrain/jsii-struct-builder'; 251 | import { awscdk } from 'projen'; 252 | 253 | const project = new awscdk.AwsCdkConstructLibrary({ 254 | // your config - see https://projen.io/awscdk-construct.html 255 | }); 256 | 257 | new ProjenStruct(project, { 258 | name: 'CustomFargateServiceProps', 259 | docs: { 260 | summary: 'ecs.FargateServiceProps without taskDefinition and desiredCount', 261 | remarks: `We do not want to allow the user to specify a task definition or desired count, 262 | as this construct is meant to deploy a single service with a single task definition and 263 | desired count of 1. So we narrow the ecs.FargateServiceProps type to exclude these properties. 264 | Then we add some custom properties that are specific to our use case.`, 265 | custom: { 266 | 'internal': 'true', // use this if you want to hide the struct from the public API 267 | }, 268 | }, 269 | }) 270 | .mixin(Struct.fromFqn('aws-cdk-lib.aws_ecs.FargateServiceProps')) 271 | .omit('taskDefinition', 'desiredCount') 272 | .add({ 273 | name: 'logMappings', 274 | type: { 275 | collection: { 276 | elementtype: { fqn: 'my_project.LogMapping' }, // my_project is the "name" in your projen project 277 | kind: CollectionKind.Array, 278 | }, 279 | }, 280 | docs: { 281 | summary: 'An array of a locally-defined type. The fqn comes from the jsii module name.', 282 | remarks: 'This can be looked in the .jsii file like this: `jq -r \'.types[] | select (.name == "LogMapping") | .fqn\' .jsii`', 283 | }, 284 | }) 285 | .add({ 286 | name: 'agentCpu', 287 | type: { primitive: PrimitiveType.Number }, 288 | optional: true, // an optional property 289 | docs: { 290 | summary: 'The amount of CPU units to allocate to the agent.', // here is how you format a long description 291 | remarks: `1024 CPU units = 1 vCPU. 292 | This is passed to the Fargate task definition. 293 | You might need to increase this if you have a lot of logs to process. 294 | Only some combinations of memory and CPU are valid.`, 295 | see: 'https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.TaskDefinition.html#memorymib', 296 | default: '512', 297 | }, 298 | }) 299 | .add({ 300 | name: 'mapToArray', 301 | type: { 302 | collection: { 303 | elementtype: { 304 | collection: { 305 | elementtype: { fqn: 'aws-cdk-lib.aws_logs.LogGroup' }, 306 | kind: CollectionKind.Array, 307 | } 308 | }, 309 | kind: CollectionKind.Map, 310 | }, 311 | }, 312 | docs: { 313 | summary: 'A map of string to an array of LogGroup objects.' 314 | }, 315 | }) 316 | .add({ 317 | name: 'mysteryObject', 318 | type: { primitive: PrimitiveType.Any }, 319 | docs: { 320 | summary: 'An "any" type object.' 321 | }, 322 | }) 323 | ``` 324 | 325 | ### Use without projen 326 | 327 | It is not required to use _projen_ with this package. 328 | You can use a renderer directly to create files: 329 | 330 | ```ts 331 | const myProps = Struct.empty('@my-scope/my-pkg.MyFunctionProps') 332 | .mixin(Struct.fromFqn('aws-cdk-lib.aws_lambda.FunctionProps')) 333 | .withoutDeprecated(); 334 | 335 | const renderer = new TypeScriptRenderer(); 336 | fs.writeFileSync('my-props.ts', renderer.renderStruct(myProps)); 337 | ``` 338 | 339 | ### Advanced usage 340 | 341 | `Struct` and `ProjenStruct` both share the same interface. 342 | This allows some advanced applications. 343 | 344 | For example you can manipulate the source for re-use: 345 | 346 | ```ts 347 | const base = Struct.fromFqn('projen.typescript.TypeScriptProjectOptions'); 348 | base.omit('sampleCode', 'projenrcTs', 'projenrcTsOptions'); 349 | ``` 350 | 351 | Or you can mix in a `ProjenStruct` with another: 352 | 353 | ```ts 354 | const foo = new ProjenStruct(project, { name: 'Foo' }); 355 | const bar = new ProjenStruct(project, { name: 'Bar' }); 356 | 357 | bar.mixin(foo); 358 | ``` 359 | 360 | You can also use `Struct` and `ProjenStruct` as type of a property: 361 | 362 | ```ts 363 | const foo = new ProjenStruct(project, { name: 'Foo' }); 364 | const bar = new ProjenStruct(project, { name: 'Bar' }); 365 | 366 | foo.add({ 367 | name: 'barSettings', 368 | type: bar, 369 | }); 370 | ``` 371 | 372 | The default configuration makes assumptions about the new interface that are usually okay. 373 | For more complex scenarios `fqn`, `filePath` and `importLocations` can be used to influence the rendered output. 374 | 375 | ```ts 376 | new JsiiInterface(project, { 377 | name: 'MyProjectOptions', 378 | fqn: 'my_project.nested.location.MyProjectOptions', // my_project is the "name" in your projen project 379 | filePath: 'src/nested/my-project-options.ts', 380 | importLocations: { 381 | my_project: '../enums', 382 | }, 383 | }).add({ 384 | name: 'complexSetting', 385 | type: { fqn: 'my_project.SomeEnum' }, // my_project is the "name" in your projen project 386 | }); 387 | ``` 388 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrgrain/jsii-struct-builder/4ee80c2205933756433f84752a24e2fda8406e91/images/logo.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /images/wordmark-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrgrain/jsii-struct-builder/4ee80c2205933756433f84752a24e2fda8406e91/images/wordmark-dark.png -------------------------------------------------------------------------------- /images/wordmark-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | jsii-struct-builder 37 | -------------------------------------------------------------------------------- /images/wordmark-dynamic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrgrain/jsii-struct-builder/4ee80c2205933756433f84752a24e2fda8406e91/images/wordmark-dynamic.png -------------------------------------------------------------------------------- /images/wordmark-dynamic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | jsii-struct-builder 49 | -------------------------------------------------------------------------------- /images/wordmark-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrgrain/jsii-struct-builder/4ee80c2205933756433f84752a24e2fda8406e91/images/wordmark-light.png -------------------------------------------------------------------------------- /images/wordmark-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | jsii-struct-builder 37 | -------------------------------------------------------------------------------- /images/wordmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrgrain/jsii-struct-builder/4ee80c2205933756433f84752a24e2fda8406e91/images/wordmark.png -------------------------------------------------------------------------------- /images/wordmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | jsii-struct-builder 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mrgrain/jsii-struct-builder", 3 | "description": "Build jsii structs with ease", 4 | "repository": { 5 | "type": "git", 6 | "url": "git@github.com:mrgrain/jsii-struct-builder.git" 7 | }, 8 | "scripts": { 9 | "build": "npx projen build", 10 | "bump": "npx projen bump", 11 | "clobber": "npx projen clobber", 12 | "compile": "npx projen compile", 13 | "default": "npx projen default", 14 | "eject": "npx projen eject", 15 | "eslint": "npx projen eslint", 16 | "logo": "npx projen logo", 17 | "package": "npx projen package", 18 | "post-compile": "npx projen post-compile", 19 | "post-upgrade": "npx projen post-upgrade", 20 | "pre-compile": "npx projen pre-compile", 21 | "release": "npx projen release", 22 | "test": "npx projen test", 23 | "test:watch": "npx projen test:watch", 24 | "unbump": "npx projen unbump", 25 | "upgrade": "npx projen upgrade", 26 | "watch": "npx projen watch", 27 | "projen": "npx projen" 28 | }, 29 | "author": { 30 | "name": "Momo Kornher", 31 | "url": "https://moritzkornher.de", 32 | "organization": false 33 | }, 34 | "devDependencies": { 35 | "@stylistic/eslint-plugin": "^2", 36 | "@types/jest": "^29.5.14", 37 | "@types/node": "^18", 38 | "@typescript-eslint/eslint-plugin": "^8", 39 | "@typescript-eslint/parser": "^8", 40 | "commit-and-tag-version": "^12", 41 | "constructs": "^10.0.0", 42 | "eslint": "^9", 43 | "eslint-import-resolver-typescript": "^3.10.1", 44 | "eslint-plugin-import": "^2.31.0", 45 | "jest": "^29.7.0", 46 | "jest-junit": "^16", 47 | "mrpj": "^0.1.96", 48 | "projen": "^0.92.9", 49 | "ts-jest": "^29.3.4", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.8.3" 52 | }, 53 | "peerDependencies": { 54 | "projen": "x.x.x" 55 | }, 56 | "dependencies": { 57 | "@jsii/spec": "^1.112.0" 58 | }, 59 | "engines": { 60 | "node": ">=18" 61 | }, 62 | "main": "lib/index.js", 63 | "license": "Apache-2.0", 64 | "homepage": "https://github.com/mrgrain/jsii-struct-builder", 65 | "publishConfig": { 66 | "access": "public" 67 | }, 68 | "version": "0.0.0", 69 | "jest": { 70 | "coverageProvider": "v8", 71 | "testMatch": [ 72 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 73 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 74 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 75 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 76 | ], 77 | "clearMocks": true, 78 | "collectCoverage": true, 79 | "coverageReporters": [ 80 | "json", 81 | "lcov", 82 | "clover", 83 | "cobertura", 84 | "text" 85 | ], 86 | "coverageDirectory": "coverage", 87 | "coveragePathIgnorePatterns": [ 88 | "/node_modules/" 89 | ], 90 | "testPathIgnorePatterns": [ 91 | "/node_modules/" 92 | ], 93 | "watchPathIgnorePatterns": [ 94 | "/node_modules/" 95 | ], 96 | "reporters": [ 97 | "default", 98 | [ 99 | "jest-junit", 100 | { 101 | "outputDirectory": "test-reports" 102 | } 103 | ] 104 | ], 105 | "transform": { 106 | "^.+\\.[t]sx?$": [ 107 | "ts-jest", 108 | { 109 | "tsconfig": "tsconfig.dev.json" 110 | } 111 | ] 112 | } 113 | }, 114 | "types": "lib/index.d.ts", 115 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 116 | } 117 | -------------------------------------------------------------------------------- /src/builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './struct'; 2 | -------------------------------------------------------------------------------- /src/builder/struct.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceType, Property, TypeKind } from '@jsii/spec'; 2 | import { findInterface } from '../private'; 3 | 4 | /** 5 | * Something that has jsii properties. 6 | */ 7 | export interface HasProperties { 8 | /** 9 | * The list of properties of the thing. 10 | */ 11 | readonly properties?: Property[]; 12 | } 13 | 14 | /** 15 | * Something that has a fully-qualified-name. 16 | */ 17 | export interface HasFullyQualifiedName { 18 | /** 19 | * The fully-qualified-name of the thing. 20 | */ 21 | readonly fqn: string; 22 | } 23 | 24 | /** 25 | * Something that has a fully-qualified-name. 26 | */ 27 | export interface HasStructSpec { 28 | /** 29 | * Get the current spec of the builder. 30 | */ 31 | readonly spec: InterfaceType; 32 | } 33 | 34 | export interface IStructBuilder { 35 | /** 36 | * Add properties. 37 | * 38 | * In the same call, the first defined properties take priority. 39 | * However later calls will overwrite existing properties. 40 | */ 41 | add(...props: Property[]): IStructBuilder; 42 | 43 | /** 44 | * Mix the properties of these sources into the struct. 45 | * 46 | * In the same call, the first defined sources and properties take priority. 47 | * However later calls will overwrite existing properties. 48 | */ 49 | mixin(...sources: HasProperties[]): IStructBuilder; 50 | 51 | /** 52 | * Replaces an existing property with a new spec. 53 | */ 54 | replace(name: string, replacement: Property): IStructBuilder; 55 | 56 | /** 57 | * Calls a defined callback function on each property, and replaces the property with the returned property. 58 | * 59 | * @param callbackfn — A function that accepts a property spec as an argument. The map method calls the callbackfn function one time for each property. 60 | */ 61 | map(callbackfn: (prop: Property) => Property): IStructBuilder; 62 | 63 | /** 64 | * Update an existing property. 65 | */ 66 | update(name: string, update: Partial): IStructBuilder; 67 | 68 | /** 69 | * Calls a defined callback function on each property, and merges the property with the returned property partial. 70 | * 71 | * @param callbackfn — A function that accepts a property spec as an argument. The map method calls the callbackfn function one time for each property. 72 | */ 73 | updateEvery(callbackfn: (prop: Property) => Partial): IStructBuilder; 74 | 75 | /** 76 | * Update all existing properties. 77 | */ 78 | updateAll(update: Partial): IStructBuilder; 79 | 80 | /** 81 | * Rename a property. 82 | * 83 | * If another property with the new name exists, it will be overridden. 84 | */ 85 | rename(from: string, to: string): IStructBuilder; 86 | 87 | /** 88 | * Mark all properties as optional. 89 | */ 90 | allOptional(): IStructBuilder; 91 | 92 | /** 93 | * Keep only the properties that meet the condition specified in the callback function. 94 | */ 95 | filter(predicate: (prop: Property) => boolean): IStructBuilder; 96 | 97 | /** 98 | * Only keep these properties. 99 | */ 100 | only(...keep: string[]): IStructBuilder; 101 | 102 | /** 103 | * Omit these properties. 104 | */ 105 | omit(...remove: string[]): IStructBuilder; 106 | 107 | /** 108 | * Remove all deprecated properties. 109 | */ 110 | withoutDeprecated(): IStructBuilder; 111 | } 112 | 113 | /** 114 | * Build a jsii struct 115 | */ 116 | export class Struct implements IStructBuilder, HasProperties, HasFullyQualifiedName, HasStructSpec { 117 | /** 118 | * Create a builder from an jsii spec 119 | */ 120 | public static fromSpec(spec: InterfaceType) { 121 | return new Struct(spec); 122 | } 123 | 124 | /** 125 | * Create a builder from a jsii FQN 126 | * 127 | * @param fqn The jsii fqn of the source spec. 128 | * @param mergeParents Merge parent interfaces into the spec. Defaults to `true`. 129 | */ 130 | public static fromFqn(fqn: string, mergeParents: boolean = true) { 131 | const source = findInterface(fqn, mergeParents); 132 | return new Struct(source); 133 | } 134 | 135 | /** 136 | * Create an empty builder 137 | * 138 | * Note that the behavior of `builder.spec` is undefined when using this method. 139 | */ 140 | public static empty(fqn = '<>.<>') { 141 | return new Struct({ 142 | assembly: fqn.split('.').at(0) ?? '<>', 143 | fqn, 144 | name: fqn.split('.').at(-1) ?? '<>', 145 | kind: TypeKind.Interface, 146 | }); 147 | } 148 | 149 | private _base: InterfaceType; 150 | private _properties: Map; 151 | 152 | private constructor(base: InterfaceType) { 153 | this._base = structuredClone(base); 154 | this._properties = new Map( 155 | base.properties?.map((p) => [p.name, structuredClone(p)]), 156 | ); 157 | } 158 | 159 | public add(...props: Property[]): this { 160 | for (const prop of props.reverse()) { 161 | this._properties.set(prop.name, prop); 162 | } 163 | 164 | return this; 165 | } 166 | 167 | public mixin(...sources: HasProperties[]): this { 168 | for (const source of sources.reverse()) { 169 | this.add(...(source.properties || [])); 170 | } 171 | 172 | return this; 173 | } 174 | 175 | public replace(name: string, replacement: Property): this { 176 | const current = this._properties.get(name); 177 | 178 | if (!current) { 179 | throw `Unable to replace property '${name}' in '${this._base.fqn}: Property does not exists, please use \`add\`.'`; 180 | } 181 | 182 | if (replacement.name !== name) { 183 | this.omit(name); 184 | } 185 | 186 | return this.add(replacement); 187 | } 188 | 189 | public map(callbackfn: (prop: Property) => Property): this { 190 | const keys = this._properties.keys(); 191 | for (const propertyKey of keys) { 192 | const current = structuredClone(this._properties.get(propertyKey)!); 193 | this.replace(propertyKey, callbackfn(current)); 194 | } 195 | 196 | return this; 197 | } 198 | 199 | public update(name: string, update: Partial): this { 200 | const old = this._properties.get(name); 201 | 202 | if (!old) { 203 | throw `Unable to update property '${name}' in '${this._base.fqn}: Property does not exists, please use \`add\`.'`; 204 | } 205 | 206 | const updatedProp = { 207 | ...old, 208 | ...update, 209 | docs: { 210 | ...old?.docs, 211 | ...update?.docs, 212 | custom: { 213 | ...old?.docs?.custom, 214 | ...update?.docs?.custom, 215 | }, 216 | }, 217 | }; 218 | 219 | if (updatedProp.name !== name) { 220 | this.omit(name); 221 | } 222 | 223 | return this.add(updatedProp); 224 | } 225 | 226 | public updateEvery(callbackfn: (prop: Property) => Partial): this { 227 | const keys = this._properties.keys(); 228 | for (const propertyKey of keys) { 229 | const current = structuredClone(this._properties.get(propertyKey)!); 230 | this.update(current.name, callbackfn(current) ?? {}); 231 | } 232 | 233 | return this; 234 | } 235 | 236 | public updateAll(update: Partial): this { 237 | for (const propertyKey of this._properties.keys()) { 238 | this.update(propertyKey, update); 239 | } 240 | return this; 241 | } 242 | 243 | public rename(from: string, to: string): this { 244 | return this.update(from, { name: to }); 245 | } 246 | 247 | public allOptional(): this { 248 | this.map((property) => { 249 | property.optional = true; 250 | return property; 251 | }); 252 | 253 | return this; 254 | } 255 | 256 | public filter(predicate: (prop: Property) => boolean): this { 257 | for (const propertyKey of this._properties.keys()) { 258 | if (!predicate(this._properties.get(propertyKey)!)) { 259 | this._properties.delete(propertyKey); 260 | } 261 | } 262 | 263 | return this; 264 | } 265 | 266 | public only(...keep: string[]): this { 267 | return this.filter((prop) => keep.includes(prop.name)); 268 | } 269 | 270 | public omit(...remove: string[]): this { 271 | for (const prop of remove) { 272 | this._properties.delete(prop); 273 | } 274 | 275 | return this; 276 | } 277 | 278 | public withoutDeprecated(): this { 279 | return this.filter((prop) => null == prop.docs?.deprecated); 280 | } 281 | 282 | /** 283 | * Get the current state of the builder 284 | */ 285 | public get spec(): InterfaceType { 286 | const properties = Array.from(this._properties.values()); 287 | 288 | return { 289 | ...structuredClone(this._base), 290 | properties, 291 | }; 292 | } 293 | 294 | /** 295 | * Get the current properties of the builder 296 | */ 297 | public get properties(): Property[] { 298 | return this.spec.properties ?? []; 299 | } 300 | 301 | /** 302 | * Get the FQN for the builder 303 | */ 304 | public get fqn(): string { 305 | return this._base.fqn; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder'; 2 | export * from './projen'; 3 | export * from './renderer'; 4 | -------------------------------------------------------------------------------- /src/private/assembly.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path'; 2 | import { 3 | Assembly, 4 | InterfaceType, 5 | loadAssemblyFromPath, 6 | Method, 7 | Property, 8 | TypeKind, 9 | } from '@jsii/spec'; 10 | 11 | const DOT_JSII = '.jsii'; 12 | 13 | const assemblies: Record = {}; 14 | 15 | function assemblyPath(asm: string): string { 16 | return dirname(require.resolve(join(asm, DOT_JSII))); 17 | } 18 | 19 | function loadAssemblyByName(asm: string): Assembly { 20 | return loadAssemblyFromDirectory(asm, assemblyPath(asm)); 21 | } 22 | 23 | function loadAssemblyFromDirectory(asm: string, dir: string): Assembly { 24 | const assembly = loadAssemblyFromPath(dir, false); 25 | if (assembly.name !== asm) { 26 | throw `jsii assembly ${asm} not found in ${join(dir, DOT_JSII)}, got: ${assembly.name}`; 27 | } 28 | 29 | return assembly; 30 | } 31 | 32 | function loadAssembly(asm: string): Assembly { 33 | if (!assemblies[asm]) { 34 | try { 35 | assemblies[asm] = loadAssemblyByName(asm); 36 | } catch (error) { 37 | try { 38 | assemblies[asm] = loadAssemblyFromDirectory(asm, process.cwd()); 39 | } catch (errorLocal) { 40 | throw new AggregateError([error, errorLocal], `jsii assembly ${asm} not found`); 41 | } 42 | } 43 | } 44 | 45 | return assemblies[asm]; 46 | } 47 | 48 | function loadInterface(fqn: string) { 49 | const asm = loadAssembly(fqn.split('.').at(0)!); 50 | 51 | const candidate = asm.types?.[fqn]; 52 | 53 | if (!candidate) { 54 | throw `Type ${fqn} not found in jsii assembly ${asm}`; 55 | } 56 | 57 | if (candidate?.kind !== TypeKind.Interface) { 58 | throw `Expected ${fqn} to be an interface, but got: ${candidate?.kind}`; 59 | } 60 | 61 | return structuredClone(candidate); 62 | } 63 | 64 | export function findInterface( 65 | fqn: string, 66 | mergeInherited: boolean = true, 67 | ): InterfaceType { 68 | const spec = loadInterface(fqn); 69 | 70 | if (!mergeInherited || !spec.interfaces) { 71 | return spec; 72 | } 73 | 74 | const props = new Map(); 75 | const methods = new Map(); 76 | 77 | for (const parent of spec.interfaces) { 78 | const parentSpec = findInterface(parent, true); 79 | parentSpec.properties?.forEach((p) => { 80 | props.set(p.name, p); 81 | }); 82 | parentSpec.methods?.forEach((m) => { 83 | methods.set(m.name, m); 84 | }); 85 | } 86 | 87 | spec.properties?.forEach((p) => { 88 | props.set(p.name, p); 89 | }); 90 | spec.methods?.forEach((m) => { 91 | methods.set(m.name, m); 92 | }); 93 | 94 | return { 95 | ...spec, 96 | methods: Array.from(methods.values()), 97 | properties: Array.from(props.values()), 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /src/private/index.ts: -------------------------------------------------------------------------------- 1 | export * from './assembly'; 2 | export * from './utils'; 3 | -------------------------------------------------------------------------------- /src/private/utils.ts: -------------------------------------------------------------------------------- 1 | export function compareLowerCase(a: string, b: string) { 2 | return a.localeCompare(b, undefined, { sensitivity: 'base' }); 3 | } 4 | 5 | export function comparePath(a: string, b: string) { 6 | if (a[0] < b[0]) { 7 | return 1; 8 | } 9 | return compareLowerCase(a, b); 10 | } 11 | -------------------------------------------------------------------------------- /src/projen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './projen-struct'; 2 | export * from './ts-interface'; 3 | -------------------------------------------------------------------------------- /src/projen/projen-struct.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join, posix } from 'path'; 2 | import { Docs, Property, TypeKind } from '@jsii/spec'; 3 | import { Component, typescript } from 'projen'; 4 | import { TypeScriptInterfaceFile, TypeScriptInterfaceFileOptions } from './ts-interface'; 5 | import { 6 | Struct, 7 | HasProperties, 8 | IStructBuilder, 9 | HasFullyQualifiedName, 10 | HasStructSpec, 11 | } from '../builder'; 12 | 13 | export interface ProjenStructOptions { 14 | /** 15 | * The name of the new struct 16 | */ 17 | readonly name: string; 18 | /** 19 | * Doc string for the struct 20 | * 21 | * Just does the summary 22 | * If you want to add a full description, use `docs` instead. 23 | * This will not be used if `docs` is provided. 24 | * 25 | * @default - struct name 26 | */ 27 | readonly description?: string; 28 | /** 29 | * Docs for the struct 30 | * 31 | * Use this to add a full description. 32 | * If you only want to add a summary, use `description` instead. 33 | * 34 | * @default - none, use `description` if provided 35 | */ 36 | readonly docs?: Docs; 37 | /** 38 | * The fqn of the struct 39 | * 40 | * Used to auto-add imports. 41 | * All referenced types are loaded based on the fqn hierarchy. 42 | * 43 | * See `importLocations` for customization options. 44 | * 45 | * @default `${project.name}.${options.name}` 46 | */ 47 | readonly fqn?: string; 48 | /** 49 | * Output file path 50 | * @default - inside `srcdir` or `outdir` with the fqn turned into a path 51 | */ 52 | readonly filePath?: string; 53 | /** 54 | * The module locations assemblies should be imported from 55 | * 56 | * All types are imported from the top-level of the module. 57 | * This map can be used to overwrite the default import locations. 58 | * 59 | * For local imports, the import traverse up a number of levels equivalent to the number of fqn parts of the rendered type. 60 | * E.g. if the the rendered type is `pkg.interface`, it is assumed to be at the top-level and imports from the same assembly would be from `./`. 61 | * If the rendered type is `pkg.nested.sub.interface` a local import will be from `../../`. 62 | * 63 | * @default - uses the assembly name for external packages 64 | * local imports traverse up a number of levels equivalent to the number of fqn parts 65 | */ 66 | readonly importLocations?: Record; 67 | /** 68 | * Options for the created output `TypeScriptInterfaceFile` 69 | */ 70 | readonly outputFileOptions?: Omit; 71 | } 72 | 73 | /** 74 | * A component generating a jsii-compatible struct 75 | */ 76 | export class ProjenStruct extends Component implements IStructBuilder, HasProperties, HasFullyQualifiedName, HasStructSpec { 77 | private builder: Struct; 78 | 79 | public constructor( 80 | private tsProject: typescript.TypeScriptProject, 81 | private options: ProjenStructOptions, 82 | ) { 83 | super(tsProject); 84 | 85 | const fqn = options.fqn ?? `${tsProject.name}.${options.name}`; 86 | this.builder = Struct.fromSpec({ 87 | kind: TypeKind.Interface, 88 | assembly: fqn.split('.').at(0) ?? tsProject.name, 89 | fqn, 90 | name: options.name, 91 | docs: options.docs ?? { 92 | summary: options.description ?? options.name, 93 | }, 94 | }); 95 | } 96 | 97 | preSynthesize(): void { 98 | const baseDir = this.tsProject.srcdir ?? this.tsProject.outdir; 99 | const outputFile = 100 | this.options.filePath ?? join(baseDir, fqnToPath(this.builder.spec.fqn)); 101 | new TypeScriptInterfaceFile(this.tsProject, outputFile, this.builder.spec, { 102 | ...this.options.outputFileOptions, 103 | importLocations: { 104 | [this.builder.spec.assembly]: relativeImport(outputFile, baseDir), 105 | ...this.options.importLocations, 106 | }, 107 | }); 108 | } 109 | 110 | public get spec() { 111 | return this.builder.spec; 112 | } 113 | add(...props: Property[]): this { 114 | this.builder.add(...props); 115 | return this; 116 | } 117 | mixin(...sources: HasProperties[]): this { 118 | this.builder.mixin(...sources); 119 | return this; 120 | } 121 | replace(name: string, replacement: Property): IStructBuilder { 122 | this.builder.replace(name, replacement); 123 | return this; 124 | } 125 | map(callbackfn: (prop: Property) => Property): this { 126 | this.builder.map(callbackfn); 127 | return this; 128 | } 129 | update(name: string, update: Partial): this { 130 | this.builder.update(name, update); 131 | return this; 132 | } 133 | updateEvery(callbackfn: (prop: Property) => Partial): this { 134 | this.builder.updateEvery(callbackfn); 135 | return this; 136 | } 137 | updateAll(update: Partial): this { 138 | this.builder.updateAll(update); 139 | return this; 140 | } 141 | rename(from: string, to: string): this { 142 | this.builder.rename(from, to); 143 | return this; 144 | } 145 | allOptional(): this { 146 | this.builder.allOptional(); 147 | return this; 148 | } 149 | filter(predicate: (prop: Property) => boolean): this { 150 | this.builder.filter(predicate); 151 | return this; 152 | } 153 | only(...keep: string[]): this { 154 | this.builder.only(...keep); 155 | return this; 156 | } 157 | omit(...remove: string[]): this { 158 | this.builder.omit(...remove); 159 | return this; 160 | } 161 | withoutDeprecated(): this { 162 | this.builder.withoutDeprecated(); 163 | return this; 164 | } 165 | public get properties(): Property[] { 166 | return this.builder.properties; 167 | } 168 | public get fqn(): string { 169 | return this.builder.fqn; 170 | } 171 | } 172 | 173 | function relativeImport(from: string, target: string): string { 174 | const diff = posix.relative(dirname(from), target); 175 | if (!diff) { 176 | return '.' + posix.sep; 177 | } 178 | return diff + posix.sep; 179 | } 180 | 181 | function fqnToPath(fqn: string): string { 182 | return join(...fqn.split('.').slice(1)) + '.ts'; 183 | } 184 | -------------------------------------------------------------------------------- /src/projen/ts-interface.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceType } from '@jsii/spec'; 2 | import { Project, SourceCodeOptions, TextFile } from 'projen'; 3 | import { TypeScriptRenderer, TypeScriptRendererOptions } from '../renderer'; 4 | 5 | /** 6 | * Options for `TypeScriptInterfaceFile`. 7 | */ 8 | export interface TypeScriptInterfaceFileOptions extends TypeScriptRendererOptions, SourceCodeOptions {} 9 | 10 | /** 11 | * A TypeScript interface rendered from a jsii interface specification 12 | */ 13 | export class TypeScriptInterfaceFile extends TextFile { 14 | public constructor( 15 | project: Project, 16 | filePath: string, 17 | spec: InterfaceType, 18 | options: TypeScriptInterfaceFileOptions = {}, 19 | ) { 20 | super(project, filePath, options); 21 | 22 | const renderer = new TypeScriptRenderer(options); 23 | this.addLine(`// ${this.marker}`); 24 | this.addLine(renderer.renderStructSpec(spec)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typescript'; 2 | -------------------------------------------------------------------------------- /src/renderer/typescript.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CollectionKind, 3 | Docs, 4 | InterfaceType, 5 | isCollectionTypeReference, 6 | isNamedTypeReference, 7 | isPrimitiveTypeReference, 8 | isUnionTypeReference, 9 | Property, 10 | TypeReference, 11 | } from '@jsii/spec'; 12 | import { HasStructSpec } from '../builder'; 13 | import { compareLowerCase, comparePath } from '../private'; 14 | 15 | /** 16 | * Options for `TypeScriptRenderer`. 17 | */ 18 | export interface TypeScriptRendererOptions { 19 | /** 20 | * The module locations assemblies should be imported from 21 | * 22 | * All types are imported from the top-level of the module. 23 | * This map can be used to overwrite the default import locations. 24 | * 25 | * For local imports, the import traverse up a number of levels equivalent to the number of fqn parts of the rendered type. 26 | * E.g. if the the rendered type is `pkg.interface`, it is assumed to be at the top-level and imports from the same assembly would be from `./`. 27 | * If the rendered type is `pkg.nested.sub.interface` a local import will be from `../../`. 28 | * 29 | * @default - uses the assembly name for external packages 30 | * local imports traverse up a number of levels equivalent to the number of fqn parts 31 | */ 32 | readonly importLocations?: Record; 33 | 34 | /** 35 | * Indentation size 36 | * 37 | * @default 2 38 | */ 39 | readonly indent?: number; 40 | 41 | /** 42 | * Use explicit `type` imports when importing referenced modules. 43 | * 44 | * @see https://www.typescriptlang.org/docs/handbook/modules.html#importing-types 45 | * @default true 46 | */ 47 | readonly useTypeImports?: boolean; 48 | } 49 | 50 | /** 51 | * Jsii to TypeScript renderer 52 | */ 53 | export class TypeScriptRenderer { 54 | private buffer: CodeBuffer; 55 | private options: Required; 56 | 57 | public constructor(options: TypeScriptRendererOptions = {}) { 58 | this.options = { 59 | importLocations: options.importLocations ?? {}, 60 | indent: options.indent ?? 2, 61 | useTypeImports: options.useTypeImports ?? true, 62 | }; 63 | this.buffer = new CodeBuffer(' '.repeat(this.options.indent)); 64 | } 65 | 66 | /** 67 | * Render something that has a struct spec 68 | */ 69 | public renderStruct(struct: HasStructSpec): string { 70 | return this.renderStructSpec(struct.spec); 71 | } 72 | 73 | /** 74 | * Render a jsii InterfaceType spec 75 | */ 76 | public renderStructSpec(spec: InterfaceType): string { 77 | this.buffer.flush(); 78 | 79 | this.renderImports(extractImports(spec, this.options.importLocations)); 80 | this.buffer.line(); 81 | this.renderDocBlock(docsToLines(spec.docs)); 82 | this.buffer.open(`export interface ${spec.name} {`); 83 | spec.properties?.forEach((p) => this.renderProperty(p, spec.fqn)); 84 | this.buffer.close('}'); 85 | this.buffer.line(); 86 | 87 | return this.buffer.flush().join('\n'); 88 | } 89 | 90 | protected renderImports(modules: Map>) { 91 | Array.from(modules.keys()) 92 | .sort(comparePath) 93 | .forEach((mod) => { 94 | const imports = Array.from(modules.get(mod)?.values() || []); 95 | const importStmt = this.options.useTypeImports 96 | ? 'import type' 97 | : 'import'; 98 | this.buffer.line( 99 | `${importStmt} { ${imports 100 | .sort(compareLowerCase) 101 | .join(', ')} } from '${mod}';`, 102 | ); 103 | }); 104 | } 105 | 106 | protected renderProperty(p: Property, containingFqn: string) { 107 | const docs = structuredClone(p.docs); 108 | if (docs) { 109 | this.renderDocBlock(docsToLines(docs)); 110 | } 111 | 112 | this.buffer.line( 113 | `readonly ${p.name}${p.optional ? '?' : ''}: ${typeRefToType( 114 | p.type, 115 | containingFqn, 116 | )};`, 117 | ); 118 | } 119 | 120 | protected renderDocBlock(lines: string[]) { 121 | if (!lines.length) { 122 | return; 123 | } 124 | 125 | this.buffer.line('/**'); 126 | lines.forEach((line) => this.buffer.line(` * ${line}`)); 127 | this.buffer.line(' */'); 128 | } 129 | } 130 | 131 | class CodeBuffer { 132 | private lines = new Array(); 133 | private indentLevel = 0; 134 | 135 | public constructor(private readonly indent = ' ') {} 136 | 137 | public flush(): string[] { 138 | const current = this.lines; 139 | this.reset(); 140 | 141 | return current; 142 | } 143 | 144 | public line(code?: string) { 145 | const prefix = this.indent.repeat(this.indentLevel); 146 | this.lines.push((prefix + (code ?? '')).trimEnd()); 147 | } 148 | 149 | public open(code?: string) { 150 | if (code) { 151 | this.line(code); 152 | } 153 | 154 | this.indentLevel++; 155 | } 156 | 157 | public close(code?: string) { 158 | if (this.indentLevel === 0) { 159 | throw new Error('Cannot decrease indent level below zero'); 160 | } 161 | this.indentLevel--; 162 | 163 | if (code) { 164 | this.line(code); 165 | } 166 | } 167 | 168 | private reset(): void { 169 | this.lines = new Array(); 170 | this.indentLevel = 0; 171 | } 172 | } 173 | 174 | function docsToLines(docs?: Docs): string[] { 175 | if (!docs) { 176 | return []; 177 | } 178 | 179 | const lines = new Array(); 180 | 181 | if (docs.summary) { 182 | lines.push(docs.summary); 183 | } 184 | if (docs.remarks) { 185 | lines.push(...docs.remarks.split('\n')); 186 | } 187 | if (docs.default) { 188 | lines.push(`@default ${docs.default}`); 189 | } 190 | if (docs.deprecated) { 191 | lines.push(`@deprecated ${docs.deprecated}`); 192 | } 193 | if (docs.stability) { 194 | lines.push(`@stability ${docs.stability}`); 195 | } 196 | if (docs.custom) { 197 | Object.entries(docs.custom).forEach((entry) => { 198 | const [tag, value] = entry; 199 | lines.push(`@${tag} ${value}`); 200 | }); 201 | } 202 | 203 | return lines; 204 | } 205 | 206 | function typeRefToType(t: TypeReference, containingFqn: string): string { 207 | if (isPrimitiveTypeReference(t)) { 208 | return t.primitive; 209 | } 210 | 211 | if (isNamedTypeReference(t)) { 212 | return t.fqn.split('.').slice(1).join('.'); 213 | } 214 | 215 | if (isCollectionTypeReference(t)) { 216 | switch (t.collection.kind) { 217 | case CollectionKind.Array: 218 | return `Array<${typeRefToType( 219 | t.collection.elementtype, 220 | containingFqn, 221 | )}>`; 222 | case CollectionKind.Map: 223 | return `Record`; 227 | default: 228 | return 'any'; 229 | } 230 | } 231 | if (isUnionTypeReference(t)) { 232 | return t.union.types 233 | .map((ut) => typeRefToType(ut, containingFqn)) 234 | .join(' | '); 235 | } 236 | 237 | return 'any'; 238 | } 239 | 240 | function extractImports( 241 | spec: InterfaceType, 242 | importLocations: Record, 243 | ): Map> { 244 | const refs = spec.properties?.flatMap((p) => collectFQNs(p.type)) || []; 245 | return refs.reduce((mods, ref) => { 246 | const packageName = fqnToImportName(ref, spec.fqn, importLocations); 247 | const imports = mods.get(packageName) || new Set(); 248 | const importName = ref.split('.').slice(1)[0] || ref; 249 | return mods.set(packageName, imports.add(importName)); 250 | }, new Map>()); 251 | } 252 | 253 | function fqnToImportName( 254 | fqn: string, 255 | importingFqn: string, 256 | importLocations: Record, 257 | ): string { 258 | const localAssembly = importingFqn.split('.', 1)[0]; 259 | const importAssembly = fqn.split('.', 1)[0]; 260 | 261 | // we have a custom import path 262 | if (importLocations[importAssembly]) { 263 | return importLocations[importAssembly]; 264 | } 265 | 266 | // this is the same assembly, try to guess the path to the root 267 | if (importAssembly === localAssembly) { 268 | const fqnParts = importingFqn.split('.').length; 269 | if (fqnParts <= 2) { 270 | return './'; 271 | } 272 | return '../'.repeat(fqnParts - 2); 273 | } 274 | 275 | // regular third party module 276 | return importAssembly; 277 | } 278 | 279 | function collectFQNs(t: TypeReference): string[] { 280 | if (isNamedTypeReference(t)) { 281 | return [t.fqn]; 282 | } 283 | 284 | if (isUnionTypeReference(t)) { 285 | return t.union.types.flatMap(collectFQNs); 286 | } 287 | 288 | if (isCollectionTypeReference(t)) { 289 | return collectFQNs(t.collection.elementtype); 290 | } 291 | 292 | return []; 293 | } 294 | -------------------------------------------------------------------------------- /test/builder/__snapshots__/struct.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`can render a struct directly 1`] = ` 4 | " 5 | export interface MyFunctionProps { 6 | /** 7 | * Whether to use \`SWC\` for ts-node. 8 | * @default false 9 | * @stability experimental 10 | */ 11 | readonly swc?: boolean; 12 | /** 13 | * A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file. 14 | * @default "projenrc" 15 | * @stability experimental 16 | */ 17 | readonly projenCodeDir?: string; 18 | /** 19 | * The name of the projenrc file. 20 | * @default ".projenrc.ts" 21 | * @stability experimental 22 | */ 23 | readonly filename?: string; 24 | } 25 | " 26 | `; 27 | -------------------------------------------------------------------------------- /test/builder/struct.test.ts: -------------------------------------------------------------------------------- 1 | import { Struct, TypeScriptRenderer } from '../../src'; 2 | 3 | test('can render a struct directly', () => { 4 | // ARRANGE 5 | const renderer = new TypeScriptRenderer(); 6 | 7 | // ACT 8 | const struct = Struct.empty('@my-scope/my-pkg.MyFunctionProps'); 9 | struct.mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')); 10 | 11 | // PREPARE 12 | const renderedFile = renderer.renderStruct(struct); 13 | 14 | // ASSERT 15 | expect(renderedFile).toMatchSnapshot(); 16 | expect(renderedFile).toContain('projenCodeDir'); 17 | expect(renderedFile).toContain('filename'); 18 | }); 19 | -------------------------------------------------------------------------------- /test/projen/__snapshots__/projen-struct.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`can load structs from an assembly in the current working directory 1`] = ` 4 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 5 | 6 | /** 7 | * MyExtendedInterface 8 | */ 9 | export interface MyExtendedInterface { 10 | /** 11 | * Whether to use \`SWC\` for ts-node. 12 | * @default false 13 | * @stability experimental 14 | */ 15 | readonly swc?: boolean; 16 | /** 17 | * A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file. 18 | * @default "projenrc" 19 | * @stability experimental 20 | */ 21 | readonly projenCodeDir?: string; 22 | /** 23 | * The name of the projenrc file. 24 | * @default ".projenrc.ts" 25 | * @stability experimental 26 | */ 27 | readonly filename?: string; 28 | } 29 | " 30 | `; 31 | 32 | exports[`can map properties 1`] = ` 33 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 34 | 35 | /** 36 | * MyInterface 37 | */ 38 | export interface MyInterface { 39 | /** 40 | * This property is required 41 | * @default true 42 | */ 43 | readonly propFour: boolean; 44 | readonly aNumber: number; 45 | readonly aString: string; 46 | readonly aDate: date; 47 | } 48 | " 49 | `; 50 | 51 | exports[`can mixin a struct 1`] = ` 52 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 53 | 54 | /** 55 | * MyInterface 56 | */ 57 | export interface MyInterface { 58 | /** 59 | * Whether to use \`SWC\` for ts-node. 60 | * @default false 61 | * @stability experimental 62 | */ 63 | readonly swc?: boolean; 64 | /** 65 | * A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file. 66 | * @default "projenrc" 67 | * @stability experimental 68 | */ 69 | readonly projenCodeDir?: string; 70 | /** 71 | * The name of the projenrc file. 72 | * @default ".projenrc.ts" 73 | * @stability experimental 74 | */ 75 | readonly filename?: string; 76 | } 77 | " 78 | `; 79 | 80 | exports[`can mixin another projen struct 1`] = ` 81 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 82 | 83 | /** 84 | * MyInterface 85 | */ 86 | export interface MyInterface { 87 | /** 88 | * The name of the projenrc file. 89 | * @default ".projenrc.ts" 90 | * @stability experimental 91 | */ 92 | readonly filename?: string; 93 | /** 94 | * A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file. 95 | * @default "projenrc" 96 | * @stability experimental 97 | */ 98 | readonly projenCodeDir?: string; 99 | /** 100 | * Whether to use \`SWC\` for ts-node. 101 | * @default false 102 | * @stability experimental 103 | */ 104 | readonly swc?: boolean; 105 | } 106 | " 107 | `; 108 | 109 | exports[`can rename a prop 1`] = ` 110 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 111 | 112 | /** 113 | * MyInterface 114 | */ 115 | export interface MyInterface { 116 | readonly newProp?: boolean; 117 | } 118 | " 119 | `; 120 | 121 | exports[`can replace a prop 1`] = ` 122 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 123 | 124 | /** 125 | * MyInterface 126 | */ 127 | export interface MyInterface { 128 | readonly newProp: number; 129 | } 130 | " 131 | `; 132 | 133 | exports[`can update props 1`] = ` 134 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 135 | 136 | /** 137 | * MyInterface 138 | */ 139 | export interface MyInterface { 140 | /** 141 | * Whether to use \`SWC\` for ts-node. 142 | * @default false 143 | * @stability experimental 144 | */ 145 | readonly swc?: boolean; 146 | /** 147 | * New summary 148 | * @default "projenrc" 149 | * @stability stable 150 | * @pjnew "newVal" 151 | */ 152 | readonly projenCodeDir: string; 153 | /** 154 | * The name of the projenrc file. 155 | * @default ".projenrc.ts" 156 | * @stability experimental 157 | */ 158 | readonly filename?: string; 159 | } 160 | " 161 | `; 162 | 163 | exports[`can updateAll props 1`] = ` 164 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 165 | 166 | /** 167 | * MyInterface 168 | */ 169 | export interface MyInterface { 170 | /** 171 | * Whether to use \`SWC\` for ts-node. 172 | * @default false 173 | * @stability stable 174 | */ 175 | readonly swc?: boolean; 176 | /** 177 | * A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file. 178 | * @default "projenrc" 179 | * @stability stable 180 | */ 181 | readonly projenCodeDir?: string; 182 | /** 183 | * The name of the projenrc file. 184 | * @default ".projenrc.ts" 185 | * @stability stable 186 | */ 187 | readonly filename?: string; 188 | } 189 | " 190 | `; 191 | 192 | exports[`can updateEvery property 1`] = ` 193 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 194 | 195 | /** 196 | * MyInterface 197 | */ 198 | export interface MyInterface { 199 | /** 200 | * This property is required 201 | */ 202 | readonly propThree: boolean; 203 | /** 204 | * This property is required 205 | */ 206 | readonly propTwo: boolean; 207 | /** 208 | * This property is required 209 | */ 210 | readonly propOne: boolean; 211 | } 212 | " 213 | `; 214 | 215 | exports[`can use struct as type in add 1`] = ` 216 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 217 | 218 | /** 219 | * MyInterface 220 | */ 221 | export interface MyInterface { 222 | /** 223 | * Whether to use \`SWC\` for ts-node. 224 | * @default false 225 | * @stability experimental 226 | */ 227 | readonly swc?: boolean; 228 | /** 229 | * A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file. 230 | * @default "projenrc" 231 | * @stability experimental 232 | */ 233 | readonly projenCodeDir?: string; 234 | /** 235 | * The name of the projenrc file. 236 | * @default .projenRC.ts 237 | * @stability experimental 238 | */ 239 | readonly filename?: string; 240 | } 241 | " 242 | `; 243 | 244 | exports[`can use struct as type in add 2`] = ` 245 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 246 | import type { MyInterface } from './'; 247 | 248 | /** 249 | * MyBaseInterface 250 | */ 251 | export interface MyBaseInterface { 252 | /** 253 | * My new Summary 254 | */ 255 | readonly projenrcTsOptions: MyInterface; 256 | } 257 | " 258 | `; 259 | 260 | exports[`can use the result of a chain 1`] = ` 261 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 262 | 263 | /** 264 | * MyInterface 265 | */ 266 | export interface MyInterface { 267 | /** 268 | * The name of the projenrc file. 269 | * @default ".projenrc.ts" 270 | * @stability experimental 271 | */ 272 | readonly filename?: string; 273 | /** 274 | * A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file. 275 | * @default "projenrc" 276 | * @stability experimental 277 | */ 278 | readonly projenCodeDir?: string; 279 | /** 280 | * Whether to use \`SWC\` for ts-node. 281 | * @default false 282 | * @stability experimental 283 | */ 284 | readonly swc?: boolean; 285 | } 286 | " 287 | `; 288 | -------------------------------------------------------------------------------- /test/projen/__snapshots__/ts-interface.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`can render a struct 1`] = ` 4 | "// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 5 | import type { github, GitOptions, GroupRunnerOptions, IgnoreFileOptions, javascript, LoggerOptions, Project, ProjectType, ProjenrcJsonOptions, ReleasableCommits, release, RenovatebotOptions, SampleReadmeProps, typescript } from '../'; 6 | 7 | /** 8 | * @stability experimental 9 | */ 10 | export interface TypeScriptProjectOptions { 11 | /** 12 | * This is the name of your project. 13 | * @default $BASEDIR 14 | * @stability experimental 15 | * @featured true 16 | */ 17 | readonly name: string; 18 | /** 19 | * Whether to commit the managed files by default. 20 | * @default true 21 | * @stability experimental 22 | */ 23 | readonly commitGenerated?: boolean; 24 | /** 25 | * Configuration options for .gitignore file. 26 | * @stability experimental 27 | */ 28 | readonly gitIgnoreOptions?: IgnoreFileOptions; 29 | /** 30 | * Configuration options for git. 31 | * @stability experimental 32 | */ 33 | readonly gitOptions?: GitOptions; 34 | /** 35 | * Configure logging options such as verbosity. 36 | * @default {} 37 | * @stability experimental 38 | */ 39 | readonly logging?: LoggerOptions; 40 | /** 41 | * The root directory of the project. 42 | * Relative to this directory, all files are synthesized. 43 | * 44 | * If this project has a parent, this directory is relative to the parent 45 | * directory and it cannot be the same as the parent or any of it's other 46 | * subprojects. 47 | * @default "." 48 | * @stability experimental 49 | */ 50 | readonly outdir?: string; 51 | /** 52 | * The parent project, if this project is part of a bigger project. 53 | * @stability experimental 54 | */ 55 | readonly parent?: Project; 56 | /** 57 | * The shell command to use in order to run the projen CLI. 58 | * Can be used to customize in special environments. 59 | * @default "npx projen" 60 | * @stability experimental 61 | */ 62 | readonly projenCommand?: string; 63 | /** 64 | * Generate (once) .projenrc.json (in JSON). Set to \`false\` in order to disable .projenrc.json generation. 65 | * @default false 66 | * @stability experimental 67 | */ 68 | readonly projenrcJson?: boolean; 69 | /** 70 | * Options for .projenrc.json. 71 | * @default - default options 72 | * @stability experimental 73 | */ 74 | readonly projenrcJsonOptions?: ProjenrcJsonOptions; 75 | /** 76 | * Use renovatebot to handle dependency upgrades. 77 | * @default false 78 | * @stability experimental 79 | */ 80 | readonly renovatebot?: boolean; 81 | /** 82 | * Options for renovatebot. 83 | * @default - default options 84 | * @stability experimental 85 | */ 86 | readonly renovatebotOptions?: RenovatebotOptions; 87 | /** 88 | * Enable and configure the 'auto approve' workflow. 89 | * @default - auto approve is disabled 90 | * @stability experimental 91 | */ 92 | readonly autoApproveOptions?: github.AutoApproveOptions; 93 | /** 94 | * Enable automatic merging on GitHub. 95 | * Has no effect if \`github.mergify\` 96 | * is set to false. 97 | * @default true 98 | * @stability experimental 99 | */ 100 | readonly autoMerge?: boolean; 101 | /** 102 | * Configure options for automatic merging on GitHub. 103 | * Has no effect if 104 | * \`github.mergify\` or \`autoMerge\` is set to false. 105 | * @default - see defaults in \`AutoMergeOptions\` 106 | * @stability experimental 107 | */ 108 | readonly autoMergeOptions?: github.AutoMergeOptions; 109 | /** 110 | * Add a \`clobber\` task which resets the repo to origin. 111 | * @default - true, but false for subprojects 112 | * @stability experimental 113 | */ 114 | readonly clobber?: boolean; 115 | /** 116 | * Add a VSCode development environment (used for GitHub Codespaces). 117 | * @default false 118 | * @stability experimental 119 | */ 120 | readonly devContainer?: boolean; 121 | /** 122 | * Enable GitHub integration. 123 | * Enabled by default for root projects. Disabled for non-root projects. 124 | * @default true 125 | * @stability experimental 126 | */ 127 | readonly github?: boolean; 128 | /** 129 | * Options for GitHub integration. 130 | * @default - see GitHubOptions 131 | * @stability experimental 132 | */ 133 | readonly githubOptions?: github.GitHubOptions; 134 | /** 135 | * Add a Gitpod development environment. 136 | * @default false 137 | * @stability experimental 138 | */ 139 | readonly gitpod?: boolean; 140 | /** 141 | * Whether mergify should be enabled on this repository or not. 142 | * @default true 143 | * @deprecated use \`githubOptions.mergify\` instead 144 | * @stability deprecated 145 | */ 146 | readonly mergify?: boolean; 147 | /** 148 | * Options for mergify. 149 | * @default - default options 150 | * @deprecated use \`githubOptions.mergifyOptions\` instead 151 | * @stability deprecated 152 | */ 153 | readonly mergifyOptions?: github.MergifyOptions; 154 | /** 155 | * Which type of project this is (library/app). 156 | * @default ProjectType.UNKNOWN 157 | * @deprecated no longer supported at the base project level 158 | * @stability deprecated 159 | */ 160 | readonly projectType?: ProjectType; 161 | /** 162 | * Choose a method of providing GitHub API access for projen workflows. 163 | * @default - use a personal access token named PROJEN_GITHUB_TOKEN 164 | * @stability experimental 165 | */ 166 | readonly projenCredentials?: github.GithubCredentials; 167 | /** 168 | * The name of a secret which includes a GitHub Personal Access Token to be used by projen workflows. 169 | * This token needs to have the \`repo\`, \`workflows\` 170 | * and \`packages\` scope. 171 | * @default "PROJEN_GITHUB_TOKEN" 172 | * @deprecated use \`projenCredentials\` 173 | * @stability deprecated 174 | */ 175 | readonly projenTokenSecret?: string; 176 | /** 177 | * The README setup. 178 | * @default - { filename: 'README.md', contents: '# replace this' } 179 | * @stability experimental 180 | */ 181 | readonly readme?: SampleReadmeProps; 182 | /** 183 | * Auto-close of stale issues and pull request. 184 | * See \`staleOptions\` for options. 185 | * @default false 186 | * @stability experimental 187 | */ 188 | readonly stale?: boolean; 189 | /** 190 | * Auto-close stale issues and pull requests. 191 | * To disable set \`stale\` to \`false\`. 192 | * @default - see defaults in \`StaleOptions\` 193 | * @stability experimental 194 | */ 195 | readonly staleOptions?: github.StaleOptions; 196 | /** 197 | * Enable VSCode integration. 198 | * Enabled by default for root projects. Disabled for non-root projects. 199 | * @default true 200 | * @stability experimental 201 | */ 202 | readonly vscode?: boolean; 203 | /** 204 | * Allow the project to include \`peerDependencies\` and \`bundledDependencies\`. 205 | * This is normally only allowed for libraries. For apps, there's no meaning 206 | * for specifying these. 207 | * @default true 208 | * @stability experimental 209 | */ 210 | readonly allowLibraryDependencies?: boolean; 211 | /** 212 | * Author's e-mail. 213 | * @stability experimental 214 | */ 215 | readonly authorEmail?: string; 216 | /** 217 | * Author's name. 218 | * @stability experimental 219 | */ 220 | readonly authorName?: string; 221 | /** 222 | * Is the author an organization. 223 | * @stability experimental 224 | */ 225 | readonly authorOrganization?: boolean; 226 | /** 227 | * Author's URL / Website. 228 | * @stability experimental 229 | */ 230 | readonly authorUrl?: string; 231 | /** 232 | * Automatically add all executables under the \`bin\` directory to your \`package.json\` file under the \`bin\` section. 233 | * @default true 234 | * @stability experimental 235 | */ 236 | readonly autoDetectBin?: boolean; 237 | /** 238 | * Binary programs vended with your module. 239 | * You can use this option to add/customize how binaries are represented in 240 | * your \`package.json\`, but unless \`autoDetectBin\` is \`false\`, every 241 | * executable file under \`bin\` will automatically be added to this section. 242 | * @stability experimental 243 | */ 244 | readonly bin?: Record; 245 | /** 246 | * The email address to which issues should be reported. 247 | * @stability experimental 248 | */ 249 | readonly bugsEmail?: string; 250 | /** 251 | * The url to your project's issue tracker. 252 | * @stability experimental 253 | */ 254 | readonly bugsUrl?: string; 255 | /** 256 | * List of dependencies to bundle into this module. 257 | * These modules will be 258 | * added both to the \`dependencies\` section and \`bundledDependencies\` section of 259 | * your \`package.json\`. 260 | * 261 | * The recommendation is to only specify the module name here (e.g. 262 | * \`express\`). This will behave similar to \`yarn add\` or \`npm install\` in the 263 | * sense that it will add the module as a dependency to your \`package.json\` 264 | * file with the latest version (\`^\`). You can specify semver requirements in 265 | * the same syntax passed to \`npm i\` or \`yarn add\` (e.g. \`express@^2\`) and 266 | * this will be what you \`package.json\` will eventually include. 267 | * @stability experimental 268 | */ 269 | readonly bundledDeps?: Array; 270 | /** 271 | * The version of Bun to use if using Bun as a package manager. 272 | * @default "latest" 273 | * @stability experimental 274 | */ 275 | readonly bunVersion?: string; 276 | /** 277 | * Options for npm packages using AWS CodeArtifact. 278 | * This is required if publishing packages to, or installing scoped packages from AWS CodeArtifact 279 | * @default - undefined 280 | * @stability experimental 281 | */ 282 | readonly codeArtifactOptions?: javascript.CodeArtifactOptions; 283 | /** 284 | * Runtime dependencies of this module. 285 | * The recommendation is to only specify the module name here (e.g. 286 | * \`express\`). This will behave similar to \`yarn add\` or \`npm install\` in the 287 | * sense that it will add the module as a dependency to your \`package.json\` 288 | * file with the latest version (\`^\`). You can specify semver requirements in 289 | * the same syntax passed to \`npm i\` or \`yarn add\` (e.g. \`express@^2\`) and 290 | * this will be what you \`package.json\` will eventually include. 291 | * @default [] 292 | * @stability experimental 293 | * @featured true 294 | */ 295 | readonly deps?: Array; 296 | /** 297 | * The description is just a string that helps people understand the purpose of the package. 298 | * It can be used when searching for packages in a package manager as well. 299 | * See https://classic.yarnpkg.com/en/docs/package-json/#toc-description 300 | * @stability experimental 301 | * @featured true 302 | */ 303 | readonly description?: string; 304 | /** 305 | * Build dependencies for this module. 306 | * These dependencies will only be 307 | * available in your build environment but will not be fetched when this 308 | * module is consumed. 309 | * 310 | * The recommendation is to only specify the module name here (e.g. 311 | * \`express\`). This will behave similar to \`yarn add\` or \`npm install\` in the 312 | * sense that it will add the module as a dependency to your \`package.json\` 313 | * file with the latest version (\`^\`). You can specify semver requirements in 314 | * the same syntax passed to \`npm i\` or \`yarn add\` (e.g. \`express@^2\`) and 315 | * this will be what you \`package.json\` will eventually include. 316 | * @default [] 317 | * @stability experimental 318 | * @featured true 319 | */ 320 | readonly devDeps?: Array; 321 | /** 322 | * Module entrypoint (\`main\` in \`package.json\`). 323 | * Set to an empty string to not include \`main\` in your package.json 324 | * @default "lib/index.js" 325 | * @stability experimental 326 | */ 327 | readonly entrypoint?: string; 328 | /** 329 | * Package's Homepage / Website. 330 | * @stability experimental 331 | */ 332 | readonly homepage?: string; 333 | /** 334 | * Keywords to include in \`package.json\`. 335 | * @stability experimental 336 | */ 337 | readonly keywords?: Array; 338 | /** 339 | * License's SPDX identifier. 340 | * See https://github.com/projen/projen/tree/main/license-text for a list of supported licenses. 341 | * Use the \`licensed\` option if you want to no license to be specified. 342 | * @default "Apache-2.0" 343 | * @stability experimental 344 | */ 345 | readonly license?: string; 346 | /** 347 | * Indicates if a license should be added. 348 | * @default true 349 | * @stability experimental 350 | */ 351 | readonly licensed?: boolean; 352 | /** 353 | * The maximum node version supported by this package. Most projects should not use this option. 354 | * The value indicates that the package is incompatible with any newer versions of node. 355 | * This requirement is enforced via the engines field. 356 | * 357 | * You will normally not need to set this option. 358 | * Consider this option only if your package is known to not function with newer versions of node. 359 | * @default - no maximum version is enforced 360 | * @stability experimental 361 | */ 362 | readonly maxNodeVersion?: string; 363 | /** 364 | * The minimum node version required by this package to function. Most projects should not use this option. 365 | * The value indicates that the package is incompatible with any older versions of node. 366 | * This requirement is enforced via the engines field. 367 | * 368 | * You will normally not need to set this option, even if your package is incompatible with EOL versions of node. 369 | * Consider this option only if your package depends on a specific feature, that is not available in other LTS versions. 370 | * Setting this option has very high impact on the consumers of your package, 371 | * as package managers will actively prevent usage with node versions you have marked as incompatible. 372 | * 373 | * To change the node version of your CI/CD workflows, use \`workflowNodeVersion\`. 374 | * @default - no minimum version is enforced 375 | * @stability experimental 376 | */ 377 | readonly minNodeVersion?: string; 378 | /** 379 | * Access level of the npm package. 380 | * @default - for scoped packages (e.g. \`foo@bar\`), the default is 381 | \`NpmAccess.RESTRICTED\`, for non-scoped packages, the default is 382 | \`NpmAccess.PUBLIC\`. 383 | * @stability experimental 384 | */ 385 | readonly npmAccess?: javascript.NpmAccess; 386 | /** 387 | * Should provenance statements be generated when the package is published. 388 | * A supported package manager is required to publish a package with npm provenance statements and 389 | * you will need to use a supported CI/CD provider. 390 | * 391 | * Note that the projen \`Release\` and \`Publisher\` components are using \`publib\` to publish packages, 392 | * which is using npm internally and supports provenance statements independently of the package manager used. 393 | * @default - true for public packages, false otherwise 394 | * @stability experimental 395 | */ 396 | readonly npmProvenance?: boolean; 397 | /** 398 | * The host name of the npm registry to publish to. 399 | * Cannot be set together with \`npmRegistryUrl\`. 400 | * @deprecated use \`npmRegistryUrl\` instead 401 | * @stability deprecated 402 | */ 403 | readonly npmRegistry?: string; 404 | /** 405 | * The base URL of the npm package registry. 406 | * Must be a URL (e.g. start with "https://" or "http://") 407 | * @default "https://registry.npmjs.org" 408 | * @stability experimental 409 | */ 410 | readonly npmRegistryUrl?: string; 411 | /** 412 | * GitHub secret which contains the NPM token to use when publishing packages. 413 | * @default "NPM_TOKEN" 414 | * @stability experimental 415 | */ 416 | readonly npmTokenSecret?: string; 417 | /** 418 | * The Node Package Manager used to execute scripts. 419 | * @default NodePackageManager.YARN_CLASSIC 420 | * @stability experimental 421 | */ 422 | readonly packageManager?: javascript.NodePackageManager; 423 | /** 424 | * The "name" in package.json. 425 | * @default - defaults to project name 426 | * @stability experimental 427 | * @featured true 428 | */ 429 | readonly packageName?: string; 430 | /** 431 | * Options for \`peerDeps\`. 432 | * @stability experimental 433 | */ 434 | readonly peerDependencyOptions?: javascript.PeerDependencyOptions; 435 | /** 436 | * Peer dependencies for this module. 437 | * Dependencies listed here are required to 438 | * be installed (and satisfied) by the _consumer_ of this library. Using peer 439 | * dependencies allows you to ensure that only a single module of a certain 440 | * library exists in the \`node_modules\` tree of your consumers. 441 | * 442 | * Note that prior to npm@7, peer dependencies are _not_ automatically 443 | * installed, which means that adding peer dependencies to a library will be a 444 | * breaking change for your customers. 445 | * 446 | * Unless \`peerDependencyOptions.pinnedDevDependency\` is disabled (it is 447 | * enabled by default), projen will automatically add a dev dependency with a 448 | * pinned version for each peer dependency. This will ensure that you build & 449 | * test your module against the lowest peer version required. 450 | * @default [] 451 | * @stability experimental 452 | */ 453 | readonly peerDeps?: Array; 454 | /** 455 | * The version of PNPM to use if using PNPM as a package manager. 456 | * @default "9" 457 | * @stability experimental 458 | */ 459 | readonly pnpmVersion?: string; 460 | /** 461 | * The repository is the location where the actual code for your package lives. 462 | * See https://classic.yarnpkg.com/en/docs/package-json/#toc-repository 463 | * @stability experimental 464 | */ 465 | readonly repository?: string; 466 | /** 467 | * If the package.json for your package is not in the root directory (for example if it is part of a monorepo), you can specify the directory in which it lives. 468 | * @stability experimental 469 | */ 470 | readonly repositoryDirectory?: string; 471 | /** 472 | * Options for privately hosted scoped packages. 473 | * @default - fetch all scoped packages from the public npm registry 474 | * @stability experimental 475 | */ 476 | readonly scopedPackagesOptions?: Array; 477 | /** 478 | * npm scripts to include. 479 | * If a script has the same name as a standard script, 480 | * the standard script will be overwritten. 481 | * Also adds the script as a task. 482 | * @default {} 483 | * @deprecated use \`project.addTask()\` or \`package.setScript()\` 484 | * @stability deprecated 485 | */ 486 | readonly scripts?: Record; 487 | /** 488 | * Package's Stability. 489 | * @stability experimental 490 | */ 491 | readonly stability?: string; 492 | /** 493 | * Options for Yarn Berry. 494 | * @default - Yarn Berry v4 with all default options 495 | * @stability experimental 496 | */ 497 | readonly yarnBerryOptions?: javascript.YarnBerryOptions; 498 | /** 499 | * The \`commit-and-tag-version\` compatible package used to bump the package version, as a dependency string. 500 | * This can be any compatible package version, including the deprecated \`standard-version@9\`. 501 | * @default - A recent version of "commit-and-tag-version" 502 | * @stability experimental 503 | */ 504 | readonly bumpPackage?: string; 505 | /** 506 | * Version requirement of \`publib\` which is used to publish modules to npm. 507 | * @default "latest" 508 | * @stability experimental 509 | */ 510 | readonly jsiiReleaseVersion?: string; 511 | /** 512 | * Major version to release from the default branch. 513 | * If this is specified, we bump the latest version of this major version line. 514 | * If not specified, we bump the global latest version. 515 | * @default - Major version is not enforced. 516 | * @stability experimental 517 | */ 518 | readonly majorVersion?: number; 519 | /** 520 | * Minimal Major version to release. 521 | * This can be useful to set to 1, as breaking changes before the 1.x major 522 | * release are not incrementing the major version number. 523 | * 524 | * Can not be set together with \`majorVersion\`. 525 | * @default - No minimum version is being enforced 526 | * @stability experimental 527 | */ 528 | readonly minMajorVersion?: number; 529 | /** 530 | * A shell command to control the next version to release. 531 | * If present, this shell command will be run before the bump is executed, and 532 | * it determines what version to release. It will be executed in the following 533 | * environment: 534 | * 535 | * - Working directory: the project directory. 536 | * - \`$VERSION\`: the current version. Looks like \`1.2.3\`. 537 | * - \`$LATEST_TAG\`: the most recent tag. Looks like \`prefix-v1.2.3\`, or may be unset. 538 | * - \`$SUGGESTED_BUMP\`: the suggested bump action based on commits. One of \`major|minor|patch|none\`. 539 | * 540 | * The command should print one of the following to \`stdout\`: 541 | * 542 | * - Nothing: the next version number will be determined based on commit history. 543 | * - \`x.y.z\`: the next version number will be \`x.y.z\`. 544 | * - \`major|minor|patch\`: the next version number will be the current version number 545 | * with the indicated component bumped. 546 | * 547 | * This setting cannot be specified together with \`minMajorVersion\`; the invoked 548 | * script can be used to achieve the effects of \`minMajorVersion\`. 549 | * @default - The next version will be determined based on the commit history and project settings. 550 | * @stability experimental 551 | */ 552 | readonly nextVersionCommand?: string; 553 | /** 554 | * The npmDistTag to use when publishing from the default branch. 555 | * To set the npm dist-tag for release branches, set the \`npmDistTag\` property 556 | * for each branch. 557 | * @default "latest" 558 | * @stability experimental 559 | */ 560 | readonly npmDistTag?: string; 561 | /** 562 | * Steps to execute after build as part of the release workflow. 563 | * @default [] 564 | * @stability experimental 565 | */ 566 | readonly postBuildSteps?: Array; 567 | /** 568 | * Bump versions from the default branch as pre-releases (e.g. "beta", "alpha", "pre"). 569 | * @default - normal semantic versions 570 | * @stability experimental 571 | */ 572 | readonly prerelease?: string; 573 | /** 574 | * Instead of actually publishing to package managers, just print the publishing command. 575 | * @default false 576 | * @stability experimental 577 | */ 578 | readonly publishDryRun?: boolean; 579 | /** 580 | * Define publishing tasks that can be executed manually as well as workflows. 581 | * Normally, publishing only happens within automated workflows. Enable this 582 | * in order to create a publishing task for each publishing activity. 583 | * @default false 584 | * @stability experimental 585 | */ 586 | readonly publishTasks?: boolean; 587 | /** 588 | * Find commits that should be considered releasable Used to decide if a release is required. 589 | * @default ReleasableCommits.everyCommit() 590 | * @stability experimental 591 | */ 592 | readonly releasableCommits?: ReleasableCommits; 593 | /** 594 | * Defines additional release branches. 595 | * A workflow will be created for each 596 | * release branch which will publish releases from commits in this branch. 597 | * Each release branch _must_ be assigned a major version number which is used 598 | * to enforce that versions published from that branch always use that major 599 | * version. If multiple branches are used, the \`majorVersion\` field must also 600 | * be provided for the default branch. 601 | * @default - no additional branches are used for release. you can use 602 | \`addBranch()\` to add additional branches. 603 | * @stability experimental 604 | */ 605 | readonly releaseBranches?: Record; 606 | /** 607 | * Automatically release new versions every commit to one of branches in \`releaseBranches\`. 608 | * @default true 609 | * @deprecated Use \`releaseTrigger: ReleaseTrigger.continuous()\` instead 610 | * @stability deprecated 611 | */ 612 | readonly releaseEveryCommit?: boolean; 613 | /** 614 | * Create a github issue on every failed publishing task. 615 | * @default false 616 | * @stability experimental 617 | */ 618 | readonly releaseFailureIssue?: boolean; 619 | /** 620 | * The label to apply to issues indicating publish failures. 621 | * Only applies if \`releaseFailureIssue\` is true. 622 | * @default "failed-release" 623 | * @stability experimental 624 | */ 625 | readonly releaseFailureIssueLabel?: string; 626 | /** 627 | * CRON schedule to trigger new releases. 628 | * @default - no scheduled releases 629 | * @deprecated Use \`releaseTrigger: ReleaseTrigger.scheduled()\` instead 630 | * @stability deprecated 631 | */ 632 | readonly releaseSchedule?: string; 633 | /** 634 | * Automatically add the given prefix to release tags. Useful if you are releasing on multiple branches with overlapping version numbers. 635 | * Note: this prefix is used to detect the latest tagged version 636 | * when bumping, so if you change this on a project with an existing version 637 | * history, you may need to manually tag your latest release 638 | * with the new prefix. 639 | * @default "v" 640 | * @stability experimental 641 | */ 642 | readonly releaseTagPrefix?: string; 643 | /** 644 | * The release trigger to use. 645 | * @default - Continuous releases (\`ReleaseTrigger.continuous()\`) 646 | * @stability experimental 647 | */ 648 | readonly releaseTrigger?: release.ReleaseTrigger; 649 | /** 650 | * The name of the default release workflow. 651 | * @default "release" 652 | * @stability experimental 653 | */ 654 | readonly releaseWorkflowName?: string; 655 | /** 656 | * A set of workflow steps to execute in order to setup the workflow container. 657 | * @stability experimental 658 | */ 659 | readonly releaseWorkflowSetupSteps?: Array; 660 | /** 661 | * Custom configuration used when creating changelog with commit-and-tag-version package. 662 | * Given values either append to default configuration or overwrite values in it. 663 | * @default - standard configuration applicable for GitHub repositories 664 | * @stability experimental 665 | */ 666 | readonly versionrcOptions?: Record; 667 | /** 668 | * Container image to use for GitHub workflows. 669 | * @default - default image 670 | * @stability experimental 671 | */ 672 | readonly workflowContainerImage?: string; 673 | /** 674 | * Github Runner selection labels. 675 | * @default ["ubuntu-latest"] 676 | * @stability experimental 677 | * @description Defines a target Runner by labels 678 | * @throws {Error} if both \`runsOn\` and \`runsOnGroup\` are specified 679 | */ 680 | readonly workflowRunsOn?: Array; 681 | /** 682 | * Github Runner Group selection options. 683 | * @stability experimental 684 | * @description Defines a target Runner Group by name and/or labels 685 | * @throws {Error} if both \`runsOn\` and \`runsOnGroup\` are specified 686 | */ 687 | readonly workflowRunsOnGroup?: GroupRunnerOptions; 688 | /** 689 | * The name of the main release branch. 690 | * @default "main" 691 | * @stability experimental 692 | */ 693 | readonly defaultReleaseBranch: string; 694 | /** 695 | * A directory which will contain build artifacts. 696 | * @default "dist" 697 | * @stability experimental 698 | */ 699 | readonly artifactsDirectory?: string; 700 | /** 701 | * Automatically approve deps upgrade PRs, allowing them to be merged by mergify (if configued). 702 | * Throw if set to true but \`autoApproveOptions\` are not defined. 703 | * @default - true 704 | * @stability experimental 705 | */ 706 | readonly autoApproveUpgrades?: boolean; 707 | /** 708 | * Define a GitHub workflow for building PRs. 709 | * @default - true if not a subproject 710 | * @stability experimental 711 | */ 712 | readonly buildWorkflow?: boolean; 713 | /** 714 | * Options for PR build workflow. 715 | * @stability experimental 716 | */ 717 | readonly buildWorkflowOptions?: javascript.BuildWorkflowOptions; 718 | /** 719 | * Build workflow triggers. 720 | * @default "{ pullRequest: {}, workflowDispatch: {} }" 721 | * @deprecated - Use \`buildWorkflowOptions.workflowTriggers\` 722 | * @stability deprecated 723 | */ 724 | readonly buildWorkflowTriggers?: github.workflows.Triggers; 725 | /** 726 | * Options for \`Bundler\`. 727 | * @stability experimental 728 | */ 729 | readonly bundlerOptions?: javascript.BundlerOptions; 730 | /** 731 | * Configure which licenses should be deemed acceptable for use by dependencies. 732 | * This setting will cause the build to fail, if any prohibited or not allowed licenses ares encountered. 733 | * @default - no license checks are run during the build and all licenses will be accepted 734 | * @stability experimental 735 | */ 736 | readonly checkLicenses?: javascript.LicenseCheckerOptions; 737 | /** 738 | * Define a GitHub workflow step for sending code coverage metrics to https://codecov.io/ Uses codecov/codecov-action@v4 A secret is required for private repos. Configured with \`@codeCovTokenSecret\`. 739 | * @default false 740 | * @stability experimental 741 | */ 742 | readonly codeCov?: boolean; 743 | /** 744 | * Define the secret name for a specified https://codecov.io/ token A secret is required to send coverage for private repositories. 745 | * @default - if this option is not specified, only public repositories are supported 746 | * @stability experimental 747 | */ 748 | readonly codeCovTokenSecret?: string; 749 | /** 750 | * License copyright owner. 751 | * @default - defaults to the value of authorName or "" if \`authorName\` is undefined. 752 | * @stability experimental 753 | */ 754 | readonly copyrightOwner?: string; 755 | /** 756 | * The copyright years to put in the LICENSE file. 757 | * @default - current year 758 | * @stability experimental 759 | */ 760 | readonly copyrightPeriod?: string; 761 | /** 762 | * Use dependabot to handle dependency upgrades. 763 | * Cannot be used in conjunction with \`depsUpgrade\`. 764 | * @default false 765 | * @stability experimental 766 | */ 767 | readonly dependabot?: boolean; 768 | /** 769 | * Options for dependabot. 770 | * @default - default options 771 | * @stability experimental 772 | */ 773 | readonly dependabotOptions?: github.DependabotOptions; 774 | /** 775 | * Use tasks and github workflows to handle dependency upgrades. 776 | * Cannot be used in conjunction with \`dependabot\`. 777 | * @default true 778 | * @stability experimental 779 | */ 780 | readonly depsUpgrade?: boolean; 781 | /** 782 | * Options for \`UpgradeDependencies\`. 783 | * @default - default options 784 | * @stability experimental 785 | */ 786 | readonly depsUpgradeOptions?: javascript.UpgradeDependenciesOptions; 787 | /** 788 | * Additional entries to .gitignore. 789 | * @stability experimental 790 | */ 791 | readonly gitignore?: Array; 792 | /** 793 | * Setup jest unit tests. 794 | * @default true 795 | * @stability experimental 796 | */ 797 | readonly jest?: boolean; 798 | /** 799 | * Jest options. 800 | * @default - default options 801 | * @stability experimental 802 | */ 803 | readonly jestOptions?: javascript.JestOptions; 804 | /** 805 | * Automatically update files modified during builds to pull-request branches. 806 | * This means 807 | * that any files synthesized by projen or e.g. test snapshots will always be up-to-date 808 | * before a PR is merged. 809 | * 810 | * Implies that PR builds do not have anti-tamper checks. 811 | * @default true 812 | * @deprecated - Use \`buildWorkflowOptions.mutableBuild\` 813 | * @stability deprecated 814 | */ 815 | readonly mutableBuild?: boolean; 816 | /** 817 | * Additional entries to .npmignore. 818 | * @deprecated - use \`project.addPackageIgnore\` 819 | * @stability deprecated 820 | */ 821 | readonly npmignore?: Array; 822 | /** 823 | * Defines an .npmignore file. Normally this is only needed for libraries that are packaged as tarballs. 824 | * @default true 825 | * @stability experimental 826 | */ 827 | readonly npmignoreEnabled?: boolean; 828 | /** 829 | * Configuration options for .npmignore file. 830 | * @stability experimental 831 | */ 832 | readonly npmIgnoreOptions?: IgnoreFileOptions; 833 | /** 834 | * Defines a \`package\` task that will produce an npm tarball under the artifacts directory (e.g. \`dist\`). 835 | * @default true 836 | * @stability experimental 837 | */ 838 | readonly package?: boolean; 839 | /** 840 | * Setup prettier. 841 | * @default false 842 | * @stability experimental 843 | */ 844 | readonly prettier?: boolean; 845 | /** 846 | * Prettier options. 847 | * @default - default options 848 | * @stability experimental 849 | */ 850 | readonly prettierOptions?: javascript.PrettierOptions; 851 | /** 852 | * Indicates of "projen" should be installed as a devDependency. 853 | * @default - true if not a subproject 854 | * @stability experimental 855 | */ 856 | readonly projenDevDependency?: boolean; 857 | /** 858 | * Generate (once) .projenrc.js (in JavaScript). Set to \`false\` in order to disable .projenrc.js generation. 859 | * @default - true if projenrcJson is false 860 | * @stability experimental 861 | */ 862 | readonly projenrcJs?: boolean; 863 | /** 864 | * Options for .projenrc.js. 865 | * @default - default options 866 | * @stability experimental 867 | */ 868 | readonly projenrcJsOptions?: javascript.ProjenrcOptions; 869 | /** 870 | * Version of projen to install. 871 | * @default - Defaults to the latest version. 872 | * @stability experimental 873 | */ 874 | readonly projenVersion?: string; 875 | /** 876 | * Include a GitHub pull request template. 877 | * @default true 878 | * @stability experimental 879 | */ 880 | readonly pullRequestTemplate?: boolean; 881 | /** 882 | * The contents of the pull request template. 883 | * @default - default content 884 | * @stability experimental 885 | */ 886 | readonly pullRequestTemplateContents?: Array; 887 | /** 888 | * Add release management to this project. 889 | * @default - true (false for subprojects) 890 | * @stability experimental 891 | */ 892 | readonly release?: boolean; 893 | /** 894 | * Automatically release to npm when new versions are introduced. 895 | * @default false 896 | * @stability experimental 897 | */ 898 | readonly releaseToNpm?: boolean; 899 | /** 900 | * DEPRECATED: renamed to \`release\`. 901 | * @default - true if not a subproject 902 | * @deprecated see \`release\`. 903 | * @stability deprecated 904 | */ 905 | readonly releaseWorkflow?: boolean; 906 | /** 907 | * Workflow steps to use in order to bootstrap this repo. 908 | * @default "yarn install --frozen-lockfile && yarn projen" 909 | * @stability experimental 910 | */ 911 | readonly workflowBootstrapSteps?: Array; 912 | /** 913 | * The git identity to use in workflows. 914 | * @default - GitHub Actions 915 | * @stability experimental 916 | */ 917 | readonly workflowGitIdentity?: github.GitIdentity; 918 | /** 919 | * The node version used in GitHub Actions workflows. 920 | * Always use this option if your GitHub Actions workflows require a specific to run. 921 | * @default - \`minNodeVersion\` if set, otherwise \`lts/*\`. 922 | * @stability experimental 923 | */ 924 | readonly workflowNodeVersion?: string; 925 | /** 926 | * Enable Node.js package cache in GitHub workflows. 927 | * @default false 928 | * @stability experimental 929 | */ 930 | readonly workflowPackageCache?: boolean; 931 | /** 932 | * Do not generate a \`tsconfig.json\` file (used by jsii projects since tsconfig.json is generated by the jsii compiler). 933 | * @default false 934 | * @stability experimental 935 | */ 936 | readonly disableTsconfig?: boolean; 937 | /** 938 | * Do not generate a \`tsconfig.dev.json\` file. 939 | * @default false 940 | * @stability experimental 941 | */ 942 | readonly disableTsconfigDev?: boolean; 943 | /** 944 | * Docgen by Typedoc. 945 | * @default false 946 | * @stability experimental 947 | */ 948 | readonly docgen?: boolean; 949 | /** 950 | * Docs directory. 951 | * @default "docs" 952 | * @stability experimental 953 | */ 954 | readonly docsDirectory?: string; 955 | /** 956 | * The .d.ts file that includes the type declarations for this module. 957 | * @default - .d.ts file derived from the project's entrypoint (usually lib/index.d.ts) 958 | * @stability experimental 959 | */ 960 | readonly entrypointTypes?: string; 961 | /** 962 | * Setup eslint. 963 | * @default true 964 | * @stability experimental 965 | */ 966 | readonly eslint?: boolean; 967 | /** 968 | * Eslint options. 969 | * @default - opinionated default options 970 | * @stability experimental 971 | */ 972 | readonly eslintOptions?: javascript.EslintOptions; 973 | /** 974 | * Typescript artifacts output directory. 975 | * @default "lib" 976 | * @stability experimental 977 | */ 978 | readonly libdir?: string; 979 | /** 980 | * Use TypeScript for your projenrc file (\`.projenrc.ts\`). 981 | * @default false 982 | * @stability experimental 983 | * @pjnew true 984 | */ 985 | readonly projenrcTs?: boolean; 986 | /** 987 | * Options for .projenrc.ts. 988 | * @stability experimental 989 | */ 990 | readonly projenrcTsOptions?: typescript.ProjenrcOptions; 991 | /** 992 | * Generate one-time sample in \`src/\` and \`test/\` if there are no files there. 993 | * @default true 994 | * @stability experimental 995 | */ 996 | readonly sampleCode?: boolean; 997 | /** 998 | * Typescript sources directory. 999 | * @default "src" 1000 | * @stability experimental 1001 | */ 1002 | readonly srcdir?: string; 1003 | /** 1004 | * Jest tests directory. Tests files should be named \`xxx.test.ts\`. 1005 | * If this directory is under \`srcdir\` (e.g. \`src/test\`, \`src/__tests__\`), 1006 | * then tests are going to be compiled into \`lib/\` and executed as javascript. 1007 | * If the test directory is outside of \`src\`, then we configure jest to 1008 | * compile the code in-memory. 1009 | * @default "test" 1010 | * @stability experimental 1011 | */ 1012 | readonly testdir?: string; 1013 | /** 1014 | * Custom TSConfig. 1015 | * @default - default options 1016 | * @stability experimental 1017 | */ 1018 | readonly tsconfig?: javascript.TypescriptConfigOptions; 1019 | /** 1020 | * Custom tsconfig options for the development tsconfig.json file (used for testing). 1021 | * @default - use the production tsconfig options 1022 | * @stability experimental 1023 | */ 1024 | readonly tsconfigDev?: javascript.TypescriptConfigOptions; 1025 | /** 1026 | * The name of the development tsconfig.json file. 1027 | * @default "tsconfig.dev.json" 1028 | * @stability experimental 1029 | */ 1030 | readonly tsconfigDevFile?: string; 1031 | /** 1032 | * Options for ts-jest. 1033 | * @stability experimental 1034 | */ 1035 | readonly tsJestOptions?: typescript.TsJestOptions; 1036 | /** 1037 | * TypeScript version to use. 1038 | * NOTE: Typescript is not semantically versioned and should remain on the 1039 | * same minor, so we recommend using a \`~\` dependency (e.g. \`~1.2.3\`). 1040 | * @default "latest" 1041 | * @stability experimental 1042 | */ 1043 | readonly typescriptVersion?: string; 1044 | } 1045 | " 1046 | `; 1047 | -------------------------------------------------------------------------------- /test/projen/projen-struct.test.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { PrimitiveType, Stability } from '@jsii/spec'; 4 | import { JsiiProject } from 'projen/lib/cdk'; 5 | import { NodePackageManager } from 'projen/lib/javascript'; 6 | import { 7 | TypeScriptProject, 8 | TypeScriptProjectOptions, 9 | } from 'projen/lib/typescript'; 10 | import { synthSnapshot } from 'projen/lib/util/synth'; 11 | import { Struct, ProjenStruct } from '../../src'; 12 | 13 | class TestProject extends TypeScriptProject { 14 | public constructor(options: Partial = {}) { 15 | super({ 16 | name: 'test', 17 | defaultReleaseBranch: 'main', 18 | ...options, 19 | }); 20 | } 21 | } 22 | 23 | test('can mixin a struct', () => { 24 | // ARRANGE 25 | const project = new TestProject(); 26 | 27 | // ACT 28 | const struct = new ProjenStruct(project, { 29 | name: 'MyInterface', 30 | }); 31 | struct.mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')); 32 | 33 | // PREPARE 34 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 35 | 36 | // ASSERT 37 | expect(renderedFile).toMatchSnapshot(); 38 | expect(renderedFile).toContain('projenCodeDir'); 39 | expect(renderedFile).toContain('filename'); 40 | }); 41 | 42 | test('can load structs from an assembly in the current working directory', () => { 43 | // ARRANGE 44 | const cwd = process.cwd(); 45 | const project = new JsiiProject({ 46 | name: '@mrgrain/local-assembly-test', 47 | author: 'Test', 48 | authorAddress: 'me@example.com', 49 | defaultReleaseBranch: 'main', 50 | repositoryUrl: 'https://example.com', 51 | jsiiVersion: '5.1.x', 52 | packageManager: NodePackageManager.NPM, 53 | }); 54 | 55 | // Create a new interface at src/index.ts, synth the project and run jsii to build the local assembly 56 | writeFileSync(join(project.outdir, '.jsii'), JSON.stringify({ 57 | author: { 58 | email: 'me@example.com', 59 | name: 'Test', 60 | roles: [ 61 | 'author', 62 | ], 63 | }, 64 | description: '@mrgrain/local-assembly-test', 65 | docs: { 66 | stability: 'stable', 67 | }, 68 | homepage: 'https://example.com', 69 | jsiiVersion: '5.1.12 (build 0675712)', 70 | license: 'Apache-2.0', 71 | metadata: { 72 | jsii: { 73 | pacmak: { 74 | hasDefaultInterfaces: true, 75 | }, 76 | }, 77 | tscRootDir: 'src', 78 | }, 79 | name: '@mrgrain/local-assembly-test', 80 | readme: { 81 | markdown: '# replace this', 82 | }, 83 | repository: { 84 | type: 'git', 85 | url: 'https://example.com', 86 | }, 87 | schema: 'jsii/0.10.0', 88 | targets: { 89 | js: { 90 | npm: '@mrgrain/local-assembly-test', 91 | }, 92 | }, 93 | types: { 94 | '@mrgrain/local-assembly-test.MyInterface': { 95 | assembly: '@mrgrain/local-assembly-test', 96 | datatype: true, 97 | docs: { 98 | stability: 'stable', 99 | summary: 'MyInterface.', 100 | }, 101 | fqn: '@mrgrain/local-assembly-test.MyInterface', 102 | kind: 'interface', 103 | locationInModule: { 104 | filename: 'src/index.ts', 105 | line: 6, 106 | }, 107 | name: 'MyInterface', 108 | properties: [ 109 | { 110 | abstract: true, 111 | docs: { 112 | default: '".projenrc.ts"', 113 | stability: 'experimental', 114 | summary: 'The name of the projenrc file.', 115 | }, 116 | immutable: true, 117 | locationInModule: { 118 | filename: 'src/index.ts', 119 | line: 24, 120 | }, 121 | name: 'filename', 122 | optional: true, 123 | type: { 124 | primitive: 'string', 125 | }, 126 | }, 127 | { 128 | abstract: true, 129 | docs: { 130 | default: '"projenrc"', 131 | stability: 'experimental', 132 | summary: 'A directory tree that may contain *.ts files that can be referenced from your projenrc typescript file.', 133 | }, 134 | immutable: true, 135 | locationInModule: { 136 | filename: 'src/index.ts', 137 | line: 18, 138 | }, 139 | name: 'projenCodeDir', 140 | optional: true, 141 | type: { 142 | primitive: 'string', 143 | }, 144 | }, 145 | { 146 | abstract: true, 147 | docs: { 148 | default: 'false', 149 | stability: 'experimental', 150 | summary: 'Whether to use `SWC` for ts-node.', 151 | }, 152 | immutable: true, 153 | locationInModule: { 154 | filename: 'src/index.ts', 155 | line: 12, 156 | }, 157 | name: 'swc', 158 | optional: true, 159 | type: { 160 | primitive: 'boolean', 161 | }, 162 | }, 163 | ], 164 | symbolId: 'src/index:MyInterface', 165 | }, 166 | }, 167 | version: '0.0.0', 168 | fingerprint: 'dWWza3Loc/4ffyckw0hxDDzZ2NAxtYLy55aT+FdHwBU=', 169 | })); 170 | 171 | // ACT 172 | process.chdir(project.outdir); 173 | new ProjenStruct(project, { name: 'MyExtendedInterface' }) 174 | .mixin(Struct.fromFqn('@mrgrain/local-assembly-test.MyInterface')); 175 | 176 | // PREPARE 177 | const renderedFile = synthSnapshot(project)['src/MyExtendedInterface.ts']; 178 | 179 | // ASSERT 180 | expect(renderedFile).toMatchSnapshot(); 181 | expect(renderedFile).toContain('projenCodeDir'); 182 | expect(renderedFile).toContain('filename'); 183 | 184 | // CLEANUP 185 | process.chdir(cwd); 186 | }); 187 | 188 | test('can use the result of a chain', () => { 189 | // ARRANGE 190 | const project = new TestProject(); 191 | const base = new ProjenStruct(project, { 192 | name: 'MyInterface', 193 | }); 194 | const other = new ProjenStruct(project, { 195 | name: 'MyOtherInterface', 196 | }).mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')); 197 | 198 | // ACT 199 | base.mixin(other); 200 | 201 | // PREPARE 202 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 203 | 204 | // ASSERT 205 | expect(renderedFile).toMatchSnapshot(); 206 | expect(renderedFile).toContain('projenCodeDir'); 207 | expect(renderedFile).toContain('filename'); 208 | }); 209 | 210 | test('can mixin another projen struct', () => { 211 | // ARRANGE 212 | const project = new TestProject(); 213 | 214 | // ACT 215 | const base = new ProjenStruct(project, { 216 | name: 'MyBaseInterface', 217 | }); 218 | base.mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')); 219 | 220 | const struct = new ProjenStruct(project, { 221 | name: 'MyInterface', 222 | }); 223 | struct.mixin(base); 224 | 225 | // PREPARE 226 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 227 | 228 | // ASSERT 229 | expect(renderedFile).toMatchSnapshot(); 230 | expect(renderedFile).toContain('projenCodeDir'); 231 | expect(renderedFile).toContain('filename'); 232 | }); 233 | 234 | test('can omit props', () => { 235 | // ARRANGE 236 | const project = new TestProject(); 237 | 238 | // ACT 239 | const struct = new ProjenStruct(project, { 240 | name: 'MyInterface', 241 | }); 242 | struct 243 | .mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')) 244 | .omit('projenCodeDir', 'filename'); 245 | 246 | // PREPARE 247 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 248 | 249 | // ASSERT 250 | expect(renderedFile).not.toContain('projenCodeDir'); 251 | expect(renderedFile).not.toContain('filename'); 252 | }); 253 | 254 | test('can keep only some props', () => { 255 | // ARRANGE 256 | const project = new TestProject(); 257 | 258 | // ACT 259 | const struct = new ProjenStruct(project, { 260 | name: 'MyInterface', 261 | }); 262 | struct 263 | .mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')) 264 | .only('projenCodeDir'); 265 | 266 | // PREPARE 267 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 268 | 269 | // ASSERT 270 | expect(renderedFile).toContain('projenCodeDir'); 271 | expect(renderedFile).not.toContain('filename'); 272 | }); 273 | 274 | test('can ignore deprecated props', () => { 275 | // ARRANGE 276 | const project = new TestProject(); 277 | 278 | // ACT 279 | const struct = new ProjenStruct(project, { 280 | name: 'MyInterface', 281 | }); 282 | struct.add( 283 | { 284 | name: 'currentProp', 285 | type: { primitive: PrimitiveType.Boolean }, 286 | }, 287 | { 288 | name: 'deprecatedProps', 289 | type: { primitive: PrimitiveType.Boolean }, 290 | docs: { deprecated: 'use `currentProp`' }, 291 | }, 292 | ); 293 | struct.withoutDeprecated(); 294 | 295 | // PREPARE 296 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 297 | 298 | // ASSERT 299 | expect(renderedFile).not.toContain('deprecatedProps'); 300 | expect(renderedFile).toContain('currentProp'); 301 | }); 302 | 303 | test('can make all props optional', () => { 304 | // ARRANGE 305 | const project = new TestProject(); 306 | 307 | // ACT 308 | const struct = new ProjenStruct(project, { name: 'MyInterface' }); 309 | struct.add( 310 | { 311 | name: 'optionalProp', 312 | type: { primitive: PrimitiveType.Boolean }, 313 | optional: true, 314 | }, 315 | { 316 | name: 'requiredProp', 317 | type: { primitive: PrimitiveType.Boolean }, 318 | optional: false, 319 | }, 320 | ); 321 | struct.allOptional(); 322 | 323 | // PREPARE 324 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 325 | 326 | // ASSERT 327 | expect(renderedFile).toContain('optionalProp?: boolean'); 328 | expect(renderedFile).toContain('requiredProp?: boolean'); 329 | }); 330 | 331 | test('can overwrite props', () => { 332 | // ARRANGE 333 | const project = new TestProject(); 334 | 335 | // ACT 336 | const struct = new ProjenStruct(project, { 337 | name: 'MyInterface', 338 | }); 339 | struct.mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')).add({ 340 | name: 'projenCodeDir', 341 | type: { 342 | primitive: PrimitiveType.Number, 343 | }, 344 | }); 345 | 346 | // PREPARE 347 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 348 | 349 | // ASSERT 350 | expect(renderedFile).toContain('projenCodeDir: number'); 351 | }); 352 | 353 | test('can update props', () => { 354 | // ARRANGE 355 | const project = new TestProject(); 356 | 357 | // ACT 358 | const struct = new ProjenStruct(project, { 359 | name: 'MyInterface', 360 | }); 361 | struct 362 | .mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')) 363 | .update('projenCodeDir', { 364 | docs: { 365 | summary: 'New summary', 366 | stability: Stability.Stable, 367 | custom: { 368 | pjnew: '"newVal"', 369 | }, 370 | }, 371 | optional: false, 372 | }); 373 | 374 | // PREPARE 375 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 376 | 377 | // ASSERT 378 | expect(renderedFile).toContain('New summary'); 379 | expect(renderedFile).toContain('@stability stable'); 380 | expect(renderedFile).toContain('@pjnew "newVal"'); 381 | expect(renderedFile).toMatchSnapshot(); 382 | }); 383 | 384 | test('can updateAll props', () => { 385 | // ARRANGE 386 | const project = new TestProject(); 387 | 388 | // ACT 389 | const struct = new ProjenStruct(project, { 390 | name: 'MyInterface', 391 | }); 392 | struct.mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')).updateAll({ 393 | docs: { 394 | stability: Stability.Stable, 395 | }, 396 | }); 397 | 398 | // PREPARE 399 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 400 | 401 | // ASSERT 402 | expect(renderedFile).toContain('@stability stable'); 403 | expect(renderedFile).toMatchSnapshot(); 404 | }); 405 | 406 | test('can rename a prop', () => { 407 | // ARRANGE 408 | const project = new TestProject(); 409 | 410 | // ACT 411 | const struct = new ProjenStruct(project, { name: 'MyInterface' }); 412 | struct.add( 413 | { 414 | name: 'oldProp', 415 | type: { primitive: PrimitiveType.Boolean }, 416 | optional: true, 417 | }, 418 | ); 419 | struct.rename('oldProp', 'newProp'); 420 | 421 | // PREPARE 422 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 423 | 424 | // ASSERT 425 | expect(renderedFile).not.toContain('oldProp'); 426 | expect(renderedFile).toContain('newProp?: boolean'); 427 | expect(renderedFile).toMatchSnapshot(); 428 | }); 429 | 430 | test('can updateEvery property', () => { 431 | // ARRANGE 432 | const project = new TestProject(); 433 | const spec = { 434 | type: { primitive: PrimitiveType.Boolean }, 435 | optional: false, 436 | docs: { 437 | summary: 'This property is required', 438 | default: 'true', 439 | }, 440 | }; 441 | 442 | // ACT 443 | const struct = new ProjenStruct(project, { name: 'MyInterface' }); 444 | struct.add( 445 | { name: 'propOne', ...spec }, 446 | { name: 'propTwo', ...spec }, 447 | { name: 'propThree', ...spec }, 448 | ).updateEvery((property) => { 449 | if (!property.optional) { 450 | return { 451 | docs: { 452 | default: undefined, 453 | }, 454 | }; 455 | } 456 | return {}; 457 | }); 458 | 459 | // PREPARE 460 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 461 | 462 | // ASSERT 463 | expect(renderedFile).not.toContain('@default true'); 464 | expect(renderedFile).toMatchSnapshot(); 465 | }); 466 | 467 | test('can replace a prop', () => { 468 | // ARRANGE 469 | const project = new TestProject(); 470 | 471 | // ACT 472 | const struct = new ProjenStruct(project, { name: 'MyInterface' }); 473 | struct.add( 474 | { 475 | name: 'oldProp', 476 | type: { primitive: PrimitiveType.Boolean }, 477 | optional: true, 478 | }, 479 | ); 480 | struct.replace('oldProp', { 481 | name: 'newProp', 482 | type: { primitive: PrimitiveType.Number }, 483 | optional: false, 484 | }); 485 | 486 | // PREPARE 487 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 488 | 489 | // ASSERT 490 | expect(renderedFile).not.toContain('oldProp'); 491 | expect(renderedFile).toContain('newProp: number'); 492 | expect(renderedFile).toMatchSnapshot(); 493 | }); 494 | 495 | test('can map properties', () => { 496 | // ARRANGE 497 | const project = new TestProject(); 498 | const spec = { 499 | type: { primitive: PrimitiveType.Boolean }, 500 | optional: false, 501 | docs: { 502 | summary: 'This property is required', 503 | default: 'true', 504 | }, 505 | }; 506 | 507 | // ACT 508 | const struct = new ProjenStruct(project, { name: 'MyInterface' }); 509 | struct.add( 510 | { name: 'propOne', ...spec }, 511 | { name: 'propTwo', ...spec }, 512 | { name: 'propThree', ...spec }, 513 | { name: 'propFour', ...spec }, 514 | ).map((property) => { 515 | switch (property.name) { 516 | case 'propOne': 517 | return { 518 | name: 'aDate', 519 | type: { primitive: PrimitiveType.Date }, 520 | }; 521 | case 'propTwo': 522 | return { 523 | name: 'aString', 524 | type: { primitive: PrimitiveType.String }, 525 | }; 526 | case 'propThree': 527 | return { 528 | name: 'aNumber', 529 | type: { primitive: PrimitiveType.Number }, 530 | }; 531 | default: 532 | return property; 533 | } 534 | }); 535 | 536 | // PREPARE 537 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 538 | 539 | // ASSERT 540 | expect(renderedFile).not.toContain('propOne'); 541 | expect(renderedFile).toContain('aDate'); 542 | expect(renderedFile).not.toContain('propTwo'); 543 | expect(renderedFile).toContain('aString'); 544 | expect(renderedFile).not.toContain('propThree'); 545 | expect(renderedFile).toContain('aNumber'); 546 | expect(renderedFile).toContain('propFour'); 547 | expect(renderedFile).toMatchSnapshot(); 548 | }); 549 | 550 | 551 | test('can import type from the same package at the top level', () => { 552 | // ARRANGE 553 | const project = new TestProject(); 554 | 555 | // ACT 556 | const struct = new ProjenStruct(project, { 557 | name: 'MyInterface', 558 | fqn: 'test.MyInterface', 559 | }); 560 | struct.add( 561 | { 562 | name: 'localProp', 563 | type: { 564 | fqn: 'test.OtherInterface', 565 | }, 566 | }, 567 | { 568 | name: 'localNestedProp', 569 | type: { 570 | fqn: 'test.more.OtherInterface', 571 | }, 572 | }, 573 | ); 574 | 575 | // PREPARE 576 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 577 | 578 | // ASSERT 579 | expect(renderedFile).toContain("import type { more, OtherInterface } from './';"); 580 | }); 581 | 582 | test('can import type from the same package when nested', () => { 583 | // ARRANGE 584 | const project = new TestProject(); 585 | 586 | // ACT 587 | const struct = new ProjenStruct(project, { 588 | name: 'MyInterface', 589 | fqn: 'test.a.b.c.MyInterface', 590 | }); 591 | struct.add( 592 | { 593 | name: 'localProp', 594 | type: { 595 | fqn: 'test.OtherInterface', 596 | }, 597 | }, 598 | { 599 | name: 'localNestedProp', 600 | type: { 601 | fqn: 'test.more.OtherInterface', 602 | }, 603 | }, 604 | ); 605 | 606 | // PREPARE 607 | const renderedFile = synthSnapshot(project)['src/a/b/c/MyInterface.ts']; 608 | 609 | // ASSERT 610 | expect(renderedFile).toContain( 611 | "import type { more, OtherInterface } from '../../../';", 612 | ); 613 | }); 614 | 615 | test("can import type from the same package when in a location that's not matching fqn levels", () => { 616 | // ARRANGE 617 | const project = new TestProject(); 618 | 619 | // ACT 620 | const struct = new ProjenStruct(project, { 621 | name: 'MyInterface', 622 | fqn: 'test.one.two.three.MyInterface', 623 | filePath: join(project.srcdir, 'sub', 'MyInterface.ts'), 624 | }); 625 | struct.add( 626 | { 627 | name: 'localProp', 628 | type: { 629 | fqn: 'test.OtherInterface', 630 | }, 631 | }, 632 | { 633 | name: 'localNestedProp', 634 | type: { 635 | fqn: 'test.more.OtherInterface', 636 | }, 637 | }, 638 | ); 639 | 640 | // PREPARE 641 | const renderedFile = synthSnapshot(project)['src/sub/MyInterface.ts']; 642 | 643 | // ASSERT 644 | expect(renderedFile).toContain("import type { more, OtherInterface } from '../';"); 645 | }); 646 | 647 | test('can import type from an other package', () => { 648 | // ARRANGE 649 | const project = new TestProject(); 650 | 651 | // ACT 652 | const struct = new ProjenStruct(project, { 653 | name: 'MyInterface', 654 | fqn: 'test.MyInterface', 655 | }); 656 | 657 | struct.mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')).add({ 658 | name: 'testProp', 659 | type: { 660 | fqn: 'projen.typescript.TypeScriptProjectOptions', 661 | }, 662 | }); 663 | 664 | // PREPARE 665 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 666 | 667 | // ASSERT 668 | expect(renderedFile).toContain("import type { typescript } from 'projen';"); 669 | }); 670 | 671 | test('can override import locations', () => { 672 | // ARRANGE 673 | const project = new TestProject(); 674 | 675 | // ACT 676 | const struct = new ProjenStruct(project, { 677 | name: 'MyInterface', 678 | fqn: 'test.MyInterface', 679 | importLocations: { 680 | foo: 'bar', 681 | projen: 'banana', 682 | }, 683 | }); 684 | 685 | struct.mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')).add( 686 | { 687 | name: 'localProp', 688 | type: { 689 | fqn: 'foo.OtherInterface', 690 | }, 691 | }, 692 | { 693 | name: 'localNestedProp', 694 | type: { 695 | fqn: 'foo.more.OtherInterface', 696 | }, 697 | }, 698 | { 699 | name: 'externalProp', 700 | type: { 701 | fqn: 'projen.typescript.TypeScriptProjectOptions', 702 | }, 703 | }, 704 | ); 705 | 706 | // PREPARE 707 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 708 | 709 | // ASSERT 710 | expect(renderedFile).toContain("import type { more, OtherInterface } from 'bar';"); 711 | expect(renderedFile).toContain("import type { typescript } from 'banana';"); 712 | }); 713 | 714 | test('can use struct as type in add', () => { 715 | // ARRANGE 716 | const project = new TestProject(); 717 | 718 | // ACT 719 | const base = new ProjenStruct(project, { 720 | name: 'MyBaseInterface', 721 | }); 722 | 723 | const nestedStruct = new ProjenStruct(project, { 724 | name: 'MyInterface', 725 | }); 726 | nestedStruct 727 | .mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')) 728 | .update('filename', { docs: { default: '.projenRC.ts' } }); 729 | 730 | base.add({ 731 | name: 'projenrcTsOptions', 732 | type: nestedStruct, 733 | docs: { 734 | summary: 'My new Summary', 735 | }, 736 | }); 737 | 738 | // PREPARE 739 | const synthSnapshotOutput = synthSnapshot(project); 740 | const renderedNestedFile = synthSnapshotOutput['src/MyInterface.ts']; 741 | const renderedBaseFile = synthSnapshotOutput['src/MyBaseInterface.ts']; 742 | 743 | // ASSERT 744 | expect(renderedNestedFile).toMatchSnapshot(); 745 | expect(renderedNestedFile).toContain('@default .projenRC.ts'); 746 | expect(renderedBaseFile).toMatchSnapshot(); 747 | expect(renderedBaseFile).toContain("import type { MyInterface } from './'"); 748 | expect(renderedBaseFile).toContain( 749 | 'readonly projenrcTsOptions: MyInterface;', 750 | ); 751 | }); 752 | 753 | test('can use struct as type in update', () => { 754 | // ARRANGE 755 | const project = new TestProject(); 756 | 757 | // ACT 758 | const base = new ProjenStruct(project, { 759 | name: 'MyBaseInterface', 760 | }); 761 | 762 | const nestedStruct = new ProjenStruct(project, { 763 | name: 'MyInterface', 764 | }); 765 | nestedStruct 766 | .mixin(Struct.fromFqn('projen.typescript.ProjenrcOptions')) 767 | .update('filename', { docs: { default: '.projenRC.ts' } }); 768 | 769 | base 770 | .mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')) 771 | .update('projenrcTsOptions', { 772 | type: nestedStruct, 773 | docs: { 774 | summary: 'My new Summary', 775 | }, 776 | }); 777 | 778 | // PREPARE 779 | const synthSnapshotOutput = synthSnapshot(project); 780 | const renderedNestedFile = synthSnapshotOutput['src/MyInterface.ts']; 781 | const renderedBaseFile = synthSnapshotOutput['src/MyBaseInterface.ts']; 782 | 783 | // ASSERT 784 | expect(renderedNestedFile).toContain('@default .projenRC.ts'); 785 | expect(renderedBaseFile).toContain("import type { MyInterface } from './'"); 786 | expect(renderedBaseFile).toContain( 787 | 'readonly projenrcTsOptions?: MyInterface', 788 | ); 789 | }); 790 | 791 | test('can create a struct from empty', () => { 792 | // ARRANGE 793 | const project = new TestProject(); 794 | 795 | // ACT 796 | const base = new ProjenStruct(project, { 797 | name: 'MyInterface', 798 | }); 799 | const addedStruct = Struct.empty(); 800 | addedStruct.add({ 801 | name: 'emptyProp', 802 | type: { primitive: PrimitiveType.Boolean }, 803 | }); 804 | base.mixin(addedStruct); 805 | 806 | // PREPARE 807 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 808 | 809 | // ASSERT 810 | expect(renderedFile).toContain('emptyProp'); 811 | }); 812 | 813 | test('can use an empty struct as type with name', () => { 814 | // ARRANGE 815 | const project = new TestProject(); 816 | 817 | // ACT 818 | const nestedStruct = Struct.empty('pkg.sub.MyOtherInterface'); 819 | nestedStruct.add({ 820 | name: 'emptyProp', 821 | type: { primitive: PrimitiveType.Boolean }, 822 | }); 823 | 824 | const base = new ProjenStruct(project, { 825 | name: 'MyInterface', 826 | }); 827 | base.add({ 828 | name: 'nestedProp', 829 | type: nestedStruct, 830 | }); 831 | 832 | // PREPARE 833 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 834 | 835 | // ASSERT 836 | expect(renderedFile).toContain("import type { sub } from 'pkg';"); 837 | expect(renderedFile).toContain('readonly nestedProp: sub.MyOtherInterface'); 838 | }); 839 | 840 | test('can set renderer options', () => { 841 | // ARRANGE 842 | const project = new TestProject(); 843 | 844 | // ACT 845 | const struct = new ProjenStruct(project, { 846 | name: 'MyInterface', 847 | outputFileOptions: { 848 | useTypeImports: true, 849 | indent: 10, 850 | }, 851 | }); 852 | struct.mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')); 853 | 854 | // PREPARE 855 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 856 | 857 | // ASSERT 858 | expect(renderedFile).toContain('import type'); 859 | expect(renderedFile).toContain(' '.repeat(10)); 860 | }); 861 | 862 | test('can set tsdoc with just docs', () => { 863 | // ARRANGE 864 | const project = new TestProject(); 865 | 866 | // ACT 867 | const struct = new ProjenStruct(project, { 868 | name: 'MyInterface', 869 | docs: { 870 | summary: 'This is a summary', 871 | remarks: 'This is a full description', 872 | stability: Stability.Stable, 873 | custom: { 874 | internal: 'true', 875 | }, 876 | }, 877 | }); 878 | struct.mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')); 879 | 880 | // PREPARE 881 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 882 | 883 | // ASSERT 884 | expect(renderedFile).toContain('This is a summary'); 885 | expect(renderedFile).toContain('This is a full description'); 886 | expect(renderedFile).toContain('@stability stable'); 887 | expect(renderedFile).toContain('@internal true'); 888 | }); 889 | 890 | test('can set tsdoc with just description', () => { 891 | // ARRANGE 892 | const project = new TestProject(); 893 | 894 | // ACT 895 | const struct = new ProjenStruct(project, { 896 | name: 'MyInterface', 897 | description: 'This is a summary', 898 | }); 899 | struct.mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')); 900 | 901 | // PREPARE 902 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 903 | 904 | // ASSERT 905 | expect(renderedFile).toContain('This is a summary'); 906 | }); 907 | 908 | test('description is not used if docs are provided', () => { 909 | // ARRANGE 910 | const project = new TestProject(); 911 | 912 | // ACT 913 | const struct = new ProjenStruct(project, { 914 | name: 'MyInterface', 915 | description: 'This is a description that should not be used', 916 | docs: { 917 | summary: 'This is a summary that should be used', 918 | remarks: 'This is a full description', 919 | }, 920 | }); 921 | struct.mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')); 922 | 923 | // PREPARE 924 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 925 | 926 | // ASSERT 927 | expect(renderedFile).toContain('This is a summary that should be used'); 928 | expect(renderedFile).not.toContain('This is a description that should not be used'); 929 | expect(renderedFile).toContain('This is a full description'); 930 | }); 931 | 932 | test('name is used as description if no docs or description are provided', () => { 933 | // ARRANGE 934 | const project = new TestProject(); 935 | 936 | // ACT 937 | const struct = new ProjenStruct(project, { 938 | name: 'MyInterface', 939 | }); 940 | struct.mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions')); 941 | 942 | // PREPARE 943 | const renderedFile = synthSnapshot(project)['src/MyInterface.ts']; 944 | 945 | // ASSERT 946 | // we don't want a false positive from the actual struct definition, so check for the comment format 947 | expect(renderedFile).toContain(' * MyInterface'); 948 | }); 949 | -------------------------------------------------------------------------------- /test/projen/ts-interface.test.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceType, TypeKind } from '@jsii/spec'; 2 | import { Project } from 'projen'; 3 | import { synthSnapshot } from 'projen/lib/util/synth'; 4 | import { TypeScriptInterfaceFile } from '../../src'; 5 | import { findInterface } from '../../src/private'; 6 | 7 | test('can render a struct', () => { 8 | // Arrange 9 | const project = new TestProject(); 10 | const spec = findInterface('projen.typescript.TypeScriptProjectOptions'); 11 | 12 | // ACT 13 | new TypeScriptInterfaceFile(project, 'interface.ts', spec); 14 | const renderedFile = synthSnapshot(project)['interface.ts']; 15 | 16 | // ASSERT 17 | expect(renderedFile).toMatchSnapshot(); 18 | }); 19 | 20 | test('will include properties from all parents', () => { 21 | // Arrange 22 | const project = new TestProject(); 23 | const spec = findInterface('projen.typescript.TypeScriptProjectOptions'); 24 | 25 | // ACT 26 | new TypeScriptInterfaceFile(project, 'interface.ts', spec); 27 | const renderedFile = synthSnapshot(project)['interface.ts']; 28 | 29 | // ASSERT 30 | expect(renderedFile).toContain('readonly parent?: Project;'); 31 | }); 32 | 33 | test('can import from an external package', () => { 34 | // Arrange 35 | const project = new TestProject(); 36 | const spec = findInterface( 37 | 'projen.typescript.TypeScriptProjectOptions', 38 | false, 39 | ); 40 | 41 | // ACT 42 | spec.fqn = 'mypackage.Interface'; 43 | new TypeScriptInterfaceFile(project, 'interface.ts', spec); 44 | const renderedFile = synthSnapshot(project)['interface.ts']; 45 | 46 | // ASSERT 47 | expect(renderedFile).toContain( 48 | "import type { javascript, typescript } from 'projen';", 49 | ); 50 | }); 51 | 52 | test('can import from the same package', () => { 53 | // Arrange 54 | const project = new TestProject(); 55 | const spec: InterfaceType = { 56 | kind: TypeKind.Interface, 57 | assembly: 'test', 58 | fqn: 'test.sub.MyInterface', 59 | name: 'MyInterface', 60 | docs: { summary: 'MyInterface' }, 61 | properties: [ 62 | { name: 'localNestedProp', type: { fqn: 'test.more.OtherInterface' } }, 63 | { name: 'localProp', type: { fqn: 'test.OtherInterface' } }, 64 | ], 65 | }; 66 | 67 | // ACT 68 | new TypeScriptInterfaceFile(project, 'interface.ts', spec); 69 | const renderedFile = synthSnapshot(project)['interface.ts']; 70 | 71 | // ASSERT 72 | expect(renderedFile).toContain("import type { more, OtherInterface } from '../';"); 73 | }); 74 | 75 | test('can override package imports', () => { 76 | // Arrange 77 | const project = new TestProject(); 78 | const spec = findInterface( 79 | 'projen.typescript.TypeScriptProjectOptions', 80 | false, 81 | ); 82 | 83 | // ACT 84 | new TypeScriptInterfaceFile(project, 'interface.ts', spec, { 85 | importLocations: { 86 | projen: 'banana', 87 | }, 88 | }); 89 | const renderedFile = synthSnapshot(project)['interface.ts']; 90 | 91 | // ASSERT 92 | expect(renderedFile).toContain( 93 | "import type { javascript, typescript } from 'banana';", 94 | ); 95 | }); 96 | 97 | test('can use explicit type imports', () => { 98 | // Arrange 99 | const project = new TestProject(); 100 | const spec = findInterface( 101 | 'projen.typescript.TypeScriptProjectOptions', 102 | false, 103 | ); 104 | 105 | // ACT 106 | new TypeScriptInterfaceFile(project, 'interface.ts', spec, { 107 | useTypeImports: true, 108 | }); 109 | const renderedFile = synthSnapshot(project)['interface.ts']; 110 | 111 | // ASSERT 112 | expect(renderedFile).toContain( 113 | "import type { javascript, typescript } from '../';", 114 | ); 115 | }); 116 | 117 | class TestProject extends Project { 118 | public constructor() { 119 | super({ 120 | name: 'test', 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /test/renderer/__snapshots__/typescript.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`can use struct manipulation to render required properties without a @default doctag 1`] = ` 4 | " 5 | export interface MyFunctionProps { 6 | readonly requiredProp: string; 7 | } 8 | " 9 | `; 10 | 11 | exports[`required properties do render @default by default 1`] = ` 12 | " 13 | export interface MyFunctionProps { 14 | /** 15 | * @default "foobar" 16 | */ 17 | readonly requiredProp: string; 18 | } 19 | " 20 | `; 21 | -------------------------------------------------------------------------------- /test/renderer/typescript.test.ts: -------------------------------------------------------------------------------- 1 | import { PrimitiveType } from '@jsii/spec'; 2 | import { Struct, TypeScriptRenderer } from '../../src'; 3 | 4 | test('required properties do render @default by default', () => { 5 | // ARRANGE 6 | const renderer = new TypeScriptRenderer(); 7 | const struct = Struct.empty('@my-scope/my-pkg.MyFunctionProps'); 8 | struct.add({ 9 | name: 'requiredProp', 10 | type: { primitive: PrimitiveType.String }, 11 | optional: false, 12 | docs: { 13 | default: '"foobar"', 14 | }, 15 | }); 16 | 17 | // ACT 18 | const renderedFile = renderer.renderStruct(struct); 19 | 20 | // ASSERT 21 | expect(renderedFile).toMatchSnapshot(); 22 | expect(renderedFile).toContain('@default'); 23 | expect(renderedFile).toContain('foobar'); 24 | }); 25 | 26 | 27 | test('can use struct manipulation to render required properties without a @default doctag', () => { 28 | // ARRANGE 29 | const renderer = new TypeScriptRenderer(); 30 | const struct = Struct.empty('@my-scope/my-pkg.MyFunctionProps'); 31 | struct.add({ 32 | name: 'requiredProp', 33 | type: { primitive: PrimitiveType.String }, 34 | optional: false, 35 | docs: { 36 | default: '"foobar"', 37 | }, 38 | }); 39 | 40 | // ACT 41 | struct.map(property => { 42 | if (!property.optional) { 43 | delete property.docs?.default; 44 | } 45 | return property; 46 | }); 47 | const renderedFile = renderer.renderStruct(struct); 48 | 49 | // ASSERT 50 | expect(renderedFile).toMatchSnapshot(); 51 | expect(renderedFile).not.toContain('@default'); 52 | expect(renderedFile).not.toContain('foobar'); 53 | }); 54 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2022" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2020" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "es2022" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2020" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [] 34 | } 35 | --------------------------------------------------------------------------------