├── .eslintrc.js.txt ├── .gitattributes ├── .github ├── labeler.yml ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── changes.yml │ ├── labeler.yml │ ├── post-release.yml │ ├── post-renovate.yml │ └── publish.yml ├── .gitignore ├── .prettierrc.js ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── common ├── changes │ └── fs-syncer │ │ └── fs-syncer-jest_pr-238.json ├── config │ └── rush │ │ ├── .npmrc │ │ ├── .npmrc-publish │ │ ├── command-line.json │ │ ├── common-versions.json │ │ ├── experiments.json │ │ ├── pnpm-lock.yaml │ │ ├── pnpmfile.js │ │ ├── repo-state.json │ │ └── version-policies.json ├── git-hooks │ └── commit-msg.sample └── scripts │ ├── install-run-rush.js │ ├── install-run-rushx.js │ └── install-run.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── check-clean │ ├── .eslintrc.js │ ├── .npmignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── __tests__ │ │ │ ├── check-clean.test.ts │ │ │ └── index.test.ts │ │ ├── cli.ts │ │ └── index.ts │ └── tsconfig.json ├── eslint-plugin-codegen │ ├── .eslintrc.js │ ├── .npmignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── gifs │ │ ├── barrel.gif │ │ ├── custom.gif │ │ ├── labeler.gif │ │ ├── markdownFromJsdoc.gif │ │ ├── markdownFromTests.gif │ │ ├── markdownTOC.gif │ │ └── monorepoTOC.gif │ ├── jest.config.js │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── __tests__ │ │ │ ├── plugin.test.ts │ │ │ └── rule.test.ts │ │ ├── index.ts │ │ └── presets │ │ │ ├── __tests__ │ │ │ ├── barrel.test.ts │ │ │ ├── custom-preset.js │ │ │ ├── custom-preset.ts │ │ │ ├── custom.test.ts │ │ │ ├── empty.test.ts │ │ │ ├── invalid-custom-preset.js │ │ │ ├── labeler.test.ts │ │ │ ├── markdown-from-jsdoc.test.ts │ │ │ ├── markdown-from-tests.test.ts │ │ │ ├── markdown-toc.test.ts │ │ │ └── monorepo-toc.test.ts │ │ │ ├── barrel.ts │ │ │ ├── custom.ts │ │ │ ├── empty.ts │ │ │ ├── index.ts │ │ │ ├── labeler.ts │ │ │ ├── markdown-from-jsdoc.ts │ │ │ ├── markdown-from-tests.ts │ │ │ ├── markdown-toc.ts │ │ │ ├── monorepo-toc.ts │ │ │ └── util │ │ │ ├── monorepo.ts │ │ │ └── path.ts │ └── tsconfig.json ├── expect-type │ └── readme.md ├── io-ts-extra │ ├── .eslintrc.js │ ├── .npmignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── __tests__ │ │ │ ├── combinators.test.ts │ │ │ ├── either-serializer.ts │ │ │ ├── mapper.test.ts │ │ │ ├── match.test.ts │ │ │ ├── narrow.test.ts │ │ │ ├── reporters.test.ts │ │ │ ├── shorthand.test.ts │ │ │ └── util.test.ts │ │ ├── combinators.ts │ │ ├── index.ts │ │ ├── mapper.ts │ │ ├── match.ts │ │ ├── narrow.ts │ │ ├── reporters.ts │ │ ├── shorthand.ts │ │ └── util.ts │ └── tsconfig.json └── memorable-moniker │ ├── .eslintrc.js │ ├── .npmignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── package.json │ ├── readme.md │ ├── src │ ├── __tests__ │ │ ├── index.test.ts │ │ └── rng.test.ts │ ├── dict │ │ ├── animal.ts │ │ ├── femaleName.ts │ │ ├── index.ts │ │ ├── lastName.ts │ │ ├── maleName.ts │ │ ├── positiveAdjective.ts │ │ └── util.ts │ └── index.ts │ └── tsconfig.json ├── renovate.json ├── rush.json ├── scripts └── prepublish.js ├── tools └── rig │ ├── .eslintrc.js │ ├── .npmignore │ ├── .prettierrc.js │ ├── badges.js │ ├── init.js │ ├── jest.config.js │ ├── package.json │ ├── permalink.js │ ├── readme.md │ ├── rig.js │ ├── src │ ├── __tests__ │ │ ├── github-release.test.ts │ │ ├── rush.test.ts │ │ └── util.ts │ ├── github-release.ts │ ├── index.ts │ └── rush.ts │ ├── tsconfig.json │ ├── unpermalink.js │ └── webpack.config.js └── tsconfig.json /.eslintrc.js.txt: -------------------------------------------------------------------------------- 1 | // todo: make this not a .txt file. VSCode can't really handle linting the whole project though. 2 | // Probably not worth fixing before https://github.com/eslint/rfcs/9 is implemented though. 3 | module.exports = require('./tools/rig/.eslintrc') 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=binary 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | 16 | * text=auto 17 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # codegen:start {preset: labeler} 2 | eslint-plugin-codegen: 3 | - packages/eslint-plugin-codegen/**/* 4 | expect-type: 5 | - packages/expect-type/**/* 6 | fs-syncer: 7 | - packages/fs-syncer/**/* 8 | io-ts-extra: 9 | - packages/io-ts-extra/**/* 10 | memorable-moniker: 11 | - packages/memorable-moniker/**/* 12 | # codegen:end 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: {} 4 | pull_request: {} 5 | 6 | jobs: 7 | build: 8 | # CI runs on push and pull_request. For PRs from forks, only pull_request will run. 9 | # This condition skips the pull request job for internal PRs - push kicks off faster 10 | if: github.event_name == 'push' || github.event.pull_request.base.repo.url != github.event.pull_request.head.repo.url 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [14.x] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - id: restore_node_modules 22 | uses: actions/cache@v2 23 | with: 24 | path: common/temp 25 | key: ${{ runner.os }}-rush-node-modules-v0.4-${{ hashFiles('common/config/**') }} 26 | restore-keys: | 27 | ${{ runner.os }}-rush-node-modules-v0.4- 28 | 29 | - id: restore_rush_cache 30 | uses: actions/cache@v2 31 | with: 32 | path: | 33 | tools/*/.rush/** 34 | packages/*/.rush/** 35 | tools/*/dist/** 36 | packages/*/dist/** 37 | tools/*/coverage/** 38 | packages/*/coverage/** 39 | key: ${{ runner.os }}-commands-v0.4-${{ github.ref }} 40 | restore-keys: | 41 | ${{ runner.os }}-commands-v0.4- 42 | 43 | - run: node common/scripts/install-run-rush install 44 | - run: node common/scripts/install-run-rush build 45 | - run: node common/scripts/install-run-rush lint 46 | - run: node common/scripts/install-run-rush test -v --coverage 47 | - id: find_coverage 48 | run: echo "::set-output name=files::$(find packages tools -iname coverage-final.json -path '*/coverage/*' -not -path '*/node_modules/*' | paste -sd "," -)" 49 | - uses: codecov/codecov-action@v1 50 | if: matrix.node-version == '14.x' 51 | with: 52 | files: ${{ steps.find_coverage.outputs.files }} 53 | -------------------------------------------------------------------------------- /.github/workflows/changes.yml: -------------------------------------------------------------------------------- 1 | name: create change files 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened, edited] 5 | 6 | jobs: 7 | run: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | # by default, GitHub checks out a detached head on an ephemeral pull ref. Check out the actual branch since `rush change` relies on real branches: 13 | repository: ${{ github.event.pull_request.head.repo.full_name }} 14 | ref: ${{ github.head_ref }} 15 | fetch-depth: 0 16 | # todo: use GITHUB_TOKEN, if https://github.community/t/triggering-a-new-workflow-from-another-workflow/16250/2 is ever addressed 17 | token: ${{ secrets.GH_CI_TOKEN || github.token }} 18 | 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: 12.x 22 | - name: rush install 23 | run: node common/scripts/install-run-rush install 24 | - name: configure git username and email 25 | run: | 26 | # this needs to run before `rush change` 27 | git config --global user.email "${{ github.event.pull_request.user.login }}@users.noreply.github.com" 28 | git config --global user.name "${{ github.event.pull_request.user.login }}" 29 | 30 | CLONE_URL_NO_SUFFIX=$(echo "${{ github.event.repository.clone_url }}" | sed -E "s/\.git$//") 31 | git remote add upstream $CLONE_URL_NO_SUFFIX 32 | git fetch upstream 33 | - name: patch rush to allow for overwriteable changefiles 34 | run: | 35 | # hack: workaround https://github.com/microsoft/rushstack/issues/2195 by replacing the suffix of 36 | # the generated changefile with a suffix of pr-123 where 123 is the pull request number. this 37 | # makes sure only one change file is generated, and it gets updated when the pull request is updated 38 | # 39 | # e.g. if the pull request owner adds "BREAKING CHANGE" to the body, the change file will update 40 | # to type 'major' 41 | FILE_TO_BE_EDITED="common/temp/install-run/@microsoft+rush@5.34.2/node_modules/@microsoft/rush-lib/lib/api/ChangeFile.js" 42 | TO_BE_REPLACED='_getTimestamp(useSeconds = false) {' 43 | REPLACEMENT="$TO_BE_REPLACED \n return 'pr-${{ github.event.pull_request.number }}' // patched_code \n" 44 | 45 | echo "patching $TO_BE_REPLACED in $FILE_TO_BE_EDITED" 46 | sed -i "s~$TO_BE_REPLACED~$REPLACEMENT~g" $FILE_TO_BE_EDITED 47 | 48 | echo "grepping for patched code, this will fail if rush code changed recently, and the sed command didn't replace anything" 49 | cat $FILE_TO_BE_EDITED | grep patched_code 50 | - name: create or update change files - SEE LOGS FOR COMMAND TO FIX FAILING PRs 51 | env: 52 | PR_BODY: ${{ github.event.pull_request.body }} 53 | PR_TITLE: ${{ github.event.pull_request.title }} 54 | PR_NUMBER: ${{ github.event.pull_request.number }} 55 | PR_AUTHOR: ${{ github.event.pull_request.user.login }} 56 | REPO_OWNER: ${{ github.repository_owner }} 57 | run: | 58 | # do basic pull request title/body parsing to figure out if it's a major, minor or patch change 59 | # it'd probably be a good idea to use something like @commitlint for this, but it's an annoyingly 60 | # big dependency, with multiple peers and required config files, for such a simple task 61 | # this gist has a list of the allowed types (technically `chore` isn't one) https://gist.github.com/brianclements/841ea7bffdb01346392c 62 | 63 | MAJOR_CHANGE_MATCH=$(echo "$PR_TITLE $PR_BODY" | grep 'BREAKING CHANGE' || echo '') 64 | PATCH_CHANGE_MATCH=$(echo "$PR_TITLE" | grep -E '^(build|chore|ci|docs|fix|perf|refactor|style|test)[:\(]' || echo '') 65 | 66 | BUMP_TYPE=minor 67 | if [ -n "$MAJOR_CHANGE_MATCH" ]; then 68 | BUMP_TYPE=major 69 | PR_TITLE="$PR_TITLE ($MAJOR_CHANGE_MATCH)" 70 | elif [ -n "$PATCH_CHANGE_MATCH" ]; then 71 | BUMP_TYPE=patch 72 | fi 73 | 74 | MESSAGE="$PR_TITLE (#$PR_NUMBER)" 75 | if [ "$PR_AUTHOR" != "$REPO_OWNER" ]; then 76 | MESSAGE="$MESSAGE - @$PR_AUTHOR" 77 | fi 78 | 79 | node common/scripts/install-run-rush change --message "$MESSAGE" --overwrite --bulk --bump-type $BUMP_TYPE 80 | 81 | GIT_STATUS=$(git status --porcelain) 82 | 83 | git add -A 84 | DIFF_BASE64=$(git diff --cached | base64 -w 0) 85 | APPLY_PATCH_COMMAND="echo $DIFF_BASE64 | base64 --decode | git apply" 86 | 87 | if [ -z "$GIT_STATUS" ]; then 88 | echo "no changes made" 89 | elif [ -n "${{ secrets.GH_CI_TOKEN }}" ]; then 90 | echo "Attempting to push a commit to your branch. If this fails, try running the following command locally, committing the changes and pushing manually:" 91 | echo "$APPLY_PATCH_COMMAND" 92 | 93 | git commit -m "chore: change files" 94 | 95 | git push 96 | 97 | echo "::set-env name=CHANGE_HASH::$(git rev-parse --short HEAD)" 98 | else 99 | echo "changes were made, but can't push to the branch from this context" 100 | echo "run the following commands locally from a bash-like shell, then commit and push the changes it generates to your branch:" 101 | echo "git clone ${{ github.event.pull_request.head.repo.clone_url }} tmp" 102 | echo "cd tmp" 103 | echo "git checkout ${{ github.head_ref }}" 104 | echo "$APPLY_PATCH_COMMAND" 105 | echo "git commit -am 'chore: changefile'" 106 | exit 1 107 | fi 108 | 109 | - name: Comment on PR 110 | uses: actions/github-script@v3 111 | if: ${{ env.CHANGE_HASH != null }} 112 | with: 113 | script: | 114 | github.issues.createComment({ 115 | owner: context.repo.owner, 116 | repo: context.repo.repo, 117 | issue_number: context.payload.pull_request.number, 118 | body: `@${{ github.event.pull_request.user.login }} - changes were pushed to your branch: ${process.env.CHANGE_HASH}.`, 119 | }) 120 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: labeler 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | label: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/labeler@v3 10 | with: 11 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 12 | -------------------------------------------------------------------------------- /.github/workflows/post-release.yml: -------------------------------------------------------------------------------- 1 | name: Post-release 2 | on: 3 | release: 4 | types: 5 | - published 6 | - edited 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: apexskier/github-release-commenter@v1 12 | with: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | comment-template: This is addressed by {release_link}. 15 | label-template: released 16 | -------------------------------------------------------------------------------- /.github/workflows/post-renovate.yml: -------------------------------------------------------------------------------- 1 | name: renovate post-upgrade 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'renovate/**' 7 | 8 | jobs: 9 | run: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | token: ${{ secrets.GH_CI_TOKEN}} 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 14 18 | - name: run rush update and push changes 19 | run: | 20 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 21 | git config --global user.name "${{ github.actor }}" 22 | 23 | node common/scripts/install-run-rush update 24 | 25 | RUSH_CONFIG_DIR=common/config/rush 26 | GIT_STATUS=$(git status --porcelain) 27 | RUSH_CHANGES=$(echo "$GIT_STATUS" | grep "M $RUSH_CONFIG_DIR" || echo "") 28 | 29 | if [ -z "$RUSH_CHANGES" ]; then 30 | echo "no rush changes made. git status:" 31 | echo "$GIT_STATUS" 32 | else 33 | git add $RUSH_CONFIG_DIR 34 | git commit -m "build(rush): config changes from rush update" 35 | git push 36 | fi 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | header: 6 | description: Optional release notes header text 7 | footer: 8 | description: Optional release notes footer text 9 | tags: 10 | # This can be used either to publish releases for old tags, or to skip releasing (by setting the field to a non-existent tag) 11 | description: Comma-separated list of tags. If not set, tags pointing at HEAD will be used. 12 | 13 | jobs: 14 | run: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: 14.x 21 | - name: publish 22 | env: 23 | # referenced in common/config/rush/.npmrc-publish 24 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 25 | run: | 26 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 27 | git config --global user.name "${{ github.actor }}" 28 | git remote set-url origin "https://${{ github.actor }}:${{ secrets.GH_CI_TOKEN }}@github.com/${{ github.repository }}.git" 29 | 30 | node common/scripts/install-run-rush.js install 31 | node common/scripts/install-run-rush.js build 32 | node common/scripts/install-run-rush.js publish --target-branch main --publish --apply 33 | - name: create github release 34 | uses: actions/github-script@v3 35 | with: 36 | script: | 37 | const rig = require(`${process.env.GITHUB_WORKSPACE}/tools/rig`) 38 | return rig.createGitHubRelease({ context, github }) 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # Bower dependency directory (https://bower.io/) 25 | bower_components 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (https://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules/ 35 | jspm_packages/ 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional eslint cache 41 | .eslintcache 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | 46 | # Output of 'npm pack' 47 | *.tgz 48 | 49 | # Yarn Integrity file 50 | .yarn-integrity 51 | 52 | # dotenv environment variables file 53 | .env 54 | 55 | # next.js build output 56 | .next 57 | 58 | # OS X temporary files 59 | .DS_Store 60 | 61 | # Rush temporary files 62 | common/deploy/ 63 | common/temp/ 64 | common/autoinstallers/*/.npmrc 65 | **/.rush/temp/ 66 | 67 | **/generated/** 68 | **/dist/** 69 | **/lib/** 70 | 71 | .cache 72 | *ignoreme* 73 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // prettier is *mostly* run through eslint. But jest-inline-snapshots also use it, and need to 2 | // be able to resolve the correct config, so single-quotes aren't turned into doubles, etc. 3 | module.exports = require('./tools/rig/.prettierrc') 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "eslint", 11 | "program": "${workspaceFolder}/node_modules/eslint/bin/eslint", 12 | "args": ["README.md", "--fix"], 13 | "sourceMaps": true, 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen" 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "jest", 21 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 22 | "args": ["--runInBand", "--no-cache", "--watch", "${file}"], 23 | "restart": true, 24 | "sourceMaps": true, 25 | "skipFiles": ["/**"], 26 | "console": "integratedTerminal", 27 | "internalConsoleOptions": "neverOpen" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "tools/rig/node_modules/typescript/lib", 3 | "files.exclude": { 4 | "**/.git": false 5 | }, 6 | "files.associations": { 7 | ".env*": "shellscript", 8 | ".json": "jsonc" 9 | }, 10 | "files.watcherExclude": { 11 | "**/.git/objects/**": true, 12 | "**/.git/subtree-cache/**": true, 13 | "**/node_modules/**": true 14 | }, 15 | "javascript.implicitProjectConfig.checkJs": true, 16 | "eslint.validate": ["markdown", "javascript", "typescript", "yaml"], 17 | "eslint.packageManager": "pnpm", 18 | "eslint.nodePath": "tools/rig/node_modules", 19 | "eslint.debug": false, 20 | "editor.codeActionsOnSave": { 21 | "source.fixAll.eslint": "explicit" 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts 2 | 3 | [![Node CI](https://github.com/mmkal/ts/workflows/Node%20CI/badge.svg)](https://github.com/mmkal/ts/actions?query=workflow%3A%22Node+CI%22) 4 | [![codecov](https://codecov.io/gh/mmkal/ts/branch/main/graph/badge.svg)](https://codecov.io/gh/mmkal/ts) 5 | 6 | Monorepo of typescript projects. 7 | 8 | ## Packages 9 | 10 | > ⚠️⚠️ NOTE! I started this repo as a way to learn about monorepo tooling (specifically, rush). Some of the packages in it have become reasonably popular, and reasonably stable. So, I'm going to slowly move them out into their own repos, one by one. Once all are moved out this repo will be archived. ⚠️⚠️ 11 | 12 | ### Moved 13 | 14 | - [expect-type](https://github.com/mmkal/expect-type#readme) - Compile-time tests for types. Useful to make sure types don't regress into being overly-permissive as changes go in over time. 15 | - [eslint-plugin-codegen](https://github.com/mmkal/eslint-plugin-codegen#readme) - An eslint plugin for inline codegen, with presets for barrels, jsdoc to markdown and a monorepo workspace table of contents generator. Auto-fixes out of sync code. 16 | - [fs-syncer](https://github.com/mmkal/fs-syncer) - A helper to recursively read and write text files to a specified directory. 17 | 18 | ### Still here 19 | 20 | - [io-ts-extra](https://github.com/mmkal/ts/tree/main/packages/io-ts-extra#readme) - Adds pattern matching, optional properties, and several other helpers and types, to io-ts. 21 | - [memorable-moniker](https://github.com/mmkal/ts/tree/main/packages/memorable-moniker#readme) - Name generator with some in-built dictionaries and presets. 22 | 23 | 24 | ### Development 25 | 26 | Packages are managed using [rush](https://rushjs.io/pages/developer/new_developer/). Make sure rush is installed: 27 | 28 | ```bash 29 | npm install --global @microsoft/rush 30 | ``` 31 | 32 | Then install, build, lint and test with: 33 | 34 | ```bash 35 | rush update 36 | rush build 37 | rush lint 38 | rush test 39 | ``` 40 | 41 | `rush update` should be run when updating the main branch too. 42 | 43 | ___ 44 | 45 | Add a dependency to a package (for example, adding lodash to fictional package in this monorepo `some-pkg`): 46 | 47 | ```bash 48 | cd packages/some-pkg 49 | rush add --package lodash 50 | rush add --package @types/lodash --dev 51 | ``` 52 | 53 | You can also manually edit package.json then run `rush update`. 54 | 55 | Create a new package: 56 | 57 | ```bash 58 | cd packages 59 | mkdir new-pkg 60 | cd new-pkg 61 | node ../../tools/rig/init # sets up package.json, .eslintrc.js, tsconfig.json, jest.config.js 62 | ``` 63 | 64 | 65 | Then open `rush.json`, find the `projects` array, and adda new entry: `{ "packageName": "new-pkg", "projectFolder": "packages/new-pkg" }` 66 | 67 | ### Publishing 68 | [![publish](https://github.com/mmkal/ts/workflows/publish/badge.svg)](https://github.com/mmkal/ts/actions/workflows/publish.yml) 69 | 70 | Publishing is automated, but kicked off manually. The process is: 71 | 72 | - Changes to published packages in this repo should be proposed in a pull request 73 | - On every pull request, a [GitHub action](./.github/workflows/changes.yml) uses the `rush change` command to create a changefile: 74 | - the change is based on the PR title and body: 75 | - if the words "BREAKING CHANGE" appear anywhere, it's considered "major" 76 | - if the PR title starts with "chore", or "fix", it's considered a "patch" 77 | - otherwise, it's considered "minor" 78 | - the created changefile is pushed to the PR's branch, and a comment is left on the PR (example [PR](https://github.com/mmkal/ts/pull/166), [comment](https://github.com/mmkal/ts/pull/166#issuecomment-694554963) and [change](https://github.com/mmkal/ts/commit/8d8c442fdd54dc6732bf56e9a074afea58dc8303)) 79 | - if the PR title or body is edited, or changes are pushed, the job will re-run and push a modification if necessary 80 | - most of the time, no change is necessary and the job exits after no-oping 81 | - if necessary, `rush change` can also be run locally to add additional messages - but ideally the PR title would be descriptive enough 82 | - the changefile should be merged in along with the rest of the changes 83 | 84 | When a PR is merged, publishing is initiated by kicking off the [publish worfklow](https://github.com/mmkal/ts/actions/workflows/publish.yml): 85 | 86 | - Clicking "Run workflow" will start another [GitHub action](./.github/workflows/publish.yml): 87 | - The workflow runs `rush publish`, which uses the changefiles merged with feature PRs, bumps versions and create git tags 88 | - When the publish step succeeds, a custom script reads the generated `CHANGELOG.json` files to create a GitHub release 89 | 90 |
91 | Old instructions 92 | 93 | Links to trees with previous iteration of publish instructions: 94 | 95 | - For creating canary releases: https://github.com/mmkal/ts/tree/fc5f2dd50a04439573bcfb1f4b7bf0cad59c1c59 96 | - For publishing to GitHub Packages' npm registry: https://github.com/mmkal/ts/tree/56bed6ba6c3fa7eca06c9f73adf104438e9b0f8a 97 | 98 |
99 | -------------------------------------------------------------------------------- /common/changes/fs-syncer/fs-syncer-jest_pr-238.json: -------------------------------------------------------------------------------- 1 | { 2 | "changes": [ 3 | { 4 | "comment": "feat: jest helper, custom merger, jsonc parser, yaml stringifier (#238)", 5 | "type": "minor", 6 | "packageName": "fs-syncer" 7 | } 8 | ], 9 | "packageName": "fs-syncer", 10 | "email": "mmkal@users.noreply.github.com" 11 | } -------------------------------------------------------------------------------- /common/config/rush/.npmrc: -------------------------------------------------------------------------------- 1 | # Rush uses this file to configure the NPM package registry during installation. It is applicable 2 | # to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", 3 | # "rush update", and the "install-run.js" scripts. 4 | # 5 | # NOTE: The "rush publish" command uses .npmrc-publish instead. 6 | # 7 | # Before invoking the package manager, Rush will copy this file to the folder where installation 8 | # is performed. The copied file will omit any config lines that reference environment variables 9 | # that are undefined in that session; this avoids problems that would otherwise result due to 10 | # a missing variable being replaced by an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | registry=https://registry.npmjs.org/ 22 | always-auth=false 23 | -------------------------------------------------------------------------------- /common/config/rush/.npmrc-publish: -------------------------------------------------------------------------------- 1 | # This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish 2 | # is used by the "rush publish" command, as publishing often involves different credentials 3 | # and registries than other operations. 4 | # 5 | # Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" 6 | # and then temporarily map that folder as the "home directory" for the current user account. 7 | # This enables the same settings to apply for each project folder that gets published. The copied file 8 | # will omit any config lines that reference environment variables that are undefined in that session; 9 | # this avoids problems that would otherwise result due to a missing variable being replaced by 10 | # an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | -------------------------------------------------------------------------------- /common/config/rush/common-versions.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file specifies NPM dependency version selections that affect all projects 3 | * in a Rush repo. For full documentation, please see https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", 7 | 8 | /** 9 | * A table that specifies a "preferred version" for a given NPM package. This feature is typically used 10 | * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. 11 | * 12 | * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into 13 | * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager 14 | * will calculate versions. The specific effect depends on your package manager. Generally it will have no 15 | * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be 16 | * achieved using the pnpmfile.js hook. See the Rush documentation for more details. 17 | * 18 | * After modifying this field, it's recommended to run "rush update --full" so that the package manager 19 | * will recalculate all version selections. 20 | */ 21 | "preferredVersions": { 22 | /** 23 | * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, 24 | * instead of the latest version. 25 | */ 26 | // "some-library": "1.2.3" 27 | }, 28 | 29 | /** 30 | * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, 31 | * except in cases where different projects specify different version ranges for a given dependency. For older 32 | * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause 33 | * trouble for indirect dependencies with incompatible peerDependencies ranges. 34 | * 35 | * The default value is true. If you're encountering installation errors related to peer dependencies, 36 | * it's recommended to set this to false. 37 | * 38 | * After modifying this field, it's recommended to run "rush update --full" so that the package manager 39 | * will recalculate all version selections. 40 | */ 41 | // "implicitlyPreferredVersions": false, 42 | 43 | /** 44 | * The "rush check" command can be used to enforce that every project in the repo must specify 45 | * the same SemVer range for a given dependency. However, sometimes exceptions are needed. 46 | * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be 47 | * accepted by "rush check" for a given dependency. 48 | * 49 | * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE 50 | * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). 51 | * This design avoids unnecessary churn in this file. 52 | */ 53 | "allowedAlternativeVersions": { 54 | /** 55 | * For example, allow some projects to use an older TypeScript compiler 56 | * (in addition to whatever "usual" version is being used by other projects in the repo): 57 | */ 58 | // "typescript": [ 59 | // "~2.4.0" 60 | // ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /common/config/rush/experiments.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file allows repo maintainers to enable and disable experimental 3 | * Rush features. For full documentation, please see https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", 7 | 8 | /** 9 | * Rush 5.14.0 improved incremental builds to ignore spurious changes in the pnpm-lock.json file. 10 | * This optimization is enabled by default. If you encounter a problem where "rush build" is neglecting 11 | * to build some projects, please open a GitHub issue. As a workaround you can uncomment this line 12 | * to temporarily restore the old behavior where everything must be rebuilt whenever pnpm-lock.json 13 | * is modified. 14 | */ 15 | // "legacyIncrementalBuildDependencyDetection": true, 16 | 17 | /** 18 | * By default, rush passes --no-prefer-frozen-lockfile to 'pnpm install'. 19 | * Set this option to true to pass '--frozen-lockfile' instead. 20 | */ 21 | // "usePnpmFrozenLockfileForRushInstall": true, 22 | 23 | /** 24 | * If true, the chmod field in temporary project tar headers will not be normalized. 25 | * This normalization can help ensure consistent tarball integrity across platforms. 26 | */ 27 | // "noChmodFieldInTarHeaderNormalization": true 28 | } 29 | -------------------------------------------------------------------------------- /common/config/rush/pnpmfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * When using the PNPM package manager, you can use pnpmfile.js to workaround 5 | * dependencies that have mistakes in their package.json file. (This feature is 6 | * functionally similar to Yarn's "resolutions".) 7 | * 8 | * For details, see the PNPM documentation: 9 | * https://pnpm.js.org/docs/en/hooks.html 10 | * 11 | * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE 12 | * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run 13 | * "rush update --full" so that PNPM will recalculate all version selections. 14 | */ 15 | module.exports = { 16 | hooks: { 17 | readPackage 18 | } 19 | }; 20 | 21 | /** 22 | * This hook is invoked during installation before a package's dependencies 23 | * are selected. 24 | * The `packageJson` parameter is the deserialized package.json 25 | * contents for the package that is about to be installed. 26 | * The `context` parameter provides a log() function. 27 | * The return value is the updated object. 28 | */ 29 | function readPackage(packageJson, context) { 30 | 31 | // // The karma types have a missing dependency on typings from the log4js package. 32 | // if (packageJson.name === '@types/karma') { 33 | // context.log('Fixed up dependencies for @types/karma'); 34 | // packageJson.dependencies['log4js'] = '0.6.38'; 35 | // } 36 | 37 | // for some reason I can't figure out, pnpm complains about a missing eslint dependency 38 | // even though it's there. 39 | const peers = packageJson.peerDependencies || {} 40 | if ('eslint' in peers) { 41 | context.log(`Deleting peer dependency eslint from ${packageJson.name}`) 42 | delete peers.eslint 43 | } 44 | 45 | return packageJson; 46 | } 47 | -------------------------------------------------------------------------------- /common/config/rush/repo-state.json: -------------------------------------------------------------------------------- 1 | // DO NOT MODIFY THIS FILE. It is generated and used by Rush. 2 | { 3 | "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" 4 | } 5 | -------------------------------------------------------------------------------- /common/config/rush/version-policies.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This is configuration file is used for advanced publishing configurations with Rush. 3 | * For full documentation, please see https://rushjs.io 4 | */ 5 | 6 | /** 7 | * A list of version policy definitions. A "version policy" is a custom package versioning 8 | * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies 9 | * to a set of projects that are specified using the "versionPolicyName" field in rush.json. 10 | */ 11 | [ 12 | // { 13 | // /** 14 | // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). 15 | // * 16 | // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This 17 | // * strategy is appropriate for a set of packages that act as selectable components of a 18 | // * unified product. The entire set of packages are always published together, and always share 19 | // * the same NPM version number. When the packages depend on other packages in the set, the 20 | // * SemVer range is usually restricted to a single version. 21 | // */ 22 | // "definitionName": "lockStepVersion", 23 | // 24 | // /** 25 | // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. 26 | // * This name is also used command-line parameters such as "--version-policy" 27 | // * and "--to-version-policy". 28 | // */ 29 | // "policyName": "MyBigFramework", 30 | // 31 | // /** 32 | // * (Required) The current version. All packages belonging to the set should have this version 33 | // * in the current branch. When bumping versions, Rush uses this to determine the next version. 34 | // * (The "version" field in package.json is NOT considered.) 35 | // */ 36 | // "version": "1.0.0", 37 | // 38 | // /** 39 | // * (Required) The type of bump that will be performed when publishing the next release. 40 | // * When creating a release branch in Git, this field should be updated according to the 41 | // * type of release. 42 | // * 43 | // * Valid values are: "prerelease", "release", "minor", "patch", "major" 44 | // */ 45 | // "nextBump": "prerelease", 46 | // 47 | // /** 48 | // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. 49 | // * This file is stored with the specified "main" project, which must be a member of the set. 50 | // * 51 | // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each 52 | // * package in the set. 53 | // */ 54 | // "mainProject": "my-app" 55 | // }, 56 | // 57 | // { 58 | // /** 59 | // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). 60 | // * 61 | // * The "individualVersion" mode specifies that the projects will use "individual versioning". 62 | // * This is the typical NPM model where each package has an independent version number 63 | // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the 64 | // * packages, they otherwise don't have any special relationship. The version bumping will 65 | // * depend on how developers answer the "rush change" questions for each package that 66 | // * is changed. 67 | // */ 68 | // "definitionName": "individualVersion", 69 | // 70 | // "policyName": "MyRandomLibraries", 71 | // 72 | // /** 73 | // * (Optional) This can be used to enforce that all packages in the set must share a common 74 | // * major version number, e.g. because they are from the same major release branch. 75 | // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes 76 | // * inappropriately. The minor/patch version parts will be bumped independently according 77 | // * to the types of changes made to each project, according to the "rush change" command. 78 | // */ 79 | // "lockedMajor": 3, 80 | // 81 | // /** 82 | // * (Optional) When publishing is managed by Rush, by default the "rush change" command will 83 | // * request changes for any projects that are modified by a pull request. These change entries 84 | // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates 85 | // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects 86 | // * belonging to this version policy. 87 | // */ 88 | // "exemptFromRushChange": false 89 | // } 90 | ] 91 | -------------------------------------------------------------------------------- /common/git-hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is an example Git hook for use with Rush. To enable this hook, rename this file 4 | # to "commit-msg" and then run "rush install", which will copy it from common/git-hooks 5 | # to the .git/hooks folder. 6 | # 7 | # TO LEARN MORE ABOUT GIT HOOKS 8 | # 9 | # The Git documentation is here: https://git-scm.com/docs/githooks 10 | # Some helpful resources: https://githooks.com 11 | # 12 | # ABOUT THIS EXAMPLE 13 | # 14 | # The commit-msg hook is called by "git commit" with one argument, the name of the file 15 | # that has the commit message. The hook should exit with non-zero status after issuing 16 | # an appropriate message if it wants to stop the commit. The hook is allowed to edit 17 | # the commit message file. 18 | 19 | # This example enforces that commit message should contain a minimum amount of 20 | # description text. 21 | if [ `cat $1 | wc -w` -lt 3 ]; then 22 | echo "" 23 | echo "Invalid commit message: The message must contain at least 3 words." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /common/scripts/install-run-rush.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | // See the @microsoft/rush package's LICENSE file for license information. 4 | Object.defineProperty(exports, "__esModule", { value: true }); 5 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 6 | // 7 | // This script is intended for usage in an automated build environment where the Rush command may not have 8 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 9 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. 10 | // An example usage would be: 11 | // 12 | // node common/scripts/install-run-rush.js install 13 | // 14 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 15 | const path = require("path"); 16 | const fs = require("fs"); 17 | const install_run_1 = require("./install-run"); 18 | const PACKAGE_NAME = '@microsoft/rush'; 19 | const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; 20 | function _getRushVersion() { 21 | const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; 22 | if (rushPreviewVersion !== undefined) { 23 | console.log(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); 24 | return rushPreviewVersion; 25 | } 26 | const rushJsonFolder = install_run_1.findRushJsonFolder(); 27 | const rushJsonPath = path.join(rushJsonFolder, install_run_1.RUSH_JSON_FILENAME); 28 | try { 29 | const rushJsonContents = fs.readFileSync(rushJsonPath, 'utf-8'); 30 | // Use a regular expression to parse out the rushVersion value because rush.json supports comments, 31 | // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. 32 | const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); 33 | return rushJsonMatches[1]; 34 | } 35 | catch (e) { 36 | throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` + 37 | "The 'rushVersion' field is either not assigned in rush.json or was specified " + 38 | 'using an unexpected syntax.'); 39 | } 40 | } 41 | function _run() { 42 | const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; 43 | // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the 44 | // appropriate binary inside the rush package to run 45 | const scriptName = path.basename(scriptPath); 46 | const bin = scriptName.toLowerCase() === 'install-run-rushx.js' ? 'rushx' : 'rush'; 47 | if (!nodePath || !scriptPath) { 48 | throw new Error('Unexpected exception: could not detect node path or script path'); 49 | } 50 | if (process.argv.length < 3) { 51 | console.log(`Usage: ${scriptName} [args...]`); 52 | if (scriptName === 'install-run-rush.js') { 53 | console.log(`Example: ${scriptName} build --to myproject`); 54 | } 55 | else { 56 | console.log(`Example: ${scriptName} custom-command`); 57 | } 58 | process.exit(1); 59 | } 60 | install_run_1.runWithErrorAndStatusCode(() => { 61 | const version = _getRushVersion(); 62 | console.log(`The rush.json configuration requests Rush version ${version}`); 63 | return install_run_1.installAndRun(PACKAGE_NAME, version, bin, packageBinArgs); 64 | }); 65 | } 66 | _run(); 67 | //# sourceMappingURL=install-run-rush.js.map -------------------------------------------------------------------------------- /common/scripts/install-run-rushx.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | // See the @microsoft/rush package's LICENSE file for license information. 4 | Object.defineProperty(exports, "__esModule", { value: true }); 5 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 6 | // 7 | // This script is intended for usage in an automated build environment where the Rush command may not have 8 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 9 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to the 10 | // rushx command. 11 | // 12 | // An example usage would be: 13 | // 14 | // node common/scripts/install-run-rushx.js custom-command 15 | // 16 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 17 | require("./install-run-rush"); 18 | //# sourceMappingURL=install-run-rushx.js.map -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.tsx?$': '/tools/rig/node_modules/ts-jest', 4 | }, 5 | globals: { 6 | 'ts-jest': { 7 | diagnostics: false, 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | testMatch: ['**/*/*.test.ts'], 12 | collectCoverageFrom: [ 13 | '**/*.{ts,js}', 14 | '!**/node_modules/**', 15 | '!**/dist/**', 16 | '!**/scripts/**', 17 | '!common/**', // rush files 18 | '!tools/rig/*.js', // todo: tests. this package is internal-only, for now 19 | '!**/coverage/**', 20 | '!**/*.config.js', 21 | '!**/.prettierrc.js', 22 | '!**/.eslintrc.js', 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "independent", 4 | "toposort": true, 5 | "stream": true, 6 | "command": { 7 | "publish": { 8 | "conventionalCommits": true, 9 | "allowBranch": ["main"], 10 | "exact": true, 11 | "createRelease": "github" 12 | } 13 | }, 14 | "ignoreChanges": ["**/__tests__/**"] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts", 3 | "version": "0.0.1", 4 | "description": "Monorepo of assorted typescript projects", 5 | "repository": "https://github.com/mmkal/ts", 6 | "author": "mmkal ", 7 | "license": "Apache-2.0", 8 | "private": true, 9 | "scripts": { 10 | "jest": "./tools/rig/node_modules/.bin/jest", 11 | "coverage": "npm run jest -- --coverage", 12 | "fix-packages": "node tools/rig/rig sort-package-json 'packages/*/package.json'", 13 | "build": "rush build", 14 | "lint": "rush lint", 15 | "test": "rush test", 16 | "clean": "npx lerna exec -- 'rm -rf dist'", 17 | "eslint": "./tools/rig/node_modules/.bin/eslint --ext .ts,.js,.md,.yml", 18 | "commit-date": "git log -n 1 --date=format:'%Y-%m-%d-%H-%M-%S' --pretty=format:'%ad'", 19 | "current-branch": "echo \"${CURRENT_BRANCH-$(git rev-parse --abbrev-ref HEAD)}\" | sed -E 's/refs\\/heads\\///' | sed -E 's/\\W|_/-/g'", 20 | "canary-preid": "echo \"$(yarn --silent current-branch)-$(yarn --silent commit-date)\"", 21 | "publish-canary": "lerna publish --canary --preid $(yarn --silent canary-preid) --dist-tag $(yarn --silent current-branch)", 22 | "prepublish-packages": "node scripts/prepublish && npm run fix-packages && yarn clean && yarn build", 23 | "prepack": "node scripts/permalink", 24 | "check-entrypoints": "lerna exec -- node -e 'require.resolve(`.`)'", 25 | "postpack": "yarn check-entrypoints && git checkout -- .", 26 | "publish-packages": "lerna publish" 27 | }, 28 | "workspaces": [ 29 | "packages/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/check-clean/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@mmkal/rig/.eslintrc') 2 | -------------------------------------------------------------------------------- /packages/check-clean/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/__tests__ 3 | dist/buildinfo.json 4 | .eslintcache 5 | .eslintrc.js 6 | .rush 7 | .heft 8 | *.log 9 | tsconfig.json 10 | config/jest.config.json 11 | jest.config.js 12 | coverage 13 | -------------------------------------------------------------------------------- /packages/check-clean/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check-clean", 3 | "entries": [ 4 | { 5 | "version": "0.3.0", 6 | "tag": "check-clean_v0.3.0", 7 | "date": "Tue, 27 Oct 2020 16:18:39 GMT", 8 | "comments": { 9 | "minor": [ 10 | { 11 | "comment": "Permalink readme urls before publishing (#211)" 12 | } 13 | ] 14 | } 15 | }, 16 | { 17 | "version": "0.2.5", 18 | "tag": "check-clean_v0.2.5", 19 | "date": "Mon, 12 Oct 2020 12:24:41 GMT", 20 | "comments": { 21 | "patch": [ 22 | { 23 | "comment": "fix(package): include repository info in each leaf package (#190)" 24 | } 25 | ] 26 | } 27 | }, 28 | { 29 | "version": "0.2.4", 30 | "tag": "check-clean_v0.2.4", 31 | "date": "Thu, 01 Oct 2020 14:48:13 GMT", 32 | "comments": { 33 | "patch": [ 34 | { 35 | "comment": "chore: npmignore coverage folder (#184)" 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | "version": "0.2.3", 42 | "tag": "check-clean_v0.2.3", 43 | "date": "Wed, 30 Sep 2020 15:34:55 GMT", 44 | "comments": { 45 | "patch": [ 46 | { 47 | "comment": "chore: cache commands (#182)" 48 | }, 49 | { 50 | "comment": "fix: mark cli.ts as bin script (#183)" 51 | } 52 | ] 53 | } 54 | }, 55 | { 56 | "version": "0.2.2", 57 | "tag": "check-clean_v0.2.2", 58 | "date": "Fri, 18 Sep 2020 16:56:41 GMT", 59 | "comments": { 60 | "patch": [ 61 | { 62 | "comment": "fix(npmignore): exclude rush files (#169)" 63 | } 64 | ] 65 | } 66 | }, 67 | { 68 | "version": "0.2.1", 69 | "tag": "check-clean_v0.2.1", 70 | "date": "Fri, 18 Sep 2020 15:09:11 GMT", 71 | "comments": { 72 | "patch": [ 73 | { 74 | "comment": "fix: check clean cli path (#168)" 75 | } 76 | ] 77 | } 78 | }, 79 | { 80 | "version": "0.2.0", 81 | "tag": "check-clean_v0.2.0", 82 | "date": "Fri, 18 Sep 2020 03:17:48 GMT", 83 | "comments": { 84 | "minor": [ 85 | { 86 | "comment": "Port check-clean to typescript (#167)" 87 | } 88 | ] 89 | } 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /packages/check-clean/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - check-clean 2 | 3 | This log was last generated on Tue, 27 Oct 2020 16:18:39 GMT and should not be manually modified. 4 | 5 | ## 0.3.0 6 | Tue, 27 Oct 2020 16:18:39 GMT 7 | 8 | ### Minor changes 9 | 10 | - Permalink readme urls before publishing (#211) 11 | 12 | ## 0.2.5 13 | Mon, 12 Oct 2020 12:24:41 GMT 14 | 15 | ### Patches 16 | 17 | - fix(package): include repository info in each leaf package (#190) 18 | 19 | ## 0.2.4 20 | Thu, 01 Oct 2020 14:48:13 GMT 21 | 22 | ### Patches 23 | 24 | - chore: npmignore coverage folder (#184) 25 | 26 | ## 0.2.3 27 | Wed, 30 Sep 2020 15:34:55 GMT 28 | 29 | ### Patches 30 | 31 | - chore: cache commands (#182) 32 | - fix: mark cli.ts as bin script (#183) 33 | 34 | ## 0.2.2 35 | Fri, 18 Sep 2020 16:56:41 GMT 36 | 37 | ### Patches 38 | 39 | - fix(npmignore): exclude rush files (#169) 40 | 41 | ## 0.2.1 42 | Fri, 18 Sep 2020 15:09:11 GMT 43 | 44 | ### Patches 45 | 46 | - fix: check clean cli path (#168) 47 | 48 | ## 0.2.0 49 | Fri, 18 Sep 2020 03:17:48 GMT 50 | 51 | ### Minor changes 52 | 53 | - Port check-clean to typescript (#167) 54 | 55 | -------------------------------------------------------------------------------- /packages/check-clean/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@mmkal/rig/jest.config') 2 | -------------------------------------------------------------------------------- /packages/check-clean/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check-clean", 3 | "version": "0.3.1", 4 | "description": "A cli tool to make sure you have no git changes", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/mmkal/ts.git", 8 | "directory": "packages/check-clean" 9 | }, 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "bin": { 13 | "check-clean": "dist/cli.js" 14 | }, 15 | "scripts": { 16 | "prebuild": "npm run clean", 17 | "build": "rig tsc -p .", 18 | "clean": "rig rimraf dist", 19 | "lint": "rig eslint --cache .", 20 | "prepack": "rig permalink", 21 | "postpack": "rig unpermalink", 22 | "test": "rig jest" 23 | }, 24 | "devDependencies": { 25 | "@mmkal/rig": "workspace:*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/check-clean/readme.md: -------------------------------------------------------------------------------- 1 | # check-clean 2 | 3 | A cli tool to make sure you have no git changes. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | npx check-clean 9 | ``` 10 | 11 | ___ 12 | 13 | You can also use `npm install --save-dev check-clean` and add an entry like `"check-clean": "check-clean"` to your package.json scripts section. If using yarn, `yarn add --dev check-clean` will enable `yarn check-clean` to be used even without a package.json script. 14 | 15 | If you have local git changes, the command will exit with code 1 after printing something similar to the following: 16 | 17 | ``` 18 | error: git changes detected 19 | check them in before running again 20 | changes: 21 | M path/to/changed/file.txt 22 | ``` 23 | 24 | If there are no changes, nothing is printed and the script will exit with code 0. 25 | 26 | ### API 27 | 28 | You can also call it programmatically. Note, though, that it's designed to be run as a shell script, so by default will call `process.exit(1)` if there are changes. If that isn't what you want, [raise an issue](https://github.com/mmkal/ts/issues). 29 | 30 | ```bash 31 | npm install check-clean 32 | ``` 33 | 34 | ```js 35 | const {checkClean} = require('check-clean') 36 | 37 | checkClean() 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/check-clean/src/__tests__/check-clean.test.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process' 2 | 3 | jest.mock('child_process', () => ({ 4 | execSync: jest.fn().mockReturnValue('\n'), 5 | })) 6 | 7 | test('requiring the cli module kicks off the script', () => { 8 | require('../cli') 9 | expect(childProcess.execSync).toHaveBeenCalledTimes(1) 10 | expect(childProcess.execSync).toHaveBeenCalledWith('git status --porcelain') 11 | }) 12 | -------------------------------------------------------------------------------- /packages/check-clean/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process' 2 | import {checkClean} from '..' 3 | 4 | jest.mock('child_process') 5 | 6 | const execSync = jest.spyOn(childProcess, 'execSync') 7 | 8 | const logs = [] as any[] 9 | const consoleInfo = jest.spyOn(console, 'info').mockImplementation((...args) => logs.push(['info', ...args])) 10 | const consoleError = jest.spyOn(console, 'error').mockImplementation((...args) => logs.push(['error', ...args])) 11 | 12 | beforeEach(() => { 13 | jest.clearAllMocks() 14 | logs.splice(0, logs.length) 15 | }) 16 | 17 | test('checks using git status', () => { 18 | execSync.mockReturnValue(Buffer.from('\n')) 19 | const exit: any = jest.fn() 20 | 21 | checkClean({exit, env: {}, argv: []}) 22 | 23 | expect(execSync).toHaveBeenCalledTimes(1) 24 | expect(execSync).toHaveBeenCalledWith('git status --porcelain') 25 | 26 | expect(exit).not.toHaveBeenCalled() 27 | expect(consoleInfo).not.toHaveBeenCalled() 28 | expect(consoleError).not.toHaveBeenCalled() 29 | }) 30 | 31 | test('logs and exits with error code if there are changes', () => { 32 | execSync.mockReturnValue(Buffer.from('A some/file.txt')) 33 | const exit: any = jest.fn() 34 | 35 | checkClean({exit, env: {CI: ''}, argv: []}) 36 | 37 | expect(exit).toHaveBeenCalledTimes(1) 38 | expect(exit).toHaveBeenCalledWith(1) 39 | 40 | expect(logs).toMatchInlineSnapshot(` 41 | Array [ 42 | Array [ 43 | "error", 44 | "error: git changes detected", 45 | ], 46 | Array [ 47 | "error", 48 | "check them in before running again", 49 | ], 50 | Array [ 51 | "info", 52 | "changes: 53 | A some/file.txt", 54 | ], 55 | ] 56 | `) 57 | }) 58 | 59 | test('logs and exits with error code if there are changes in CI', () => { 60 | execSync.mockReturnValue(Buffer.from('A some/file.txt')) 61 | const exit: any = jest.fn() 62 | 63 | checkClean({exit, env: {CI: 'true'}, argv: ['node', 'check-clean', 'some important context']}) 64 | 65 | expect(exit).toHaveBeenCalledTimes(1) 66 | expect(exit).toHaveBeenCalledWith(1) 67 | 68 | expect(logs).toMatchInlineSnapshot(` 69 | Array [ 70 | Array [ 71 | "error", 72 | "error: git changes detected", 73 | ], 74 | Array [ 75 | "error", 76 | "this was run in a CI environment, you probably don't want changes to have been generated here. Try to reproduce this locally, and check the changes in before re-running in CI", 77 | ], 78 | Array [ 79 | "info", 80 | "additional info: some important context", 81 | ], 82 | Array [ 83 | "info", 84 | "changes: 85 | A some/file.txt", 86 | ], 87 | ] 88 | `) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/check-clean/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {checkClean} from '.' 3 | 4 | checkClean() 5 | -------------------------------------------------------------------------------- /packages/check-clean/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process' 2 | 3 | const colours = { 4 | red: '\u001B[31m', 5 | yellow: '\u001B[33m', 6 | reset: '\u001B[0m', 7 | } 8 | 9 | export const checkClean = ({exit, env, argv} = process as Pick) => { 10 | const gitStatus = childProcess.execSync('git status --porcelain').toString().trim() 11 | if (!gitStatus) { 12 | return 13 | } 14 | console.error(`${colours.red}error: git changes detected`) 15 | console.error( 16 | env.CI 17 | ? `${colours.yellow}this was run in a CI environment, you probably don't want changes to have been generated here. Try to reproduce this locally, and check the changes in before re-running in CI${colours.reset}` 18 | : `${colours.red}check them in before running again${colours.reset}` 19 | ) 20 | 21 | if (argv[2]) { 22 | console.info(`${colours.reset}additional info: ${argv[2]}`) 23 | } 24 | console.info(`${colours.reset}changes:\n${gitStatus}`) 25 | exit(1) 26 | } 27 | -------------------------------------------------------------------------------- /packages/check-clean/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@mmkal/rig/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "tsBuildInfoFile": "dist/buildinfo.json", 7 | "typeRoots": [ 8 | "node_modules/@mmkal/rig/node_modules/@types" 9 | ] 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "dist" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@mmkal/rig/.eslintrc') 2 | module.exports = { 3 | ...base, 4 | ignorePatterns: [ 5 | ...base.ignorePatterns, 6 | '**/__tests__/custom-preset.js', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/__tests__ 3 | dist/buildinfo.json 4 | .eslintcache 5 | .eslintrc.js 6 | .rush 7 | .heft 8 | *.log 9 | tsconfig.json 10 | config/jest.config.json 11 | jest.config.js 12 | coverage 13 | 14 | gifs 15 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - eslint-plugin-codegen 2 | 3 | This log was last generated on Sat, 16 Oct 2021 13:17:24 GMT and should not be manually modified. 4 | 5 | ## 0.16.1 6 | Sat, 16 Oct 2021 13:17:24 GMT 7 | 8 | ### Patches 9 | 10 | - fix: make read-pkg-up a prod dependency (#243) 11 | 12 | ## 0.15.0 13 | Tue, 29 Jun 2021 08:48:24 GMT 14 | 15 | ### Minor changes 16 | 17 | - Run typescript in custom codegen script (#228) 18 | 19 | ### Patches 20 | 21 | - fix: disambiguate barrelled imports (#222) 22 | - chore(deps): update devdependencies (#203) - @renovate[bot] 23 | - fix(deps): update dependency @babel/generator to ~7.12.0 (#210) - @renovate[bot] 24 | - chore(deps): pin dependency eslint to 7.15.0 (#224) - @renovate[bot] 25 | 26 | ## 0.14.3 27 | Thu, 03 Dec 2020 19:10:22 GMT 28 | 29 | _Version update only_ 30 | 31 | ## 0.14.2 32 | Sat, 28 Nov 2020 19:10:00 GMT 33 | 34 | _Version update only_ 35 | 36 | ## 0.14.1 37 | Thu, 26 Nov 2020 17:06:36 GMT 38 | 39 | _Version update only_ 40 | 41 | ## 0.14.0 42 | Tue, 27 Oct 2020 16:18:39 GMT 43 | 44 | ### Minor changes 45 | 46 | - Permalink readme urls before publishing (#211) 47 | 48 | ## 0.13.3 49 | Mon, 12 Oct 2020 12:24:41 GMT 50 | 51 | ### Patches 52 | 53 | - fix: trim empty lines (#201) 54 | - fix(deps): update babel monorepo (#189) 55 | - fix(deps): update dependency eslint-config-xo-typescript to ~0.33.0 (#195) - @renovate[bot] 56 | - chore(deps): pin dependencies (#173) - @renovate[bot] 57 | 58 | ## 0.13.2 59 | Mon, 05 Oct 2020 22:38:33 GMT 60 | 61 | _Version update only_ 62 | 63 | ## 0.13.1 64 | Thu, 01 Oct 2020 14:48:13 GMT 65 | 66 | ### Patches 67 | 68 | - chore: npmignore coverage folder (#184) 69 | 70 | ## 0.13.0 71 | Mon, 28 Sep 2020 14:08:20 GMT 72 | 73 | ### Minor changes 74 | 75 | - Add support for `.tsx` and `.jsx` file extensions (#175) 76 | 77 | ## 0.12.3 78 | Fri, 18 Sep 2020 16:56:41 GMT 79 | 80 | ### Patches 81 | 82 | - fix(npmignore): exclude rush files (#169) 83 | 84 | ## 0.12.2 85 | Thu, 17 Sep 2020 10:47:43 GMT 86 | 87 | _Version update only_ 88 | 89 | ## 0.12.1 90 | Wed, 16 Sep 2020 21:06:32 GMT 91 | 92 | _Initial release_ 93 | 94 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/gifs/barrel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmkal/ts/0c4bbe0cb9d8d755e07cb120058280396bfaaade/packages/eslint-plugin-codegen/gifs/barrel.gif -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/gifs/custom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmkal/ts/0c4bbe0cb9d8d755e07cb120058280396bfaaade/packages/eslint-plugin-codegen/gifs/custom.gif -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/gifs/labeler.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmkal/ts/0c4bbe0cb9d8d755e07cb120058280396bfaaade/packages/eslint-plugin-codegen/gifs/labeler.gif -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/gifs/markdownFromJsdoc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmkal/ts/0c4bbe0cb9d8d755e07cb120058280396bfaaade/packages/eslint-plugin-codegen/gifs/markdownFromJsdoc.gif -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/gifs/markdownFromTests.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmkal/ts/0c4bbe0cb9d8d755e07cb120058280396bfaaade/packages/eslint-plugin-codegen/gifs/markdownFromTests.gif -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/gifs/markdownTOC.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmkal/ts/0c4bbe0cb9d8d755e07cb120058280396bfaaade/packages/eslint-plugin-codegen/gifs/markdownTOC.gif -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/gifs/monorepoTOC.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmkal/ts/0c4bbe0cb9d8d755e07cb120058280396bfaaade/packages/eslint-plugin-codegen/gifs/monorepoTOC.gif -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@mmkal/rig/jest.config') 2 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-codegen", 3 | "version": "0.16.1", 4 | "keywords": [ 5 | "eslint", 6 | "eslint plugin", 7 | "codegen", 8 | "markdown", 9 | "typescript", 10 | "documentation", 11 | "generator", 12 | "generation", 13 | "md", 14 | "contents", 15 | "jsdoc", 16 | "barrel" 17 | ], 18 | "homepage": "https://github.com/mmkal/ts/tree/main/packages/eslint-plugin-codegen#readme", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/mmkal/ts.git", 22 | "directory": "packages/eslint-plugin-codegen" 23 | }, 24 | "license": "Apache-2.0", 25 | "main": "dist/index.js", 26 | "types": "dist/index.d.ts", 27 | "scripts": { 28 | "prebuild": "npm run clean", 29 | "build": "rig tsc -p .", 30 | "clean": "rig rimraf dist", 31 | "lint": "rig eslint --cache .", 32 | "prepack": "rig permalink", 33 | "postpack": "rig unpermalink", 34 | "test": "rig jest" 35 | }, 36 | "dependencies": { 37 | "@babel/core": "^7.11.6", 38 | "@babel/generator": "~7.12.0", 39 | "@babel/parser": "^7.11.5", 40 | "@babel/traverse": "^7.11.5", 41 | "expect": "^26.0.0", 42 | "fp-ts": "^2.1.0", 43 | "glob": "^7.1.4", 44 | "io-ts": "^2.2.4", 45 | "io-ts-extra": "workspace:*", 46 | "js-yaml": "^3.14.0", 47 | "lodash": "^4.17.15", 48 | "string.prototype.matchall": "^4.0.2", 49 | "read-pkg-up": "^7.0.1" 50 | }, 51 | "devDependencies": { 52 | "@babel/types": "7.12.11", 53 | "@mmkal/rig": "workspace:*", 54 | "@types/babel__generator": "7.6.2", 55 | "@types/babel__traverse": "7.11.0", 56 | "@types/dedent": "0.7.0", 57 | "@types/eslint": "7.2.6", 58 | "@types/glob": "7.1.3", 59 | "@types/js-yaml": "3.12.5", 60 | "@types/lodash": "4.14.165", 61 | "@types/minimatch": "3.0.3", 62 | "dedent": "0.7.0", 63 | "expect-type2": "npm:expect-type@0.14.0", 64 | "minimatch": "3.0.4", 65 | "eslint": "7.15.0", 66 | "ts-node": "9.1.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/__tests__/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import {processors} from '..' 2 | import dedent from 'dedent' 3 | 4 | describe('markdown processor', () => { 5 | const markdownProcessor = processors['.md'] 6 | 7 | test('preprocessor comments out markdown', () => { 8 | const markdown = dedent` 9 | # Title 10 | 11 | 12 | 13 |
html
14 | 15 | \`\`\`js 16 | // some javascript 17 | const x = 1 18 | \`\`\` 19 | ` 20 | 21 | const preprocessed = markdownProcessor.preprocess!(markdown) 22 | 23 | expect(preprocessed).toMatchInlineSnapshot(` 24 | Array [ 25 | "// eslint-plugin-codegen:trim# Title 26 | 27 | // eslint-plugin-codegen:trim 28 | 29 | // eslint-plugin-codegen:trim
html
30 | 31 | // eslint-plugin-codegen:trim\`\`\`js 32 | // eslint-plugin-codegen:trim// some javascript 33 | // eslint-plugin-codegen:trimconst x = 1 34 | // eslint-plugin-codegen:trim\`\`\`", 35 | ] 36 | `) 37 | }) 38 | 39 | test('postprocessor flattens message lists', () => { 40 | // @ts-expect-error 41 | const postprocessed = markdownProcessor.postprocess!([[{line: 1}], [{line: 2}]]) 42 | 43 | expect(postprocessed).toEqual([{line: 1}, {line: 2}]) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/__tests__/rule.test.ts: -------------------------------------------------------------------------------- 1 | import {RuleTester} from 'eslint' 2 | import * as codegen from '..' 3 | import baseDedent from 'dedent' 4 | import * as os from 'os' 5 | 6 | jest.mock('glob', () => ({ 7 | sync: () => ['foo.ts', 'bar.ts'], 8 | })) 9 | 10 | /** wrapper for dedent which respects os.EOL */ 11 | const dedent = (...args: Parameters) => { 12 | const result = baseDedent(...args) 13 | return result.replace(/\r?\n/g, os.EOL) 14 | } 15 | 16 | Object.assign(RuleTester, { 17 | /* eslint-disable jest/expect-expect, jest/valid-title */ 18 | it: (name: string, fn: any) => { 19 | test(name.replace(/\r?\n/g, ' \\n ').trim(), fn) 20 | }, 21 | /* eslint-enable jest/expect-expect, jest/valid-title */ 22 | }) 23 | 24 | const tester = new RuleTester() 25 | tester.run('codegen', codegen.rules.codegen, { 26 | valid: [ 27 | { 28 | filename: 'index.ts', 29 | code: '', 30 | }, 31 | { 32 | filename: __filename, 33 | code: dedent` 34 | // codegen:start {preset: empty} 35 | // codegen:end 36 | `, 37 | }, 38 | ], 39 | invalid: [ 40 | { 41 | filename: 'index.ts', 42 | code: dedent` 43 | // codegen:start {preset: barrel} 44 | `, 45 | errors: [{message: `couldn't find end marker (expected regex /\\/\\/ codegen:end/g)`}], 46 | output: dedent` 47 | // codegen:start {preset: barrel} 48 | // codegen:end 49 | `, 50 | }, 51 | { 52 | filename: __filename, 53 | code: dedent` 54 | // codegen:start "" 55 | // codegen:end 56 | `, 57 | errors: [{message: /unknown preset undefined./}], 58 | }, 59 | { 60 | filename: __filename, 61 | code: dedent` 62 | // codegen:start {preset: doesNotExist} 63 | // codegen:end 64 | `, 65 | errors: [{message: /unknown preset doesNotExist. Available presets: .*/}], 66 | }, 67 | { 68 | filename: __filename, 69 | code: dedent` 70 | // codegen:start {abc: !Tag: not valid yaml!} 71 | // codegen:end 72 | `, 73 | errors: [{message: /Error parsing options. YAMLException/}], 74 | }, 75 | { 76 | filename: __filename, 77 | code: dedent` 78 | // codegen:start {preset: empty} 79 | // codegen:start {preset: empty} 80 | // codegen:end 81 | `, 82 | errors: [{message: /couldn't find end marker/}], 83 | output: dedent` 84 | // codegen:start {preset: empty} 85 | // codegen:end 86 | // codegen:start {preset: empty} 87 | // codegen:end 88 | `, 89 | }, 90 | { 91 | filename: __filename, 92 | parserOptions: {ecmaVersion: 2015, sourceType: 'module'}, 93 | code: dedent` 94 | // codegen:start {preset: barrel} 95 | // codegen:end 96 | `, 97 | errors: [{message: /content doesn't match/}], 98 | output: dedent` 99 | // codegen:start {preset: barrel} 100 | export * from './foo' 101 | export * from './bar' 102 | // codegen:end 103 | `, 104 | }, 105 | { 106 | filename: __filename, 107 | code: dedent` 108 | // codegen:start {preset: custom, source: ../presets/__tests__/custom-preset.js, export: thrower} 109 | // codegen:end 110 | `, 111 | errors: [{message: /Error: test error!/}], 112 | }, 113 | ], 114 | }) 115 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as os from 'os' 3 | import * as jsYaml from 'js-yaml' 4 | import {tryCatch} from 'fp-ts/lib/Either' 5 | import * as eslint from 'eslint' 6 | import * as presetsModule from './presets' 7 | import expect from 'expect' 8 | 9 | // todo: run prettier on output, if found 10 | // todo: codegen/fs rule. type fs.anything and it generates an import for fs. same for path and os. 11 | 12 | type MatchAll = (text: string, pattern: string | RegExp) => Iterable>> 13 | // eslint-disable-next-line @typescript-eslint/no-var-requires 14 | const matchAll: MatchAll = require('string.prototype.matchall') 15 | 16 | export {Preset} from './presets' 17 | 18 | export {presetsModule as presets} 19 | 20 | const getPreprocessor = (): eslint.Linter.LintOptions => { 21 | return { 22 | preprocess: text => [ 23 | text 24 | .split(/\r?\n/) 25 | .map(line => line && `// eslint-plugin-codegen:trim${line}`) 26 | .join(os.EOL), 27 | ], 28 | postprocess: messageLists => ([] as eslint.Linter.LintMessage[]).concat(...messageLists), 29 | // @ts-expect-error 30 | supportsAutofix: true, 31 | } 32 | } 33 | export const processors: Record = { 34 | '.md': getPreprocessor(), 35 | '.yml': getPreprocessor(), 36 | '.yaml': getPreprocessor(), 37 | } 38 | 39 | const codegen: eslint.Rule.RuleModule = { 40 | // @ts-expect-error 41 | meta: {fixable: true}, 42 | create: (context: eslint.Rule.RuleContext) => { 43 | const validate = () => { 44 | const sourceCode = context 45 | .getSourceCode() 46 | .text.split(os.EOL) 47 | .map(line => `${line}`.replace('// eslint-plugin-codegen:trim', '')) 48 | .join(os.EOL) 49 | 50 | const markersByExtension: Record = { 51 | '.md': { 52 | start: //g, 53 | end: //g, 54 | }, 55 | '.ts': { 56 | start: /\/\/ codegen:start ?(.*)/g, 57 | end: /\/\/ codegen:end/g, 58 | }, 59 | '.yml': { 60 | start: /# codegen:start ?(.*)/g, 61 | end: /# codegen:end/g, 62 | }, 63 | } 64 | markersByExtension['.tsx'] = markersByExtension['.ts'] 65 | markersByExtension['.js'] = markersByExtension['.ts'] 66 | markersByExtension['.cjs'] = markersByExtension['.ts'] 67 | markersByExtension['.mjs'] = markersByExtension['.ts'] 68 | markersByExtension['.jsx'] = markersByExtension['.ts'] 69 | markersByExtension['.yaml'] = markersByExtension['.yml'] 70 | 71 | const markers = markersByExtension[path.extname(context.getFilename())] 72 | const position = (index: number) => { 73 | const stringUpToPosition = sourceCode.slice(0, index) 74 | const lines = stringUpToPosition.split(os.EOL) 75 | return {line: lines.length, column: lines[lines.length - 1].length} 76 | } 77 | 78 | const startMatches = [...matchAll(sourceCode, markers.start)].filter(startMatch => { 79 | const prevCharacter = sourceCode[startMatch.index! - 1] 80 | return !prevCharacter || prevCharacter === '\n' 81 | }) 82 | 83 | startMatches.forEach((startMatch, startMatchesIndex) => { 84 | const startIndex = startMatch.index!.valueOf() 85 | const start = position(startIndex) 86 | const startMarkerLoc = {start, end: {...start, column: start.column + startMatch[0].length}} 87 | const searchForEndMarkerUpTo = 88 | startMatchesIndex === startMatches.length - 1 ? sourceCode.length : startMatches[startMatchesIndex + 1].index 89 | const endMatch = [...matchAll(sourceCode.slice(0, searchForEndMarkerUpTo), markers.end)].find( 90 | e => e.index! > startMatch.index! 91 | ) 92 | if (!endMatch) { 93 | const afterStartMatch = startIndex + startMatch[0].length 94 | context.report({ 95 | message: `couldn't find end marker (expected regex ${markers.end})`, 96 | loc: startMarkerLoc, 97 | fix: fixer => 98 | fixer.replaceTextRange( 99 | [afterStartMatch, afterStartMatch], 100 | os.EOL + markers.end.source.replace(/\\/g, '') 101 | ), 102 | }) 103 | return 104 | } 105 | const maybeOptions = tryCatch( 106 | () => jsYaml.safeLoad(startMatch[1]) as Record, 107 | err => err 108 | ) 109 | if (maybeOptions._tag === 'Left') { 110 | context.report({message: `Error parsing options. ${maybeOptions.left}`, loc: startMarkerLoc}) 111 | return 112 | } 113 | const opts = maybeOptions.right || {} 114 | const presets: Record | undefined> = { 115 | ...presetsModule, 116 | ...context.options[0]?.presets, 117 | } 118 | const preset = typeof opts?.preset === 'string' && presets[opts.preset] 119 | if (typeof preset !== 'function') { 120 | context.report({ 121 | message: `unknown preset ${opts.preset}. Available presets: ${Object.keys(presets).join(', ')}`, 122 | loc: startMarkerLoc, 123 | }) 124 | return 125 | } 126 | 127 | const range: eslint.AST.Range = [startIndex + startMatch[0].length + os.EOL.length, endMatch.index!] 128 | const existingContent = sourceCode.slice(...range) 129 | const normalise = (val: string) => val.trim().replace(/\r?\n/g, os.EOL) 130 | 131 | const result = tryCatch( 132 | () => { 133 | const meta = {filename: context.getFilename(), existingContent} 134 | return preset({meta, options: opts}) 135 | }, 136 | err => `${err}` 137 | ) 138 | 139 | if (result._tag === 'Left') { 140 | context.report({message: result.left, loc: startMarkerLoc}) 141 | return 142 | } 143 | const expected = result.right 144 | try { 145 | expect(normalise(existingContent)).toBe(normalise(expected)) 146 | } catch (e: unknown) { 147 | const loc = {start: position(range[0]), end: position(range[1])} 148 | context.report({ 149 | message: `content doesn't match: ${e}`, 150 | loc, 151 | fix: fixer => fixer.replaceTextRange(range, normalise(expected) + os.EOL), 152 | }) 153 | } 154 | }) 155 | } 156 | validate() 157 | return {} 158 | }, 159 | } 160 | 161 | export const rules = {codegen} 162 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/custom-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = ({options}) => 'Whole module export with input: ' + options.input 2 | module.exports.getText = ({options}) => 'Named export with input: ' + options.input 3 | module.exports.thrower = () => { 4 | throw Error('test error!') 5 | } 6 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/custom-preset.ts: -------------------------------------------------------------------------------- 1 | export const getText = () => 'typescript text' 2 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/custom.test.ts: -------------------------------------------------------------------------------- 1 | import * as preset from '../custom' 2 | import * as path from 'path' 3 | 4 | jest.mock('ts-node/register/transpile-only') 5 | 6 | test('custom preset validation', () => { 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const customPreset = require('./custom-preset') 9 | 10 | expect(Object.keys(customPreset)).toEqual(['getText', 'thrower']) 11 | 12 | expect(customPreset.getText.toString().trim()).toMatch(/'Named export with input: ' \+ options.input/) 13 | }) 14 | 15 | test('named export', () => { 16 | expect( 17 | preset.custom({ 18 | meta: {filename: __filename, existingContent: ''}, 19 | options: {source: './custom-preset.js', export: 'getText', input: 'abc'}, 20 | }) 21 | ).toMatchInlineSnapshot(`"Named export with input: abc"`) 22 | }) 23 | 24 | test('whole module export', () => { 25 | expect( 26 | preset.custom({ 27 | meta: {filename: __filename, existingContent: ''}, 28 | options: {source: './custom-preset.js', input: 'def'}, 29 | }) 30 | ).toMatchInlineSnapshot(`"Whole module export with input: def"`) 31 | }) 32 | 33 | test('load typescript with ts-node', () => { 34 | expect( 35 | preset.custom({ 36 | meta: {filename: __filename, existingContent: ''}, 37 | options: {source: './custom-preset.ts', export: 'getText'}, 38 | }) 39 | ).toMatchInlineSnapshot(`"typescript text"`) 40 | }) 41 | 42 | test('dev mode, deletes require cache', () => { 43 | expect( 44 | preset.custom({ 45 | meta: {filename: __filename, existingContent: ''}, 46 | options: {source: './custom-preset.js', input: 'ghi', dev: true}, 47 | }) 48 | ).toMatchInlineSnapshot(`"Whole module export with input: ghi"`) 49 | }) 50 | 51 | test(`when source isn't specified, uses filename`, () => { 52 | expect( 53 | preset.custom({ 54 | meta: {filename: path.join(__dirname, 'custom-preset.js'), existingContent: ''}, 55 | options: {input: 'abc'}, 56 | }) 57 | ).toEqual('Whole module export with input: abc') 58 | }) 59 | 60 | test('errors for non-existent source file', () => { 61 | expect(() => 62 | preset.custom({ 63 | meta: {filename: __filename, existingContent: ''}, 64 | options: {source: './does-not-exist.ts'}, 65 | }) 66 | ).toThrowError(/Source path is not a file: .*does-not-exist.ts/) 67 | }) 68 | 69 | test('errors if directory passed as source', () => { 70 | expect(() => 71 | preset.custom({ 72 | meta: {filename: __filename, existingContent: ''}, 73 | options: {source: '__tests__'}, 74 | }) 75 | ).toThrowError(/Source path is not a file: .*__tests__/) 76 | }) 77 | 78 | test('errors for non-existent export', () => { 79 | expect(() => 80 | preset.custom({ 81 | meta: {filename: __filename, existingContent: ''}, 82 | options: {source: './custom-preset.js', export: 'doesNotExist', input: 'abc'}, 83 | }) 84 | ).toThrowError(/Couldn't find export doesNotExist from .*custom-preset.js - got undefined/) 85 | }) 86 | 87 | test('errors for export with wrong type', () => { 88 | expect(() => 89 | preset.custom({ 90 | meta: {filename: __filename, existingContent: ''}, 91 | options: {source: './invalid-custom-preset.js', input: 'abc'}, 92 | }) 93 | ).toThrowError(/Couldn't find export function from .*invalid-custom-preset.js - got object/) 94 | }) 95 | 96 | test('can require module first', () => { 97 | expect(() => 98 | preset.custom({ 99 | meta: {filename: __filename, existingContent: ''}, 100 | options: {source: './custom-preset.js', require: 'thismoduledoesnotexist'}, 101 | }) 102 | ).toThrowError(/Cannot find module 'thismoduledoesnotexist' from 'src\/presets\/custom.ts'/) 103 | }) 104 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/empty.test.ts: -------------------------------------------------------------------------------- 1 | import * as preset from '../empty' 2 | 3 | const emptyReadme = {filename: 'readme.md', existingContent: ''} 4 | 5 | test('generates nothing', () => { 6 | expect( 7 | preset.empty({ 8 | meta: emptyReadme, 9 | options: {}, 10 | }) 11 | ).toEqual('') 12 | }) 13 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/invalid-custom-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = {x: 1} 2 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/labeler.test.ts: -------------------------------------------------------------------------------- 1 | import * as preset from '../labeler' 2 | import * as glob from 'glob' 3 | import minimatch from 'minimatch' 4 | import readPkgUp from 'read-pkg-up' 5 | 6 | const mockFs: any = {} 7 | 8 | beforeEach(() => { 9 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 10 | Object.keys(mockFs).forEach(k => delete mockFs[k]) 11 | }) 12 | 13 | jest.mock('fs', () => { 14 | const actual = jest.requireActual('fs') 15 | const reader = (orig: string) => (...args: any[]) => { 16 | const path = args[0].replace(process.cwd() + '\\', '').replace(/\\/g, '/') 17 | // const fn = path in mockFs ? mockImpl : actual[orig] 18 | if (path in mockFs) { 19 | return mockFs[path] 20 | } 21 | return actual[orig](...args) 22 | } 23 | return { 24 | ...actual, 25 | readFileSync: reader('readFileSync'), 26 | existsSync: reader('existsSync'), 27 | readdirSync: (path: string) => Object.keys(mockFs).filter(k => k.startsWith(path.replace(/^\.\/?/, ''))), 28 | statSync: () => ({isFile: () => true, isDirectory: () => false}), 29 | } 30 | }) 31 | 32 | jest.mock('glob') 33 | 34 | jest.spyOn(glob, 'sync').mockImplementation((pattern, opts) => { 35 | const found = Object.keys(mockFs).filter(k => minimatch(k, pattern)) 36 | const ignores = typeof opts?.ignore === 'string' ? [opts?.ignore] : opts?.ignore || [] 37 | return found.filter(f => ignores.every(i => !minimatch(f, i))) 38 | }) 39 | 40 | jest.mock('read-pkg-up') 41 | 42 | jest.spyOn(readPkgUp, 'sync').mockImplementation(options => 43 | Object.entries(mockFs) 44 | .map(([path, content]) => ({ 45 | path, 46 | packageJson: JSON.parse(content as string), 47 | })) 48 | .find(p => options.cwd?.includes(p.path.replace('package.json', ''))) 49 | ) 50 | 51 | const labelerDotYml = {filename: '.github/labeler.yml', existingContent: ''} 52 | 53 | beforeEach(() => { 54 | Object.assign(mockFs, { 55 | 'package.json': '{ "workspaces": ["packages/*"] }', 56 | 57 | 'packages/package1/package.json': '{ "name": "package1-aaa"}', 58 | 'packages/package2/package.json': '{ "name": "package2-bbb"}', 59 | 'packages/package3/package.json': '{ "name": "package3-ccc"}', 60 | }) 61 | }) 62 | 63 | test('generate labels', () => { 64 | expect( 65 | preset.labeler({ 66 | meta: labelerDotYml, 67 | options: {}, 68 | }) 69 | ).toMatchInlineSnapshot(` 70 | "package1-aaa: 71 | - packages/package1/**/* 72 | package2-bbb: 73 | - packages/package2/**/* 74 | package3-ccc: 75 | - packages/package3/**/* 76 | " 77 | `) 78 | }) 79 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/markdown-from-jsdoc.test.ts: -------------------------------------------------------------------------------- 1 | import * as preset from '../markdown-from-jsdoc' 2 | import dedent from 'dedent' 3 | 4 | const mockFs: any = {} 5 | 6 | beforeEach(() => { 7 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 8 | Object.keys(mockFs).forEach(k => delete mockFs[k]) 9 | }) 10 | 11 | jest.mock('fs', () => { 12 | const actual = jest.requireActual('fs') 13 | const reader = (orig: string) => (...args: any[]) => { 14 | const path = args[0].replace(/\\/g, '/') 15 | // const fn = path in mockFs ? mockImpl : actual[orig] 16 | if (path in mockFs) { 17 | return mockFs[path] 18 | } 19 | return actual[orig](...args) 20 | } 21 | return { 22 | readFileSync: reader('readFileSync'), 23 | existsSync: reader('existsSync'), 24 | readdirSync: (path: string) => Object.keys(mockFs).filter(k => k.startsWith(path.replace(/^\.\/?/, ''))), 25 | statSync: () => ({isFile: () => true, isDirectory: () => false}), 26 | } 27 | }) 28 | 29 | const emptyReadme = {filename: 'readme.md', existingContent: ''} 30 | test('generate markdown', () => { 31 | Object.assign(mockFs, { 32 | 'index.ts': dedent` 33 | /** 34 | * Adds two numbers 35 | * 36 | * @example const example1 = fn(1, 2) // returns 3 37 | * 38 | * @description Uses js \`+\` operator 39 | * 40 | * @example const example1 = fn(1, 20) // returns 21 41 | * 42 | * @see subtract for the converse 43 | * 44 | * @link http://google.com has a calculator in it too 45 | * 46 | * @param a {number} the first number 47 | * @param b {number} the second number 48 | */ 49 | export const add = (a: number, b: number) => a + b 50 | 51 | /** 52 | * Subtracts two numbers 53 | * 54 | * @example const example1 = subtract(5, 3) // returns 2 55 | * 56 | * @description Uses js \`-\` operator 57 | * 58 | * @param a {number} the first number 59 | * @param b {number} the second number 60 | */ 61 | export const add = (a: number, b: number) => a - b 62 | 63 | /** 64 | * @param a 65 | * @param b 66 | * multi-line description 67 | * for 'b' 68 | */ 69 | export const multiply = (a: number, b: number) => a * b 70 | `, 71 | }) 72 | 73 | expect( 74 | preset.markdownFromJsdoc({ 75 | meta: emptyReadme, 76 | options: {source: 'index.ts', export: 'add'}, 77 | }) 78 | ).toMatchInlineSnapshot(` 79 | "#### [add](./index.ts#L17) 80 | 81 | Adds two numbers 82 | 83 | ##### Example 84 | 85 | \`\`\`typescript 86 | const example1 = fn(1, 2) // returns 3 87 | \`\`\` 88 | 89 | Uses js \`+\` operator 90 | 91 | ##### Example 92 | 93 | \`\`\`typescript 94 | const example1 = fn(1, 20) // returns 21 95 | \`\`\` 96 | 97 | ##### Link 98 | 99 | http://google.com has a calculator in it too 100 | 101 | ##### Params 102 | 103 | |name|description | 104 | |----|--------------------------| 105 | |a |{number} the first number | 106 | |b |{number} the second number|" 107 | `) 108 | 109 | expect( 110 | preset.markdownFromJsdoc({ 111 | meta: emptyReadme, 112 | options: {source: 'index.ts', export: 'multiply'}, 113 | }) 114 | ).toMatchInlineSnapshot(` 115 | "#### [multiply](./index.ts#L37) 116 | 117 | ##### Params 118 | 119 | |name|description | 120 | |----|-----------------------------------| 121 | |a | | 122 | |b |multi-line description
for 'b'|" 123 | `) 124 | }) 125 | 126 | test('not found export', () => { 127 | Object.assign(mockFs, { 128 | 'index.ts': dedent` 129 | /** docs */ 130 | export const add = (a: number, b: number) => a + b 131 | `, 132 | }) 133 | 134 | expect(() => 135 | preset.markdownFromJsdoc({ 136 | meta: emptyReadme, 137 | options: {source: 'index.ts', export: 'subtract'}, 138 | }) 139 | ).toThrowError(/Couldn't find export in .*index.ts with jsdoc called subtract/) 140 | }) 141 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/markdown-from-tests.test.ts: -------------------------------------------------------------------------------- 1 | import * as preset from '../markdown-from-tests' 2 | import dedent from 'dedent' 3 | 4 | const mockFs: any = {} 5 | 6 | beforeEach(() => { 7 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 8 | Object.keys(mockFs).forEach(k => delete mockFs[k]) 9 | }) 10 | 11 | jest.mock('fs', () => { 12 | const actual = jest.requireActual('fs') 13 | const reader = (orig: string) => (...args: any[]) => { 14 | const path = args[0].replace(/\\/g, '/') 15 | // const fn = path in mockFs ? mockImpl : actual[orig] 16 | if (path in mockFs) { 17 | return mockFs[path] 18 | } 19 | return actual[orig](...args) 20 | } 21 | return { 22 | readFileSync: reader('readFileSync'), 23 | existsSync: reader('existsSync'), 24 | readdirSync: (path: string) => Object.keys(mockFs).filter(k => k.startsWith(path.replace(/^\.\/?/, ''))), 25 | statSync: () => ({isFile: () => true, isDirectory: () => false}), 26 | } 27 | }) 28 | 29 | const emptyReadme = {filename: 'readme.md', existingContent: ''} 30 | 31 | test('generate markdown', () => { 32 | Object.assign(mockFs, { 33 | 'test.ts': dedent` 34 | import {calculator} from '..' 35 | 36 | beforeEach(() => { 37 | calculator.setup() 38 | }) 39 | 40 | test('addition', () => { 41 | expect(calculator.add(1, 1)).toEqual(2) 42 | }) 43 | 44 | it('subtraction', () => { 45 | expect(calculator.subtract(1, 1)).toEqual(0) 46 | }) 47 | 48 | const nonLiteralTestName = 'also subtraction' 49 | it(nonLiteralTestName, () => { 50 | expect(calculator.subtract(1, 1)).toEqual(0) 51 | }) 52 | 53 | test('multiplication', () => { 54 | expect(calculator.multiply(2, 3)).toEqual(6) 55 | }) 56 | 57 | test.skip('division', () => { 58 | expect(calculator.divide(1, 0)).toEqual(Infinity) 59 | }) 60 | `, 61 | }) 62 | 63 | const withHeaders = preset.markdownFromTests({ 64 | meta: emptyReadme, 65 | options: {source: 'test.ts', headerLevel: 4}, 66 | }) 67 | expect(withHeaders).toMatchInlineSnapshot(` 68 | "#### addition: 69 | 70 | \`\`\`typescript 71 | expect(calculator.add(1, 1)).toEqual(2) 72 | \`\`\` 73 | 74 | #### subtraction: 75 | 76 | \`\`\`typescript 77 | expect(calculator.subtract(1, 1)).toEqual(0) 78 | \`\`\` 79 | 80 | #### multiplication: 81 | 82 | \`\`\`typescript 83 | expect(calculator.multiply(2, 3)).toEqual(6) 84 | \`\`\`" 85 | `) 86 | const withoutHeaders = preset.markdownFromTests({ 87 | meta: emptyReadme, 88 | options: {source: 'test.ts'}, 89 | }) 90 | 91 | expect(withoutHeaders).toEqual(withHeaders.replace(/#### /g, '')) 92 | }) 93 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/markdown-toc.test.ts: -------------------------------------------------------------------------------- 1 | import * as preset from '../markdown-toc' 2 | import dedent from 'dedent' 3 | 4 | const mockFs: any = {} 5 | 6 | beforeEach(() => { 7 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 8 | Object.keys(mockFs).forEach(k => delete mockFs[k]) 9 | }) 10 | 11 | jest.mock('fs', () => { 12 | const actual = jest.requireActual('fs') 13 | const reader = (orig: string) => (...args: any[]) => { 14 | const path = args[0].replace(/\\/g, '/') 15 | // const fn = path in mockFs ? mockImpl : actual[orig] 16 | if (path in mockFs) { 17 | return mockFs[path] 18 | } 19 | return actual[orig](...args) 20 | } 21 | return { 22 | readFileSync: reader('readFileSync'), 23 | existsSync: reader('existsSync'), 24 | readdirSync: (path: string) => Object.keys(mockFs).filter(k => k.startsWith(path.replace(/^\.\/?/, ''))), 25 | statSync: () => ({isFile: () => true, isDirectory: () => false}), 26 | } 27 | }) 28 | 29 | const emptyReadme = {filename: 'readme.md', existingContent: ''} 30 | 31 | test('generate markdown', () => { 32 | Object.assign(mockFs, { 33 | 'readme.md': dedent` 34 | # H1 35 | Text 36 | ## H2 37 | More text 38 | ### H3 39 | Some content 40 | ![](an-image.png) 41 | ### Another H3 42 | #### H4 duplicate 43 | ##### H5 44 | ##### H5 45 | #### H4 duplicate 46 | More 47 | ## Another H2 48 | `, 49 | }) 50 | 51 | expect( 52 | preset.markdownTOC({ 53 | meta: emptyReadme, 54 | options: {}, 55 | }) 56 | ).toMatchInlineSnapshot(` 57 | "- [H1](#h1) 58 | - [H2](#h2) 59 | - [H3](#h3) 60 | - [Another H3](#another-h3) 61 | - [H4 duplicate](#h4-duplicate) 62 | - [H5](#h5) 63 | - [H5](#h5-1) 64 | - [H4 duplicate](#h4-duplicate-1) 65 | - [Another H2](#another-h2)" 66 | `) 67 | 68 | expect( 69 | preset.markdownTOC({ 70 | meta: emptyReadme, 71 | options: { 72 | minDepth: 2, 73 | maxDepth: 3, 74 | }, 75 | }) 76 | ).toMatchInlineSnapshot(` 77 | "- [H2](#h2) 78 | - [Another H2](#another-h2)" 79 | `) 80 | }) 81 | 82 | test('calculates min hashes', () => { 83 | Object.assign(mockFs, { 84 | 'readme.md': dedent` 85 | ### H3 86 | ### Another H3 87 | #### H4 duplicate 88 | ##### H5 89 | ##### H5 90 | `, 91 | }) 92 | 93 | expect( 94 | preset.markdownTOC({ 95 | meta: emptyReadme, 96 | options: {}, 97 | }) 98 | ).toMatchInlineSnapshot(` 99 | "- [H3](#h3) 100 | - [Another H3](#another-h3) 101 | - [H4 duplicate](#h4-duplicate) 102 | - [H5](#h5) 103 | - [H5](#h5-1)" 104 | `) 105 | 106 | expect( 107 | preset.markdownTOC({ 108 | meta: emptyReadme, 109 | options: { 110 | minDepth: 2, 111 | maxDepth: 3, 112 | }, 113 | }) 114 | ).toMatchInlineSnapshot(`""`) 115 | }) 116 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/__tests__/monorepo-toc.test.ts: -------------------------------------------------------------------------------- 1 | import * as preset from '../monorepo-toc' 2 | import dedent from 'dedent' 3 | import * as glob from 'glob' 4 | import minimatch from 'minimatch' 5 | 6 | const mockFs: any = {} 7 | 8 | beforeEach(() => { 9 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 10 | Object.keys(mockFs).forEach(k => delete mockFs[k]) 11 | }) 12 | 13 | jest.mock('fs', () => { 14 | const actual = jest.requireActual('fs') 15 | const reader = (orig: string) => (...args: any[]) => { 16 | const path = args[0] 17 | .replace(process.cwd() + '\\', '') 18 | .replace(process.cwd() + '/', '') 19 | .replace(/\\/g, '/') 20 | // const fn = path in mockFs ? mockImpl : actual[orig] 21 | if (path in mockFs) { 22 | return mockFs[path] 23 | } 24 | return actual[orig](...args) 25 | } 26 | return { 27 | ...actual, 28 | readFileSync: reader('readFileSync'), 29 | existsSync: reader('existsSync'), 30 | readdirSync: (path: string) => Object.keys(mockFs).filter(k => k.startsWith(path.replace(/^\.\/?/, ''))), 31 | statSync: () => ({isFile: () => true, isDirectory: () => false}), 32 | } 33 | }) 34 | 35 | jest.mock('glob') 36 | 37 | jest.spyOn(glob, 'sync').mockImplementation((pattern, opts) => { 38 | const found = Object.keys(mockFs).filter(k => minimatch(k, pattern)) 39 | const ignores = typeof opts?.ignore === 'string' ? [opts?.ignore] : opts?.ignore || [] 40 | return found.filter(f => ignores.every(i => !minimatch(f, i))) 41 | }) 42 | 43 | const emptyReadme = {filename: 'readme.md', existingContent: ''} 44 | 45 | beforeEach(() => { 46 | Object.assign(mockFs, { 47 | 'package.json': '{ "workspaces": ["packages/*"] }', 48 | 49 | 'withBadWorkspaces/package.json': '{ "workspaces": "not an array!" }', 50 | 51 | 'lerna.json': '{ "packages": ["packages/package1", "packages/package2"] }', 52 | 53 | 'packages/package1/package.json': 54 | '{ "name": "package1", "description": "first package with an inline package.json description. Quite a long inline description, in fact." }', 55 | 56 | 'packages/package2/package.json': '{ "name": "package2", "description": "package 2" }', 57 | 'packages/package2/readme.md': dedent` 58 | # Package 2 59 | Readme for package 2 60 | `, 61 | 62 | 'packages/package3/package.json': '{ "name": "package3", "description": "package 3" }', 63 | 'packages/package3/readme.md': dedent` 64 | # Package 3 65 | Readme for package 3 66 | `, 67 | 68 | 'packages/package4/package.json': '{ "name": "package4", "description": "fourth package" }', 69 | 'packages/package4/README.md': dedent` 70 | # Package 4 71 | 72 | ## Subheading 73 | 74 | More details about package 4. Package 4 has a detailed readme, with multiple sections 75 | 76 | ### Sub-sub-heading 77 | 78 | Here's another section, with more markdown content in it. 79 | `, 80 | }) 81 | }) 82 | 83 | test('generate markdown', () => { 84 | expect( 85 | preset.monorepoTOC({ 86 | meta: emptyReadme, 87 | options: {}, 88 | }) 89 | ).toMatchInlineSnapshot(` 90 | "- [package1](./packages/package1) - first package with an inline package.json description. Quite a long inline description, in fact. 91 | - [package2](./packages/package2) - Readme for package 2 92 | - [package3](./packages/package3) - Readme for package 3 93 | - [package4](./packages/package4) - More details about package 4. Package 4 has a detailed readme, with multiple sections" 94 | `) 95 | }) 96 | 97 | test('generate markdown with filter', () => { 98 | expect( 99 | preset.monorepoTOC({ 100 | meta: emptyReadme, 101 | options: {filter: {'package.name': 'package1|package3'}}, 102 | }) 103 | ).toMatchInlineSnapshot(` 104 | "- [package1](./packages/package1) - first package with an inline package.json description. Quite a long inline description, in fact. 105 | - [package3](./packages/package3) - Readme for package 3" 106 | `) 107 | }) 108 | 109 | test('generate markdown with sorting', () => { 110 | expect( 111 | preset.monorepoTOC({ 112 | meta: emptyReadme, 113 | options: {sort: '-readme.length'}, 114 | }) 115 | ).toMatchInlineSnapshot(` 116 | "- [package4](./packages/package4) - More details about package 4. Package 4 has a detailed readme, with multiple sections 117 | - [package1](./packages/package1) - first package with an inline package.json description. Quite a long inline description, in fact. 118 | - [package2](./packages/package2) - Readme for package 2 119 | - [package3](./packages/package3) - Readme for package 3" 120 | `) 121 | }) 122 | 123 | test('generate markdown default to lerna to find packages', () => { 124 | mockFs['package.json'] = '{}' 125 | expect( 126 | preset.monorepoTOC({ 127 | meta: emptyReadme, 128 | options: {}, 129 | }) 130 | ).toMatchInlineSnapshot(` 131 | "- [package1](./packages/package1) - first package with an inline package.json description. Quite a long inline description, in fact. 132 | - [package2](./packages/package2) - Readme for package 2" 133 | `) 134 | }) 135 | 136 | test('generate markdown fails when no package.json exists', () => { 137 | expect(() => 138 | preset.monorepoTOC({ 139 | meta: {filename: 'subdir/test.md', existingContent: ''}, 140 | options: {}, 141 | }) 142 | ).toThrowError(/ENOENT: no such file or directory, open '.*subdir.*package.json'/) 143 | }) 144 | 145 | test('generate markdown with separate rootDir', () => { 146 | expect( 147 | preset.monorepoTOC({ 148 | meta: {filename: 'subdir/test.md', existingContent: ''}, 149 | options: {repoRoot: '..'}, 150 | }) 151 | ).toMatchInlineSnapshot(` 152 | "- [package1](../packages/package1) - first package with an inline package.json description. Quite a long inline description, in fact. 153 | - [package2](../packages/package2) - Readme for package 2 154 | - [package3](../packages/package3) - Readme for package 3 155 | - [package4](../packages/package4) - More details about package 4. Package 4 has a detailed readme, with multiple sections" 156 | `) 157 | }) 158 | 159 | test('invalid workspaces', () => { 160 | Object.assign(mockFs, { 161 | 'package.json': '{ "workspaces": "package.json - not an array" }', 162 | 'lerna.json': '{ "packages": "lerna.json - not an array" }', 163 | }) 164 | 165 | expect(() => 166 | preset.monorepoTOC({ 167 | meta: emptyReadme, 168 | options: {}, 169 | }) 170 | ).toThrowError(/Expected to find workspaces array, got 'package.json - not an array'/) 171 | }) 172 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/barrel.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as lodash from 'lodash' 3 | import * as glob from 'glob' 4 | import {match} from 'io-ts-extra' 5 | import {parse} from '@babel/parser' 6 | import generate from '@babel/generator' 7 | 8 | import type {Preset} from '.' 9 | 10 | /** 11 | * Bundle several modules into a single convenient one. 12 | * 13 | * @example 14 | * // codegen:start {preset: barrel, include: some/path/*.ts, exclude: some/path/*util.ts} 15 | * export * from './some/path/module-a' 16 | * export * from './some/path/module-b' 17 | * export * from './some/path/module-c' 18 | * // codegen:end 19 | * 20 | * @param include 21 | * [optional] If specified, the barrel will only include file paths that match this glob pattern 22 | * @param exclude 23 | * [optional] If specified, the barrel will exclude file paths that match these glob patterns 24 | * @param import 25 | * [optional] If specified, matching files will be imported and re-exported rather than directly exported 26 | * with `export * from './xyz'`. Use `import: star` for `import * as xyz from './xyz'` style imports. 27 | * Use `import: default` for `import xyz from './xyz'` style imports. 28 | * @param export 29 | * [optional] Only valid if the import style has been specified (either `import: star` or `import: default`). 30 | * If specified, matching modules will be bundled into a const or default export based on this name. If set 31 | * to `{name: someName, keys: path}` the relative file paths will be used as keys. Otherwise the file paths 32 | * will be camel-cased to make them valid js identifiers. 33 | */ 34 | export const barrel: Preset<{ 35 | include?: string 36 | exclude?: string | string[] 37 | import?: 'default' | 'star' 38 | export?: string | {name: string; keys: 'path' | 'camelCase'} 39 | }> = ({meta, options: opts}) => { 40 | const cwd = path.dirname(meta.filename) 41 | 42 | const ext = meta.filename.split('.').slice(-1)[0] 43 | const pattern = opts.include || `*.${ext}` 44 | 45 | const relativeFiles = glob 46 | .sync(pattern, {cwd, ignore: opts.exclude}) 47 | .filter(f => path.resolve(cwd, f) !== path.resolve(meta.filename)) 48 | .map(f => `./${f}`.replace(/(\.\/)+\./g, '.')) 49 | .filter(file => ['.js', '.mjs', '.ts', '.tsx'].includes(path.extname(file))) 50 | .map(f => f.replace(/\.\w+$/, '')) 51 | 52 | const expectedContent = match(opts.import) 53 | .case(undefined, () => { 54 | return relativeFiles.map(f => `export * from '${f}'`).join('\n') 55 | }) 56 | .case(String, s => { 57 | const importPrefix = s === 'default' ? '' : '* as ' 58 | const withIdentifiers = lodash 59 | .chain(relativeFiles) 60 | .map(f => ({ 61 | file: f, 62 | identifier: lodash 63 | .camelCase(f) 64 | .replace(/^([^a-z])/, '_$1') 65 | .replace(/Index$/, ''), 66 | })) 67 | .groupBy(info => info.identifier) 68 | .values() 69 | .flatMap(group => 70 | group.length === 1 ? group : group.map((info, i) => ({...info, identifier: `${info.identifier}_${i + 1}`})) 71 | ) 72 | .value() 73 | 74 | const imports = withIdentifiers.map(i => `import ${importPrefix}${i.identifier} from '${i.file}'`).join('\n') 75 | const exportProps = match(opts.export) 76 | .case({name: String, keys: 'path'}, () => 77 | withIdentifiers.map(i => `${JSON.stringify(i.file)}: ${i.identifier}`) 78 | ) 79 | .default(() => withIdentifiers.map(i => i.identifier)) 80 | .get() 81 | 82 | const exportPrefix = match(opts.export) 83 | .case(undefined, () => 'export') 84 | .case('default', () => 'export default') 85 | .case({name: 'default'}, () => 'export default') 86 | .case(String, name => `export const ${name} =`) 87 | .case({name: String}, ({name}) => `export const ${name} =`) 88 | .get() 89 | 90 | const exports = exportProps.join(',\n ') 91 | 92 | return `${imports}\n\n${exportPrefix} {\n ${exports}\n}\n` 93 | }) 94 | .get() 95 | 96 | // ignore stylistic differences. babel generate deals with most 97 | const normalise = (str: string) => 98 | generate(parse(str, {sourceType: 'module', plugins: ['typescript']}) as any) 99 | .code.replace(/'/g, `"`) 100 | .replace(/\/index/g, '') 101 | 102 | try { 103 | if (normalise(expectedContent) === normalise(meta.existingContent)) { 104 | return meta.existingContent 105 | } 106 | } catch {} 107 | 108 | return expectedContent 109 | } 110 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/custom.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | 4 | import type {Preset} from '.' 5 | 6 | /** 7 | * Define your own codegen function, which will receive all options specified. 8 | * 9 | * Import the `Preset` type from this library to define a strongly-typed preset function: 10 | * 11 | * @example 12 | * import {Preset} from 'eslint-plugin-codegen' 13 | * 14 | * export const jsonPrinter: Preset<{myCustomProp: string}> = ({meta, options}) => { 15 | * return 'filename: ' + meta.filename + '\\ncustom prop: ' + options.myCustomProp 16 | * } 17 | * 18 | * @description 19 | * This can be used with: 20 | * 21 | * `` 14 | * 15 | * @param source {string} relative file path containing the export with jsdoc that should be copied to markdown 16 | * @param export {string} the name of the export 17 | */ 18 | export const markdownFromJsdoc: Preset<{source: string; export?: string}> = ({ 19 | meta, 20 | options: {source: relativeFile, export: exportName}, 21 | }) => { 22 | const targetFile = path.join(path.dirname(meta.filename), relativeFile) 23 | const targetContent = fs.readFileSync(targetFile).toString() 24 | const lines = targetContent.split('\n').map(line => line.trim()) 25 | const exportLineIndex = lines.findIndex(line => line.startsWith(`export const ${exportName}`)) 26 | if (exportLineIndex < 2 || lines[exportLineIndex - 1] !== '*/') { 27 | throw Error(`Couldn't find export in ${relativeFile} with jsdoc called ${exportName}`) 28 | } 29 | const contentUpToExport = lines.slice(0, exportLineIndex).join('\n') 30 | const jsdoc = contentUpToExport 31 | .slice(contentUpToExport.lastIndexOf('/**')) 32 | .split('\n') 33 | .map(line => line.trim()) 34 | .map(line => { 35 | return line 36 | .replace(/^\/\*\*$/, '') // clean up: /** 37 | .replace(/^\* /g, '') // clean up: * blah 38 | .replace(/^\*$/g, '') // clean up: * 39 | .replace(/^\*\/$/, '') // clean up */ 40 | }) 41 | .join(os.EOL) 42 | const sections = `\n@description ${jsdoc}` 43 | .split(/\n@/) 44 | .map(section => section.trim() + ' ') 45 | .filter(Boolean) 46 | .map((section, index) => { 47 | const firstSpace = section.search(/\s/) 48 | return {type: section.slice(0, firstSpace), index, content: section.slice(firstSpace).trim()} 49 | }) 50 | .filter(s => s.content) 51 | 52 | const formatted = sections.map((sec, i, arr) => { 53 | if (sec.type === 'example') { 54 | return ['##### Example', '', '```typescript', sec.content, '```'].join(os.EOL) 55 | } 56 | if (sec.type === 'param') { 57 | const allParams = arr.filter(other => other.type === sec.type) 58 | if (sec !== allParams[0]) { 59 | return null 60 | } 61 | 62 | const rows = allParams.map((p): [string, string] => { 63 | const whitespaceMatch = p.content.match(/\s/) 64 | const firstSpace = whitespaceMatch ? whitespaceMatch.index! : p.content.length 65 | const name = p.content.slice(0, firstSpace) 66 | const description = p.content 67 | .slice(firstSpace + 1) 68 | .trim() 69 | .replace(/\r?\n/g, '
') 70 | return [name, description] 71 | }) 72 | 73 | const headers: [string, string] = ['name', 'description'] 74 | 75 | const nameSize = lodash.max([headers, ...rows].map(r => r[0].length))! 76 | const descSize = lodash.max([headers, ...rows].map(r => r[1].length))! 77 | const pad = (tuple: [string, string], padding = ' ') => 78 | `|${tuple[0].padEnd(nameSize, padding)}|${tuple[1].padEnd(descSize, padding)}|` 79 | 80 | return [ 81 | '##### Params', // breakme 82 | '', 83 | pad(headers), 84 | pad(['', ''], '-'), 85 | ...rows.map(tuple => pad(tuple)), 86 | ].join(os.EOL) 87 | } 88 | if (sec.type === 'description') { 89 | // line breaks that run into letters aren't respected by jsdoc, so shouldn't be in markdown either 90 | return sec.content.replace(/\r?\n\s*([A-Za-z])/g, ' $1') 91 | } 92 | if (sec.type === 'see') { 93 | return null 94 | } 95 | return [`##### ${lodash.startCase(sec.type)}`, sec.content].join(os.EOL + os.EOL) 96 | }) 97 | return [`#### [${exportName}](./${relativeFile}#L${exportLineIndex + 1})`, ...formatted] 98 | .filter(Boolean) 99 | .join(os.EOL + os.EOL) 100 | } 101 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/markdown-from-tests.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import * as os from 'os' 4 | import * as lodash from 'lodash' 5 | import {parse} from '@babel/parser' 6 | import traverse from '@babel/traverse' 7 | 8 | import type {Preset} from '.' 9 | 10 | /** 11 | * Use a test file to generate library usage documentation. 12 | * 13 | * Note: this has been tested with jest. It _might_ also work fine with mocha, and maybe ava, but those haven't been tested. 14 | * 15 | * ##### Example 16 | * 17 | * `` 18 | * 19 | * @param source the jest test file 20 | * @param headerLevel The number of `#` characters to prefix each title with 21 | */ 22 | export const markdownFromTests: Preset<{source: string; headerLevel?: number}> = ({meta, options}) => { 23 | const sourcePath = path.join(path.dirname(meta.filename), options.source) 24 | const sourceCode = fs.readFileSync(sourcePath).toString() 25 | const ast = parse(sourceCode, {sourceType: 'module', plugins: ['typescript']}) 26 | const specs: any[] = [] 27 | traverse(ast, { 28 | CallExpression(ce) { 29 | const identifier: any = lodash.get(ce, 'node') 30 | const isSpec = identifier && ['it', 'test'].includes(lodash.get(identifier, 'callee.name')) 31 | if (!isSpec) { 32 | return 33 | } 34 | const hasArgs = 35 | identifier.arguments.length >= 2 && 36 | identifier.arguments[0].type === 'StringLiteral' && 37 | identifier.arguments[1].body 38 | if (!hasArgs) { 39 | return 40 | } 41 | const func = identifier.arguments[1] 42 | const lines = sourceCode.slice(func.start, func.end).split(/\r?\n/).slice(1, -1) 43 | const indent = lodash.min(lines.filter(Boolean).map(line => line.length - line.trim().length))! 44 | const body = lines.map(line => line.replace(' '.repeat(indent), '')).join(os.EOL) 45 | specs.push({title: identifier.arguments[0].value, code: body}) 46 | }, 47 | }) 48 | return specs 49 | .map(s => { 50 | const lines = [ 51 | `${'#'.repeat(options.headerLevel || 0)} ${s.title}${lodash.get(s, 'suffix', ':')}${os.EOL}`.trimLeft(), 52 | '```typescript', 53 | s.code, 54 | '```', 55 | ] 56 | return lines.join(os.EOL).trim() 57 | }) 58 | .join(os.EOL + os.EOL) 59 | } 60 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/markdown-toc.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as os from 'os' 3 | import * as lodash from 'lodash' 4 | 5 | import type {Preset} from '.' 6 | 7 | /** 8 | * Generate a table of contents from the current markdown file, based on markdown headers (e.g. `### My section title`) 9 | * 10 | * ##### Example 11 | * 12 | * `` 13 | * 14 | * @param minDepth exclude headers with lower "depth". e.g. if set to 2, `# H1` would be excluded but `## H2` would be included. 15 | * @param maxDepth exclude headers with higher "depth". e.g. if set to 3, `#### H4` would be excluded but `### H3` would be included. 16 | */ 17 | export const markdownTOC: Preset<{minDepth?: number; maxDepth?: number}> = ({meta, options}) => { 18 | const lines = fs 19 | .readFileSync(meta.filename) 20 | .toString() 21 | .split('\n') 22 | .map(line => line.trim()) 23 | const headings = lines 24 | .filter(line => line.match(/^#+ /)) 25 | .filter(line => line.startsWith('#'.repeat(options.minDepth || 1))) 26 | .filter(line => line.split(' ')[0].length < (options.maxDepth || Infinity)) 27 | const minHashes = lodash.min(headings.map(h => h.split(' ')[0].length))! 28 | return headings 29 | .map(h => { 30 | const hashes = h.split(' ')[0] 31 | const indent = ' '.repeat(3 * (hashes.length - minHashes)) 32 | const text = h 33 | .slice(hashes.length + 1) 34 | .replace(/]\(.*\)/g, '') 35 | .replace(/[[\]]/g, '') 36 | const href = text 37 | .toLowerCase() 38 | .replace(/\s/g, '-') 39 | .replace(/[^\w-]/g, '') 40 | return {indent, text, href} 41 | }) 42 | .map(({indent, text, href}, i, arr) => { 43 | const previousDupes = arr.filter((x, j) => x.href === href && j < i) 44 | const fixedHref = previousDupes.length === 0 ? href : `${href}-${previousDupes.length}` 45 | return `${indent}- [${text}](#${fixedHref})` 46 | }) 47 | .join(os.EOL) 48 | } 49 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/monorepo-toc.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import * as os from 'os' 4 | import * as lodash from 'lodash' 5 | 6 | import type {Preset} from '.' 7 | import {getLeafPackages} from './util/monorepo' 8 | import {relative} from './util/path' 9 | 10 | /** 11 | * Generate a table of contents for a monorepo. 12 | * 13 | * ##### Example (basic) 14 | * 15 | * `` 16 | * 17 | * ##### Example (using config options) 18 | * 19 | * `` 20 | * 21 | * @param repoRoot 22 | * [optional] the relative path to the root of the git repository. By default, searches parent directories for a package.json to find the "root". 23 | * @param filter 24 | * [optional] a dictionary of filter rules to whitelist packages. Filters can be applied based on package.json keys, 25 | * e.g. `filter: { package.name: someRegex, path: some/relative/path }` 26 | * @param sort 27 | * [optional] sort based on package properties (see `filter`), or readme length. Use `-` as a prefix to sort descending. 28 | * e.g. `sort: -readme.length` 29 | */ 30 | export const monorepoTOC: Preset<{ 31 | repoRoot?: string 32 | filter?: string | Record 33 | sort?: string 34 | }> = ({meta, options}) => { 35 | const packages = getLeafPackages(options.repoRoot, meta.filename) 36 | 37 | const leafPackages = packages 38 | .map(({path: leafPath, packageJson: leafPkg}) => { 39 | const dirname = path.dirname(leafPath) 40 | const readmePath = [path.join(dirname, 'readme.md'), path.join(dirname, 'README.md')].find(p => fs.existsSync(p)) 41 | const readme = [readmePath && fs.readFileSync(readmePath).toString(), leafPkg.description] 42 | .filter(Boolean) 43 | .join(os.EOL + os.EOL) 44 | return {package: leafPkg, leafPath, readme} 45 | }) 46 | .filter(props => { 47 | const filter = typeof options.filter === 'object' ? options.filter : {'package.name': options.filter!} 48 | return Object.keys(filter) 49 | .filter(key => typeof filter[key] === 'string') 50 | .every(key => new RegExp(lodash.get(filter, key)).test(lodash.get(props, key))) 51 | }) 52 | .sort((...args) => { 53 | const sort = options.sort || 'package.name' 54 | const multiplier = sort.startsWith('-') ? -1 : 1 55 | const key = sort.replace(/^-/, '') 56 | const [a, b] = args.map(arg => lodash.get(arg, key)) 57 | const comp = a < b ? -1 : a > b ? 1 : 0 58 | return comp * multiplier 59 | }) 60 | .map(props => ({leafPath: props.leafPath, leafPkg: props.package, readme: props.readme})) 61 | .map(({leafPath, leafPkg, readme}) => { 62 | const description = (() => { 63 | return readme 64 | .split('\n') 65 | .map(line => line.trim()) 66 | .filter(Boolean) 67 | .find(line => line.match(/^[A-Za-z]/)) 68 | })() 69 | const name = leafPkg.name 70 | const homepage = 71 | leafPkg.homepage || relative(path.dirname(meta.filename), leafPath).replace(/\/package.json$/, '') 72 | return [`- [${name}](${homepage})`, description].filter(Boolean).join(' - ').trim() 73 | }) 74 | 75 | return leafPackages.join(os.EOL) 76 | } 77 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/util/monorepo.ts: -------------------------------------------------------------------------------- 1 | import * as readPkgUp from 'read-pkg-up' 2 | import {match} from 'io-ts-extra' 3 | import * as path from 'path' 4 | import * as glob from 'glob' 5 | import * as lodash from 'lodash' 6 | import * as fs from 'fs' 7 | import {inspect} from 'util' 8 | 9 | export interface PackageGlobbable { 10 | repoRoot?: string 11 | } 12 | 13 | export const getLeafPackages = (repoRoot: string | undefined, filename: string) => { 14 | const contextDir = match(repoRoot) 15 | .case(String, s => path.join(path.dirname(filename), s)) 16 | .default(() => path.dirname(readPkgUp.sync({cwd: path.dirname(filename)})!.path)) 17 | .get() 18 | 19 | const readJsonFile = (f: string) => JSON.parse(fs.readFileSync(path.join(contextDir, f)).toString()) 20 | const parseLernaJson = () => readJsonFile('lerna.json').packages 21 | const pkg = readJsonFile('package.json') 22 | const packageGlobs = pkg.workspaces?.packages || pkg.workspaces || parseLernaJson() 23 | 24 | if (!Array.isArray(packageGlobs)) { 25 | throw Error(`Expected to find workspaces array, got ${inspect(packageGlobs)}`) 26 | } 27 | 28 | const packages = lodash 29 | .flatMap(packageGlobs, pattern => glob.sync(`${pattern}/package.json`, {cwd: contextDir})) 30 | .map(p => ({path: p, packageJson: readJsonFile(p)})) 31 | return lodash.compact(packages) 32 | } 33 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/src/presets/util/path.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | /** replace backslashes with forward slashes */ 4 | export const unixify = (filepath: string) => filepath.replace(/\\/g, '/') 5 | 6 | /** get a relative unix-style path between two existing paths */ 7 | export const relative = (from: string, to: string) => `./${unixify(path.relative(from, to))}`.replace(/^\.\/\./, '.') 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codegen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@mmkal/rig/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "tsBuildInfoFile": "dist/buildinfo.json", 7 | "typeRoots": [ 8 | "node_modules/@mmkal/rig/node_modules/@types" 9 | ] 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "dist" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/expect-type/readme.md: -------------------------------------------------------------------------------- 1 | # expect-type 2 | 3 | Moved to https://github.com/mmkal/expect-type -------------------------------------------------------------------------------- /packages/io-ts-extra/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@mmkal/rig/.eslintrc') 2 | -------------------------------------------------------------------------------- /packages/io-ts-extra/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/__tests__ 3 | dist/buildinfo.json 4 | .eslintcache 5 | .eslintrc.js 6 | .rush 7 | .heft 8 | *.log 9 | tsconfig.json 10 | config/jest.config.json 11 | jest.config.js 12 | coverage 13 | -------------------------------------------------------------------------------- /packages/io-ts-extra/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "io-ts-extra", 3 | "entries": [ 4 | { 5 | "version": "0.11.6", 6 | "tag": "io-ts-extra_v0.11.6", 7 | "date": "Sat, 16 Oct 2021 13:17:24 GMT", 8 | "comments": { 9 | "patch": [ 10 | { 11 | "comment": "fix: make strict combinator check for missing values (#240) - @jpdenford" 12 | } 13 | ], 14 | "dependency": [ 15 | { 16 | "comment": "Updating dependency \"expect-type\" to `0.13.0`" 17 | } 18 | ] 19 | } 20 | }, 21 | { 22 | "version": "0.11.4", 23 | "tag": "io-ts-extra_v0.11.4", 24 | "date": "Tue, 29 Jun 2021 08:48:24 GMT", 25 | "comments": { 26 | "patch": [ 27 | { 28 | "comment": "chore(deps): update devdependencies (#203) - @renovate[bot]" 29 | }, 30 | { 31 | "comment": "fix(deps): update dependency lodash to v4.17.21 [security] (#236) - @renovate[bot]" 32 | } 33 | ], 34 | "dependency": [ 35 | { 36 | "comment": "Updating dependency \"expect-type\" to `0.11.0`" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "version": "0.11.3", 43 | "tag": "io-ts-extra_v0.11.3", 44 | "date": "Thu, 03 Dec 2020 19:10:22 GMT", 45 | "comments": { 46 | "dependency": [ 47 | { 48 | "comment": "Updating dependency \"expect-type\" to `0.10.0`" 49 | } 50 | ] 51 | } 52 | }, 53 | { 54 | "version": "0.11.2", 55 | "tag": "io-ts-extra_v0.11.2", 56 | "date": "Sat, 28 Nov 2020 19:10:00 GMT", 57 | "comments": { 58 | "dependency": [ 59 | { 60 | "comment": "Updating dependency \"expect-type\" to `0.9.2`" 61 | } 62 | ] 63 | } 64 | }, 65 | { 66 | "version": "0.11.1", 67 | "tag": "io-ts-extra_v0.11.1", 68 | "date": "Thu, 26 Nov 2020 17:06:36 GMT", 69 | "comments": { 70 | "dependency": [ 71 | { 72 | "comment": "Updating dependency \"expect-type\" from `0.9.0` to `0.9.1`" 73 | } 74 | ] 75 | } 76 | }, 77 | { 78 | "version": "0.11.0", 79 | "tag": "io-ts-extra_v0.11.0", 80 | "date": "Tue, 27 Oct 2020 16:18:39 GMT", 81 | "comments": { 82 | "minor": [ 83 | { 84 | "comment": "Permalink readme urls before publishing (#211)" 85 | } 86 | ], 87 | "dependency": [ 88 | { 89 | "comment": "Updating dependency \"expect-type\" from `0.8.0` to `0.9.0`" 90 | } 91 | ] 92 | } 93 | }, 94 | { 95 | "version": "0.10.10", 96 | "tag": "io-ts-extra_v0.10.10", 97 | "date": "Mon, 12 Oct 2020 12:24:41 GMT", 98 | "comments": { 99 | "patch": [ 100 | { 101 | "comment": "chore(deps): pin dependencies (#173) - @renovate[bot]" 102 | } 103 | ] 104 | } 105 | }, 106 | { 107 | "version": "0.10.9", 108 | "tag": "io-ts-extra_v0.10.9", 109 | "date": "Mon, 05 Oct 2020 22:38:33 GMT", 110 | "comments": { 111 | "dependency": [ 112 | { 113 | "comment": "Updating dependency \"expect-type\" from `0.7.11` to `0.8.0`" 114 | } 115 | ] 116 | } 117 | }, 118 | { 119 | "version": "0.10.8", 120 | "tag": "io-ts-extra_v0.10.8", 121 | "date": "Thu, 01 Oct 2020 14:48:13 GMT", 122 | "comments": { 123 | "patch": [ 124 | { 125 | "comment": "chore: npmignore coverage folder (#184)" 126 | } 127 | ], 128 | "dependency": [ 129 | { 130 | "comment": "Updating dependency \"expect-type\" from `0.7.10` to `0.7.11`" 131 | } 132 | ] 133 | } 134 | }, 135 | { 136 | "version": "0.10.7", 137 | "tag": "io-ts-extra_v0.10.7", 138 | "date": "Fri, 18 Sep 2020 16:56:41 GMT", 139 | "comments": { 140 | "patch": [ 141 | { 142 | "comment": "fix(npmignore): exclude rush files (#169)" 143 | } 144 | ], 145 | "dependency": [ 146 | { 147 | "comment": "Updating dependency \"expect-type\" from `0.7.9` to `0.7.10`" 148 | } 149 | ] 150 | } 151 | }, 152 | { 153 | "version": "0.10.6", 154 | "tag": "io-ts-extra_v0.10.6", 155 | "date": "Thu, 17 Sep 2020 10:47:43 GMT", 156 | "comments": { 157 | "patch": [ 158 | { 159 | "comment": "chore: enable consistent versions (#163)" 160 | } 161 | ] 162 | } 163 | }, 164 | { 165 | "version": "0.10.5", 166 | "tag": "io-ts-extra_v0.10.5", 167 | "date": "Wed, 16 Sep 2020 21:06:32 GMT", 168 | "comments": { 169 | "patch": [ 170 | { 171 | "comment": "chore: bump io-ts version (#161)" 172 | } 173 | ] 174 | } 175 | } 176 | ] 177 | } 178 | -------------------------------------------------------------------------------- /packages/io-ts-extra/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - io-ts-extra 2 | 3 | This log was last generated on Sat, 16 Oct 2021 13:17:24 GMT and should not be manually modified. 4 | 5 | ## 0.11.6 6 | Sat, 16 Oct 2021 13:17:24 GMT 7 | 8 | ### Patches 9 | 10 | - fix: make strict combinator check for missing values (#240) - @jpdenford 11 | 12 | ## 0.11.4 13 | Tue, 29 Jun 2021 08:48:24 GMT 14 | 15 | ### Patches 16 | 17 | - chore(deps): update devdependencies (#203) - @renovate[bot] 18 | - fix(deps): update dependency lodash to v4.17.21 [security] (#236) - @renovate[bot] 19 | 20 | ## 0.11.3 21 | Thu, 03 Dec 2020 19:10:22 GMT 22 | 23 | _Version update only_ 24 | 25 | ## 0.11.2 26 | Sat, 28 Nov 2020 19:10:00 GMT 27 | 28 | _Version update only_ 29 | 30 | ## 0.11.1 31 | Thu, 26 Nov 2020 17:06:36 GMT 32 | 33 | _Version update only_ 34 | 35 | ## 0.11.0 36 | Tue, 27 Oct 2020 16:18:39 GMT 37 | 38 | ### Minor changes 39 | 40 | - Permalink readme urls before publishing (#211) 41 | 42 | ## 0.10.10 43 | Mon, 12 Oct 2020 12:24:41 GMT 44 | 45 | ### Patches 46 | 47 | - chore(deps): pin dependencies (#173) - @renovate[bot] 48 | 49 | ## 0.10.9 50 | Mon, 05 Oct 2020 22:38:33 GMT 51 | 52 | _Version update only_ 53 | 54 | ## 0.10.8 55 | Thu, 01 Oct 2020 14:48:13 GMT 56 | 57 | ### Patches 58 | 59 | - chore: npmignore coverage folder (#184) 60 | 61 | ## 0.10.7 62 | Fri, 18 Sep 2020 16:56:41 GMT 63 | 64 | ### Patches 65 | 66 | - fix(npmignore): exclude rush files (#169) 67 | 68 | ## 0.10.6 69 | Thu, 17 Sep 2020 10:47:43 GMT 70 | 71 | ### Patches 72 | 73 | - chore: enable consistent versions (#163) 74 | 75 | ## 0.10.5 76 | Wed, 16 Sep 2020 21:06:32 GMT 77 | 78 | ### Patches 79 | 80 | - chore: bump io-ts version (#161) 81 | 82 | -------------------------------------------------------------------------------- /packages/io-ts-extra/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@mmkal/rig/jest.config') 2 | -------------------------------------------------------------------------------- /packages/io-ts-extra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "io-ts-extra", 3 | "version": "0.11.6", 4 | "keywords": [ 5 | "typescript", 6 | "validation", 7 | "inference", 8 | "codecs", 9 | "types", 10 | "runtime", 11 | "io-ts", 12 | "pattern matching" 13 | ], 14 | "homepage": "https://github.com/mmkal/ts/tree/main/packages/io-ts-extra#readme", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/mmkal/ts.git", 18 | "directory": "packages/io-ts-extra" 19 | }, 20 | "license": "Apache-2.0", 21 | "main": "dist/index.js", 22 | "types": "dist/index.d.ts", 23 | "scripts": { 24 | "prebuild": "npm run clean", 25 | "build": "rig tsc -p .", 26 | "clean": "rig rimraf dist", 27 | "lint": "rig eslint --cache .", 28 | "prepack": "rig permalink", 29 | "postpack": "rig unpermalink", 30 | "test": "rig jest" 31 | }, 32 | "dependencies": { 33 | "fp-ts": "^2.1.0", 34 | "io-ts": "^2.2.4" 35 | }, 36 | "devDependencies": { 37 | "@mmkal/rig": "workspace:*", 38 | "@types/js-yaml": "3.12.5", 39 | "@types/lodash": "4.14.165", 40 | "expect-type2": "npm:expect-type@0.14.0", 41 | "js-yaml": "^3.14.0", 42 | "lodash": "^4.17.15" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/__tests__/combinators.test.ts: -------------------------------------------------------------------------------- 1 | import {sparseType, optional} from '..' 2 | import * as t from 'io-ts' 3 | import {expectRight, expectLeft} from './either-serializer' 4 | import {expectTypeOf} from 'expect-type2' 5 | import {inspect} from 'util' 6 | import {instanceOf, regexp, strict} from '../combinators' 7 | import {validationErrors} from '../reporters' 8 | import {mapValues} from 'lodash' 9 | 10 | describe('sparseType', () => { 11 | it('handles mixed props', () => { 12 | const Person = sparseType({name: t.string, age: optional(t.number)}) 13 | 14 | expectTypeOf(Person.props).toMatchTypeOf({ 15 | name: t.string, 16 | age: optional(t.number), 17 | }) 18 | expect(inspect(Person.props)).toEqual( 19 | inspect({ 20 | name: t.string, 21 | age: optional(t.number), 22 | }) 23 | ) 24 | 25 | expectTypeOf(Person._A).toEqualTypeOf<{name: string; age?: number | null | undefined}>() 26 | expectTypeOf(Person._A).not.toMatchTypeOf<{name: string; age: number | null | undefined}>() 27 | 28 | expectTypeOf({name: 'bob'}).toMatchTypeOf(Person._A) 29 | expectTypeOf({name: 'bob', age: 30}).toMatchTypeOf(Person._A) 30 | expectTypeOf({name: 'bob', age: 'thirty'}).not.toMatchTypeOf(Person._A) 31 | 32 | expectLeft(Person.decode({name: 'bob', age: 'thirty'})) 33 | expectRight(Person.decode({name: 'bob', age: 30})) 34 | expectRight(Person.decode({name: 'bob'})) 35 | expectLeft(Person.decode({})) 36 | }) 37 | 38 | it('handles all required props', () => { 39 | const Person = sparseType({name: t.string, age: t.number}) 40 | 41 | expectTypeOf(Person._A).toEqualTypeOf<{name: string; age: number}>() 42 | 43 | expectLeft(Person.decode({name: 'bob', age: 'thirty'})) 44 | expectRight(Person.decode({name: 'bob', age: 30})) 45 | expectLeft(Person.decode({name: 'bob'})) 46 | expectLeft(Person.decode({})) 47 | }) 48 | 49 | it('handles all optional props', () => { 50 | const Person = sparseType({name: optional(t.string), age: optional(t.number)}) 51 | expectLeft(Person.decode({name: 'bob', age: 'thirty'})) 52 | expectRight(Person.decode({name: 'bob', age: 30})) 53 | expectRight(Person.decode({name: 'bob'})) 54 | expectRight(Person.decode({})) 55 | }) 56 | }) 57 | 58 | test('instanceOf', () => { 59 | const DateType = instanceOf(Date) 60 | expectRight(DateType.decode(new Date())) 61 | expectLeft(DateType.decode('not a date')) 62 | 63 | expect(DateType.is(new Date())).toBe(true) 64 | expect(DateType.is('not a date')).toBe(false) 65 | }) 66 | 67 | test('instanceOf names anonymous functions appropriately', () => { 68 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 69 | class Foo {} 70 | Object.defineProperty(Foo, 'name', {value: null}) 71 | expect(instanceOf(Foo).name).toEqual('InstanceOf') 72 | }) 73 | 74 | test('regex', () => { 75 | const AllCaps = regexp(/^[A-Z]*$/) 76 | expectRight(AllCaps.decode('HELLO')) 77 | expectLeft(AllCaps.decode('hello')) 78 | expectLeft(AllCaps.decode(123)) 79 | }) 80 | 81 | test('regex can encode and decode', () => { 82 | const s = 'foo bar baz' 83 | const R = regexp(/b(a)(r)/) 84 | 85 | const success = R.decode(s) 86 | expectRight(success).toEqual({ 87 | _tag: 'Right', 88 | right: Object.assign(['bar', 'a', 'r'], {index: 4, input: s}), 89 | }) 90 | 91 | if (success._tag === 'Right') { 92 | expectTypeOf(success.right).toEqualTypeOf(Object.assign(['bar', 'a', 'r'], {index: 4, input: s})) 93 | // eslint-disable-next-line jest/no-conditional-expect 94 | expect(R.encode(success.right)).toEqual(s) 95 | } 96 | 97 | expectLeft(R.decode('abc')) 98 | expectLeft(R.decode(null)) 99 | }) 100 | 101 | test('strict', () => { 102 | const Person = strict({name: t.string, age: t.number}) 103 | 104 | expectRight(Person.decode({name: 'Alice', age: 30})) 105 | expectLeft(Person.decode({name: 'Alice'})) 106 | expectLeft(Person.decode({name: 'Bob', age: 30, unexpectedProp: 'abc'})) 107 | expectRight(Person.decode({name: 'Bob', age: 30, unexpectedProp: undefined})) 108 | 109 | expect(Person.is({name: 'Alice', age: 30})).toBe(true) 110 | expect(Person.is({name: 'Bob', age: 30, unexpectedProp: 'abc'})).toBe(false) 111 | expect(Person.is({name: 'Bob', age: 30, unexpectedProp: undefined})).toBe(false) 112 | 113 | const errorCases = { 114 | null: null, 115 | undefined: undefined, 116 | withExtraProp: {name: 'Bob', age: 30, unexpectedProp: 'abc'}, 117 | withInvalidAndExtraProp: {name: 123, age: 30, unexpectedProp: 'abc'}, 118 | } 119 | expect( 120 | mapValues(errorCases, val => { 121 | const decoded = Person.decode(val) 122 | expectLeft(decoded) 123 | return validationErrors(decoded) 124 | }) 125 | ).toMatchInlineSnapshot(` 126 | Object { 127 | "null": Array [ 128 | "Invalid value {null} supplied to Strict<{ name: string, age: number }. Expected Strict<{ name: string, age: number }.", 129 | ], 130 | "undefined": Array [ 131 | "Invalid value {undefined} supplied to Strict<{ name: string, age: number }. Expected Strict<{ name: string, age: number }.", 132 | ], 133 | "withExtraProp": Array [ 134 | "Invalid value {'abc'} supplied to Strict<{ name: string, age: number }.unexpectedProp. Expected undefined.", 135 | ], 136 | "withInvalidAndExtraProp": Array [ 137 | "Invalid value {123} supplied to Strict<{ name: string, age: number }.name. Expected string.", 138 | "Invalid value {'abc'} supplied to Strict<{ name: string, age: number }.unexpectedProp. Expected undefined.", 139 | ], 140 | } 141 | `) 142 | }) 143 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/__tests__/either-serializer.ts: -------------------------------------------------------------------------------- 1 | import * as jsYaml from 'js-yaml' 2 | 3 | expect.addSnapshotSerializer({ 4 | test: val => val && (val._tag === 'Right' || val._tag === 'Left'), 5 | print: val => jsYaml.safeDump(val, {skipInvalid: true}).trim(), 6 | }) 7 | 8 | export const expectLeft = (val: any) => { 9 | expect(val).toMatchObject({_tag: 'Left', left: expect.anything()}) 10 | return expect(val) 11 | } 12 | export const expectRight = (val: any) => { 13 | expect(val).not.toMatchObject({_tag: 'Left', left: expect.anything()}) 14 | expect(val).toMatchObject({_tag: 'Right', right: expect.anything()}) 15 | return expect(val) 16 | } 17 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/__tests__/mapper.test.ts: -------------------------------------------------------------------------------- 1 | import {right} from 'fp-ts/lib/Either' 2 | import {RichError} from '../util' 3 | import * as t from 'io-ts' 4 | import {mapper, parser} from '../mapper' 5 | import {expectTypeOf} from 'expect-type2' 6 | import './either-serializer' 7 | import {instanceOf} from '../combinators' 8 | 9 | describe('mapper', () => { 10 | it('maps', () => { 11 | const BoolToStringArray = mapper(t.boolean, t.array(t.string), b => b.toString().split('')) 12 | expectTypeOf(BoolToStringArray._O).toEqualTypeOf(true) 13 | expectTypeOf(BoolToStringArray._A).toEqualTypeOf(['t', 'r', 'u', 'e']) 14 | expect(BoolToStringArray.decode(true)).toMatchInlineSnapshot(` 15 | _tag: Right 16 | right: 17 | - t 18 | - r 19 | - u 20 | - e 21 | `) 22 | expect(BoolToStringArray.decode(['input/output confusion'])).toMatchInlineSnapshot(` 23 | _tag: Left 24 | left: 25 | - value: &ref_0 26 | - input/output confusion 27 | context: 28 | - key: '' 29 | type: 30 | name: boolean |> b => b.toString().split('') |> Array 31 | from: 32 | name: boolean 33 | _tag: BooleanType 34 | to: 35 | name: Array 36 | type: 37 | name: string 38 | _tag: StringType 39 | _tag: ArrayType 40 | actual: *ref_0 41 | `) 42 | }) 43 | 44 | it('can unmap', () => { 45 | const BoolToStringArray = mapper( 46 | t.boolean, 47 | t.array(t.string), 48 | b => b.toString().split(''), 49 | arr => JSON.parse(arr.join('')) 50 | ) 51 | expect(BoolToStringArray.encode(['f', 'a', 'l', 's', 'e'])).toEqual(false) 52 | }) 53 | 54 | it('throws helpfully when unmap not implemented', () => { 55 | const BoolToStringArray = mapper(t.boolean, t.array(t.string), b => b.toString().split('')) 56 | expect(() => (BoolToStringArray as any).encode(['f', 'a', 'l', 's', 'e'])).toThrowErrorMatchingInlineSnapshot(` 57 | "{ 58 | \\"context\\": \\"unmapper/encoder not implemented\\", 59 | \\"details\\": [ 60 | \\"f\\", 61 | \\"a\\", 62 | \\"l\\", 63 | \\"s\\", 64 | \\"e\\" 65 | ] 66 | }" 67 | `) 68 | }) 69 | }) 70 | 71 | describe('parser', () => { 72 | it('parses', () => { 73 | const IntFromString = parser(t.Int, parseFloat) 74 | expectTypeOf(IntFromString._A).toMatchTypeOf() 75 | expectTypeOf(IntFromString._A).toEqualTypeOf>() 76 | expectTypeOf(IntFromString._O).toBeString() 77 | expect(IntFromString.decode('123')).toMatchInlineSnapshot(` 78 | _tag: Right 79 | right: 123 80 | `) 81 | expect(IntFromString.decode('123.1')).toMatchInlineSnapshot(` 82 | _tag: Left 83 | left: 84 | - value: 123.1 85 | context: 86 | - key: '' 87 | type: 88 | name: string |> parseFloat |> Int 89 | from: 90 | name: string 91 | _tag: StringType 92 | to: 93 | name: Int 94 | type: 95 | name: number 96 | _tag: NumberType 97 | _tag: RefinementType 98 | actual: '123.1' 99 | `) 100 | expect(IntFromString.decode('xyz')).toMatchInlineSnapshot(` 101 | _tag: Left 102 | left: 103 | - value: .nan 104 | context: 105 | - key: '' 106 | type: 107 | name: string |> parseFloat |> Int 108 | from: 109 | name: string 110 | _tag: StringType 111 | to: 112 | name: Int 113 | type: 114 | name: number 115 | _tag: NumberType 116 | _tag: RefinementType 117 | actual: xyz 118 | `) 119 | }) 120 | 121 | it('parses dates', () => { 122 | const ValidDate = t.refinement(instanceOf(Date), d => !Number.isNaN(d.getTime())) 123 | const DateFromString = parser( 124 | ValidDate, 125 | s => new Date(s), 126 | d => d.toISOString() 127 | ) 128 | expect(DateFromString.decode('2000')).toMatchInlineSnapshot(` 129 | _tag: Right 130 | right: 2000-01-01T00:00:00.000Z 131 | `) 132 | expect(DateFromString.decode('not a date')).toMatchObject({_tag: 'Left'}) 133 | expect(DateFromString.decode(null as any)).toMatchObject({_tag: 'Left'}) 134 | expect(DateFromString.decode(123 as any)).toMatchObject({_tag: 'Left'}) 135 | 136 | expect(DateFromString.encode(new Date('2001'))).toMatchInlineSnapshot(`"2001-01-01T00:00:00.000Z"`) 137 | }) 138 | 139 | it('catches failures', () => { 140 | const StringFromBoolWithDecoderBug = mapper(t.boolean, t.string, b => (b ? RichError.throw({b}) : 'nope')) 141 | expect(StringFromBoolWithDecoderBug.decode(false)).toEqual(right('nope')) 142 | expect(StringFromBoolWithDecoderBug.decode(true)).toMatchInlineSnapshot(` 143 | _tag: Left 144 | left: 145 | - value: true 146 | context: 147 | - key: '' 148 | type: 149 | name: >- 150 | boolean |> b => (b ? util_1.RichError.throw({ b }) : 'nope') |> 151 | string 152 | from: 153 | name: boolean 154 | _tag: BooleanType 155 | to: &ref_0 156 | name: string 157 | _tag: StringType 158 | actual: true 159 | - key: >- 160 | decoder [b => (b ? util_1.RichError.throw({ b }) : 'nope')]: error 161 | thrown decoding: [Error: { 162 | "b": true 163 | }] 164 | type: *ref_0 165 | `) 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/__tests__/narrow.test.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts' 2 | import {narrow} from '../narrow' 3 | import {expectRight, expectLeft} from './either-serializer' 4 | import {expectTypeOf} from 'expect-type2' 5 | import {validationErrors} from '../reporters' 6 | 7 | it('refines', () => { 8 | const Person = t.type({name: t.string}) 9 | const Family = t.type({ 10 | members: t.record( 11 | t.string, 12 | narrow(Person, (val, ctx) => ctx.length === 0 || val.name === ctx[ctx.length - 1].key) 13 | ), 14 | }) 15 | expectTypeOf(Family._A).toEqualTypeOf<{members: Record}>() 16 | 17 | expectRight(Family.decode({members: {bob: {name: 'bob'}}})) 18 | expectLeft(Family.decode({members: {bob: {name: 'bib'}}})) 19 | 20 | expect(Family.is({members: {bob: {name: 'bob'}}})).toBe(true) 21 | 22 | // note! only decode passes context to the predicate, so .is can have unexpected behaviour: 23 | expect(Family.is({members: {bob: {name: 'bib'}}})).toBe(true) 24 | }) 25 | 26 | it('refines primitives', () => { 27 | const Family = t.type({ 28 | members: t.record( 29 | t.string, 30 | t.type({ 31 | name: narrow(t.string, (val, ctx) => { 32 | return ctx.length <= 2 || val === ctx[ctx.length - 2].key 33 | }), 34 | }) 35 | ), 36 | }) 37 | 38 | expectRight(Family.decode({members: {bob: {name: 'bob'}}})) 39 | expectLeft(Family.decode({members: {bob: {name: 'bib'}}})) 40 | }) 41 | 42 | it('can refine with another codec', () => { 43 | const CloudResources = narrow( 44 | t.type({ 45 | database: t.type({username: t.string, password: t.string}), 46 | service: t.type({dbConnectionString: t.string}), 47 | }), 48 | ({database}) => { 49 | expectTypeOf(database).toEqualTypeOf<{username: string; password: string}>() 50 | return t.type({ 51 | service: t.type({dbConnectionString: t.literal(`${database.username}:${database.password}`)}), 52 | }) 53 | } 54 | ) 55 | 56 | expectRight( 57 | CloudResources.decode({ 58 | database: {username: 'user', password: 'pass'}, 59 | service: {dbConnectionString: 'user:pass'}, 60 | } as typeof CloudResources._A) 61 | ) 62 | 63 | expect( 64 | CloudResources.is({ 65 | database: {username: 'user', password: 'pass'}, 66 | service: {dbConnectionString: 'user:pass'}, 67 | } as typeof CloudResources._A) 68 | ).toBe(true) 69 | 70 | const badResources = { 71 | database: {username: 'user'}, // missing password 72 | service: {dbConnectionString: 'user:pass'}, 73 | } 74 | const badResourcesValidation = CloudResources.decode(badResources) 75 | expectLeft(badResourcesValidation) 76 | expect(CloudResources.is(badResources)).toBe(false) 77 | expect(validationErrors(badResourcesValidation, 'CloudResources')).toMatchInlineSnapshot(` 78 | Array [ 79 | "Invalid value {undefined} supplied to CloudResources.database.password. Expected string.", 80 | ] 81 | `) 82 | 83 | const invalidConnectionString = CloudResources.decode({ 84 | database: {username: 'user', password: 'pass'}, 85 | service: {dbConnectionString: 'user:typo'}, 86 | } as typeof CloudResources._A) 87 | expectLeft(invalidConnectionString) 88 | expect(validationErrors(invalidConnectionString, 'CloudResources')).toMatchInlineSnapshot(` 89 | Array [ 90 | "Invalid value {'user:typo'} supplied to CloudResources.service.dbConnectionString. Expected \\"user:pass\\".", 91 | ] 92 | `) 93 | }) 94 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/__tests__/reporters.test.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts' 2 | import {validationErrors, getRightUnsafe} from '../reporters' 3 | 4 | test('validationErrors', () => { 5 | const Person = t.type({name: t.string, age: t.number}) 6 | expect(validationErrors(Person.decode({foo: 'bar'}), 'Person')).toMatchInlineSnapshot(` 7 | Array [ 8 | "Invalid value {undefined} supplied to Person.name. Expected string.", 9 | "Invalid value {undefined} supplied to Person.age. Expected number.", 10 | ] 11 | `) 12 | expect(validationErrors(Person.decode({name: 'Bob', age: 90}), 'Person')).toMatchInlineSnapshot(` 13 | Array [ 14 | "No errors!", 15 | ] 16 | `) 17 | }) 18 | 19 | test('no type alias', () => { 20 | expect(validationErrors(t.string.decode(123))).toMatchInlineSnapshot(` 21 | Array [ 22 | "Invalid value {123} supplied to string. Expected string.", 23 | ] 24 | `) 25 | }) 26 | 27 | test('getRightUnsafe', () => { 28 | const Person = t.type({name: t.string, age: t.number}) 29 | expect(() => getRightUnsafe(Person.decode({foo: 'bar'}), 'Person')).toThrowErrorMatchingInlineSnapshot(` 30 | "Invalid value {undefined} supplied to Person.name. Expected string. 31 | Invalid value {undefined} supplied to Person.age. Expected number." 32 | `) 33 | expect(getRightUnsafe(Person.decode({name: 'Bob', age: 90}), 'Person')).toMatchInlineSnapshot(` 34 | Object { 35 | "age": 90, 36 | "name": "Bob", 37 | } 38 | `) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/__tests__/shorthand.test.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts' 2 | import {codecFromShorthand as shorthand} from '../shorthand' 3 | import {expectTypeOf as e} from 'expect-type2' 4 | 5 | const expectTypeRuntimeBehaviour = (inverted = false): typeof e => (actual?: any): any => { 6 | if (typeof actual === 'undefined') { 7 | return e(actual) 8 | } 9 | // eslint-disable-next-line jest/valid-expect 10 | const jestExpect = (inverted ? (...args) => expect(...args).not : expect) as typeof expect 11 | const json = (obj: unknown) => JSON.stringify(obj, null, 2) 12 | const assertions = { 13 | ...e, 14 | toEqualTypeOf: (...other: any[]) => { 15 | if (other.length === 0) { 16 | return 17 | } 18 | jestExpect(json(actual)).toEqual(json(other[0])) 19 | }, 20 | toMatchTypeOf: (...other: any[]) => { 21 | if (other.length === 0) { 22 | return 23 | } 24 | jestExpect(json(actual)).toMatchObject(json(other[0])) 25 | }, 26 | toHaveProperty: (prop: string) => { 27 | jestExpect(actual).toHaveProperty(prop) 28 | return expectTypeRuntimeBehaviour(inverted)(actual[prop]) 29 | }, 30 | } 31 | Object.defineProperty(assertions, 'not', {get: () => expectTypeRuntimeBehaviour(!inverted)(actual)}) 32 | 33 | return assertions 34 | } 35 | 36 | const expectTypeOf = expectTypeRuntimeBehaviour() 37 | 38 | test('nullish types', () => { 39 | expectTypeOf(shorthand()).toEqualTypeOf(t.unknown) 40 | expectTypeOf(shorthand(undefined)).toEqualTypeOf(t.undefined) 41 | expectTypeOf(shorthand(null)).toEqualTypeOf(t.null) 42 | }) 43 | 44 | test('primitives', () => { 45 | expectTypeOf(shorthand(String)).toEqualTypeOf(t.string) 46 | expectTypeOf(shorthand(Number)).toEqualTypeOf(t.number) 47 | expectTypeOf(shorthand(Boolean)).toEqualTypeOf(t.boolean) 48 | expectTypeOf(shorthand(t.string)).toEqualTypeOf(t.string) 49 | }) 50 | 51 | test('literals', () => { 52 | expectTypeOf(shorthand('hi')).toEqualTypeOf(t.literal('hi')) 53 | expectTypeOf(shorthand(1)).toEqualTypeOf(t.literal(1)) 54 | }) 55 | 56 | test('objects', () => { 57 | expectTypeOf(shorthand(Object)).toEqualTypeOf(t.object) 58 | }) 59 | 60 | test('complex interfaces', () => { 61 | expectTypeOf(shorthand({foo: String, bar: {baz: Number}})).toEqualTypeOf( 62 | t.type({foo: t.string, bar: t.type({baz: t.number})}) 63 | ) 64 | }) 65 | 66 | test('arrays', () => { 67 | expectTypeOf(shorthand(Array)).toEqualTypeOf(t.UnknownArray) 68 | // @ts-expect-error 69 | expectTypeOf(shorthand([])).toEqualTypeOf(t.array(t.unknown)) 70 | 71 | expectTypeOf(shorthand([String])).toEqualTypeOf(t.array(t.string)) 72 | expectTypeOf(shorthand([[String]])).toEqualTypeOf(t.array(t.array(t.string))) 73 | 74 | expectTypeOf(shorthand([{foo: String}])).toEqualTypeOf(t.array(t.type({foo: t.string}))) 75 | expectTypeOf(shorthand([[String]])).toEqualTypeOf(t.array(t.array(t.string))) 76 | }) 77 | 78 | test('tuples', () => { 79 | expectTypeOf(shorthand([1, [String]])).toEqualTypeOf(t.tuple([t.string])) 80 | 81 | expectTypeOf(shorthand([2, [String, Number]])).toEqualTypeOf(t.tuple([t.string, t.number])) 82 | 83 | expectTypeOf(shorthand([3, [String, Number, String]])).toEqualTypeOf(t.tuple([t.string, t.number, t.string])) 84 | 85 | expectTypeOf(shorthand([4, [String, Number, String, Number]])).toEqualTypeOf( 86 | t.tuple([t.string, t.number, t.string, t.number]) 87 | ) 88 | 89 | expectTypeOf(shorthand([2, [{foo: [String]}, Number]])).toEqualTypeOf( 90 | t.tuple([t.type({foo: t.array(t.string)}), t.number]) 91 | ) 92 | }) 93 | 94 | test(`functions aren't supported`, () => { 95 | // @ts-expect-error 96 | expectTypeOf(shorthand(() => 1)).toEqualTypeOf(t.unknown) 97 | }) 98 | 99 | test(`non-tuple arrays with length greater than one aren't supported`, () => { 100 | expect(() => { 101 | // @ts-expect-error 102 | expectTypeOf(shorthand([1, 2])).toEqualTypeOf(t.never) 103 | }).toThrowErrorMatchingInlineSnapshot( 104 | `"Invalid type. Arrays should be in the form \`[shorthand]\`, and tuples should be in the form \`[3, [shorthand1, shorthand2, shorthand3]]\`"` 105 | ) 106 | }) 107 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/__tests__/util.test.ts: -------------------------------------------------------------------------------- 1 | import {RichError} from '..' 2 | 3 | test('RichError throws', () => { 4 | expect(() => RichError.throw({foo: 'bar'})).toThrowErrorMatchingInlineSnapshot(` 5 | "{ 6 | \\"foo\\": \\"bar\\" 7 | }" 8 | `) 9 | expect(RichError.throw).toThrowErrorMatchingInlineSnapshot(` 10 | "{ 11 | \\"details\\": \\"none!\\" 12 | }" 13 | `) 14 | expect(RichError.thrower('foobar')).toThrowErrorMatchingInlineSnapshot(` 15 | "{ 16 | \\"context\\": \\"foobar\\" 17 | }" 18 | `) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/index.ts: -------------------------------------------------------------------------------- 1 | // codegen:start {preset: barrel} 2 | export * from './combinators' 3 | export * from './mapper' 4 | export * from './match' 5 | export * from './narrow' 6 | export * from './reporters' 7 | export * from './shorthand' 8 | export * from './util' 9 | // codegen:end 10 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/mapper.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts' 2 | import {RichError, funcLabel} from './util' 3 | import * as Either from 'fp-ts/lib/Either' 4 | import {pipe} from 'fp-ts/lib/pipeable' 5 | 6 | export type Decoder = Omit, 'encode'> 7 | // prettier-ignore 8 | interface Mapper { 9 | >(from: t.Type, to: t.Type, map: (f: From) => ToO): Decoder & {from: From; to: ToA}; 10 | >(from: t.Type, to: t.Type, map: (f: From) => ToO, unmap: (t: ToA) => From): t.Type & {from: From; to: ToA}; 11 | } 12 | 13 | /** 14 | * A helper for building "parser-decoder" types - that is, types that validate an input, 15 | * transform it into another type, and then validate the target type. 16 | * 17 | * @example 18 | * const StringsFromMixedArray = mapper( 19 | * t.array(t.any), 20 | * t.array(t.string), 21 | * mixedArray => mixedArray.filter(value => typeof value === 'string') 22 | * ) 23 | * StringsFromMixedArray.decode(['a', 1, 'b', 2]) // right(['a', 'b']) 24 | * StringsFromMixedArray.decode('not an array') // left(...) 25 | * 26 | * @see parser 27 | * 28 | * @param from the expected type of input value 29 | * @param to the expected type of the decoded value 30 | * @param map transform (decode) a `from` type to a `to` type 31 | * @param unmap transfrom a `to` type back to a `from` type 32 | */ 33 | export const mapper: Mapper = ( 34 | from: t.Type, 35 | to: t.Type, 36 | map: (f: From) => To, 37 | unmap: (t: To) => From = RichError.thrower('unmapper/encoder not implemented') 38 | ) => { 39 | const fail = (s: From, c: t.Context, info: string) => 40 | t.failure(s, c.concat([{key: `decoder [${funcLabel(map)}]: ${info}`, type: to}])) 41 | const piped = from.pipe( 42 | new t.Type( 43 | to.name, 44 | to.is, 45 | (s, c) => 46 | pipe( 47 | Either.tryCatch( 48 | () => map(s), 49 | err => `error thrown decoding: [${err}]` 50 | ), 51 | Either.fold( 52 | e => fail(s, c, e), 53 | value => to.validate(value, c) 54 | ) 55 | ), 56 | unmap 57 | ), 58 | `${from.name} |> ${funcLabel(map)} |> ${to.name}` 59 | ) as any 60 | return Object.assign(piped, {from, to}) 61 | } 62 | 63 | /** 64 | * A helper for parsing strings into other types. A wrapper around `mapper` where the `from` type is `t.string`. 65 | * @see mapper 66 | * 67 | * @example 68 | * const IntFromString = parser(t.Int, parseFloat) 69 | * IntFromString.decode('123') // right(123) 70 | * IntFromString.decode('123.4') // left(...) 71 | * IntFromString.decode('not a number') // left(...) 72 | * IntFromString.decode(123) // left(...) 73 | * 74 | * @param type the target type 75 | * @param decode transform a string into the target type 76 | * @param encode transform the target type back into a string 77 | */ 78 | export const parser = >( 79 | type: t.Type, 80 | decode: (value: string) => ToO, 81 | encode: (value: ToA) => string = String 82 | ): t.Type & {from: string; to: ToA} => mapper(t.string, type, decode, encode) 83 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/narrow.ts: -------------------------------------------------------------------------------- 1 | import {Type, TypeOf, RefinementC, RefinementType, success, failure, getFunctionName, Context, Any} from 'io-ts' 2 | import {either} from 'fp-ts/lib/Either' 3 | 4 | const chain = either.chain 5 | 6 | /** 7 | * Like io-ts's refinement type but: 8 | * 1. Not deprecated (see https://github.com/gcanti/io-ts/issues/373) 9 | * 2. Passes in `Context` to the predicate argument, so you can check parent key names etc. 10 | * 3. Optionally allows returning another io-ts codec instead of a boolean for better error messages. 11 | * 12 | * @example 13 | * const CloudResources = narrow( 14 | * t.type({ 15 | * database: t.type({username: t.string, password: t.string}), 16 | * service: t.type({dbConnectionString: t.string}), 17 | * }), 18 | * ({database}) => t.type({ 19 | * service: t.type({dbConnectionString: t.literal(`${database.username}:${database.password}`)}), 20 | * }) 21 | * ) 22 | * 23 | * const valid = CloudResources.decode({ 24 | * database: {username: 'user', password: 'pass'}, 25 | * service: {dbConnectionString: 'user:pass'}, 26 | * }) 27 | * // returns a `Right` 28 | * 29 | * const invalid = CloudResources.decode({ 30 | * database: {username: 'user', password: 'pass'}, 31 | * service: {dbConnectionString: 'user:wrongpassword'}, 32 | * }) 33 | * // returns a `Left` - service.dbConnectionString expected "user:pass", but got "user:wrongpassword" 34 | */ 35 | export const narrow = ( 36 | codec: C, 37 | predicate: (value: TypeOf, context: Context) => D | boolean, 38 | name = `(${codec.name} | ${getFunctionName(predicate)})` 39 | ): RefinementC => { 40 | return new RefinementType( 41 | name, 42 | (u): u is TypeOf => { 43 | if (!codec.is(u)) { 44 | return false 45 | } 46 | const refined = predicate(u, []) 47 | if (refined instanceof Type) { 48 | return refined.is(u) 49 | } 50 | return refined 51 | }, 52 | (i, c) => 53 | chain(codec.validate(i, c), a => { 54 | const refined = predicate(a, c) 55 | if (refined instanceof Type) { 56 | return refined.validate(a, c) 57 | } 58 | if (refined) { 59 | return success(a) 60 | } 61 | return failure(a, c) 62 | }), 63 | codec.encode, 64 | codec, 65 | predicate as any 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/reporters.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts' 2 | import {inspect} from 'util' 3 | import {EOL} from 'os' 4 | 5 | /** 6 | * Similar to io-ts's PathReporter, but gives slightly less verbose output. 7 | * @param validation Usually the result of calling `.decode` with an io-ts codec. 8 | * @param typeAlias io-ts type names can be verbose. If the type you're using doesn't have a name, 9 | * you can use this to keep error messages shorter. 10 | */ 11 | export const validationErrors = (validation: t.Validation, typeAlias?: string) => { 12 | if (validation._tag === 'Right') { 13 | return ['No errors!'] 14 | } 15 | return validation.left.map(e => { 16 | const name = typeAlias || e.context[0]?.type.name 17 | const lastType = e.context.length && e.context[e.context.length - 1].type.name 18 | const path = name + e.context.map(c => c.key).join('.') 19 | return `Invalid value {${inspect(e.value)}} supplied to ${path}. Expected ${lastType}.` 20 | }) 21 | } 22 | 23 | /** 24 | * Either returns the `.right` of a io-ts `Validation`, or throws with a report of the validation error. 25 | * @see validationErrors 26 | */ 27 | export const getRightUnsafe = (validation: t.Validation, typeAlias?: string) => { 28 | if (validation._tag === 'Right') { 29 | return validation.right 30 | } 31 | throw Error(validationErrors(validation, typeAlias).join(EOL)) 32 | } 33 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/shorthand.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts' 2 | import {RegExpCodec, regexp} from './combinators' 3 | 4 | export type ShorthandPrimitive = typeof String | typeof Number | typeof Boolean 5 | export type ShorthandLiteral = string | number | boolean | null | undefined 6 | export type ShorthandInput = 7 | | ShorthandPrimitive 8 | | ShorthandLiteral 9 | | RegExp 10 | | typeof Array 11 | | typeof Object 12 | | [ShorthandInput] 13 | | [1, [ShorthandInput]] 14 | | [2, [ShorthandInput, ShorthandInput]] 15 | | [3, [ShorthandInput, ShorthandInput, ShorthandInput]] 16 | | [4, [ShorthandInput, ShorthandInput, ShorthandInput, ShorthandInput]] 17 | | {[K in string]: ShorthandInput} 18 | | t.Type 19 | 20 | export type Shorthand = V extends string | number | boolean 21 | ? t.LiteralC 22 | : V extends null 23 | ? t.NullC 24 | : V extends undefined 25 | ? t.UndefinedC 26 | : V extends typeof String 27 | ? t.StringC 28 | : V extends typeof Number 29 | ? t.NumberC 30 | : V extends typeof Boolean 31 | ? t.BooleanC 32 | : V extends typeof Array 33 | ? t.UnknownArrayC 34 | : V extends typeof Object 35 | ? t.ObjectC 36 | : V extends RegExp 37 | ? RegExpCodec 38 | : V extends [ShorthandInput] 39 | ? t.ArrayC> 40 | : V extends [1, [ShorthandInput]] 41 | ? t.TupleC<[Shorthand]> 42 | : V extends [2, [ShorthandInput, ShorthandInput]] 43 | ? t.TupleC<[Shorthand, Shorthand]> 44 | : V extends [3, [ShorthandInput, ShorthandInput, ShorthandInput]] 45 | ? t.TupleC<[Shorthand, Shorthand, Shorthand]> 46 | : V extends [4, [ShorthandInput, ShorthandInput, ShorthandInput, ShorthandInput]] 47 | ? t.TupleC<[Shorthand, Shorthand, Shorthand, Shorthand]> 48 | : V extends t.Type 49 | ? V 50 | : V extends Record 51 | ? t.TypeC<{[K in keyof V]: Shorthand}> 52 | : never 53 | 54 | export type CodecFromShorthand = { 55 | (): t.UnknownC 56 | (v: V): Shorthand 57 | } 58 | 59 | /* eslint-disable complexity */ 60 | 61 | /** 62 | * Gets an io-ts codec from a shorthand input: 63 | * 64 | * |shorthand|io-ts type| 65 | * |-|-| 66 | * |`String`, `Number`, `Boolean`|`t.string`, `t.number`, `t.boolean`| 67 | * |Literal raw strings, numbers and booleans e.g. `7` or `'foo'`|`t.literal(7)`, `t.literal('foo')` etc.| 68 | * |Regexes e.g. `/^foo/`|see [regexp](#regexp)| 69 | * |`null` and `undefined`|`t.null` and `t.undefined`| 70 | * |No input (_not_ the same as explicitly passing `undefined`)|`t.unknown`| 71 | * |Objects e.g. `{ foo: String, bar: { baz: Number } }`|`t.type(...)` e.g. `t.type({foo: t.string, bar: t.type({ baz: t.number }) })` 72 | * |`Array`|`t.unknownArray`| 73 | * |`Object`|`t.object`| 74 | * |One-element arrays e.g. `[String]`|`t.array(...)` e.g. `t.array(t.string)`| 75 | * |Tuples with explicit length e.g. `[2, [String, Number]]`|`t.tuple` e.g. `t.tuple([t.string, t.number])`| 76 | * |io-ts codecs|unchanged| 77 | * |Unions, intersections, partials, tuples with more than 3 elements, and other complex types|not supported, except by passing in an io-ts codec| 78 | */ 79 | export const codecFromShorthand: CodecFromShorthand = (...args: unknown[]): any => { 80 | if (args.length === 0) { 81 | return t.unknown 82 | } 83 | const v = args[0] 84 | if (v === String) { 85 | return t.string 86 | } 87 | if (v === Number) { 88 | return t.number 89 | } 90 | if (v === Boolean) { 91 | return t.boolean 92 | } 93 | if (v === Array) { 94 | return t.UnknownArray 95 | } 96 | if (v === Object) { 97 | return t.object 98 | } 99 | if (v === null) { 100 | return t.null 101 | } 102 | if (typeof v === 'undefined') { 103 | return t.undefined 104 | } 105 | if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { 106 | return t.literal(v) 107 | } 108 | if (v instanceof RegExp) { 109 | return regexp(v) 110 | } 111 | if (Array.isArray(v) && v.length === 0) { 112 | return t.array(t.unknown) 113 | } 114 | if (Array.isArray(v) && v.length === 1) { 115 | return t.array(codecFromShorthand(v[0])) 116 | } 117 | if (Array.isArray(v) && v.length === 2 && typeof v[0] === 'number' && Array.isArray(v[1])) { 118 | return t.tuple(v[1].map(codecFromShorthand) as any) 119 | } 120 | if (Array.isArray(v)) { 121 | throw new TypeError( 122 | `Invalid type. Arrays should be in the form \`[shorthand]\`, and tuples should be in the form \`[3, [shorthand1, shorthand2, shorthand3]]\`` 123 | ) 124 | } 125 | if (v instanceof t.Type) { 126 | return v 127 | } 128 | if (typeof v === 'object' && v) { 129 | return t.type( 130 | Object.entries(v).reduce((acc, [prop, val]) => { 131 | return {...acc, [prop]: codecFromShorthand(val)} 132 | }, {}) 133 | ) 134 | } 135 | return t.unknown 136 | } 137 | -------------------------------------------------------------------------------- /packages/io-ts-extra/src/util.ts: -------------------------------------------------------------------------------- 1 | const secret = Symbol('secret') 2 | type Secret = typeof secret 3 | type IsNever = [T] extends [never] ? 1 : 0 4 | type Not = T extends 1 ? 0 : 1 5 | type IsAny = [T] extends [Secret] ? Not> : 0 6 | type OneOf = T extends 1 ? 1 : U 7 | export type IsNeverOrAny = OneOf, IsAny> 8 | 9 | export class RichError extends Error { 10 | private constructor(public details: unknown) { 11 | super(JSON.stringify(details, null, 2)) 12 | } 13 | 14 | public static thrower(context: string) { 15 | return (info?: T): never => RichError.throw({context, details: info}) 16 | } 17 | 18 | public static throw(details?: T): never { 19 | const resolvedDetails = details || {details: 'none!'} 20 | throw Object.assign(new RichError(resolvedDetails), {details: resolvedDetails}) 21 | } 22 | } 23 | 24 | export const funcLabel = (func: Function) => 25 | func.name || 26 | func 27 | .toString() 28 | .split('\n') 29 | .filter((_, i, arr) => i === 0 || i === arr.length - 1) 30 | .map(line => line.trim()) 31 | .join(' ... ') 32 | -------------------------------------------------------------------------------- /packages/io-ts-extra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@mmkal/rig/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "tsBuildInfoFile": "dist/buildinfo.json", 7 | "typeRoots": [ 8 | "node_modules/@mmkal/rig/node_modules/@types" 9 | ] 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "dist" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/memorable-moniker/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@mmkal/rig/.eslintrc') 2 | -------------------------------------------------------------------------------- /packages/memorable-moniker/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/__tests__ 3 | dist/buildinfo.json 4 | .eslintcache 5 | .eslintrc.js 6 | .rush 7 | .heft 8 | *.log 9 | tsconfig.json 10 | config/jest.config.json 11 | jest.config.js 12 | coverage 13 | -------------------------------------------------------------------------------- /packages/memorable-moniker/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memorable-moniker", 3 | "entries": [ 4 | { 5 | "version": "0.5.1", 6 | "tag": "memorable-moniker_v0.5.1", 7 | "date": "Sat, 16 Oct 2021 13:17:24 GMT", 8 | "comments": { 9 | "dependency": [ 10 | { 11 | "comment": "Updating dependency \"expect-type\" to `0.13.0`" 12 | } 13 | ] 14 | } 15 | }, 16 | { 17 | "version": "0.4.0", 18 | "tag": "memorable-moniker_v0.4.0", 19 | "date": "Tue, 29 Jun 2021 08:48:24 GMT", 20 | "comments": { 21 | "minor": [ 22 | { 23 | "comment": "Remove ass/donkey from animal dictionary (#230)" 24 | } 25 | ], 26 | "patch": [ 27 | { 28 | "comment": "chore(deps): update devdependencies (#203) - @renovate[bot]" 29 | }, 30 | { 31 | "comment": "fix(deps): update dependency lodash to v4.17.21 [security] (#236) - @renovate[bot]" 32 | } 33 | ], 34 | "dependency": [ 35 | { 36 | "comment": "Updating dependency \"expect-type\" to `0.11.0`" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "version": "0.3.3", 43 | "tag": "memorable-moniker_v0.3.3", 44 | "date": "Thu, 03 Dec 2020 19:10:22 GMT", 45 | "comments": { 46 | "dependency": [ 47 | { 48 | "comment": "Updating dependency \"expect-type\" to `0.10.0`" 49 | } 50 | ] 51 | } 52 | }, 53 | { 54 | "version": "0.3.2", 55 | "tag": "memorable-moniker_v0.3.2", 56 | "date": "Sat, 28 Nov 2020 19:10:00 GMT", 57 | "comments": { 58 | "dependency": [ 59 | { 60 | "comment": "Updating dependency \"expect-type\" to `0.9.2`" 61 | } 62 | ] 63 | } 64 | }, 65 | { 66 | "version": "0.3.1", 67 | "tag": "memorable-moniker_v0.3.1", 68 | "date": "Thu, 26 Nov 2020 17:06:36 GMT", 69 | "comments": { 70 | "dependency": [ 71 | { 72 | "comment": "Updating dependency \"expect-type\" from `0.9.0` to `0.9.1`" 73 | } 74 | ] 75 | } 76 | }, 77 | { 78 | "version": "0.3.0", 79 | "tag": "memorable-moniker_v0.3.0", 80 | "date": "Tue, 27 Oct 2020 16:18:39 GMT", 81 | "comments": { 82 | "minor": [ 83 | { 84 | "comment": "Permalink readme urls before publishing (#211)" 85 | } 86 | ], 87 | "dependency": [ 88 | { 89 | "comment": "Updating dependency \"expect-type\" from `0.8.0` to `0.9.0`" 90 | } 91 | ] 92 | } 93 | }, 94 | { 95 | "version": "0.2.20", 96 | "tag": "memorable-moniker_v0.2.20", 97 | "date": "Mon, 12 Oct 2020 12:24:41 GMT", 98 | "comments": { 99 | "patch": [ 100 | { 101 | "comment": "chore(deps): pin dependencies (#173) - @renovate[bot]" 102 | } 103 | ] 104 | } 105 | }, 106 | { 107 | "version": "0.2.19", 108 | "tag": "memorable-moniker_v0.2.19", 109 | "date": "Mon, 05 Oct 2020 22:38:33 GMT", 110 | "comments": { 111 | "dependency": [ 112 | { 113 | "comment": "Updating dependency \"expect-type\" from `0.7.11` to `0.8.0`" 114 | } 115 | ] 116 | } 117 | }, 118 | { 119 | "version": "0.2.18", 120 | "tag": "memorable-moniker_v0.2.18", 121 | "date": "Thu, 01 Oct 2020 14:48:13 GMT", 122 | "comments": { 123 | "patch": [ 124 | { 125 | "comment": "chore: npmignore coverage folder (#184)" 126 | } 127 | ], 128 | "dependency": [ 129 | { 130 | "comment": "Updating dependency \"expect-type\" from `0.7.10` to `0.7.11`" 131 | } 132 | ] 133 | } 134 | }, 135 | { 136 | "version": "0.2.17", 137 | "tag": "memorable-moniker_v0.2.17", 138 | "date": "Fri, 18 Sep 2020 16:56:41 GMT", 139 | "comments": { 140 | "patch": [ 141 | { 142 | "comment": "fix(npmignore): exclude rush files (#169)" 143 | } 144 | ], 145 | "dependency": [ 146 | { 147 | "comment": "Updating dependency \"expect-type\" from `0.7.9` to `0.7.10`" 148 | } 149 | ] 150 | } 151 | }, 152 | { 153 | "version": "0.2.16", 154 | "tag": "memorable-moniker_v0.2.16", 155 | "date": "Fri, 18 Sep 2020 00:05:29 GMT", 156 | "comments": { 157 | "patch": [ 158 | { 159 | "comment": "chore: more detailed docs for memorable moniker (#166)" 160 | } 161 | ] 162 | } 163 | }, 164 | { 165 | "version": "0.2.15", 166 | "tag": "memorable-moniker_v0.2.15", 167 | "date": "Thu, 17 Sep 2020 10:47:43 GMT", 168 | "comments": { 169 | "patch": [ 170 | { 171 | "comment": "chore: enable consistent versions (#163)" 172 | } 173 | ] 174 | } 175 | } 176 | ] 177 | } 178 | -------------------------------------------------------------------------------- /packages/memorable-moniker/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - memorable-moniker 2 | 3 | This log was last generated on Sat, 16 Oct 2021 13:17:24 GMT and should not be manually modified. 4 | 5 | ## 0.5.1 6 | Sat, 16 Oct 2021 13:17:24 GMT 7 | 8 | _Version update only_ 9 | 10 | ## 0.4.0 11 | Tue, 29 Jun 2021 08:48:24 GMT 12 | 13 | ### Minor changes 14 | 15 | - Remove ass/donkey from animal dictionary (#230) 16 | 17 | ### Patches 18 | 19 | - chore(deps): update devdependencies (#203) - @renovate[bot] 20 | - fix(deps): update dependency lodash to v4.17.21 [security] (#236) - @renovate[bot] 21 | 22 | ## 0.3.3 23 | Thu, 03 Dec 2020 19:10:22 GMT 24 | 25 | _Version update only_ 26 | 27 | ## 0.3.2 28 | Sat, 28 Nov 2020 19:10:00 GMT 29 | 30 | _Version update only_ 31 | 32 | ## 0.3.1 33 | Thu, 26 Nov 2020 17:06:36 GMT 34 | 35 | _Version update only_ 36 | 37 | ## 0.3.0 38 | Tue, 27 Oct 2020 16:18:39 GMT 39 | 40 | ### Minor changes 41 | 42 | - Permalink readme urls before publishing (#211) 43 | 44 | ## 0.2.20 45 | Mon, 12 Oct 2020 12:24:41 GMT 46 | 47 | ### Patches 48 | 49 | - chore(deps): pin dependencies (#173) - @renovate[bot] 50 | 51 | ## 0.2.19 52 | Mon, 05 Oct 2020 22:38:33 GMT 53 | 54 | _Version update only_ 55 | 56 | ## 0.2.18 57 | Thu, 01 Oct 2020 14:48:13 GMT 58 | 59 | ### Patches 60 | 61 | - chore: npmignore coverage folder (#184) 62 | 63 | ## 0.2.17 64 | Fri, 18 Sep 2020 16:56:41 GMT 65 | 66 | ### Patches 67 | 68 | - fix(npmignore): exclude rush files (#169) 69 | 70 | ## 0.2.16 71 | Fri, 18 Sep 2020 00:05:29 GMT 72 | 73 | ### Patches 74 | 75 | - chore: more detailed docs for memorable moniker (#166) 76 | 77 | ## 0.2.15 78 | Thu, 17 Sep 2020 10:47:43 GMT 79 | 80 | ### Patches 81 | 82 | - chore: enable consistent versions (#163) 83 | 84 | -------------------------------------------------------------------------------- /packages/memorable-moniker/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@mmkal/rig/jest.config') 2 | -------------------------------------------------------------------------------- /packages/memorable-moniker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memorable-moniker", 3 | "version": "0.5.1", 4 | "keywords": [ 5 | "word", 6 | "words", 7 | "list", 8 | "array", 9 | "random", 10 | "animal", 11 | "dictionary", 12 | "dictionaries", 13 | "name", 14 | "names", 15 | "female", 16 | "male", 17 | "nicknames", 18 | "generator" 19 | ], 20 | "homepage": "https://github.com/mmkal/ts/tree/main/packages/memorable-moniker#readme", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/mmkal/ts.git", 24 | "directory": "packages/memorable-moniker" 25 | }, 26 | "license": "Apache-2.0", 27 | "main": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "scripts": { 30 | "prebuild": "npm run clean", 31 | "build": "rig tsc -p .", 32 | "clean": "rig rimraf dist", 33 | "lint": "rig eslint --cache .", 34 | "prepack": "rig permalink", 35 | "postpack": "rig unpermalink", 36 | "test": "rig jest" 37 | }, 38 | "dependencies": { 39 | "seedrandom": "^3.0.5" 40 | }, 41 | "devDependencies": { 42 | "@mmkal/rig": "workspace:*", 43 | "@types/lodash": "4.14.165", 44 | "@types/seedrandom": "2.4.28", 45 | "expect-type2": "npm:expect-type@0.14.0", 46 | "lodash": "^4.17.15" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/memorable-moniker/src/__tests__/rng.test.ts: -------------------------------------------------------------------------------- 1 | import {nicknames} from '..' 2 | import {range} from 'lodash' 3 | 4 | test('Invalid random-number generator fails gracefully', () => { 5 | jest.spyOn(console, 'error').mockReset() 6 | const generator = nicknames.modify({ 7 | rng: () => 200, 8 | }) 9 | 10 | range(0, 20).map(generator.next) 11 | expect(console.error).toHaveBeenCalled() 12 | }) 13 | -------------------------------------------------------------------------------- /packages/memorable-moniker/src/dict/animal.ts: -------------------------------------------------------------------------------- 1 | import {split} from './util' 2 | 3 | // from https://web.archive.org/web/20200105051826/https://en.wikipedia.org/wiki/List_of_animal_names 4 | // ran in dev tools: [...document.querySelectorAll('table.wikitable')[1].querySelectorAll('tr')].slice(1).map(td => td.querySelector('a')).filter(Boolean).map(a => a.innerText).filter(Boolean).map(t => t.split(' (')[0]).filter(Boolean).filter(t => !t.includes('Wikipedia')).join('\n') 5 | 6 | export const animal = split(` 7 | Aardvark 8 | Albatross 9 | Alligator 10 | Alpaca 11 | Ant 12 | Anteater 13 | Antelope 14 | Ape 15 | Armadillo 16 | Baboon 17 | Badger 18 | Barracuda 19 | Bat 20 | Bear 21 | Beaver 22 | Bee 23 | Binturong 24 | Bird 25 | Bison 26 | Bluebird 27 | Boar 28 | Bobcat 29 | Buffalo 30 | Butterfly 31 | Camel 32 | Capybara 33 | Caracal 34 | Caribou 35 | Cassowary 36 | Cat 37 | Caterpillar 38 | Cattle 39 | Chameleon 40 | Chamois 41 | Cheetah 42 | Chicken 43 | Chimpanzee 44 | Chinchilla 45 | Chough 46 | Coati 47 | Cobra 48 | Cockroach 49 | Cod 50 | Cormorant 51 | Cougar 52 | Coyote 53 | Crab 54 | Crane 55 | Cricket 56 | Crocodile 57 | Crow 58 | Cuckoo 59 | Curlew 60 | Deer 61 | Degu 62 | Dhole 63 | Dingo 64 | Dinosaur 65 | Dog 66 | Dogfish 67 | Dolphin 68 | Donkey 69 | Dotterel 70 | Dove 71 | Dragonfly 72 | Duck 73 | Dugong 74 | Dunlin 75 | Eagle 76 | Echidna 77 | Eel 78 | Eland 79 | Elephant 80 | Elephant seal 81 | Elk 82 | Emu 83 | Falcon 84 | Ferret 85 | Finch 86 | Fish 87 | Flamingo 88 | Fly 89 | Fox 90 | Frog 91 | Gaur 92 | Gazelle 93 | Gecko 94 | Gerbil 95 | Giant panda 96 | Giraffe 97 | Gnat 98 | Gnu 99 | Goat 100 | Goldfinch 101 | Goosander 102 | Goose 103 | Gorilla 104 | Goshawk 105 | Grasshopper 106 | Grouse 107 | Guanaco 108 | Guinea fowl 109 | Guinea pig 110 | Gull 111 | Hamster 112 | Hare 113 | Hawk 114 | Hedgehog 115 | Hermit crab 116 | Heron 117 | Herring 118 | Hippopotamus 119 | Hoatzin 120 | Hoopoe 121 | Hornet 122 | Horse 123 | Human 124 | Hummingbird 125 | Hyena 126 | Ibex 127 | Ibis 128 | Iguana 129 | Impala 130 | Jackal 131 | Jaguar 132 | Jay 133 | Jellyfish 134 | Jerboa 135 | Kangaroo 136 | Kingfisher 137 | Kinkajou 138 | Koala 139 | Komodo dragon 140 | Kookaburra 141 | Kouprey 142 | Kudu 143 | Lapwing 144 | Lark 145 | Lemur 146 | Leopard 147 | Lion 148 | Lizard 149 | Llama 150 | Lobster 151 | Locust 152 | Loris 153 | Louse 154 | Lynx 155 | Lyrebird 156 | Macaque 157 | Macaw 158 | Magpie 159 | Mallard 160 | Mammoth 161 | Manatee 162 | Mandrill 163 | Marmoset 164 | Marmot 165 | Meerkat 166 | Mink 167 | Mole 168 | Mongoose 169 | Monkey 170 | Moose 171 | Mosquito 172 | Mouse 173 | Myna 174 | Narwhal 175 | Newt 176 | Nightingale 177 | Nine-banded armadillo 178 | Octopus 179 | Okapi 180 | Opossum 181 | Orangutan 182 | Oryx 183 | Ostrich 184 | Otter 185 | Owl 186 | Oyster 187 | Panther 188 | Parrot 189 | Panda 190 | Partridge 191 | Peafowl 192 | Pelican 193 | Penguin 194 | Pheasant 195 | Pig 196 | Pigeon 197 | Pika 198 | Polar bear 199 | Pony 200 | Porcupine 201 | Porpoise 202 | Prairie dog 203 | Pug 204 | Quail 205 | Quelea 206 | Quetzal 207 | Rabbit 208 | Raccoon 209 | Ram 210 | Rat 211 | Raven 212 | Red deer 213 | Red panda 214 | Reindeer 215 | Rhea 216 | Rhinoceros 217 | Rook 218 | Salamander 219 | Salmon 220 | Sand dollar 221 | Sandpiper 222 | Sardine 223 | Sea lion 224 | Seahorse 225 | Seal 226 | Shark 227 | Sheep 228 | Shrew 229 | Siamang 230 | Skunk 231 | Sloth 232 | Snail 233 | Snake 234 | Spider 235 | Squid 236 | Squirrel 237 | Starling 238 | Stegosaurus 239 | Swan 240 | Tamarin 241 | Tapir 242 | Tarsier 243 | Termite 244 | Tiger 245 | Toad 246 | Toucan 247 | Turaco 248 | Turkey 249 | Turtle 250 | Umbrellabird 251 | Vicuña 252 | Vinegaroon 253 | Viper 254 | Vulture 255 | Wallaby 256 | Walrus 257 | Wasp 258 | Water buffalo 259 | Waxwing 260 | Weasel 261 | Whale 262 | Wobbegong 263 | Wolf 264 | Wolverine 265 | Wombat 266 | Woodpecker 267 | Worm 268 | Wren 269 | X-ray tetra 270 | Yak 271 | Zebra 272 | `) 273 | -------------------------------------------------------------------------------- /packages/memorable-moniker/src/dict/index.ts: -------------------------------------------------------------------------------- 1 | // codegen:start {preset: barrel, exclude: util.ts} 2 | export * from './animal' 3 | export * from './femaleName' 4 | export * from './lastName' 5 | export * from './maleName' 6 | export * from './positiveAdjective' 7 | // codegen:end 8 | -------------------------------------------------------------------------------- /packages/memorable-moniker/src/dict/positiveAdjective.ts: -------------------------------------------------------------------------------- 1 | import {split} from './util' 2 | 3 | // from https://web.archive.org/web/20191223202350/https://www.scribd.com/document/357627199/Feeling-Adjectives-1 4 | 5 | export const positiveAdjective = split(` 6 | admirable 7 | energetic 8 | lucky 9 | affable 10 | enjoyable 11 | magnificent 12 | affectionate 13 | enthusiastic 14 | marvelous 15 | agreeable 16 | euphoric 17 | meritorious 18 | amazing 19 | excellent 20 | merry 21 | amiable 22 | exceptional 23 | mild-mannered 24 | amused 25 | excited 26 | nice 27 | amusing 28 | extraordinary 29 | noble 30 | animated 31 | exultant 32 | outstanding 33 | appreciative 34 | fabulous 35 | overjoyed 36 | astonishing 37 | faithful 38 | passionate 39 | authentic 40 | fantastic 41 | peaceful 42 | believable 43 | fervent 44 | placid 45 | benevolent 46 | fortunate 47 | pleasant 48 | blissful 49 | friendly 50 | pleasing 51 | bouncy 52 | fun 53 | pleasurable 54 | brilliant 55 | genuine 56 | positive 57 | bubbly 58 | glad 59 | praiseworthy 60 | buoyant 61 | glorious 62 | prominent 63 | calm 64 | good 65 | proud 66 | charming 67 | good-humored 68 | relaxed 69 | cheerful 70 | good-natured 71 | reliable 72 | cheery 73 | gracious 74 | respectable 75 | clever 76 | grateful 77 | sharp 78 | comfortable 79 | great 80 | sincere 81 | comical 82 | happy 83 | spirited 84 | commendable 85 | heartfelt 86 | splendid 87 | confident 88 | honest 89 | superb 90 | congenial 91 | honorable 92 | superior 93 | content 94 | hopeful 95 | terrific 96 | cordial 97 | humorous 98 | thankful 99 | courteous 100 | incredible 101 | tremendous 102 | dedicated 103 | inspirational 104 | triumphant 105 | delighted 106 | jolly 107 | trustworthy 108 | delightful 109 | jovial 110 | trusty 111 | dependable 112 | joyful 113 | truthful 114 | devoted 115 | joyous 116 | uplifting 117 | docile 118 | jubilant 119 | victorious 120 | dynamic 121 | keen 122 | vigorous 123 | eager 124 | kind 125 | virtuous 126 | earnest 127 | laudable 128 | vivacious 129 | easygoing 130 | laughing 131 | whimsical 132 | ebullient 133 | likable 134 | witty 135 | ecstatic 136 | lively 137 | wonderful 138 | elated 139 | lovely 140 | worthy 141 | emphatic 142 | loving 143 | zealous 144 | enchanting 145 | loyal 146 | zestful 147 | `) 148 | -------------------------------------------------------------------------------- /packages/memorable-moniker/src/dict/util.ts: -------------------------------------------------------------------------------- 1 | export const split = (multiLineString: string) => 2 | multiLineString 3 | .trim() 4 | .split(/\r?\n/) 5 | .map(line => line.trim()) 6 | .filter(Boolean) 7 | -------------------------------------------------------------------------------- /packages/memorable-moniker/src/index.ts: -------------------------------------------------------------------------------- 1 | import seedrandom from 'seedrandom' 2 | 3 | import * as dict from './dict' 4 | 5 | export type WordList = keyof typeof dict 6 | export type Dictionary = WordList | {words: string[]} | Dictionary[] 7 | 8 | export interface Params { 9 | dictionaries: Dictionary[] 10 | rng: Rng 11 | format: (word: string) => string 12 | choose: (params: {dict: string[]; rng: () => number}) => string 13 | join: (parts: string[]) => T 14 | } 15 | 16 | export type InputParams = 17 | // prettier-ignore 18 | Partial, 'rng'> & { 19 | rng: () => number; 20 | }> 21 | 22 | export interface NameGenerator { 23 | params: Params 24 | modify: (changes: InputParams | ((original: Params) => InputParams)) => NameGenerator 25 | next: () => T 26 | } 27 | 28 | const resolveDictionaries = (dictionary: Dictionary): string[] => { 29 | if (typeof dictionary === 'string') { 30 | return dict[dictionary] 31 | } 32 | if (Array.isArray(dictionary)) { 33 | return ([] as string[]).concat(...dictionary.map(resolveDictionaries)) 34 | } 35 | return dictionary.words 36 | } 37 | 38 | export type Rng = (() => number) & {seed: (seed: any) => Rng} 39 | export const getRng = (seed?: string): Rng => Object.assign(seedrandom(seed), {seed: getRng}) 40 | 41 | export const createNameGenerator = (params: Params): NameGenerator => { 42 | const wordLists = params.dictionaries.map(resolveDictionaries) 43 | const rng: Rng = Object.assign( 44 | () => { 45 | const num = params.rng() 46 | if (num >= 0 && num < 1) { 47 | return num 48 | } 49 | console.error(`rng should return a number in [0,1). got ${num}`) 50 | return Math.random() 51 | }, 52 | {seed: getRng} 53 | ) 54 | return { 55 | params, 56 | modify: changes => { 57 | const updated: any = typeof changes === 'function' ? changes(params) : changes 58 | return createNameGenerator({...params, ...updated}) 59 | }, 60 | next: () => params.join(wordLists.map(dict => params.choose({dict, rng})).map(params.format)), 61 | } 62 | } 63 | 64 | /** 65 | * The easiest way to get a name-generator is to import the `nicknames` generator and customise it as necessary. 66 | * The `.modify(...)` method returns a new instance of a generator which extends the original. 67 | * It receives a partial dictionary of parameters, or a function which returns one - the function receives the 68 | * parent's configuration as an input. 69 | * 70 | * Parameters that can be modified: 71 | * 72 | * @description **dictionaries** -- 73 | * 74 | * A list of "dictionaries" that words should be chosen from. These can be one of the preset 75 | * values ('animal', 'femaleName', 'maleName', 'lastName', 'positiveAdjective'), or an object with a property 76 | * called `words` which should be an array of strings. It's also possible to pass a list of dictionaries, in the 77 | * same format. Some examples: 78 | * 79 | * @example 80 | * const animalGenerator = nicknames.modify({ 81 | * dictionaries: ['animal'] 82 | * }) 83 | * const formalAnimalGenerator = nicknames.modify({ 84 | * dictionaries: ['animal', 'lastName'] 85 | * }) 86 | * const veryFormalAnimalGenerator = nicknames.modify({ 87 | * dictionaries: [{words: ['Mr', 'Ms', 'Mrs']}, 'animal', 'lastName'] 88 | * }) 89 | * 90 | * @description **rng** -- 91 | * 92 | * A random-number generator. A function that should return a value between 0 and 1. The lower bound 93 | * should be inclusive and the upper bound exclusive. As a convenience, the default random-number generator 94 | * has an `rng.seed('...')` function to allow getting a seeded rng based on the original. Usage: 95 | * 96 | * @example 97 | * const myNameGenerator = nicknames.modify(params => ({ rng: params.rng.seed('my-seed-value') })) 98 | * console.log(myNameGenerator.next()) // always returns the same value 99 | * 100 | * @description **format** -- 101 | * 102 | * A function which transforms dictionary words before returning them from the generator. For example, 103 | * you could convert from kebab-case to snake_case with: 104 | * 105 | * @example 106 | * const myGenerator = nicknames.modify({ 107 | * format: word => word.replace(/-/g, '_') 108 | * }) 109 | * 110 | * @description **choose** -- 111 | * 112 | * A function which receives a list of words, and a random-number generator function, and should return 113 | * a single word. Typically this wouldn't need to be modified. 114 | * 115 | * @description **join** -- 116 | * 117 | * A function which receives one word from each dictionary, and is responsible for joining them into a single 118 | * value. Usually the return value is a string, but if another format is returned the type will be correctly inferred. 119 | * 120 | * @example 121 | * const informalPeople = nicknames.modify({ 122 | * dictionaries: [['maleName', 'femaleName'], 'lastName'] 123 | * join: (firstName, lastName) => `${firstName} ${lastName}`, 124 | * }) 125 | * const formalPeople = nicknames.modify({ 126 | * dictionaries: [['maleName', 'femaleName'], 'lastName'] 127 | * join: (firstName, lastName) => `${lastName}, ${firstName}`, 128 | * }) 129 | * const structuredPeople = nicknames.modify({ 130 | * dictionaries: [['maleName', 'femaleName'], 'lastName'] 131 | * join: (firstName, lastName) => ({ name: { first: firstName, last: lastName } }), 132 | * }) 133 | */ 134 | export const nicknames = createNameGenerator({ 135 | dictionaries: ['positiveAdjective', 'animal'], 136 | // prettier-ignore 137 | format: x => x.toLowerCase().split(/\W/).filter(Boolean).join('-'), 138 | join: parts => parts.join('-'), 139 | rng: getRng(), 140 | choose: ({dict, rng}) => dict[Math.floor(rng() * dict.length)], 141 | }) 142 | 143 | export const people = nicknames.modify({ 144 | dictionaries: [['maleName', 'femaleName'], 'lastName'], 145 | format: x => x.slice(0, 1).toUpperCase() + x.slice(1), 146 | join: parts => parts.join(' '), 147 | }) 148 | 149 | export const women = people.modify({ 150 | dictionaries: ['femaleName', 'lastName'], 151 | }) 152 | 153 | export const men = people.modify({ 154 | dictionaries: ['maleName', 'lastName'], 155 | }) 156 | -------------------------------------------------------------------------------- /packages/memorable-moniker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@mmkal/rig/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "tsBuildInfoFile": "dist/buildinfo.json", 7 | "typeRoots": [ 8 | "node_modules/@mmkal/rig/node_modules/@types" 9 | ] 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "dist" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":semanticCommits" 6 | ], 7 | "dependencyDashboard": true, 8 | "dependencyDashboardAutoclose": true, 9 | "prConcurrentLimit": 4, 10 | "packageRules": [ 11 | { 12 | "depTypeList": ["dependencies"], 13 | "paths": ["packages/**"], 14 | "rangeStrategy": "update-lockfile" 15 | }, 16 | { 17 | "depTypeList": ["dependencies"], 18 | "paths": ["packages/**"], 19 | "updateTypes": ["lockFileMaintenance"], 20 | "automerge": true 21 | }, 22 | { 23 | "depTypeList": ["dependencies"], 24 | "paths": ["packages/eslint-plugin-codegen/*"], 25 | "groupName": "eslint-plugin-codegen" 26 | }, 27 | { 28 | "depTypeList": ["dependencies", "devDependencies"], 29 | "paths": ["tools/**"], 30 | "groupName": "devDependencies", 31 | "automerge": true 32 | }, 33 | { 34 | "depTypeList": ["devDependencies"], 35 | "groupName": "devDependencies", 36 | "automerge": true 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /scripts/prepublish.js: -------------------------------------------------------------------------------- 1 | if (!process.env.GH_TOKEN) { 2 | throw Error(`A GH_TOKEN environment variable is required to create Github releases.`) 3 | } 4 | -------------------------------------------------------------------------------- /tools/rig/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/__tests__ 3 | dist/buildinfo.json 4 | .eslintcache 5 | .eslintrc.js 6 | .rush 7 | .heft 8 | *.log 9 | tsconfig.json 10 | config/jest.config.json 11 | jest.config.js 12 | coverage 13 | 14 | !.eslintrc.js 15 | !tsconfig.json 16 | !jest.config.js 17 | -------------------------------------------------------------------------------- /tools/rig/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false, 4 | arrowParens: 'avoid', 5 | trailingComma: 'es5', 6 | bracketSpacing: false, 7 | endOfLine: 'auto', 8 | printWidth: 120, 9 | } 10 | -------------------------------------------------------------------------------- /tools/rig/badges.js: -------------------------------------------------------------------------------- 1 | const {EOL} = require('os') 2 | const {getRushJson} = require('.') 3 | 4 | /** @type {import('../../packages/eslint-plugin-codegen').Preset<{}>} */ 5 | module.exports = params => { 6 | const {rush} = getRushJson() 7 | const matchedProject = rush.projects.find(p => params.meta.filename.replace(/\\/g, '/').includes(p.projectFolder)) 8 | const relativePath = matchedProject.projectFolder 9 | const leafPkg = {name: matchedProject.packageName} 10 | 11 | const {url: repo, defaultBranch: branch} = rush.repository 12 | const codecovUrl = repo.replace('github.com', 'codecov.io/gh') 13 | 14 | return ` 15 | [![Node CI](${repo}/workflows/Node%20CI/badge.svg)](${repo}/actions?query=workflow%3A%22Node+CI%22) 16 | [![codecov](${codecovUrl}/branch/${branch}/graph/badge.svg)](${codecovUrl}/tree/${branch}/${relativePath}) 17 | [![npm version](https://badge.fury.io/js/${leafPkg.name}.svg)](https://npmjs.com/package/${leafPkg.name}) 18 | ` 19 | .replace(/\r?\n +/g, EOL) 20 | .trim() 21 | } 22 | -------------------------------------------------------------------------------- /tools/rig/init.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const os = require('os') 5 | const sortPackageJson = require('sort-package-json') 6 | 7 | // eslint-disable-next-line complexity 8 | exports.init = () => { 9 | const helperPkgJson = require('./package.json') 10 | const {rush, directory: rootDir} = require('.').getRushJson() 11 | 12 | const cwd = process.cwd() 13 | const pkgJsonPath = path.join(cwd, 'package.json') 14 | const oldContent = fs.existsSync(pkgJsonPath) ? fs.readFileSync(pkgJsonPath).toString() : '{}' 15 | const pkgJson = JSON.parse(oldContent) 16 | 17 | const relativePath = process 18 | .cwd() 19 | .replace(rootDir + '\\', '') 20 | .replace(/\\/g, '/') 21 | 22 | pkgJson.name = pkgJson.name || path.basename(cwd) 23 | pkgJson.version = pkgJson.version || '0.0.1' 24 | pkgJson.main = pkgJson.main || 'dist/index.js' 25 | pkgJson.types = pkgJson.types || 'dist/index.d.ts' 26 | pkgJson.repository = { 27 | type: 'git', 28 | url: `${rush.repository.url}.git`.replace(/\.git\.git$/, '.git'), 29 | directory: relativePath, 30 | } 31 | pkgJson.scripts = pkgJson.scripts || {} 32 | pkgJson.scripts.clean = pkgJson.scripts.clean || 'rig rimraf dist' 33 | pkgJson.scripts.prebuild = pkgJson.scripts.prebuild || 'npm run clean' 34 | pkgJson.scripts.build = pkgJson.scripts.build || 'rig tsc -p .' 35 | pkgJson.scripts.lint = pkgJson.scripts.lint || 'rig eslint --cache .' 36 | pkgJson.scripts.test = pkgJson.scripts.test || 'rig jest' 37 | pkgJson.scripts.prepack = 'rig permalink' 38 | pkgJson.scripts.postpack = 'rig unpermalink' 39 | pkgJson.devDependencies = pkgJson.devDependencies || {} 40 | if (pkgJson.name !== helperPkgJson.name) { 41 | pkgJson.devDependencies[helperPkgJson.name] = pkgJson.devDependencies[helperPkgJson.name] || helperPkgJson.version 42 | } 43 | 44 | const stringify = obj => JSON.stringify(obj, null, 2) + os.EOL 45 | 46 | const newContent = stringify(sortPackageJson(pkgJson)) 47 | if (newContent !== oldContent) { 48 | fs.writeFileSync(pkgJsonPath, newContent, 'utf8') 49 | } 50 | 51 | const eslintrcPath = path.join(process.cwd(), '.eslintrc.js') 52 | if (!fs.existsSync(eslintrcPath)) { 53 | fs.writeFileSync(eslintrcPath, `module.exports = require('${helperPkgJson.name}/.eslintrc')${os.EOL}`, 'utf8') 54 | } 55 | 56 | const jestConfigPath = path.join(cwd, 'jest.config.js') 57 | if (!fs.existsSync(jestConfigPath)) { 58 | fs.writeFileSync(jestConfigPath, `module.exports = require('${helperPkgJson.name}/jest.config')${os.EOL}`, 'utf8') 59 | } 60 | 61 | const tsconfigPath = path.join(cwd, 'tsconfig.json') 62 | if (!fs.existsSync(tsconfigPath)) { 63 | fs.writeFileSync( 64 | tsconfigPath, 65 | stringify({ 66 | extends: `./node_modules/${helperPkgJson.name}/tsconfig.json`, 67 | compilerOptions: { 68 | rootDir: 'src', 69 | outDir: 'dist', 70 | tsBuildInfoFile: 'dist/buildinfo.json', 71 | typeRoots: [`node_modules/${helperPkgJson.name}/node_modules/@types`], 72 | }, 73 | exclude: ['node_modules', 'dist'], 74 | }), 75 | 'utf8' 76 | ) 77 | } 78 | 79 | const npmIgnorePath = path.join(cwd, '.npmignore') 80 | const content = ` 81 | node_modules 82 | **/__tests__ 83 | dist/buildinfo.json 84 | .eslintcache 85 | .eslintrc.js 86 | .rush 87 | .heft 88 | *.log 89 | tsconfig.json 90 | config/jest.config.json 91 | jest.config.js 92 | coverage 93 | ` 94 | .trim() 95 | .replace(/\r?\n +/g, os.EOL) 96 | const exists = fs.existsSync(npmIgnorePath) 97 | if (exists && !fs.readFileSync(npmIgnorePath).toString().startsWith(content)) { 98 | throw Error(`${npmIgnorePath} is expected to include this content:\n\n${content}`) 99 | } else if (!exists) { 100 | fs.writeFileSync(npmIgnorePath, content + os.EOL) 101 | } 102 | 103 | const readmePath = path.join(cwd, 'readme.md') 104 | if (!fs.existsSync(readmePath) && !fs.existsSync(path.join(cwd, 'README.md'))) { 105 | fs.writeFileSync(readmePath, `# ${pkgJson.name}${os.EOL}${os.EOL}${pkgJson.description || ''}`.trim(), 'utf8') 106 | } 107 | } 108 | 109 | if (require.main === module) { 110 | exports.init() 111 | } 112 | -------------------------------------------------------------------------------- /tools/rig/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.tsx?$': `${__dirname}/node_modules/ts-jest`, 4 | }, 5 | globals: { 6 | 'ts-jest': { 7 | diagnostics: false, 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | testMatch: ['**/*/*.test.ts'], 12 | collectCoverageFrom: ['src/*.{ts,js}', 'src/**/*.{ts,js}', '!**/__tests__/**/fixtures/**'], 13 | } 14 | -------------------------------------------------------------------------------- /tools/rig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mmkal/rig", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "All-in-one dev dependency - designed for packages within this repo, but in theory could be used externally too", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mmkal/ts.git", 9 | "directory": "tools/rig" 10 | }, 11 | "main": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "bin": { 14 | "init": "./init.js", 15 | "rig": "./rig.js" 16 | }, 17 | "scripts": { 18 | "prebuild": "npm run clean", 19 | "build": "node rig tsc -p .", 20 | "clean": "node rig rimraf dist", 21 | "lint": "node rig eslint --cache .", 22 | "test": "node rig jest" 23 | }, 24 | "dependencies": { 25 | "@actions/github": "4.0.0", 26 | "@types/jest": "26.0.19", 27 | "@types/js-yaml": "3.12.5", 28 | "@types/node": "14.14.14", 29 | "@typescript-eslint/eslint-plugin": "4.10.0", 30 | "@typescript-eslint/parser": "4.10.0", 31 | "check-clean": "0.3.0", 32 | "concurrently": "5.3.0", 33 | "eslint": "7.15.0", 34 | "eslint-config-xo": "0.33.1", 35 | "eslint-config-xo-typescript": "0.37.0", 36 | "eslint-plugin-codegen": "0.14.4", 37 | "eslint-plugin-import": "2.22.1", 38 | "eslint-plugin-jest": "24.1.3", 39 | "eslint-plugin-prettier": "3.3.0", 40 | "eslint-plugin-unicorn": "21.0.0", 41 | "eson-parser": "0.0.5", 42 | "find-up": "5.0.0", 43 | "jest": "26.6.3", 44 | "js-yaml": "^3.14.0", 45 | "lodash": "^4.17.15", 46 | "prettier": "2.2.1", 47 | "rimraf": "3.0.2", 48 | "sort-package-json": "1.48.0", 49 | "ts-jest": "26.4.4", 50 | "ts-loader": "8.0.12", 51 | "typescript": "4.1.3", 52 | "webpack": "4.44.2", 53 | "webpack-cli": "3.3.12" 54 | }, 55 | "devDependencies": { 56 | "@microsoft/rush-lib": "5.35.2", 57 | "@types/eslint": "7.2.6", 58 | "@types/lodash": "4.14.165" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tools/rig/permalink.js: -------------------------------------------------------------------------------- 1 | const {getRushJson} = require('.') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const childProcess = require('child_process') 5 | 6 | const permalinkable = ['readme.md'] 7 | 8 | const gitHash = childProcess.execSync('git rev-parse --short HEAD').toString().trim() 9 | 10 | permalinkable.forEach(file => { 11 | const {rush} = getRushJson() 12 | const matchedProject = rush.projects.find(p => 13 | path.join(process.cwd(), file).replace(/\\/g, '/').includes(p.projectFolder) 14 | ) 15 | const repo = rush.repository.url 16 | 17 | const tag = gitHash 18 | const content = fs.readFileSync(file).toString() 19 | const withPermaLinks = content.replace(/\[(.*?)]\((.*?)\)/g, (match, text, href) => { 20 | if ( 21 | href.startsWith('#') || 22 | href.startsWith('http://') || 23 | href.startsWith('https://') || 24 | href.includes('../') || 25 | href.match(/\s/) 26 | ) { 27 | return match 28 | } 29 | 30 | const isImage = href.endsWith('.jpg') || href.endsWith('.png') || href.endsWith('.gif') 31 | const baseURL = isImage ? repo.replace('github.com', 'raw.githubusercontent.com') : `${repo}/tree` 32 | const permalinkedHref = 33 | `${baseURL}/${encodeURIComponent(tag)}/${matchedProject.projectFolder}/` + href.replace(/^\.\//, '') 34 | 35 | return `[${text}](${permalinkedHref})` 36 | }) 37 | if (withPermaLinks !== content) { 38 | fs.writeFileSync(file + '.bak', content, 'utf8') 39 | fs.writeFileSync(file, withPermaLinks, 'utf8') 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /tools/rig/readme.md: -------------------------------------------------------------------------------- 1 | # @mmkal/rig 2 | 3 | A bundle with jest, eslint and tsconfig presets/dependencies/configs/passthrough bin scripts exposed. 4 | 5 | These configs are very opinionated, and a work in progress. They make sense for me to use because I can change them at any time. They likely don't make sense for you to use, unless you are me. 6 | 7 | Usage (note - these instructions assume you're using pnpm in a monorepo, but they should also work with a regular npm single-package repo. yarn/lerna monorepos with hoisting enabled may differ slightly, since hoisting means node_modules layout can vary): 8 | 9 | ```bash 10 | pnpm install --save-dev @mmkal/rig 11 | ``` 12 | 13 | ## package.json 14 | 15 | Use the passthrough bin script `run` in package.json to access `tsc` and `eslint`: 16 | 17 | ```json5 18 | { 19 | "scripts": { 20 | "build": "run tsc -p .", 21 | "lint": "run eslint --cache ." 22 | } 23 | } 24 | ``` 25 | 26 | ## .eslintrc.js 27 | 28 | ```js 29 | module.exports = require('@mmkal/rig/.eslintrc') 30 | ``` 31 | 32 | ## tsconfig.json 33 | 34 | ```json5 35 | { 36 | "extends": "./node_modules/@mmkal/rig/tsconfig.json", 37 | "compilerOptions": { 38 | "rootDir": "src", 39 | "outDir": "dist", 40 | "tsBuildInfoFile": "dist/buildinfo.json", 41 | // convenient abstraction, however leaky: the helper package exposes node and jest types 42 | // but they're tucked away in a nested node_modules folder. This lets them be used 43 | "typeRoots": ["node_modules/@mmkal/rig/node_modules/@types"] 44 | }, 45 | "exclude": ["node_modules", "dist"] 46 | } 47 | ``` 48 | 49 | ## jest.config.js 50 | 51 | ```js 52 | module.exports = require('@mmkal/rig/jest.config') 53 | ``` 54 | 55 | ## webpack.config.js 56 | 57 | Webpack preferred over parcel. It's customisable (in most projects, intimidatingly so), but the rig package attempts to abstract that away as much as possible. This will give you a config for a bundled commonjs module: 58 | 59 | ```js 60 | module.exports = require('@mmkal/rig/webpack.config').with(__filename) 61 | ``` 62 | 63 | That should be good as a serverless function entrypoint or similar. For web/a cli program, you'd have to use `...` to extend it. Or maybe, eventually this library should export a few different config options (while trying to avoid the inner platform effect). 64 | -------------------------------------------------------------------------------- /tools/rig/rig.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** @type {string[]} */ 3 | const argv = process.argv 4 | const [command] = argv.splice(2, 1) 5 | const commands = { 6 | tsc: 'typescript/bin/tsc', 7 | eslint: 'eslint/bin/eslint', 8 | jest: 'jest/bin/jest', 9 | rimraf: 'rimraf/bin', 10 | concurrently: 'concurrently/bin/concurrently', 11 | webpack: 'webpack-cli/bin/cli', 12 | 'sort-package-json': 'sort-package-json/cli', 13 | permalink: './permalink', 14 | unpermalink: './unpermalink', 15 | } 16 | if (command === 'jest') { 17 | // hack: workaround https://github.com/facebook/jest/issues/5064 to avoid "completed with warnings" messages 18 | Object.defineProperty(process, 'stderr', {get: () => process.stdout}) 19 | } 20 | 21 | require(commands[command]) 22 | -------------------------------------------------------------------------------- /tools/rig/src/__tests__/rush.test.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path' 2 | import {getChangeLog, getRushJson} from '../rush' 3 | 4 | test('snapshot rush.json keys', () => { 5 | expect(Object.keys(getRushJson().rush)).toMatchInlineSnapshot(` 6 | Array [ 7 | "$schema", 8 | "rushVersion", 9 | "pnpmVersion", 10 | "pnpmOptions", 11 | "nodeSupportedVersionRange", 12 | "ensureConsistentVersions", 13 | "gitPolicy", 14 | "repository", 15 | "eventHooks", 16 | "variants", 17 | "projects", 18 | ] 19 | `) 20 | }) 21 | 22 | test('snapshot changelog.json', () => { 23 | const {directory} = getRushJson() 24 | const exampleChangelog = getChangeLog(join(directory, 'packages/eslint-plugin-codegen'))! 25 | expect(exampleChangelog).toMatchObject({name: 'eslint-plugin-codegen'}) 26 | expect(Object.keys(exampleChangelog)).toMatchInlineSnapshot(` 27 | Array [ 28 | "name", 29 | "entries", 30 | ] 31 | `) 32 | }) 33 | 34 | test('non-existent changelog', () => { 35 | expect(getChangeLog('this/path/does/not/exist')).toBeUndefined() 36 | }) 37 | -------------------------------------------------------------------------------- /tools/rig/src/__tests__/util.ts: -------------------------------------------------------------------------------- 1 | import * as jsYaml from 'js-yaml' 2 | 3 | export const addYamlSerializer = () => { 4 | expect.addSnapshotSerializer({ 5 | test: val => typeof val !== 'function', 6 | print: val => jsYaml.safeDump(val).trim(), 7 | }) 8 | 9 | expect.addSnapshotSerializer({ 10 | test: jest.isMockFunction, 11 | print: (val: any) => jsYaml.safeDump({mock: true, calls: val.mock.calls}).trim(), 12 | }) 13 | } 14 | 15 | export type PartialMock = { 16 | [K in keyof T]+?: T[K] extends (...args: infer A) => infer R 17 | ? jest.Mock 18 | : T[K] extends Array 19 | ? Array> 20 | : T[K] extends string | boolean | number | symbol | bigint 21 | ? T[K] 22 | : PartialMock 23 | } 24 | 25 | export const buildMockParams = (_fn: (...args: [Arg]) => unknown) => (partial: PartialMock) => 26 | partial as Arg & typeof partial 27 | -------------------------------------------------------------------------------- /tools/rig/src/github-release.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process' 2 | import {getChangeLog, getRushJson} from './rush' 3 | import * as lodash from 'lodash' 4 | import type {Context} from '@actions/github/lib/context' 5 | import type {GitHub} from '@actions/github/lib/utils' 6 | import type {IChangelog} from '@microsoft/rush-lib/lib/api/Changelog' 7 | import {join} from 'path' 8 | 9 | interface CreateReleaseParams { 10 | context: Context 11 | github: InstanceType 12 | logger?: Console 13 | } 14 | 15 | /** 16 | * Reads tags pointing at the current git head, compares them with CHANGELOG.json files for each rush project, 17 | * and creates a GitHub release accordingly. This assumes that the git head is a commit created by `rush publish`. 18 | * @param param an object consisting of `context` and `github` values, as supplied by the `github-script` action. 19 | */ 20 | export const createGitHubRelease = async ({context, github, logger = console}: CreateReleaseParams) => { 21 | const tags: string[] = 22 | context.payload?.inputs?.tags?.split(',') || 23 | childProcess 24 | .execSync('git tag --points-at HEAD') 25 | .toString() 26 | .split('\n') 27 | .map(t => t.trim()) 28 | .filter(Boolean) 29 | 30 | const {rush, directory} = getRushJson() 31 | 32 | const allReleaseParams = lodash 33 | .chain(rush.projects) 34 | .flatMap(project => tags.map(tag => ({tag, project}))) 35 | .map(({project, tag}): null | NonNullable[0]> => { 36 | const changelog = getChangeLog(join(directory, project.projectFolder)) 37 | if (!changelog) { 38 | return null 39 | } 40 | const {name, body} = getReleaseContent(changelog, tag) 41 | if (!body) { 42 | return null 43 | } 44 | 45 | return { 46 | owner: context.repo.owner, 47 | repo: context.repo.repo, 48 | tag_name: tag, 49 | name, 50 | body, 51 | } 52 | }) 53 | .compact() 54 | .map(p => { 55 | const inputs = context?.payload?.inputs 56 | return { 57 | ...p, 58 | body: [inputs?.header, p.body, inputs?.footer].filter(Boolean).join('\n\n'), 59 | } 60 | }) 61 | .value() 62 | 63 | logger.info('releasing', allReleaseParams) 64 | for (const params of allReleaseParams) { 65 | try { 66 | // eslint-disable-next-line no-await-in-loop 67 | const r = await github.repos.createRelease(params) 68 | logger.info('released', r.data) 69 | } catch (e: unknown) { 70 | logger.error('failed to release', e) 71 | } 72 | } 73 | } 74 | 75 | export const getReleaseContent = (changelog: IChangelog, tag: string) => { 76 | const relevantEntries = changelog.entries.filter(e => e.tag === tag) 77 | 78 | const versions = lodash.uniq(relevantEntries.map(e => 'v' + e.version)) 79 | const name = `${changelog.name} ${versions.join(', ')}`.trim() 80 | 81 | const ordering: Record = { 82 | major: 0, 83 | minor: 1, 84 | } 85 | 86 | const body = lodash 87 | .chain(relevantEntries) 88 | .flatMap(({comments, ...e}) => 89 | Object.keys(comments).map(type => ({...e, comments, type: type as keyof typeof comments})) 90 | ) 91 | .map(({comments, ...e}) => ({...e, comment: comments[e.type]})) 92 | .flatMap(({comment, ...e}) => comment!.map(c => ({...e, ...c}))) 93 | .map(e => ({ 94 | ...e, 95 | bullet: ['-', e.comment.replace(/\n/g, '\n '), e.author && `(@${e.author})`, e.commit].filter(Boolean).join(' '), 96 | })) 97 | .groupBy(e => e.type) 98 | .entries() 99 | .sortBy(([type]) => ordering[type] ?? 2) 100 | .map(([type, group]) => [`## ${type} changes\n`, ...group.map(c => c.bullet)].join('\n')) 101 | .join('\n\n') 102 | .value() 103 | .trim() 104 | 105 | return {name, body} 106 | } 107 | -------------------------------------------------------------------------------- /tools/rig/src/index.ts: -------------------------------------------------------------------------------- 1 | // codegen:start {preset: barrel} 2 | export * from './github-release' 3 | export * from './rush' 4 | // codegen:end 5 | -------------------------------------------------------------------------------- /tools/rig/src/rush.ts: -------------------------------------------------------------------------------- 1 | import * as ESON from 'eson-parser' // Fork of json5, probably wouldn't use it if it weren't mine. Should be easy to switch to json5 if there are any problems. https://github.com/json5/json5/issues/190#issuecomment-636935746 2 | import * as findUp from 'find-up' 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | import type {IChangelog} from '@microsoft/rush-lib/lib/api/Changelog' 6 | 7 | export interface RushJson { 8 | repository: { 9 | url: string 10 | defaultBranch: string 11 | defaultRemote: string 12 | } 13 | projects: Array<{ 14 | packageName: string 15 | projectFolder: string 16 | }> 17 | } 18 | 19 | export const getRushJson = (): {directory: string; rush: RushJson} => { 20 | const rushJsonPath = findUp.sync('rush.json')! 21 | return { 22 | directory: path.dirname(rushJsonPath), 23 | rush: ESON.parse(fs.readFileSync(rushJsonPath).toString()), 24 | } 25 | } 26 | 27 | export const getChangeLog = (projectFolder: string): IChangelog | undefined => { 28 | const changeLogPath = path.join(projectFolder, 'CHANGELOG.json') 29 | if (!fs.existsSync(changeLogPath)) { 30 | return undefined 31 | } 32 | return ESON.parse(fs.readFileSync(changeLogPath).toString()) 33 | } 34 | -------------------------------------------------------------------------------- /tools/rig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "incremental": true, 11 | "tsBuildInfoFile": "dist/buildinfo.json", 12 | "outDir": "dist", 13 | "rootDir": "src" 14 | }, 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /tools/rig/unpermalink.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const permalinkable = ['readme.md'] 5 | 6 | permalinkable.forEach(file => { 7 | const filepath = path.join(process.cwd(), file) 8 | const backupPath = filepath + '.bak' 9 | 10 | if (fs.existsSync(backupPath)) { 11 | fs.renameSync(backupPath, filepath) 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /tools/rig/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | /** @type {(dirname: string) => import('webpack').Configuration} */ 5 | const get = dirname => ({ 6 | entry: './src/index.ts', 7 | mode: 'none', 8 | output: { 9 | filename: 'index.js', 10 | path: path.resolve(dirname, 'dist'), 11 | libraryTarget: 'commonjs2', 12 | }, 13 | optimization: { 14 | minimize: false, 15 | }, 16 | target: 'node', 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | // use require.resolve so this config can be used from other packages 22 | use: [{loader: require.resolve('ts-loader'), options: {transpileOnly: true}}], 23 | exclude: /node_modules/, 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: ['.tsx', '.ts', '.js'], 29 | }, 30 | plugins: [ 31 | new webpack.ContextReplacementPlugin( 32 | // avoid Critical dependency: the request of a dependency is an expression 33 | // any-promise dynamic loads some things that aren't used because global.Promise exists 34 | /any-promise/, 35 | context => 36 | context.dependencies.forEach(d => { 37 | d.critical = false 38 | }) 39 | ), 40 | ], 41 | }) 42 | 43 | module.exports = Object.assign(get(__dirname), {with: get}) 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "incremental": true 11 | }, 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | --------------------------------------------------------------------------------