├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── codecov.yml ├── package-lock.json ├── package.json ├── src └── index.ts ├── test ├── fixtures.ts └── index.test.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Begin CI... 13 | uses: actions/checkout@v2 14 | 15 | - name: Use Node 14 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 14.x 19 | 20 | - name: Use cached node_modules 21 | uses: actions/cache@v1 22 | with: 23 | path: node_modules 24 | key: nodeModules-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | nodeModules- 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | env: 31 | CI: true 32 | 33 | - name: Lint 34 | run: npm run lint 35 | env: 36 | CI: true 37 | 38 | - name: Test 39 | run: npm test -- --coverage 40 | env: 41 | CI: true 42 | 43 | - name: Build 44 | run: npm run build 45 | env: 46 | CI: true 47 | 48 | - name: Codecov 49 | run: npx codecov 50 | env: 51 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jason Etcovitch 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
Lil' helper for replacing a section of the contents of a README.
3 |
4 | 5 | ## Usage 6 | 7 | ### Installation 8 | 9 | ```sh 10 | $ npm install readme-box 11 | ``` 12 | 13 | ```js 14 | const { ReadmeBox } = require('readme-box') 15 | import { ReadmeBox } from 'readme-box' 16 | ``` 17 | 18 | You can quickly update a section of a README: 19 | 20 | ```js 21 | const result = await ReadmeBox.updateSection('New contents!', { 22 | owner: 'JasonEtco', 23 | repo: 'example', 24 | token: process.env.GITHUB_TOKEN, 25 | // branch is assumed to be 'master' by default, you can also specify `branch: 'main'` 26 | branch: 'main', 27 | section: 'example-section', 28 | // set to `true` to allow empty commits when there are no changes 29 | emptyCommits: false 30 | }) 31 | 32 | // `result` is the response object of the README update request or 33 | // `undefined` if no changes were made. 34 | ``` 35 | 36 | Or, if you need to access parts of it more granularly, you can use the `ReadmeBox` class methods: 37 | 38 | ```js 39 | const box = new ReadmeBox({ owner, repo, token }) 40 | 41 | // Get the contents of the README from the API 42 | const { content, sha } = await box.getReadme() 43 | 44 | // Get the contents of a section of the provided string 45 | const sectionContents = box.getSection('example-section', content) 46 | 47 | // Return a string with the replaced contents 48 | const replacedContents = box.replaceSection({ 49 | section: 'example-section', 50 | oldContent, 51 | newContent 52 | }) 53 | 54 | // Update the README via the API, with an optional commit message 55 | await box.updateReadme({ content, sha, message: 'Updating the README!' }) 56 | ``` 57 | 58 | ## How it works 59 | 60 | `ReadmeBox.updateSection` combines a couple of the methods exposed on the `ReadmeBox` class, to do the following: 61 | 62 | - Get the README file's contents from the API 63 | - Replace a section of it using Regular Expressions 64 | - Update the file via the API 65 | 66 | It expects your README to have a "section", using HTML comments: 67 | 68 | ```html 69 | Check out this README! 70 | 71 | 72 | Old contents... 73 | 74 | ``` 75 | 76 | When the above example code is run, everything between the start and end comments will be replaced. 77 | 78 | ## Local Development 79 | 80 | This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx). Below is a list of commands you will probably find useful. 81 | 82 | ### `npm start` 83 | 84 | Runs the project in development/watch mode. The project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab. The library will be rebuilt if you make edits. 85 | 86 | ### `npm run build` 87 | 88 | Bundles the package to the `dist` folder. 89 | The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module). 90 | s 91 | 92 | ### `npm test` 93 | 94 | Runs the test watcher (Jest) in an interactive mode. 95 | By default, runs tests related to files changed since the last commit. 96 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # Fail the status if coverage drops by >= 3% 6 | threshold: 3 7 | patch: 8 | default: 9 | threshold: 3 10 | 11 | comment: false 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readme-box", 3 | "author": "Jason Etcovitch", 4 | "description": "Lil' helper for replacing a section of the contents of a README.", 5 | "version": "1.0.0", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/JasonEtco/readme-box" 10 | }, 11 | "scripts": { 12 | "start": "tsdx watch", 13 | "build": "tsdx build", 14 | "test": "tsdx test", 15 | "lint": "tsdx lint", 16 | "prepare": "tsdx build" 17 | }, 18 | "main": "dist/index.js", 19 | "typings": "dist/index.d.ts", 20 | "module": "dist/readme-box.esm.js", 21 | "files": [ 22 | "dist", 23 | "src" 24 | ], 25 | "husky": { 26 | "hooks": { 27 | "pre-commit": "pretty-quick --staged" 28 | } 29 | }, 30 | "prettier": { 31 | "printWidth": 80, 32 | "semi": false, 33 | "singleQuote": true 34 | }, 35 | "dependencies": { 36 | "@octokit/request": "^5.6.0" 37 | }, 38 | "devDependencies": { 39 | "husky": "^6.0.0", 40 | "nock": "^13.1.0", 41 | "pretty-quick": "^3.1.1", 42 | "tsdx": "^0.14.1", 43 | "tslib": "^2.3.0", 44 | "typescript": "^4.3.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@octokit/request' 2 | 3 | export interface ReadmeBoxOpts { 4 | owner: string 5 | repo: string 6 | token: string 7 | branch?: string 8 | } 9 | 10 | export interface UpdateSectionOpts extends ReadmeBoxOpts { 11 | section: string 12 | message?: string 13 | emptyCommits?: boolean 14 | } 15 | 16 | export interface ReplaceSectionOpts { 17 | section: string 18 | newContents: string 19 | oldContents: string 20 | } 21 | 22 | export class ReadmeBox { 23 | public owner: string 24 | public repo: string 25 | public token: string 26 | public branch: string 27 | private request: typeof request 28 | 29 | constructor(opts: ReadmeBoxOpts) { 30 | this.owner = opts.owner 31 | this.repo = opts.repo 32 | this.token = opts.token 33 | this.branch = opts.branch || 'master' 34 | 35 | this.request = request.defaults({ 36 | headers: { 37 | authorization: `token ${this.token}` 38 | } 39 | }) 40 | } 41 | 42 | static async updateSection(newContents: string, opts: UpdateSectionOpts) { 43 | const box = new ReadmeBox(opts) 44 | 45 | // Get the README 46 | const { content, sha, path } = await box.getReadme() 47 | 48 | // Replace the old contents with the new 49 | const replaced = box.replaceSection({ 50 | section: opts.section, 51 | oldContents: content, 52 | newContents 53 | }) 54 | 55 | if (opts.emptyCommits !== true && content === replaced) { 56 | return 57 | } 58 | 59 | // Actually update the README 60 | return box.updateReadme({ 61 | content: replaced, 62 | message: opts.message, 63 | branch: opts.branch, 64 | sha, 65 | path 66 | }) 67 | } 68 | 69 | async getReadme() { 70 | const { data } = await this.request('GET /repos/:owner/:repo/readme', { 71 | owner: this.owner, 72 | repo: this.repo, 73 | ref: this.branch 74 | }) 75 | 76 | // The API returns the blob as base64 encoded, we need to decode it 77 | const encoded = data.content 78 | const decoded = Buffer.from(encoded, 'base64').toString('utf8') 79 | 80 | return { 81 | content: decoded, 82 | sha: data.sha, 83 | path: data.path 84 | } 85 | } 86 | 87 | async updateReadme(opts: { 88 | content: string 89 | sha: string 90 | path?: string 91 | message?: string 92 | branch?: string 93 | }) { 94 | return this.request('PUT /repos/:owner/:repo/contents/:path', { 95 | owner: this.owner, 96 | repo: this.repo, 97 | content: Buffer.from(opts.content).toString('base64'), 98 | path: opts.path || 'README.md', 99 | message: opts.message || 'Updating the README!', 100 | sha: opts.sha, 101 | branch: opts.branch || 'master' 102 | }) 103 | } 104 | 105 | getSection(section: string, content: string) { 106 | const { regex } = this.createRegExp(section) 107 | const match = content.match(regex) 108 | return match?.groups?.content 109 | } 110 | 111 | replaceSection(opts: ReplaceSectionOpts) { 112 | const { regex, start, end } = this.createRegExp(opts.section) 113 | 114 | if (!regex.test(opts.oldContents)) { 115 | throw new Error( 116 | `Contents do not contain start/end comments for section "${opts.section}"` 117 | ) 118 | } 119 | 120 | const newContentsWithComments = `${start}\n${opts.newContents}\n${end}` 121 | return opts.oldContents.replace(regex, newContentsWithComments) 122 | } 123 | 124 | private createRegExp(section: string) { 125 | const start = `` 126 | const end = `` 127 | const regex = new RegExp(`${start}\n(?:(?