├── .codeclimate.yml ├── .editorconfig ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING ├── FUNDING.yml ├── ISSUE_TEMPLATE ├── PULL_REQUEST_TEMPLATE └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── API.md ├── ARCHITECTURE.md ├── CHANGELOG.md ├── DEPENDENCIES.mmd ├── DEPENDENCIES.mmd.svg ├── LICENSE ├── README.md ├── bin └── jsarch.js ├── eslint.config.js ├── package-lock.json ├── package.json ├── src ├── compareNotes.test.ts ├── compareNotes.ts ├── jsarch.test.ts ├── jsarch.ts ├── parser.ts ├── runJSArch.ts └── utils.ts └── tsconfig.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it here, your changes would 3 | # be overridden. 4 | 5 | engines: 6 | eslint: 7 | enabled: true 8 | 9 | ratings: 10 | paths: 11 | - "'src/**/*.ts'" 12 | ## Exclude test files. 13 | exclude_patterns: 14 | - "dist/" 15 | - "**/node_modules/" 16 | - "src/**/*.tests.ts" 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it elsewhere, changes would 3 | # be overridden. 4 | 5 | # EditorConfig is awesome: http://EditorConfig.org 6 | 7 | # top-most EditorConfig file 8 | root = true 9 | 10 | # Unix-style newlines with a newline ending every file 11 | [*] 12 | end_of_line = lf 13 | insert_final_newline = true 14 | 15 | # Matches multiple files with brace expansion notation 16 | # Set default charset 17 | # 2 space indentation 18 | [*.{js,css}] 19 | charset = utf-8 20 | indent_style = space 21 | trim_trailing_whitespace = true 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Be kind, except if I behave like an asshole, if so, tell me by linking to this 4 | file. 5 | 6 | I try hard to document and automate things so that you cannot create noises 7 | without really willing to do so. 8 | 9 | This is why I'll just delete issues/comments making be sad. 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Contributing to this project requires you to be 2 | a gentleman. 3 | 4 | By contributing you must agree with publishing your 5 | changes into the same license that apply to the current code. 6 | 7 | You will find the license in the LICENSE file at 8 | the root of this repository. 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [nfroidure] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## Issue 2 | 16 | 17 | I'm a gentledev I: 18 | - [ ] fully read the README recently 19 | - [ ] searched for existing issues 20 | - [ ] checked I'm up to date with the latest version of the project 21 | 22 | ### Expected behavior 23 | 24 | ### Actual behavior 25 | 26 | ### Steps to reproduce the behavior 27 | 28 | ### Debugging informations 29 | - `node -v` result: 30 | ``` 31 | 32 | ``` 33 | 34 | - `npm -v` result: 35 | ``` 36 | 37 | ``` 38 | If the result is lower than 20.11.1, there is 39 | poor chances I even have a look to it. Please, 40 | use the last [NodeJS LTS version](https://nodejs.org/en/). 41 | 42 | ## Feature request 43 | 54 | 55 | ### Feature description 56 | 57 | ### Use cases 58 | 59 | - [ ] I will/did implement the feature 60 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 10 | 11 | Fixes # 12 | 13 | ### Proposed changes 14 | - 15 | - 16 | 17 | 18 | 19 | ### Code quality 20 | - [ ] I made some tests for my changes 21 | - [ ] I added my name in the 22 | [contributors](https://docs.npmjs.com/files/package.json#people-fields-author-contributors) 23 | field of the `package.json` file. Beware to use the same format than for the author field 24 | for the entries so that you'll get a mention in the `README.md` with a link to your website. 25 | 26 | ### License 27 | To get your contribution merged, you must check the following. 28 | 29 | - [ ] I read the project license in the LICENSE file 30 | - [ ] I agree with publishing under this project license 31 | 32 | 46 | ### Join 47 | - [ ] I wish to join the core team 48 | - [ ] I agree that with great powers comes responsibilities 49 | - [ ] I'm a nice person 50 | 51 | My NPM username: 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it here, your changes would 3 | # be overridden. 4 | 5 | name: Node.js CI 6 | 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | branches: [main] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - name: Install dependencies 29 | run: npm ci 30 | - name: Run pre-commit tests 31 | run: npm run precz 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it elsewhere, changes would 3 | # be overridden. 4 | 5 | # Created by https://www.gitignore.io/api/osx,node,linux 6 | 7 | ### Linux ### 8 | *~ 9 | 10 | # temporary files which can be created if a process still has a handle open of a deleted file 11 | .fuse_hidden* 12 | 13 | # KDE directory preferences 14 | .directory 15 | 16 | # Linux trash folder which might appear on any partition or disk 17 | .Trash-* 18 | 19 | # .nfs files are created when an open file is removed but is still being accessed 20 | .nfs* 21 | 22 | ### Node ### 23 | # Logs 24 | logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | 42 | # nyc test coverage 43 | .nyc_output 44 | 45 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 46 | .grunt 47 | 48 | # Bower dependency directory (https://bower.io/) 49 | bower_components 50 | 51 | # node-waf configuration 52 | .lock-wscript 53 | 54 | # Compiled binary addons (https://nodejs.org/api/addons.html) 55 | build/Release 56 | 57 | # Dependency directories 58 | node_modules/ 59 | jspm_packages/ 60 | 61 | # TypeScript v1 declaration files 62 | typings/ 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # next.js build output 86 | .next 87 | 88 | # nuxt.js build output 89 | .nuxt 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless 96 | 97 | ### OSX ### 98 | # General 99 | .DS_Store 100 | .AppleDouble 101 | .LSOverride 102 | 103 | # Icon must end with two \r 104 | Icon 105 | 106 | # Thumbnails 107 | ._* 108 | 109 | # Files that might appear in the root of a volume 110 | .DocumentRevisions-V100 111 | .fseventsd 112 | .Spotlight-V100 113 | .TemporaryItems 114 | .Trashes 115 | .VolumeIcon.icns 116 | .com.apple.timemachine.donotpresent 117 | 118 | # Directories potentially created on remote AFP share 119 | .AppleDB 120 | .AppleDesktop 121 | Network Trash Folder 122 | Temporary Items 123 | .apdisk 124 | 125 | 126 | # End of https://www.gitignore.io/api/osx,node,linux 127 | 128 | # Coveralls key 129 | .coveralls.yml 130 | 131 | # Project custom ignored file 132 | dist 133 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it elsewhere, changes would 3 | # be overridden. 4 | 5 | language: node_js 6 | node_js: 7 | - 12 8 | - 12.19.0 9 | - 13 10 | - 14 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "gruntfuggly.todo-tree" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | 4 | ## initJSArch(services) ⇒ Promise.<function()> 5 | Declare jsArch in the dependency injection system 6 | 7 | **Kind**: global function 8 | 9 | | Param | Type | Default | Description | 10 | | --- | --- | --- | --- | 11 | | services | Object | | Services (provided by the dependency injector) | 12 | | services.CONFIG | Object | | The JSArch config | 13 | | services.EOL | Object | | The OS EOL chars | 14 | | services.glob | Object | | Globbing service | 15 | | services.fs | Object | | File system service | 16 | | services.parser | Object | | Parser service | 17 | | [services.log] | Object | noop | Logging service | 18 | 19 | 20 | 21 | ### initJSArch~jsArch(options) ⇒ Promise.<String> 22 | Compile an run a template 23 | 24 | **Kind**: inner method of [initJSArch](#initJSArch) 25 | **Returns**: Promise.<String> - Computed architecture notes as a markdown file 26 | 27 | | Param | Type | Description | 28 | | --- | --- | --- | 29 | | options | Object | Options (destructured) | 30 | | options.cwd | Object | Current working directory | 31 | | options.patterns | Object | Patterns to look files for (see node-glob) | 32 | | options.eol | Object | End of line character (default to the OS one) | 33 | | options.titleLevel | Object | The base title level of the output makdown document | 34 | | options.base | Object | The base directory for the ARCHITECTURE.md references | 35 | 36 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | [//]: # ( ) 2 | [//]: # (This file is automatically generated by the `jsarch`) 3 | [//]: # (module. Do not change it elsewhere, changes would) 4 | [//]: # (be overriden.) 5 | [//]: # ( ) 6 | # Architecture Notes 7 | 8 | ## Summary 9 | 10 | 1. [jsArch service](#1-jsarch-service) 11 | 1. [Extraction](#11-extraction) 12 | 2. [Ordering](#12-ordering) 13 | 3. [Title level](#13-title-level) 14 | 4. [Base](#14-base) 15 | 2. [CLI](#2-cli) 16 | 17 | 18 | ## 1. jsArch service 19 | 20 | JSArch is basically a service that exposes a function allowing 21 | to extract and output architecture notes from the code. 22 | 23 | This service needs some other services. To be able to mock and 24 | interchange them, we use 25 | [Knifecycle](https://github.com/nfroidure/knifecycle) for its 26 | dependency injection and inversion of control feature. 27 | 28 | ![Dependencies Graph](./DEPENDENCIES.mmd.svg) 29 | 30 | [See in context](./src/jsarch.ts#L98-L109) 31 | 32 | 33 | 34 | ### 1.1. Extraction 35 | 36 | We use AST parsing and visiting to retrieve well formed 37 | architecture notes. It should be structured like that: 38 | ```js 39 | 40 | /** Architecture Note #{order}: {title} 41 | 42 | {body} 43 | ``` 44 | 45 | [See in context](./src/jsarch.ts#L298-L308) 46 | 47 | 48 | 49 | ### 1.2. Ordering 50 | 51 | To order architecture notes in a meaningful way we 52 | use title hierarchy like we used too at school with 53 | argumentative texts ;). 54 | 55 | A sample tree structure could be: 56 | - 1 57 | - 1.1 58 | - 1.2 59 | - 2 60 | - 3 61 | 62 | [See in context](./src/compareNotes.ts#L3-L16) 63 | 64 | 65 | 66 | ### 1.3. Title level 67 | 68 | By default, titles will be added like if the architecture 69 | notes were in a single separated file. 70 | 71 | If you wish to add the architecture notes in a README.md file 72 | you will have to set the `titleLevel` option to as much `#` 73 | as necessar to fit the title hierarchy of you README file. 74 | 75 | [See in context](./src/jsarch.ts#L76-L84) 76 | 77 | 78 | 79 | ### 1.4. Base 80 | 81 | By default, links to the architecture notes right in the code 82 | are supposed relative to the README.md file like you would 83 | find it in the GitHub homepage of you projects. 84 | 85 | To override it, use the `base` option. 86 | 87 | [See in context](./src/jsarch.ts#L87-L95) 88 | 89 | 90 | 91 | ## 2. CLI 92 | 93 | The JSArch CLI tool basically wraps the jsArch service 94 | to make it usable from the CLI. 95 | 96 | To see its options, run: 97 | ``` 98 | jsarch -h 99 | ``` 100 | 101 | [See in context](./src/runJSArch.ts#L1-L10) 102 | 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [6.1.2](https://github.com/nfroidure/jsarch/compare/v6.1.1...v6.1.2) (2024-12-04) 2 | 3 | 4 | 5 | ## [6.1.1](https://github.com/nfroidure/jsarch/compare/v6.1.0...v6.1.1) (2024-12-04) 6 | 7 | 8 | 9 | # [6.1.0](https://github.com/nfroidure/jsarch/compare/v6.0.4...v6.1.0) (2024-10-29) 10 | 11 | 12 | 13 | ## [6.0.4](https://github.com/nfroidure/jsarch/compare/v6.0.3...v6.0.4) (2024-07-15) 14 | 15 | 16 | 17 | ## [6.0.3](https://github.com/nfroidure/jsarch/compare/v6.0.2...v6.0.3) (2023-08-16) 18 | 19 | 20 | 21 | ## [6.0.2](https://github.com/nfroidure/jsarch/compare/v6.0.1...v6.0.2) (2023-08-12) 22 | 23 | 24 | 25 | ## [6.0.1](https://github.com/nfroidure/jsarch/compare/v6.0.0...v6.0.1) (2023-03-09) 26 | 27 | 28 | 29 | # [6.0.0](https://github.com/nfroidure/jsarch/compare/v5.0.2...v6.0.0) (2022-08-30) 30 | 31 | 32 | 33 | ## [5.0.2](https://github.com/nfroidure/jsarch/compare/v5.0.1...v5.0.2) (2022-05-25) 34 | 35 | 36 | 37 | ## [5.0.1](https://github.com/nfroidure/jsarch/compare/v5.0.0...v5.0.1) (2021-11-11) 38 | 39 | 40 | ### Features 41 | 42 | * **title-anchor:** correct title anchor computation for level 2+ nested ([092fa1f](https://github.com/nfroidure/jsarch/commit/092fa1f062ea1dfd430f50ee0d2e7d1ff3b7a0e3)) 43 | 44 | 45 | 46 | # [5.0.0](https://github.com/nfroidure/jsarch/compare/v4.0.1...v5.0.0) (2021-10-31) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **summary:** set summary as a markdown list ([390876a](https://github.com/nfroidure/jsarch/commit/390876af434980a54868e2244d0ea9b73fbdda38)) 52 | * **tests:** fix the tests before publishing ([4b9a56e](https://github.com/nfroidure/jsarch/commit/4b9a56e558e9fe2c748100fa6a04f8af886897c3)) 53 | 54 | 55 | ### Features 56 | 57 | * **summary:** add an auto generated summary at the beginning of architecture.md file ([2195a06](https://github.com/nfroidure/jsarch/commit/2195a06e008dad1b211e2418fc14e200c8d2a890)) 58 | * **summary:** add number to titles ([882ff3a](https://github.com/nfroidure/jsarch/commit/882ff3a70d5868210c538060db5a5921ba913fa7)) 59 | 60 | 61 | 62 | ## [4.0.1](https://github.com/nfroidure/jsarch/compare/v4.0.0...v4.0.1) (2021-04-09) 63 | 64 | 65 | 66 | # [4.0.0](https://github.com/nfroidure/jsarch/compare/v3.0.0...v4.0.0) (2021-04-09) 67 | 68 | 69 | 70 | # [3.0.0](https://github.com/nfroidure/jsarch/compare/v2.0.3...v3.0.0) (2020-05-17) 71 | 72 | 73 | 74 | ## [2.0.3](https://github.com/nfroidure/jsarch/compare/v2.0.2...v2.0.3) (2019-06-11) 75 | 76 | 77 | 78 | ## [2.0.2](https://github.com/nfroidure/jsarch/compare/v2.0.1...v2.0.2) (2019-02-10) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * **RegExp:** Fix notes detection when a $ is in the title ([dcbc031](https://github.com/nfroidure/jsarch/commit/dcbc031)) 84 | 85 | 86 | 87 | ## [2.0.1](https://github.com/nfroidure/jsarch/compare/v2.0.0...v2.0.1) (2019-01-13) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **CONFIG:** Fix `package.json` configuration for edge cases ([5ddc915](https://github.com/nfroidure/jsarch/commit/5ddc915)) 93 | * **Documentation:** Add missing docs ([eb3dc50](https://github.com/nfroidure/jsarch/commit/eb3dc50)) 94 | 95 | 96 | 97 | # [2.0.0](https://github.com/nfroidure/jsarch/compare/v1.3.0...v2.0.0) (2019-01-12) 98 | 99 | 100 | ### Features 101 | 102 | * **configuration:** add configuration support (.jsarchrc or package) ([5b79a9d](https://github.com/nfroidure/jsarch/commit/5b79a9d)) 103 | 104 | 105 | 106 | 107 | # [1.3.0](https://github.com/nfroidure/jsarch/compare/v1.2.7...v1.3.0) (2018-02-05) 108 | 109 | 110 | ### Features 111 | 112 | * **jsarch.js:** switch from HTML comment to most compatible Markdown version ([b901e4b](https://github.com/nfroidure/jsarch/commit/b901e4b)) 113 | 114 | 115 | 116 | 117 | ## [1.2.7](https://github.com/nfroidure/jsarch/compare/v1.2.6...v1.2.7) (2018-02-01) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * **AST:** Fix AST traversal for experimental features ([000b2d5](https://github.com/nfroidure/jsarch/commit/000b2d5)) 123 | 124 | 125 | 126 | 127 | ## [1.2.6](https://github.com/nfroidure/jsarch/compare/v1.2.5...v1.2.6) (2017-12-02) 128 | 129 | 130 | 131 | 132 | ## [1.2.5](https://github.com/nfroidure/jsarch/compare/v1.2.4...v1.2.5) (2017-12-02) 133 | 134 | 135 | 136 | 137 | ## [1.2.4](https://github.com/nfroidure/jsarch/compare/v1.2.3...v1.2.4) (2017-12-02) 138 | 139 | 140 | 141 | 142 | ## [1.2.3](https://github.com/nfroidure/jsarch/compare/v1.2.2...v1.2.3) (2017-11-08) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * **Parser:** Fix parser source type configuration ([6e3e816](https://github.com/nfroidure/jsarch/commit/6e3e816)) 148 | 149 | 150 | 151 | 152 | ## [1.2.2](https://github.com/nfroidure/jsarch/compare/v1.2.1...v1.2.2) (2017-11-08) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * **Parser:** From recast to espree ([afc9015](https://github.com/nfroidure/jsarch/commit/afc9015)), closes [#7](https://github.com/nfroidure/jsarch/issues/7) 158 | 159 | 160 | 161 | 162 | ## [1.2.1](https://github.com/nfroidure/jsarch/compare/v1.2.0...v1.2.1) (2017-04-11) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * **cli:** Fix the base CLI option ([0e93178](https://github.com/nfroidure/jsarch/commit/0e93178)) 168 | * **ordering:** Fix Architecture notes ordering and add tests for it ([caa0839](https://github.com/nfroidure/jsarch/commit/caa0839)) 169 | 170 | 171 | 172 | 173 | # [1.2.0](https://github.com/nfroidure/jsarch/compare/v1.1.3...v1.2.0) (2017-03-16) 174 | 175 | 176 | ### Features 177 | 178 | * **indent:** Take care of indentation ([063ffdc](https://github.com/nfroidure/jsarch/commit/063ffdc)), closes [#1](https://github.com/nfroidure/jsarch/issues/1) 179 | 180 | 181 | 182 | 183 | ## [1.1.3](https://github.com/nfroidure/jsarch/compare/v1.1.2...v1.1.3) (2017-03-14) 184 | 185 | 186 | ### Bug Fixes 187 | 188 | * **dependencies:** Fix forgotten dependency ([18fc72a](https://github.com/nfroidure/jsarch/commit/18fc72a)) 189 | 190 | 191 | 192 | 193 | ## [1.1.2](https://github.com/nfroidure/jsarch/compare/v1.1.1...v1.1.2) (2017-03-14) 194 | 195 | 196 | 197 | 198 | ## [1.1.1](https://github.com/nfroidure/jsarch/compare/v1.1.0...v1.1.1) (2017-03-11) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * **docs:** Fix the architecture file output ([29f994d](https://github.com/nfroidure/jsarch/commit/29f994d)) 204 | 205 | 206 | 207 | 208 | # [1.1.0](https://github.com/nfroidure/jsarch/compare/v1.0.0...v1.1.0) (2017-03-11) 209 | 210 | 211 | ### Features 212 | 213 | * **output:** Add a prefix to the outputted architecture notes ([6ff89f8](https://github.com/nfroidure/jsarch/commit/6ff89f8)) 214 | 215 | 216 | 217 | 218 | # 1.0.0 (2017-03-10) 219 | 220 | 221 | ### Features 222 | 223 | * **First working version:** This is the first working version, it generates the architecture notes ([51b9b4e](https://github.com/nfroidure/jsarch/commit/51b9b4e)) 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /DEPENDENCIES.mmd: -------------------------------------------------------------------------------- 1 | graph TD 2 | program-->packageConf 3 | parser-->CONFIG{CONFIG} 4 | CONFIG{CONFIG}-->PROJECT_DIR{PROJECT_DIR} 5 | jsArch((jsArch))-->CONFIG{CONFIG} 6 | jsArch((jsArch))-->EOL{EOL} 7 | jsArch((jsArch))-->glob 8 | jsArch((jsArch))-->fs 9 | jsArch((jsArch))-->parser 10 | jsArch((jsArch))-->log 11 | classDef jsarch fill:#e7cdd2,stroke:#ebd4cb,stroke-width:1px,color:#000; 12 | classDef config fill:#d4cdcc,stroke:#ebd4cb,stroke-width:1px,color:#000; 13 | classDef others fill:#ebd4cb,stroke:#000,stroke-width:1px,color:#000; 14 | class program others; 15 | class packageConf others; 16 | class CONFIG config; 17 | class parser others; 18 | class PROJECT_DIR config; 19 | class jsArch jsarch; 20 | class EOL config; 21 | class glob others; 22 | class fs others; 23 | class log others; -------------------------------------------------------------------------------- /DEPENDENCIES.mmd.svg: -------------------------------------------------------------------------------- 1 |

program

packageConf

parser

CONFIG

PROJECT_DIR

jsArch

EOL

glob

fs

log

-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2017 Nicolas Froidure 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [//]: # ( ) 2 | [//]: # (This file is automatically generated by a `metapak`) 3 | [//]: # (module. Do not change it except between the) 4 | [//]: # (`content:start/end` flags, your changes would) 5 | [//]: # (be overridden.) 6 | [//]: # ( ) 7 | # jsarch 8 | > A simple module to extract architecture notes from your code. 9 | 10 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nfroidure/jsarch/blob/main/LICENSE) 11 | 12 | 13 | [//]: # (::contents:start) 14 | 15 | ## Usage 16 | 17 | To generate any project's architecture notes: 18 | 19 | ``` 20 | jsarch src/*.js > ARCHITECTURE.md 21 | 22 | ``` 23 | 24 | ### Configuration 25 | 26 | You can set your own configuration by adding a `jsarch` property in your 27 | `package.json` file (see 28 | [the defaults](https://github.com/nfroidure/jsarch/blob/master/src/jsarch.js#L20-L36)). 29 | 30 | For example, if you which to have TypeScript support and you use Gitlab instead 31 | of GitHub, just add this: 32 | 33 | ```js 34 | { 35 | // (...) 36 | "jsarch": { 37 | "gitProvider": "bitbucket", 38 | "parserOptions": { 39 | "plugins": ["typescript"] 40 | } 41 | } 42 | // (...) 43 | } 44 | ``` 45 | 46 | Per default, the Babel parser is used, but you can change it with the `parser` 47 | option. You'll have to install it before using it. 48 | 49 | ## Development 50 | 51 | To get involved into this module's development: 52 | ```sh 53 | npm i -g jsarch 54 | 55 | git clone git@github.com:nfroidure/jsarch.git 56 | 57 | cd jsarch 58 | 59 | npm it 60 | npm run build 61 | 62 | node bin/jsarch **/*.js > ARCHITECTURE.md 63 | ``` 64 | 65 | ## Architecture Notes 66 | 67 | You can see [this repository architecture notes](./ARCHITECTURE.md) for an 68 | example of the kind of content generated by this module. 69 | 70 | 71 | [//]: # (::contents:end) 72 | 73 | # API 74 | 75 | 76 | ## initJSArch(services) ⇒ Promise.<function()> 77 | Declare jsArch in the dependency injection system 78 | 79 | **Kind**: global function 80 | 81 | | Param | Type | Default | Description | 82 | | --- | --- | --- | --- | 83 | | services | Object | | Services (provided by the dependency injector) | 84 | | services.CONFIG | Object | | The JSArch config | 85 | | services.EOL | Object | | The OS EOL chars | 86 | | services.glob | Object | | Globbing service | 87 | | services.fs | Object | | File system service | 88 | | services.parser | Object | | Parser service | 89 | | [services.log] | Object | noop | Logging service | 90 | 91 | 92 | 93 | ### initJSArch~jsArch(options) ⇒ Promise.<String> 94 | Compile an run a template 95 | 96 | **Kind**: inner method of [initJSArch](#initJSArch) 97 | **Returns**: Promise.<String> - Computed architecture notes as a markdown file 98 | 99 | | Param | Type | Description | 100 | | --- | --- | --- | 101 | | options | Object | Options (destructured) | 102 | | options.cwd | Object | Current working directory | 103 | | options.patterns | Object | Patterns to look files for (see node-glob) | 104 | | options.eol | Object | End of line character (default to the OS one) | 105 | | options.titleLevel | Object | The base title level of the output makdown document | 106 | | options.base | Object | The base directory for the ARCHITECTURE.md references | 107 | 108 | 109 | # Authors 110 | - [Nicolas Froidure](http://insertafter.com/en/index.html) 111 | 112 | 113 | # License 114 | [MIT](https://github.com/nfroidure/jsarch/blob/main/LICENSE) 115 | -------------------------------------------------------------------------------- /bin/jsarch.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import runJSArch from '../dist/runJSArch.js'; 4 | 5 | runJSArch(); 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // This file is automatically generated by a `metapak` 3 | // module. Do not change it elsewhere, changes would 4 | // be overridden. 5 | 6 | import eslint from '@eslint/js'; 7 | import tseslint from 'typescript-eslint'; 8 | import eslintConfigPrettier from 'eslint-config-prettier'; 9 | import eslintPluginJest from 'eslint-plugin-jest'; 10 | 11 | export default tseslint.config( 12 | { 13 | files: ['**/*.ts'], 14 | ignores: ['**/*.d.ts'], 15 | extends: [ 16 | eslint.configs.recommended, 17 | ...tseslint.configs.recommended, 18 | ], 19 | }, 20 | { 21 | files: ['*.test.ts'], 22 | ...eslintPluginJest.configs['flat/recommended'], 23 | }, 24 | eslintConfigPrettier, 25 | { 26 | name: 'Project config', 27 | languageOptions: { 28 | ecmaVersion: 2018, 29 | sourceType: 'module', 30 | }, 31 | ignores: ['*.d.ts'], 32 | }, 33 | ); 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "metapak": { 3 | "configs": [ 4 | "main", 5 | "readme", 6 | "jest", 7 | "tsesm", 8 | "ghactions", 9 | "eslint", 10 | "codeclimate", 11 | "jsdocs" 12 | ], 13 | "data": { 14 | "files": "'src/**/*.ts'", 15 | "testsFiles": "'src/**/*.tests.ts'", 16 | "distFiles": "'dist/**/*.js'", 17 | "ignore": [ 18 | "dist" 19 | ], 20 | "bundleFiles": [ 21 | "bin/**/*.js", 22 | "dist", 23 | "src" 24 | ] 25 | } 26 | }, 27 | "name": "jsarch", 28 | "version": "6.1.2", 29 | "description": "A simple module to extract architecture notes from your code.", 30 | "main": "dist/index.js", 31 | "type": "module", 32 | "types": "dist/index.d.ts", 33 | "scripts": { 34 | "architecture": "node bin/jsarch.js src/*.ts > ARCHITECTURE.md", 35 | "build": "rimraf 'dist' && tsc --outDir dist", 36 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", 37 | "cli": "env NODE_ENV=${NODE_ENV:-cli}", 38 | "cover": "npm run jest -- --coverage", 39 | "cz": "env NODE_ENV=${NODE_ENV:-cli} git cz", 40 | "doc": "echo \"# API\" > API.md; jsdoc2md 'dist/**/*.js' >> API.md && git add API.md", 41 | "format": "npm run prettier", 42 | "graph": "npm run graph:build && npm run graph:generate && git add DEPENDENCIES.mmd*", 43 | "graph:build": "MERMAID_RUN=1 node bin/jsarch.js > DEPENDENCIES.mmd", 44 | "graph:generate": "docker run --rm -u `id -u`:`id -g` -v $(pwd):/data minlag/mermaid-cli -i DEPENDENCIES.mmd -o DEPENDENCIES.mmd.svg", 45 | "jest": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest", 46 | "jsarch": "node bin/jsarch.js", 47 | "lint": "eslint 'src/**/*.ts'", 48 | "metapak": "metapak", 49 | "mocha": "mocha --require '@babel/register' src/*.mocha.js", 50 | "precz": "npm run build && npm run graph && npm run architecture && npm run doc && npm t && npm run lint && npm run metapak -- -s", 51 | "prettier": "prettier --write 'src/**/*.ts'", 52 | "preversion": "npm run build && npm t && npm run lint && npm run metapak -- -s && npm run doc", 53 | "rebuild": "swc ./src -s -d dist -C jsc.target=es2022", 54 | "test": "npm run jest", 55 | "type-check": "tsc --pretty --noEmit", 56 | "version": "npm run changelog" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/nfroidure/jsarch.git" 61 | }, 62 | "keywords": [ 63 | "architecture", 64 | "documentation" 65 | ], 66 | "bin": { 67 | "jsarch": "bin/jsarch.js" 68 | }, 69 | "dependencies": { 70 | "@babel/parser": "^7.26.2", 71 | "ast-types": "^0.16.1", 72 | "commander": "^12.1.0", 73 | "debug": "^4.3.7", 74 | "deep-extend": "^0.6.0", 75 | "glob": "^11.0.0", 76 | "knifecycle": "^18.0.0", 77 | "pkg-dir": "^8.0.0", 78 | "rc": "^1.2.8", 79 | "yerror": "^8.0.0" 80 | }, 81 | "devDependencies": { 82 | "@eslint/js": "^9.16.0", 83 | "@swc/cli": "^0.5.2", 84 | "@swc/core": "^1.10.0", 85 | "@swc/helpers": "^0.5.15", 86 | "@swc/jest": "^0.2.37", 87 | "@types/node": "^18.14.6", 88 | "commitizen": "^4.3.1", 89 | "conventional-changelog-cli": "^5.0.0", 90 | "cz-conventional-changelog": "^3.3.0", 91 | "eslint": "^9.16.0", 92 | "eslint-config-prettier": "^9.1.0", 93 | "eslint-plugin-jest": "^28.9.0", 94 | "eslint-plugin-prettier": "^5.2.1", 95 | "jest": "^29.7.0", 96 | "jsdoc-to-markdown": "^9.1.1", 97 | "metapak": "^6.0.1", 98 | "metapak-nfroidure": "19.0.0", 99 | "prettier": "^3.4.2", 100 | "rimraf": "^6.0.1", 101 | "typescript": "^5.7.2", 102 | "typescript-eslint": "^8.17.0" 103 | }, 104 | "author": { 105 | "name": "Nicolas Froidure", 106 | "email": "nicolas.froidure@insertafter.com", 107 | "url": "http://insertafter.com/en/index.html" 108 | }, 109 | "license": "MIT", 110 | "bugs": { 111 | "url": "https://github.com/nfroidure/jsarch/issues" 112 | }, 113 | "homepage": "https://github.com/nfroidure/jsarch#readme", 114 | "engines": { 115 | "node": ">=20.11.1" 116 | }, 117 | "config": { 118 | "commitizen": { 119 | "path": "./node_modules/cz-conventional-changelog" 120 | } 121 | }, 122 | "contributors": [ 123 | { 124 | "name": "Antoine Demon-Chaine", 125 | "url": "https://github.com/antoinedmc" 126 | } 127 | ], 128 | "files": [ 129 | "bin/**/*.js", 130 | "dist", 131 | "src", 132 | "LICENSE", 133 | "README.md", 134 | "CHANGELOG.md" 135 | ], 136 | "greenkeeper": { 137 | "ignore": [ 138 | "commitizen", 139 | "cz-conventional-changelog", 140 | "conventional-changelog-cli", 141 | "jest", 142 | "@swc/jest", 143 | "typescript", 144 | "rimraf", 145 | "@swc/cli", 146 | "@swc/core", 147 | "@swc/helpers", 148 | "eslint", 149 | "prettier", 150 | "eslint-config-prettier", 151 | "eslint-plugin-prettier", 152 | "typescript-eslint", 153 | "jsdoc-to-markdown" 154 | ] 155 | }, 156 | "prettier": { 157 | "semi": true, 158 | "printWidth": 80, 159 | "singleQuote": true, 160 | "trailingComma": "all", 161 | "proseWrap": "always" 162 | }, 163 | "jest": { 164 | "coverageReporters": [ 165 | "lcov" 166 | ], 167 | "testPathIgnorePatterns": [ 168 | "/node_modules/" 169 | ], 170 | "roots": [ 171 | "/src" 172 | ], 173 | "transform": { 174 | "^.+\\.tsx?$": [ 175 | "@swc/jest", 176 | {} 177 | ] 178 | }, 179 | "testEnvironment": "node", 180 | "moduleNameMapper": { 181 | "(.+)\\.js": "$1" 182 | }, 183 | "extensionsToTreatAsEsm": [ 184 | ".ts" 185 | ], 186 | "prettierPath": null 187 | }, 188 | "jsarch": { 189 | "parserOptions": { 190 | "plugins": [ 191 | "typescript" 192 | ] 193 | } 194 | }, 195 | "overrides": { 196 | "eslint": "^9.16.0" 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/compareNotes.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from '@jest/globals'; 2 | import { compareNotes } from './compareNotes.js'; 3 | 4 | describe('compareNotes()', () => { 5 | test('should well compare one level notes', () => { 6 | expect(compareNotes({ num: '3' }, { num: '4' })).toEqual(-1); 7 | expect(compareNotes({ num: '4' }, { num: '3' })).toEqual(1); 8 | expect(compareNotes({ num: '1' }, { num: '10' })).toEqual(-1); 9 | expect(compareNotes({ num: '10' }, { num: '1' })).toEqual(1); 10 | }); 11 | 12 | test('should well compare two level notes', () => { 13 | expect(compareNotes({ num: '10.1' }, { num: '10.2' })).toEqual(-1); 14 | expect(compareNotes({ num: '10.2' }, { num: '10.1' })).toEqual(1); 15 | expect(compareNotes({ num: '10.1' }, { num: '10.20' })).toEqual(-1); 16 | expect(compareNotes({ num: '10.20' }, { num: '10.1' })).toEqual(1); 17 | }); 18 | 19 | test('should well compare different level notes', () => { 20 | expect(compareNotes({ num: '10' }, { num: '10.1' })).toEqual(-1); 21 | expect(compareNotes({ num: '10.1' }, { num: '10' })).toEqual(1); 22 | expect(compareNotes({ num: '10.1.3' }, { num: '10.1.4.5' })).toEqual(-1); 23 | expect(compareNotes({ num: '10.20' }, { num: '10.1.1' })).toEqual(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/compareNotes.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedNote } from './jsarch.js'; 2 | 3 | /* Architecture Note #1.2: Ordering 4 | 5 | To order architecture notes in a meaningful way we 6 | use title hierarchy like we used too at school with 7 | argumentative texts ;). 8 | 9 | A sample tree structure could be: 10 | - 1 11 | - 1.1 12 | - 1.2 13 | - 2 14 | - 3 15 | 16 | */ 17 | export function compareNotes( 18 | aNote: Pick, 19 | bNote: Pick, 20 | ): number { 21 | const aNoteLevels = aNote.num.split('.').map((n) => parseInt(n, 10)); 22 | const bNoteLevels = bNote.num.split('.').map((n) => parseInt(n, 10)); 23 | const levelsDepth = Math.max(aNoteLevels.length, bNoteLevels.length); 24 | let result = 0; 25 | 26 | result = new Array(levelsDepth).fill('').reduce((result, unused, index) => { 27 | if (0 !== result) { 28 | return result; 29 | } else if ('undefined' === typeof aNoteLevels[index]) { 30 | result = -1; 31 | } else if ('undefined' === typeof bNoteLevels[index]) { 32 | result = 1; 33 | } else if (aNoteLevels[index] === bNoteLevels[index]) { 34 | result = 0; 35 | } else if (aNoteLevels[index] < bNoteLevels[index]) { 36 | result = -1; 37 | } else if (aNoteLevels[index] > bNoteLevels[index]) { 38 | result = 1; 39 | } 40 | return result; 41 | }, result); 42 | return result; 43 | } 44 | -------------------------------------------------------------------------------- /src/jsarch.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | describe, 3 | beforeEach, 4 | afterEach, 5 | test, 6 | expect, 7 | jest, 8 | } from '@jest/globals'; 9 | import { Knifecycle, constant } from 'knifecycle'; 10 | import initJSArch, { DEFAULT_CONFIG } from './jsarch.js'; 11 | import initParser from './parser.js'; 12 | import { glob as _glob } from 'glob'; 13 | import fs from 'fs'; 14 | 15 | const JSARCH_PREFIX = `[//]: # ( ) 16 | [//]: # (This file is automatically generated by the \`jsarch\`) 17 | [//]: # (module. Do not change it elsewhere, changes would) 18 | [//]: # (be overriden.) 19 | [//]: # ( ) 20 | `; 21 | 22 | describe('jsArch service', () => { 23 | const log = jest.fn(); 24 | const glob = jest.fn(); 25 | const readFile = jest.fn(); 26 | let $; 27 | 28 | beforeEach(() => { 29 | $ = new Knifecycle(); 30 | $.register(constant('EOL', '\n')); 31 | $.register(constant('CONFIG', DEFAULT_CONFIG)); 32 | $.register(constant('log', log)); 33 | $.register(constant('glob', glob)); 34 | $.register(constant('fs', { readFile: readFile })); 35 | $.register(initParser); 36 | $.register(initJSArch); 37 | }); 38 | 39 | afterEach(() => { 40 | log.mockReset(); 41 | glob.mockReset(); 42 | readFile.mockReset(); 43 | }); 44 | 45 | test('with no architecture notes', async () => { 46 | glob.mockResolvedValueOnce([ 47 | '/home/me/project/lulz.js', 48 | '/home/me/project/kikoo.js', 49 | ]); 50 | 51 | readFile.mockResolvedValue(` 52 | 53 | console.log('test'); 54 | 55 | `); 56 | 57 | const { jsArch } = await $.run(['jsArch']); 58 | const content = await jsArch({ 59 | patterns: ['**/*.js'], 60 | base: './blob/master', 61 | cwd: '/home/me/project', 62 | }); 63 | expect(readFile.mock.calls).toEqual([ 64 | ['/home/me/project/lulz.js', 'utf-8'], 65 | ['/home/me/project/kikoo.js', 'utf-8'], 66 | ]); 67 | expect(content).toEqual(''); 68 | }); 69 | 70 | test('with some architecture notes in a file', async () => { 71 | glob.mockResolvedValueOnce(['/home/me/project/kikoo.js']); 72 | 73 | readFile.mockResolvedValueOnce(` 74 | 75 | /* Architecture Note #1: Title 76 | 77 | Some content ! 78 | */ 79 | 80 | console.log('test'); 81 | 82 | `); 83 | 84 | const { jsArch } = await $.run(['jsArch']); 85 | const content = await jsArch({ 86 | patterns: ['**/*.js'], 87 | base: './blob/master', 88 | cwd: '/home/me/project', 89 | }); 90 | expect(readFile.mock.calls).toEqual([ 91 | ['/home/me/project/kikoo.js', 'utf-8'], 92 | ]); 93 | expect(content).toEqual( 94 | `${JSARCH_PREFIX}# Architecture Notes 95 | 96 | ## Summary 97 | 98 | 1. [Title](#1-title) 99 | 100 | 101 | ## 1. Title 102 | 103 | Some content ! 104 | 105 | [See in context](./blob/master/kikoo.js#L3-L6) 106 | 107 | `, 108 | ); 109 | }); 110 | 111 | test('with some architecture notes containing a $', async () => { 112 | glob.mockResolvedValueOnce(['/home/me/project/kikoo.js']); 113 | 114 | readFile.mockResolvedValueOnce(` 115 | 116 | /* Architecture Note #1: \`$autoload\` 117 | 118 | Some content ! 119 | */ 120 | 121 | console.log('test'); 122 | 123 | `); 124 | 125 | const { jsArch } = await $.run(['jsArch']); 126 | const content = await jsArch({ 127 | patterns: ['**/*.js'], 128 | base: './blob/master', 129 | cwd: '/home/me/project', 130 | }); 131 | expect(readFile.mock.calls).toEqual([ 132 | ['/home/me/project/kikoo.js', 'utf-8'], 133 | ]); 134 | expect(content).toEqual( 135 | `${JSARCH_PREFIX}# Architecture Notes 136 | 137 | ## Summary 138 | 139 | 1. [\`$autoload\`](#1-\`$autoload\`) 140 | 141 | 142 | ## 1. \`$autoload\` 143 | 144 | Some content ! 145 | 146 | [See in context](./blob/master/kikoo.js#L3-L6) 147 | 148 | `, 149 | ); 150 | }); 151 | 152 | test('with some architecture notes in a TypeScript file', async () => { 153 | $.register( 154 | constant('CONFIG', { 155 | ...DEFAULT_CONFIG, 156 | parserOptions: { 157 | ...DEFAULT_CONFIG.parserOptions, 158 | plugins: ['typescript'], 159 | }, 160 | }), 161 | ); 162 | glob.mockResolvedValueOnce(['/home/me/project/kikoo.js']); 163 | 164 | readFile.mockResolvedValueOnce(` 165 | 166 | interface lol { 167 | lol : string; 168 | } 169 | 170 | /* Architecture Note #1: Title 171 | 172 | Some content ! 173 | */ 174 | 175 | console.log('test'); 176 | 177 | `); 178 | 179 | const { jsArch } = await $.run(['jsArch']); 180 | const content = await jsArch({ 181 | patterns: ['**/*.js'], 182 | base: './blob/master', 183 | cwd: '/home/me/project', 184 | }); 185 | expect(readFile.mock.calls).toEqual([ 186 | ['/home/me/project/kikoo.js', 'utf-8'], 187 | ]); 188 | expect(content).toEqual( 189 | `${JSARCH_PREFIX}# Architecture Notes 190 | 191 | ## Summary 192 | 193 | 1. [Title](#1-title) 194 | 195 | 196 | ## 1. Title 197 | 198 | Some content ! 199 | 200 | [See in context](./blob/master/kikoo.js#L7-L10) 201 | 202 | `, 203 | ); 204 | }); 205 | 206 | test('with some architecture notes in a file and bitbucket links', async () => { 207 | $.register( 208 | constant('CONFIG', { 209 | ...DEFAULT_CONFIG, 210 | gitProvider: 'bitbucket', 211 | }), 212 | ); 213 | 214 | glob.mockResolvedValueOnce(['/home/me/project/kikoo.js']); 215 | 216 | readFile.mockResolvedValueOnce(` 217 | 218 | /* Architecture Note #1: Title 219 | 220 | Some content ! 221 | */ 222 | 223 | console.log('test'); 224 | 225 | `); 226 | 227 | const { jsArch } = await $.run(['jsArch']); 228 | const content = await jsArch({ 229 | patterns: ['**/*.js'], 230 | base: './blob/master', 231 | cwd: '/home/me/project', 232 | }); 233 | expect(readFile.mock.calls).toEqual([ 234 | ['/home/me/project/kikoo.js', 'utf-8'], 235 | ]); 236 | expect(content).toEqual( 237 | `${JSARCH_PREFIX}# Architecture Notes 238 | 239 | ## Summary 240 | 241 | 1. [Title](#1-title) 242 | 243 | 244 | ## 1. Title 245 | 246 | Some content ! 247 | 248 | [See in context](./blob/master/kikoo.js#kikoo.js-3:6) 249 | 250 | `, 251 | ); 252 | }); 253 | 254 | test('with some indented architecture notes in a file', async () => { 255 | glob.mockResolvedValueOnce(['/home/me/project/kikoo.js']); 256 | 257 | readFile.mockResolvedValueOnce(` 258 | 259 | /* Architecture Note #1: Title 260 | 261 | Some content ! 262 | Nice! 263 | */ 264 | 265 | console.log('test'); 266 | 267 | `); 268 | 269 | const { jsArch } = await $.run(['jsArch']); 270 | const content = await jsArch({ 271 | patterns: ['**/*.js'], 272 | base: './blob/master', 273 | cwd: '/home/me/project', 274 | }); 275 | expect(readFile.mock.calls).toEqual([ 276 | ['/home/me/project/kikoo.js', 'utf-8'], 277 | ]); 278 | expect(content).toEqual( 279 | `${JSARCH_PREFIX}# Architecture Notes 280 | 281 | ## Summary 282 | 283 | 1. [Title](#1-title) 284 | 285 | 286 | ## 1. Title 287 | 288 | Some content ! 289 | Nice! 290 | 291 | [See in context](./blob/master/kikoo.js#L3-L7) 292 | 293 | `, 294 | ); 295 | }); 296 | 297 | test('with architecture notes in several files', async () => { 298 | glob.mockResolvedValueOnce([ 299 | '/home/me/project/lulz.js', 300 | '/home/me/project/kikoo.js', 301 | ]); 302 | 303 | readFile.mockResolvedValueOnce(` 304 | /* Architecture Note #1.1: Title 305 | 306 | Some content ! 307 | */ 308 | 309 | console.log('test'); 310 | /* Architecture Note #1: Title 311 | 312 | Some content ! 313 | */ 314 | 315 | `); 316 | 317 | readFile.mockResolvedValueOnce(` 318 | /* Architecture Note #1.3: Title 319 | 320 | Some content ! 321 | */ 322 | 323 | console.log('test'); 324 | /* Architecture Note #2: Title 325 | 326 | Some content ! 327 | */ 328 | 329 | `); 330 | 331 | const { jsArch } = await $.run(['jsArch']); 332 | const content = await jsArch({ 333 | patterns: ['**/*.js'], 334 | base: './blob/master', 335 | cwd: '/home/me/project', 336 | }); 337 | expect(readFile.mock.calls).toEqual([ 338 | ['/home/me/project/lulz.js', 'utf-8'], 339 | ['/home/me/project/kikoo.js', 'utf-8'], 340 | ]); 341 | expect(content).toEqual( 342 | `${JSARCH_PREFIX}# Architecture Notes 343 | 344 | ## Summary 345 | 346 | 1. [Title](#1-title) 347 | 1. [Title](#11-title) 348 | 3. [Title](#13-title) 349 | 2. [Title](#2-title) 350 | 351 | 352 | ## 1. Title 353 | 354 | Some content ! 355 | 356 | [See in context](./blob/master/lulz.js#L8-L11) 357 | 358 | 359 | 360 | ### 1.1. Title 361 | 362 | Some content ! 363 | 364 | [See in context](./blob/master/lulz.js#L2-L5) 365 | 366 | 367 | 368 | ### 1.3. Title 369 | 370 | Some content ! 371 | 372 | [See in context](./blob/master/kikoo.js#L2-L5) 373 | 374 | 375 | 376 | ## 2. Title 377 | 378 | Some content ! 379 | 380 | [See in context](./blob/master/kikoo.js#L8-L11) 381 | 382 | `, 383 | ); 384 | }); 385 | }); 386 | -------------------------------------------------------------------------------- /src/jsarch.ts: -------------------------------------------------------------------------------- 1 | import { service, name, autoInject } from 'knifecycle'; 2 | import { YError } from 'yerror'; 3 | import path from 'path'; 4 | import { Type, finalize, visit } from 'ast-types'; 5 | import { compareNotes } from './compareNotes.js'; 6 | import type { ParserOptions } from '@babel/parser'; 7 | 8 | const { def } = Type; 9 | 10 | // Temporary fix to make jsarch work 11 | // on codebases parsed with espree 12 | def('ExperimentalSpreadProperty') 13 | .bases('Node') 14 | .build('argument') 15 | .field('argument', def('Expression')); 16 | def('ExperimentalRestProperty') 17 | .bases('Node') 18 | .build('argument') 19 | .field('argument', def('Expression')); 20 | finalize(); 21 | 22 | export type ParsedNote = { 23 | num: string; 24 | title: string; 25 | content: string; 26 | filePath: string; 27 | loc: { 28 | start: { line: number }; 29 | end: { line: number }; 30 | }; 31 | }; 32 | 33 | export type JSArchConfig = { 34 | gitProvider: 'github' | 'bitbucket'; 35 | parser: string; 36 | parserOptions: ParserOptions; 37 | }; 38 | export type JSArchOptions = { 39 | cwd: string; 40 | patterns: string[]; 41 | eol?: string; 42 | titleLevel?: string; 43 | base: string; 44 | }; 45 | export type JSArchService = (options: JSArchOptions) => Promise; 46 | 47 | export const DEFAULT_CONFIG: JSArchConfig = { 48 | gitProvider: 'github', 49 | parser: '@babel/parser', 50 | parserOptions: { 51 | attachComment: true, 52 | sourceType: 'module', 53 | loc: true, 54 | range: true, 55 | ecmaVersion: 8, 56 | ecmaFeatures: { 57 | jsx: false, 58 | globalReturn: false, 59 | impliedStrict: false, 60 | experimentalObjectRestSpread: true, 61 | }, 62 | } as ParserOptions, 63 | }; 64 | 65 | const ARCHITECTURE_NOTE_REGEXP = 66 | /^\s*Architecture Note #((?:\d+(?:\.(?=\d)|)){1,8}):\s+([^\r\n]*|$)/; 67 | const SHEBANG_REGEXP = /#! (\/\w+)+ node/; 68 | const EOL_REGEXP = /\n/g; 69 | const JSARCH_PREFIX = `[//]: # ( ) 70 | [//]: # (This file is automatically generated by the \`jsarch\`) 71 | [//]: # (module. Do not change it elsewhere, changes would) 72 | [//]: # (be overriden.) 73 | [//]: # ( ) 74 | `; 75 | 76 | /* Architecture Note #1.3: Title level 77 | 78 | By default, titles will be added like if the architecture 79 | notes were in a single separated file. 80 | 81 | If you wish to add the architecture notes in a README.md file 82 | you will have to set the `titleLevel` option to as much `#` 83 | as necessar to fit the title hierarchy of you README file. 84 | */ 85 | const TITLE_LEVEL = '#'; 86 | 87 | /* Architecture Note #1.4: Base 88 | 89 | By default, links to the architecture notes right in the code 90 | are supposed relative to the README.md file like you would 91 | find it in the GitHub homepage of you projects. 92 | 93 | To override it, use the `base` option. 94 | 95 | */ 96 | const BASE = '.'; 97 | 98 | /* Architecture Note #1: jsArch service 99 | 100 | JSArch is basically a service that exposes a function allowing 101 | to extract and output architecture notes from the code. 102 | 103 | This service needs some other services. To be able to mock and 104 | interchange them, we use 105 | [Knifecycle](https://github.com/nfroidure/knifecycle) for its 106 | dependency injection and inversion of control feature. 107 | 108 | ![Dependencies Graph](./DEPENDENCIES.mmd.svg) 109 | */ 110 | export default service(name('jsArch', autoInject(initJSArch))); 111 | 112 | /** 113 | * Declare jsArch in the dependency injection system 114 | * @param {Object} services 115 | * Services (provided by the dependency injector) 116 | * @param {Object} services.CONFIG 117 | * The JSArch config 118 | * @param {Object} services.EOL 119 | * The OS EOL chars 120 | * @param {Object} services.glob 121 | * Globbing service 122 | * @param {Object} services.fs 123 | * File system service 124 | * @param {Object} services.parser 125 | * Parser service 126 | * @param {Object} [services.log = noop] 127 | * Logging service 128 | * @returns {Promise} 129 | */ 130 | async function initJSArch({ 131 | CONFIG, 132 | EOL, 133 | glob, 134 | fs, 135 | parser, 136 | log = noop, 137 | }): Promise { 138 | return jsArch; 139 | 140 | /** 141 | * Compile an run a template 142 | * @param {Object} options 143 | * Options (destructured) 144 | * @param {Object} options.cwd 145 | * Current working directory 146 | * @param {Object} options.patterns 147 | * Patterns to look files for (see node-glob) 148 | * @param {Object} options.eol 149 | * End of line character (default to the OS one) 150 | * @param {Object} options.titleLevel 151 | * The base title level of the output makdown document 152 | * @param {Object} options.base 153 | * The base directory for the ARCHITECTURE.md references 154 | * @return {Promise} 155 | * Computed architecture notes as a markdown file 156 | */ 157 | async function jsArch({ 158 | cwd, 159 | patterns, 160 | eol = EOL, 161 | titleLevel = TITLE_LEVEL, 162 | base = BASE, 163 | }): Promise { 164 | const files = await _computePatterns({ glob, log }, cwd, patterns); 165 | const architectureNotes = _linearize( 166 | await Promise.all( 167 | files.map(_extractArchitectureNotes.bind(null, { parser, fs, log })), 168 | ), 169 | ); 170 | 171 | const summary = architectureNotes 172 | .sort(compareNotes) 173 | .reduce((summary, architectureNote) => { 174 | const titleAnchor = 175 | architectureNote.num.replace(/\./g, '') + 176 | '-' + 177 | architectureNote.title.toLowerCase().replace(/ /g, '-'); 178 | 179 | const titleNums = architectureNote.num.split('.'); 180 | 181 | let tabulation = ''; 182 | for (let i = 0; i < titleNums.length; i++) { 183 | if (i > 0) { 184 | tabulation += ' '; 185 | } 186 | } 187 | 188 | return ( 189 | summary + 190 | eol + 191 | tabulation + 192 | titleNums[titleNums.length - 1] + 193 | '. [' + 194 | architectureNote.title + 195 | ']' + 196 | '(#' + 197 | titleAnchor + 198 | ')' 199 | ); 200 | }, ''); 201 | 202 | const content = architectureNotes 203 | .sort(compareNotes) 204 | .reduce((content, architectureNote) => { 205 | let linesLink = 206 | '#L' + 207 | architectureNote.loc.start.line + 208 | '-L' + 209 | architectureNote.loc.end.line; 210 | if (CONFIG.gitProvider.toLowerCase() === 'bitbucket') { 211 | linesLink = 212 | '#' + 213 | path.basename(architectureNote.filePath) + 214 | '-' + 215 | architectureNote.loc.start.line + 216 | ':' + 217 | architectureNote.loc.end.line; 218 | } 219 | return ( 220 | content + 221 | eol + 222 | eol + 223 | titleLevel + 224 | architectureNote.num 225 | .split('.') 226 | .map(() => '#') 227 | .join('') + 228 | ' ' + 229 | architectureNote.num + 230 | '. ' + 231 | architectureNote.title + 232 | eol + 233 | eol + 234 | architectureNote.content.replace( 235 | new RegExp( 236 | '([\r\n]+)[ \t]{' + architectureNote.loc.start.column + '}', 237 | 'g', 238 | ), 239 | '$1', 240 | ) + 241 | eol + 242 | eol + 243 | '[See in context](' + 244 | base + 245 | '/' + 246 | path.relative(cwd, architectureNote.filePath) + 247 | linesLink + 248 | ')' + 249 | eol + 250 | eol 251 | ); 252 | }, ''); 253 | 254 | if (content && summary) { 255 | return ( 256 | JSARCH_PREFIX.replace(EOL_REGEXP, eol) + 257 | titleLevel + 258 | ' Architecture Notes' + 259 | eol + 260 | eol + 261 | '## Summary' + 262 | eol + 263 | summary + 264 | eol + 265 | content 266 | ); 267 | } 268 | return content; 269 | } 270 | } 271 | 272 | async function _computePatterns({ glob, log }, cwd, patterns) { 273 | return _linearize( 274 | await Promise.all( 275 | patterns.map(async (pattern) => { 276 | log('debug', 'Processing pattern:', pattern); 277 | try { 278 | const files = await glob(pattern, { 279 | cwd, 280 | dot: true, 281 | nodir: true, 282 | absolute: true, 283 | }); 284 | 285 | log('debug', 'Pattern sucessfully resolved', pattern); 286 | log('debug', 'Files:', files); 287 | return files; 288 | } catch (err) { 289 | log('error', 'Pattern failure:', pattern); 290 | log('stack', 'Stack:', (err as Error).stack); 291 | throw YError.wrap(err as Error, 'E_PATTERN_FAILURE', pattern); 292 | } 293 | }), 294 | ), 295 | ); 296 | } 297 | 298 | /* Architecture Note #1.1: Extraction 299 | 300 | We use AST parsing and visiting to retrieve well formed 301 | architecture notes. It should be structured like that: 302 | ```js 303 | 304 | /** Architecture Note #{order}: {title} 305 | 306 | {body} 307 | ``` 308 | */ 309 | async function _extractArchitectureNotes({ parser, fs, log }, filePath) { 310 | let content; 311 | 312 | log('debug', 'Reading file at', filePath); 313 | 314 | try { 315 | content = await fs.readFile(filePath, 'utf-8'); 316 | 317 | log('debug', 'File sucessfully read', filePath); 318 | 319 | if (SHEBANG_REGEXP.test(content)) { 320 | log('debug', 'Found a shebang, commenting it', filePath); 321 | content = '// Shebang commented by jsarch: ' + content; 322 | } 323 | } catch (err) { 324 | log('error', 'File read failure:', filePath); 325 | log('stack', 'Stack:', (err as Error).stack); 326 | throw YError.wrap(err as Error, 'E_FILE_FAILURE', filePath); 327 | } 328 | 329 | try { 330 | const ast = parser(content); 331 | const architectureNotes: ParsedNote[] = []; 332 | 333 | visit(ast, { 334 | visitComment: function (path) { 335 | const comment = path.value.value; 336 | const matches = ARCHITECTURE_NOTE_REGEXP.exec(comment); 337 | 338 | if (matches) { 339 | architectureNotes.push({ 340 | num: matches[1], 341 | title: matches[2].trim(), 342 | content: comment.substr(matches[0].length).trim(), 343 | filePath: filePath, 344 | loc: path.value.loc, 345 | }); 346 | } 347 | this.traverse(path); 348 | }, 349 | }); 350 | 351 | log('debug', 'File sucessfully processed', path); 352 | log( 353 | 'debug', 354 | 'Architecture notes found:', 355 | architectureNotes.map((a) => a.title), 356 | ); 357 | 358 | return architectureNotes; 359 | } catch (err) { 360 | log('error', 'File parse failure:', filePath); 361 | log('stack', 'Stack:', (err as Error).stack); 362 | throw YError.wrap(err as Error, 'E_FILE_PARSE_FAILURE', filePath); 363 | } 364 | } 365 | 366 | function _linearize(bulks) { 367 | return bulks.reduce((array, arrayBulk) => array.concat(arrayBulk), []); 368 | } 369 | 370 | function noop() { 371 | return; 372 | } 373 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { autoService } from 'knifecycle'; 2 | import { YError } from 'yerror'; 3 | import { DEFAULT_CONFIG } from './jsarch.js'; 4 | import type { JSArchConfig } from './jsarch.js'; 5 | import type { parse } from '@babel/parser'; 6 | 7 | export default autoService(initParser); 8 | 9 | export type Parser = (content: string) => ReturnType; 10 | 11 | async function initParser({ 12 | CONFIG = DEFAULT_CONFIG, 13 | }: { 14 | CONFIG: Partial; 15 | }): Promise { 16 | let parser: typeof parse; 17 | 18 | try { 19 | parser = (await import(CONFIG.parser || '@babel/parser')).default 20 | .parse as typeof parse; 21 | } catch (err) { 22 | throw YError.wrap(err as Error, 'E_PARSER_LACK', CONFIG.parser); 23 | } 24 | 25 | return (content) => parser(content, CONFIG.parserOptions); 26 | } 27 | -------------------------------------------------------------------------------- /src/runJSArch.ts: -------------------------------------------------------------------------------- 1 | /* Architecture Note #2: CLI 2 | 3 | The JSArch CLI tool basically wraps the jsArch service 4 | to make it usable from the CLI. 5 | 6 | To see its options, run: 7 | ``` 8 | jsarch -h 9 | ``` 10 | */ 11 | import { 12 | Knifecycle, 13 | constant, 14 | name, 15 | service, 16 | autoInject, 17 | autoService, 18 | } from 'knifecycle'; 19 | import initDebug from 'debug'; 20 | import initParser from './parser.js'; 21 | import fs from 'fs'; 22 | import os from 'os'; 23 | import path from 'path'; 24 | import { glob } from 'glob'; 25 | import { createCommand } from 'commander'; 26 | import deepExtend from 'deep-extend'; 27 | import rc from 'rc'; 28 | import { packageDirectory } from 'pkg-dir'; 29 | import initJSArch, { DEFAULT_CONFIG } from './jsarch.js'; 30 | import { dirname } from 'path'; 31 | import { fileURLToPath } from 'url'; 32 | import type { Command } from 'commander'; 33 | import type { JSArchService } from './jsarch.js'; 34 | 35 | const __dirname = dirname(fileURLToPath(import.meta.url)); 36 | 37 | export default async function runJSArch() { 38 | try { 39 | const $ = await prepareJSArch(); 40 | const { ENV, jsArch, program } = await $.run<{ 41 | ENV: typeof process.env; 42 | jsArch: JSArchService; 43 | program: Command; 44 | }>(['ENV', 'jsArch', 'program']); 45 | 46 | if (ENV.MERMAID_RUN) { 47 | const JSARCH_REG_EXP = /^jsArch$/; 48 | const CONFIG_REG_EXP = /^([A-Z0-9_]+)$/; 49 | const MERMAID_GRAPH_CONFIG = { 50 | classes: { 51 | jsarch: 'fill:#e7cdd2,stroke:#ebd4cb,stroke-width:1px,color:#000;', 52 | config: 'fill:#d4cdcc,stroke:#ebd4cb,stroke-width:1px,color:#000;', 53 | others: 'fill:#ebd4cb,stroke:#000,stroke-width:1px,color:#000;', 54 | }, 55 | styles: [ 56 | { 57 | pattern: JSARCH_REG_EXP, 58 | className: 'jsarch', 59 | }, 60 | { 61 | pattern: CONFIG_REG_EXP, 62 | className: 'config', 63 | }, 64 | { 65 | pattern: /^(.+)$/, 66 | className: 'others', 67 | }, 68 | ], 69 | shapes: [ 70 | { 71 | pattern: JSARCH_REG_EXP, 72 | template: '$0(($0))', 73 | }, 74 | { 75 | pattern: CONFIG_REG_EXP, 76 | template: '$0{$0}', 77 | }, 78 | ], 79 | }; 80 | process.stdout.write($.toMermaidGraph(MERMAID_GRAPH_CONFIG)); 81 | process.exit(0); 82 | } 83 | const content = await jsArch({ 84 | patterns: program.args, 85 | cwd: process.cwd(), 86 | base: program.opts().base as string, 87 | }); 88 | process.stdout.write(content); 89 | } catch (err) { 90 | console.error(err); 91 | process.exit(1); 92 | } 93 | } 94 | 95 | async function prepareJSArch($ = new Knifecycle()) { 96 | const debug = initDebug('jsarch'); 97 | 98 | $.register( 99 | autoService(async function initProgram({ packageConf }) { 100 | return createCommand() 101 | .version(packageConf.version) 102 | .option('-b, --base [value]', 'Base for links') 103 | .parse(process.argv); 104 | }), 105 | ); 106 | $.register(initParser); 107 | $.register( 108 | constant( 109 | 'packageConf', 110 | JSON.parse( 111 | ( 112 | await fs.promises.readFile(path.join(__dirname, '..', 'package.json')) 113 | ).toString(), 114 | ), 115 | ), 116 | ); 117 | $.register(constant('fs', fs.promises)); 118 | $.register(constant('EOL', os.EOL)); 119 | $.register(constant('ENV', process.env)); 120 | $.register( 121 | name( 122 | 'PROJECT_DIR', 123 | service(async () => packageDirectory()), 124 | ), 125 | ); 126 | $.register( 127 | name( 128 | 'CONFIG', 129 | service( 130 | autoInject(async function initConfig({ PROJECT_DIR }) { 131 | const packageJSON = JSON.parse( 132 | ( 133 | await fs.promises.readFile(path.join(PROJECT_DIR, 'package.json')) 134 | ).toString(), 135 | ); 136 | const baseConfig = deepExtend( 137 | {}, 138 | packageJSON.jsarch || {}, 139 | DEFAULT_CONFIG, 140 | ); 141 | 142 | return rc('jsarch', baseConfig); 143 | }), 144 | ), 145 | ), 146 | ); 147 | $.register(constant('glob', glob)); 148 | $.register( 149 | constant('log', (type: string, ...args: string[]) => { 150 | if ('debug' === type || 'stack' === type) { 151 | debug(args[0], ...args.slice(1)); 152 | return; 153 | } 154 | console[type](...args); 155 | }), 156 | ); 157 | $.register(initJSArch); 158 | 159 | return $; 160 | } 161 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function deepExtend(target: T, ...sources: T[]): T { 2 | for (const source of sources) { 3 | for (const key in source) { 4 | if (Object.prototype.hasOwnProperty.call(source, key)) { 5 | target[key] = source[key]; 6 | } 7 | } 8 | } 9 | 10 | return target; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "moduleResolution": "Node16", 5 | "target": "es2022", 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "preserveConstEnums": true, 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "declaration": true, 13 | "outDir": "dist", 14 | "sourceMap": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } --------------------------------------------------------------------------------