├── .github └── workflows │ ├── test-dummy_project.yml │ ├── test-no-params.yml │ ├── test-with-params.yml │ └── test.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── action.yml ├── dist ├── index.js └── licenses.txt ├── dummy_project ├── .tsgrc.json ├── data.json ├── src │ ├── config.ts │ ├── includeFiles │ │ ├── D.vue │ │ ├── a.ts │ │ ├── abstractions │ │ │ ├── children │ │ │ │ └── childA.ts │ │ │ ├── j.ts │ │ │ ├── k.ts │ │ │ └── l.ts │ │ ├── b.ts │ │ ├── c.ts │ │ ├── children │ │ │ └── childA.ts │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── d │ │ │ ├── d.ts │ │ │ └── index.ts │ │ └── excludeFiles │ │ │ ├── children │ │ │ └── childA.ts │ │ │ ├── class │ │ │ └── classA.ts │ │ │ ├── g.ts │ │ │ ├── h.ts │ │ │ ├── i.ts │ │ │ └── style │ │ │ └── style.ts │ ├── main.ts │ ├── otherFiles │ │ ├── children │ │ │ ├── :id.json │ │ │ ├── childA.ts │ │ │ └── {id}.json │ │ ├── d.ts │ │ ├── e.ts │ │ └── f.ts │ └── utils.ts └── tsconfig-dummy.json ├── eslint.config.mjs ├── img └── top-sample.png ├── jest.config.js ├── package-lock.json ├── package.json ├── renovate.json ├── scripts └── update-workflowfiles.mjs ├── src ├── README.md ├── getFullGraph.ts ├── graph │ ├── __snapshots__ │ │ ├── build2GraphsMessage.test.ts.snap │ │ └── buildGraphMessage.test.ts.snap │ ├── addStatus.ts │ ├── applyMutualDifferences.ts │ ├── build2GraphsMessage.test.ts │ ├── build2GraphsMessage.ts │ ├── buildGraphMessage.test.ts │ ├── buildGraphMessage.ts │ ├── createIncludeList.test.ts │ ├── createIncludeList.ts │ ├── extractAbstractionTarget.test.ts │ ├── extractAbstractionTarget.ts │ ├── extractAbstractionTargetFromGraphs.test.ts │ ├── extractAbstractionTargetFromGraphs.ts │ ├── extractIndexFileDependencies │ │ ├── extractIndexFileDependencies.test.ts │ │ ├── extractIndexFileDependencies.ts │ │ └── index.ts │ ├── extractNoAbstractionDirs.test.ts │ ├── extractNoAbstractionDirs.ts │ ├── mergeGraphsWithDifferences.ts │ ├── updateRelationsStatus.test.ts │ └── updateRelationsStatus.ts ├── index.ts ├── metrics │ ├── buildMetricsMessage.ts │ ├── createScoreDiff.test.ts │ ├── createScoreDiff.ts │ ├── formatAndOutputMetrics.ts │ └── round.ts ├── reset.d.ts ├── tsg │ ├── createTsgCommand.test.ts │ ├── createTsgCommand.ts │ ├── getCreateGraphsArguments.test.ts │ └── getCreateGraphsArguments.ts └── utils │ ├── config.ts │ ├── context.ts │ ├── createCommentTitle.ts │ ├── dummyContext.ts │ ├── github.ts │ ├── log.ts │ ├── reducer.ts │ ├── retry.test.ts │ └── retry.ts └── tsconfig.json /.github/workflows/test-dummy_project.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | # Sets permissions of the GITHUB_TOKEN to allow write pull-requests 4 | permissions: 5 | pull-requests: write 6 | 7 | jobs: 8 | delta-typescript-graph-job: 9 | runs-on: ubuntu-latest 10 | name: Test dummy project 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 14 | - name: Delta Typescript Graph 15 | id: tsg 16 | uses: ysk8hori/delta-typescript-graph-action@44b8652261a2d95e733bdc7068bde410ab7df09f 17 | with: 18 | access-token: ${{ secrets.GITHUB_TOKEN }} 19 | tsconfig: './dummy_project/tsconfig-dummy.json' 20 | max-size: 100 21 | orientation: LR 22 | debug: true 23 | include-index-file-dependencies: true 24 | comment-title: 'Dummy project dependency graph' 25 | show-metrics: true 26 | -------------------------------------------------------------------------------- /.github/workflows/test-no-params.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | # Sets permissions of the GITHUB_TOKEN to allow write pull-requests 4 | permissions: 5 | pull-requests: write 6 | 7 | jobs: 8 | delta-typescript-graph-job: 9 | runs-on: ubuntu-latest 10 | name: Test no params 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 14 | - uses: ysk8hori/delta-typescript-graph-action@44b8652261a2d95e733bdc7068bde410ab7df09f 15 | with: 16 | max-size: 50 17 | -------------------------------------------------------------------------------- /.github/workflows/test-with-params.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | # Sets permissions of the GITHUB_TOKEN to allow write pull-requests 4 | permissions: 5 | pull-requests: write 6 | 7 | jobs: 8 | delta-typescript-graph-job: 9 | runs-on: ubuntu-latest 10 | name: Test with params 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 14 | - name: Delta Typescript Graph 15 | id: tsg 16 | uses: ysk8hori/delta-typescript-graph-action@44b8652261a2d95e733bdc7068bde410ab7df09f 17 | with: 18 | access-token: ${{ secrets.GITHUB_TOKEN }} 19 | tsconfig-root: './' 20 | max-size: 50 21 | orientation: LR 22 | debug: true 23 | in-details: true 24 | exclude: 'test' 25 | include-index-file-dependencies: true 26 | comment-title: 'Delta TSG with params' 27 | show-metrics: true 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [20.x] 13 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | cache: 'npm' 22 | - run: npm ci 23 | - run: npm run type-check 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # vuepress v2.x temp and cache directory 103 | .temp 104 | .cache 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # TernJS port file 119 | .tern-port 120 | 121 | # Stores VSCode versions used for testing VSCode extensions 122 | .vscode-test 123 | 124 | # yarn v2 125 | .yarn/cache 126 | .yarn/unplugged 127 | .yarn/build-state.yml 128 | .yarn/install-state.gz 129 | .pnp.* 130 | 131 | typescript-graph.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "endOfLine": "auto", 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ホリちゃん 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delta TypeScript Graph Action 2 | 3 | This GitHub Action uses Mermaid to visualize in a diagram the files that were changed in a Pull Request and their related dependency files. This approach aims to reduce the initial cognitive load during the review process and assist in understanding the structure around the modified code. 4 | 5 | ![sample](img/top-sample.png) 6 | 7 | ## Sample Usage 8 | 9 | ### Basic File Modifications 10 | 11 | In this example, we show the dependency graph when you've modified `outputGraph.ts` and its related test files. The modified files are highlighted in yellow, and the files they depend on are also explicitly displayed on the graph. 12 | 13 | ```mermaid 14 | flowchart 15 | classDef modified fill:yellow,stroke:#999,color:black 16 | subgraph src["src"] 17 | src/utils["/utils"]:::dir 18 | src/index.ts["index.ts"] 19 | subgraph src/outputGraph["/outputGraph"] 20 | src/outputGraph/outputGraph.ts["outputGraph.ts"]:::modified 21 | src/outputGraph/output2Graphs.test.ts["output2Graphs.test.ts"]:::modified 22 | src/outputGraph/mergeGraphsWithDifferences.ts["mergeGraphsWithDifferences.ts"] 23 | src/outputGraph/applyMutualDifferences.ts["applyMutualDifferences.ts"] 24 | end 25 | end 26 | src/outputGraph/outputGraph.ts-->src/utils 27 | src/outputGraph/outputGraph.ts-->src/outputGraph/mergeGraphsWithDifferences.ts 28 | src/outputGraph/outputGraph.ts-->src/outputGraph/applyMutualDifferences.ts 29 | src/index.ts-->src/outputGraph/outputGraph.ts 30 | src/outputGraph/output2Graphs.test.ts-->src/outputGraph/outputGraph.ts 31 | src/outputGraph/mergeGraphsWithDifferences.ts-->src/utils 32 | src/outputGraph/applyMutualDifferences.ts-->src/utils 33 | src/index.ts-->src/utils 34 | ``` 35 | 36 | ### Changes Involving File Deletion or Movement 37 | 38 | This case demonstrates the impact when a file is deleted or moved. Dependency graphs are generated for both the base branch and the head branch. Deleted files are displayed in a grayed-out manner. 39 | 40 | #### Base Branch 41 | 42 | ```mermaid 43 | flowchart 44 | classDef modified fill:yellow,stroke:#999,color:black 45 | classDef deleted fill:dimgray,stroke:#999,color:black,stroke-dasharray: 4 4,stroke-width:2px; 46 | subgraph src["src"] 47 | src/index.ts["index.ts"]:::modified 48 | src/index.test.ts["index.test.ts"] 49 | src/getRenameFiles.ts["getRenameFiles.ts"] 50 | src/getFullGraph.ts["getFullGraph.ts"] 51 | subgraph src/graph_["/graph"] 52 | src/_graph__/index.ts["index.ts"]:::deleted 53 | src/_graph__/outputGraph.ts["outputGraph.ts"] 54 | src/_graph__/output2Graphs.ts["output2Graphs.ts"] 55 | end 56 | end 57 | src/_graph__/index.ts-->src/_graph__/outputGraph.ts 58 | src/_graph__/index.ts-->src/_graph__/output2Graphs.ts 59 | src/index.ts-->src/getRenameFiles.ts 60 | src/index.ts-->src/getFullGraph.ts 61 | src/index.ts-->src/_graph__/index.ts 62 | src/index.test.ts-->src/index.ts 63 | ``` 64 | 65 | #### Head Branch 66 | 67 | ```mermaid 68 | flowchart 69 | classDef modified fill:yellow,stroke:#999,color:black 70 | subgraph src["src"] 71 | src/index.ts["index.ts"]:::modified 72 | src/index.test.ts["index.test.ts"] 73 | src/getRenameFiles.ts["getRenameFiles.ts"] 74 | src/getFullGraph.ts["getFullGraph.ts"] 75 | subgraph src/graph_["/graph"] 76 | src/_graph__/output2Graphs.ts["output2Graphs.ts"] 77 | src/_graph__/outputGraph.ts["outputGraph.ts"] 78 | end 79 | end 80 | src/index.ts-->src/getRenameFiles.ts 81 | src/index.ts-->src/getFullGraph.ts 82 | src/index.ts-->src/_graph__/output2Graphs.ts 83 | src/index.ts-->src/_graph__/outputGraph.ts 84 | src/index.test.ts-->src/index.ts 85 | ``` 86 | 87 | ## Getting Started 88 | 89 | To quickly integrate this Action into your workflow, you can use the following minimal YAML configuration. This setup is sufficient to start using the Action with its default settings on pull request events. 90 | 91 | ```yml 92 | on: pull_request 93 | 94 | # Sets permissions of the GITHUB_TOKEN to allow write pull-requests 95 | permissions: 96 | pull-requests: write 97 | 98 | jobs: 99 | delta-typescript-graph-job: 100 | runs-on: ubuntu-latest 101 | name: Delta TypeScript Graph 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # specify latest version 105 | - uses: ysk8hori/delta-typescript-graph-action@v # specify latest version 106 | ``` 107 | 108 | This basic setup will trigger the Action on every pull request. The Action will run on the latest Ubuntu runner and use its default settings. If you want to customize the Action, you can add parameters under the `with` section of the workflow file. 109 | 110 | ## Configuration 111 | 112 | This Action provides several parameters to customize its behavior. You can specify these parameters in your GitHub Actions workflow file. 113 | 114 | | Parameter | Type | Default Value | Description | 115 | | --------------------------------- | ------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | 116 | | `access-token` | `string` | `${{ github.token }}` | Access token for the repo. | 117 | | `tsconfig-root` | `string` | `'./'` | **Deprecated**: Specifies the root directory where tsconfig will be searched. Ignored if `tsconfig` is specified. | 118 | | `tsconfig` | `string` | | Relative path from the codebase root to the tsconfig file to be used for TypeScript Graph analysis. | 119 | | `max-size` | `number` | `30` | Limits the number of nodes to display in the graph when there are many changed files. | 120 | | `orientation` | `TB` or `LR` | `'TB'` | Specifies the orientation (`TB` or `LR`) of the graph. Note: Mermaid may produce graphs in the opposite direction. | 121 | | `debug` | `boolean` | `false` | Enables debug mode. Logs will be output in debug mode. | 122 | | `in-details` | `boolean` | `false` | Specifies whether to enclose Mermaid in a `
` tag for collapsing. | 123 | | `exclude` | `string` | `'node_modules, test'` | Specifies a comma-separated list of files to exclude from the graph. | 124 | | `include-index-file-dependencies` | `boolean` | `false` | Determines whether to display dependency files when the changed file is referenced from an index.ts in the same directory. | 125 | | `comment-title` | `string` | `Delta TypeScript Graph` | Specifies the title of the comment posted on the PR. Useful for distinguishing analyses in monorepos or multiple CI runs. | 126 | | `show-metrics` | `boolean` | `false` | Specifies whether to calculate and display metrics for the graph. | 127 | 128 | To use these parameters, include them under the `with` section of your workflow file when using this Action. For example: 129 | 130 | ```yml 131 | steps: 132 | - uses: ysk8hori/delta-typescript-graph-action@v # specify latest version 133 | with: 134 | access-token: ${{ secrets.GITHUB_TOKEN }} 135 | tsconfig: './my-app/tsconfig.json' 136 | max-size: 20 137 | orientation: 'LR' 138 | debug: true 139 | in-details: true 140 | exclude: 'node_modules, test' 141 | include-index-file-dependencies: true 142 | show-metrics: true 143 | ``` 144 | 145 | This configuration will set up the Action with the specified parameters, allowing you to customize its behavior according to your project's needs. 146 | 147 | ## About the `tsg` Command 148 | 149 | Using the `tsg` command found in the comments generated by this action, you can achieve results similar to the graphs produced by this action. Modifying the arguments of the `tsg` command may also yield better results. 150 | 151 | For more information about the `tsg` command, please refer to the following repository: 152 | https://github.com/ysk8hori/typescript-graph 153 | 154 | ## About the metrics 155 | 156 | This is a beta feature for measuring code metrics, including the Maintainability Index and Cognitive Complexity, among others. 157 | While these metrics are widely recognized, their accuracy in TypeScript-specific contexts may vary. 158 | Nonetheless, they can serve as helpful indicators for evaluating code quality. 159 | 160 | For more details, please refer to the TypeScript Graph README: 161 | [TypeScript Graph README](https://github.com/ysk8hori/typescript-graph) 162 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Delta TypeScript Graph' 2 | auther: 'ysk8hori' 3 | description: 'Visualizes changes in typescript-file dependencies.' 4 | inputs: 5 | access-token: 6 | description: 'Access token for the repo' 7 | required: false 8 | default: ${{ github.token }} 9 | tsconfig-root: 10 | description: '(Deprecated: Use tsconfig instead.) Specifies the root directory where tsconfig will be searched.' 11 | required: false 12 | default: './' 13 | tsconfig: 14 | description: 'Specifies the path to the tsconfig file.' 15 | required: false 16 | max-size: 17 | description: 'Maximum number of nodes to display in the graph when there are many change files' 18 | required: false 19 | default: '30' 20 | orientation: 21 | description: 'Orientation of the graph (TB or LR)' 22 | required: false 23 | default: 'TB' 24 | debug: 25 | description: 'Enable debug mode' 26 | required: false 27 | default: false 28 | in-details: 29 | description: 'Whether to wrap Mermaid in a `
` tag for collapsing' 30 | required: false 31 | default: false 32 | exclude: 33 | description: 'Array of files to exclude' 34 | required: false 35 | default: 'node_modules' # node_modules をデフォルトの除外リストに追加 36 | include-index-file-dependencies: 37 | description: 'Whether to display dependencies of index.ts files that refer to the change target files' 38 | required: false 39 | default: false 40 | comment-title: 41 | description: 'Specifies the title of the comment posted on the PR.' 42 | required: false 43 | default: 'Delta TypeScript Graph' 44 | show-metrics: 45 | description: 'Calculate and display metrics for the graph' 46 | required: false 47 | default: false 48 | runs: 49 | using: 'node20' 50 | main: 'dist/index.js' 51 | branding: 52 | icon: 'git-pull-request' 53 | color: 'blue' 54 | -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @actions/exec 14 | MIT 15 | The MIT License (MIT) 16 | 17 | Copyright 2019 GitHub 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | @actions/github 26 | MIT 27 | The MIT License (MIT) 28 | 29 | Copyright 2019 GitHub 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | 37 | @actions/http-client 38 | MIT 39 | Actions Http Client for Node.js 40 | 41 | Copyright (c) GitHub, Inc. 42 | 43 | All rights reserved. 44 | 45 | MIT License 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 48 | associated documentation files (the "Software"), to deal in the Software without restriction, 49 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 50 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 51 | subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 56 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 57 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 58 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 59 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 60 | 61 | 62 | @actions/io 63 | MIT 64 | The MIT License (MIT) 65 | 66 | Copyright 2019 GitHub 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 73 | 74 | @fastify/busboy 75 | MIT 76 | Copyright Brian White. All rights reserved. 77 | 78 | Permission is hereby granted, free of charge, to any person obtaining a copy 79 | of this software and associated documentation files (the "Software"), to 80 | deal in the Software without restriction, including without limitation the 81 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 82 | sell copies of the Software, and to permit persons to whom the Software is 83 | furnished to do so, subject to the following conditions: 84 | 85 | The above copyright notice and this permission notice shall be included in 86 | all copies or substantial portions of the Software. 87 | 88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 89 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 90 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 91 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 92 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 94 | IN THE SOFTWARE. 95 | 96 | @octokit/auth-token 97 | MIT 98 | The MIT License 99 | 100 | Copyright (c) 2019 Octokit contributors 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a copy 103 | of this software and associated documentation files (the "Software"), to deal 104 | in the Software without restriction, including without limitation the rights 105 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 106 | copies of the Software, and to permit persons to whom the Software is 107 | furnished to do so, subject to the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be included in 110 | all copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 115 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 116 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 117 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 118 | THE SOFTWARE. 119 | 120 | 121 | @octokit/core 122 | MIT 123 | The MIT License 124 | 125 | Copyright (c) 2019 Octokit contributors 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy 128 | of this software and associated documentation files (the "Software"), to deal 129 | in the Software without restriction, including without limitation the rights 130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 131 | copies of the Software, and to permit persons to whom the Software is 132 | furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in 135 | all copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 143 | THE SOFTWARE. 144 | 145 | 146 | @octokit/endpoint 147 | MIT 148 | The MIT License 149 | 150 | Copyright (c) 2018 Octokit contributors 151 | 152 | Permission is hereby granted, free of charge, to any person obtaining a copy 153 | of this software and associated documentation files (the "Software"), to deal 154 | in the Software without restriction, including without limitation the rights 155 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 156 | copies of the Software, and to permit persons to whom the Software is 157 | furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in 160 | all copies or substantial portions of the Software. 161 | 162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 168 | THE SOFTWARE. 169 | 170 | 171 | @octokit/graphql 172 | MIT 173 | The MIT License 174 | 175 | Copyright (c) 2018 Octokit contributors 176 | 177 | Permission is hereby granted, free of charge, to any person obtaining a copy 178 | of this software and associated documentation files (the "Software"), to deal 179 | in the Software without restriction, including without limitation the rights 180 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 181 | copies of the Software, and to permit persons to whom the Software is 182 | furnished to do so, subject to the following conditions: 183 | 184 | The above copyright notice and this permission notice shall be included in 185 | all copies or substantial portions of the Software. 186 | 187 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 188 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 189 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 190 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 191 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 192 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 193 | THE SOFTWARE. 194 | 195 | 196 | @octokit/plugin-paginate-rest 197 | MIT 198 | MIT License Copyright (c) 2019 Octokit contributors 199 | 200 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 201 | 202 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 203 | 204 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 205 | 206 | 207 | @octokit/plugin-rest-endpoint-methods 208 | MIT 209 | MIT License Copyright (c) 2019 Octokit contributors 210 | 211 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 212 | 213 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 214 | 215 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 216 | 217 | 218 | @octokit/request 219 | MIT 220 | The MIT License 221 | 222 | Copyright (c) 2018 Octokit contributors 223 | 224 | Permission is hereby granted, free of charge, to any person obtaining a copy 225 | of this software and associated documentation files (the "Software"), to deal 226 | in the Software without restriction, including without limitation the rights 227 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 228 | copies of the Software, and to permit persons to whom the Software is 229 | furnished to do so, subject to the following conditions: 230 | 231 | The above copyright notice and this permission notice shall be included in 232 | all copies or substantial portions of the Software. 233 | 234 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 235 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 236 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 237 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 238 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 239 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 240 | THE SOFTWARE. 241 | 242 | 243 | @octokit/request-error 244 | MIT 245 | The MIT License 246 | 247 | Copyright (c) 2019 Octokit contributors 248 | 249 | Permission is hereby granted, free of charge, to any person obtaining a copy 250 | of this software and associated documentation files (the "Software"), to deal 251 | in the Software without restriction, including without limitation the rights 252 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 253 | copies of the Software, and to permit persons to whom the Software is 254 | furnished to do so, subject to the following conditions: 255 | 256 | The above copyright notice and this permission notice shall be included in 257 | all copies or substantial portions of the Software. 258 | 259 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 260 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 261 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 262 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 263 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 264 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 265 | THE SOFTWARE. 266 | 267 | 268 | @ysk8hori/typescript-graph 269 | ISC 270 | 271 | before-after-hook 272 | Apache-2.0 273 | Apache License 274 | Version 2.0, January 2004 275 | http://www.apache.org/licenses/ 276 | 277 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 278 | 279 | 1. Definitions. 280 | 281 | "License" shall mean the terms and conditions for use, reproduction, 282 | and distribution as defined by Sections 1 through 9 of this document. 283 | 284 | "Licensor" shall mean the copyright owner or entity authorized by 285 | the copyright owner that is granting the License. 286 | 287 | "Legal Entity" shall mean the union of the acting entity and all 288 | other entities that control, are controlled by, or are under common 289 | control with that entity. For the purposes of this definition, 290 | "control" means (i) the power, direct or indirect, to cause the 291 | direction or management of such entity, whether by contract or 292 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 293 | outstanding shares, or (iii) beneficial ownership of such entity. 294 | 295 | "You" (or "Your") shall mean an individual or Legal Entity 296 | exercising permissions granted by this License. 297 | 298 | "Source" form shall mean the preferred form for making modifications, 299 | including but not limited to software source code, documentation 300 | source, and configuration files. 301 | 302 | "Object" form shall mean any form resulting from mechanical 303 | transformation or translation of a Source form, including but 304 | not limited to compiled object code, generated documentation, 305 | and conversions to other media types. 306 | 307 | "Work" shall mean the work of authorship, whether in Source or 308 | Object form, made available under the License, as indicated by a 309 | copyright notice that is included in or attached to the work 310 | (an example is provided in the Appendix below). 311 | 312 | "Derivative Works" shall mean any work, whether in Source or Object 313 | form, that is based on (or derived from) the Work and for which the 314 | editorial revisions, annotations, elaborations, or other modifications 315 | represent, as a whole, an original work of authorship. For the purposes 316 | of this License, Derivative Works shall not include works that remain 317 | separable from, or merely link (or bind by name) to the interfaces of, 318 | the Work and Derivative Works thereof. 319 | 320 | "Contribution" shall mean any work of authorship, including 321 | the original version of the Work and any modifications or additions 322 | to that Work or Derivative Works thereof, that is intentionally 323 | submitted to Licensor for inclusion in the Work by the copyright owner 324 | or by an individual or Legal Entity authorized to submit on behalf of 325 | the copyright owner. For the purposes of this definition, "submitted" 326 | means any form of electronic, verbal, or written communication sent 327 | to the Licensor or its representatives, including but not limited to 328 | communication on electronic mailing lists, source code control systems, 329 | and issue tracking systems that are managed by, or on behalf of, the 330 | Licensor for the purpose of discussing and improving the Work, but 331 | excluding communication that is conspicuously marked or otherwise 332 | designated in writing by the copyright owner as "Not a Contribution." 333 | 334 | "Contributor" shall mean Licensor and any individual or Legal Entity 335 | on behalf of whom a Contribution has been received by Licensor and 336 | subsequently incorporated within the Work. 337 | 338 | 2. Grant of Copyright License. Subject to the terms and conditions of 339 | this License, each Contributor hereby grants to You a perpetual, 340 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 341 | copyright license to reproduce, prepare Derivative Works of, 342 | publicly display, publicly perform, sublicense, and distribute the 343 | Work and such Derivative Works in Source or Object form. 344 | 345 | 3. Grant of Patent License. Subject to the terms and conditions of 346 | this License, each Contributor hereby grants to You a perpetual, 347 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 348 | (except as stated in this section) patent license to make, have made, 349 | use, offer to sell, sell, import, and otherwise transfer the Work, 350 | where such license applies only to those patent claims licensable 351 | by such Contributor that are necessarily infringed by their 352 | Contribution(s) alone or by combination of their Contribution(s) 353 | with the Work to which such Contribution(s) was submitted. If You 354 | institute patent litigation against any entity (including a 355 | cross-claim or counterclaim in a lawsuit) alleging that the Work 356 | or a Contribution incorporated within the Work constitutes direct 357 | or contributory patent infringement, then any patent licenses 358 | granted to You under this License for that Work shall terminate 359 | as of the date such litigation is filed. 360 | 361 | 4. Redistribution. You may reproduce and distribute copies of the 362 | Work or Derivative Works thereof in any medium, with or without 363 | modifications, and in Source or Object form, provided that You 364 | meet the following conditions: 365 | 366 | (a) You must give any other recipients of the Work or 367 | Derivative Works a copy of this License; and 368 | 369 | (b) You must cause any modified files to carry prominent notices 370 | stating that You changed the files; and 371 | 372 | (c) You must retain, in the Source form of any Derivative Works 373 | that You distribute, all copyright, patent, trademark, and 374 | attribution notices from the Source form of the Work, 375 | excluding those notices that do not pertain to any part of 376 | the Derivative Works; and 377 | 378 | (d) If the Work includes a "NOTICE" text file as part of its 379 | distribution, then any Derivative Works that You distribute must 380 | include a readable copy of the attribution notices contained 381 | within such NOTICE file, excluding those notices that do not 382 | pertain to any part of the Derivative Works, in at least one 383 | of the following places: within a NOTICE text file distributed 384 | as part of the Derivative Works; within the Source form or 385 | documentation, if provided along with the Derivative Works; or, 386 | within a display generated by the Derivative Works, if and 387 | wherever such third-party notices normally appear. The contents 388 | of the NOTICE file are for informational purposes only and 389 | do not modify the License. You may add Your own attribution 390 | notices within Derivative Works that You distribute, alongside 391 | or as an addendum to the NOTICE text from the Work, provided 392 | that such additional attribution notices cannot be construed 393 | as modifying the License. 394 | 395 | You may add Your own copyright statement to Your modifications and 396 | may provide additional or different license terms and conditions 397 | for use, reproduction, or distribution of Your modifications, or 398 | for any such Derivative Works as a whole, provided Your use, 399 | reproduction, and distribution of the Work otherwise complies with 400 | the conditions stated in this License. 401 | 402 | 5. Submission of Contributions. Unless You explicitly state otherwise, 403 | any Contribution intentionally submitted for inclusion in the Work 404 | by You to the Licensor shall be under the terms and conditions of 405 | this License, without any additional terms or conditions. 406 | Notwithstanding the above, nothing herein shall supersede or modify 407 | the terms of any separate license agreement you may have executed 408 | with Licensor regarding such Contributions. 409 | 410 | 6. Trademarks. This License does not grant permission to use the trade 411 | names, trademarks, service marks, or product names of the Licensor, 412 | except as required for reasonable and customary use in describing the 413 | origin of the Work and reproducing the content of the NOTICE file. 414 | 415 | 7. Disclaimer of Warranty. Unless required by applicable law or 416 | agreed to in writing, Licensor provides the Work (and each 417 | Contributor provides its Contributions) on an "AS IS" BASIS, 418 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 419 | implied, including, without limitation, any warranties or conditions 420 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 421 | PARTICULAR PURPOSE. You are solely responsible for determining the 422 | appropriateness of using or redistributing the Work and assume any 423 | risks associated with Your exercise of permissions under this License. 424 | 425 | 8. Limitation of Liability. In no event and under no legal theory, 426 | whether in tort (including negligence), contract, or otherwise, 427 | unless required by applicable law (such as deliberate and grossly 428 | negligent acts) or agreed to in writing, shall any Contributor be 429 | liable to You for damages, including any direct, indirect, special, 430 | incidental, or consequential damages of any character arising as a 431 | result of this License or out of the use or inability to use the 432 | Work (including but not limited to damages for loss of goodwill, 433 | work stoppage, computer failure or malfunction, or any and all 434 | other commercial damages or losses), even if such Contributor 435 | has been advised of the possibility of such damages. 436 | 437 | 9. Accepting Warranty or Additional Liability. While redistributing 438 | the Work or Derivative Works thereof, You may choose to offer, 439 | and charge a fee for, acceptance of support, warranty, indemnity, 440 | or other liability obligations and/or rights consistent with this 441 | License. However, in accepting such obligations, You may act only 442 | on Your own behalf and on Your sole responsibility, not on behalf 443 | of any other Contributor, and only if You agree to indemnify, 444 | defend, and hold each Contributor harmless for any liability 445 | incurred by, or claims asserted against, such Contributor by reason 446 | of your accepting any such warranty or additional liability. 447 | 448 | END OF TERMS AND CONDITIONS 449 | 450 | APPENDIX: How to apply the Apache License to your work. 451 | 452 | To apply the Apache License to your work, attach the following 453 | boilerplate notice, with the fields enclosed by brackets "{}" 454 | replaced with your own identifying information. (Don't include 455 | the brackets!) The text should be enclosed in the appropriate 456 | comment syntax for the file format. We also recommend that a 457 | file or class name and description of purpose be included on the 458 | same "printed page" as the copyright notice for easier 459 | identification within third-party archives. 460 | 461 | Copyright 2018 Gregor Martynus and other contributors. 462 | 463 | Licensed under the Apache License, Version 2.0 (the "License"); 464 | you may not use this file except in compliance with the License. 465 | You may obtain a copy of the License at 466 | 467 | http://www.apache.org/licenses/LICENSE-2.0 468 | 469 | Unless required by applicable law or agreed to in writing, software 470 | distributed under the License is distributed on an "AS IS" BASIS, 471 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 472 | See the License for the specific language governing permissions and 473 | limitations under the License. 474 | 475 | 476 | buffer-from 477 | MIT 478 | MIT License 479 | 480 | Copyright (c) 2016, 2018 Linus Unnebäck 481 | 482 | Permission is hereby granted, free of charge, to any person obtaining a copy 483 | of this software and associated documentation files (the "Software"), to deal 484 | in the Software without restriction, including without limitation the rights 485 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 486 | copies of the Software, and to permit persons to whom the Software is 487 | furnished to do so, subject to the following conditions: 488 | 489 | The above copyright notice and this permission notice shall be included in all 490 | copies or substantial portions of the Software. 491 | 492 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 493 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 494 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 495 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 496 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 497 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 498 | SOFTWARE. 499 | 500 | 501 | deprecation 502 | ISC 503 | The ISC License 504 | 505 | Copyright (c) Gregor Martynus and contributors 506 | 507 | Permission to use, copy, modify, and/or distribute this software for any 508 | purpose with or without fee is hereby granted, provided that the above 509 | copyright notice and this permission notice appear in all copies. 510 | 511 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 512 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 513 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 514 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 515 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 516 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 517 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 518 | 519 | 520 | is-plain-object 521 | MIT 522 | The MIT License (MIT) 523 | 524 | Copyright (c) 2014-2017, Jon Schlinkert. 525 | 526 | Permission is hereby granted, free of charge, to any person obtaining a copy 527 | of this software and associated documentation files (the "Software"), to deal 528 | in the Software without restriction, including without limitation the rights 529 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 530 | copies of the Software, and to permit persons to whom the Software is 531 | furnished to do so, subject to the following conditions: 532 | 533 | The above copyright notice and this permission notice shall be included in 534 | all copies or substantial portions of the Software. 535 | 536 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 537 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 538 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 539 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 540 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 541 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 542 | THE SOFTWARE. 543 | 544 | 545 | once 546 | ISC 547 | The ISC License 548 | 549 | Copyright (c) Isaac Z. Schlueter and Contributors 550 | 551 | Permission to use, copy, modify, and/or distribute this software for any 552 | purpose with or without fee is hereby granted, provided that the above 553 | copyright notice and this permission notice appear in all copies. 554 | 555 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 556 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 557 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 558 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 559 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 560 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 561 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 562 | 563 | 564 | remeda 565 | MIT 566 | MIT License 567 | 568 | Copyright (c) 2018 remeda 569 | 570 | Permission is hereby granted, free of charge, to any person obtaining a copy 571 | of this software and associated documentation files (the "Software"), to deal 572 | in the Software without restriction, including without limitation the rights 573 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 574 | copies of the Software, and to permit persons to whom the Software is 575 | furnished to do so, subject to the following conditions: 576 | 577 | The above copyright notice and this permission notice shall be included in all 578 | copies or substantial portions of the Software. 579 | 580 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 581 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 582 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 583 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 584 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 585 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 586 | SOFTWARE. 587 | 588 | 589 | source-map 590 | BSD-3-Clause 591 | 592 | Copyright (c) 2009-2011, Mozilla Foundation and contributors 593 | All rights reserved. 594 | 595 | Redistribution and use in source and binary forms, with or without 596 | modification, are permitted provided that the following conditions are met: 597 | 598 | * Redistributions of source code must retain the above copyright notice, this 599 | list of conditions and the following disclaimer. 600 | 601 | * Redistributions in binary form must reproduce the above copyright notice, 602 | this list of conditions and the following disclaimer in the documentation 603 | and/or other materials provided with the distribution. 604 | 605 | * Neither the names of the Mozilla Foundation nor the names of project 606 | contributors may be used to endorse or promote products derived from this 607 | software without specific prior written permission. 608 | 609 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 610 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 611 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 612 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 613 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 614 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 615 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 616 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 617 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 618 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 619 | 620 | 621 | source-map-support 622 | MIT 623 | The MIT License (MIT) 624 | 625 | Copyright (c) 2014 Evan Wallace 626 | 627 | Permission is hereby granted, free of charge, to any person obtaining a copy 628 | of this software and associated documentation files (the "Software"), to deal 629 | in the Software without restriction, including without limitation the rights 630 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 631 | copies of the Software, and to permit persons to whom the Software is 632 | furnished to do so, subject to the following conditions: 633 | 634 | The above copyright notice and this permission notice shall be included in all 635 | copies or substantial portions of the Software. 636 | 637 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 638 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 639 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 640 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 641 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 642 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 643 | SOFTWARE. 644 | 645 | 646 | tunnel 647 | MIT 648 | The MIT License (MIT) 649 | 650 | Copyright (c) 2012 Koichi Kobayashi 651 | 652 | Permission is hereby granted, free of charge, to any person obtaining a copy 653 | of this software and associated documentation files (the "Software"), to deal 654 | in the Software without restriction, including without limitation the rights 655 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 656 | copies of the Software, and to permit persons to whom the Software is 657 | furnished to do so, subject to the following conditions: 658 | 659 | The above copyright notice and this permission notice shall be included in 660 | all copies or substantial portions of the Software. 661 | 662 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 663 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 664 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 665 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 666 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 667 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 668 | THE SOFTWARE. 669 | 670 | 671 | typescript 672 | Apache-2.0 673 | Apache License 674 | 675 | Version 2.0, January 2004 676 | 677 | http://www.apache.org/licenses/ 678 | 679 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 680 | 681 | 1. Definitions. 682 | 683 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 684 | 685 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 686 | 687 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 688 | 689 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 690 | 691 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 692 | 693 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 694 | 695 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 696 | 697 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 698 | 699 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 700 | 701 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 702 | 703 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 704 | 705 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 706 | 707 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 708 | 709 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 710 | 711 | You must cause any modified files to carry prominent notices stating that You changed the files; and 712 | 713 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 714 | 715 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 716 | 717 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 718 | 719 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 720 | 721 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 722 | 723 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 724 | 725 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 726 | 727 | END OF TERMS AND CONDITIONS 728 | 729 | 730 | undici 731 | MIT 732 | MIT License 733 | 734 | Copyright (c) Matteo Collina and Undici contributors 735 | 736 | Permission is hereby granted, free of charge, to any person obtaining a copy 737 | of this software and associated documentation files (the "Software"), to deal 738 | in the Software without restriction, including without limitation the rights 739 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 740 | copies of the Software, and to permit persons to whom the Software is 741 | furnished to do so, subject to the following conditions: 742 | 743 | The above copyright notice and this permission notice shall be included in all 744 | copies or substantial portions of the Software. 745 | 746 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 747 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 748 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 749 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 750 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 751 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 752 | SOFTWARE. 753 | 754 | 755 | universal-user-agent 756 | ISC 757 | # [ISC License](https://spdx.org/licenses/ISC) 758 | 759 | Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m) 760 | 761 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 762 | 763 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 764 | 765 | 766 | wrappy 767 | ISC 768 | The ISC License 769 | 770 | Copyright (c) Isaac Z. Schlueter and Contributors 771 | 772 | Permission to use, copy, modify, and/or distribute this software for any 773 | purpose with or without fee is hereby granted, provided that the above 774 | copyright notice and this permission notice appear in all copies. 775 | 776 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 777 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 778 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 779 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 780 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 781 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 782 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 783 | 784 | 785 | zod 786 | MIT 787 | MIT License 788 | 789 | Copyright (c) 2020 Colin McDonnell 790 | 791 | Permission is hereby granted, free of charge, to any person obtaining a copy 792 | of this software and associated documentation files (the "Software"), to deal 793 | in the Software without restriction, including without limitation the rights 794 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 795 | copies of the Software, and to permit persons to whom the Software is 796 | furnished to do so, subject to the following conditions: 797 | 798 | The above copyright notice and this permission notice shall be included in all 799 | copies or substantial portions of the Software. 800 | 801 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 802 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 803 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 804 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 805 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 806 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 807 | SOFTWARE. 808 | -------------------------------------------------------------------------------- /dummy_project/.tsgrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"] 3 | } 4 | -------------------------------------------------------------------------------- /dummy_project/data.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /dummy_project/src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = {}; 2 | import { log } from './utils'; 3 | import C from './includeFiles/c'; 4 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/D.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/a.ts: -------------------------------------------------------------------------------- 1 | import childA from './children/childA'; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const a2 = require('./excludeFiles/g'); 4 | const c2 = import('./excludeFiles/i'); 5 | import { style } from './excludeFiles/style/style'; 6 | import ClassA from './excludeFiles/class/classA'; 7 | 8 | export default async function a() { 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const b2 = require('./excludeFiles/h'); 11 | const d = await import('./d/index'); 12 | childA(); 13 | a2(); 14 | b2(); 15 | } 16 | import { log } from '../utils'; 17 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/abstractions/children/childA.ts: -------------------------------------------------------------------------------- 1 | export default function childA() {} 2 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/abstractions/j.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../../utils'; 2 | import childA from './children/childA'; 3 | import data from '../../../data.json'; 4 | 5 | export default function a() { 6 | childA(); 7 | log(); 8 | } 9 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/abstractions/k.ts: -------------------------------------------------------------------------------- 1 | import c from './l'; 2 | export default function b() { 3 | c(); 4 | } 5 | import { log } from '../../utils'; 6 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/abstractions/l.ts: -------------------------------------------------------------------------------- 1 | export default function c() {} 2 | import { log } from '../../utils'; 3 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/b.ts: -------------------------------------------------------------------------------- 1 | export default function b() { 2 | config; 3 | } 4 | import { log } from '../utils'; 5 | import { config } from '../config'; 6 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/c.ts: -------------------------------------------------------------------------------- 1 | export default function c() {} 2 | import { log } from '../utils'; 3 | import b from './b'; 4 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/children/childA.ts: -------------------------------------------------------------------------------- 1 | export default function childA() {} 2 | import { log } from '../../utils'; 3 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 42 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/d/d.ts: -------------------------------------------------------------------------------- 1 | export function d() {} 2 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/d/index.ts: -------------------------------------------------------------------------------- 1 | export * as d from './d'; 2 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/excludeFiles/children/childA.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../../../utils'; 2 | export default function childA() {} 3 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/excludeFiles/class/classA.ts: -------------------------------------------------------------------------------- 1 | export default class ClassA {} 2 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/excludeFiles/g.ts: -------------------------------------------------------------------------------- 1 | import childA from '../children/childA'; 2 | 3 | export default function a() { 4 | childA(); 5 | } 6 | import { log } from '../../utils'; 7 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/excludeFiles/h.ts: -------------------------------------------------------------------------------- 1 | import c from './i'; 2 | export default function b() { 3 | c(); 4 | config; 5 | } 6 | import { log } from '../../utils'; 7 | import { config } from '../../config'; 8 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/excludeFiles/i.ts: -------------------------------------------------------------------------------- 1 | export default function c() {} 2 | import { log } from '../../utils'; 3 | -------------------------------------------------------------------------------- /dummy_project/src/includeFiles/excludeFiles/style/style.ts: -------------------------------------------------------------------------------- 1 | export const style = { 2 | fontSize: 'large', 3 | }; 4 | -------------------------------------------------------------------------------- /dummy_project/src/main.ts: -------------------------------------------------------------------------------- 1 | import a from './includeFiles/a'; 2 | import b from './includeFiles/b'; 3 | import a2 from './otherFiles/d'; 4 | import b2 from './otherFiles/e'; 5 | import a3 from './includeFiles/abstractions/j'; 6 | import b3 from './includeFiles/abstractions/k'; 7 | import ts from 'typescript'; 8 | import D from './includeFiles/D.vue'; 9 | 10 | export default function main() { 11 | a(); 12 | b(); 13 | a2(); 14 | b2(); 15 | a3(); 16 | b3(); 17 | } 18 | import { log } from './utils'; 19 | -------------------------------------------------------------------------------- /dummy_project/src/otherFiles/children/:id.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysk8hori/delta-typescript-graph-action/0bfd5fd49b9490ac55cba02142dc946ecffc4e6e/dummy_project/src/otherFiles/children/:id.json -------------------------------------------------------------------------------- /dummy_project/src/otherFiles/children/childA.ts: -------------------------------------------------------------------------------- 1 | export default function childA() {} 2 | import { log } from '../../utils'; 3 | import id from './:id.json'; 4 | import id2 from './{id}.json'; 5 | -------------------------------------------------------------------------------- /dummy_project/src/otherFiles/children/{id}.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysk8hori/delta-typescript-graph-action/0bfd5fd49b9490ac55cba02142dc946ecffc4e6e/dummy_project/src/otherFiles/children/{id}.json -------------------------------------------------------------------------------- /dummy_project/src/otherFiles/d.ts: -------------------------------------------------------------------------------- 1 | import childA from './children/childA'; 2 | 3 | export default function a() { 4 | childA(); 5 | } 6 | import { log } from '../utils'; 7 | -------------------------------------------------------------------------------- /dummy_project/src/otherFiles/e.ts: -------------------------------------------------------------------------------- 1 | import c from './f'; 2 | export default function b() { 3 | c(); 4 | config; 5 | } 6 | import { log } from '../utils'; 7 | import { config } from '../config'; 8 | -------------------------------------------------------------------------------- /dummy_project/src/otherFiles/f.ts: -------------------------------------------------------------------------------- 1 | export default function c() {} 2 | import { log } from '../utils'; 3 | -------------------------------------------------------------------------------- /dummy_project/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function log() {} 2 | -------------------------------------------------------------------------------- /dummy_project/tsconfig-dummy.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | "jsx": "preserve" /* Specify what JSX code is generated. */, 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | "resolveJsonModule": true /* Enable importing .json files */, 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 46 | "declarationMap": true /* Create sourcemaps for d.ts files. */, 47 | // "emitDeclarationOnly": true /* Only output d.ts files and not JavaScript files. */, 48 | "sourceMap": true /* Create source map files for emitted JavaScript files. */, 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist/" /* Specify an output folder for all emitted files. */, 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 75 | 76 | /* Type Checking */ 77 | "strict": true /* Enable all strict type-checking options. */, 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | "include": ["./src/*"], 102 | "exclude": ["./**/*.spec.ts"] 103 | } 104 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import { flatConfigs as pluginImportFlatConfigs } from 'eslint-plugin-import'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | /** @type {import('eslint').Linter.Config[]} */ 7 | export default [ 8 | { 9 | languageOptions: { 10 | parserOptions: { 11 | projectService: true, 12 | tsconfigRootDir: import.meta.dirname, 13 | }, 14 | }, 15 | }, 16 | { files: ['**/*.{ts}'] }, 17 | { 18 | files: ['**/*.{js,mjs,cjs}'], 19 | extends: [tseslint.configs.disableTypeChecked], 20 | }, 21 | { languageOptions: { globals: globals.browser } }, 22 | pluginJs.configs.recommended, 23 | pluginImportFlatConfigs.recommended, 24 | pluginImportFlatConfigs.typescript, 25 | ...tseslint.configs.strict, 26 | ...tseslint.configs.stylistic, 27 | { 28 | // eslint 29 | rules: { 30 | 'import/order': 'error', 31 | 'import/no-unresolved': 'off', 32 | }, 33 | }, 34 | { 35 | // typescript-eslint 36 | rules: { 37 | '@typescript-eslint/no-invalid-void-type': 'off', 38 | '@typescript-eslint/consistent-type-imports': 'error', 39 | '@typescript-eslint/no-unused-vars': [ 40 | 'error', 41 | { args: 'all', argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 42 | ], 43 | '@typescript-eslint/no-floating-promises': 'error', 44 | }, 45 | }, 46 | ]; 47 | -------------------------------------------------------------------------------- /img/top-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ysk8hori/delta-typescript-graph-action/0bfd5fd49b9490ac55cba02142dc946ecffc4e6e/img/top-sample.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delta-typescript-graph-action", 3 | "version": "1.0.0", 4 | "description": "This GitHub Action uses Mermaid to visualize in a diagram the files that were changed in a Pull Request and their related dependency files. This approach aims to reduce the initial cognitive load during the review process and assist in understanding the structure around the modified code.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "ncc build src/index.ts --license licenses.txt", 9 | "type-check": "tsc --noEmit", 10 | "prettier": "prettier --write ./src", 11 | "lint:fix": "eslint --fix src", 12 | "lint": "eslint \"src/**/*.ts\"", 13 | "update-workflowfiles": "node ./scripts/update-workflowfiles.mjs" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@eslint/js": "9.28.0", 20 | "@jest/globals": "29.7.0", 21 | "@total-typescript/ts-reset": "0.6.1", 22 | "@types/jest": "29.5.14", 23 | "@types/node": "22.15.29", 24 | "@typescript-eslint/parser": "8.33.0", 25 | "@vercel/ncc": "0.38.3", 26 | "eslint": "9.28.0", 27 | "eslint-config-prettier": "10.1.5", 28 | "eslint-plugin-import": "2.31.0", 29 | "globals": "16.2.0", 30 | "jest": "29.7.0", 31 | "prettier": "3.5.3", 32 | "ts-jest": "29.3.4", 33 | "typescript": "5.8.3", 34 | "typescript-eslint": "8.33.0", 35 | "zx": "8.5.4" 36 | }, 37 | "dependencies": { 38 | "@actions/core": "1.11.1", 39 | "@actions/github": "6.0.1", 40 | "@ysk8hori/typescript-graph": "0.24.1", 41 | "remeda": "^2.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "ignoreDeps": ["ysk8hori/delta-typescript-graph-action"], 7 | "automerge": true 8 | } 9 | -------------------------------------------------------------------------------- /scripts/update-workflowfiles.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub Action を公開する際には、 /dist/index.js をビルドしてコミットする必要がある。それが Action の実体となる。 3 | * それをコミットしたうえで、本リポジトリの CI のテストにおいてはそのコミットハッシュを使用するようワークフローファイルを更新してコミットする。 4 | * 5 | * When publishing a GitHub Action, it is necessary to build and commit `/dist/index.js`, as it serves as the actual implementation of the Action. 6 | * After committing that, in the CI test of this repository, update the workflow file to use the commit hash and commit the changes. 7 | */ 8 | 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | import { $ } from 'zx'; 12 | 13 | await $`npm run build`; 14 | await $`git commit -am "chore: update dist"`; 15 | 16 | const headSha = (await $`git rev-parse HEAD`).toString().trim(); 17 | const files = fs.readdirSync(path.resolve('.github/workflows')); 18 | files.forEach(file => { 19 | const filePath = path.resolve('.github/workflows', file); 20 | const content = fs.readFileSync(filePath, 'utf-8'); 21 | const updatedContent = content.replace( 22 | /ysk8hori\/delta-typescript-graph-action@.*/, 23 | `ysk8hori/delta-typescript-graph-action@${headSha}`, 24 | ); 25 | fs.writeFileSync(filePath, updatedContent); 26 | }); 27 | 28 | await $`git commit -am "ci: update hash"`; 29 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # 設計 2 | 3 | ```mermaid 4 | flowchart 5 | Start((START))-->TsfileModifiers{"TypeScriptファイルの
変更有無"} 6 | TsfileModifiers-->|なし|end1((終了)) 7 | TsfileModifiers-->|あり|GenerateHeadGraph["Head の Graph を生成"] 8 | GenerateHeadGraph-->FilterHeadGraph["Head の Graphから
不要部分を除去"] 9 | FilterHeadGraph-->GenerateBaseGraph["Base の Graph を生成"] 10 | GenerateBaseGraph-->FilterBaseGraph["Base の Graphから
不要部分を除去"] 11 | 12 | 13 | ExistsNode{グラフに
表示可能な
ノードが...} 14 | ExistsNode-->|ない|TypeScripgGraph非表示-->end2((終了)) 15 | ExistsNode-->|ある|JudgeGraphAmmounts 16 | FilterBaseGraph-->ExistsNode 17 | JudgeGraphAmmounts{"ファイルの削除
またはリネームが..."} 18 | 19 | %% 1つの Graph 20 | JudgeGraphAmmounts-->|ない|GetDiff["Head と Base の差分を取り
ノードやリレーションに
ステータスを付与する"] 21 | GetDiff-->MergeGraph["Head と Base をマージする"] 22 | MergeGraph-->表示する-->end3((終了)) 23 | 24 | %% 2つの Graph 25 | JudgeGraphAmmounts-->|ある|AddStatusToHead["Head の Graph に
ステータスを付与する"] 26 | AddStatusToHead-->AddStatusToBase["Base の Graph に
ステータスを付与する"] 27 | AddStatusToBase-->Display2Graph["2つの Graph を表示する"]-->end4((終了)) 28 | 29 | 30 | ``` 31 | 32 | - ❓ なぜ Head と Base の Graph 生成を並行処理しないか 33 | - 👨🏻‍🎓 それぞれのブランチをチェックアウトする必要があるため排他的になる 34 | - ❓ なぜ 1 つの Graph と 2 つの Graph が必要なのか 35 | - 👨🏻‍🎓 その PR での構造の変化の度合いによって、1 つの Graph で見るほうが理解が容易か、2 つの Graph (Before - After)で見るほうが理解が容易かが異なるため 36 | -------------------------------------------------------------------------------- /src/getFullGraph.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import type { Graph } from '@ysk8hori/typescript-graph'; 3 | import { 4 | GraphAnalyzer, 5 | mergeGraph, 6 | ProjectTraverser, 7 | resolveTsconfig, 8 | } from '@ysk8hori/typescript-graph'; 9 | import { anyPass, isNot, map, pipe } from 'remeda'; 10 | import { getCreateGraphsArguments } from './tsg/getCreateGraphsArguments'; 11 | import type { Context } from './utils/context'; 12 | 13 | /** word に該当するか */ 14 | const bindMatchFunc = (word: string) => (filePath: string) => 15 | filePath.toLowerCase().includes(word.toLowerCase()); 16 | /** word に完全一致するか */ 17 | const bindExactMatchFunc = (word: string) => (filePath: string) => 18 | filePath === word; 19 | /** 抽象的な判定関数 */ 20 | const judge = (filePath: string) => (f: (filePath: string) => boolean) => 21 | f(filePath); 22 | /** いずれかの word に該当するか */ 23 | const matchSome = (words: string[]) => (filePath: string) => 24 | words.map(bindMatchFunc).some(judge(filePath)); 25 | /** いずれかの word に完全一致するか */ 26 | const isExactMatchSome = (words: string[]) => (filePath: string) => 27 | words.map(bindExactMatchFunc).some(judge(filePath)); 28 | 29 | /** 30 | * TypeScript Graph の createGraph を使い head と base の Graph を生成する 31 | * 32 | * 内部的に git fetch と git checkout を実行するので、テストで実行する際には execSync を mock すること。 33 | */ 34 | export function getFullGraph( 35 | context: Pick, 36 | ): { 37 | fullHeadGraph: Graph; 38 | fullBaseGraph: Graph; 39 | traverserForHead: ProjectTraverser | undefined; 40 | traverserForBase: ProjectTraverser | undefined; 41 | } { 42 | // head の Graph を生成するために head に checkout する 43 | execSync(`git fetch origin ${context.github.getHeadSha()}`); 44 | execSync(`git checkout ${context.github.getHeadSha()}`); 45 | const [fullHeadGraph, traverserForHead] = getGraph(context); 46 | 47 | // base の Graph を生成するために base に checkout する 48 | execSync(`git fetch origin ${context.github.getBaseSha()}`); 49 | execSync(`git checkout ${context.github.getBaseSha()}`); 50 | const [fullBaseGraph, traverserForBase] = getGraph(context); 51 | 52 | return { 53 | fullHeadGraph, 54 | fullBaseGraph, 55 | traverserForHead, 56 | traverserForBase, 57 | }; 58 | } 59 | 60 | function getGraph( 61 | context: Pick, 62 | ): [Graph, ProjectTraverser | undefined] { 63 | const { modified, created, deleted, renamed } = context.filesChanged; 64 | const includeFiles = [modified, created, deleted, renamed] 65 | .flat() 66 | .map(v => v.filename); 67 | const tsconfigInfo = getCreateGraphsArguments(context.config); 68 | // - tsconfig が指定されているが、そのファイルが存在しない場合は空のグラフとする 69 | // (createGraph は指定された tsconfig がない場合、カレントディレクトリより上に向かって tsconfig.json を探すが、ここではそれをしたくない) 70 | // - tsconfig が指定されていない場合は、tsconfig-root から tsconfig.json を探索する 71 | if (!tsconfigInfo) { 72 | return [ 73 | { 74 | nodes: [], 75 | relations: [], 76 | } satisfies Graph, 77 | undefined, 78 | ]; 79 | } 80 | 81 | const tsConfig = resolveTsconfig(tsconfigInfo); 82 | const traverser = new ProjectTraverser(tsConfig); 83 | return [ 84 | pipe( 85 | traverser.traverse( 86 | anyPass([ 87 | isExactMatchSome(includeFiles), 88 | isNot(matchSome(context.config.exclude)), 89 | ]), 90 | GraphAnalyzer.create, 91 | ), 92 | map(([analyzer]) => analyzer.generateGraph()), 93 | mergeGraph, 94 | ), 95 | traverser, 96 | ] as const; 97 | } 98 | -------------------------------------------------------------------------------- /src/graph/__snapshots__/build2GraphsMessage.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`削除がある場合 1`] = ` 4 | " 5 | 6 | 7 | \`\`\`bash 8 | tsg --include src/B.tsx src/A.tsx --highlight src/B.tsx src/A.tsx --exclude node_modules --abstraction src/1 9 | \`\`\` 10 | 11 | ### Base Branch 12 | 13 | \`\`\`mermaid 14 | flowchart 15 | classDef modified fill:yellow,stroke:#999,color:black 16 | classDef deleted fill:dimgray,stroke:#999,color:black,stroke-dasharray: 4 4,stroke-width:2px; 17 | subgraph src["src"] 18 | src/A.tsx["A"]:::modified 19 | src/B.tsx["B"]:::deleted 20 | src/C.tsx["C"] 21 | src/1["/1"]:::dir 22 | end 23 | src/A.tsx-->src/B.tsx 24 | src/A.tsx-->src/C.tsx 25 | src/A.tsx-->src/1 26 | 27 | \`\`\` 28 | 29 | ### Head Branch 30 | 31 | \`\`\`mermaid 32 | flowchart 33 | classDef modified fill:yellow,stroke:#999,color:black 34 | subgraph src["src"] 35 | src/A.tsx["A"]:::modified 36 | src/C.tsx["C"] 37 | src/1["/1"]:::dir 38 | end 39 | src/A.tsx-->src/C.tsx 40 | src/A.tsx-->src/1 41 | 42 | \`\`\` 43 | 44 | 45 | 46 | " 47 | `; 48 | -------------------------------------------------------------------------------- /src/graph/__snapshots__/buildGraphMessage.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`グラフが大きすぎる場合はその旨を出力する 1`] = ` 4 | " 5 | 6 | 7 | > 表示ノード数が多いため、グラフを表示しません。 8 | > 表示ノード数の上限を変更したい場合はアクションのパラメータ \`max-size\` を設定してください。 9 | > 10 | > 本PRでの表示ノード数: 4 11 | > 最大表示ノード数: 1 12 | 13 | \`\`\`bash 14 | tsg --include src/B.tsx src/A.tsx --highlight src/B.tsx src/A.tsx --exclude node_modules --abstraction src/1 15 | \`\`\` 16 | 17 | 18 | 19 | " 20 | `; 21 | 22 | exports[`追加や依存の削除がある場合 1`] = ` 23 | " 24 | 25 | 26 | \`\`\`bash 27 | tsg --include src/B.tsx src/A.tsx --highlight src/B.tsx src/A.tsx --exclude node_modules --abstraction src/1 28 | \`\`\` 29 | 30 | \`\`\`mermaid 31 | flowchart 32 | classDef created fill:cyan,stroke:#999,color:black 33 | classDef modified fill:yellow,stroke:#999,color:black 34 | subgraph src["src"] 35 | src/A.tsx["A"]:::modified 36 | src/B.tsx["B"]:::created 37 | src/1["/1"]:::dir 38 | src/C.tsx["C"] 39 | end 40 | src/A.tsx-->src/B.tsx 41 | src/A.tsx-->src/1 42 | src/A.tsx-.->src/C.tsx 43 | 44 | \`\`\` 45 | 46 | 47 | 48 | " 49 | `; 50 | -------------------------------------------------------------------------------- /src/graph/addStatus.ts: -------------------------------------------------------------------------------- 1 | import type { Graph, ChangeStatus } from '@ysk8hori/typescript-graph'; 2 | import type { Context } from '../utils/context'; 3 | 4 | export default function addStatus( 5 | { 6 | filesChanged: { modified, created, deleted }, 7 | }: Pick, 8 | graph: Graph, 9 | ): Graph { 10 | const { nodes, relations } = graph; 11 | const newNodes = nodes.map(node => { 12 | const changeStatus: ChangeStatus = (function () { 13 | if (deleted.map(({ filename }) => filename).includes(node.path)) 14 | return 'deleted'; 15 | if (created.map(({ filename }) => filename).includes(node.path)) 16 | return 'created'; 17 | if (modified.map(({ filename }) => filename).includes(node.path)) 18 | return 'modified'; 19 | return 'not_modified'; 20 | })(); 21 | return { 22 | ...node, 23 | changeStatus, 24 | }; 25 | }); 26 | return { 27 | nodes: newNodes, 28 | relations, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/graph/applyMutualDifferences.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | import { abstraction, filterGraph } from '@ysk8hori/typescript-graph'; 3 | import { pipe } from 'remeda'; 4 | import { log } from '../utils/log'; 5 | import { createTsgCommand } from '../tsg/createTsgCommand'; 6 | import type { Context } from '../utils/context'; 7 | import addStatus from './addStatus'; 8 | import extractAbstractionTarget from './extractAbstractionTarget'; 9 | import extractNoAbstractionDirs from './extractNoAbstractionDirs'; 10 | import { extractAbstractionTargetFromGraphs } from './extractAbstractionTargetFromGraphs'; 11 | import { createIncludeList } from './createIncludeList'; 12 | 13 | /** 14 | * 2つのグラフの差分を互いに反映する。 15 | * 16 | * 実際にグラフの差分を見ているのではなく、github api で取得したファイルの差分を見ている。 17 | */ 18 | export default function applyMutualDifferences( 19 | fullBaseGraph: Graph, 20 | fullHeadGraph: Graph, 21 | context: Context, 22 | ) { 23 | const includes = createIncludeList({ 24 | context, 25 | graphs: [fullBaseGraph, fullHeadGraph], 26 | }); 27 | log('includes:', includes); 28 | 29 | const abstractionTargetsForBase = pipe( 30 | includes, 31 | extractNoAbstractionDirs, 32 | dirs => extractAbstractionTarget(dirs, fullBaseGraph), 33 | ); 34 | log('abstractionTargetsForBase:', abstractionTargetsForBase); 35 | const baseGraph = pipe( 36 | fullBaseGraph, 37 | graph => filterGraph(includes, context.config.exclude, graph), 38 | graph => ( 39 | log('filtered base graph.nodes.length:', graph.nodes.length), 40 | log('filtered base graph.relations.length:', graph.relations.length), 41 | graph 42 | ), 43 | graph => abstraction(abstractionTargetsForBase, graph), 44 | graph => ( 45 | log('abstracted base graph.nodes.length:', graph.nodes.length), 46 | log('abstracted base graph.relations.length:', graph.relations.length), 47 | graph 48 | ), 49 | graph => addStatus(context, graph), 50 | ); 51 | log('baseGraph.nodes.length:', baseGraph.nodes.length); 52 | log('baseGraph.relations.length:', baseGraph.relations.length); 53 | 54 | const abstractionTargetsForHead = pipe( 55 | includes, 56 | extractNoAbstractionDirs, 57 | dirs => extractAbstractionTarget(dirs, fullHeadGraph), 58 | ); 59 | log('abstractionTargetsForHead:', abstractionTargetsForHead); 60 | const headGraph = pipe( 61 | fullHeadGraph, 62 | graph => filterGraph(includes, context.config.exclude, graph), 63 | graph => ( 64 | log('filtered head graph.nodes.length:', graph.nodes.length), 65 | log('filtered head graph.relations.length:', graph.relations.length), 66 | graph 67 | ), 68 | graph => abstraction(abstractionTargetsForHead, graph), 69 | graph => ( 70 | log('abstracted head graph.nodes.length:', graph.nodes.length), 71 | log('abstracted head graph.relations.length:', graph.relations.length), 72 | graph 73 | ), 74 | graph => addStatus(context, graph), 75 | ); 76 | log('headGraph.nodes.length:', headGraph.nodes.length); 77 | log('headGraph.relations.length:', headGraph.relations.length); 78 | 79 | const tsgCommand = createTsgCommand({ 80 | includes, 81 | abstractions: extractAbstractionTargetFromGraphs(baseGraph, headGraph), 82 | context, 83 | }); 84 | 85 | return { baseGraph, headGraph, tsgCommand }; 86 | } 87 | -------------------------------------------------------------------------------- /src/graph/build2GraphsMessage.test.ts: -------------------------------------------------------------------------------- 1 | import type { Graph, Node } from '@ysk8hori/typescript-graph'; 2 | import { getDummyContext } from '../utils/dummyContext'; 3 | import { build2GraphsMessage } from './build2GraphsMessage'; 4 | 5 | const a: Node = { 6 | path: 'src/A.tsx', 7 | name: 'A', 8 | changeStatus: 'not_modified', 9 | }; 10 | const b: Node = { 11 | path: 'src/B.tsx', 12 | name: 'B', 13 | changeStatus: 'not_modified', 14 | }; 15 | const c: Node = { 16 | path: 'src/C.tsx', 17 | name: 'C', 18 | changeStatus: 'not_modified', 19 | }; 20 | 21 | const d: Node = { 22 | path: 'src/1/D.tsx', 23 | name: 'D', 24 | changeStatus: 'not_modified', 25 | }; 26 | 27 | const e: Node = { 28 | path: 'src/1/E.tsx', 29 | name: 'E', 30 | changeStatus: 'not_modified', 31 | }; 32 | 33 | test('削除がある場合', () => { 34 | const base: Graph = { 35 | nodes: [a, b, c, d, e], 36 | relations: [ 37 | { 38 | from: a, 39 | to: b, 40 | kind: 'depends_on', 41 | changeStatus: 'not_modified', 42 | }, 43 | { 44 | from: a, 45 | to: c, 46 | kind: 'depends_on', 47 | changeStatus: 'not_modified', 48 | }, 49 | { 50 | from: a, 51 | to: d, 52 | kind: 'depends_on', 53 | changeStatus: 'not_modified', 54 | }, 55 | { 56 | from: d, 57 | to: e, 58 | kind: 'depends_on', 59 | changeStatus: 'not_modified', 60 | }, 61 | ], 62 | }; 63 | const head: Graph = { 64 | nodes: [a, c, d, e], 65 | relations: [ 66 | { 67 | from: a, 68 | to: c, 69 | kind: 'depends_on', 70 | changeStatus: 'not_modified', 71 | }, 72 | { 73 | from: a, 74 | to: d, 75 | kind: 'depends_on', 76 | changeStatus: 'not_modified', 77 | }, 78 | { 79 | from: d, 80 | to: e, 81 | kind: 'depends_on', 82 | changeStatus: 'not_modified', 83 | }, 84 | ], 85 | }; 86 | const context = getDummyContext(); 87 | const result = build2GraphsMessage(base, head, { 88 | ...context, 89 | filesChanged: { 90 | created: [], 91 | deleted: [ 92 | { filename: b.path, previous_filename: undefined, status: 'removed' }, 93 | ], 94 | modified: [ 95 | { filename: a.path, previous_filename: undefined, status: 'modified' }, 96 | ], 97 | renamed: [], 98 | }, 99 | }); 100 | expect(result).toMatchSnapshot(); 101 | }); 102 | -------------------------------------------------------------------------------- /src/graph/build2GraphsMessage.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | import { mermaidify } from '@ysk8hori/typescript-graph'; 3 | import { getMaxSize, getOrientation, isInDetails } from '../utils/config'; 4 | import type { Context } from '../utils/context'; 5 | import applyMutualDifferences from './applyMutualDifferences'; 6 | 7 | /** 8 | * ファイルの削除またはリネームがある場合は Graph を2つ表示する 9 | */ 10 | export function build2GraphsMessage( 11 | fullBaseGraph: Graph, 12 | fullHeadGraph: Graph, 13 | context: Context, 14 | ): string { 15 | const { baseGraph, headGraph, tsgCommand } = applyMutualDifferences( 16 | fullBaseGraph, 17 | fullHeadGraph, 18 | context, 19 | ); 20 | 21 | if (baseGraph.nodes.length === 0 && headGraph.nodes.length === 0) { 22 | // base と head のグラフが空の場合は表示しない 23 | return 'The graph is empty.\n\n'; 24 | } 25 | 26 | if ( 27 | baseGraph.nodes.length > getMaxSize() || 28 | headGraph.nodes.length > getMaxSize() 29 | ) { 30 | // base または head のグラフが大きすぎる場合は表示しない 31 | return buildGraphSizeExceededMessage(baseGraph, headGraph, tsgCommand); 32 | } 33 | 34 | // base の書き出し 35 | const baseLines: string[] = []; 36 | const orientation = getOrientation(); 37 | mermaidify((arg: string) => baseLines.push(arg), baseGraph, orientation); 38 | 39 | // head の書き出し 40 | const headLines: string[] = []; 41 | mermaidify((arg: string) => headLines.push(arg), headGraph, orientation); 42 | 43 | return buildNormal2GraphMessage(tsgCommand, baseLines, headLines); 44 | } 45 | 46 | function buildNormal2GraphMessage( 47 | tsgCommand: string, 48 | baseLines: string[], 49 | headLines: string[], 50 | ): string { 51 | return ` 52 | ${outputIfInDetails(` 53 |
54 | mermaid 55 | `)} 56 | 57 | \`\`\`bash 58 | ${tsgCommand} 59 | \`\`\` 60 | 61 | ### Base Branch 62 | 63 | \`\`\`mermaid 64 | ${baseLines.join('')} 65 | \`\`\` 66 | 67 | ### Head Branch 68 | 69 | \`\`\`mermaid 70 | ${headLines.join('')} 71 | \`\`\` 72 | 73 | ${outputIfInDetails('
')} 74 | 75 | `; 76 | } 77 | 78 | function buildGraphSizeExceededMessage( 79 | baseGraph: Graph, 80 | headGraph: Graph, 81 | tsgCommand: string, 82 | ): string { 83 | return ` 84 | ${outputIfInDetails(` 85 |
86 | mermaid 87 | `)} 88 | 89 | > 表示ノード数が多いため、グラフを表示しません。 90 | > 表示ノード数の上限を変更したい場合はアクションのパラメータ \`max-size\` を設定してください。 91 | > 92 | > Base branch の表示ノード数: ${baseGraph.nodes.length} 93 | > Head branch の表示ノード数: ${headGraph.nodes.length} 94 | 95 | \`\`\`bash 96 | ${tsgCommand} 97 | \`\`\` 98 | 99 | ${outputIfInDetails('
')} 100 | 101 | `; 102 | } 103 | 104 | /** isMermaidInDetails() の結果が true ならば与えられた文字列を返し、そうでなければ空文字を返す関数。 */ 105 | function outputIfInDetails(str: string): string { 106 | return isInDetails() ? str : ''; 107 | } 108 | -------------------------------------------------------------------------------- /src/graph/buildGraphMessage.test.ts: -------------------------------------------------------------------------------- 1 | import type { Graph, Node } from '@ysk8hori/typescript-graph'; 2 | import { getDummyContext } from '../utils/dummyContext'; 3 | import { buildGraphMessage } from './buildGraphMessage'; 4 | 5 | const a: Node = { 6 | path: 'src/A.tsx', 7 | name: 'A', 8 | changeStatus: 'not_modified', 9 | }; 10 | const b: Node = { 11 | path: 'src/B.tsx', 12 | name: 'B', 13 | changeStatus: 'not_modified', 14 | }; 15 | const c: Node = { 16 | path: 'src/C.tsx', 17 | name: 'C', 18 | changeStatus: 'not_modified', 19 | }; 20 | 21 | const d: Node = { 22 | path: 'src/1/D.tsx', 23 | name: 'D', 24 | changeStatus: 'not_modified', 25 | }; 26 | 27 | const e: Node = { 28 | path: 'src/1/E.tsx', 29 | name: 'E', 30 | changeStatus: 'not_modified', 31 | }; 32 | 33 | test('出力可能なグラフがない場合は何も出力しない', () => { 34 | const graph = { 35 | nodes: [], 36 | relations: [], 37 | }; 38 | const context = getDummyContext(); 39 | const message = buildGraphMessage(graph, graph, context); 40 | expect(message).toBe('The graph is empty.\n\n'); 41 | }); 42 | 43 | test('グラフが大きすぎる場合はその旨を出力する', () => { 44 | const graphA: Graph = { 45 | nodes: [a, c, d, e], 46 | relations: [ 47 | { 48 | from: a, 49 | to: c, 50 | kind: 'depends_on', 51 | changeStatus: 'not_modified', 52 | }, 53 | { 54 | from: a, 55 | to: d, 56 | kind: 'depends_on', 57 | changeStatus: 'not_modified', 58 | }, 59 | { 60 | from: d, 61 | to: e, 62 | kind: 'depends_on', 63 | changeStatus: 'not_modified', 64 | }, 65 | ], 66 | }; 67 | const graphB: Graph = { 68 | nodes: [a, b, c, d, e], 69 | relations: [ 70 | { 71 | from: a, 72 | to: b, 73 | kind: 'depends_on', 74 | changeStatus: 'not_modified', 75 | }, 76 | { 77 | from: a, 78 | to: d, 79 | kind: 'depends_on', 80 | changeStatus: 'not_modified', 81 | }, 82 | { 83 | from: d, 84 | to: e, 85 | kind: 'depends_on', 86 | changeStatus: 'not_modified', 87 | }, 88 | ], 89 | }; 90 | const context = getDummyContext(); 91 | const message = buildGraphMessage(graphA, graphB, { 92 | ...context, 93 | config: { ...context.config, maxSize: 1 }, 94 | filesChanged: { 95 | created: [ 96 | { filename: b.path, previous_filename: undefined, status: 'added' }, 97 | ], 98 | deleted: [], 99 | modified: [ 100 | { filename: a.path, previous_filename: undefined, status: 'modified' }, 101 | ], 102 | renamed: [], 103 | }, 104 | }); 105 | expect(message).toMatchSnapshot(); 106 | }); 107 | 108 | test('追加や依存の削除がある場合', () => { 109 | const graphA: Graph = { 110 | nodes: [a, c, d, e], 111 | relations: [ 112 | { 113 | from: a, 114 | to: c, 115 | kind: 'depends_on', 116 | changeStatus: 'not_modified', 117 | }, 118 | { 119 | from: a, 120 | to: d, 121 | kind: 'depends_on', 122 | changeStatus: 'not_modified', 123 | }, 124 | { 125 | from: d, 126 | to: e, 127 | kind: 'depends_on', 128 | changeStatus: 'not_modified', 129 | }, 130 | ], 131 | }; 132 | const graphB: Graph = { 133 | nodes: [a, b, c, d, e], 134 | relations: [ 135 | { 136 | from: a, 137 | to: b, 138 | kind: 'depends_on', 139 | changeStatus: 'not_modified', 140 | }, 141 | { 142 | from: a, 143 | to: d, 144 | kind: 'depends_on', 145 | changeStatus: 'not_modified', 146 | }, 147 | { 148 | from: d, 149 | to: e, 150 | kind: 'depends_on', 151 | changeStatus: 'not_modified', 152 | }, 153 | ], 154 | }; 155 | const context = getDummyContext(); 156 | const message = buildGraphMessage(graphA, graphB, { 157 | ...context, 158 | filesChanged: { 159 | created: [ 160 | { filename: b.path, previous_filename: undefined, status: 'added' }, 161 | ], 162 | deleted: [], 163 | modified: [ 164 | { filename: a.path, previous_filename: undefined, status: 'modified' }, 165 | ], 166 | renamed: [], 167 | }, 168 | }); 169 | expect(message).toMatchSnapshot(); 170 | }); 171 | -------------------------------------------------------------------------------- /src/graph/buildGraphMessage.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | import { mermaidify } from '@ysk8hori/typescript-graph'; 3 | import { isInDetails } from '../utils/config'; 4 | import type { Context } from '../utils/context'; 5 | import mergeGraphsWithDifferences from './mergeGraphsWithDifferences'; 6 | 7 | export function buildGraphMessage( 8 | fullBaseGraph: Graph, 9 | fullHeadGraph: Graph, 10 | context: Context, 11 | ): string { 12 | const { graph, tsgCommand } = mergeGraphsWithDifferences( 13 | fullBaseGraph, 14 | fullHeadGraph, 15 | context, 16 | ); 17 | 18 | if (graph.nodes.length === 0) { 19 | return 'The graph is empty.\n\n'; 20 | } 21 | 22 | if (graph.nodes.length > context.config.maxSize) { 23 | // グラフが大きすぎる場合は表示しない 24 | return buildGraphSizeExceededMessage(graph, context, tsgCommand); 25 | } 26 | 27 | const mermaidLines: string[] = []; 28 | mermaidify( 29 | (arg: string) => mermaidLines.push(arg), 30 | graph, 31 | context.config.orientation, 32 | ); 33 | 34 | return buildNormalGraphMessage(tsgCommand, mermaidLines); 35 | } 36 | 37 | function buildNormalGraphMessage( 38 | tsgCommand: string, 39 | mermaidLines: string[], 40 | ): string { 41 | return ` 42 | ${outputIfInDetails(` 43 |
44 | mermaid 45 | `)} 46 | 47 | \`\`\`bash 48 | ${tsgCommand} 49 | \`\`\` 50 | 51 | \`\`\`mermaid 52 | ${mermaidLines.join('')} 53 | \`\`\` 54 | 55 | ${outputIfInDetails('
')} 56 | 57 | `; 58 | } 59 | 60 | function buildGraphSizeExceededMessage( 61 | graph: Graph, 62 | context: Context, 63 | tsgCommand: string, 64 | ): string { 65 | return ` 66 | ${outputIfInDetails(` 67 |
68 | mermaid 69 | `)} 70 | 71 | > 表示ノード数が多いため、グラフを表示しません。 72 | > 表示ノード数の上限を変更したい場合はアクションのパラメータ \`max-size\` を設定してください。 73 | > 74 | > 本PRでの表示ノード数: ${graph.nodes.length} 75 | > 最大表示ノード数: ${context.config.maxSize} 76 | 77 | \`\`\`bash 78 | ${tsgCommand} 79 | \`\`\` 80 | 81 | ${outputIfInDetails('
')} 82 | 83 | `; 84 | } 85 | 86 | /** isMermaidInDetails() の結果が true ならば与えられた文字列を返し、そうでなければ空文字を返す関数。 */ 87 | function outputIfInDetails(str: string): string { 88 | return isInDetails() ? str : ''; 89 | } 90 | -------------------------------------------------------------------------------- /src/graph/createIncludeList.test.ts: -------------------------------------------------------------------------------- 1 | import type { Node, Relation } from '@ysk8hori/typescript-graph'; 2 | import { isIncludeIndexFileDependencies } from '../utils/config'; 3 | import { createIncludeList } from './createIncludeList'; 4 | 5 | jest.mock('../utils/config', () => ({ 6 | isIncludeIndexFileDependencies: jest.fn(), 7 | })); 8 | 9 | test('新規作成、更新、削除、リネーム前後のファイルが include 対象となる', () => { 10 | expect( 11 | createIncludeList({ 12 | context: { 13 | filesChanged: { 14 | created: [ 15 | { 16 | filename: 'created.ts', 17 | status: 'added', 18 | previous_filename: undefined, 19 | }, 20 | ], 21 | deleted: [ 22 | { 23 | filename: 'deleted.ts', 24 | status: 'removed', 25 | previous_filename: undefined, 26 | }, 27 | ], 28 | modified: [ 29 | { 30 | filename: 'modified.ts', 31 | status: 'modified', 32 | previous_filename: undefined, 33 | }, 34 | ], 35 | renamed: [ 36 | { 37 | previous_filename: 'before.ts', 38 | filename: 'after.ts', 39 | status: 'renamed', 40 | }, 41 | ], 42 | }, 43 | }, 44 | // created: ['created.ts'], 45 | // deleted: ['deleted.ts'], 46 | // modified: ['modified.ts'], 47 | // renamed: [{ previous_filename: 'before.ts', filename: 'after.ts' }], 48 | graphs: [], 49 | }), 50 | ).toEqual([ 51 | 'created.ts', 52 | 'deleted.ts', 53 | 'modified.ts', 54 | 'before.ts', 55 | 'after.ts', 56 | ]); 57 | }); 58 | 59 | test('TSG_INCLUDE_INDEX_FILE_DEPENDENCIES が false の場合は include 対象となるファイルを参照している index.ts を含めない', () => { 60 | (isIncludeIndexFileDependencies as jest.Mock).mockImplementation(() => false); 61 | expect( 62 | createIncludeList({ 63 | context: { 64 | filesChanged: { 65 | created: [], 66 | deleted: [], 67 | modified: [ 68 | { 69 | filename: 'src/a.ts', 70 | status: 'modified', 71 | previous_filename: undefined, 72 | }, 73 | ], 74 | renamed: [], 75 | }, 76 | }, 77 | // created: [], 78 | // deleted: [], 79 | // modified: ['src/a.ts'], 80 | // renamed: [], 81 | graphs: [ 82 | { 83 | nodes: [ 84 | { 85 | changeStatus: 'not_modified', 86 | name: 'a.ts', 87 | path: 'src/a.ts', 88 | } satisfies Node, 89 | ], 90 | relations: [ 91 | { 92 | from: { 93 | changeStatus: 'not_modified', 94 | name: 'index.ts', 95 | path: 'src/index.ts', 96 | } satisfies Node, 97 | to: { 98 | changeStatus: 'not_modified', 99 | name: 'a.ts', 100 | path: 'src/a.ts', 101 | } satisfies Node, 102 | changeStatus: 'not_modified', 103 | kind: 'depends_on', 104 | } satisfies Relation, 105 | ], 106 | }, 107 | ], 108 | }), 109 | ).toEqual(['src/a.ts']); 110 | }); 111 | 112 | test('TSG_INCLUDE_INDEX_FILE_DEPENDENCIES が true の場合は include 対象となるファイルを参照している index.ts を含める', () => { 113 | (isIncludeIndexFileDependencies as jest.Mock).mockImplementation(() => true); 114 | expect( 115 | createIncludeList({ 116 | context: { 117 | filesChanged: { 118 | created: [], 119 | deleted: [], 120 | modified: [ 121 | { 122 | filename: 'src/a.ts', 123 | status: 'modified', 124 | previous_filename: undefined, 125 | }, 126 | ], 127 | renamed: [], 128 | }, 129 | }, 130 | graphs: [ 131 | { 132 | nodes: [ 133 | { 134 | changeStatus: 'not_modified', 135 | name: 'a.ts', 136 | path: 'src/a.ts', 137 | } satisfies Node, 138 | ], 139 | relations: [ 140 | { 141 | from: { 142 | changeStatus: 'not_modified', 143 | name: 'index.ts', 144 | path: 'src/index.ts', 145 | } satisfies Node, 146 | to: { 147 | changeStatus: 'not_modified', 148 | name: 'a.ts', 149 | path: 'src/a.ts', 150 | } satisfies Node, 151 | changeStatus: 'not_modified', 152 | kind: 'depends_on', 153 | } satisfies Relation, 154 | ], 155 | }, 156 | ], 157 | }), 158 | ).toEqual(['src/a.ts', 'src/index.ts']); 159 | }); 160 | -------------------------------------------------------------------------------- /src/graph/createIncludeList.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | import { isIncludeIndexFileDependencies } from '../utils/config'; 3 | import type { Context } from '../utils/context'; 4 | import { extractIndexFileDependencies } from './extractIndexFileDependencies'; 5 | 6 | export function createIncludeList({ 7 | context: { 8 | filesChanged: { created, deleted, modified, renamed }, 9 | }, 10 | graphs, 11 | }: { 12 | context: Pick; 13 | graphs: Graph[]; 14 | }) { 15 | const tmp = [ 16 | ...created.map(({ filename }) => filename), 17 | ...deleted.map(({ filename }) => filename), 18 | ...modified.map(({ filename }) => filename), 19 | ...(renamed 20 | ?.flatMap(diff => [diff.previous_filename, diff.filename]) 21 | .filter(Boolean) ?? []), 22 | ]; 23 | return isIncludeIndexFileDependencies() 24 | ? [ 25 | ...tmp, 26 | ...extractIndexFileDependencies({ graphs, includeFilePaths: tmp }), 27 | ] 28 | : tmp; 29 | } 30 | -------------------------------------------------------------------------------- /src/graph/extractAbstractionTarget.test.ts: -------------------------------------------------------------------------------- 1 | // extractAbstractionTarget のテスト 2 | import extractAbstractionTarget from './extractAbstractionTarget'; 3 | import extractNoAbstractionDirs from './extractNoAbstractionDirs'; 4 | 5 | it('グラフと、抽象化してはいけないファイルのパスから、抽象化して良いディレクトリのパスを取得する', () => { 6 | expect( 7 | extractAbstractionTarget( 8 | [ 9 | '.github', 10 | '.github/workflows', 11 | 'src', 12 | 'src/components', 13 | 'src/components/game', 14 | 'src/components/game/cell', 15 | ], 16 | { 17 | nodes: [ 18 | { 19 | path: 'src/components/game/cell/Hoge.ts', 20 | name: 'Hoge.ts', 21 | changeStatus: 'not_modified', 22 | }, 23 | { 24 | path: 'src/components/game/cell/Cell.tsx', 25 | name: 'Cell.tsx', 26 | changeStatus: 'not_modified', 27 | }, 28 | { 29 | path: 'src/components/game/utils/answers/getAnswerClass.ts', 30 | name: 'getAnswerClass.ts', 31 | changeStatus: 'not_modified', 32 | }, 33 | { 34 | path: 'src/components/game/cell/MemoLayer.tsx', 35 | name: 'MemoLayer.tsx', 36 | changeStatus: 'not_modified', 37 | }, 38 | { 39 | path: 'src/atoms.ts', 40 | name: 'atoms.ts', 41 | changeStatus: 'not_modified', 42 | }, 43 | { 44 | path: 'src/components/game/GameBoard.tsx', 45 | name: 'GameBoard.tsx', 46 | changeStatus: 'not_modified', 47 | }, 48 | { 49 | path: 'src/components/game/cell/Cell.stories.tsx', 50 | name: 'Cell.stories.tsx', 51 | changeStatus: 'not_modified', 52 | }, 53 | { 54 | path: 'src/components/game/cell/Cell.test.tsx', 55 | name: 'Cell.test.tsx', 56 | changeStatus: 'not_modified', 57 | }, 58 | ], 59 | relations: [ 60 | { 61 | kind: 'depends_on', 62 | from: { 63 | path: 'src/components/game/cell/Cell.tsx', 64 | name: 'Cell.tsx', 65 | changeStatus: 'not_modified', 66 | }, 67 | to: { 68 | path: 'src/components/game/utils/answers/getAnswerClass.ts', 69 | name: 'getAnswerClass.ts', 70 | changeStatus: 'not_modified', 71 | }, 72 | changeStatus: 'not_modified', 73 | }, 74 | { 75 | kind: 'depends_on', 76 | from: { 77 | path: 'src/components/game/cell/Cell.tsx', 78 | name: 'Cell.tsx', 79 | changeStatus: 'not_modified', 80 | }, 81 | to: { 82 | path: 'src/components/game/cell/MemoLayer.tsx', 83 | name: 'MemoLayer.tsx', 84 | changeStatus: 'not_modified', 85 | }, 86 | changeStatus: 'not_modified', 87 | }, 88 | { 89 | kind: 'depends_on', 90 | from: { 91 | path: 'src/components/game/cell/Cell.tsx', 92 | name: 'Cell.tsx', 93 | changeStatus: 'not_modified', 94 | }, 95 | to: { 96 | path: 'src/atoms.ts', 97 | name: 'atoms.ts', 98 | changeStatus: 'not_modified', 99 | }, 100 | changeStatus: 'not_modified', 101 | }, 102 | { 103 | kind: 'depends_on', 104 | from: { 105 | path: 'src/components/game/cell/Cell.tsx', 106 | name: 'Cell.tsx', 107 | changeStatus: 'not_modified', 108 | }, 109 | to: { 110 | path: 'src/components/game/cell/Hoge.ts', 111 | name: 'Hoge.ts', 112 | changeStatus: 'not_modified', 113 | }, 114 | changeStatus: 'not_modified', 115 | }, 116 | { 117 | kind: 'depends_on', 118 | from: { 119 | path: 'src/components/game/GameBoard.tsx', 120 | name: 'GameBoard.tsx', 121 | changeStatus: 'not_modified', 122 | }, 123 | to: { 124 | path: 'src/components/game/cell/Cell.tsx', 125 | name: 'Cell.tsx', 126 | changeStatus: 'not_modified', 127 | }, 128 | changeStatus: 'not_modified', 129 | }, 130 | { 131 | kind: 'depends_on', 132 | from: { 133 | path: 'src/components/game/cell/Cell.stories.tsx', 134 | name: 'Cell.stories.tsx', 135 | changeStatus: 'not_modified', 136 | }, 137 | to: { 138 | path: 'src/components/game/cell/Cell.tsx', 139 | name: 'Cell.tsx', 140 | changeStatus: 'not_modified', 141 | }, 142 | changeStatus: 'not_modified', 143 | }, 144 | { 145 | kind: 'depends_on', 146 | from: { 147 | path: 'src/components/game/cell/Cell.test.tsx', 148 | name: 'Cell.test.tsx', 149 | changeStatus: 'not_modified', 150 | }, 151 | to: { 152 | path: 'src/components/game/cell/Cell.tsx', 153 | name: 'Cell.tsx', 154 | changeStatus: 'not_modified', 155 | }, 156 | changeStatus: 'not_modified', 157 | }, 158 | ], 159 | }, 160 | ), 161 | ).toEqual(['src/components/game/utils/answers']); 162 | }); 163 | 164 | it('深い階層のディレクトリが可能な限り浅い階層で抽象化されること', () => { 165 | expect( 166 | extractAbstractionTarget(extractNoAbstractionDirs(['src/a/a.ts']), { 167 | nodes: [ 168 | { 169 | path: 'src/a/a.ts', 170 | name: 'a.ts', 171 | changeStatus: 'not_modified', 172 | }, 173 | { 174 | path: 'src/a/b/b.ts', 175 | name: 'b.ts', 176 | changeStatus: 'not_modified', 177 | }, 178 | { 179 | path: 'src/a/b/c/c.ts', 180 | name: 'c.ts', 181 | changeStatus: 'not_modified', 182 | }, 183 | { 184 | path: 'src/a/b/c/d/d.ts', 185 | name: 'd.ts', 186 | changeStatus: 'not_modified', 187 | }, 188 | ], 189 | 190 | relations: [], 191 | }), 192 | ).toMatchInlineSnapshot(` 193 | [ 194 | "src/a/b", 195 | "src/a/b/c", 196 | "src/a/b/c/d", 197 | ] 198 | `); 199 | }); 200 | -------------------------------------------------------------------------------- /src/graph/extractAbstractionTarget.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { Graph } from '@ysk8hori/typescript-graph'; 3 | 4 | /** グラフと、抽象化してはいけないファイルのパスから、抽象化して良いディレクトリのパスを取得する */ 5 | export default function extractAbstractionTarget( 6 | noAbstractionDirs: string[], 7 | fullGraph: Graph, 8 | ): string[] { 9 | return ( 10 | fullGraph.nodes 11 | .map(node => path.dirname(node.path)) 12 | .filter(path => path !== '.' && !path.includes('node_modules')) 13 | .filter(path => noAbstractionDirs.every(dir => dir !== path)) 14 | .sort() 15 | // 重複を除去する 16 | .reduce((prev, current) => { 17 | if (!current) return prev; 18 | if (!prev.includes(current)) prev.push(current); 19 | return prev; 20 | }, []) 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/graph/extractAbstractionTargetFromGraphs.test.ts: -------------------------------------------------------------------------------- 1 | import { extractAbstractionTargetFromGraphs } from './extractAbstractionTargetFromGraphs'; 2 | 3 | test('グラフにディレクトリノードが含まれない場合はから配列を返す', () => { 4 | expect( 5 | extractAbstractionTargetFromGraphs( 6 | { 7 | nodes: [ 8 | { 9 | path: 'src/a/a.ts', 10 | name: 'a.ts', 11 | changeStatus: 'not_modified', 12 | }, 13 | { 14 | path: 'src/a/b/b.ts', 15 | name: 'b.ts', 16 | changeStatus: 'not_modified', 17 | }, 18 | { 19 | path: 'src/a/b/c/c.ts', 20 | name: 'c.ts', 21 | changeStatus: 'not_modified', 22 | }, 23 | { 24 | path: 'src/a/b/c/d/d.ts', 25 | name: 'd.ts', 26 | changeStatus: 'not_modified', 27 | }, 28 | ], 29 | 30 | relations: [], 31 | }, 32 | { 33 | nodes: [ 34 | { 35 | path: 'src/a/a.ts', 36 | name: 'a.ts', 37 | changeStatus: 'not_modified', 38 | }, 39 | { 40 | path: 'src/a/b/b.ts', 41 | name: 'b.ts', 42 | changeStatus: 'not_modified', 43 | }, 44 | { 45 | path: 'src/a/b/c/c.ts', 46 | name: 'c.ts', 47 | changeStatus: 'not_modified', 48 | }, 49 | { 50 | path: 'src/a/b/c/d/d.ts', 51 | name: 'd.ts', 52 | changeStatus: 'not_modified', 53 | }, 54 | ], 55 | 56 | relations: [], 57 | }, 58 | ), 59 | ).toEqual([]); 60 | }); 61 | 62 | test('グラフにディレクトリノードが含まれる場合は、そのノードのパスのリストを返す', () => { 63 | expect( 64 | extractAbstractionTargetFromGraphs( 65 | { 66 | nodes: [ 67 | { 68 | path: 'src/a/a.ts', 69 | name: 'a.ts', 70 | changeStatus: 'not_modified', 71 | }, 72 | { 73 | path: 'src/a/b/b.ts', 74 | name: 'b.ts', 75 | changeStatus: 'not_modified', 76 | }, 77 | { 78 | path: 'src/a/b/c/c.ts', 79 | name: 'c.ts', 80 | changeStatus: 'not_modified', 81 | }, 82 | { 83 | path: 'src/a/b/c/d', 84 | name: 'd', 85 | changeStatus: 'not_modified', 86 | isDirectory: true, 87 | }, 88 | ], 89 | relations: [], 90 | }, 91 | { 92 | nodes: [ 93 | { 94 | path: 'src/1/2/3', 95 | name: '3', 96 | changeStatus: 'not_modified', 97 | isDirectory: true, 98 | }, 99 | ], 100 | 101 | relations: [], 102 | }, 103 | ), 104 | ).toEqual(['src/1/2/3', 'src/a/b/c/d']); 105 | }); 106 | -------------------------------------------------------------------------------- /src/graph/extractAbstractionTargetFromGraphs.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | 3 | /** 4 | * graph に含まれる抽象化されたノードのパスのリストを返す 5 | * 6 | * もともとは tsg コマンド生成時にも extractNoAbstractionDirs や extractAbstractionTarget を使って抽出した abstractionTarget を使っていたが、フィルター後の graph に含まれないパスも指定しており、無駄に量が多くなるため必要最低限とする。 7 | */ 8 | export function extractAbstractionTargetFromGraphs(...graphs: Graph[]) { 9 | return ( 10 | graphs 11 | .map(graph => 12 | graph.nodes.filter(node => node.isDirectory).map(node => node.path), 13 | ) 14 | .flat() 15 | .sort() 16 | // 重複を除去する 17 | .reduce((prev, current) => { 18 | if (!current) return prev; 19 | if (!prev.includes(current)) prev.push(current); 20 | return prev; 21 | }, []) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/graph/extractIndexFileDependencies/extractIndexFileDependencies.test.ts: -------------------------------------------------------------------------------- 1 | import type { Node, Relation } from '@ysk8hori/typescript-graph'; 2 | import { extractIndexFileDependencies } from './extractIndexFileDependencies'; 3 | 4 | test('すべて空配列でもエラーとならない', () => { 5 | expect( 6 | extractIndexFileDependencies({ 7 | includeFilePaths: [], 8 | graphs: [{ nodes: [], relations: [] }], 9 | }), 10 | ).toEqual([]); 11 | }); 12 | 13 | const main = { 14 | changeStatus: 'not_modified', 15 | name: 'main.ts', 16 | path: 'src/main.ts', 17 | } satisfies Node; 18 | const indexA = { 19 | changeStatus: 'not_modified', 20 | name: 'index.ts', 21 | path: 'src/a/index.ts', 22 | } satisfies Node; 23 | const a = { 24 | changeStatus: 'not_modified', 25 | name: 'a.ts', 26 | path: 'src/a/a.ts', 27 | } satisfies Node; 28 | const a2 = { 29 | changeStatus: 'not_modified', 30 | name: 'a2.ts', 31 | path: 'src/a/a2.ts', 32 | } satisfies Node; 33 | const indexB = { 34 | changeStatus: 'not_modified', 35 | name: 'index.ts', 36 | path: 'src/b/index.ts', 37 | } satisfies Node; 38 | const b = { 39 | changeStatus: 'not_modified', 40 | name: 'b.ts', 41 | path: 'src/b/b.ts', 42 | } satisfies Node; 43 | const nodes = [main, indexA, a, a2, indexB, b]; 44 | 45 | function relation(from: Node, to: Node): Relation { 46 | return { 47 | from: from, 48 | to: to, 49 | changeStatus: 'not_modified', 50 | kind: 'depends_on', 51 | }; 52 | } 53 | 54 | test('index.ts から参照されるノードが includeFilePath に含まれる場合はその index.ts を返す(通常ケース)', () => { 55 | expect( 56 | extractIndexFileDependencies({ 57 | includeFilePaths: [a.path, a2.path, b.path], 58 | graphs: [ 59 | { 60 | nodes, 61 | relations: [ 62 | // 抽出対象となる relation 63 | relation(indexA, a), 64 | // 抽出対象となる relation 65 | relation(indexA, a2), 66 | // 抽出対象とならない relation 67 | relation(main, b), 68 | ], 69 | }, 70 | ], 71 | }), 72 | ).toEqual([indexA.path]); 73 | }); 74 | 75 | test('includeFilePaths が空配列でもエラーにならない', () => { 76 | expect( 77 | extractIndexFileDependencies({ 78 | includeFilePaths: [], 79 | graphs: [ 80 | { 81 | nodes, 82 | relations: [relation(indexA, a), relation(main, b)], 83 | }, 84 | ], 85 | }), 86 | ).toEqual([]); 87 | }); 88 | 89 | test('includeFilePaths に空文字が含まれていても全ての index.ts が抽出対象になったりはしない', () => { 90 | expect( 91 | extractIndexFileDependencies({ 92 | includeFilePaths: [''], 93 | graphs: [ 94 | { 95 | nodes, 96 | relations: [relation(indexA, a), relation(main, b)], 97 | }, 98 | ], 99 | }), 100 | ).toEqual([]); 101 | }); 102 | 103 | test('graph が空っぽでもエラーにならない', () => { 104 | expect( 105 | extractIndexFileDependencies({ 106 | includeFilePaths: ['a.ts', 'b.ts'], 107 | graphs: [ 108 | { 109 | nodes: [], 110 | relations: [], 111 | }, 112 | ], 113 | }), 114 | ).toEqual([]); 115 | }); 116 | -------------------------------------------------------------------------------- /src/graph/extractIndexFileDependencies/extractIndexFileDependencies.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | import { uniqueString } from '../../utils/reducer'; 3 | 4 | /** 5 | * includes 対象のファイルが同階層の index.ts または index.tsx から参照されている場合、その index.ts のパスのリストを返却する。 6 | */ 7 | export function extractIndexFileDependencies({ 8 | graphs, 9 | includeFilePaths, 10 | }: { 11 | graphs: Graph[]; 12 | includeFilePaths: string[]; 13 | }): string[] { 14 | return includeFilePaths 15 | .map(path => ({ 16 | from: path.split('/').slice(0, -1).join('/').concat('/index.'), 17 | to: path, 18 | })) 19 | .map( 20 | ({ from, to }) => 21 | graphs 22 | .map( 23 | graph => 24 | graph.relations.find( 25 | relation => 26 | relation.from.path.startsWith(from) && 27 | relation.to.path === to, 28 | )?.from.path, 29 | ) 30 | .filter(Boolean)[0], 31 | ) 32 | .reduce(uniqueString, []) 33 | .filter(Boolean); 34 | } 35 | -------------------------------------------------------------------------------- /src/graph/extractIndexFileDependencies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './extractIndexFileDependencies'; 2 | -------------------------------------------------------------------------------- /src/graph/extractNoAbstractionDirs.test.ts: -------------------------------------------------------------------------------- 1 | import extractNoAbstractionDirs from './extractNoAbstractionDirs'; 2 | 3 | it('ファイルのパスのリストから重複のないディレクトリのリストを抽出する', () => { 4 | expect( 5 | extractNoAbstractionDirs([ 6 | '.github/workflows/pre-workflow.yml', 7 | 'src/components/game/cell/Cell.tsx', 8 | 'src/components/game/cell/Hoge.ts', 9 | ]), 10 | ).toEqual([ 11 | '.github', 12 | '.github/workflows', 13 | 'src', 14 | 'src/components', 15 | 'src/components/game', 16 | 'src/components/game/cell', 17 | ]); 18 | }); 19 | -------------------------------------------------------------------------------- /src/graph/extractNoAbstractionDirs.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | /** (本PRで)変更のあったファイルのパスから、抽象化してはいけないディレクトリのリストを作成する */ 4 | export default function extractNoAbstractionDirs(filePaths: string[]) { 5 | return ( 6 | filePaths 7 | .map(file => { 8 | const array = file.split('/'); 9 | // node_modules より深いディレクトリ階層の情報は捨てる 10 | // node_modules 内の node の name はパッケージ名のようなものになっているのでそれで良い 11 | if (array.includes('node_modules')) return 'node_modules'; 12 | 13 | // トップレベルのファイルの場合 14 | if (array.length === 1) return undefined; 15 | 16 | // 末尾のファイル名は不要 17 | return path.join(...array.slice(0, array.length - 1)); 18 | }) 19 | .filter(Boolean) 20 | .sort() 21 | // noAbstractionDirs の重複を除去する 22 | .reduce((prev, current) => { 23 | if (!current) return prev; 24 | if (!prev.includes(current)) prev.push(current); 25 | return prev; 26 | }, []) 27 | .map(dir => { 28 | const directoryPaths: string[] = []; 29 | let currentPath = dir; 30 | while (currentPath !== path.dirname(currentPath)) { 31 | directoryPaths.unshift(currentPath); 32 | currentPath = path.dirname(currentPath); 33 | } 34 | directoryPaths.unshift(currentPath); 35 | return directoryPaths; 36 | }) 37 | .flat() 38 | .filter(dir => dir !== '.') 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/graph/mergeGraphsWithDifferences.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | import { 3 | abstraction, 4 | mergeGraph, 5 | filterGraph, 6 | } from '@ysk8hori/typescript-graph'; 7 | import { pipe } from 'remeda'; 8 | import { log } from '../utils/log'; 9 | import { createTsgCommand } from '../tsg/createTsgCommand'; 10 | import type { Context } from '../utils/context'; 11 | import addStatus from './addStatus'; 12 | import extractAbstractionTarget from './extractAbstractionTarget'; 13 | import extractNoAbstractionDirs from './extractNoAbstractionDirs'; 14 | import updateRelationsStatus from './updateRelationsStatus'; 15 | import { extractAbstractionTargetFromGraphs } from './extractAbstractionTargetFromGraphs'; 16 | import { createIncludeList } from './createIncludeList'; 17 | 18 | /** 2つのグラフからその差分を反映した1つのグラフを生成する */ 19 | export default function mergeGraphsWithDifferences( 20 | fullBaseGraph: Graph, 21 | fullHeadGraph: Graph, 22 | context: Pick, 23 | ) { 24 | const { createdRelations, deletedRelations } = updateRelationsStatus( 25 | fullBaseGraph, 26 | fullHeadGraph, 27 | ); 28 | log('createdRelations:', createdRelations); 29 | log('deletedRelations:', deletedRelations); 30 | 31 | // base と head のグラフをマージする 32 | const mergedGraph = mergeGraph([fullHeadGraph, fullBaseGraph]); 33 | log('mergedGraph.nodes.length:', mergedGraph.nodes.length); 34 | log('mergedGraph.relations.length:', mergedGraph.relations.length); 35 | 36 | const includes = createIncludeList({ context, graphs: [mergedGraph] }); 37 | log('includes:', includes); 38 | 39 | const abstractionTarget = pipe(includes, extractNoAbstractionDirs, dirs => 40 | extractAbstractionTarget(dirs, mergedGraph), 41 | ); 42 | log('abstractionTarget:', abstractionTarget); 43 | 44 | const graph = pipe( 45 | mergedGraph, 46 | graph => filterGraph(includes, context.config.exclude, graph), 47 | graph => ( 48 | log('filteredGraph.nodes.length:', graph.nodes.length), 49 | log('filteredGraph.relations.length:', graph.relations.length), 50 | graph 51 | ), 52 | graph => abstraction(abstractionTarget, graph), 53 | graph => ( 54 | log('abstractedGraph.nodes.length:', graph.nodes.length), 55 | log('abstractedGraph.relations.length:', graph.relations.length), 56 | graph 57 | ), 58 | graph => addStatus(context, graph), 59 | graph => ( 60 | log('graph.nodes.length:', graph.nodes.length), 61 | log('graph.relations.length:', graph.relations.length), 62 | graph 63 | ), 64 | ); 65 | 66 | const tsgCommand = createTsgCommand({ 67 | includes, 68 | abstractions: extractAbstractionTargetFromGraphs(graph), 69 | context, 70 | }); 71 | 72 | return { graph, tsgCommand }; 73 | } 74 | -------------------------------------------------------------------------------- /src/graph/updateRelationsStatus.test.ts: -------------------------------------------------------------------------------- 1 | import type { Graph, Node } from '@ysk8hori/typescript-graph'; 2 | import updateRelationsStatus from './updateRelationsStatus'; 3 | 4 | test('updateRelationsStatus は削除されたリレーションに deleted のステータスを付与する', () => { 5 | const a: Node = { 6 | path: 'src/A.tsx', 7 | name: 'A', 8 | changeStatus: 'not_modified', 9 | }; 10 | const b: Node = { 11 | path: 'src/B.tsx', 12 | name: 'B', 13 | changeStatus: 'not_modified', 14 | }; 15 | const c: Node = { 16 | path: 'src/C.tsx', 17 | name: 'C', 18 | changeStatus: 'not_modified', 19 | }; 20 | const baseGraph: Graph = { 21 | nodes: [a, b, c], 22 | relations: [ 23 | { 24 | from: a, 25 | to: b, 26 | kind: 'depends_on', 27 | changeStatus: 'not_modified', 28 | }, 29 | { 30 | from: a, 31 | to: c, 32 | kind: 'depends_on', 33 | changeStatus: 'not_modified', 34 | }, 35 | ], 36 | }; 37 | const headGraph: Graph = { 38 | nodes: [a, b, c], 39 | relations: [ 40 | { 41 | from: a, 42 | to: b, 43 | kind: 'depends_on', 44 | changeStatus: 'not_modified', 45 | }, 46 | ], 47 | }; 48 | 49 | const { deletedRelations, createdRelations } = updateRelationsStatus( 50 | baseGraph, 51 | headGraph, 52 | ); 53 | 54 | expect(baseGraph).toMatchInlineSnapshot(` 55 | { 56 | "nodes": [ 57 | { 58 | "changeStatus": "not_modified", 59 | "name": "A", 60 | "path": "src/A.tsx", 61 | }, 62 | { 63 | "changeStatus": "not_modified", 64 | "name": "B", 65 | "path": "src/B.tsx", 66 | }, 67 | { 68 | "changeStatus": "not_modified", 69 | "name": "C", 70 | "path": "src/C.tsx", 71 | }, 72 | ], 73 | "relations": [ 74 | { 75 | "changeStatus": "not_modified", 76 | "from": { 77 | "changeStatus": "not_modified", 78 | "name": "A", 79 | "path": "src/A.tsx", 80 | }, 81 | "kind": "depends_on", 82 | "to": { 83 | "changeStatus": "not_modified", 84 | "name": "B", 85 | "path": "src/B.tsx", 86 | }, 87 | }, 88 | { 89 | "changeStatus": "deleted", 90 | "from": { 91 | "changeStatus": "not_modified", 92 | "name": "A", 93 | "path": "src/A.tsx", 94 | }, 95 | "kind": "depends_on", 96 | "to": { 97 | "changeStatus": "not_modified", 98 | "name": "C", 99 | "path": "src/C.tsx", 100 | }, 101 | }, 102 | ], 103 | } 104 | `); 105 | expect(deletedRelations).toEqual([ 106 | { 107 | changeStatus: 'deleted', 108 | from: a, 109 | kind: 'depends_on', 110 | to: c, 111 | }, 112 | ]); 113 | expect(createdRelations).toEqual([]); 114 | }); 115 | 116 | test('updateRelationsStatus は作成されたリレーションに created のステータスを付与する', () => { 117 | const a: Node = { 118 | path: 'src/A.tsx', 119 | name: 'A', 120 | changeStatus: 'not_modified', 121 | }; 122 | const b: Node = { 123 | path: 'src/B.tsx', 124 | name: 'B', 125 | changeStatus: 'not_modified', 126 | }; 127 | const c: Node = { 128 | path: 'src/C.tsx', 129 | name: 'C', 130 | changeStatus: 'not_modified', 131 | }; 132 | const baseGraph: Graph = { 133 | nodes: [a, b], 134 | relations: [ 135 | { 136 | from: a, 137 | to: b, 138 | kind: 'depends_on', 139 | changeStatus: 'not_modified', 140 | }, 141 | ], 142 | }; 143 | const headGraph: Graph = { 144 | nodes: [a, b, c], 145 | relations: [ 146 | { 147 | from: a, 148 | to: b, 149 | kind: 'depends_on', 150 | changeStatus: 'not_modified', 151 | }, 152 | { 153 | from: a, 154 | to: c, 155 | kind: 'depends_on', 156 | changeStatus: 'not_modified', 157 | }, 158 | ], 159 | }; 160 | 161 | const { deletedRelations, createdRelations } = updateRelationsStatus( 162 | baseGraph, 163 | headGraph, 164 | ); 165 | 166 | expect(headGraph).toMatchInlineSnapshot(` 167 | { 168 | "nodes": [ 169 | { 170 | "changeStatus": "not_modified", 171 | "name": "A", 172 | "path": "src/A.tsx", 173 | }, 174 | { 175 | "changeStatus": "not_modified", 176 | "name": "B", 177 | "path": "src/B.tsx", 178 | }, 179 | { 180 | "changeStatus": "not_modified", 181 | "name": "C", 182 | "path": "src/C.tsx", 183 | }, 184 | ], 185 | "relations": [ 186 | { 187 | "changeStatus": "not_modified", 188 | "from": { 189 | "changeStatus": "not_modified", 190 | "name": "A", 191 | "path": "src/A.tsx", 192 | }, 193 | "kind": "depends_on", 194 | "to": { 195 | "changeStatus": "not_modified", 196 | "name": "B", 197 | "path": "src/B.tsx", 198 | }, 199 | }, 200 | { 201 | "changeStatus": "created", 202 | "from": { 203 | "changeStatus": "not_modified", 204 | "name": "A", 205 | "path": "src/A.tsx", 206 | }, 207 | "kind": "depends_on", 208 | "to": { 209 | "changeStatus": "not_modified", 210 | "name": "C", 211 | "path": "src/C.tsx", 212 | }, 213 | }, 214 | ], 215 | } 216 | `); 217 | expect(deletedRelations).toEqual([]); 218 | expect(createdRelations).toEqual([ 219 | { 220 | changeStatus: 'created', 221 | from: a, 222 | kind: 'depends_on', 223 | to: c, 224 | }, 225 | ]); 226 | }); 227 | -------------------------------------------------------------------------------- /src/graph/updateRelationsStatus.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Graph, 3 | RelationOfDependsOn, 4 | Relation, 5 | } from '@ysk8hori/typescript-graph'; 6 | import { isSameRelation } from '@ysk8hori/typescript-graph'; 7 | import { pipe, filter, forEach } from 'remeda'; 8 | 9 | /** 削除された Relation にマークをつける */ 10 | export default function updateRelationsStatus( 11 | baseGraph: Graph, 12 | headGraph: Graph, 13 | ) { 14 | const createdRelations = pipe( 15 | headGraph.relations, 16 | filter(isRelationOfDependsOn), 17 | filter( 18 | headRelation => 19 | !baseGraph.relations.some(baseRelation => 20 | isSameRelation(baseRelation, headRelation), 21 | ), 22 | ), 23 | forEach(relation => (relation.changeStatus = 'created')), 24 | ); 25 | const deletedRelations = pipe( 26 | baseGraph.relations, 27 | filter(isRelationOfDependsOn), 28 | filter( 29 | baseRelation => 30 | !headGraph.relations.some(headRelation => 31 | isSameRelation(baseRelation, headRelation), 32 | ), 33 | ), 34 | forEach(relation => (relation.changeStatus = 'deleted')), 35 | ); 36 | return { deletedRelations, createdRelations }; 37 | } 38 | 39 | function isRelationOfDependsOn( 40 | relation: Relation, 41 | ): relation is RelationOfDependsOn { 42 | return relation.kind === 'depends_on'; 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@ysk8hori/typescript-graph'; 2 | import { getFullGraph } from './getFullGraph'; 3 | import { info, log } from './utils/log'; 4 | import type { PullRequestFileInfo } from './utils/github'; 5 | import { createContext } from './utils/context'; 6 | import { build2GraphsMessage } from './graph/build2GraphsMessage'; 7 | import { buildGraphMessage } from './graph/buildGraphMessage'; 8 | import { buildMetricsMessage } from './metrics/buildMetricsMessage'; 9 | 10 | async function makeGraph() { 11 | const context = await createContext(); 12 | const { modified, created, deleted, renamed } = context.filesChanged; 13 | // 以下の *_files は src/index.ts のようなパス文字列になっている 14 | log('modified:', modified); 15 | log('created:', created); 16 | log('deleted:', deleted); 17 | log('renamed:', renamed); 18 | const allModifiedFiles = [modified, created, deleted, renamed].flat(); 19 | 20 | // .tsファイルの変更がある場合のみ Graph を生成する。コンパイル対象外の ts ファイルもあるかもしれないがわからないので気にしない 21 | if (allModifiedFiles.length === 0) { 22 | await context.github.deleteComment(context.config.commentTitle); 23 | info('No TypeScript files were changed.'); 24 | return; 25 | } 26 | 27 | const { fullHeadGraph, fullBaseGraph, traverserForHead, traverserForBase } = 28 | getFullGraph(context); 29 | log('fullBaseGraph.nodes.length:', fullBaseGraph.nodes.length); 30 | log('fullBaseGraph.relations.length:', fullBaseGraph.relations.length); 31 | log('fullHeadGraph.nodes.length:', fullHeadGraph.nodes.length); 32 | log('fullHeadGraph.relations.length:', fullHeadGraph.relations.length); 33 | 34 | // head のグラフが空の場合は何もしない 35 | if (fullHeadGraph.nodes.length === 0) return; 36 | 37 | let message = ''; 38 | 39 | if (deleted.length !== 0 || hasRenamedFiles(fullHeadGraph, renamed)) { 40 | // ファイルの削除またはリネームがある場合は Graph を2つ表示する 41 | message += build2GraphsMessage(fullBaseGraph, fullHeadGraph, context); 42 | } else { 43 | message += buildGraphMessage(fullBaseGraph, fullHeadGraph, context); 44 | } 45 | 46 | buildMetricsMessage({ 47 | context, 48 | traverserForHead, 49 | traverserForBase, 50 | allModifiedFiles, 51 | write: str => (message += str), 52 | }); 53 | 54 | await context.github.commentToPR(context.fullCommentTitle, message); 55 | } 56 | 57 | makeGraph().catch(err => { 58 | info('Error in delta-typescript-graph-action: ', err); 59 | }); 60 | 61 | function hasRenamedFiles(fullHeadGraph: Graph, renamed: PullRequestFileInfo[]) { 62 | return fullHeadGraph.nodes.some(headNode => 63 | renamed?.map(({ filename }) => filename).includes(headNode.path), 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/metrics/buildMetricsMessage.ts: -------------------------------------------------------------------------------- 1 | import type { ProjectTraverser } from '@ysk8hori/typescript-graph'; 2 | import { calculateCodeMetrics } from '@ysk8hori/typescript-graph/feature/metric/calculateCodeMetrics.js'; 3 | import type { 4 | CodeMetrics, 5 | Score, 6 | } from '@ysk8hori/typescript-graph/feature/metric/metricsModels.js'; 7 | import { pipe } from 'remeda'; 8 | import { unTree } from '@ysk8hori/typescript-graph/utils/Tree.js'; 9 | import type { PullRequestFileInfo } from '../utils/github'; 10 | import type { Context } from '../utils/context'; 11 | import { createScoreDiff } from './createScoreDiff'; 12 | import { round } from './round'; 13 | import { formatAndOutputMetrics } from './formatAndOutputMetrics'; 14 | 15 | export function buildMetricsMessage({ 16 | context, 17 | traverserForHead, 18 | traverserForBase, 19 | allModifiedFiles, 20 | write, 21 | }: { 22 | context: Context; 23 | traverserForHead: ProjectTraverser | undefined; 24 | traverserForBase: ProjectTraverser | undefined; 25 | allModifiedFiles: PullRequestFileInfo[]; 26 | write: (str: string) => void; 27 | }): void { 28 | if ( 29 | !context.config.showMetrics || 30 | !traverserForHead || 31 | !traverserForBase || 32 | allModifiedFiles.length === 0 33 | ) { 34 | return; 35 | } 36 | 37 | const baseMetrics = generateScoreMetrics(traverserForBase, allModifiedFiles); 38 | const headMetrics = generateScoreMetrics(traverserForHead, allModifiedFiles); 39 | 40 | // メトリクスの差分を計算 41 | const { metricsMap, sortedKeys } = createScoreDiff(headMetrics, baseMetrics); 42 | 43 | // 変更対象ファイルがトラバース対象に含まれない場合はメトリクスを出力しない 44 | if (sortedKeys.length === 0) return; 45 | 46 | write('## Metrics\n\n'); 47 | // メトリクスの差分をファイルごとに書き込む 48 | formatAndOutputMetrics(sortedKeys, metricsMap, write); 49 | return; 50 | } 51 | 52 | function generateScoreMetrics( 53 | traverser: ProjectTraverser, 54 | allModifiedFiles: PullRequestFileInfo[], 55 | ) { 56 | return pipe( 57 | calculateCodeMetrics({ metrics: true }, traverser, filePath => 58 | allModifiedFiles.map(v => v.filename).includes(filePath), 59 | ), 60 | unTree, 61 | toScoreFilteredMetrics, 62 | toScoreRoundedMetrics, 63 | ); 64 | } 65 | 66 | function toScoreFilteredMetrics(metrics: CodeMetrics[]): CodeMetrics[] { 67 | return metrics.map(metric => ({ 68 | ...metric, 69 | scores: metric.scores.filter(score => 70 | [ 71 | 'Maintainability Index', 72 | 'Cognitive Complexity', 73 | 'semantic syntax volume', 74 | ].includes(score.name), 75 | ), 76 | })); 77 | } 78 | 79 | function toScoreRoundedMetrics(metrics: CodeMetrics[]): CodeMetrics[] { 80 | return metrics.map(metric => ({ 81 | ...metric, 82 | scores: metric.scores.map( 83 | score => 84 | ({ 85 | ...score, 86 | value: round(score.value), 87 | }) satisfies Score, 88 | ), 89 | })); 90 | } 91 | -------------------------------------------------------------------------------- /src/metrics/createScoreDiff.test.ts: -------------------------------------------------------------------------------- 1 | import type { CodeMetrics } from '@ysk8hori/typescript-graph/feature/metric/metricsModels.js'; 2 | import type { FlattenMatericsWithDiff } from './createScoreDiff'; 3 | import { createScoreDiff } from './createScoreDiff'; 4 | 5 | test('差分がない場合', () => { 6 | const baseFileData = [ 7 | { 8 | filePath: 'a.ts', 9 | name: '-', 10 | scope: 'file', 11 | scores: [ 12 | { name: '1', betterDirection: 'none', state: 'normal', value: 50 }, 13 | ], 14 | }, 15 | ] satisfies CodeMetrics[]; 16 | const headFileData = [ 17 | { 18 | filePath: 'a.ts', 19 | name: '-', 20 | scope: 'file', 21 | scores: [ 22 | { name: '1', betterDirection: 'none', state: 'normal', value: 50 }, 23 | ], 24 | }, 25 | ] satisfies CodeMetrics[]; 26 | const { metricsMap, sortedKeys } = createScoreDiff( 27 | headFileData, 28 | baseFileData, 29 | ); 30 | expect(sortedKeys).toEqual(['a.ts']); 31 | expect(metricsMap.get('a.ts')).toEqual([ 32 | { 33 | filePath: 'a.ts', 34 | key: 'a.ts-', 35 | name: '-', 36 | scope: 'file', 37 | scores: [ 38 | { 39 | name: '1', 40 | betterDirection: 'none', 41 | state: 'normal', 42 | value: 50, 43 | diff: 0, 44 | diffStr: undefined, 45 | }, 46 | ], 47 | status: 'updated', 48 | }, 49 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 50 | }); 51 | 52 | test('追加された場合', () => { 53 | const baseFileData = [] satisfies CodeMetrics[]; 54 | const headFileData = [ 55 | { 56 | filePath: 'a.ts', 57 | name: '-', 58 | scope: 'file', 59 | scores: [ 60 | { name: '1', betterDirection: 'none', state: 'normal', value: 50 }, 61 | ], 62 | }, 63 | ] satisfies CodeMetrics[]; 64 | const { metricsMap, sortedKeys } = createScoreDiff( 65 | headFileData, 66 | baseFileData, 67 | ); 68 | expect(sortedKeys).toEqual(['a.ts']); 69 | expect(metricsMap.get('a.ts')).toEqual([ 70 | { 71 | filePath: 'a.ts', 72 | key: 'a.ts-', 73 | name: '-', 74 | scope: 'file', 75 | scores: [ 76 | { 77 | name: '1', 78 | betterDirection: 'none', 79 | state: 'normal', 80 | value: 50, 81 | diff: undefined, 82 | diffStr: undefined, 83 | }, 84 | ], 85 | status: 'added', 86 | }, 87 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 88 | }); 89 | 90 | test('削除された場合', () => { 91 | const baseFileData = [ 92 | { 93 | filePath: 'a.ts', 94 | name: '-', 95 | scope: 'file', 96 | scores: [ 97 | { name: '1', betterDirection: 'none', state: 'normal', value: 50 }, 98 | ], 99 | }, 100 | ] satisfies CodeMetrics[]; 101 | const headFileData = [] satisfies CodeMetrics[]; 102 | const { metricsMap, sortedKeys } = createScoreDiff( 103 | headFileData, 104 | baseFileData, 105 | ); 106 | expect(sortedKeys).toEqual(['a.ts']); 107 | expect(metricsMap.get('a.ts')).toEqual([ 108 | { 109 | filePath: 'a.ts', 110 | key: 'a.ts-', 111 | name: '-', 112 | scope: 'file', 113 | scores: [ 114 | { 115 | name: '1', 116 | betterDirection: 'none', 117 | state: 'normal', 118 | value: 50, 119 | diff: undefined, 120 | diffStr: undefined, 121 | }, 122 | ], 123 | status: 'deleted', 124 | }, 125 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 126 | }); 127 | 128 | describe('更新された場合', () => { 129 | test('betterDirection が none の場合は diffStr なし', () => { 130 | const baseFileData = [ 131 | { 132 | filePath: 'a.ts', 133 | name: '-', 134 | scope: 'file', 135 | scores: [ 136 | { name: '1', betterDirection: 'none', state: 'normal', value: 40 }, 137 | ], 138 | }, 139 | ] satisfies CodeMetrics[]; 140 | const headFileData = [ 141 | { 142 | filePath: 'a.ts', 143 | name: '-', 144 | scope: 'file', 145 | scores: [ 146 | { name: '1', betterDirection: 'none', state: 'normal', value: 50 }, 147 | ], 148 | }, 149 | ] satisfies CodeMetrics[]; 150 | const { metricsMap, sortedKeys } = createScoreDiff( 151 | headFileData, 152 | baseFileData, 153 | ); 154 | expect(sortedKeys).toEqual(['a.ts']); 155 | expect(metricsMap.get('a.ts')).toEqual([ 156 | { 157 | filePath: 'a.ts', 158 | key: 'a.ts-', 159 | name: '-', 160 | scope: 'file', 161 | scores: [ 162 | { 163 | name: '1', 164 | betterDirection: 'none', 165 | state: 'normal', 166 | value: 50, 167 | diff: 10, 168 | diffStr: undefined, 169 | }, 170 | ], 171 | status: 'updated', 172 | }, 173 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 174 | }); 175 | 176 | test('betterDirection が higher かつ value が上昇の場合は +10', () => { 177 | const baseFileData = [ 178 | { 179 | filePath: 'a.ts', 180 | name: '-', 181 | scope: 'file', 182 | scores: [ 183 | { name: '1', betterDirection: 'higher', state: 'normal', value: 40 }, 184 | ], 185 | }, 186 | ] satisfies CodeMetrics[]; 187 | const headFileData = [ 188 | { 189 | filePath: 'a.ts', 190 | name: '-', 191 | scope: 'file', 192 | scores: [ 193 | { name: '1', betterDirection: 'higher', state: 'normal', value: 50 }, 194 | ], 195 | }, 196 | ] satisfies CodeMetrics[]; 197 | const { metricsMap, sortedKeys } = createScoreDiff( 198 | headFileData, 199 | baseFileData, 200 | ); 201 | expect(sortedKeys).toEqual(['a.ts']); 202 | expect(metricsMap.get('a.ts')).toEqual([ 203 | { 204 | filePath: 'a.ts', 205 | key: 'a.ts-', 206 | name: '-', 207 | scope: 'file', 208 | scores: [ 209 | { 210 | name: '1', 211 | betterDirection: 'higher', 212 | state: 'normal', 213 | value: 50, 214 | diff: 10, 215 | diffStr: '+10', 216 | }, 217 | ], 218 | status: 'updated', 219 | }, 220 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 221 | }); 222 | 223 | test('betterDirection が higher かつ value が下降の場合は 🔻-10', () => { 224 | const baseFileData = [ 225 | { 226 | filePath: 'a.ts', 227 | name: '-', 228 | scope: 'file', 229 | scores: [ 230 | { name: '1', betterDirection: 'higher', state: 'normal', value: 50 }, 231 | ], 232 | }, 233 | ] satisfies CodeMetrics[]; 234 | const headFileData = [ 235 | { 236 | filePath: 'a.ts', 237 | name: '-', 238 | scope: 'file', 239 | scores: [ 240 | { name: '1', betterDirection: 'higher', state: 'normal', value: 40 }, 241 | ], 242 | }, 243 | ] satisfies CodeMetrics[]; 244 | const { metricsMap, sortedKeys } = createScoreDiff( 245 | headFileData, 246 | baseFileData, 247 | ); 248 | expect(sortedKeys).toEqual(['a.ts']); 249 | expect(metricsMap.get('a.ts')).toEqual([ 250 | { 251 | filePath: 'a.ts', 252 | key: 'a.ts-', 253 | name: '-', 254 | scope: 'file', 255 | scores: [ 256 | { 257 | name: '1', 258 | betterDirection: 'higher', 259 | state: 'normal', 260 | value: 40, 261 | diff: -10, 262 | diffStr: '🔻-10', 263 | }, 264 | ], 265 | status: 'updated', 266 | }, 267 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 268 | }); 269 | 270 | test('betterDirection が lower かつ value が上昇の場合は 🔺+10', () => { 271 | const baseFileData = [ 272 | { 273 | filePath: 'a.ts', 274 | name: '-', 275 | scope: 'file', 276 | scores: [ 277 | { name: '1', betterDirection: 'lower', state: 'normal', value: 40 }, 278 | ], 279 | }, 280 | ] satisfies CodeMetrics[]; 281 | const headFileData = [ 282 | { 283 | filePath: 'a.ts', 284 | name: '-', 285 | scope: 'file', 286 | scores: [ 287 | { name: '1', betterDirection: 'lower', state: 'normal', value: 50 }, 288 | ], 289 | }, 290 | ] satisfies CodeMetrics[]; 291 | const { metricsMap, sortedKeys } = createScoreDiff( 292 | headFileData, 293 | baseFileData, 294 | ); 295 | expect(sortedKeys).toEqual(['a.ts']); 296 | expect(metricsMap.get('a.ts')).toEqual([ 297 | { 298 | filePath: 'a.ts', 299 | key: 'a.ts-', 300 | name: '-', 301 | scope: 'file', 302 | scores: [ 303 | { 304 | name: '1', 305 | betterDirection: 'lower', 306 | state: 'normal', 307 | value: 50, 308 | diff: 10, 309 | diffStr: '🔺+10', 310 | }, 311 | ], 312 | status: 'updated', 313 | }, 314 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 315 | }); 316 | 317 | test('betterDirection が lower かつ value が下降の場合は -10', () => { 318 | const baseFileData = [ 319 | { 320 | filePath: 'a.ts', 321 | name: '-', 322 | scope: 'file', 323 | scores: [ 324 | { name: '1', betterDirection: 'lower', state: 'normal', value: 50 }, 325 | ], 326 | }, 327 | ] satisfies CodeMetrics[]; 328 | const headFileData = [ 329 | { 330 | filePath: 'a.ts', 331 | name: '-', 332 | scope: 'file', 333 | scores: [ 334 | { name: '1', betterDirection: 'lower', state: 'normal', value: 40 }, 335 | ], 336 | }, 337 | ] satisfies CodeMetrics[]; 338 | const { metricsMap, sortedKeys } = createScoreDiff( 339 | headFileData, 340 | baseFileData, 341 | ); 342 | expect(sortedKeys).toEqual(['a.ts']); 343 | expect(metricsMap.get('a.ts')).toEqual([ 344 | { 345 | filePath: 'a.ts', 346 | key: 'a.ts-', 347 | name: '-', 348 | scope: 'file', 349 | scores: [ 350 | { 351 | name: '1', 352 | betterDirection: 'lower', 353 | state: 'normal', 354 | value: 40, 355 | diff: -10, 356 | diffStr: '-10', 357 | }, 358 | ], 359 | status: 'updated', 360 | }, 361 | ] satisfies (FlattenMatericsWithDiff & { key: string })[]); 362 | }); 363 | }); 364 | -------------------------------------------------------------------------------- /src/metrics/createScoreDiff.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CodeMetrics, 3 | Score, 4 | } from '@ysk8hori/typescript-graph/feature/metric/metricsModels.js'; 5 | import { zip } from 'remeda'; 6 | import { round } from './round'; 7 | 8 | type ScoreWithDiff = Score & { 9 | diff?: number; 10 | diffStr?: string; 11 | }; 12 | 13 | /** visible for testing */ 14 | export type FlattenMatericsWithDiff = Omit & { 15 | scores: ScoreWithDiff[]; 16 | status: 'added' | 'deleted' | 'updated'; 17 | }; 18 | export function createScoreDiff( 19 | headFileData: CodeMetrics[], 20 | baseFileData: CodeMetrics[], 21 | ): { 22 | metricsMap: Map; 23 | sortedKeys: string[]; 24 | } { 25 | const headFileDataWithKey = headFileData.map(data => ({ 26 | ...data, 27 | key: data.filePath + data.name, 28 | })); 29 | const baseFileDataWithKey = baseFileData.map(data => ({ 30 | ...data, 31 | key: data.filePath + data.name, 32 | })); 33 | 34 | const scoresWithDiffMap = new Map(); 35 | for (const headData of headFileDataWithKey) { 36 | const baseData = baseFileDataWithKey.find( 37 | data => data.key === headData.key, 38 | ); 39 | 40 | if (!baseData) { 41 | scoresWithDiffMap.set(headData.key, { ...headData, status: 'added' }); 42 | continue; 43 | } 44 | 45 | // scores の中身は同じ順番であることが前提 46 | const scores = calculateScoreDifferences(headData, baseData); 47 | 48 | scoresWithDiffMap.set(headData.key, { 49 | ...headData, 50 | scores, 51 | status: 'updated', 52 | }); 53 | } 54 | 55 | for (const baseData of baseFileDataWithKey) { 56 | if (!scoresWithDiffMap.has(baseData.key)) { 57 | scoresWithDiffMap.set(baseData.key, { ...baseData, status: 'deleted' }); 58 | } 59 | } 60 | 61 | const array = Array.from(scoresWithDiffMap.values()); 62 | const metricsMap = array.reduce((map, currentValue) => { 63 | const filePath = currentValue.filePath; 64 | if (!map.has(filePath)) { 65 | map.set(filePath, []); 66 | } 67 | map.get(filePath)?.push(currentValue); 68 | return map; 69 | }, new Map()); 70 | const sortedKeys = getSortedKeys(metricsMap); 71 | return { metricsMap, sortedKeys }; 72 | } 73 | 74 | function calculateScoreDifferences( 75 | headData: CodeMetrics, 76 | baseData: CodeMetrics, 77 | ): ScoreWithDiff[] { 78 | const zipped = zip(headData.scores, baseData.scores); 79 | const scores = zipped.map(([headScore, baseScore]) => { 80 | const diff = round(round(headScore.value) - round(baseScore.value)); 81 | return { 82 | ...headScore, 83 | diff, 84 | diffStr: getChalkedDiff(headScore.betterDirection, diff), 85 | }; 86 | }); 87 | return scores; 88 | } 89 | function getChalkedDiff( 90 | betterDirection: Score['betterDirection'], 91 | diff: number | undefined, 92 | ): string | undefined { 93 | if (diff === undefined) return ''; 94 | if (betterDirection === 'lower' && diff < 0) return `${diff}`; 95 | if (betterDirection === 'lower' && 0 < diff) return `🔺+${diff}`; 96 | if (betterDirection === 'higher' && diff < 0) return `🔻${diff}`; 97 | if (betterDirection === 'higher' && 0 < diff) return `+${diff}`; 98 | return undefined; 99 | } 100 | 101 | function getSortedKeys(map: Map) { 102 | const keys = Array.from(map.keys()); 103 | const sortedKeys = keys 104 | .map(key => map.get(key)) 105 | .filter(Boolean) 106 | .map(fileGroupedMetrics => fileGroupedMetrics.find(m => m.scope === 'file')) 107 | .filter(Boolean) 108 | .toSorted((a, b) => (a.scores[0]?.value ?? 0) - (b.scores[0]?.value ?? 0)) 109 | .map(m => m?.filePath); 110 | return sortedKeys; 111 | } 112 | -------------------------------------------------------------------------------- /src/metrics/formatAndOutputMetrics.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CodeMetrics, 3 | Score, 4 | } from '@ysk8hori/typescript-graph/feature/metric/metricsModels.js'; 5 | import { getIconByState } from '@ysk8hori/typescript-graph/feature/metric/functions/getIconByState.js'; 6 | 7 | /** メトリクスの差分をファイルごとに書き込む */ 8 | export function formatAndOutputMetrics( 9 | sortedKeys: string[], 10 | metricsMap: Map< 11 | string, 12 | (Omit & { 13 | scores: (Score & { diff?: number; diffStr?: string })[]; 14 | status: 'added' | 'deleted' | 'updated'; 15 | })[] 16 | >, 17 | write: (str: string) => void, 18 | ) { 19 | for (const filePath of sortedKeys) { 20 | const scoreTitles = 21 | metricsMap 22 | .values() 23 | .next() 24 | .value?.[0].scores.map(score => score.name) ?? []; 25 | if (scoreTitles.length === 0) continue; 26 | const metrics = metricsMap.get(filePath); 27 | if (!metrics) continue; 28 | const isNewFile = metrics[0]?.status === 'added'; 29 | write(`### ${isNewFile ? '🆕 ' : ''}${filePath}\n\n`); 30 | 31 | if (metrics.length === 0 || metrics[0].status === 'deleted') { 32 | write('🗑️ This file has been deleted.\n\n'); 33 | continue; 34 | } 35 | 36 | // メトリクスのヘッダー 37 | write(`name | scope | ` + scoreTitles.join(' | ') + '\n'); 38 | 39 | // メトリクスのヘッダーの区切り 40 | write(`-- | -- | ` + scoreTitles.map(() => '--:').join(' | ') + '\n'); 41 | 42 | // メトリクスの本体 43 | for (const metric of metrics) { 44 | write( 45 | `${formatMetricName(metric, isNewFile)} | ${metric.scope} | ` + 46 | metric.scores.map(formatScore).join(' | ') + 47 | '\n', 48 | ); 49 | } 50 | write('\n\n'); 51 | } 52 | } 53 | 54 | function formatScore( 55 | score: Score & { diff?: number; diffStr?: string }, 56 | ): string { 57 | return `${ 58 | score.diffStr 59 | ? // 全角カッコを使うことで余白を取っている 60 | `(${score.diffStr})` 61 | : '' 62 | }${getIconByState(score.state)}${score.value}`; 63 | } 64 | 65 | function formatMetricName( 66 | metric: Omit & { 67 | scores: (Score & { diff?: number; diffStr?: string })[]; 68 | status: 'added' | 'deleted' | 'updated'; 69 | }, 70 | isNewFile: boolean, 71 | ) { 72 | return metric.scope === 'file' 73 | ? '~' 74 | : `${metric.status === 'added' && !isNewFile ? `🆕 ${metric.name}` : metric.status === 'deleted' ? `🗑️ ~~${metric.name}~~` : metric.name}`; 75 | } 76 | -------------------------------------------------------------------------------- /src/metrics/round.ts: -------------------------------------------------------------------------------- 1 | export function round(value: number) { 2 | return Math.round(value * 100) / 100; 3 | } 4 | -------------------------------------------------------------------------------- /src/reset.d.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset'; 2 | -------------------------------------------------------------------------------- /src/tsg/createTsgCommand.test.ts: -------------------------------------------------------------------------------- 1 | import { getDummyContext } from '../utils/dummyContext'; 2 | import { createTsgCommand } from './createTsgCommand'; 3 | 4 | test('何の指定もない場合は tsg を出力する', () => { 5 | expect( 6 | createTsgCommand({ 7 | includes: [], 8 | abstractions: [], 9 | context: getDummyContext({ configExclude: [] }), 10 | }), 11 | ).toBe('tsg'); 12 | }); 13 | 14 | test('includes を指定した場合は `--include` と `--highlight` を出力する', () => { 15 | expect( 16 | createTsgCommand({ 17 | includes: ['a.ts', 'b.tsx'], 18 | abstractions: [], 19 | context: getDummyContext({ configExclude: [] }), 20 | }), 21 | ).toBe('tsg --include a.ts b.tsx --highlight a.ts b.tsx'); 22 | }); 23 | 24 | test('abstractions を指定した場合は `--abstraction` を出力する', () => { 25 | expect( 26 | createTsgCommand({ 27 | includes: [], 28 | abstractions: ['a', 'b'], 29 | context: getDummyContext({ configExclude: [] }), 30 | }), 31 | ).toBe('tsg --abstraction a b'); 32 | }); 33 | 34 | test('exclude を指定した場合は `--exclude` を出力する', () => { 35 | expect( 36 | createTsgCommand({ 37 | includes: [], 38 | abstractions: [], 39 | context: getDummyContext({ configExclude: ['a.ts', 'b.tsx'] }), 40 | }), 41 | ).toBe('tsg --exclude a.ts b.tsx'); 42 | }); 43 | 44 | test('tsconfig を指定した場合は `--tsconfig` を出力する', () => { 45 | expect( 46 | createTsgCommand({ 47 | includes: [], 48 | abstractions: [], 49 | context: getDummyContext({ 50 | configTsconfig: './my-app/tsconfig.json', 51 | configExclude: [], 52 | }), 53 | }), 54 | ).toBe('tsg --tsconfig ./my-app/tsconfig.json'); 55 | }); 56 | 57 | test('tsconfig を指定した場合は include 等のパスを tsconfig からの相対パスに変換する', () => { 58 | expect( 59 | createTsgCommand({ 60 | includes: ['my-app/src/a.ts'], 61 | abstractions: ['my-app/src/c.ts'], 62 | context: getDummyContext({ 63 | configTsconfig: './my-app/tsconfig.json', 64 | configExclude: ['my-app/src/e.ts'], 65 | }), 66 | }), 67 | ).toBe( 68 | 'tsg --include src/a.ts --highlight src/a.ts --exclude src/e.ts --abstraction src/c.ts --tsconfig ./my-app/tsconfig.json', 69 | ); 70 | }); 71 | -------------------------------------------------------------------------------- /src/tsg/createTsgCommand.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { Context } from '../utils/context'; 3 | 4 | /** 5 | * コメントに出力する tsg コマンドを生成する 6 | * 7 | * このコマンドは、厳密にはコメント中に出力するグラフとは異なる結果となるものであるが、あると便利と思われるので出力している。 8 | */ 9 | export function createTsgCommand({ 10 | includes, 11 | abstractions, 12 | context, 13 | }: { 14 | includes: string[]; 15 | abstractions: string[]; 16 | context: Pick; 17 | }) { 18 | const tsconfigRoot = context.config.tsconfig 19 | ? path 20 | .relative('./', path.resolve(context.config.tsconfig)) 21 | ?.split('/') 22 | .slice(0, -1) 23 | .join('/') 24 | .concat('/') 25 | : undefined; // context.config.tsconfigRoot は一旦考えない 26 | // TODO: tsg は tsconfig から相対パスでファイルパスを出力しないようになったら、以下の関数は使用しないようにする。 27 | // 現状、tsg コマンドで指定可能なファイルのパスは、--tsconfig からの相対パスとなる。 28 | // しかし、Delta TypeScript Graph Action においてはリポジトリのルートからの相対パスで指定になっている。 29 | // そのため、ここで入力された includes 等々のパスは tsg に合わせて tsconfig からの相対パスに変換する。 30 | function convertToRelatedPathFromTsconfig(path: string) { 31 | if (!tsconfigRoot) return path; 32 | return path.replace(new RegExp(`^${tsconfigRoot}`), ''); 33 | } 34 | 35 | const includeOption = 36 | includes.length === 0 37 | ? '' 38 | : `--include ${includes.map(convertToRelatedPathFromTsconfig).join(' ')}`; 39 | const highlightOption = 40 | includes.length === 0 41 | ? '' 42 | : `--highlight ${includes.map(convertToRelatedPathFromTsconfig).join(' ')}`; 43 | const excludeOption = 44 | context.config.exclude.length === 0 45 | ? '' 46 | : `--exclude ${context.config.exclude.map(convertToRelatedPathFromTsconfig).join(' ')}`; 47 | const abstractionOption = 48 | abstractions.length === 0 49 | ? '' 50 | : `--abstraction ${abstractions.map(convertToRelatedPathFromTsconfig).join(' ')}`; 51 | const tsconfig = context.config.tsconfig 52 | ? `--tsconfig ${context.config.tsconfig}` 53 | : ''; 54 | return [ 55 | 'tsg', 56 | includeOption, 57 | highlightOption, 58 | excludeOption, 59 | abstractionOption, 60 | tsconfig, 61 | ] 62 | .filter(Boolean) 63 | .join(' ') 64 | .trim(); 65 | } 66 | -------------------------------------------------------------------------------- /src/tsg/getCreateGraphsArguments.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { getCreateGraphsArguments } from './getCreateGraphsArguments'; 3 | 4 | jest.mock('fs'); 5 | jest.mock('../utils/config', () => { 6 | return { 7 | isDebugEnabled: () => false, 8 | }; 9 | }); 10 | 11 | test('root も path も 未指定の場合は undefined', () => { 12 | const mockExistsSync = jest.fn(); 13 | mockExistsSync.mockReturnValueOnce(true); 14 | fs.existsSync = mockExistsSync; 15 | 16 | expect(getCreateGraphsArguments({})).toBeUndefined(); 17 | }); 18 | 19 | test('tsconfigRoot を指定するとそれが dir に設定される', () => { 20 | const mockExistsSync = jest.fn(); 21 | mockExistsSync.mockReturnValueOnce(true); 22 | fs.existsSync = mockExistsSync; 23 | 24 | expect(getCreateGraphsArguments({ tsconfigRoot: './' })).toStrictEqual({ 25 | dir: './', 26 | }); 27 | }); 28 | 29 | test.each([ 30 | { 31 | tsconfig: './tsconfig.json', 32 | expected: { 33 | tsconfig: './tsconfig.json', 34 | dir: './', 35 | }, 36 | }, 37 | { 38 | tsconfig: './tsconfig.json5', 39 | expected: { 40 | tsconfig: './tsconfig.json5', 41 | dir: './', 42 | }, 43 | }, 44 | { 45 | tsconfig: './my_app/tsconfig.json', 46 | expected: { 47 | tsconfig: './my_app/tsconfig.json', 48 | dir: './my_app/', 49 | }, 50 | }, 51 | ])( 52 | 'tsconfig を指定するとそれが tsconfig に、tsconfig のあるディレクトリが dir に設定される', 53 | ({ tsconfig, expected }) => { 54 | const mockExistsSync = jest.fn(); 55 | mockExistsSync.mockReturnValueOnce(true); 56 | fs.existsSync = mockExistsSync; 57 | 58 | expect(getCreateGraphsArguments({ tsconfig })).toStrictEqual(expected); 59 | }, 60 | ); 61 | 62 | test('tsconfig が存在しない場合は undefined を返す', () => { 63 | const mockExistsSync = jest.fn(); 64 | mockExistsSync.mockReturnValueOnce(false); 65 | fs.existsSync = mockExistsSync; 66 | 67 | expect( 68 | getCreateGraphsArguments({ tsconfig: './tsconfig.json' }), 69 | ).toBeUndefined(); 70 | }); 71 | 72 | test.each([ 73 | { tsconfig: './tsconfig' }, 74 | { tsconfig: './tsconfig/' }, 75 | { tsconfig: './tsconfig.yaml' }, 76 | { tsconfig: './tsconfig.json.js' }, 77 | ])('json でない場合は undefined を返す', ({ tsconfig }) => { 78 | const mockExistsSync = jest.fn(); 79 | mockExistsSync.mockReturnValueOnce(true); 80 | fs.existsSync = mockExistsSync; 81 | 82 | expect(getCreateGraphsArguments({ tsconfig })).toBeUndefined(); 83 | }); 84 | 85 | test('tsconfigRoot と tsconfig を指定すると tsconfigRoot は無視される', () => { 86 | const mockExistsSync = jest.fn(); 87 | mockExistsSync.mockReturnValueOnce(true); 88 | fs.existsSync = mockExistsSync; 89 | 90 | expect( 91 | getCreateGraphsArguments({ 92 | tsconfigRoot: './', 93 | tsconfig: './my_app/tsconfig.json', 94 | }), 95 | ).toStrictEqual({ 96 | tsconfig: './my_app/tsconfig.json', 97 | dir: './my_app/', 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/tsg/getCreateGraphsArguments.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { log } from '../utils/log'; 3 | 4 | export function getCreateGraphsArguments({ 5 | tsconfig, 6 | tsconfigRoot, 7 | }: { 8 | tsconfig?: string; 9 | tsconfigRoot?: string; 10 | }): 11 | | { 12 | dir: string | undefined; 13 | tsconfig?: string; 14 | } 15 | | undefined { 16 | // 基本、root も path もないことはない 17 | if (!tsconfigRoot && !tsconfig) return undefined; 18 | if (tsconfig) { 19 | if (!/\.json.?$/.test(tsconfig) || !fs.existsSync(tsconfig)) { 20 | log('Head: tsconfig does not exist'); 21 | return undefined; 22 | } 23 | const tsconfigRelatedPath = tsconfig; 24 | return { 25 | tsconfig: tsconfigRelatedPath, 26 | dir: tsconfigRelatedPath.split('/').slice(0, -1).join('/') + '/', 27 | }; 28 | } 29 | return { 30 | dir: tsconfigRoot, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import type { mermaidify } from '@ysk8hori/typescript-graph'; 2 | import * as core from '@actions/core'; 3 | import { uniqueString } from './reducer'; 4 | 5 | /** tsconfig のルートディレクトリ */ 6 | const TSCONFIG_ROOT = 'tsconfig-root'; 7 | /** tsconfig のパス */ 8 | const TSCONFIG_PATH = 'tsconfig'; 9 | /** 変更ファイル数が多い場合にグラフの表示を抑止するが、その際のノード数を指定する値 */ 10 | const MAX_SIZE = 'max-size'; 11 | /** グラフの方向を指定する */ 12 | const ORIENTATION = 'orientation'; 13 | /** デバッグモード */ 14 | const DEBUG = 'debug'; 15 | /** Mermaid を `
` タグで囲み折りたたむかどうか */ 16 | const IN_DETAILS = 'in-details'; 17 | /** ファイルの除外対象 */ 18 | const EXCLUDE = 'exclude'; 19 | /** 変更対象のファイルが同階層の index.ts などから参照されている場合、その index.ts への依存ファイルも表示するかどうか */ 20 | const INCLUDE_INDEX_FILE_DEPENDENCIES = 'include-index-file-dependencies'; 21 | /** コメントのタイトル */ 22 | const COMMENT_TITLE = 'comment-title'; 23 | /** メトリクスを表示するかどうか */ 24 | const SHOW_METRICS = 'show-metrics'; 25 | 26 | /** 27 | * tsconfig を探索するディレクトリ情報を取得する。 28 | * 29 | * ここで指定した階層から上の階層に向かって tsconfig を探索する。 30 | */ 31 | export function getTsconfigRoot(): string { 32 | return core.getInput(TSCONFIG_ROOT) ?? './'; 33 | } 34 | 35 | /** 36 | * tsconfig.json のパスを取得する。ファイル名が異なる場合などにはこちらを指定する。 37 | */ 38 | export function getTsconfigPath(): string | undefined { 39 | return core.getInput(TSCONFIG_PATH) ?? undefined; 40 | } 41 | 42 | /** 変更ファイル数が多い場合にグラフの表示を抑止するが、その際のノード数を指定する値を取得する。 */ 43 | export function getMaxSize(): number { 44 | const maxSize = core.getInput(MAX_SIZE); 45 | if (maxSize && !isNaN(parseInt(maxSize, 10))) { 46 | return parseInt(maxSize, 10); 47 | } 48 | return 30; 49 | } 50 | 51 | /** グラフの方向を指定したオブジェクトを取得する */ 52 | export function getOrientation(): Pick< 53 | Parameters[2], 54 | 'LR' | 'TB' 55 | > { 56 | const orientation = core.getInput(ORIENTATION); 57 | if (orientation === 'TB') { 58 | return { TB: true }; 59 | } 60 | if (orientation === 'LR') { 61 | return { LR: true }; 62 | } 63 | return {}; 64 | } 65 | 66 | export function isDebugEnabled(): boolean { 67 | return core.getInput(DEBUG) === 'true'; 68 | } 69 | 70 | /** Mermaid を `
` タグで囲み折りたたむかどうか */ 71 | export function isInDetails(): boolean { 72 | return core.getInput(IN_DETAILS) === 'true'; 73 | } 74 | 75 | export function getShowMetrics(): boolean { 76 | return core.getInput(SHOW_METRICS) === 'true'; 77 | } 78 | 79 | export function exclude(): string[] { 80 | return core 81 | .getInput(EXCLUDE) 82 | .split(',') 83 | .map(s => s.trim()) 84 | .filter(Boolean) 85 | .reduce(uniqueString, ['node_modules']); // デフォルトで node_modules を含める 86 | } 87 | 88 | /** 変更対象のファイルが同階層の index.ts などから参照されている場合、その index.ts への依存ファイルも表示するかどうか */ 89 | export function isIncludeIndexFileDependencies(): boolean { 90 | return core.getInput(INCLUDE_INDEX_FILE_DEPENDENCIES) === 'true'; 91 | } 92 | 93 | /** コメントのタイトルを取得する */ 94 | export function getCommentTitle(): string { 95 | return core.getInput(COMMENT_TITLE) ?? 'Delta TypeScript Graph'; 96 | } 97 | 98 | export function getConfig() { 99 | return { 100 | tsconfigRoot: getTsconfigRoot(), 101 | tsconfig: getTsconfigPath(), 102 | maxSize: getMaxSize(), 103 | orientation: getOrientation(), 104 | debugEnabled: isDebugEnabled(), 105 | inDetails: isInDetails(), 106 | exclude: exclude(), 107 | includeIndexFileDependencies: isIncludeIndexFileDependencies(), 108 | /** Action の parameter として指定された comment-title */ 109 | commentTitle: getCommentTitle(), 110 | showMetrics: getShowMetrics(), 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /src/utils/context.ts: -------------------------------------------------------------------------------- 1 | import { createCommentTitle } from './createCommentTitle'; 2 | import { getConfig } from './config'; 3 | import type { FilesChanged } from './github'; 4 | import GitHub from './github'; 5 | 6 | export interface Context { 7 | config: { 8 | tsconfigRoot: string; 9 | tsconfig: string | undefined; 10 | maxSize: number; 11 | orientation: { TB: true } | { LR: true } | object; 12 | debugEnabled: boolean; 13 | inDetails: boolean; 14 | exclude: string[]; 15 | includeIndexFileDependencies: boolean; 16 | commentTitle: string; 17 | showMetrics: boolean; 18 | }; 19 | github: GitHub; 20 | fullCommentTitle: string; 21 | filesChanged: FilesChanged; 22 | } 23 | 24 | export async function createContext(): Promise { 25 | const github = new GitHub(); 26 | const filesChanged = await github.getTSFiles(); 27 | const config = getConfig(); 28 | return { 29 | config, 30 | github, 31 | fullCommentTitle: createCommentTitle( 32 | config.commentTitle, 33 | github.getWorkflowName(), 34 | ), 35 | filesChanged, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/createCommentTitle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * コメントタイトルを生成する。 3 | * 4 | * @param workflow ワークフロー名 5 | * @returns コメントタイトル 6 | */ 7 | export function createCommentTitle( 8 | commentTitle: string, 9 | workflow: string, 10 | ): string { 11 | return `## ${commentTitle}`; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/dummyContext.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from './context'; 2 | import type GitHub from './github'; 3 | 4 | export function getDummyContext(context?: { 5 | configExclude?: string[]; 6 | configTsconfig?: string; 7 | }): Context { 8 | return { 9 | config: { 10 | tsconfigRoot: '', 11 | tsconfig: context?.configTsconfig ?? '', 12 | maxSize: 100, 13 | orientation: {}, 14 | debugEnabled: false, 15 | inDetails: false, 16 | exclude: context?.configExclude ?? ['node_modules'], 17 | includeIndexFileDependencies: false, 18 | commentTitle: '', 19 | showMetrics: false, 20 | }, 21 | github: { 22 | getWorkflowName: jest.fn(), 23 | commentToPR: jest.fn(), 24 | deleteComment: jest.fn(), 25 | getTSFiles: jest.fn(), 26 | getBaseSha: jest.fn(), 27 | getHeadSha: jest.fn(), 28 | cloneRepo: jest.fn(), 29 | } as unknown as GitHub, 30 | fullCommentTitle: '## Delta TypeScript Graph', 31 | filesChanged: { 32 | created: [], 33 | deleted: [], 34 | modified: [], 35 | renamed: [], 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/github.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import * as core from '@actions/core'; 3 | import * as github from '@actions/github'; 4 | import { info, log } from './log'; 5 | import { retry } from './retry'; 6 | 7 | export interface PullRequestFileInfo { 8 | filename: string; 9 | status: 10 | | 'added' 11 | | 'removed' 12 | | 'modified' 13 | | 'renamed' 14 | | 'copied' 15 | | 'changed' 16 | | 'unchanged'; 17 | previous_filename: string | undefined; 18 | } 19 | 20 | export interface FilesChanged { 21 | created: PullRequestFileInfo[]; 22 | deleted: PullRequestFileInfo[]; 23 | modified: PullRequestFileInfo[]; 24 | renamed: PullRequestFileInfo[]; 25 | } 26 | 27 | /** 28 | * 400、401、403、404、422、451を除く、サーバーの4xx/5xx応答の場合はエラーをスローする。 29 | * 30 | * @param e エラーオブジェクト 31 | * @returns 400、401、403、404、422、451 の場合は undefined を返す。 32 | * @see https://github.com/octokit/plugin-retry.js 33 | */ 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | function throwUnexpectedError(e: any) { 36 | // @see https://github.com/octokit/plugin-retry.js 37 | if ([400, 401, 403, 404, 422, 451].includes(e?.status)) { 38 | console.warn(e); 39 | return undefined; 40 | } 41 | throw new Error(e.message, { cause: e }); 42 | } 43 | 44 | export default class GitHub { 45 | #octokit: ReturnType; 46 | 47 | constructor() { 48 | this.#octokit = github.getOctokit(core.getInput('access-token')); 49 | } 50 | 51 | public async getTSFiles(): Promise { 52 | const compareResult = 53 | await this.#octokit.rest.repos.compareCommitsWithBasehead({ 54 | owner: github.context.repo.owner, 55 | repo: github.context.repo.repo, 56 | basehead: `${github.context.payload.pull_request?.base.sha}...${github.context.payload.pull_request?.head.sha}`, 57 | }); 58 | const files = compareResult.data.files 59 | ?.filter(file => 60 | // TODO: tsg の isTsFile を使用する 61 | /\.ts$|\.tsx$|\.vue$|\.astro$|\.svelte$/.test(file.filename), 62 | ) 63 | .map(file => ({ 64 | filename: file.filename, 65 | status: file.status, 66 | previous_filename: file.previous_filename, 67 | })); 68 | log(files); 69 | 70 | // typescript-graph では、以下の分類で処理する。 71 | return { 72 | created: 73 | files?.filter( 74 | file => file.status === 'added' || file.status === 'copied', 75 | ) ?? [], 76 | deleted: files?.filter(file => file.status === 'removed') ?? [], 77 | modified: 78 | files?.filter( 79 | file => file.status === 'modified' || file.status === 'changed', 80 | ) ?? [], 81 | renamed: files?.filter(file => file.status === 'renamed') ?? [], 82 | }; 83 | } 84 | 85 | public getBaseSha() { 86 | return github.context.payload.pull_request?.base.sha; 87 | } 88 | 89 | public getHeadSha() { 90 | return github.context.payload.pull_request?.head.sha; 91 | } 92 | 93 | /** 94 | * ワークフロー名を取得する。 95 | * 96 | * @returns ワークフロー名 97 | */ 98 | public getWorkflowName() { 99 | return github.context.workflow; 100 | } 101 | 102 | /** 103 | * PRにコメントする。 104 | * 105 | * 同一PR内では同一コメントを上書きし続ける。 106 | */ 107 | public async commentToPR(fullCommentTitle: string, body: string) { 108 | const owner = github.context.repo.owner; 109 | const repo = github.context.repo.repo; 110 | const issue_number = github.context.payload.number; 111 | // 1. 既存のコメントを取得する 112 | const comments = await retry(() => 113 | this.#octokit.rest.issues 114 | .listComments({ 115 | owner, 116 | repo, 117 | issue_number, 118 | }) 119 | .catch(throwUnexpectedError), 120 | ); 121 | 122 | if (!comments) { 123 | info('commentToPR:コメントの取得に失敗しました'); 124 | return; 125 | } 126 | 127 | // 2. 既存のコメントがあれば、そのコメントのIDを取得する 128 | const existingComment = comments.data.find(comment => 129 | comment.body?.trim().startsWith(fullCommentTitle), 130 | ); 131 | 132 | if (existingComment) { 133 | // 3. 既存のコメントがあれば、そのコメントを更新する 134 | await retry(() => 135 | this.#octokit.rest.issues 136 | .updateComment({ 137 | owner, 138 | repo, 139 | comment_id: existingComment.id, 140 | body: fullCommentTitle + '\n\n' + body, 141 | }) 142 | .catch(throwUnexpectedError), 143 | ); 144 | } else { 145 | // 4. 既存のコメントがなければ、新規にコメントを作成する 146 | await retry(() => 147 | this.#octokit.rest.issues 148 | .createComment({ 149 | owner, 150 | repo, 151 | issue_number, 152 | body: fullCommentTitle + '\n\n' + body, 153 | }) 154 | .catch(throwUnexpectedError), 155 | ); 156 | } 157 | } 158 | 159 | /** 160 | * PRのコメントを削除する 161 | */ 162 | public async deleteComment(fullCommentTitle: string) { 163 | const owner = github.context.repo.owner; 164 | const repo = github.context.repo.repo; 165 | const issue_number = github.context.payload.number; 166 | // 1. 既存のコメントを取得する 167 | const comments = await retry(() => 168 | this.#octokit.rest.issues 169 | .listComments({ 170 | owner, 171 | repo, 172 | issue_number, 173 | }) 174 | .catch(throwUnexpectedError), 175 | ); 176 | 177 | if (!comments) { 178 | info('deleteComment:コメントの取得に失敗しました'); 179 | return; 180 | } 181 | 182 | // 2. 既存のコメントがあれば、そのコメントのIDを取得する 183 | const existingComment = comments.data.find(comment => 184 | comment.body?.trim().startsWith(fullCommentTitle), 185 | ); 186 | 187 | if (existingComment) { 188 | // 3. 既存のコメントがあれば、そのコメントを削除する 189 | await retry(() => 190 | this.#octokit.rest.issues 191 | .deleteComment({ 192 | owner, 193 | repo, 194 | comment_id: existingComment.id, 195 | }) 196 | .catch(throwUnexpectedError), 197 | ); 198 | } 199 | } 200 | 201 | public cloneRepo() { 202 | const repo = github.context.repo; 203 | // リポジトリのURLを取得 204 | const repoUrl = `https://github.com/${repo.owner}/${repo.repo}.git`; 205 | // リポジトリをチェックアウト 206 | execSync(`git clone ${repoUrl}`); 207 | // result としてリポジトリ名を返す 208 | return { repoDir: repo.repo }; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import { isDebugEnabled } from './config'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export function log(message?: any, ...optionalParams: any[]): void { 5 | if (!isDebugEnabled()) return; 6 | console.log(stringifyObject(message), ...optionalParams.map(stringifyObject)); 7 | } 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | function stringifyObject(message: any) { 11 | // message がオブジェクトの場合は JSON.stringify で文字列化する 12 | if (message !== null && typeof message === 'object') { 13 | return JSON.stringify(message, null, 2); 14 | } 15 | return message; 16 | } 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | export function info(message?: any, ...optionalParams: any[]): void { 20 | console.info( 21 | stringifyObject(message), 22 | ...optionalParams.map(stringifyObject), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/reducer.ts: -------------------------------------------------------------------------------- 1 | export function uniqueString(prev: string[], current: string) { 2 | if (!prev.includes(current)) { 3 | prev.push(current); 4 | } 5 | return prev; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/retry.test.ts: -------------------------------------------------------------------------------- 1 | import { retry } from './retry'; 2 | 3 | const warn = global.console.warn; 4 | 5 | beforeAll(() => { 6 | jest.useFakeTimers({ doNotFake: ['nextTick'] }); 7 | global.console.warn = jest.fn(); 8 | }); 9 | 10 | afterAll(() => { 11 | jest.useRealTimers(); 12 | global.console.warn = warn; 13 | }); 14 | 15 | async function runTimersAndNextTick(times: number) { 16 | if (times <= 0) return; 17 | await new Promise(process.nextTick); 18 | jest.runAllTimers(); 19 | await runTimersAndNextTick(times - 1); 20 | } 21 | 22 | test('成功時には値を返す', async () => { 23 | const result = await retry(() => Promise.resolve('success')); 24 | 25 | expect(result).toBe('success'); 26 | }); 27 | 28 | test('成功後はコールバックを実行しない', async () => { 29 | const fn = jest.fn(); 30 | 31 | const result = retry<'success'>( 32 | () => 33 | new Promise(resolve => { 34 | fn(); 35 | resolve('success'); 36 | }), 37 | ); 38 | await runTimersAndNextTick(5); 39 | 40 | expect(fn).toHaveBeenCalledTimes(1); 41 | expect(await result).toBe('success'); 42 | }); 43 | 44 | test('失敗時にはundefinedを返す', async () => { 45 | const fn = jest.fn(); 46 | fn.mockRejectedValue(new Error('fail')); 47 | 48 | const result = retry(fn); 49 | await runTimersAndNextTick(5); 50 | 51 | expect(await result).toBeUndefined(); 52 | }); 53 | 54 | test('コールバック関数を最初の1回+リトライ回数分実行する', async () => { 55 | const fn = jest.fn(); 56 | 57 | const result = retry( 58 | () => 59 | new Promise((_, reject) => { 60 | fn(); 61 | reject(new Error('fail')); 62 | }), 63 | 5, 64 | ); 65 | await runTimersAndNextTick(7); 66 | 67 | expect(fn).toHaveBeenCalledTimes(6); 68 | expect(await result).toBeUndefined(); 69 | }); 70 | 71 | test('指定した回数だけリトライし成功できる', async () => { 72 | const fn = jest.fn(); 73 | fn.mockRejectedValueOnce(new Error('fail')) 74 | .mockRejectedValueOnce(new Error('fail')) 75 | .mockRejectedValueOnce(new Error('fail')) 76 | .mockResolvedValueOnce('success'); 77 | 78 | const result = retry(fn, 3); 79 | await runTimersAndNextTick(5); 80 | 81 | expect(fn).toHaveBeenCalledTimes(4); 82 | expect(await result).toBe('success'); 83 | }); 84 | -------------------------------------------------------------------------------- /src/utils/retry.ts: -------------------------------------------------------------------------------- 1 | export function retry( 2 | fn: () => Promise, 3 | maxRetries = 2, 4 | interval = 1000, 5 | ): Promise { 6 | return fn().catch(async (error: Error) => { 7 | console.warn('\n', error); 8 | if (maxRetries <= 0) return undefined; 9 | 10 | await new Promise(resolve => setTimeout(resolve, interval)); 11 | return retry(fn, maxRetries - 1); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "ESNext", 17 | "DOM" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 25 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 29 | 30 | /* Modules */ 31 | "module": "Node16" /* Specify what module code is generated. */, 32 | "rootDir": "src" /* Specify the root folder within your source files. */, 33 | "moduleResolution": "node16" /* Specify how TypeScript looks up a file from a given module specifier. */, 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 42 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 43 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 44 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 45 | "resolveJsonModule": true /* Enable importing .json files. */, 46 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 47 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 48 | 49 | /* JavaScript Support */ 50 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 51 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 52 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 53 | 54 | /* Emit */ 55 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 56 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 57 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 58 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 61 | "outDir": "lib" /* Specify an output folder for all emitted files. */, 62 | // "removeComments": true, /* Disable emitting comments. */ 63 | // "noEmit": true, /* Disable emitting files from a compilation. */ 64 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 65 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 66 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 67 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 68 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 69 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 70 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 71 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 72 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 73 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 74 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 75 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 76 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 77 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 78 | 79 | /* Interop Constraints */ 80 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 81 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 82 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 83 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 84 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 85 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 86 | 87 | /* Type Checking */ 88 | "strict": true /* Enable all strict type-checking options. */, 89 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 90 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 91 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 92 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 93 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 94 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 95 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 96 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 97 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 98 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 99 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 100 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 101 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 102 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 103 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 104 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 105 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 106 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 107 | 108 | /* Completeness */ 109 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 110 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 111 | }, 112 | "exclude": ["dummy_project"] 113 | } 114 | --------------------------------------------------------------------------------