├── .gitattributes ├── .github └── workflows │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json5 ├── .vscode ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── beachball.config.js ├── lage.config.js ├── package.json ├── packages ├── grapher │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ │ └── grapher.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── commands │ │ │ └── depsCommand.ts │ │ └── index.ts │ └── tsconfig.json └── workspace-tools │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── README.md │ ├── etc │ └── workspace-tools.api.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── __tests__ │ │ ├── dependencies.test.ts │ │ ├── getChangedPackages.test.ts │ │ ├── getDefaultBranch.test.ts │ │ ├── getDefaultRemote.test.ts │ │ ├── getPackagesByFiles.test.ts │ │ ├── getRepositoryName.test.ts │ │ ├── getScopedPackages.test.ts │ │ ├── getWorkspaceRoot.test.ts │ │ ├── getWorkspaces.test.ts │ │ ├── graph.test.ts │ │ ├── lockfile.test.ts │ │ └── queryLockFile.test.ts │ ├── dependencies │ │ ├── index.ts │ │ └── transitiveDeps.ts │ ├── getPackageInfos.ts │ ├── getPackagePaths.ts │ ├── git │ │ ├── getDefaultRemote.ts │ │ ├── getDefaultRemoteBranch.ts │ │ ├── getRepositoryName.ts │ │ ├── git.ts │ │ ├── gitUtilities.ts │ │ └── index.ts │ ├── graph │ │ ├── createDependencyMap.ts │ │ ├── createPackageGraph.ts │ │ ├── getPackageDependencies.ts │ │ └── index.ts │ ├── index.ts │ ├── infoFromPackageJson.ts │ ├── isCachingEnabled.ts │ ├── lockfile │ │ ├── index.ts │ │ ├── nameAtVersion.ts │ │ ├── parseBerryLock.ts │ │ ├── parseNpmLock.ts │ │ ├── parsePnpmLock.ts │ │ ├── queryLockFile.ts │ │ ├── readYaml.ts │ │ └── types.ts │ ├── logging.ts │ ├── paths.ts │ ├── scope.ts │ ├── types │ │ ├── PackageGraph.ts │ │ ├── PackageInfo.ts │ │ └── WorkspaceInfo.ts │ └── workspaces │ │ ├── WorkspaceManager.ts │ │ ├── findWorkspacePath.ts │ │ ├── getAllPackageJsonFiles.ts │ │ ├── getChangedPackages.ts │ │ ├── getPackagesByFiles.ts │ │ ├── getWorkspacePackageInfo.ts │ │ ├── getWorkspacePackagePaths.ts │ │ ├── getWorkspaceRoot.ts │ │ ├── getWorkspaces.ts │ │ ├── implementations │ │ ├── getWorkspaceManagerAndRoot.ts │ │ ├── getWorkspaceUtilities.ts │ │ ├── index.ts │ │ ├── lerna.ts │ │ ├── npm.ts │ │ ├── packageJsonWorkspaces.ts │ │ ├── pnpm.ts │ │ ├── rush.ts │ │ └── yarn.ts │ │ └── listOfWorkspacePackageNames.ts │ └── tsconfig.json ├── renovate.json5 ├── scripts ├── api-extractor │ ├── api-extractor.base.json │ └── index.js ├── bin │ └── ws-tools-scripts.js ├── jest │ ├── __fixtures__ │ │ ├── basic-2 │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── basic-pnpm │ │ │ ├── .npmrc │ │ │ ├── package.json │ │ │ └── pnpm-lock.yaml │ │ ├── basic-without-lock-file │ │ │ └── package.json │ │ ├── basic-yarn-2 │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── basic-yarn │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── basic │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── monorepo-globby │ │ │ ├── individual │ │ │ │ └── package.json │ │ │ ├── package.json │ │ │ ├── packages │ │ │ │ ├── package-a │ │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ │ └── package.json │ │ │ └── yarn.lock │ │ ├── monorepo-lerna-npm │ │ │ ├── lerna.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── packages │ │ │ │ ├── package-a │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ └── package.json │ │ ├── monorepo-nested │ │ │ └── monorepo │ │ │ │ ├── package.json │ │ │ │ ├── packages │ │ │ │ ├── package-a │ │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ │ └── package.json │ │ │ │ └── yarn.lock │ │ ├── monorepo-npm-unsupported │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── packages │ │ │ │ ├── package-a │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ └── package.json │ │ ├── monorepo-npm │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── packages │ │ │ │ ├── package-a │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ └── package.json │ │ ├── monorepo-pnpm │ │ │ ├── .npmrc │ │ │ ├── package.json │ │ │ ├── packages │ │ │ │ ├── package-a │ │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ │ └── package.json │ │ │ ├── pnpm-lock.yaml │ │ │ └── pnpm-workspace.yaml │ │ ├── monorepo-rush-pnpm │ │ │ ├── common │ │ │ │ └── config │ │ │ │ │ └── rush │ │ │ │ │ ├── command-line.json │ │ │ │ │ └── pnpm-lock.yaml │ │ │ ├── packages │ │ │ │ ├── package-a │ │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ │ └── package.json │ │ │ └── rush.json │ │ ├── monorepo-rush-yarn │ │ │ ├── common │ │ │ │ └── config │ │ │ │ │ └── rush │ │ │ │ │ ├── command-line.json │ │ │ │ │ └── yarn.lock │ │ │ ├── packages │ │ │ │ ├── package-a │ │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ │ └── package.json │ │ │ ├── rush.json │ │ │ └── yarn.lock │ │ ├── monorepo-shorthand │ │ │ ├── individual │ │ │ │ └── package.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── packages │ │ │ │ ├── package-a │ │ │ │ └── package.json │ │ │ │ └── package-b │ │ │ │ └── package.json │ │ └── monorepo │ │ │ ├── package.json │ │ │ ├── packages │ │ │ ├── package-a │ │ │ │ └── package.json │ │ │ └── package-b │ │ │ │ └── package.json │ │ │ └── yarn.lock │ ├── debugTests.js │ ├── jest.config.js │ ├── setupFixture.ts │ └── setupTests.ts ├── package.json ├── tsconfig.base.json └── tsconfig.json ├── typedoc.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.json linguist-language=JSON-with-Comments -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: PR 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | 10 | permissions: {} 11 | 12 | concurrency: 13 | # For PRs, use the ref (branch) in the concurrency group so that new pushes cancel any old runs. 14 | # For pushes to main, ideally we wouldn't set a concurrency group, but github actions doesn't 15 | # support conditional blocks of settings, so we use the SHA so the "group" is unique. 16 | group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 25 | 26 | - name: Install Node.js 27 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 28 | with: 29 | node-version-file: .nvmrc 30 | 31 | - run: yarn --frozen-lockfile 32 | 33 | - run: yarn format:check 34 | 35 | - run: yarn build 36 | 37 | # checkchange must come after build in case beachball ends up using the local workspace-tools 38 | # (this will happen when beachball depends on a workspace-tools version which is compatible 39 | # with the local version) 40 | - run: yarn checkchange 41 | 42 | - run: yarn build:docs 43 | 44 | - run: yarn test 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | # release daily 5 | # https://crontab-generator.org/ 6 | schedule: 7 | - cron: "0 8 * * *" 8 | # or on manual trigger 9 | workflow_dispatch: 10 | 11 | permissions: {} 12 | 13 | # Only run one release at a time to avoid duplicate attempts to publish particular versions. 14 | # To avoid backups after multiple pushes in rapid succession, the prerelease job below emulates 15 | # batching (which github actions don't support) by skipping the release job if a newer run is pending. 16 | # 17 | # (There's an option "cancel-in-progress" to cancel in-progress workflows upon a new request, but 18 | # that's not safe because it could potentially cause a job to be cancelled in the middle of the 19 | # actual npm publish step, leaving things in an inconsistent state.) 20 | concurrency: 21 | group: release-${{ github.ref }} 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | 27 | # This environment contains secrets needed for publishing 28 | environment: release 29 | 30 | steps: 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 32 | with: 33 | # Don't save creds in the git config (so it's easier to override later) 34 | persist-credentials: false 35 | 36 | - name: Install Node.js 37 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 38 | with: 39 | node-version-file: .nvmrc 40 | 41 | - run: yarn --frozen-lockfile 42 | 43 | - run: yarn build 44 | - run: yarn build:docs 45 | 46 | - run: yarn test 47 | 48 | - name: Publish package 49 | run: | 50 | git config user.email "kchau@microsoft.com" 51 | git config user.name "Ken Chau" 52 | 53 | # Get the existing remote URL without creds, and use a trap (like try/finally) 54 | # to restore it after this step finishes 55 | trap "git remote set-url origin '$(git remote get-url origin)'" EXIT 56 | 57 | # Add a token to the remote URL for auth during release 58 | git remote set-url origin "https://$REPO_PAT@github.com/$GITHUB_REPOSITORY" 59 | 60 | yarn release -y -n "$NPM_AUTHTOKEN" 61 | env: 62 | NPM_AUTHTOKEN: ${{ secrets.NPM_AUTHTOKEN }} 63 | REPO_PAT: ${{ secrets.REPO_PAT }} 64 | 65 | - name: Upload docs artifact 66 | uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 67 | with: 68 | path: docs 69 | 70 | deploy: 71 | name: Deploy to GitHub Pages 72 | needs: build 73 | 74 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 75 | permissions: 76 | pages: write # to deploy to Pages 77 | id-token: write # to verify the deployment originates from an appropriate source 78 | 79 | # Deploy to the github-pages environment 80 | environment: 81 | name: github-pages 82 | url: ${{ steps.deployment.outputs.page_url }} 83 | 84 | runs-on: ubuntu-latest 85 | steps: 86 | - name: Deploy to GitHub Pages 87 | id: deployment 88 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | docs/ 3 | lib/ 4 | temp/ 5 | .rush/ 6 | **/common/scripts/ 7 | .DS_Store 8 | *.log* 9 | **/__fixtures__/*/node_modules/ 10 | **/__fixtures__/*/.yarn/cache/ 11 | install-state.gz 12 | /package-lock.json 13 | !**/__fixtures__/**/package-lock.json -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | lib/ 3 | node_modules/ 4 | temp/ 5 | .rush/ 6 | **/common/scripts/ 7 | 8 | yarn.lock 9 | pnpm-lock.* 10 | package-lock.json 11 | 12 | change 13 | CHANGELOG.json 14 | CHANGELOG.md 15 | SECURITY.md 16 | 17 | .DS_Store 18 | *.log* 19 | *.api.md 20 | -------------------------------------------------------------------------------- /.prettierrc.json5: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "singleQuote": false, 5 | "printWidth": 120, 6 | "overrides": [ 7 | { 8 | // format .json5 files as jsonc for VS Code support 9 | "files": ["*.json5"], 10 | "options": { 11 | "parser": "json" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug 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": "Debug current open test", 11 | "program": "${workspaceRoot}/scripts/jest/debugTests.js", 12 | "cwd": "${fileDirname}", 13 | "runtimeArgs": ["--nolazy", "--inspect"], 14 | "runtimeExecutable": null, 15 | "args": ["${fileBasenameNoExtension}"], 16 | "sourceMaps": true, 17 | "outputCapture": "std", 18 | "console": "integratedTerminal" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.trimAutoWhitespace": true, 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 2, 6 | "files.trimTrailingWhitespace": true, 7 | "search.exclude": { 8 | "**/node_modules": true, 9 | "**/lib": true 10 | }, 11 | "files.associations": { 12 | // VS Code doesn't have json5 support, so handle .json5 files as jsonc. 13 | // Note that Prettier must also be configured to format *.json5 as jsonc 14 | // (since json5 allows things like unquoted keys that jsonc doesn't). 15 | "*.json5": "jsonc", 16 | "api-extractor*.json": "jsonc" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # workspace-tools monorepo 2 | 3 | Please see the [`workspace-tools` README](./packages/workspace-tools/README.md) for more information. 4 | 5 | ## Contributing 6 | 7 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 8 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 9 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 10 | 11 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 12 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 13 | provided by the bot. You will only need to do this once across all repos using our CLA. 14 | 15 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 16 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 17 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /beachball.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('beachball').BeachballConfig} */ 4 | const config = { 5 | access: "public", 6 | disallowedChangeTypes: ["major"], 7 | groupChanges: true, 8 | scope: ["!**/__fixtures__/**"], 9 | ignorePatterns: ["**/jest.config.js", "**/src/__fixtures__/**", "**/src/__tests__/**"], 10 | }; 11 | module.exports = config; 12 | -------------------------------------------------------------------------------- /lage.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pipeline: { 3 | api: ["build"], 4 | build: ["^build"], 5 | test: ["build"], 6 | }, 7 | npmClient: "yarn", 8 | // These options are sent to `backfill`: https://github.com/microsoft/backfill/blob/master/README.md 9 | cacheOptions: { 10 | // These are relative to the git root, and affects the hash of the cache 11 | // Any of these file changes will invalidate cache 12 | environmentGlob: [".github/workflows/*", "*.js", "package.json"], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ws-tools/monorepo", 3 | "private": true, 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/microsoft/workspace-tools" 9 | }, 10 | "workspaces": [ 11 | "packages/*", 12 | "scripts", 13 | "!scripts/jest/__fixtures__/*" 14 | ], 15 | "scripts": { 16 | "api": "yarn lage api", 17 | "build": "yarn lage build api", 18 | "build:docs": "cd packages/workspace-tools && typedoc --out ../../docs src/index.ts", 19 | "change": "beachball change", 20 | "checkchange": "beachball check", 21 | "format": "prettier --write .", 22 | "format:check": "prettier --check .", 23 | "release": "beachball publish -y", 24 | "test": "yarn lage test" 25 | }, 26 | "devDependencies": { 27 | "@microsoft/api-extractor": "^7.34.9", 28 | "@types/fs-extra": "^11.0.0", 29 | "@types/git-url-parse": "^9.0.1", 30 | "@types/jest": "^29.5.1", 31 | "@types/jju": "^1.4.2", 32 | "@types/js-yaml": "^4.0.5", 33 | "@types/micromatch": "^4.0.2", 34 | "@types/node": "^16.0.0", 35 | "@types/tmp": "^0.2.3", 36 | "@types/yarnpkg__lockfile": "^1.1.5", 37 | "@types/lodash": "^4.14.194", 38 | "beachball": "^2.51.0", 39 | "cross-env": "^7.0.3", 40 | "fs-extra": "^11.0.0", 41 | "jest": "^29.5.0", 42 | "lage": "^2.6.2", 43 | "prettier": "^3.0.0", 44 | "tmp": "^0.2.1", 45 | "ts-jest": "^29.1.0", 46 | "typedoc": "^0.25.2", 47 | "typescript": "~5.2.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/grapher/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ws-tools/grapher", 3 | "entries": [ 4 | { 5 | "date": "Thu, 17 Apr 2025 02:50:51 GMT", 6 | "version": "0.2.10", 7 | "tag": "@ws-tools/grapher_v0.2.10", 8 | "comments": { 9 | "patch": [ 10 | { 11 | "author": "beachball", 12 | "package": "@ws-tools/grapher", 13 | "comment": "Bump workspace-tools to v0.38.4", 14 | "commit": "not available" 15 | } 16 | ] 17 | } 18 | }, 19 | { 20 | "date": "Mon, 14 Apr 2025 22:35:08 GMT", 21 | "version": "0.2.9", 22 | "tag": "@ws-tools/grapher_v0.2.9", 23 | "comments": { 24 | "patch": [ 25 | { 26 | "author": "beachball", 27 | "package": "@ws-tools/grapher", 28 | "comment": "Bump workspace-tools to v0.38.3", 29 | "commit": "not available" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "date": "Mon, 24 Mar 2025 21:48:16 GMT", 36 | "version": "0.2.8", 37 | "tag": "@ws-tools/grapher_v0.2.8", 38 | "comments": { 39 | "patch": [ 40 | { 41 | "author": "beachball", 42 | "package": "@ws-tools/grapher", 43 | "comment": "Bump workspace-tools to v0.38.2", 44 | "commit": "not available" 45 | } 46 | ] 47 | } 48 | }, 49 | { 50 | "date": "Wed, 13 Nov 2024 08:01:48 GMT", 51 | "version": "0.2.7", 52 | "tag": "@ws-tools/grapher_v0.2.7", 53 | "comments": { 54 | "patch": [ 55 | { 56 | "author": "beachball", 57 | "package": "@ws-tools/grapher", 58 | "comment": "Bump workspace-tools to v0.38.1", 59 | "commit": "not available" 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "date": "Sat, 02 Nov 2024 08:01:50 GMT", 66 | "version": "0.2.6", 67 | "tag": "@ws-tools/grapher_v0.2.6", 68 | "comments": { 69 | "patch": [ 70 | { 71 | "author": "beachball", 72 | "package": "@ws-tools/grapher", 73 | "comment": "Bump workspace-tools to v0.38.0", 74 | "commit": "not available" 75 | } 76 | ] 77 | } 78 | }, 79 | { 80 | "date": "Sat, 19 Oct 2024 08:01:45 GMT", 81 | "version": "0.2.5", 82 | "tag": "@ws-tools/grapher_v0.2.5", 83 | "comments": { 84 | "patch": [ 85 | { 86 | "author": "beachball", 87 | "package": "@ws-tools/grapher", 88 | "comment": "Bump workspace-tools to v0.37.0", 89 | "commit": "not available" 90 | } 91 | ] 92 | } 93 | }, 94 | { 95 | "date": "Mon, 11 Dec 2023 23:58:13 GMT", 96 | "version": "0.2.4", 97 | "tag": "@ws-tools/grapher_v0.2.4", 98 | "comments": { 99 | "patch": [ 100 | { 101 | "author": "beachball", 102 | "package": "@ws-tools/grapher", 103 | "comment": "Bump workspace-tools to v0.36.4", 104 | "commit": "not available" 105 | } 106 | ] 107 | } 108 | }, 109 | { 110 | "date": "Wed, 18 Oct 2023 22:14:54 GMT", 111 | "version": "0.2.3", 112 | "tag": "@ws-tools/grapher_v0.2.3", 113 | "comments": { 114 | "patch": [ 115 | { 116 | "author": "beachball", 117 | "package": "@ws-tools/grapher", 118 | "comment": "Bump workspace-tools to v0.36.3", 119 | "commit": "not available" 120 | } 121 | ] 122 | } 123 | }, 124 | { 125 | "date": "Wed, 18 Oct 2023 06:44:38 GMT", 126 | "version": "0.2.2", 127 | "tag": "@ws-tools/grapher_v0.2.2", 128 | "comments": { 129 | "none": [ 130 | { 131 | "author": "elcraig@microsoft.com", 132 | "package": "@ws-tools/grapher", 133 | "commit": "4a42b4e5a83f4260a10d7c592a2f0bd674874b2d", 134 | "comment": "Rename monorepo and scripts package to use `@ws-tools` scope" 135 | } 136 | ], 137 | "patch": [ 138 | { 139 | "author": "beachball", 140 | "package": "@ws-tools/grapher", 141 | "comment": "Bump workspace-tools to v0.36.2", 142 | "commit": "not available" 143 | } 144 | ] 145 | } 146 | }, 147 | { 148 | "date": "Wed, 18 Oct 2023 05:45:05 GMT", 149 | "version": "0.2.1", 150 | "tag": "@ws-tools/grapher_v0.2.1", 151 | "comments": { 152 | "patch": [ 153 | { 154 | "author": "beachball", 155 | "package": "@ws-tools/grapher", 156 | "comment": "Bump workspace-tools to v0.36.1", 157 | "commit": "not available" 158 | } 159 | ] 160 | } 161 | }, 162 | { 163 | "date": "Wed, 18 Oct 2023 05:00:25 GMT", 164 | "version": "0.2.0", 165 | "tag": "@ws-tools/grapher_v0.2.0", 166 | "comments": { 167 | "minor": [ 168 | { 169 | "author": "email not defined", 170 | "package": "@ws-tools/grapher", 171 | "commit": "305519f4f73a112fbb24c6c96e0001e1930bb798", 172 | "comment": "Update dependency commander to v11 (requires Node 16)" 173 | }, 174 | { 175 | "author": "beachball", 176 | "package": "@ws-tools/grapher", 177 | "comment": "Bump workspace-tools to v0.36.0", 178 | "commit": "not available" 179 | } 180 | ] 181 | } 182 | }, 183 | { 184 | "date": "Wed, 18 Oct 2023 04:16:22 GMT", 185 | "version": "0.1.6", 186 | "tag": "@ws-tools/grapher_v0.1.6", 187 | "comments": { 188 | "patch": [ 189 | { 190 | "author": "beachball", 191 | "package": "@ws-tools/grapher", 192 | "comment": "Bump workspace-tools to v0.35.3", 193 | "commit": "not available" 194 | } 195 | ] 196 | } 197 | }, 198 | { 199 | "date": "Thu, 21 Sep 2023 08:01:58 GMT", 200 | "version": "0.1.5", 201 | "tag": "@ws-tools/grapher_v0.1.5", 202 | "comments": { 203 | "patch": [ 204 | { 205 | "author": "dzearing@microsoft.com", 206 | "package": "@ws-tools/grapher", 207 | "commit": "78092edbb6e6af87d5b9ceae75502f5d184e4536", 208 | "comment": "Update README.md" 209 | } 210 | ] 211 | } 212 | }, 213 | { 214 | "date": "Tue, 05 Sep 2023 21:12:43 GMT", 215 | "version": "0.1.4", 216 | "tag": "@ws-tools/grapher_v0.1.4", 217 | "comments": { 218 | "patch": [ 219 | { 220 | "author": "beachball", 221 | "package": "@ws-tools/grapher", 222 | "comment": "Bump workspace-tools to v0.35.2", 223 | "commit": "not available" 224 | } 225 | ] 226 | } 227 | }, 228 | { 229 | "date": "Fri, 01 Sep 2023 08:02:04 GMT", 230 | "version": "0.1.3", 231 | "tag": "@ws-tools/grapher_v0.1.3", 232 | "comments": { 233 | "none": [ 234 | { 235 | "author": "elcraig@microsoft.com", 236 | "package": "@ws-tools/grapher", 237 | "commit": "d72c67ada32cc7580a03ddb6d92aa343c090dfc1", 238 | "comment": "Unpin devDependencies" 239 | } 240 | ] 241 | } 242 | }, 243 | { 244 | "date": "Fri, 01 Sep 2023 01:20:47 GMT", 245 | "tag": "@ws-tools/grapher_v0.1.3", 246 | "version": "0.1.3", 247 | "comments": { 248 | "patch": [ 249 | { 250 | "author": "beachball", 251 | "package": "@ws-tools/grapher", 252 | "comment": "Bump workspace-tools to v0.35.1", 253 | "commit": "c73150422bbe64332daa3876b06ed81fdd363f36" 254 | } 255 | ] 256 | } 257 | }, 258 | { 259 | "date": "Sat, 15 Jul 2023 08:02:42 GMT", 260 | "tag": "@ws-tools/grapher_v0.1.2", 261 | "version": "0.1.2", 262 | "comments": { 263 | "patch": [ 264 | { 265 | "author": "beachball", 266 | "package": "@ws-tools/grapher", 267 | "comment": "Bump workspace-tools to v0.35.0", 268 | "commit": "536eacac8831d04f801446280daf347128be37b6" 269 | } 270 | ] 271 | } 272 | }, 273 | { 274 | "date": "Wed, 17 May 2023 01:25:45 GMT", 275 | "tag": "@ws-tools/grapher_v0.1.1", 276 | "version": "0.1.1", 277 | "comments": { 278 | "patch": [ 279 | { 280 | "author": "elcraig@microsoft.com", 281 | "package": "@ws-tools/grapher", 282 | "commit": "a14af9fe6d1081712a1f84483a38185fc7603cc8", 283 | "comment": "Add `bin` and prevent importing the package" 284 | }, 285 | { 286 | "author": "kchau@microsoft.com", 287 | "package": "@ws-tools/grapher", 288 | "commit": "9311481fb7acc75402900473a38bd6dcc169014d", 289 | "comment": "adding a deps graph tool" 290 | }, 291 | { 292 | "author": "beachball", 293 | "package": "@ws-tools/grapher", 294 | "comment": "Bump workspace-tools to v0.34.6", 295 | "commit": "1ae1a23fe0402f213d6ef2c5fad221a9bacab185" 296 | } 297 | ] 298 | } 299 | } 300 | ] 301 | } 302 | -------------------------------------------------------------------------------- /packages/grapher/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - @ws-tools/grapher 2 | 3 | 4 | 5 | 6 | 7 | ## 0.2.10 8 | 9 | Thu, 17 Apr 2025 02:50:51 GMT 10 | 11 | ### Patches 12 | 13 | - Bump workspace-tools to v0.38.4 14 | 15 | ## 0.2.9 16 | 17 | Mon, 14 Apr 2025 22:35:08 GMT 18 | 19 | ### Patches 20 | 21 | - Bump workspace-tools to v0.38.3 22 | 23 | ## 0.2.8 24 | 25 | Mon, 24 Mar 2025 21:48:16 GMT 26 | 27 | ### Patches 28 | 29 | - Bump workspace-tools to v0.38.2 30 | 31 | ## 0.2.7 32 | 33 | Wed, 13 Nov 2024 08:01:48 GMT 34 | 35 | ### Patches 36 | 37 | - Bump workspace-tools to v0.38.1 38 | 39 | ## 0.2.6 40 | 41 | Sat, 02 Nov 2024 08:01:50 GMT 42 | 43 | ### Patches 44 | 45 | - Bump workspace-tools to v0.38.0 46 | 47 | ## 0.2.5 48 | 49 | Sat, 19 Oct 2024 08:01:45 GMT 50 | 51 | ### Patches 52 | 53 | - Bump workspace-tools to v0.37.0 54 | 55 | ## 0.2.4 56 | 57 | Mon, 11 Dec 2023 23:58:13 GMT 58 | 59 | ### Patches 60 | 61 | - Bump workspace-tools to v0.36.4 62 | 63 | ## 0.2.3 64 | 65 | Wed, 18 Oct 2023 22:14:54 GMT 66 | 67 | ### Patches 68 | 69 | - Bump workspace-tools to v0.36.3 70 | 71 | ## 0.2.2 72 | 73 | Wed, 18 Oct 2023 06:44:38 GMT 74 | 75 | ### Patches 76 | 77 | - Bump workspace-tools to v0.36.2 78 | 79 | ## 0.2.1 80 | 81 | Wed, 18 Oct 2023 05:45:05 GMT 82 | 83 | ### Patches 84 | 85 | - Bump workspace-tools to v0.36.1 86 | 87 | ## 0.2.0 88 | 89 | Wed, 18 Oct 2023 05:00:25 GMT 90 | 91 | ### Minor changes 92 | 93 | - Update dependency commander to v11 (requires Node 16) (email not defined) 94 | - Bump workspace-tools to v0.36.0 95 | 96 | ## 0.1.6 97 | 98 | Wed, 18 Oct 2023 04:16:22 GMT 99 | 100 | ### Patches 101 | 102 | - Bump workspace-tools to v0.35.3 103 | 104 | ## 0.1.5 105 | 106 | Thu, 21 Sep 2023 08:01:58 GMT 107 | 108 | ### Patches 109 | 110 | - Update README.md (dzearing@microsoft.com) 111 | 112 | ## 0.1.4 113 | 114 | Tue, 05 Sep 2023 21:12:43 GMT 115 | 116 | ### Patches 117 | 118 | - Bump workspace-tools to v0.35.2 119 | 120 | ## 0.1.3 121 | 122 | Fri, 01 Sep 2023 01:20:47 GMT 123 | 124 | ### Patches 125 | 126 | - Bump workspace-tools to v0.35.1 127 | 128 | ## 0.1.2 129 | 130 | Sat, 15 Jul 2023 08:02:42 GMT 131 | 132 | ### Patches 133 | 134 | - Bump workspace-tools to v0.35.0 135 | 136 | ## 0.1.1 137 | 138 | Wed, 17 May 2023 01:25:45 GMT 139 | 140 | ### Patches 141 | 142 | - Add `bin` and prevent importing the package (elcraig@microsoft.com) 143 | - adding a deps graph tool (kchau@microsoft.com) 144 | - Bump workspace-tools to v0.34.6 145 | -------------------------------------------------------------------------------- /packages/grapher/README.md: -------------------------------------------------------------------------------- 1 | # @ws-tools/grapher 2 | 3 | ## Generates a list of dependents and dependencies (internal to the workspace) for a package or packages 4 | 5 | For one package 6 | 7 | ``` 8 | npx @ws-tools/grapher deps --scope foo 9 | ``` 10 | 11 | For multiple packages: 12 | 13 | ``` 14 | npx @ws-tools/grapher deps --scope foo --scope bar 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/grapher/bin/grapher.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("../lib/index"); 3 | -------------------------------------------------------------------------------- /packages/grapher/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@ws-tools/scripts/jest/jest.config"); 2 | -------------------------------------------------------------------------------- /packages/grapher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ws-tools/grapher", 3 | "version": "0.2.10", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/workspace-tools" 8 | }, 9 | "bin": { 10 | "grapher": "./bin/grapher.js" 11 | }, 12 | "exports": null, 13 | "files": [ 14 | "lib/!(__*)", 15 | "lib/!(__*)/**/*" 16 | ], 17 | "scripts": { 18 | "build": "tsc", 19 | "start": "tsc -w --preserveWatchOutput", 20 | "test": "jest" 21 | }, 22 | "dependencies": { 23 | "commander": "^11.1.0", 24 | "workspace-tools": "^0.38.4" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^16.0.0", 28 | "lodash": "^4.17.21", 29 | "@ws-tools/scripts": "*" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/grapher/src/commands/depsCommand.ts: -------------------------------------------------------------------------------- 1 | import { getPackageInfos, getTransitiveDependencies, getTransitiveDependents, getWorkspaceRoot } from "workspace-tools"; 2 | 3 | interface DepsCommandOptions { 4 | scope?: string[]; 5 | } 6 | 7 | export function depsCommand(options: DepsCommandOptions): void { 8 | const root = getWorkspaceRoot(process.cwd()); 9 | 10 | if (!root) { 11 | throw new Error("Could not find workspace root"); 12 | } 13 | 14 | const packageInfos = getPackageInfos(root); 15 | 16 | const dependents = getTransitiveDependents(options.scope ?? [], packageInfos); 17 | console.log("Dependent Packages:"); 18 | console.log( 19 | dependents 20 | .sort() 21 | .map((d) => ` - ${d}`) 22 | .join("\n") 23 | ); 24 | 25 | console.log(""); 26 | 27 | const dependencies = getTransitiveDependencies(options.scope ?? [], packageInfos); 28 | console.log("Dependencies of Package:"); 29 | console.log( 30 | dependencies 31 | .sort() 32 | .map((d) => ` - ${d}`) 33 | .join("\n") 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/grapher/src/index.ts: -------------------------------------------------------------------------------- 1 | import commander from "commander"; 2 | import { depsCommand } from "./commands/depsCommand"; 3 | 4 | async function main() { 5 | try { 6 | const program = new commander.Command(); 7 | program.version(require("../package.json").version); 8 | program 9 | .command("deps") 10 | .description("Generate a list of dependencies and dependents for a package") 11 | .option("--scope ", "Package names, give multiple names by have multiple --scope flags") 12 | .action(depsCommand); 13 | 14 | program.parse(process.argv); 15 | } catch (e) { 16 | if (e instanceof Error) { 17 | console.error(e.stack); 18 | } else { 19 | console.error(String(e)); 20 | } 21 | process.exit(1); 22 | } 23 | } 24 | 25 | main(); 26 | -------------------------------------------------------------------------------- /packages/grapher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ws-tools/scripts/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/workspace-tools/README.md: -------------------------------------------------------------------------------- 1 | # workspace-tools 2 | 3 | A collection of utilities that are useful in a git-controlled monorepo managed by one of these tools: 4 | 5 | - lerna 6 | - npm workspaces 7 | - pnpm workspaces 8 | - rush 9 | - yarn workspaces 10 | 11 | ## Environment variables 12 | 13 | ### GIT_DEBUG 14 | 15 | Set to any value to log output for all git commands. 16 | 17 | ### GIT_MAX_BUFFER 18 | 19 | Override the `maxBuffer` value for git processes, for example if the repo is very large. `workspace-tools` uses 500MB by default. 20 | 21 | ### PREFERRED_WORKSPACE_MANAGER 22 | 23 | Sometimes if multiple workspace manager files are checked in, it's necessary to hint which manager is used: `npm`, `yarn`, `pnpm`, `rush`, or `lerna`. 24 | 25 | ### VERBOSE 26 | 27 | Log additional output from certain functions. 28 | -------------------------------------------------------------------------------- /packages/workspace-tools/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@ws-tools/scripts/jest/jest.config"); 2 | -------------------------------------------------------------------------------- /packages/workspace-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace-tools", 3 | "version": "0.38.4", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/workspace-tools" 8 | }, 9 | "main": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "files": [ 12 | "lib/!(__*)", 13 | "lib/!(__*)/**/*" 14 | ], 15 | "scripts": { 16 | "api": "ws-tools-scripts api", 17 | "build": "tsc", 18 | "start": "tsc -w --preserveWatchOutput", 19 | "test": "jest" 20 | }, 21 | "dependencies": { 22 | "@yarnpkg/lockfile": "^1.1.0", 23 | "fast-glob": "^3.3.1", 24 | "git-url-parse": "^16.0.0", 25 | "globby": "^11.0.0", 26 | "jju": "^1.4.0", 27 | "js-yaml": "^4.1.0", 28 | "micromatch": "^4.0.0" 29 | }, 30 | "devDependencies": { 31 | "lodash": "^4.17.21", 32 | "@ws-tools/scripts": "*" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/dependencies.test.ts: -------------------------------------------------------------------------------- 1 | import { PackageInfo } from "../types/PackageInfo"; 2 | import { getTransitiveConsumers, getTransitiveProviders } from "../dependencies/index"; 3 | 4 | describe("getTransitiveConsumers", () => { 5 | it("can get linear transitive consumers", () => { 6 | const allPackages = { 7 | a: stubPackage("a", ["b"]), 8 | b: stubPackage("b", ["c"]), 9 | c: stubPackage("c"), 10 | }; 11 | 12 | const actual = getTransitiveConsumers(["c"], allPackages); 13 | 14 | expect(actual).toContain("a"); 15 | expect(actual).toContain("b"); 16 | }); 17 | 18 | it("can get linear transitive consumers with scope", () => { 19 | const allPackages = { 20 | grid: stubPackage("grid", ["foo"]), 21 | word: stubPackage("word", ["bar"]), 22 | foo: stubPackage("foo", ["core"]), 23 | bar: stubPackage("bar", ["core"]), 24 | core: stubPackage("core"), 25 | demo: stubPackage("demo", ["grid", "word"]), 26 | }; 27 | 28 | const actual = getTransitiveConsumers(["core"], allPackages, ["grid", "word"]); 29 | 30 | expect(actual).toContain("foo"); 31 | expect(actual).toContain("bar"); 32 | expect(actual).toContain("grid"); 33 | expect(actual).toContain("word"); 34 | expect(actual).not.toContain("demo"); 35 | }); 36 | 37 | it("can get transitive consumer with deps", () => { 38 | /* 39 | [b, a] 40 | [d, a] 41 | [c, b] 42 | [e, b] 43 | [f, d] 44 | [c, g] 45 | 46 | expected: a, b, g (orignates from c) 47 | */ 48 | 49 | const allPackages = { 50 | a: stubPackage("a", ["b", "d"]), 51 | b: stubPackage("b", ["c", "e"]), 52 | 53 | c: stubPackage("c"), 54 | 55 | d: stubPackage("d", ["f"]), 56 | e: stubPackage("e"), 57 | f: stubPackage("f"), 58 | g: stubPackage("g", ["c"]), 59 | }; 60 | 61 | const actual = getTransitiveConsumers(["c"], allPackages); 62 | 63 | expect(actual).toContain("a"); 64 | expect(actual).toContain("b"); 65 | expect(actual).toContain("g"); 66 | 67 | expect(actual).not.toContain("d"); 68 | expect(actual).not.toContain("e"); 69 | expect(actual).not.toContain("f"); 70 | expect(actual).not.toContain("c"); 71 | }); 72 | }); 73 | 74 | describe("getTransitiveProviders", () => { 75 | it("can get linear transitive providers", () => { 76 | const allPackages = { 77 | a: stubPackage("a", ["b"]), 78 | b: stubPackage("b", ["c"]), 79 | c: stubPackage("c"), 80 | }; 81 | 82 | const actual = getTransitiveProviders(["a"], allPackages); 83 | 84 | expect(actual).toContain("b"); 85 | expect(actual).toContain("c"); 86 | }); 87 | 88 | it("can get transitive providers with deps", () => { 89 | /* 90 | [b, a] 91 | [c, b] 92 | [e, c] 93 | [f, c] 94 | [f, e] 95 | [g, f] 96 | 97 | expected: e, f, g 98 | */ 99 | 100 | const allPackages = { 101 | a: stubPackage("a", ["b"]), 102 | b: stubPackage("b", ["c"]), 103 | 104 | c: stubPackage("c", ["e", "f"]), 105 | d: stubPackage("d"), 106 | e: stubPackage("e", ["f"]), 107 | f: stubPackage("f", ["g"]), 108 | g: stubPackage("g"), 109 | }; 110 | 111 | const actual = getTransitiveProviders(["c"], allPackages); 112 | 113 | expect(actual).toContain("e"); 114 | expect(actual).toContain("f"); 115 | expect(actual).toContain("g"); 116 | 117 | expect(actual).not.toContain("a"); 118 | expect(actual).not.toContain("b"); 119 | expect(actual).not.toContain("d"); 120 | expect(actual).not.toContain("c"); 121 | }); 122 | 123 | it("can get transitive consumers with deps and scope", () => { 124 | /* 125 | [b, a] 126 | [c, b] 127 | [e, c] 128 | [f, c] 129 | [f, e] 130 | [g, f] 131 | 132 | expected: e, f, g 133 | */ 134 | 135 | const allPackages = { 136 | a: stubPackage("a", ["b", "h"]), 137 | b: stubPackage("b", ["c"]), 138 | 139 | c: stubPackage("c", ["e", "f"]), 140 | d: stubPackage("d"), 141 | e: stubPackage("e", ["f"]), 142 | f: stubPackage("f", ["g"]), 143 | g: stubPackage("g"), 144 | h: stubPackage("h", ["i"]), 145 | i: stubPackage("i", ["f"]), 146 | }; 147 | 148 | const actual = getTransitiveConsumers(["f"], allPackages, ["b"]); 149 | 150 | expect(actual).toContain("e"); 151 | expect(actual).toContain("c"); 152 | expect(actual).toContain("b"); 153 | expect(actual).not.toContain("h"); 154 | }); 155 | }); 156 | 157 | function stubPackage(name: string, deps: string[] = []) { 158 | return { 159 | name, 160 | packageJsonPath: `packages/${name}`, 161 | version: "1.0", 162 | dependencies: deps.reduce((depMap, dep) => ({ ...depMap, [dep]: "*" }), {}), 163 | devDependencies: {}, 164 | } as PackageInfo; 165 | } 166 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/getChangedPackages.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | import { cleanupFixtures, setupFixture, setupLocalRemote } from "@ws-tools/scripts/jest/setupFixture"; 5 | import { stageAndCommit, git } from "../git"; 6 | import { getChangedPackages, getChangedPackagesBetweenRefs } from "../workspaces/getChangedPackages"; 7 | 8 | describe("getChangedPackages", () => { 9 | afterAll(() => { 10 | cleanupFixtures(); 11 | }); 12 | 13 | it("can detect changes inside an untracked file", () => { 14 | const root = setupFixture("monorepo"); 15 | 16 | const newFile = path.join(root, "packages/package-a/footest.txt"); 17 | fs.writeFileSync(newFile, "hello foo test"); 18 | 19 | const changedPkgs = getChangedPackages(root, "main"); 20 | 21 | expect(changedPkgs).toEqual(["package-a"]); 22 | }); 23 | 24 | it("can detect changes inside an untracked file in a nested monorepo", () => { 25 | const root = path.join(setupFixture("monorepo-nested"), "monorepo"); 26 | 27 | const newFile = path.join(root, "packages/package-a/footest.txt"); 28 | fs.writeFileSync(newFile, "hello foo test"); 29 | 30 | const changedPkgs = getChangedPackages(root, "main"); 31 | 32 | expect(changedPkgs).toEqual(["package-a"]); 33 | }); 34 | 35 | it("can detect changes when multiple files are changed", () => { 36 | const root = path.join(setupFixture("monorepo-nested"), "monorepo"); 37 | 38 | const readmeFile = path.join(root, "README.md"); 39 | const lageFile = path.join(root, "lage.config.json"); 40 | fs.writeFileSync(readmeFile, "hello foo test"); 41 | fs.writeFileSync(lageFile, "hello foo test"); 42 | 43 | const changedPkgs = getChangedPackages(root, "main"); 44 | 45 | expect(changedPkgs).toEqual(["package-a", "package-b"]); 46 | }); 47 | 48 | it("can ignore changes when multiple files are changed", () => { 49 | const root = path.join(setupFixture("monorepo-nested"), "monorepo"); 50 | 51 | const readmeFile = path.join(root, "README.md"); 52 | const lageFile = path.join(root, "lage.config.json"); 53 | fs.writeFileSync(readmeFile, "hello foo test"); 54 | fs.writeFileSync(lageFile, "hello foo test"); 55 | 56 | const ignoreGlobs = ["lage.config.json", "README.md"]; 57 | 58 | const changedPkgs = getChangedPackages(root, "main", ignoreGlobs); 59 | 60 | expect(changedPkgs).toEqual([]); 61 | }); 62 | 63 | it("can detect changes inside an unstaged file", () => { 64 | const root = setupFixture("monorepo"); 65 | 66 | const newFile = path.join(root, "packages/package-a/index.ts"); 67 | fs.writeFileSync(newFile, "hello foo test"); 68 | 69 | const changedPkgs = getChangedPackages(root, "main"); 70 | 71 | expect(changedPkgs).toEqual(["package-a"]); 72 | }); 73 | 74 | it("can detect changes inside an unstaged file in a nested monorepo", () => { 75 | const root = path.join(setupFixture("monorepo-nested"), "monorepo"); 76 | 77 | const newFile = path.join(root, "packages/package-a/index.ts"); 78 | fs.writeFileSync(newFile, "hello foo test"); 79 | 80 | const changedPkgs = getChangedPackages(root, "main"); 81 | 82 | expect(changedPkgs).toEqual(["package-a"]); 83 | }); 84 | 85 | it("can detect changes inside a staged file", () => { 86 | const root = setupFixture("monorepo"); 87 | 88 | const newFile = path.join(root, "packages/package-a/footest.txt"); 89 | fs.writeFileSync(newFile, "hello foo test"); 90 | git(["add", newFile], { cwd: root }); 91 | 92 | const changedPkgs = getChangedPackages(root, "main"); 93 | 94 | expect(changedPkgs).toEqual(["package-a"]); 95 | }); 96 | 97 | it("can detect changes inside a staged file in a nested monorepo", () => { 98 | const root = path.join(setupFixture("monorepo-nested"), "monorepo"); 99 | 100 | const newFile = path.join(root, "packages/package-a/footest.txt"); 101 | fs.writeFileSync(newFile, "hello foo test"); 102 | git(["add", newFile], { cwd: root }); 103 | 104 | const changedPkgs = getChangedPackages(root, "main"); 105 | 106 | expect(changedPkgs).toEqual(["package-a"]); 107 | }); 108 | 109 | it("can detect changes inside a file that has been committed in a different branch", () => { 110 | const root = setupFixture("monorepo"); 111 | 112 | const newFile = path.join(root, "packages/package-a/footest.txt"); 113 | fs.writeFileSync(newFile, "hello foo test"); 114 | git(["checkout", "-b", "newbranch"], { cwd: root }); 115 | stageAndCommit([newFile], "test commit", root); 116 | 117 | const changedPkgs = getChangedPackages(root, "main"); 118 | 119 | expect(changedPkgs).toEqual(["package-a"]); 120 | }); 121 | 122 | it("can detect changes inside a file that has been committed in a different branch in a nested monorepo", () => { 123 | const root = path.join(setupFixture("monorepo-nested"), "monorepo"); 124 | 125 | const newFile = path.join(root, "packages/package-a/footest.txt"); 126 | fs.writeFileSync(newFile, "hello foo test"); 127 | git(["checkout", "-b", "newbranch"], { cwd: root }); 128 | stageAndCommit(["add", newFile], "test commit", root); 129 | 130 | const changedPkgs = getChangedPackages(root, "main"); 131 | 132 | expect(changedPkgs).toEqual(["package-a"]); 133 | }); 134 | 135 | it("can detect changes inside a file that has been committed in a different branch using default remote", () => { 136 | const root = setupFixture("monorepo"); 137 | setupLocalRemote(root, "origin", "basic"); 138 | 139 | const newFile = path.join(root, "packages/package-a/footest.txt"); 140 | fs.writeFileSync(newFile, "hello foo test"); 141 | git(["checkout", "-b", "newbranch"], { cwd: root }); 142 | stageAndCommit(["add", newFile], "test commit", root); 143 | 144 | const changedPkgs = getChangedPackages(root, undefined); 145 | 146 | expect(changedPkgs).toContain("package-a"); 147 | }); 148 | 149 | it("can ignore glob patterns in detecting changes", () => { 150 | const root = setupFixture("monorepo"); 151 | 152 | const newFile = path.join(root, "packages/package-a/footest.txt"); 153 | fs.writeFileSync(newFile, "hello foo test"); 154 | git(["add", newFile], { cwd: root }); 155 | 156 | const changedPkgs = getChangedPackages(root, "main", ["packages/package-a/*"]); 157 | 158 | expect(changedPkgs).toEqual([]); 159 | }); 160 | 161 | describe("getChangedPackagesBetweenRefs", () => { 162 | it("can detect changed packages between two refs", () => { 163 | const root = setupFixture("monorepo"); 164 | 165 | const newFile = path.join(root, "packages/package-a/footest.txt"); 166 | fs.writeFileSync(newFile, "hello foo test"); 167 | git(["add", newFile], { cwd: root }); 168 | stageAndCommit(["packages/package-a/footest.txt"], "test commit in a", root); 169 | 170 | const newFile2 = path.join(root, "packages/package-b/footest2.txt"); 171 | fs.writeFileSync(newFile2, "hello foo test"); 172 | git(["add", newFile2], { cwd: root }); 173 | stageAndCommit(["packages/package-b/footest2.txt"], "test commit in b", root); 174 | 175 | const changedPkgs = getChangedPackagesBetweenRefs(root, "HEAD^1", "HEAD"); 176 | 177 | expect(changedPkgs).toContain("package-b"); 178 | expect(changedPkgs).not.toContain("package-a"); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/getDefaultBranch.test.ts: -------------------------------------------------------------------------------- 1 | import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; 2 | import { getDefaultBranch, git } from "../git"; 3 | 4 | describe("getDefaultBranch()", () => { 5 | afterAll(() => { 6 | cleanupFixtures(); 7 | }); 8 | 9 | it("is main or master in the default test repo", () => { 10 | const cwd = setupFixture(); 11 | 12 | const branch = getDefaultBranch(cwd); 13 | 14 | // avoid dependency on git version or other particulars of test environment 15 | expect(branch).toMatch(/^(main|master)$/); 16 | }); 17 | 18 | it("is myMain when default branch is different", () => { 19 | const cwd = setupFixture(); 20 | git(["config", "init.defaultBranch", "myMain"], { cwd }); 21 | 22 | const branch = getDefaultBranch(cwd); 23 | 24 | expect(branch).toBe("myMain"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/getPackagesByFiles.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; 5 | import { getPackagesByFiles } from "../workspaces/getPackagesByFiles"; 6 | 7 | describe("getPackagesByFiles", () => { 8 | afterAll(() => { 9 | cleanupFixtures(); 10 | }); 11 | 12 | it("can find all packages that contain the files in a monorepo", () => { 13 | const root = setupFixture("monorepo"); 14 | 15 | const newFile = path.join(root, "packages/package-a/footest.txt"); 16 | fs.writeFileSync(newFile, "hello foo test"); 17 | 18 | const packages = getPackagesByFiles(root, ["packages/package-a/footest.txt"]); 19 | 20 | expect(packages).toEqual(["package-a"]); 21 | }); 22 | 23 | it("can find can ignore changes in a glob pattern", () => { 24 | const root = setupFixture("monorepo"); 25 | 26 | const newFileA = path.join(root, "packages/package-a/footest.txt"); 27 | fs.writeFileSync(newFileA, "hello foo test"); 28 | 29 | const newFileB = path.join(root, "packages/package-b/footest.txt"); 30 | fs.writeFileSync(newFileB, "hello foo test"); 31 | 32 | const packages = getPackagesByFiles( 33 | root, 34 | ["packages/package-a/footest.txt", "packages/package-b/footest.txt"], 35 | ["packages/package-b/**"] 36 | ); 37 | 38 | expect(packages).toEqual(["package-a"]); 39 | }); 40 | 41 | it("can find can handle empty files", () => { 42 | const root = setupFixture("monorepo"); 43 | 44 | const packages = getPackagesByFiles(root, []); 45 | 46 | expect(packages).toEqual([]); 47 | }); 48 | 49 | it("can find can handle unrelated files", () => { 50 | const root = setupFixture("monorepo"); 51 | 52 | const packages = getPackagesByFiles(root, ["package.json"]); 53 | 54 | expect(packages).toEqual([]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/getRepositoryName.test.ts: -------------------------------------------------------------------------------- 1 | import { getRepositoryName } from "../git/getRepositoryName"; 2 | 3 | // This mostly uses gitUrlParse internally, so only test a couple basic github cases plus the 4 | // the special cases we added to handle the annoyingly large variety of equivalent VSTS/ADO URLs 5 | describe("getRepositoryName", () => { 6 | describe("github", () => { 7 | it("works with HTTPS URLs", () => { 8 | expect(getRepositoryName("https://github.com/microsoft/workspace-tools")).toBe("microsoft/workspace-tools"); 9 | }); 10 | it("works with HTTPS URLs with .git", () => { 11 | expect(getRepositoryName("https://github.com/microsoft/workspace-tools.git")).toBe("microsoft/workspace-tools"); 12 | }); 13 | it("works with SSH URLs", () => { 14 | expect(getRepositoryName("git@github.com:microsoft/workspace-tools.git")).toBe("microsoft/workspace-tools"); 15 | }); 16 | it("works with git:// URLs", () => { 17 | expect(getRepositoryName("git://github.com/microsoft/workspace-tools")).toBe("microsoft/workspace-tools"); 18 | }); 19 | }); 20 | 21 | // All of these ADO and VSO variants point to the same repo 22 | describe("ADO", () => { 23 | it("works with HTTPS URLs", () => { 24 | expect(getRepositoryName("https://dev.azure.com/foo/bar/_git/some-repo")).toBe("foo/bar/some-repo"); 25 | }); 26 | it("works with HTTPS URLs with _optimized", () => { 27 | expect(getRepositoryName("https://dev.azure.com/foo/bar/_git/_optimized/some-repo")).toBe("foo/bar/some-repo"); 28 | }); 29 | it("works with HTTPS URLs with user", () => { 30 | expect(getRepositoryName("https://user@dev.azure.com/foo/bar/_git/some-repo")).toBe("foo/bar/some-repo"); 31 | }); 32 | it("works with HTTPS URLs with user and token", () => { 33 | expect(getRepositoryName("https://user:fakePAT@dev.azure.com/foo/bar/_git/some-repo")).toBe("foo/bar/some-repo"); 34 | }); 35 | it("works SSH URLs", () => { 36 | expect(getRepositoryName("git@ssh.dev.azure.com:v3/foo/bar/some-repo")).toBe("foo/bar/some-repo"); 37 | }); 38 | }); 39 | 40 | describe("VSO", () => { 41 | it("works with HTTPS URLs", () => { 42 | expect(getRepositoryName("https://foo.visualstudio.com/bar/_git/some-repo")).toBe("foo/bar/some-repo"); 43 | }); 44 | it("works with HTTPS URLs with DefaultCollection", () => { 45 | expect(getRepositoryName("https://foo.visualstudio.com/DefaultCollection/bar/_git/some-repo")).toBe( 46 | "foo/bar/some-repo" 47 | ); 48 | }); 49 | it("works with HTTPS URLs with _optimized", () => { 50 | expect(getRepositoryName("https://foo.visualstudio.com/DefaultCollection/bar/_git/_optimized/some-repo")).toBe( 51 | "foo/bar/some-repo" 52 | ); 53 | }); 54 | it("works with HTTPS URLs with user", () => { 55 | expect(getRepositoryName("https://user@foo.visualstudio.com/bar/_git/some-repo")).toBe("foo/bar/some-repo"); 56 | }); 57 | it("works with HTTPS URLs with user and token", () => { 58 | expect(getRepositoryName("https://user:fakePAT@foo.visualstudio.com/bar/_git/some-repo")).toBe( 59 | "foo/bar/some-repo" 60 | ); 61 | }); 62 | it("works with SSH URLs", () => { 63 | expect(getRepositoryName("foo@vs-ssh.visualstudio.com:v3/foo/bar/some-repo")).toBe("foo/bar/some-repo"); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/getScopedPackages.test.ts: -------------------------------------------------------------------------------- 1 | import { getScopedPackages } from "../scope"; 2 | import { PackageInfos } from "../types/PackageInfo"; 3 | 4 | describe("getScopedPackages", () => { 5 | it("can match scopes for full matches for an array", () => { 6 | const results = getScopedPackages(["foo", "bar"], ["foo", "bar", "baz"]); 7 | expect(results).toContain("foo"); 8 | expect(results).toContain("bar"); 9 | expect(results).not.toContain("baz"); 10 | }); 11 | 12 | it("can match scopes for full matches for a map", () => { 13 | const results = getScopedPackages(["foo", "bar"], { 14 | foo: {}, 15 | bar: {}, 16 | baz: {}, 17 | }); 18 | expect(results).toContain("foo"); 19 | expect(results).toContain("bar"); 20 | expect(results).not.toContain("baz"); 21 | }); 22 | 23 | it("can match scopes for full matches for a map of PackageInfos", () => { 24 | const results = getScopedPackages(["foo", "bar"], { 25 | foo: { name: "foo", packageJsonPath: "nowhere", version: "1.0.0" }, 26 | bar: { name: "bar", packageJsonPath: "nowhere", version: "1.0.0" }, 27 | baz: { name: "baz", packageJsonPath: "nowhere", version: "1.0.0" }, 28 | } as PackageInfos); 29 | expect(results).toContain("foo"); 30 | expect(results).toContain("bar"); 31 | expect(results).not.toContain("baz"); 32 | }); 33 | 34 | it("can match with wildcards", () => { 35 | const results = getScopedPackages(["foo*"], ["foo1", "foo2", "baz"]); 36 | expect(results).toContain("foo1"); 37 | expect(results).toContain("foo2"); 38 | expect(results).not.toContain("baz"); 39 | }); 40 | 41 | it("matches the correct packages when search pattern starts with @, irrespective of case", () => { 42 | const results = getScopedPackages( 43 | ["@i-love/theavettbrothers"], 44 | [ 45 | "@i-love/theavettbrothers", 46 | "@i-love/THEAVETTBROTHERS", 47 | "@i-love/TheAvettBrothers", 48 | "theAvettBrothers", 49 | "@i-love/JimmyEatWorld", 50 | ] 51 | ); 52 | expect(results).toContain("@i-love/theavettbrothers"); 53 | expect(results).toContain("@i-love/THEAVETTBROTHERS"); 54 | expect(results).toContain("@i-love/TheAvettBrothers"); 55 | expect(results).not.toContain("theAvettBrothers"); 56 | expect(results).not.toContain("@i-love/JimmyEatWorld"); 57 | }); 58 | 59 | it("matches the correct package, irrespective of case", () => { 60 | const results = getScopedPackages( 61 | ["ilovetheavettbrothers"], 62 | ["ilovetheavettbrothers", "ILOVETHEAVETTBROTHERS", "iLoveTheAvettBrothers", "IDoNotLoveTaylorSwift"] 63 | ); 64 | expect(results).toContain("ilovetheavettbrothers"); 65 | expect(results).toContain("ILOVETHEAVETTBROTHERS"); 66 | expect(results).toContain("iLoveTheAvettBrothers"); 67 | expect(results).not.toContain("IDoNotLoveTaylorSwift"); 68 | }); 69 | 70 | it("can match with npm package scopes", () => { 71 | const results = getScopedPackages(["foo"], ["@yay/foo", "@yay1/foo", "foo", "baz"]); 72 | expect(results).toContain("@yay/foo"); 73 | expect(results).toContain("@yay1/foo"); 74 | expect(results).toContain("foo"); 75 | expect(results).not.toContain("baz"); 76 | }); 77 | 78 | it("can match with npm package scopes with wildcards", () => { 79 | const results = getScopedPackages(["foo*"], ["@yay/foo1", "@yay1/foo2", "foo", "baz"]); 80 | expect(results).toContain("@yay/foo1"); 81 | expect(results).toContain("@yay1/foo2"); 82 | expect(results).toContain("foo"); 83 | expect(results).not.toContain("baz"); 84 | }); 85 | 86 | it("uses the correct package scope when the search pattern starts a @ character", () => { 87 | const results = getScopedPackages(["@yay/foo*"], ["@yay/foo1", "@yay1/foo2", "foo", "baz"]); 88 | expect(results).toContain("@yay/foo1"); 89 | expect(results).not.toContain("@yay1/foo2"); 90 | expect(results).not.toContain("foo"); 91 | expect(results).not.toContain("baz"); 92 | }); 93 | 94 | it("can deal with brace expansion with scopes", () => { 95 | const results = getScopedPackages(["@yay/foo{1,2}"], ["@yay/foo1", "@yay/foo2", "@yay/foo3", "foo", "baz"]); 96 | expect(results).toContain("@yay/foo1"); 97 | expect(results).toContain("@yay/foo2"); 98 | expect(results).not.toContain("@yay/foo3"); 99 | expect(results).not.toContain("foo"); 100 | expect(results).not.toContain("baz"); 101 | }); 102 | 103 | it("can deal with negated search", () => { 104 | const results = getScopedPackages( 105 | ["@yay/foo*", "!@yay/foo3"], 106 | ["@yay/foo1", "@yay/foo2", "@yay/foo3", "foo", "baz"] 107 | ); 108 | expect(results).toContain("@yay/foo1"); 109 | expect(results).toContain("@yay/foo2"); 110 | expect(results).not.toContain("@yay/foo3"); 111 | expect(results).not.toContain("foo"); 112 | expect(results).not.toContain("baz"); 113 | }); 114 | }); 115 | 116 | // cspell:ignore ilovetheavettbrothers, theavettbrothers 117 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/getWorkspaceRoot.test.ts: -------------------------------------------------------------------------------- 1 | import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; 2 | import { getWorkspaceRoot } from "../workspaces/getWorkspaceRoot"; 3 | 4 | describe("getWorkspaceRoot", () => { 5 | afterAll(() => { 6 | cleanupFixtures(); 7 | }); 8 | 9 | it("handles yarn workspace", () => { 10 | const repoRoot = setupFixture("monorepo"); 11 | expect(getWorkspaceRoot(repoRoot)).toBe(repoRoot); 12 | }); 13 | 14 | it("handles pnpm workspace", () => { 15 | const repoRoot = setupFixture("monorepo-pnpm"); 16 | expect(getWorkspaceRoot(repoRoot)).toBe(repoRoot); 17 | }); 18 | 19 | it("handles rush workspace", () => { 20 | const repoRoot = setupFixture("monorepo-rush-pnpm"); 21 | expect(getWorkspaceRoot(repoRoot)).toBe(repoRoot); 22 | }); 23 | 24 | it("handles npm workspace", () => { 25 | const repoRoot = setupFixture("monorepo-npm"); 26 | expect(getWorkspaceRoot(repoRoot)).toBe(repoRoot); 27 | }); 28 | 29 | it("handles lerna workspace", () => { 30 | const repoRoot = setupFixture("monorepo-lerna-npm"); 31 | expect(getWorkspaceRoot(repoRoot)).toBe(repoRoot); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/getWorkspaces.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; 4 | import { getWorkspaceManagerAndRoot } from "../workspaces/implementations"; 5 | import { getYarnWorkspaces, getYarnWorkspacesAsync } from "../workspaces/implementations/yarn"; 6 | import { getPnpmWorkspaces, getPnpmWorkspacesAsync } from "../workspaces/implementations/pnpm"; 7 | import { getRushWorkspaces, getRushWorkspacesAsync } from "../workspaces/implementations/rush"; 8 | import { getNpmWorkspaces, getNpmWorkspacesAsync } from "../workspaces/implementations/npm"; 9 | import { getLernaWorkspaces, getLernaWorkspacesAsync } from "../workspaces/implementations/lerna"; 10 | 11 | import _ from "lodash"; 12 | 13 | describe("getWorkspaces", () => { 14 | afterAll(() => { 15 | cleanupFixtures(); 16 | }); 17 | 18 | describe.each([ 19 | ["getYarnWorkspaces", getYarnWorkspaces], 20 | ["getYarnWorkspacesAsync", getYarnWorkspacesAsync], 21 | ])("yarn (%s)", (name, getWorkspaces) => { 22 | it("gets the name and path of the workspaces", async () => { 23 | const packageRoot = setupFixture("monorepo"); 24 | 25 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "yarn", root: packageRoot }); 26 | 27 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 28 | 29 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 30 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 31 | 32 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 33 | { name: "package-a", path: packageAPath }, 34 | { name: "package-b", path: packageBPath }, 35 | ]); 36 | }); 37 | 38 | it("gets the name and path of the workspaces against a packages spec of an individual package", async () => { 39 | const packageRoot = setupFixture("monorepo-globby"); 40 | 41 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "yarn", root: packageRoot }); 42 | 43 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 44 | 45 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 46 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 47 | const individualPath = path.join(packageRoot, "individual"); 48 | 49 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 50 | { name: "individual", path: individualPath }, 51 | { name: "package-a", path: packageAPath }, 52 | { name: "package-b", path: packageBPath }, 53 | ]); 54 | }); 55 | }); 56 | 57 | describe.each([ 58 | ["getPnpmWorkspaces", getPnpmWorkspaces], 59 | ["getPnpmWorkspacesAsync", getPnpmWorkspacesAsync], 60 | ])("pnpm (%s)", (name, getWorkspaces) => { 61 | it("gets the name and path of the workspaces", async () => { 62 | const packageRoot = setupFixture("monorepo-pnpm"); 63 | 64 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "pnpm", root: packageRoot }); 65 | 66 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 67 | 68 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 69 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 70 | 71 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 72 | { name: "package-a", path: packageAPath }, 73 | { name: "package-b", path: packageBPath }, 74 | ]); 75 | }); 76 | }); 77 | 78 | describe.each([ 79 | ["getRushWorkspaces", getRushWorkspaces], 80 | ["getRushWorkspacesAsync", getRushWorkspacesAsync], 81 | ])("rush + pnpm (%s)", (name, getWorkspaces) => { 82 | it("gets the name and path of the workspaces", async () => { 83 | const packageRoot = setupFixture("monorepo-rush-pnpm"); 84 | 85 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "rush", root: packageRoot }); 86 | 87 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 88 | 89 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 90 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 91 | 92 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 93 | { name: "package-a", path: packageAPath }, 94 | { name: "package-b", path: packageBPath }, 95 | ]); 96 | }); 97 | }); 98 | 99 | describe.each([ 100 | ["getRushWorkspaces", getRushWorkspaces], 101 | ["getRushWorkspacesAsync", getRushWorkspacesAsync], 102 | ])("rush + pnpm (%s)", (name, getWorkspaces) => { 103 | it("gets the name and path of the workspaces", async () => { 104 | const packageRoot = setupFixture("monorepo-rush-yarn"); 105 | 106 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "rush", root: packageRoot }); 107 | 108 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 109 | 110 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 111 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 112 | 113 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 114 | { name: "package-a", path: packageAPath }, 115 | { name: "package-b", path: packageBPath }, 116 | ]); 117 | }); 118 | }); 119 | 120 | describe.each([ 121 | ["getNpmWorkspaces", getNpmWorkspaces], 122 | ["getNpmWorkspacesAsync", getNpmWorkspacesAsync], 123 | ])("npm (%s)", (name, getWorkspaces) => { 124 | it("gets the name and path of the workspaces", async () => { 125 | const packageRoot = setupFixture("monorepo-npm"); 126 | 127 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "npm", root: packageRoot }); 128 | 129 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 130 | 131 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 132 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 133 | 134 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 135 | { name: "package-a", path: packageAPath }, 136 | { name: "package-b", path: packageBPath }, 137 | ]); 138 | }); 139 | 140 | it("gets the name and path of the workspaces using the shorthand configuration", async () => { 141 | const packageRoot = setupFixture("monorepo-shorthand"); 142 | 143 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "npm", root: packageRoot }); 144 | 145 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 146 | 147 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 148 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 149 | const individualPath = path.join(packageRoot, "individual"); 150 | 151 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 152 | { name: "individual", path: individualPath }, 153 | { name: "package-a", path: packageAPath }, 154 | { name: "package-b", path: packageBPath }, 155 | ]); 156 | }); 157 | }); 158 | 159 | describe.each([ 160 | ["getLernaWorkspaces", getLernaWorkspaces], 161 | ["getLernaWorkspacesAsync", getLernaWorkspacesAsync], 162 | ])("lerna (%s)", (name, getWorkspaces) => { 163 | it("gets the name and path of the workspaces", async () => { 164 | const packageRoot = setupFixture("monorepo-lerna-npm"); 165 | 166 | expect(getWorkspaceManagerAndRoot(packageRoot, new Map())).toEqual({ manager: "lerna", root: packageRoot }); 167 | 168 | const workspacesPackageInfo = await getWorkspaces(packageRoot); 169 | 170 | const packageAPath = path.join(packageRoot, "packages", "package-a"); 171 | const packageBPath = path.join(packageRoot, "packages", "package-b"); 172 | 173 | expect(_.orderBy(workspacesPackageInfo, ["name"], ["asc"])).toMatchObject([ 174 | { name: "package-a", path: packageAPath }, 175 | { name: "package-b", path: packageBPath }, 176 | ]); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/lockfile.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import path from "path"; 3 | import { setupFixture } from "@ws-tools/scripts/jest/setupFixture"; 4 | import { parseLockFile } from "../lockfile"; 5 | import { PackageInfo } from "../types/PackageInfo"; 6 | 7 | const ERROR_MESSAGES = { 8 | NO_LOCK: "You do not have yarn.lock, pnpm-lock.yaml or package-lock.json. Please use one of these package managers.", 9 | UNSUPPORTED: 10 | "Your package-lock.json version is not supported: lockfileVersion is 1. You need npm version 7 or above and package-lock version 2 or above. Please, upgrade npm or choose a different package manager.", 11 | }; 12 | 13 | describe("parseLockFile()", () => { 14 | describe("general", () => { 15 | it("throws if it cannot find lock file", async () => { 16 | const packageRoot = setupFixture("basic-without-lock-file"); 17 | 18 | await expect(parseLockFile(packageRoot)).rejects.toThrow(ERROR_MESSAGES.NO_LOCK); 19 | }); 20 | }); 21 | 22 | describe("NPM", () => { 23 | it("parses package-lock.json file when it is found", async () => { 24 | const packageRoot = setupFixture("monorepo-npm"); 25 | const parsedLockFile = await parseLockFile(packageRoot); 26 | 27 | expect(parsedLockFile).toHaveProperty("type", "success"); 28 | }); 29 | 30 | it("throws if npm version is unsupported", async () => { 31 | const packageRoot = setupFixture("monorepo-npm-unsupported"); 32 | 33 | await expect(parseLockFile(packageRoot)).rejects.toThrow(ERROR_MESSAGES.UNSUPPORTED); 34 | }); 35 | }); 36 | 37 | describe.each([1, 2] as const)("yarn %s", (yarnVersion) => { 38 | const updatePath = (path: string) => (yarnVersion === 1 ? path : `${path}-2`); 39 | 40 | it("parses yarn.lock file when it is found", async () => { 41 | const packageRoot = setupFixture(updatePath("basic")); 42 | const parsedLockFile = await parseLockFile(packageRoot); 43 | 44 | expect(parsedLockFile).toHaveProperty("type", "success"); 45 | }); 46 | 47 | it("parses combined ranges in yarn.lock", async () => { 48 | const packageRoot = setupFixture(updatePath("basic-yarn")); 49 | 50 | // Verify that __fixtures__/basic-yarn still follows these assumptions: 51 | // - "execa" is listed as a dep in package.json 52 | // - "@types/execa" is also listed as a dep, and internally has a dep on "execa@*" 53 | const packageName = "execa"; 54 | const packageInfo = fs.readJSONSync(path.join(packageRoot, "package.json")) as PackageInfo; 55 | expect(packageInfo.dependencies?.[packageName]).toBeTruthy(); 56 | expect(packageInfo.devDependencies?.[`@types/${packageName}`]).toBeTruthy(); 57 | 58 | // The actual test: execa@* resolves to the same thing as execa@ 59 | const expectedSpec = `${packageName}@*`; 60 | const parsedLockFile = await parseLockFile(packageRoot); 61 | expect(parsedLockFile.object[expectedSpec]).toBeTruthy(); 62 | const otherSpecs = Object.entries(parsedLockFile.object).filter( 63 | ([spec]) => spec.startsWith(`${packageName}@`) && spec !== expectedSpec 64 | ); 65 | expect(otherSpecs.length).toBeGreaterThanOrEqual(1); 66 | expect(otherSpecs).toContainEqual([expect.anything(), parsedLockFile.object[expectedSpec]]); 67 | }); 68 | }); 69 | 70 | describe("PNPM", () => { 71 | it("parses pnpm-lock.yaml file when it is found", async () => { 72 | const packageRoot = setupFixture("basic-pnpm"); 73 | const parsedLockFile = await parseLockFile(packageRoot); 74 | 75 | const yargs = Object.keys(parsedLockFile.object).find((key) => /^yargs@/.test(key)); 76 | // if either of these fails, check the actual lock file to verify the deps didn't change 77 | // with renovate updates or something 78 | expect(yargs).toBeTruthy(); 79 | expect(parsedLockFile.object[yargs!].dependencies?.["cliui"]).toBeTruthy(); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/__tests__/queryLockFile.test.ts: -------------------------------------------------------------------------------- 1 | import { setupFixture } from "@ws-tools/scripts/jest/setupFixture"; 2 | import { parseLockFile, queryLockFile, ParsedLock } from ".."; 3 | 4 | /** 5 | * These tests rely on the "@microsoft/task-scheduler" package as defined in package.json in fixtures: 6 | * - monorepo-npm 7 | * - basic-yarn 8 | * - monorepo-pnpm 9 | * 10 | * If making any changes to those fixtures, update the `packageName` constant. 11 | */ 12 | 13 | const packageName = "@microsoft/task-scheduler"; 14 | const getPackageVersion = (parsedLockFile: ParsedLock) => { 15 | const packageSpec = Object.keys(parsedLockFile.object).find((spec) => spec.startsWith(`${packageName}@`)); 16 | expect(packageSpec).toBeTruthy(); 17 | return parsedLockFile.object[packageSpec!].version; 18 | }; 19 | 20 | describe("queryLockFile", () => { 21 | describe("NPM", () => { 22 | it("retrieves a dependency from a lock generated by npm", async () => { 23 | const packageRoot = setupFixture("monorepo-npm"); 24 | const parsedLockFile = await parseLockFile(packageRoot); 25 | const packageVersion = getPackageVersion(parsedLockFile)!; 26 | 27 | const result = queryLockFile(packageName, packageVersion, parsedLockFile); 28 | expect(result).toBeTruthy(); 29 | expect(result.version).toBe(packageVersion); 30 | }); 31 | }); 32 | 33 | // TODO: add yarn 2 34 | describe.each([1] as const)("yarn %s", (yarnVersion) => { 35 | const updatePath = (path: string) => (yarnVersion === 1 ? path : `${path}-2`); 36 | 37 | it("retrieves a dependency from a lock generated by yarn", async () => { 38 | const packageRoot = setupFixture(updatePath("basic-yarn")); 39 | const parsedLockFile = await parseLockFile(packageRoot); 40 | const packageVersion = getPackageVersion(parsedLockFile)!; 41 | 42 | // NOTE: Yarn’s locks include ranges. 43 | const result = queryLockFile(packageName, `^${packageVersion}`, parsedLockFile); 44 | expect(result).toBeTruthy(); 45 | expect(result.version).toBe(packageVersion); 46 | }); 47 | }); 48 | 49 | describe("PNPM", () => { 50 | it("retrieves a dependency from a lock generated by pnpm", async () => { 51 | const packageRoot = setupFixture("monorepo-pnpm"); 52 | const parsedLockFile = await parseLockFile(packageRoot); 53 | const packageVersion = getPackageVersion(parsedLockFile)!; 54 | 55 | const result = queryLockFile(packageName, packageVersion, parsedLockFile); 56 | expect(result).toBeTruthy(); 57 | expect(result.version).toBe(packageVersion); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/dependencies/index.ts: -------------------------------------------------------------------------------- 1 | import { getTransitiveConsumers, getTransitiveProviders } from "./transitiveDeps"; 2 | import { getPackageDependencies } from "../graph/getPackageDependencies"; 3 | 4 | // Some deprecated functions below for backwards compatibility 5 | 6 | export const getTransitiveDependencies = getTransitiveProviders; 7 | 8 | export { getTransitiveProviders }; 9 | 10 | export const getTransitiveDependents = getTransitiveConsumers; 11 | 12 | export { getTransitiveConsumers }; 13 | 14 | /** @deprecated Do not use */ 15 | export const getInternalDeps = getPackageDependencies; 16 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/dependencies/transitiveDeps.ts: -------------------------------------------------------------------------------- 1 | import { PackageInfos } from "../types/PackageInfo"; 2 | import { getPackageDependencies } from "../graph/getPackageDependencies"; 3 | import { isCachingEnabled } from "../isCachingEnabled"; 4 | 5 | const graphCache = new Map(); 6 | 7 | function memoizedKey(packages: PackageInfos, scope: string[] = []) { 8 | return JSON.stringify({ packages, scope }); 9 | } 10 | 11 | function getPackageGraph(packages: PackageInfos, scope: string[] = []) { 12 | const internalPackages = new Set(Object.keys(packages)); 13 | const key = memoizedKey(packages, scope); 14 | 15 | if (isCachingEnabled() && graphCache.has(key)) { 16 | return graphCache.get(key)!; 17 | } 18 | 19 | const edges: [string | null, string][] = []; 20 | 21 | const visited = new Set(); 22 | const stack: string[] = scope.length > 0 ? [...scope] : Object.keys(packages); 23 | 24 | while (stack.length > 0) { 25 | const pkg = stack.pop()!; 26 | 27 | if (visited.has(pkg)) { 28 | continue; 29 | } 30 | 31 | visited.add(pkg); 32 | 33 | const info = packages[pkg]; 34 | const deps = getPackageDependencies(info, internalPackages); 35 | 36 | if (deps.length > 0) { 37 | for (const dep of deps) { 38 | stack.push(dep); 39 | edges.push([dep, pkg]); 40 | } 41 | } else { 42 | edges.push([null, pkg]); 43 | } 44 | } 45 | 46 | graphCache.set(key, edges); 47 | 48 | return edges; 49 | } 50 | 51 | export function getDependentMap(packages: PackageInfos) { 52 | const graph = getPackageGraph(packages); 53 | const map = new Map>(); 54 | for (const [from, to] of graph) { 55 | if (!map.has(to)) { 56 | map.set(to, new Set()); 57 | } 58 | 59 | if (from) { 60 | map.get(to)!.add(from); 61 | } 62 | } 63 | 64 | return map; 65 | } 66 | 67 | /** 68 | * For a package graph of `a->b->c` (where `b` depends on `a`), transitive consumers of `a` are `b` & `c` 69 | * and their consumers (or what are the consequences of `a`) 70 | * @deprecated Do not use 71 | */ 72 | export function getTransitiveConsumers(targets: string[], packages: PackageInfos, scope: string[] = []) { 73 | const graph = getPackageGraph(packages, scope); 74 | const pkgQueue: string[] = [...targets]; 75 | const visited = new Set(); 76 | 77 | while (pkgQueue.length > 0) { 78 | const pkg = pkgQueue.shift()!; 79 | 80 | if (!visited.has(pkg)) { 81 | visited.add(pkg); 82 | 83 | for (const [from, to] of graph) { 84 | if (from === pkg) { 85 | pkgQueue.push(to); 86 | } 87 | } 88 | } 89 | } 90 | 91 | return [...visited].filter((pkg) => !targets.includes(pkg)); 92 | } 93 | 94 | /** 95 | * For a package graph of `a->b->c` (where `b` depends on `a`), transitive providers of `c` are `a` & `b` 96 | * and their providers (or what is needed to satisfy `c`) 97 | * 98 | * @deprecated Do not use 99 | */ 100 | export function getTransitiveProviders(targets: string[], packages: PackageInfos) { 101 | const graph = getPackageGraph(packages); 102 | const pkgQueue: string[] = [...targets]; 103 | const visited = new Set(); 104 | 105 | while (pkgQueue.length > 0) { 106 | const pkg = pkgQueue.shift()!; 107 | 108 | if (!visited.has(pkg)) { 109 | visited.add(pkg); 110 | 111 | for (const [from, to] of graph) { 112 | if (to === pkg && from) { 113 | pkgQueue.push(from); 114 | } 115 | } 116 | } 117 | } 118 | 119 | return [...visited].filter((pkg) => !targets.includes(pkg)); 120 | } 121 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/getPackageInfos.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { PackageInfo, PackageInfos } from "./types/PackageInfo"; 4 | import { infoFromPackageJson } from "./infoFromPackageJson"; 5 | import { getWorkspaces, getWorkspacesAsync } from "./workspaces/getWorkspaces"; 6 | 7 | export function getPackageInfos(cwd: string) { 8 | const packageInfos: PackageInfos = {}; 9 | const workspacePackages = getWorkspaces(cwd); 10 | 11 | if (workspacePackages.length) { 12 | for (const pkg of workspacePackages) { 13 | packageInfos[pkg.name] = pkg.packageJson; 14 | } 15 | } else { 16 | const rootInfo = tryReadRootPackageJson(cwd); 17 | if (rootInfo) { 18 | packageInfos[rootInfo.name] = rootInfo; 19 | } 20 | } 21 | 22 | return packageInfos; 23 | } 24 | 25 | export async function getPackageInfosAsync(cwd: string) { 26 | const packageInfos: PackageInfos = {}; 27 | const workspacePackages = await getWorkspacesAsync(cwd); 28 | 29 | if (workspacePackages.length) { 30 | for (const pkg of workspacePackages) { 31 | packageInfos[pkg.name] = pkg.packageJson; 32 | } 33 | } else { 34 | const rootInfo = tryReadRootPackageJson(cwd); 35 | if (rootInfo) { 36 | packageInfos[rootInfo.name] = rootInfo; 37 | } 38 | } 39 | 40 | return packageInfos; 41 | } 42 | 43 | function tryReadRootPackageJson(cwd: string): PackageInfo | undefined { 44 | const packageJsonPath = path.join(cwd, "package.json"); 45 | if (fs.existsSync(packageJsonPath)) { 46 | try { 47 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); 48 | return infoFromPackageJson(packageJson, packageJsonPath); 49 | } catch (e) { 50 | throw new Error(`Invalid package.json file detected ${packageJsonPath}: ${(e as Error)?.message || e}`); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/getPackagePaths.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import glob, { type Options as GlobOptions } from "fast-glob"; 3 | import { isCachingEnabled } from "./isCachingEnabled"; 4 | 5 | const packagePathsCache: { [root: string]: string[] } = {}; 6 | const globOptions: GlobOptions = { 7 | absolute: true, 8 | ignore: ["**/node_modules/**", "**/__fixtures__/**"], 9 | stats: false, 10 | }; 11 | 12 | /** 13 | * Given package folder globs (such as those from package.json `workspaces`) and a workspace root 14 | * directory, get paths to actual package folders. 15 | */ 16 | export function getPackagePaths(root: string, packageGlobs: string[]): string[] { 17 | if (isCachingEnabled() && packagePathsCache[root]) { 18 | return packagePathsCache[root]; 19 | } 20 | 21 | packagePathsCache[root] = glob 22 | .sync(getPackageJsonGlobs(packageGlobs), { cwd: root, ...globOptions }) 23 | .map(getResultPackagePath); 24 | 25 | return packagePathsCache[root]; 26 | } 27 | 28 | /** 29 | * Given package folder globs (such as those from package.json `workspaces`) and a workspace root 30 | * directory, get paths to actual package folders. 31 | */ 32 | export async function getPackagePathsAsync(root: string, packageGlobs: string[]): Promise { 33 | if (isCachingEnabled() && packagePathsCache[root]) { 34 | return packagePathsCache[root]; 35 | } 36 | 37 | packagePathsCache[root] = (await glob(getPackageJsonGlobs(packageGlobs), { cwd: root, ...globOptions })).map( 38 | getResultPackagePath 39 | ); 40 | 41 | return packagePathsCache[root]; 42 | } 43 | 44 | function getPackageJsonGlobs(packageGlobs: string[]) { 45 | return packageGlobs.map((glob) => path.join(glob, "package.json").replace(/\\/g, "/")); 46 | } 47 | 48 | function getResultPackagePath(packageJsonPath: string) { 49 | const packagePath = path.dirname(packageJsonPath); 50 | return path.sep === "/" ? packagePath : packagePath.replace(/\//g, path.sep); 51 | } 52 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/git/getDefaultRemote.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { findGitRoot } from "../paths"; 4 | import { PackageInfo } from "../types/PackageInfo"; 5 | import { getRepositoryName } from "./getRepositoryName"; 6 | import { git } from "./git"; 7 | 8 | export type GetDefaultRemoteOptions = { 9 | /** Get repository info relative to this directory. */ 10 | cwd: string; 11 | /** 12 | * If true, throw an error if remote info can't be found, or if a `repository` is not specified 13 | * in package.json and no matching remote is found. 14 | */ 15 | strict?: boolean; 16 | /** If true, log debug messages about how the remote was chosen */ 17 | verbose?: boolean; 18 | }; 19 | 20 | /** 21 | * Get the name of the default remote: the one matching the `repository` field in package.json. 22 | * Throws if `options.cwd` is not in a git repo or there's no package.json at the repo root. 23 | * 24 | * The order of preference for returned remotes is: 25 | * 1. If `repository` is defined in package.json, the remote with a matching URL (if `options.strict` 26 | * is true, throws an error if no matching remote exists) 27 | * 2. `upstream` if defined 28 | * 3. `origin` if defined 29 | * 4. The first defined remote 30 | * 5. If there are no defined remotes: throws an error if `options.strict` is true; otherwise returns `origin` 31 | * 32 | * @returns The name of the inferred default remote. 33 | */ 34 | export function getDefaultRemote(options: GetDefaultRemoteOptions): string; 35 | /** @deprecated Use the object param version */ 36 | export function getDefaultRemote(cwd: string): string; 37 | export function getDefaultRemote(cwdOrOptions: string | GetDefaultRemoteOptions) { 38 | const options = typeof cwdOrOptions === "string" ? { cwd: cwdOrOptions } : cwdOrOptions; 39 | const { cwd, strict, verbose } = options; 40 | 41 | const log = (message: string) => verbose && console.log(message); 42 | const logOrThrow = (message: string) => { 43 | if (strict) { 44 | throw new Error(message); 45 | } 46 | log(message); 47 | }; 48 | 49 | const gitRoot = findGitRoot(cwd); 50 | 51 | let packageJson: Partial = {}; 52 | const packageJsonPath = path.join(gitRoot, "package.json"); 53 | try { 54 | packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8").trim()); 55 | } catch (e) { 56 | logOrThrow(`Could not read "${packageJsonPath}"`); 57 | } 58 | 59 | const { repository } = packageJson; 60 | const repositoryUrl = typeof repository === "string" ? repository : (repository && repository.url) || ""; 61 | if (!repositoryUrl) { 62 | // This is always logged because it's strongly recommended to fix 63 | console.log( 64 | `Valid "repository" key not found in "${packageJsonPath}". Consider adding this info for more accurate git remote detection.` 65 | ); 66 | } 67 | /** Repository full name (owner and repo name) specified in package.json */ 68 | const repositoryName = getRepositoryName(repositoryUrl); 69 | 70 | const remotesResult = git(["remote", "-v"], { cwd }); 71 | if (!remotesResult.success) { 72 | logOrThrow(`Could not determine available git remotes under "${cwd}"`); 73 | } 74 | 75 | /** Mapping from remote URL to full name (owner and repo name) */ 76 | const remotes: { [remoteRepoUrl: string]: string } = {}; 77 | remotesResult.stdout.split("\n").forEach((line) => { 78 | const [remoteName, remoteUrl] = line.split(/\s+/); 79 | const remoteRepoName = getRepositoryName(remoteUrl); 80 | if (remoteRepoName) { 81 | remotes[remoteRepoName] = remoteName; 82 | } 83 | }); 84 | 85 | if (repositoryName) { 86 | // If the repository name was found in package.json, check for a matching remote 87 | if (remotes[repositoryName]) { 88 | return remotes[repositoryName]; 89 | } 90 | 91 | // If `strict` is true, and repositoryName is found, there MUST be a matching remote 92 | logOrThrow(`Could not find remote pointing to repository "${repositoryName}".`); 93 | } 94 | 95 | // Default to upstream or origin if available, or the first remote otherwise 96 | const allRemoteNames = Object.values(remotes); 97 | const fallbacks = ["upstream", "origin", ...allRemoteNames]; 98 | for (const fallback of fallbacks) { 99 | if (allRemoteNames.includes(fallback)) { 100 | log(`Default to remote "${fallback}"`); 101 | return fallback; 102 | } 103 | } 104 | 105 | // If we get here, no git remotes were found. This should probably always be an error (since 106 | // subsequent operations which require a remote likely won't work), but to match old behavior, 107 | // still default to "origin" unless `strict` is true. 108 | logOrThrow(`Could not find any remotes in git repo at "${gitRoot}".`); 109 | log(`Assuming default remote "origin".`); 110 | return "origin"; 111 | } 112 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/git/getDefaultRemoteBranch.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultRemote, GetDefaultRemoteOptions } from "./getDefaultRemote"; 2 | import { git } from "./git"; 3 | import { getDefaultBranch } from "./gitUtilities"; 4 | 5 | export type GetDefaultRemoteBranchOptions = GetDefaultRemoteOptions & { 6 | /** Name of branch to use. If undefined, uses the default branch name (falling back to `master`). */ 7 | branch?: string; 8 | }; 9 | 10 | /** 11 | * Gets a reference to `options.branch` or the default branch relative to the default remote. 12 | * (See {@link getDefaultRemote} for how the default remote is determined.) 13 | * Throws if `options.cwd` is not in a git repo or there's no package.json at the repo root. 14 | * @returns A branch reference like `upstream/master` or `origin/master`. 15 | */ 16 | export function getDefaultRemoteBranch(options: GetDefaultRemoteBranchOptions): string; 17 | /** 18 | * First param: `branch`. Second param: `cwd`. See {@link GetDefaultRemoteBranchOptions} for more info. 19 | * (This had to be changed to `...args` to avoid a conflict with the object param version.) 20 | * @deprecated Use the object param version 21 | */ 22 | export function getDefaultRemoteBranch(...args: string[]): string; 23 | export function getDefaultRemoteBranch(...args: (string | GetDefaultRemoteBranchOptions)[]) { 24 | const [branchOrOptions, argsCwd] = args; 25 | const options = 26 | typeof branchOrOptions === "string" 27 | ? ({ branch: branchOrOptions, cwd: argsCwd } as GetDefaultRemoteBranchOptions) 28 | : branchOrOptions; 29 | const { cwd, branch } = options; 30 | 31 | const defaultRemote = getDefaultRemote(options); 32 | 33 | if (branch) { 34 | return `${defaultRemote}/${branch}`; 35 | } 36 | 37 | const showRemote = git(["remote", "show", defaultRemote], { cwd }); 38 | let remoteDefaultBranch: string | undefined; 39 | 40 | if (showRemote.success) { 41 | /** 42 | * `showRemote.stdout` is something like this: 43 | * 44 | * * remote origin 45 | * Fetch URL: .../monorepo-upstream 46 | * Push URL: .../monorepo-upstream 47 | * HEAD branch: main 48 | */ 49 | remoteDefaultBranch = showRemote.stdout 50 | .split(/\n/) 51 | .find((line) => line.includes("HEAD branch")) 52 | ?.replace(/^\s*HEAD branch:\s+/, ""); 53 | } 54 | 55 | return `${defaultRemote}/${remoteDefaultBranch || getDefaultBranch(cwd)}`; 56 | } 57 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/git/getRepositoryName.ts: -------------------------------------------------------------------------------- 1 | import gitUrlParse from "git-url-parse"; 2 | 3 | /** 4 | * Get a repository full name (owner and repo, plus organization for ADO/VSO) from a repository URL, 5 | * including special handling for the many ADO/VSO URL formats. 6 | * 7 | * Examples: 8 | * - returns `microsoft/workspace-tools` for `https://github.com/microsoft/workspace-tools.git` 9 | * - returns `foo/bar/some-repo` for `https://dev.azure.com/foo/bar/_git/some-repo` 10 | */ 11 | export function getRepositoryName(url: string) { 12 | try { 13 | // Mostly use this standard library, but fix some VSO/ADO-specific quirks to account for the 14 | // fact that all of the following URLs should be considered to point to the same repo: 15 | // https://foo.visualstudio.com/bar/_git/some-repo 16 | // https://foo.visualstudio.com/DefaultCollection/bar/_git/some-repo 17 | // https://user:token@foo.visualstudio.com/DefaultCollection/bar/_git/some-repo 18 | // https://foo.visualstudio.com/DefaultCollection/bar/_git/_optimized/some-repo 19 | // foo@vs-ssh.visualstudio.com:v3/foo/bar/some-repo 20 | // https://dev.azure.com/foo/bar/_git/some-repo 21 | // https://dev.azure.com/foo/bar/_git/_optimized/some-repo 22 | // https://user@dev.azure.com/foo/bar/_git/some-repo 23 | // git@ssh.dev.azure.com:v3/foo/bar/some-repo 24 | const parsedUrl = gitUrlParse(url.replace("/_optimized/", "/").replace("/DefaultCollection/", "/")); 25 | 26 | // `host` is set in `parse-url` but not documented... https://github.com/IonicaBizau/parse-url/blob/c830d48647f33c054745a916cf7c4c58722f4b25/src/index.js#L28 27 | const host: string = (parsedUrl as any).host || ""; 28 | const isVSO = host.endsWith(".visualstudio.com"); 29 | if (!isVSO && host !== "dev.azure.com" && host !== "ssh.dev.azure.com") { 30 | return parsedUrl.full_name; 31 | } 32 | 33 | // As of writing, ADO and VSO SSH URLs are parsed completely wrong 34 | const sshMatch = parsedUrl.full_name.match( 35 | /(vs-ssh\.visualstudio\.com|ssh\.dev\.azure\.com):v\d+\/([^/]+)\/([^/]+)/ 36 | ); 37 | if (sshMatch) { 38 | return `${sshMatch[2]}/${sshMatch[3]}/${parsedUrl.name}`; 39 | } 40 | 41 | // As of writing, full_name is wrong for enough variants of ADO and VSO URLs that it 42 | // makes more sense to just build it manually. 43 | let organization: string | undefined = parsedUrl.organization; 44 | if (!organization && isVSO) { 45 | // organization is missing or wrong for VSO 46 | organization = host.match(/([^.@]+)\.visualstudio\.com$/)?.[1]; 47 | } 48 | return `${organization}/${parsedUrl.owner}/${parsedUrl.name}`; 49 | } catch (err) { 50 | return ""; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/git/git.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Basic git wrappers 3 | // 4 | 5 | import { SpawnSyncReturns } from "child_process"; 6 | import { spawnSync, SpawnSyncOptions } from "child_process"; 7 | 8 | export class GitError extends Error { 9 | public originalError: unknown; 10 | constructor(message: string, originalError?: unknown) { 11 | if (originalError instanceof Error) { 12 | super(`${message}: ${originalError.message}`); 13 | } else { 14 | super(message); 15 | } 16 | this.originalError = originalError; 17 | } 18 | } 19 | 20 | /** 21 | * A global maxBuffer override for all git operations. 22 | * Bumps up the default to 500MB instead of 1MB. 23 | * Override this value with the `GIT_MAX_BUFFER` environment variable. 24 | */ 25 | const defaultMaxBuffer = process.env.GIT_MAX_BUFFER ? parseInt(process.env.GIT_MAX_BUFFER) : 500 * 1024 * 1024; 26 | 27 | const isDebug = !!process.env.GIT_DEBUG; 28 | 29 | export type GitProcessOutput = { 30 | stderr: string; 31 | stdout: string; 32 | success: boolean; 33 | } & Omit, "stdout" | "stderr">; 34 | 35 | /** Observes the git operations called from `git()` or `gitFailFast()` */ 36 | export type GitObserver = (args: string[], output: GitProcessOutput) => void; 37 | const observers: GitObserver[] = []; 38 | let observing: boolean; 39 | 40 | /** 41 | * Adds an observer for the git operations, e.g. for testing 42 | * @returns a function to remove the observer 43 | */ 44 | export function addGitObserver(observer: GitObserver) { 45 | observers.push(observer); 46 | return () => removeGitObserver(observer); 47 | } 48 | 49 | /** Clear all git observers */ 50 | export function clearGitObservers() { 51 | observers.splice(0, observers.length); 52 | } 53 | 54 | /** Remove a git observer */ 55 | function removeGitObserver(observer: GitObserver) { 56 | const index = observers.indexOf(observer); 57 | if (index > -1) { 58 | observers.splice(index, 1); 59 | } 60 | } 61 | 62 | /** 63 | * Runs git command - use this for read-only commands. 64 | * `gitFailFast` is recommended for commands that make changes to the filesystem. 65 | * 66 | * The caller is responsible for validating the input. 67 | * `shell` will always be set to false. 68 | */ 69 | export function git(args: string[], options?: SpawnSyncOptions): GitProcessOutput { 70 | isDebug && console.log(`git ${args.join(" ")}`); 71 | if (args.some((arg) => arg.startsWith("--upload-pack"))) { 72 | // This is a security issue and not needed for any expected usage of this library. 73 | throw new GitError("git command contains --upload-pack, which is not allowed: " + args.join(" ")); 74 | } 75 | 76 | const results = spawnSync("git", args, { maxBuffer: defaultMaxBuffer, ...options, shell: false }); 77 | 78 | const output: GitProcessOutput = { 79 | ...results, 80 | // these may be undefined if stdio: inherit is set 81 | stderr: (results.stderr || "").toString().trimEnd(), 82 | stdout: (results.stdout || "").toString().trimEnd(), 83 | success: results.status === 0, 84 | }; 85 | 86 | if (isDebug) { 87 | console.log("exited with code " + results.status); 88 | output.stdout && console.log("git stdout:\n", output.stdout); 89 | output.stderr && console.warn("git stderr:\n", output.stderr); 90 | } 91 | 92 | // notify observers, flipping the observing bit to prevent infinite loops 93 | if (!observing) { 94 | observing = true; 95 | for (const observer of observers) { 96 | observer(args, output); 97 | } 98 | observing = false; 99 | } 100 | 101 | return output; 102 | } 103 | 104 | /** 105 | * Runs git command and throws an error if it fails. 106 | * Use this for commands that make changes to the filesystem. 107 | * 108 | * The caller is responsible for validating the input. 109 | * `shell` will always be set to false. 110 | */ 111 | export function gitFailFast(args: string[], options?: SpawnSyncOptions & { noExitCode?: boolean }) { 112 | const gitResult = git(args, options); 113 | if (!gitResult.success) { 114 | if (!options?.noExitCode) { 115 | process.exitCode = 1; 116 | } 117 | 118 | throw new GitError(`CRITICAL ERROR: running git command: git ${args.join(" ")}! 119 | ${gitResult.stdout?.toString().trimEnd()} 120 | ${gitResult.stderr?.toString().trimEnd()}`); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/git/gitUtilities.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Assorted other git utilities 3 | // (could be split into separate files later if desired) 4 | // 5 | 6 | import { git, GitError, GitProcessOutput } from "./git"; 7 | 8 | export function getUntrackedChanges(cwd: string) { 9 | try { 10 | return processGitOutput(git(["ls-files", "--others", "--exclude-standard"], { cwd })); 11 | } catch (e) { 12 | throw new GitError(`Cannot gather information about untracked changes`, e); 13 | } 14 | } 15 | 16 | export function fetchRemote(remote: string, cwd: string) { 17 | const results = git(["fetch", "--", remote], { cwd }); 18 | 19 | if (!results.success) { 20 | throw new GitError(`Cannot fetch remote "${remote}"`); 21 | } 22 | } 23 | 24 | export function fetchRemoteBranch(remote: string, remoteBranch: string, cwd: string) { 25 | const results = git(["fetch", "--", remote, remoteBranch], { cwd }); 26 | 27 | if (!results.success) { 28 | throw new GitError(`Cannot fetch branch "${remoteBranch}" from remote "${remote}"`); 29 | } 30 | } 31 | 32 | /** 33 | * Gets all the changes that have not been staged yet 34 | * @param cwd 35 | */ 36 | export function getUnstagedChanges(cwd: string) { 37 | try { 38 | return processGitOutput(git(["--no-pager", "diff", "--name-only", "--relative"], { cwd })); 39 | } catch (e) { 40 | throw new GitError(`Cannot gather information about unstaged changes`, e); 41 | } 42 | } 43 | 44 | export function getChanges(branch: string, cwd: string) { 45 | try { 46 | return processGitOutput(git(["--no-pager", "diff", "--relative", "--name-only", branch + "..."], { cwd })); 47 | } catch (e) { 48 | throw new GitError(`Cannot gather information about changes`, e); 49 | } 50 | } 51 | 52 | /** 53 | * Gets all the changes between the branch and the merge-base 54 | */ 55 | export function getBranchChanges(branch: string, cwd: string) { 56 | return getChangesBetweenRefs(branch, "", [], "", cwd); 57 | } 58 | 59 | export function getChangesBetweenRefs(fromRef: string, toRef: string, options: string[], pattern: string, cwd: string) { 60 | try { 61 | return processGitOutput( 62 | git( 63 | [ 64 | "--no-pager", 65 | "diff", 66 | "--name-only", 67 | "--relative", 68 | ...options, 69 | `${fromRef}...${toRef}`, 70 | ...(pattern ? ["--", pattern] : []), 71 | ], 72 | { cwd } 73 | ) 74 | ); 75 | } catch (e) { 76 | throw new GitError(`Cannot gather information about change between refs changes (${fromRef} to ${toRef})`, e); 77 | } 78 | } 79 | 80 | export function getStagedChanges(cwd: string) { 81 | try { 82 | return processGitOutput(git(["--no-pager", "diff", "--relative", "--staged", "--name-only"], { cwd })); 83 | } catch (e) { 84 | throw new GitError(`Cannot gather information about staged changes`, e); 85 | } 86 | } 87 | 88 | export function getRecentCommitMessages(branch: string, cwd: string) { 89 | try { 90 | const results = git(["log", "--decorate", "--pretty=format:%s", `${branch}..HEAD`], { cwd }); 91 | 92 | if (!results.success) { 93 | return []; 94 | } 95 | 96 | return results.stdout 97 | .split(/\n/) 98 | .map((line) => line.trim()) 99 | .filter((line) => !!line); 100 | } catch (e) { 101 | throw new GitError(`Cannot gather information about recent commits`, e); 102 | } 103 | } 104 | 105 | export function getUserEmail(cwd: string) { 106 | try { 107 | const results = git(["config", "user.email"], { cwd }); 108 | 109 | return results.success ? results.stdout : null; 110 | } catch (e) { 111 | throw new GitError(`Cannot gather information about user.email`, e); 112 | } 113 | } 114 | 115 | export function getBranchName(cwd: string) { 116 | try { 117 | const results = git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd }); 118 | 119 | return results.success ? results.stdout : null; 120 | } catch (e) { 121 | throw new GitError(`Cannot get branch name`, e); 122 | } 123 | } 124 | 125 | export function getFullBranchRef(branch: string, cwd: string) { 126 | const showRefResults = git(["show-ref", "--heads", branch], { cwd }); 127 | 128 | return showRefResults.success ? showRefResults.stdout.split(" ")[1] : null; 129 | } 130 | 131 | export function getShortBranchName(fullBranchRef: string, cwd: string) { 132 | const showRefResults = git(["name-rev", "--name-only", fullBranchRef], { 133 | cwd, 134 | }); 135 | 136 | return showRefResults.success ? showRefResults.stdout : null; 137 | } 138 | 139 | export function getCurrentHash(cwd: string) { 140 | try { 141 | const results = git(["rev-parse", "HEAD"], { cwd }); 142 | 143 | return results.success ? results.stdout : null; 144 | } catch (e) { 145 | throw new GitError(`Cannot get current git hash`, e); 146 | } 147 | } 148 | 149 | /** 150 | * Get the commit hash in which the file was first added. 151 | */ 152 | export function getFileAddedHash(filename: string, cwd: string) { 153 | const results = git(["rev-list", "--max-count=1", "HEAD", filename], { cwd }); 154 | 155 | if (results.success) { 156 | return results.stdout.trim(); 157 | } 158 | 159 | return undefined; 160 | } 161 | 162 | export function init(cwd: string, email?: string, username?: string) { 163 | git(["init"], { cwd }); 164 | 165 | const configLines = git(["config", "--list"], { cwd }).stdout.split("\n"); 166 | 167 | if (!configLines.find((line) => line.includes("user.name"))) { 168 | if (!username) { 169 | throw new GitError("must include a username when initializing git repo"); 170 | } 171 | git(["config", "user.name", username], { cwd }); 172 | } 173 | 174 | if (!configLines.find((line) => line.includes("user.email"))) { 175 | if (!email) { 176 | throw new Error("must include a email when initializing git repo"); 177 | } 178 | git(["config", "user.email", email], { cwd }); 179 | } 180 | } 181 | 182 | export function stage(patterns: string[], cwd: string) { 183 | try { 184 | patterns.forEach((pattern) => { 185 | git(["add", pattern], { cwd }); 186 | }); 187 | } catch (e) { 188 | throw new GitError(`Cannot stage changes`, e); 189 | } 190 | } 191 | 192 | export function commit(message: string, cwd: string, options: string[] = []) { 193 | try { 194 | const commitResults = git(["commit", "-m", message, ...options], { cwd }); 195 | 196 | if (!commitResults.success) { 197 | throw new Error(`Cannot commit changes: ${commitResults.stdout} ${commitResults.stderr}`); 198 | } 199 | } catch (e) { 200 | throw new GitError(`Cannot commit changes`, e); 201 | } 202 | } 203 | 204 | export function stageAndCommit(patterns: string[], message: string, cwd: string, commitOptions: string[] = []) { 205 | stage(patterns, cwd); 206 | commit(message, cwd, commitOptions); 207 | } 208 | 209 | export function revertLocalChanges(cwd: string) { 210 | const stash = `workspace-tools_${new Date().getTime()}`; 211 | git(["stash", "push", "-u", "-m", stash], { cwd }); 212 | const results = git(["stash", "list"]); 213 | if (results.success) { 214 | const lines = results.stdout.split(/\n/); 215 | const foundLine = lines.find((line) => line.includes(stash)); 216 | 217 | if (foundLine) { 218 | const matched = foundLine.match(/^[^:]+/); 219 | if (matched) { 220 | git(["stash", "drop", matched[0]]); 221 | return true; 222 | } 223 | } 224 | } 225 | 226 | return false; 227 | } 228 | 229 | export function getParentBranch(cwd: string) { 230 | const branchName = getBranchName(cwd); 231 | 232 | if (!branchName || branchName === "HEAD") { 233 | return null; 234 | } 235 | 236 | const showBranchResult = git(["show-branch", "-a"], { cwd }); 237 | 238 | if (showBranchResult.success) { 239 | const showBranchLines = showBranchResult.stdout.split(/\n/); 240 | const parentLine = showBranchLines.find( 241 | (line) => line.includes("*") && !line.includes(branchName) && !line.includes("publish_") 242 | ); 243 | 244 | const matched = parentLine?.match(/\[(.*)\]/); 245 | return matched ? matched[1] : null; 246 | } 247 | 248 | return null; 249 | } 250 | 251 | export function getRemoteBranch(branch: string, cwd: string) { 252 | const results = git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", `${branch}@\{u\}`], { cwd }); 253 | 254 | if (results.success) { 255 | return results.stdout.trim(); 256 | } 257 | 258 | return null; 259 | } 260 | 261 | export function parseRemoteBranch(branch: string) { 262 | const firstSlashPos = branch.indexOf("/", 0); 263 | const remote = branch.substring(0, firstSlashPos); 264 | const remoteBranch = branch.substring(firstSlashPos + 1); 265 | 266 | return { 267 | remote, 268 | remoteBranch, 269 | }; 270 | } 271 | 272 | /** 273 | * Gets the default branch based on `git config init.defaultBranch`, falling back to `master`. 274 | */ 275 | export function getDefaultBranch(cwd: string) { 276 | const result = git(["config", "init.defaultBranch"], { cwd }); 277 | 278 | // Default to the legacy 'master' for backwards compat and old git clients 279 | return result.success ? result.stdout.trim() : "master"; 280 | } 281 | 282 | export function listAllTrackedFiles(patterns: string[], cwd: string) { 283 | const results = git(["ls-files", ...patterns], { cwd }); 284 | 285 | return results.success && results.stdout.trim() ? results.stdout.trim().split(/\n/) : []; 286 | } 287 | 288 | function processGitOutput(output: GitProcessOutput) { 289 | if (!output.success) { 290 | if (output.stderr) { 291 | throw new Error(output.stderr); 292 | } 293 | return []; 294 | } 295 | 296 | return output.stdout 297 | .split(/\n/) 298 | .map((line) => line.trim()) 299 | .filter((line) => !!line && !line.includes("node_modules")); 300 | } 301 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/git/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./git"; 2 | export * from "./getDefaultRemote"; 3 | export * from "./getDefaultRemoteBranch"; 4 | export * from "./gitUtilities"; 5 | // getRepositoryName is not currently exported; could be changed if it would be useful externally 6 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/graph/createDependencyMap.ts: -------------------------------------------------------------------------------- 1 | import { getPackageDependencies, PackageDependenciesOptions } from "./getPackageDependencies"; 2 | import { PackageInfos } from "../types/PackageInfo"; 3 | import { getPackageInfos, getPackageInfosAsync } from "../getPackageInfos"; 4 | 5 | export interface DependencyMap { 6 | dependencies: Map>; 7 | dependents: Map>; 8 | } 9 | 10 | export function createDependencyMap( 11 | packages: PackageInfos, 12 | options: PackageDependenciesOptions = { withDevDependencies: true, withPeerDependencies: false } 13 | ): DependencyMap { 14 | const map = { 15 | dependencies: new Map>(), 16 | dependents: new Map>(), 17 | }; 18 | 19 | const internalPackages = new Set(Object.keys(packages)); 20 | 21 | for (const [pkg, info] of Object.entries(packages)) { 22 | const deps = getPackageDependencies(info, internalPackages, options); 23 | for (const dep of deps) { 24 | if (!map.dependencies.has(pkg)) { 25 | map.dependencies.set(pkg, new Set()); 26 | } 27 | map.dependencies.get(pkg)!.add(dep); 28 | 29 | if (!map.dependents.has(dep)) { 30 | map.dependents.set(dep, new Set()); 31 | } 32 | map.dependents.get(dep)!.add(pkg); 33 | } 34 | } 35 | return map; 36 | } 37 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/graph/createPackageGraph.ts: -------------------------------------------------------------------------------- 1 | import type { PackageInfos } from "../types/PackageInfo"; 2 | import type { DependencyMap } from "./createDependencyMap"; 3 | import type { PackageGraph } from "../types/PackageGraph"; 4 | 5 | import { createDependencyMap } from "./createDependencyMap"; 6 | import micromatch from "micromatch"; 7 | 8 | // Reference: https://github.com/pnpm/pnpm/blob/597047fc056dd25b83638a9ab3df0df1c555ee49/packages/filter-workspace-packages/src/parsePackageSelector.ts 9 | export interface PackageGraphFilter { 10 | namePatterns: string[]; 11 | includeDependencies?: boolean; 12 | includeDependents?: boolean; 13 | withDevDependencies?: boolean; 14 | withPeerDependencies?: boolean; 15 | withOptionalDependencies?: boolean; 16 | } 17 | 18 | /** Package graph visitor is called as it visits every package in dependency order */ 19 | interface PackageGraphVisitor { 20 | (pkg: string, dependencies: string[], dependents: string[]): void; 21 | } 22 | 23 | export function createPackageGraph( 24 | packages: PackageInfos, 25 | filters?: PackageGraphFilter[] | PackageGraphFilter 26 | ): PackageGraph { 27 | /** Set of packages being accumulated as the graph is filtered */ 28 | const packageSet = new Set(); 29 | 30 | /** Array of package names & its dependencies being accumulated as the graph is filtered */ 31 | const edges: PackageGraph["dependencies"] = []; 32 | 33 | const edgeKeys: Set = new Set(); 34 | const dependencyMapCache = new Map(); 35 | 36 | function visitorForFilter( 37 | filter: PackageGraphFilter | undefined, 38 | pkg: string, 39 | dependencies: string[], 40 | dependents: string[] 41 | ) { 42 | packageSet.add(pkg); 43 | 44 | if (!filter || (filter.includeDependencies && dependencies)) { 45 | for (const dep of dependencies) { 46 | const key = edgeKey(pkg, dep); 47 | 48 | if (!edgeKeys.has(key)) { 49 | edgeKeys.add(key); 50 | edges.push({ name: pkg, dependency: dep }); 51 | } 52 | 53 | packageSet.add(dep); 54 | } 55 | } 56 | 57 | if (!filter || (filter.includeDependents && dependents)) { 58 | for (const dep of dependents) { 59 | const key = edgeKey(dep, pkg); 60 | 61 | if (!edgeKeys.has(key)) { 62 | edgeKeys.add(key); 63 | edges.push({ name: dep, dependency: pkg }); 64 | } 65 | 66 | packageSet.add(dep); 67 | } 68 | } 69 | } 70 | 71 | if (filters) { 72 | filters = Array.isArray(filters) ? filters : [filters]; 73 | for (const filter of filters) { 74 | const visitor = visitorForFilter.bind(undefined, filter); 75 | const dependencyMap = getDependencyMapForFilter(packages, filter); 76 | visitPackageGraph(packages, dependencyMap, visitor, filter); 77 | } 78 | } else { 79 | const visitor = visitorForFilter.bind(undefined, undefined); 80 | const dependencyMap = getDependencyMapForFilter(packages); 81 | visitPackageGraph(packages, dependencyMap, visitor); 82 | } 83 | 84 | return { packages: [...packageSet], dependencies: edges }; 85 | 86 | /** Calculates a key for checking if an edge is already added */ 87 | function edgeKey(name: string, dependency: string) { 88 | return `${name}->${dependency}`; 89 | } 90 | 91 | /** Gets the dependencyMap for a filter, using a cache based on filter options */ 92 | function getDependencyMapForFilter(packages: PackageInfos, filter?: PackageGraphFilter) { 93 | const cacheKey = getCacheKeyForFilter(filter); 94 | if (!dependencyMapCache.has(cacheKey)) { 95 | const dependencyMap = createDependencyMap(packages, filter); 96 | dependencyMapCache.set(cacheKey, dependencyMap); 97 | } 98 | return dependencyMapCache.get(cacheKey)!; 99 | } 100 | 101 | /** Generates a cache key based on the filter options */ 102 | function getCacheKeyForFilter(filter?: PackageGraphFilter): string { 103 | if (!filter) { 104 | return "default"; 105 | } 106 | const options = [ 107 | filter.withDevDependencies ? "dev" : "", 108 | filter.withPeerDependencies ? "peer" : "", 109 | filter.withOptionalDependencies ? "optional" : "", 110 | ] 111 | .filter(Boolean) 112 | .join("_"); 113 | return options || "prod"; 114 | } 115 | } 116 | 117 | function visitPackageGraph( 118 | packages: PackageInfos, 119 | dependencyMap: DependencyMap, 120 | visitor: PackageGraphVisitor, 121 | filter?: PackageGraphFilter 122 | ) { 123 | const visited = new Set(); 124 | const packageNames = Object.keys(packages); 125 | 126 | const stack: string[] = filter ? micromatch(packageNames, filter.namePatterns) : packageNames; 127 | 128 | while (stack.length > 0) { 129 | const pkg = stack.pop()!; 130 | 131 | if (visited.has(pkg)) { 132 | continue; 133 | } 134 | 135 | const nextPkgs: Set = new Set(); 136 | let dependencies: string[] = []; 137 | let dependents: string[] = []; 138 | 139 | if (!filter || filter.includeDependencies) { 140 | dependencies = [...(dependencyMap.dependencies.get(pkg) ?? [])]; 141 | for (const dep of dependencies) { 142 | nextPkgs.add(dep); 143 | } 144 | } 145 | 146 | if (!filter || filter.includeDependents) { 147 | dependents = [...(dependencyMap.dependents.get(pkg) ?? [])]; 148 | for (const dep of dependents) { 149 | nextPkgs.add(dep); 150 | } 151 | } 152 | 153 | visitor(pkg, dependencies, dependents); 154 | 155 | visited.add(pkg); 156 | 157 | if (nextPkgs.size > 0) { 158 | for (const nextPkg of nextPkgs) { 159 | stack.push(nextPkg); 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/graph/getPackageDependencies.ts: -------------------------------------------------------------------------------- 1 | import { PackageInfo } from "../types/PackageInfo"; 2 | 3 | export interface PackageDependenciesOptions { 4 | withDevDependencies?: boolean; 5 | withPeerDependencies?: boolean; 6 | withOptionalDependencies?: boolean; 7 | } 8 | 9 | function isValidDependency(info: PackageInfo, dep: string): boolean { 10 | // check if the dependency range is specified by an external package like npm: or file: 11 | const range = 12 | info.dependencies?.[dep] || 13 | info.devDependencies?.[dep] || 14 | info.peerDependencies?.[dep] || 15 | info.optionalDependencies?.[dep]; 16 | 17 | // this case should not happen by this point, but we will handle it anyway 18 | if (!range) { 19 | return false; 20 | } 21 | 22 | return !range.startsWith("npm:") && !range.startsWith("file:"); 23 | } 24 | 25 | export function getPackageDependencies( 26 | info: PackageInfo, 27 | packages: Set, 28 | options: PackageDependenciesOptions = { withDevDependencies: true } 29 | ): string[] { 30 | const deps: string[] = []; 31 | 32 | if (info.dependencies) { 33 | for (const dep of Object.keys(info.dependencies)) { 34 | if (dep !== info.name && packages.has(dep)) { 35 | deps.push(dep); 36 | } 37 | } 38 | } 39 | 40 | if (info.devDependencies && options.withDevDependencies) { 41 | for (const dep of Object.keys(info.devDependencies)) { 42 | if (dep !== info.name && packages.has(dep)) { 43 | deps.push(dep); 44 | } 45 | } 46 | } 47 | 48 | if (info.peerDependencies && options.withPeerDependencies) { 49 | for (const dep of Object.keys(info.peerDependencies)) { 50 | if (dep !== info.name && packages.has(dep)) { 51 | deps.push(dep); 52 | } 53 | } 54 | } 55 | 56 | if (info.optionalDependencies && options.withOptionalDependencies) { 57 | for (const dep of Object.keys(info.optionalDependencies)) { 58 | if (dep !== info.name && packages.has(dep)) { 59 | deps.push(dep); 60 | } 61 | } 62 | } 63 | 64 | const filteredDeps = deps.filter((dep) => isValidDependency(info, dep)); 65 | 66 | return filteredDeps; 67 | } 68 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/graph/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createPackageGraph"; 2 | export * from "./createDependencyMap"; 3 | export * from "./getPackageDependencies"; 4 | import { PackageInfos } from "../types/PackageInfo"; 5 | import { createDependencyMap } from "./createDependencyMap"; 6 | 7 | /** 8 | * @deprecated - use createDependencyMap() instead 9 | * 10 | * Gets a map that has the package name as key, and its dependencies as values 11 | */ 12 | export function getDependentMap(packages: PackageInfos) { 13 | return createDependencyMap(packages).dependencies; 14 | } 15 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dependencies/index"; 2 | export * from "./getPackageInfos"; 3 | export * from "./git"; 4 | export * from "./graph/index"; 5 | export { setCachingEnabled } from "./isCachingEnabled"; 6 | export * from "./lockfile"; 7 | export * from "./paths"; 8 | export * from "./scope"; 9 | export * from "./types/PackageGraph"; 10 | export * from "./types/PackageInfo"; 11 | export * from "./types/WorkspaceInfo"; 12 | export * from "./workspaces/findWorkspacePath"; 13 | export * from "./workspaces/getWorkspaces"; 14 | export * from "./workspaces/getWorkspacePackagePaths"; 15 | export * from "./workspaces/getWorkspaceRoot"; 16 | export { getPnpmWorkspaceRoot, getPnpmWorkspaces } from "./workspaces/implementations/pnpm"; 17 | export { getRushWorkspaceRoot, getRushWorkspaces } from "./workspaces/implementations/rush"; 18 | export { getYarnWorkspaceRoot, getYarnWorkspaces } from "./workspaces/implementations/yarn"; 19 | export * from "./workspaces/getChangedPackages"; 20 | export * from "./workspaces/getPackagesByFiles"; 21 | export * from "./workspaces/listOfWorkspacePackageNames"; 22 | export * from "./workspaces/getAllPackageJsonFiles"; 23 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/infoFromPackageJson.ts: -------------------------------------------------------------------------------- 1 | import { PackageInfo } from "./types/PackageInfo"; 2 | 3 | export function infoFromPackageJson( 4 | packageJson: { 5 | name: string; 6 | version: string; 7 | dependencies?: { 8 | [dep: string]: string; 9 | }; 10 | devDependencies?: { 11 | [dep: string]: string; 12 | }; 13 | peerDependencies?: { 14 | [dep: string]: string; 15 | }; 16 | private?: boolean; 17 | pipeline?: any; 18 | scripts?: any; 19 | }, 20 | packageJsonPath: string 21 | ): PackageInfo { 22 | return { 23 | packageJsonPath, 24 | ...packageJson, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/isCachingEnabled.ts: -------------------------------------------------------------------------------- 1 | let cachingEnabled = true; 2 | 3 | /** Enable or disable caching for all utilities that support caching */ 4 | export function setCachingEnabled(enabled: boolean) { 5 | cachingEnabled = enabled; 6 | } 7 | 8 | export function isCachingEnabled() { 9 | return cachingEnabled; 10 | } 11 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/index.ts: -------------------------------------------------------------------------------- 1 | // NOTE: never place the import of lockfile implementation here, as it slows down the library as a whole 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { ParsedLock, PnpmLockFile, NpmLockFile, BerryLockFile } from "./types"; 5 | import { nameAtVersion } from "./nameAtVersion"; 6 | import { searchUp } from "../paths"; 7 | import { parsePnpmLock } from "./parsePnpmLock"; 8 | import { parseNpmLock } from "./parseNpmLock"; 9 | import { readYaml } from "./readYaml"; 10 | import { parseBerryLock } from "./parseBerryLock"; 11 | 12 | const memoization: { [path: string]: ParsedLock } = {}; 13 | 14 | export async function parseLockFile(packageRoot: string): Promise { 15 | const yarnLockPath = searchUp(["yarn.lock", "common/config/rush/yarn.lock"], packageRoot); 16 | 17 | // First, test out whether this works for yarn 18 | if (yarnLockPath) { 19 | if (memoization[yarnLockPath]) { 20 | return memoization[yarnLockPath]; 21 | } 22 | 23 | const yarnLock = fs.readFileSync(yarnLockPath, "utf-8"); 24 | 25 | const isBerry = 26 | yarnLock.includes("__metadata") || fs.existsSync(path.resolve(yarnLock.replace("yarn.lock", ".yarnrc.yml"))); 27 | 28 | let parsed: { 29 | type: "success" | "merge" | "conflict"; 30 | object: any; 31 | } = { 32 | type: "success", 33 | object: {}, 34 | }; 35 | 36 | if (isBerry) { 37 | const yaml = readYaml(yarnLockPath); 38 | parsed = parseBerryLock(yaml); 39 | } else { 40 | const parseYarnLock = (await import("@yarnpkg/lockfile")).parse; 41 | parsed = parseYarnLock(yarnLock); 42 | } 43 | 44 | memoization[yarnLockPath] = parsed; 45 | 46 | return parsed; 47 | } 48 | 49 | // Second, test out whether this works for pnpm 50 | let pnpmLockPath = searchUp(["pnpm-lock.yaml", "common/config/rush/pnpm-lock.yaml"], packageRoot); 51 | 52 | if (pnpmLockPath) { 53 | if (memoization[pnpmLockPath]) { 54 | return memoization[pnpmLockPath]; 55 | } 56 | 57 | const yaml = readYaml(pnpmLockPath); 58 | const parsed = parsePnpmLock(yaml); 59 | memoization[pnpmLockPath] = parsed; 60 | 61 | return memoization[pnpmLockPath]; 62 | } 63 | 64 | // Third, try for npm workspaces 65 | let npmLockPath = searchUp("package-lock.json", packageRoot); 66 | 67 | if (npmLockPath) { 68 | if (memoization[npmLockPath]) { 69 | return memoization[npmLockPath]; 70 | } 71 | 72 | let npmLockJson; 73 | try { 74 | npmLockJson = fs.readFileSync(npmLockPath, "utf-8"); 75 | } catch { 76 | throw new Error("Couldn't read package-lock.json"); 77 | } 78 | 79 | const npmLock: NpmLockFile = JSON.parse(npmLockJson.toString()); 80 | 81 | if (!npmLock?.lockfileVersion || npmLock.lockfileVersion < 2) { 82 | throw new Error( 83 | `Your package-lock.json version is not supported: lockfileVersion is ${npmLock.lockfileVersion}. You need npm version 7 or above and package-lock version 2 or above. Please, upgrade npm or choose a different package manager.` 84 | ); 85 | } 86 | 87 | memoization[npmLockPath] = parseNpmLock(npmLock); 88 | return memoization[npmLockPath]; 89 | } 90 | 91 | throw new Error( 92 | "You do not have yarn.lock, pnpm-lock.yaml or package-lock.json. Please use one of these package managers." 93 | ); 94 | } 95 | 96 | export { nameAtVersion }; 97 | export { queryLockFile } from "./queryLockFile"; 98 | export * from "./types"; 99 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/nameAtVersion.ts: -------------------------------------------------------------------------------- 1 | export function nameAtVersion(name: string, version: string): string { 2 | return `${name}@${version}`; 3 | } 4 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/parseBerryLock.ts: -------------------------------------------------------------------------------- 1 | import type { LockDependency, ParsedLock, BerryLockFile } from "./types"; 2 | 3 | export function parseBerryLock(yaml: BerryLockFile): ParsedLock { 4 | const results: { [key: string]: LockDependency } = {}; 5 | 6 | if (yaml) { 7 | for (const [keySpec, descriptor] of Object.entries(yaml)) { 8 | if (keySpec === "__metadata") { 9 | continue; 10 | } 11 | 12 | const keys = keySpec.split(", "); 13 | 14 | for (const key of keys) { 15 | const normalizedKey = normalizeKey(key); 16 | results[normalizedKey] = { 17 | version: descriptor.version, 18 | dependencies: descriptor.dependencies ?? {}, 19 | }; 20 | } 21 | } 22 | } 23 | 24 | return { 25 | object: results, 26 | type: "success", 27 | }; 28 | } 29 | 30 | // normalizes the version range as a key lookup 31 | function normalizeKey(key: string): string { 32 | if (key.includes("npm:")) { 33 | return key.replace(/npm:/, ""); 34 | } 35 | 36 | return key; 37 | } 38 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/parseNpmLock.ts: -------------------------------------------------------------------------------- 1 | import { nameAtVersion } from "./nameAtVersion"; 2 | import { ParsedLock, NpmLockFile } from "./types"; 3 | 4 | export function parseNpmLock(lock: NpmLockFile): ParsedLock { 5 | // Re-format the dependencies object so that the key includes the version, similarly to yarn.lock. 6 | // For example, `"@microsoft/task-scheduler": { }` will become `"@microsoft/task-scheduler@2.7.1": { }`. 7 | const dependencies = Object.fromEntries( 8 | Object.entries(lock.dependencies ?? {}).map(([key, dep]) => [nameAtVersion(key, dep.version), dep]) 9 | ); 10 | 11 | return { 12 | object: dependencies, 13 | type: "success", 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/parsePnpmLock.ts: -------------------------------------------------------------------------------- 1 | import { nameAtVersion } from "./nameAtVersion"; 2 | import { LockDependency, ParsedLock, PnpmLockFile } from "./types"; 3 | 4 | export function parsePnpmLock(yaml: PnpmLockFile): ParsedLock { 5 | const object: { 6 | [key in string]: LockDependency; 7 | } = {}; 8 | 9 | if (yaml && yaml.packages) { 10 | for (const [pkgSpec, snapshot] of Object.entries(yaml.packages)) { 11 | // TODO: handle file:foo.tgz syntax (rush uses this for internal package links) 12 | const specParts = pkgSpec.split(/\//); 13 | const name = specParts.length > 3 ? `${specParts[1]}/${specParts[2]}` : specParts[1]; 14 | const version = specParts.length > 3 ? specParts[3] : specParts[2]; 15 | 16 | object[nameAtVersion(name, version)] = { 17 | version, 18 | dependencies: snapshot.dependencies, 19 | }; 20 | } 21 | } 22 | 23 | return { 24 | object, 25 | type: "success", 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/queryLockFile.ts: -------------------------------------------------------------------------------- 1 | import { nameAtVersion } from "./nameAtVersion"; 2 | import { LockDependency, ParsedLock } from "./types"; 3 | 4 | export function queryLockFile(name: string, versionRange: string, lock: ParsedLock): LockDependency { 5 | const versionRangeSignature = nameAtVersion(name, versionRange); 6 | return lock.object[versionRangeSignature]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/readYaml.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import type jsYamlType from "js-yaml"; 3 | 4 | export function readYaml(file: string): TReturn { 5 | // This is delay loaded to avoid the perf penalty of parsing YAML utilities any time the package 6 | // is used (since usage of the YAML utilities is less common). 7 | const jsYaml: typeof jsYamlType = require("js-yaml"); 8 | 9 | const content = fs.readFileSync(file, "utf8"); 10 | return jsYaml.load(content) as TReturn; 11 | } 12 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/lockfile/types.ts: -------------------------------------------------------------------------------- 1 | export type Dependencies = { [key in string]: string }; 2 | 3 | export type LockDependency = { 4 | version: string; 5 | dependencies?: Dependencies; 6 | }; 7 | 8 | export type ParsedLock = { 9 | type: "success" | "merge" | "conflict"; 10 | object: { 11 | [key in string]: LockDependency; 12 | }; 13 | }; 14 | 15 | export interface PnpmLockFile { 16 | packages: { [name: string]: any }; 17 | } 18 | 19 | export interface NpmWorkspacesInfo { 20 | version: string; 21 | workspaces: { packages: string[] }; 22 | } 23 | 24 | export interface NpmSymlinkInfo { 25 | resolved: string; // Where the package is resolved from. 26 | link: boolean; // A flag to indicate that this is a symbolic link. 27 | integrity?: "sha512" | "sha1"; 28 | dev?: boolean; 29 | optional?: boolean; 30 | devOptional?: boolean; 31 | dependencies?: { [key: string]: LockDependency }; 32 | } 33 | 34 | export interface NpmLockFile { 35 | name: string; 36 | version: string; 37 | lockfileVersion?: 1 | 2 | 3; // 1: v5, v6; 2: backwards compatible v7; 3: non-backwards compatible v7 38 | requires?: boolean; 39 | packages?: { 40 | ""?: NpmWorkspacesInfo; // Monorepo root 41 | } & { [key: string]: NpmSymlinkInfo | LockDependency }; 42 | dependencies?: { [key: string]: LockDependency }; 43 | } 44 | 45 | export interface BerryLockFile { 46 | __metadata: any; 47 | [key: string]: { 48 | version: string; 49 | dependencies: { 50 | [dependency: string]: string; 51 | }; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/logging.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper that logs an error to `console.warn` if `process.env.VERBOSE` is set. 3 | * This should be replaced with a proper logging system eventually. 4 | */ 5 | export function logVerboseWarning(description: string, err?: unknown) { 6 | if (process.env.VERBOSE) { 7 | console.warn(`${description}${err ? ":\n" : ""}`, (err as Error | undefined)?.stack || err || ""); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/paths.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import { getWorkspaceRoot } from "./workspaces/getWorkspaceRoot"; 4 | import { git } from "./git"; 5 | import { logVerboseWarning } from "./logging"; 6 | 7 | /** 8 | * Starting from `cwd`, searches up the directory hierarchy for `filePath`. 9 | * If multiple strings are given, searches each directory level for any of them. 10 | * @returns Full path to the item found, or undefined if not found. 11 | */ 12 | export function searchUp(filePath: string | string[], cwd: string) { 13 | const paths = typeof filePath === "string" ? [filePath] : filePath; 14 | // convert to an absolute path if needed 15 | cwd = path.resolve(cwd); 16 | const root = path.parse(cwd).root; 17 | 18 | let foundPath: string | undefined; 19 | 20 | while (!foundPath && cwd !== root) { 21 | foundPath = paths.find((p) => fs.existsSync(path.join(cwd, p))); 22 | if (foundPath) { 23 | break; 24 | } 25 | 26 | cwd = path.dirname(cwd); 27 | } 28 | 29 | return foundPath ? path.join(cwd, foundPath) : undefined; 30 | } 31 | 32 | /** 33 | * Starting from `cwd`, uses `git rev-parse --show-toplevel` to find the root of the git repo. 34 | * Throws if `cwd` is not in a Git repository. 35 | */ 36 | export function findGitRoot(cwd: string) { 37 | const output = git(["rev-parse", "--show-toplevel"], { cwd }); 38 | if (!output.success) { 39 | throw new Error(`Directory "${cwd}" is not in a git repository`); 40 | } 41 | 42 | return path.normalize(output.stdout); 43 | } 44 | 45 | /** 46 | * Starting from `cwd`, searches up the directory hierarchy for `package.json`. 47 | */ 48 | export function findPackageRoot(cwd: string) { 49 | const jsonPath = searchUp("package.json", cwd); 50 | return jsonPath && path.dirname(jsonPath); 51 | } 52 | 53 | /** 54 | * Starting from `cwd`, searches up the directory hierarchy for the workspace root, 55 | * falling back to the git root if no workspace is detected. 56 | */ 57 | export function findProjectRoot(cwd: string) { 58 | let workspaceRoot: string | undefined; 59 | try { 60 | workspaceRoot = getWorkspaceRoot(cwd); 61 | } catch (err) { 62 | logVerboseWarning(`Error getting workspace root for ${cwd}`, err); 63 | } 64 | 65 | if (!workspaceRoot) { 66 | logVerboseWarning(`Could not find workspace root for ${cwd}. Falling back to git root.`); 67 | } 68 | return workspaceRoot || findGitRoot(cwd); 69 | } 70 | 71 | export function isChildOf(child: string, parent: string) { 72 | const relativePath = path.relative(child, parent); 73 | return /^[.\/\\]+$/.test(relativePath); 74 | } 75 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/scope.ts: -------------------------------------------------------------------------------- 1 | import micromatch from "micromatch"; 2 | 3 | /** 4 | * Searches all package names based on "scoping" (i.e. "scope" in the sense of inclusion). 5 | * NOTE: this is not the same as package scopes (`@scope/package`). 6 | */ 7 | export function getScopedPackages(search: string[], packages: { [pkg: string]: unknown } | string[]) { 8 | const packageNames = Array.isArray(packages) ? packages : Object.keys(packages); 9 | 10 | const results = new Set(); 11 | 12 | // perform a package-scoped search (e.g. search is @scope/foo*) 13 | const scopedSearch = search.filter((needle) => needle.startsWith("@") || needle.startsWith("!@")); 14 | if (scopedSearch.length > 0) { 15 | const matched = micromatch(packageNames, scopedSearch, { nocase: true }); 16 | for (const pkg of matched) { 17 | results.add(pkg); 18 | } 19 | } 20 | 21 | // perform a package-unscoped search (e.g. search is foo*) 22 | const unscopedSearch = search.filter((needle) => !needle.startsWith("@") && !needle.startsWith("!@")); 23 | if (unscopedSearch.length > 0) { 24 | // only generate the bare package map if there ARE unscoped searches 25 | const barePackageMap = generateBarePackageMap(packageNames); 26 | 27 | let matched = micromatch(Object.keys(barePackageMap), unscopedSearch, { nocase: true }); 28 | for (const bare of matched) { 29 | for (const pkg of barePackageMap[bare]) { 30 | results.add(pkg); 31 | } 32 | } 33 | } 34 | 35 | return [...results]; 36 | } 37 | 38 | function generateBarePackageMap(packageNames: string[]) { 39 | const barePackageMap: { [key: string]: string[] } = {}; 40 | 41 | // create a map of bare package name -> list of full package names 42 | // NOTE: do not perform barePackageMap lookup if any of the "scopes" arg starts with "@" 43 | for (const pkg of packageNames) { 44 | const bare = pkg.replace(/^@[^/]+\//, ""); 45 | barePackageMap[bare] = barePackageMap[bare] || []; 46 | barePackageMap[bare].push(pkg); 47 | } 48 | 49 | return barePackageMap; 50 | } 51 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/types/PackageGraph.ts: -------------------------------------------------------------------------------- 1 | /** A package graph edge that defines a single package name and one of its dependency */ 2 | export interface PackageDependency { 3 | name: string; 4 | dependency: string; 5 | } 6 | 7 | /** The graph is defined by as a list of package names as nodes, and a list of PackageDependency as edges*/ 8 | export interface PackageGraph { 9 | // Nodes 10 | packages: string[]; 11 | 12 | // Edges 13 | dependencies: PackageDependency[]; 14 | } 15 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/types/PackageInfo.ts: -------------------------------------------------------------------------------- 1 | export interface PackageInfo { 2 | name: string; 3 | packageJsonPath: string; 4 | version: string; 5 | dependencies?: { [dep: string]: string }; 6 | devDependencies?: { [dep: string]: string }; 7 | peerDependencies?: { [dep: string]: string }; 8 | optionalDependencies?: { [dep: string]: string }; 9 | private?: boolean; 10 | group?: string; 11 | scripts?: { [scriptName: string]: string }; 12 | repository?: string | { type: string; url: string; directory?: string }; 13 | [key: string]: any; 14 | } 15 | 16 | export interface PackageInfos { 17 | [pkgName: string]: PackageInfo; 18 | } 19 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/types/WorkspaceInfo.ts: -------------------------------------------------------------------------------- 1 | import { PackageInfo } from "./PackageInfo"; 2 | 3 | /** 4 | * Array with names, paths, and package.json contents for each package in a workspace. 5 | * 6 | * The method name is somewhat misleading due to the double meaning of "workspace", but it's retained 7 | * for compatibility. "Workspace" here refers to an individual package, in the sense of the `workspaces` 8 | * package.json config used by npm/yarn (instead of referring to the entire monorepo). 9 | */ 10 | export type WorkspaceInfo = { 11 | name: string; 12 | path: string; 13 | packageJson: PackageInfo; 14 | }[]; 15 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/WorkspaceManager.ts: -------------------------------------------------------------------------------- 1 | export type WorkspaceManager = "yarn" | "pnpm" | "rush" | "npm" | "lerna"; 2 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/findWorkspacePath.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceInfo } from "../types/WorkspaceInfo"; 2 | 3 | /** 4 | * Find the path for a particular package name from an array of info about packages within a workspace. 5 | * (See `../getWorkspaces` for why it's named this way.) 6 | * @param workspaces Array of info about packages within a workspace 7 | * @param packageName Package name to find 8 | * @returns Package path if found, or undefined 9 | */ 10 | export function findWorkspacePath(workspaces: WorkspaceInfo, packageName: string): string | undefined { 11 | return workspaces.find(({ name }) => name === packageName)?.path; 12 | } 13 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/getAllPackageJsonFiles.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { getWorkspacePackagePaths, getWorkspacePackagePathsAsync } from "./getWorkspacePackagePaths"; 3 | import { isCachingEnabled } from "../isCachingEnabled"; 4 | 5 | const cache = new Map(); 6 | 7 | /** 8 | * Get paths to every package.json in the workspace, given a cwd. 9 | */ 10 | export function getAllPackageJsonFiles(cwd: string): string[] { 11 | if (isCachingEnabled() && cache.has(cwd)) { 12 | return cache.get(cwd)!; 13 | } 14 | 15 | const packageJsonFiles = getWorkspacePackagePaths(cwd).map((packagePath) => path.join(packagePath, "package.json")); 16 | 17 | cache.set(cwd, packageJsonFiles); 18 | 19 | return packageJsonFiles; 20 | } 21 | 22 | export function _resetPackageJsonFilesCache() { 23 | cache.clear(); 24 | } 25 | 26 | /** 27 | * Get paths to every package.json in the workspace, given a cwd. 28 | */ 29 | export async function getAllPackageJsonFilesAsync(cwd: string): Promise { 30 | if (isCachingEnabled() && cache.has(cwd)) { 31 | return cache.get(cwd)!; 32 | } 33 | 34 | const packageJsonFiles = (await getWorkspacePackagePathsAsync(cwd)).map((packagePath) => 35 | path.join(packagePath, "package.json") 36 | ); 37 | 38 | cache.set(cwd, packageJsonFiles); 39 | 40 | return packageJsonFiles; 41 | } 42 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/getChangedPackages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getBranchChanges, 3 | getChangesBetweenRefs, 4 | getDefaultRemoteBranch, 5 | getStagedChanges, 6 | getUnstagedChanges, 7 | getUntrackedChanges, 8 | } from "../git"; 9 | 10 | import { getPackagesByFiles } from "./getPackagesByFiles"; 11 | /** 12 | * Finds all packages that had been changed between two refs in the repo under cwd 13 | * 14 | * executes a `git diff $fromRef...$toRef` to get changes given a merge-base 15 | * 16 | * further explanation with the three dots: 17 | * 18 | * ```txt 19 | * git diff [--options] ... [--] [...] 20 | * 21 | * This form is to view the changes on the branch containing and up to 22 | * the second , starting at a common ancestor of both 23 | * . "git diff A...B" is equivalent to "git diff 24 | * $(git-merge-base A B) B". You can omit any one of , which 25 | * has the same effect as using HEAD instead. 26 | * ``` 27 | * 28 | * @returns string[] of package names that have changed 29 | */ 30 | export function getChangedPackagesBetweenRefs( 31 | cwd: string, 32 | fromRef: string, 33 | toRef: string = "", 34 | ignoreGlobs: string[] = [] 35 | ) { 36 | let changes = [ 37 | ...new Set([ 38 | ...(getUntrackedChanges(cwd) || []), 39 | ...(getUnstagedChanges(cwd) || []), 40 | ...(getChangesBetweenRefs(fromRef, toRef, [], "", cwd) || []), 41 | ...(getStagedChanges(cwd) || []), 42 | ]), 43 | ]; 44 | 45 | return getPackagesByFiles(cwd, changes, ignoreGlobs, true); 46 | } 47 | 48 | /** 49 | * Finds all packages that had been changed in the repo under cwd 50 | * 51 | * executes a `git diff $Target...` to get changes given a merge-base 52 | * 53 | * further explanation with the three dots: 54 | * 55 | * ```txt 56 | * git diff [--options] ... [--] [...] 57 | * 58 | * This form is to view the changes on the branch containing and up to 59 | * the second , starting at a common ancestor of both 60 | * . "git diff A...B" is equivalent to "git diff 61 | * $(git-merge-base A B) B". You can omit any one of , which 62 | * has the same effect as using HEAD instead. 63 | * ``` 64 | * 65 | * @returns string[] of package names that have changed 66 | */ 67 | export function getChangedPackages(cwd: string, target: string | undefined, ignoreGlobs: string[] = []) { 68 | const targetBranch = target || getDefaultRemoteBranch({ cwd }); 69 | let changes = [ 70 | ...new Set([ 71 | ...(getUntrackedChanges(cwd) || []), 72 | ...(getUnstagedChanges(cwd) || []), 73 | ...(getBranchChanges(targetBranch, cwd) || []), 74 | ...(getStagedChanges(cwd) || []), 75 | ]), 76 | ]; 77 | 78 | return getPackagesByFiles(cwd, changes, ignoreGlobs, true); 79 | } 80 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/getPackagesByFiles.ts: -------------------------------------------------------------------------------- 1 | import micromatch from "micromatch"; 2 | import path from "path"; 3 | import { getWorkspaces } from "./getWorkspaces"; 4 | 5 | /** 6 | * Given a list of files, finds all packages names that contain those files 7 | * 8 | * @param workspaceRoot - The root of the workspace 9 | * @param files - files to search for 10 | * @param ignoreGlobs - glob patterns to ignore 11 | * @param returnAllPackagesOnNoMatch - if true, will return all packages if no matches are found 12 | * @returns package names that have changed 13 | */ 14 | export function getPackagesByFiles( 15 | workspaceRoot: string, 16 | files: string[], 17 | ignoreGlobs: string[] = [], 18 | returnAllPackagesOnNoMatch: boolean = false 19 | ) { 20 | const workspaceInfo = getWorkspaces(workspaceRoot); 21 | const ignoreSet = new Set(micromatch(files, ignoreGlobs)); 22 | 23 | files = files.filter((change) => !ignoreSet.has(change)); 24 | 25 | const packages = new Set(); 26 | 27 | for (const file of files) { 28 | const candidates = workspaceInfo.filter( 29 | (pkgPath) => file.indexOf(path.relative(workspaceRoot, pkgPath.path).replace(/\\/g, "/")) === 0 30 | ); 31 | 32 | if (candidates && candidates.length > 0) { 33 | const found = candidates.reduce((found, item) => { 34 | return found.path.length > item.path.length ? found : item; 35 | }, candidates[0]); 36 | packages.add(found.name); 37 | } else if (returnAllPackagesOnNoMatch) { 38 | return workspaceInfo.map((pkg) => pkg.name); 39 | } 40 | } 41 | 42 | return [...packages]; 43 | } 44 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/getWorkspacePackageInfo.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import fsPromises from "fs/promises"; 4 | import { WorkspaceInfo } from "../types/WorkspaceInfo"; 5 | import { PackageInfo } from "../types/PackageInfo"; 6 | import { logVerboseWarning } from "../logging"; 7 | import { infoFromPackageJson } from "../infoFromPackageJson"; 8 | 9 | /** 10 | * Get an array with names, paths, and package.json contents for each of the given package paths 11 | * within a workspace. 12 | * 13 | * This is an internal helper used by `getWorkspaces` implementations for different managers. 14 | * (See `../getWorkspaces` for why it's named this way.) 15 | * @param packagePaths Paths to packages within a workspace 16 | * @returns Array of workspace package infos 17 | * @internal 18 | */ 19 | export function getWorkspacePackageInfo(packagePaths: string[]): WorkspaceInfo { 20 | if (!packagePaths) { 21 | return []; 22 | } 23 | 24 | return packagePaths 25 | .map((workspacePath) => { 26 | let packageJson: PackageInfo; 27 | const packageJsonPath = path.join(workspacePath, "package.json"); 28 | 29 | try { 30 | packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as PackageInfo; 31 | } catch (err) { 32 | logVerboseWarning(`Error reading or parsing ${packageJsonPath} while getting workspace package info`, err); 33 | return null; 34 | } 35 | 36 | return { 37 | name: packageJson.name, 38 | path: workspacePath, 39 | packageJson: infoFromPackageJson(packageJson, packageJsonPath), 40 | }; 41 | }) 42 | .filter(Boolean) as WorkspaceInfo; 43 | } 44 | 45 | /** 46 | * Get an array with names, paths, and package.json contents for each of the given package paths 47 | * within a workspace. 48 | * 49 | * This is an internal helper used by `getWorkspaces` implementations for different managers. 50 | * (See `../getWorkspaces` for why it's named this way.) 51 | * @param packagePaths Paths to packages within a workspace 52 | * @returns Array of workspace package infos 53 | * @internal 54 | */ 55 | export async function getWorkspacePackageInfoAsync(packagePaths: string[]): Promise { 56 | if (!packagePaths) { 57 | return []; 58 | } 59 | 60 | const workspacePkgPromises = packagePaths.map>(async (workspacePath) => { 61 | const packageJsonPath = path.join(workspacePath, "package.json"); 62 | 63 | try { 64 | const packageJson = JSON.parse(await fsPromises.readFile(packageJsonPath, "utf-8")) as PackageInfo; 65 | return { 66 | name: packageJson.name, 67 | path: workspacePath, 68 | packageJson: infoFromPackageJson(packageJson, packageJsonPath), 69 | }; 70 | } catch (err) { 71 | logVerboseWarning(`Error reading or parsing ${packageJsonPath} while getting workspace package info`, err); 72 | return null; 73 | } 74 | }); 75 | 76 | return (await Promise.all(workspacePkgPromises)).filter(Boolean) as WorkspaceInfo; 77 | } 78 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/getWorkspacePackagePaths.ts: -------------------------------------------------------------------------------- 1 | import { getWorkspaceManagerAndRoot, getWorkspaceUtilities } from "./implementations"; 2 | 3 | /** 4 | * Get a list of package folder paths in the workspace. The list of included packages is based on 5 | * the manager's config file and matching package folders (which must contain package.json) on disk. 6 | */ 7 | export function getWorkspacePackagePaths(cwd: string): string[] { 8 | const utils = getWorkspaceUtilities(cwd); 9 | return utils?.getWorkspacePackagePaths(cwd) || []; 10 | } 11 | 12 | /** 13 | * Get a list of package folder paths in the workspace. The list of included packages is based on 14 | * the manager's config file and matching package folders (which must contain package.json) on disk. 15 | */ 16 | export async function getWorkspacePackagePathsAsync(cwd: string): Promise { 17 | const utils = getWorkspaceUtilities(cwd); 18 | 19 | if (!utils) { 20 | return []; 21 | } 22 | 23 | if (!utils.getWorkspacePackagePathsAsync) { 24 | const managerName = getWorkspaceManagerAndRoot(cwd)?.manager; 25 | throw new Error(`${cwd} is using ${managerName} which has not been converted to async yet`); 26 | } 27 | 28 | return utils.getWorkspacePackagePathsAsync(cwd); 29 | } 30 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/getWorkspaceRoot.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceManager } from "./WorkspaceManager"; 2 | import { getWorkspaceManagerAndRoot } from "./implementations"; 3 | 4 | /** 5 | * Get the root directory of a workspace/monorepo, defined as the directory where the workspace 6 | * manager config file is located. 7 | * @param cwd Start searching from here 8 | * @param preferredManager Search for only this manager's config file 9 | */ 10 | export function getWorkspaceRoot(cwd: string, preferredManager?: WorkspaceManager): string | undefined { 11 | return getWorkspaceManagerAndRoot(cwd, undefined, preferredManager)?.root; 12 | } 13 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/getWorkspaces.ts: -------------------------------------------------------------------------------- 1 | import { getWorkspaceUtilities, getWorkspaceManagerAndRoot } from "./implementations"; 2 | import { WorkspaceInfo } from "../types/WorkspaceInfo"; 3 | 4 | /** 5 | * Get an array with names, paths, and package.json contents for each package in a workspace. 6 | * The list of included packages is based on the workspace manager's config file. 7 | * 8 | * The method name is somewhat misleading due to the double meaning of "workspace", but it's retained 9 | * for compatibility. "Workspace" here refers to an individual package, in the sense of the `workspaces` 10 | * package.json config used by npm/yarn (instead of referring to the entire monorepo). 11 | */ 12 | export function getWorkspaces(cwd: string): WorkspaceInfo { 13 | const utils = getWorkspaceUtilities(cwd); 14 | return utils?.getWorkspaces(cwd) || []; 15 | } 16 | 17 | /** 18 | * Get an array with names, paths, and package.json contents for each package in a workspace. 19 | * The list of included packages is based on the workspace manager's config file. 20 | * 21 | * The method name is somewhat misleading due to the double meaning of "workspace", but it's retained 22 | * for compatibility. "Workspace" here refers to an individual package, in the sense of the `workspaces` 23 | * package.json config used by npm/yarn (instead of referring to the entire monorepo). 24 | */ 25 | export async function getWorkspacesAsync(cwd: string): Promise { 26 | const utils = getWorkspaceUtilities(cwd); 27 | 28 | if (!utils) { 29 | return []; 30 | } 31 | 32 | if (!utils.getWorkspacesAsync) { 33 | const managerName = getWorkspaceManagerAndRoot(cwd)?.manager; 34 | throw new Error(`${cwd} is using ${managerName} which has not been converted to async yet`); 35 | } 36 | 37 | return utils.getWorkspacesAsync(cwd); 38 | } 39 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { searchUp } from "../../paths"; 3 | import { WorkspaceManager } from "../WorkspaceManager"; 4 | import { isCachingEnabled } from "../../isCachingEnabled"; 5 | 6 | export interface WorkspaceManagerAndRoot { 7 | /** Workspace manager name */ 8 | manager: WorkspaceManager; 9 | /** Workspace root, where the manager configuration file is located */ 10 | root: string; 11 | } 12 | const workspaceCache = new Map(); 13 | 14 | /** 15 | * Files indicating the workspace root for each manager. 16 | * 17 | * DO NOT REORDER! The order of keys determines the precedence of the files, which is 18 | * important for cases like lerna where lerna.json and e.g. yarn.lock may both exist. 19 | */ 20 | const managerFiles = { 21 | // DO NOT REORDER! (see above) 22 | lerna: "lerna.json", 23 | rush: "rush.json", 24 | yarn: "yarn.lock", 25 | pnpm: "pnpm-workspace.yaml", 26 | npm: "package-lock.json", 27 | }; 28 | 29 | /** 30 | * Get the preferred workspace manager based on `process.env.PREFERRED_WORKSPACE_MANAGER` 31 | * (if valid). 32 | */ 33 | export function getPreferredWorkspaceManager(): WorkspaceManager | undefined { 34 | const preferred = process.env.PREFERRED_WORKSPACE_MANAGER as WorkspaceManager | undefined; 35 | return preferred && managerFiles[preferred] ? preferred : undefined; 36 | } 37 | 38 | /** 39 | * Get the workspace manager name and workspace root directory for `cwd`, with caching. 40 | * Also respects the `process.env.PREFERRED_WORKSPACE_MANAGER` override, provided the relevant 41 | * manager file exists. 42 | * @param cwd Directory to search up from 43 | * @param cache Optional override cache for testing 44 | * @param preferredManager Optional override manager (if provided, only searches for this manager's file) 45 | * @returns Workspace manager and root, or undefined if it can't be determined 46 | */ 47 | export function getWorkspaceManagerAndRoot( 48 | cwd: string, 49 | cache?: Map, 50 | preferredManager?: WorkspaceManager 51 | ): WorkspaceManagerAndRoot | undefined { 52 | cache = cache || workspaceCache; 53 | if (isCachingEnabled() && cache.has(cwd)) { 54 | return cache.get(cwd); 55 | } 56 | 57 | preferredManager = preferredManager || getPreferredWorkspaceManager(); 58 | const managerFile = searchUp( 59 | (preferredManager && managerFiles[preferredManager]) || Object.values(managerFiles), 60 | cwd 61 | ); 62 | 63 | if (managerFile) { 64 | const managerFileName = path.basename(managerFile); 65 | cache.set(cwd, { 66 | manager: (Object.keys(managerFiles) as WorkspaceManager[]).find( 67 | (name) => managerFiles[name] === managerFileName 68 | )!, 69 | root: path.dirname(managerFile), 70 | }); 71 | } else { 72 | // Avoid searching again if no file was found 73 | cache.set(cwd, undefined); 74 | } 75 | 76 | return cache.get(cwd); 77 | } 78 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceInfo } from "../../types/WorkspaceInfo"; 2 | import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; 3 | // These must be type imports to avoid parsing the additional deps at runtime 4 | import type * as LernaUtilities from "./lerna"; 5 | import type * as NpmUtilities from "./npm"; 6 | import type * as PnpmUtilities from "./pnpm"; 7 | import type * as RushUtilities from "./rush"; 8 | import type * as YarnUtilities from "./yarn"; 9 | 10 | export interface WorkspaceUtilities { 11 | /** 12 | * Get an array of paths to packages in the workspace, based on the manager's config file. 13 | */ 14 | getWorkspacePackagePaths: (cwd: string) => string[]; 15 | /** 16 | * Get an array of paths to packages in the workspace, based on the manager's config file. 17 | */ 18 | getWorkspacePackagePathsAsync?: (cwd: string) => Promise; 19 | /** 20 | * Get an array with names, paths, and package.json contents for each package in a workspace. 21 | * (See `../getWorkspaces` for why it's named this way.) 22 | */ 23 | getWorkspaces: (cwd: string) => WorkspaceInfo; 24 | /** 25 | * Get an array with names, paths, and package.json contents for each package in a workspace. 26 | * (See `../getWorkspaces` for why it's named this way.) 27 | */ 28 | getWorkspacesAsync?: (cwd: string) => Promise; 29 | } 30 | 31 | /** 32 | * Get utility implementations for the workspace manager of `cwd`. 33 | * Returns undefined if the manager can't be determined. 34 | */ 35 | export function getWorkspaceUtilities(cwd: string): WorkspaceUtilities | undefined { 36 | const manager = getWorkspaceManagerAndRoot(cwd)?.manager; 37 | 38 | switch (manager) { 39 | case "yarn": 40 | return require("./yarn") as typeof YarnUtilities; 41 | 42 | case "pnpm": 43 | return require("./pnpm") as typeof PnpmUtilities; 44 | 45 | case "rush": 46 | return require("./rush") as typeof RushUtilities; 47 | 48 | case "npm": 49 | return require("./npm") as typeof NpmUtilities; 50 | 51 | case "lerna": 52 | return require("./lerna") as typeof LernaUtilities; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/index.ts: -------------------------------------------------------------------------------- 1 | export { getWorkspaceManagerAndRoot, WorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; 2 | export { getWorkspaceUtilities, WorkspaceUtilities } from "./getWorkspaceUtilities"; 3 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/lerna.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import jju from "jju"; 3 | import path from "path"; 4 | import { getPackagePaths } from "../../getPackagePaths"; 5 | import { WorkspaceInfo } from "../../types/WorkspaceInfo"; 6 | import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; 7 | import { logVerboseWarning } from "../../logging"; 8 | import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; 9 | 10 | function getLernaWorkspaceRoot(cwd: string): string { 11 | const root = getWorkspaceManagerAndRoot(cwd, undefined, "lerna")?.root; 12 | if (!root) { 13 | throw new Error("Could not find lerna workspace root from " + cwd); 14 | } 15 | return root; 16 | } 17 | 18 | /** Get package paths for a lerna workspace. */ 19 | export function getWorkspacePackagePaths(cwd: string): string[] { 20 | try { 21 | const lernaWorkspaceRoot = getLernaWorkspaceRoot(cwd); 22 | const lernaJsonPath = path.join(lernaWorkspaceRoot, "lerna.json"); 23 | const lernaConfig = jju.parse(fs.readFileSync(lernaJsonPath, "utf-8")); 24 | return getPackagePaths(lernaWorkspaceRoot, lernaConfig.packages); 25 | } catch (err) { 26 | logVerboseWarning(`Error getting lerna workspace package paths for ${cwd}`, err); 27 | return []; 28 | } 29 | } 30 | 31 | /** 32 | * Get an array with names, paths, and package.json contents for each package in a lerna workspace. 33 | * (See `../getWorkspaces` for why it's named this way.) 34 | */ 35 | export function getLernaWorkspaces(cwd: string): WorkspaceInfo { 36 | try { 37 | const packagePaths = getWorkspacePackagePaths(cwd); 38 | return getWorkspacePackageInfo(packagePaths); 39 | } catch (err) { 40 | logVerboseWarning(`Error getting lerna workspaces for ${cwd}`, err); 41 | return []; 42 | } 43 | } 44 | 45 | /** 46 | * Get an array with names, paths, and package.json contents for each package in a lerna workspace. 47 | * (See `../getWorkspaces` for why it's named this way.) 48 | */ 49 | export async function getLernaWorkspacesAsync(cwd: string): Promise { 50 | try { 51 | const packagePaths = getWorkspacePackagePaths(cwd); 52 | return getWorkspacePackageInfoAsync(packagePaths); 53 | } catch (err) { 54 | logVerboseWarning(`Error getting lerna workspaces for ${cwd}`, err); 55 | return []; 56 | } 57 | } 58 | 59 | export { getLernaWorkspaces as getWorkspaces }; 60 | export { getLernaWorkspacesAsync as getWorkspacesAsync }; 61 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/npm.ts: -------------------------------------------------------------------------------- 1 | import { getWorkspaceManagerAndRoot } from "."; 2 | import { WorkspaceInfo } from "../../types/WorkspaceInfo"; 3 | import { 4 | getWorkspaceInfoFromWorkspaceRoot, 5 | getWorkspaceInfoFromWorkspaceRootAsync, 6 | getPackagePathsFromWorkspaceRoot, 7 | getPackagePathsFromWorkspaceRootAsync, 8 | } from "./packageJsonWorkspaces"; 9 | 10 | function getNpmWorkspaceRoot(cwd: string): string { 11 | const root = getWorkspaceManagerAndRoot(cwd, undefined, "npm")?.root; 12 | if (!root) { 13 | throw new Error("Could not find npm workspace root from " + cwd); 14 | } 15 | return root; 16 | } 17 | 18 | /** Get package paths for an npm workspace. */ 19 | export function getWorkspacePackagePaths(cwd: string): string[] { 20 | const npmWorkspacesRoot = getNpmWorkspaceRoot(cwd); 21 | return getPackagePathsFromWorkspaceRoot(npmWorkspacesRoot); 22 | } 23 | 24 | /** Get package paths for an npm workspace. */ 25 | export function getWorkspacePackagePathsAsync(cwd: string): Promise { 26 | const npmWorkspacesRoot = getNpmWorkspaceRoot(cwd); 27 | return getPackagePathsFromWorkspaceRootAsync(npmWorkspacesRoot); 28 | } 29 | 30 | /** 31 | * Get an array with names, paths, and package.json contents for each package in an npm workspace. 32 | * (See `../getWorkspaces` for why it's named this way.) 33 | */ 34 | export function getNpmWorkspaces(cwd: string): WorkspaceInfo { 35 | const npmWorkspacesRoot = getNpmWorkspaceRoot(cwd); 36 | return getWorkspaceInfoFromWorkspaceRoot(npmWorkspacesRoot); 37 | } 38 | 39 | /** 40 | * Get an array with names, paths, and package.json contents for each package in an npm workspace. 41 | * (See `../getWorkspaces` for why it's named this way.) 42 | */ 43 | export function getNpmWorkspacesAsync(cwd: string): Promise { 44 | const npmWorkspacesRoot = getNpmWorkspaceRoot(cwd); 45 | return getWorkspaceInfoFromWorkspaceRootAsync(npmWorkspacesRoot); 46 | } 47 | 48 | export { getNpmWorkspaces as getWorkspaces }; 49 | export { getNpmWorkspacesAsync as getWorkspacesAsync }; 50 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/packageJsonWorkspaces.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { getPackagePaths, getPackagePathsAsync } from "../../getPackagePaths"; 4 | import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; 5 | import { logVerboseWarning } from "../../logging"; 6 | 7 | type PackageJsonWithWorkspaces = { 8 | workspaces?: 9 | | { 10 | packages?: string[]; 11 | nohoist?: string[]; 12 | } 13 | | string[]; 14 | }; 15 | 16 | /** 17 | * Read the workspace root package.json and get the list of package globs from its `workspaces` property. 18 | */ 19 | function getPackages(packageJsonWorkspacesRoot: string): string[] { 20 | const packageJsonFile = path.join(packageJsonWorkspacesRoot, "package.json"); 21 | 22 | let packageJson: PackageJsonWithWorkspaces; 23 | try { 24 | packageJson = JSON.parse(fs.readFileSync(packageJsonFile, "utf-8")) as PackageJsonWithWorkspaces; 25 | } catch (e) { 26 | throw new Error("Could not load package.json from workspaces root"); 27 | } 28 | 29 | const { workspaces } = packageJson; 30 | 31 | if (Array.isArray(workspaces)) { 32 | return workspaces; 33 | } 34 | 35 | if (!workspaces?.packages) { 36 | throw new Error("Could not find a workspaces object in package.json (expected if this is not a monorepo)"); 37 | } 38 | 39 | return workspaces.packages; 40 | } 41 | 42 | export function getPackagePathsFromWorkspaceRoot(packageJsonWorkspacesRoot: string) { 43 | try { 44 | const packageGlobs = getPackages(packageJsonWorkspacesRoot); 45 | return packageGlobs ? getPackagePaths(packageJsonWorkspacesRoot, packageGlobs) : []; 46 | } catch (err) { 47 | logVerboseWarning(`Error getting package paths for ${packageJsonWorkspacesRoot}`, err); 48 | return []; 49 | } 50 | } 51 | 52 | /** 53 | * Get an array with names, paths, and package.json contents for each package in an npm/yarn workspace. 54 | * (See `../getWorkspaces` for why it's named this way.) 55 | */ 56 | export async function getPackagePathsFromWorkspaceRootAsync(packageJsonWorkspacesRoot: string): Promise { 57 | try { 58 | const packageGlobs = getPackages(packageJsonWorkspacesRoot); 59 | return packageGlobs ? getPackagePathsAsync(packageJsonWorkspacesRoot, packageGlobs) : []; 60 | } catch (err) { 61 | logVerboseWarning(`Error getting package paths for ${packageJsonWorkspacesRoot}`, err); 62 | return []; 63 | } 64 | } 65 | 66 | /** 67 | * Get an array with names, paths, and package.json contents for each package in an npm/yarn workspace. 68 | * (See `../getWorkspaces` for why it's named this way.) 69 | */ 70 | export function getWorkspaceInfoFromWorkspaceRoot(packageJsonWorkspacesRoot: string) { 71 | try { 72 | const packagePaths = getPackagePathsFromWorkspaceRoot(packageJsonWorkspacesRoot); 73 | return getWorkspacePackageInfo(packagePaths); 74 | } catch (err) { 75 | logVerboseWarning(`Error getting workspace info for ${packageJsonWorkspacesRoot}`, err); 76 | return []; 77 | } 78 | } 79 | 80 | /** 81 | * Get an array with names, paths, and package.json contents for each package in an npm/yarn workspace. 82 | * (See `../getWorkspaces` for why it's named this way.) 83 | */ 84 | export async function getWorkspaceInfoFromWorkspaceRootAsync(packageJsonWorkspacesRoot: string) { 85 | try { 86 | const packagePaths = await getPackagePathsFromWorkspaceRootAsync(packageJsonWorkspacesRoot); 87 | return getWorkspacePackageInfoAsync(packagePaths); 88 | } catch (err) { 89 | logVerboseWarning(`Error getting workspace info for ${packageJsonWorkspacesRoot}`, err); 90 | return []; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/pnpm.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { getPackagePaths } from "../../getPackagePaths"; 4 | import { WorkspaceInfo } from "../../types/WorkspaceInfo"; 5 | import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; 6 | import { readYaml } from "../../lockfile/readYaml"; 7 | import { logVerboseWarning } from "../../logging"; 8 | import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; 9 | 10 | type PnpmWorkspaceYaml = { 11 | packages: string[]; 12 | }; 13 | 14 | /** @deprecated Use `getWorkspaceRoot` */ 15 | export function getPnpmWorkspaceRoot(cwd: string): string { 16 | const root = getWorkspaceManagerAndRoot(cwd, undefined, "pnpm")?.root; 17 | if (!root) { 18 | throw new Error("Could not find pnpm workspace root from " + cwd); 19 | } 20 | return root; 21 | } 22 | 23 | /** Get package paths for a pnpm workspace. */ 24 | export function getWorkspacePackagePaths(cwd: string): string[] { 25 | try { 26 | const pnpmWorkspacesRoot = getPnpmWorkspaceRoot(cwd); 27 | const pnpmWorkspacesFile = path.join(pnpmWorkspacesRoot, "pnpm-workspace.yaml"); 28 | 29 | const pnpmWorkspaces = readYaml(pnpmWorkspacesFile) as PnpmWorkspaceYaml; 30 | 31 | return getPackagePaths(pnpmWorkspacesRoot, pnpmWorkspaces.packages); 32 | } catch (err) { 33 | logVerboseWarning(`Error getting pnpm workspace package paths for ${cwd}`, err); 34 | return []; 35 | } 36 | } 37 | 38 | /** 39 | * Get an array with names, paths, and package.json contents for each package in a pnpm workspace. 40 | * (See `../getWorkspaces` for why it's named this way.) 41 | */ 42 | export function getPnpmWorkspaces(cwd: string): WorkspaceInfo { 43 | try { 44 | const packagePaths = getWorkspacePackagePaths(cwd); 45 | return getWorkspacePackageInfo(packagePaths); 46 | } catch (err) { 47 | logVerboseWarning(`Error getting pnpm workspaces for ${cwd}`, err); 48 | return []; 49 | } 50 | } 51 | 52 | /** 53 | * Get an array with names, paths, and package.json contents for each package in a pnpm workspace. 54 | * (See `../getWorkspaces` for why it's named this way.) 55 | */ 56 | export async function getPnpmWorkspacesAsync(cwd: string): Promise { 57 | try { 58 | const packagePaths = getWorkspacePackagePaths(cwd); 59 | return getWorkspacePackageInfoAsync(packagePaths); 60 | } catch (err) { 61 | logVerboseWarning(`Error getting pnpm workspaces for ${cwd}`, err); 62 | return []; 63 | } 64 | } 65 | 66 | export { getPnpmWorkspaces as getWorkspaces }; 67 | export { getPnpmWorkspacesAsync as getWorkspacesAsync }; 68 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/rush.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import jju from "jju"; 3 | import fs from "fs"; 4 | 5 | import { WorkspaceInfo } from "../../types/WorkspaceInfo"; 6 | import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; 7 | import { logVerboseWarning } from "../../logging"; 8 | import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; 9 | 10 | /** @deprecated Use getWorkspaceRoot */ 11 | export function getRushWorkspaceRoot(cwd: string): string { 12 | const root = getWorkspaceManagerAndRoot(cwd, undefined, "rush")?.root; 13 | if (!root) { 14 | throw new Error("Could not find rush workspace root from " + cwd); 15 | } 16 | return root; 17 | } 18 | 19 | /** Get package paths for a rush workspace. */ 20 | export function getWorkspacePackagePaths(cwd: string): string[] { 21 | try { 22 | const rushWorkspaceRoot = getRushWorkspaceRoot(cwd); 23 | const rushJsonPath = path.join(rushWorkspaceRoot, "rush.json"); 24 | 25 | const rushConfig: { projects: Array<{ projectFolder: string }> } = jju.parse( 26 | fs.readFileSync(rushJsonPath, "utf-8") 27 | ); 28 | const root = path.dirname(rushJsonPath); 29 | return rushConfig.projects.map((project) => path.join(root, project.projectFolder)); 30 | } catch (err) { 31 | logVerboseWarning(`Error getting rush workspace package paths for ${cwd}`, err); 32 | return []; 33 | } 34 | } 35 | 36 | /** 37 | * Get an array with names, paths, and package.json contents for each package in a rush workspace. 38 | * (See `../getWorkspaces` for why it's named this way.) 39 | */ 40 | export function getRushWorkspaces(cwd: string): WorkspaceInfo { 41 | try { 42 | const packagePaths = getWorkspacePackagePaths(cwd); 43 | return getWorkspacePackageInfo(packagePaths); 44 | } catch (err) { 45 | logVerboseWarning(`Error getting rush workspaces for ${cwd}`, err); 46 | return []; 47 | } 48 | } 49 | 50 | /** 51 | * Get an array with names, paths, and package.json contents for each package in a rush workspace. 52 | * (See `../getWorkspaces` for why it's named this way.) 53 | */ 54 | export async function getRushWorkspacesAsync(cwd: string): Promise { 55 | try { 56 | const packagePaths = getWorkspacePackagePaths(cwd); 57 | return getWorkspacePackageInfoAsync(packagePaths); 58 | } catch (err) { 59 | logVerboseWarning(`Error getting rush workspaces for ${cwd}`, err); 60 | return []; 61 | } 62 | } 63 | 64 | export { getRushWorkspaces as getWorkspaces }; 65 | export { getRushWorkspacesAsync as getWorkspacesAsync }; 66 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/implementations/yarn.ts: -------------------------------------------------------------------------------- 1 | import { getWorkspaceManagerAndRoot } from "."; 2 | import { WorkspaceInfo } from "../../types/WorkspaceInfo"; 3 | import { 4 | getPackagePathsFromWorkspaceRoot, 5 | getPackagePathsFromWorkspaceRootAsync, 6 | getWorkspaceInfoFromWorkspaceRoot, 7 | getWorkspaceInfoFromWorkspaceRootAsync, 8 | } from "./packageJsonWorkspaces"; 9 | 10 | /** @deprecated Use `getWorkspaceRoot` */ 11 | export function getYarnWorkspaceRoot(cwd: string): string { 12 | const root = getWorkspaceManagerAndRoot(cwd, undefined, "yarn")?.root; 13 | if (!root) { 14 | throw new Error("Could not find yarn workspace root from " + cwd); 15 | } 16 | return root; 17 | } 18 | 19 | /** Get package paths for a yarn workspace. */ 20 | export function getWorkspacePackagePaths(cwd: string): string[] { 21 | const yarnWorkspacesRoot = getYarnWorkspaceRoot(cwd); 22 | return getPackagePathsFromWorkspaceRoot(yarnWorkspacesRoot); 23 | } 24 | 25 | /** Get package paths for a yarn workspace. */ 26 | export function getWorkspacePackagePathsAsync(cwd: string): Promise { 27 | const yarnWorkspacesRoot = getYarnWorkspaceRoot(cwd); 28 | return getPackagePathsFromWorkspaceRootAsync(yarnWorkspacesRoot); 29 | } 30 | 31 | /** 32 | * Get an array with names, paths, and package.json contents for each package in a yarn workspace. 33 | * (See `../getWorkspaces` for why it's named this way.) 34 | */ 35 | export function getYarnWorkspaces(cwd: string): WorkspaceInfo { 36 | const yarnWorkspacesRoot = getYarnWorkspaceRoot(cwd); 37 | return getWorkspaceInfoFromWorkspaceRoot(yarnWorkspacesRoot); 38 | } 39 | 40 | /** 41 | * Get an array with names, paths, and package.json contents for each package in a yarn workspace. 42 | * (See `../getWorkspaces` for why it's named this way.) 43 | */ 44 | export function getYarnWorkspacesAsync(cwd: string): Promise { 45 | const yarnWorkspacesRoot = getYarnWorkspaceRoot(cwd); 46 | return getWorkspaceInfoFromWorkspaceRootAsync(yarnWorkspacesRoot); 47 | } 48 | 49 | export { getYarnWorkspaces as getWorkspaces }; 50 | export { getYarnWorkspacesAsync as getWorkspacesAsync }; 51 | -------------------------------------------------------------------------------- /packages/workspace-tools/src/workspaces/listOfWorkspacePackageNames.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceInfo } from "../types/WorkspaceInfo"; 2 | 3 | export function listOfWorkspacePackageNames(workspaces: WorkspaceInfo): string[] { 4 | return workspaces.map(({ name }) => name); 5 | } 6 | -------------------------------------------------------------------------------- /packages/workspace-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ws-tools/scripts/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | // Available options: 2 | // https://docs.renovatebot.com/configuration-options/ 3 | 4 | // NOTE: Renovate only allows comments in .json5 files, but this isn't well-supported by 5 | // Prettier + VS Code. Workaround is to configure tools to treat the file as JSONC 6 | // (in .prettierrc and .vscode/settings.json). 7 | { 8 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 9 | 10 | "extends": [ 11 | // Basic recommended config + generate change files 12 | "github>microsoft/m365-renovate-config:beachballLibraryRecommended", 13 | // Auto-merge PRs only affecting @types devDependencies 14 | "github>microsoft/m365-renovate-config:automergeTypes", 15 | // Disable updating to package versions that are converted to ESM 16 | "github>microsoft/m365-renovate-config:disableEsmVersions", 17 | // Dedupe after updates, and periodically re-create the entire lock file so all deps are updated to latest 18 | "github>microsoft/m365-renovate-config:keepFresh", 19 | // Group various related updates 20 | "github>microsoft/m365-renovate-config:groupMore", 21 | // Group @types updates 22 | "github>microsoft/m365-renovate-config:groupTypes", 23 | // Don't update beyond Node 16 24 | "github>microsoft/m365-renovate-config:restrictNode(16)" 25 | ], 26 | 27 | "ignorePaths": [ 28 | "**/node_modules/**", 29 | // Renovate tends to use the wrong manager version on the fixtures, and they don't need to be 30 | // updated often (really only when one of the deps has a security issue) 31 | "**/__fixtures__/**" 32 | ], 33 | 34 | // Use this label on all PRs 35 | "labels": ["renovate"], 36 | 37 | // Limit 5 PRs per hour (could be changed later based on preference) 38 | "prHourlyLimit": 5, 39 | 40 | "reviewers": ["ecraig12345", "kenotron"], 41 | "reviewersSampleSize": 1, 42 | 43 | "semanticCommits": "disabled", 44 | 45 | "packageRules": [ 46 | { 47 | "groupName": "workspace-tools resolutions", 48 | "matchPackageNames": ["/.*workspace-tools$/"], 49 | "dependencyDashboardApproval": false 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /scripts/api-extractor/api-extractor.base.json: -------------------------------------------------------------------------------- 1 | /** 2 | * Config file for API Extractor. https://api-extractor.com 3 | * 4 | * Detailed explanation of settings and defaults: 5 | * https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/src/schemas/api-extractor-template.json 6 | */ 7 | { 8 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 9 | 10 | "mainEntryPointFilePath": "/lib/index.d.ts", 11 | 12 | // List of NPM package names whose exports should be treated as part of this package and 13 | // embedded directly in the .d.ts rollup, as if they were local files. 14 | "bundledPackages": [], 15 | 16 | // Choose LF or CRLF based on the OS (to match the typical git config) 17 | "newlineKind": "os", 18 | 19 | // Configures how the API report file (*.api.md) will be generated. 20 | "apiReport": { 21 | "enabled": true, 22 | // Include "forgotten exports" in the API report file. 23 | // These are symbols which are not directly exported, but are referenced by other exports. 24 | "includeForgottenExports": true 25 | }, 26 | 27 | // Disable the doc model file (*.api.json) 28 | "docModel": { "enabled": false }, 29 | 30 | // Disable the .d.ts rollup file 31 | "dtsRollup": { "enabled": false }, 32 | 33 | // Disable the tsdoc-metadata.json file 34 | "tsdocMetadata": { "enabled": true }, 35 | 36 | // Configures how API Extractor reports error and warning messages produced during analysis. 37 | "messages": { 38 | "extractorMessageReporting": { 39 | "ae-missing-release-tag": { 40 | "logLevel": "none" 41 | }, 42 | "ae-unresolved-link": { 43 | "logLevel": "none" 44 | } 45 | }, 46 | "tsdocMessageReporting": { 47 | "tsdoc-param-tag-missing-hyphen": { 48 | "logLevel": "none" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/api-extractor/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const extractor = require("@microsoft/api-extractor"); 4 | 5 | const cwd = process.cwd(); 6 | const configPaths = [path.join(cwd, "api-extractor.json"), path.join(__dirname, "api-extractor.base.json")]; 7 | const configPath = configPaths.find((name) => fs.existsSync(name)); 8 | 9 | if (!configPath) { 10 | console.error( 11 | "Could not find API Extractor config under any of the following paths:" + 12 | configPaths.map((name) => `\n- ${name}`).join("") 13 | ); 14 | process.exit(1); 15 | } 16 | 17 | const rawConfig = extractor.ExtractorConfig.loadFile(configPath); 18 | const preparedConfig = { 19 | configObject: rawConfig, 20 | configObjectFullPath: configPath, 21 | packageJsonFullPath: path.join(cwd, "package.json"), 22 | }; 23 | 24 | preparedConfig.configObject.projectFolder = cwd; 25 | 26 | const config = extractor.ExtractorConfig.prepare(preparedConfig); 27 | 28 | const result = extractor.Extractor.invoke(config); 29 | 30 | if (!result.apiReportChanged) { 31 | console.log(`API report is up to date.`); 32 | } else if (process.env.CI) { 33 | console.error('API report is out of date. Please run "yarn api" locally and commit the results.'); 34 | process.exit(1); 35 | } else { 36 | console.log(`Updating API report file (please check this in): "${config.reportFilePath}"`); 37 | const configDir = path.dirname(config.reportFilePath); 38 | if (!fs.existsSync(configDir)) { 39 | fs.mkdirSync(configDir); 40 | } 41 | fs.copyFileSync(config.reportTempFilePath, config.reportFilePath); 42 | } 43 | -------------------------------------------------------------------------------- /scripts/bin/ws-tools-scripts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const scriptName = process.argv[2]; 4 | 5 | switch (scriptName) { 6 | case "api": 7 | case "api-extractor": 8 | require("../api-extractor/index"); 9 | break; 10 | 11 | default: 12 | console.error(`Unknown script "${scriptName}".`); 13 | process.exit(1); 14 | } 15 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "is-number": "^7.0.0" 7 | }, 8 | "engines": { 9 | "yarn": ">=2.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic-2/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 6 6 | cacheKey: 8 7 | 8 | "basic@workspace:.": 9 | version: 0.0.0-use.local 10 | resolution: "basic@workspace:." 11 | dependencies: 12 | is-number: ^7.0.0 13 | languageName: unknown 14 | linkType: soft 15 | 16 | "is-number@npm:^7.0.0": 17 | version: 7.0.0 18 | resolution: "is-number@npm:7.0.0" 19 | checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a 20 | languageName: node 21 | linkType: hard 22 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic-pnpm/.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic-pnpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-pnpm", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "private": true, 6 | "description": "derived from sveltejs/kit", 7 | "devDependencies": { 8 | "@changesets/cli": "^2.14.1", 9 | "prettier": "2.8.0", 10 | "typescript": "^4.2.3" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic-without-lock-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-without-lock-file", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "is-number": "*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic-yarn-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-yarn-2", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "private": true, 6 | "description": "derived from microsoft/lage", 7 | "dependencies": { 8 | "@microsoft/task-scheduler": "^2.7.1", 9 | "execa": "^6.0.0" 10 | }, 11 | "devDependencies": { 12 | "@types/execa": "^2.0.0", 13 | "@types/node": "^14.0.0", 14 | "ts-node": "^10.0.0", 15 | "typescript": "^4.0.0" 16 | }, 17 | "packageManager": "yarn@3.4.1" 18 | } 19 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic-yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-yarn", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "private": true, 6 | "description": "derived from microsoft/lage", 7 | "dependencies": { 8 | "@microsoft/task-scheduler": "^2.7.1", 9 | "execa": "^6.0.0" 10 | }, 11 | "devDependencies": { 12 | "@types/execa": "^2.0.0", 13 | "@types/node": "^14.0.0", 14 | "ts-node": "^10.0.0", 15 | "typescript": "^4.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "is-number": "^7.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/basic/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | is-number@^7.0.0: 6 | version "7.0.0" 7 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 8 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-globby/individual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "individual", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-globby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*", 9 | "individual" 10 | ] 11 | }, 12 | "devDependencies": { 13 | "is-number": "^7.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-globby/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-globby/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-globby/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | is-number@^7.0.0: 6 | version "7.0.0" 7 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 8 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-lerna-npm/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-lerna-npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-lerna-npm", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "private": true, 6 | "devDependencies": { 7 | "lerna": "^6.0.0" 8 | }, 9 | "engines": { 10 | "npm": "8.x" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-lerna-npm/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-lerna-npm/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-nested/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-a", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*" 9 | ] 10 | }, 11 | "devDependencies": { 12 | "is-number": "^7.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-nested/monorepo/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-nested/monorepo/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-nested/monorepo/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | is-number@^7.0.0: 6 | version "7.0.0" 7 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 8 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm-unsupported/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-npm-unsupported", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm-unsupported/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-npm-unsupported", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*" 9 | ] 10 | }, 11 | "engines": { 12 | "npm": "6.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm-unsupported/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm-unsupported/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-npm", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "monorepo-npm", 9 | "version": "0.1.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@microsoft/task-scheduler": "^2.7.0" 13 | }, 14 | "devDependencies": { 15 | "typescript": "^4.0.0" 16 | }, 17 | "engines": { 18 | "npm": "8.x" 19 | }, 20 | "workspaces": { 21 | "packages": [ 22 | "packages/*" 23 | ] 24 | } 25 | }, 26 | "node_modules/@microsoft/task-scheduler": { 27 | "version": "2.7.1", 28 | "resolved": "https://registry.npmjs.org/@microsoft/task-scheduler/-/task-scheduler-2.7.1.tgz", 29 | "integrity": "sha512-xSmX7xgLTtf3LwVOW5HCEL4qrSWYCmPsVzpJ7PTBayN0KA9acScgsLYaDE6tE26N//DtDEW6mi4RteWU4XSrjA==", 30 | "dependencies": { 31 | "memory-streams": "^0.1.3", 32 | "p-graph": "^1.1.0" 33 | } 34 | }, 35 | "node_modules/core-util-is": { 36 | "version": "1.0.3", 37 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 38 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 39 | }, 40 | "node_modules/inherits": { 41 | "version": "2.0.4", 42 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 43 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 44 | }, 45 | "node_modules/isarray": { 46 | "version": "0.0.1", 47 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 48 | "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" 49 | }, 50 | "node_modules/memory-streams": { 51 | "version": "0.1.3", 52 | "resolved": "https://registry.npmjs.org/memory-streams/-/memory-streams-0.1.3.tgz", 53 | "integrity": "sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==", 54 | "dependencies": { 55 | "readable-stream": "~1.0.2" 56 | } 57 | }, 58 | "node_modules/p-graph": { 59 | "version": "1.1.2", 60 | "resolved": "https://registry.npmjs.org/p-graph/-/p-graph-1.1.2.tgz", 61 | "integrity": "sha512-GnEEHrOMozk0hCjXBm011oYb3zpaOolxHgqL2s7Od2niGAJKyk/4FZ2VRUAgjqqqoQnZQtwkF6fjGDJkIQTjDQ==" 62 | }, 63 | "node_modules/package-a": { 64 | "resolved": "packages/package-a", 65 | "link": true 66 | }, 67 | "node_modules/package-b": { 68 | "resolved": "packages/package-b", 69 | "link": true 70 | }, 71 | "node_modules/readable-stream": { 72 | "version": "1.0.34", 73 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", 74 | "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", 75 | "dependencies": { 76 | "core-util-is": "~1.0.0", 77 | "inherits": "~2.0.1", 78 | "isarray": "0.0.1", 79 | "string_decoder": "~0.10.x" 80 | } 81 | }, 82 | "node_modules/string_decoder": { 83 | "version": "0.10.31", 84 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 85 | "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" 86 | }, 87 | "node_modules/typescript": { 88 | "version": "4.8.4", 89 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 90 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 91 | "dev": true, 92 | "bin": { 93 | "tsc": "bin/tsc", 94 | "tsserver": "bin/tsserver" 95 | }, 96 | "engines": { 97 | "node": ">=4.2.0" 98 | } 99 | }, 100 | "packages/package-a": { 101 | "version": "0.1.0", 102 | "license": "MIT" 103 | }, 104 | "packages/package-b": { 105 | "version": "0.1.0", 106 | "license": "MIT" 107 | } 108 | }, 109 | "dependencies": { 110 | "@microsoft/task-scheduler": { 111 | "version": "2.7.1", 112 | "resolved": "https://registry.npmjs.org/@microsoft/task-scheduler/-/task-scheduler-2.7.1.tgz", 113 | "integrity": "sha512-xSmX7xgLTtf3LwVOW5HCEL4qrSWYCmPsVzpJ7PTBayN0KA9acScgsLYaDE6tE26N//DtDEW6mi4RteWU4XSrjA==", 114 | "requires": { 115 | "memory-streams": "^0.1.3", 116 | "p-graph": "^1.1.0" 117 | } 118 | }, 119 | "core-util-is": { 120 | "version": "1.0.3", 121 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 122 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 123 | }, 124 | "inherits": { 125 | "version": "2.0.4", 126 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 127 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 128 | }, 129 | "isarray": { 130 | "version": "0.0.1", 131 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 132 | "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" 133 | }, 134 | "memory-streams": { 135 | "version": "0.1.3", 136 | "resolved": "https://registry.npmjs.org/memory-streams/-/memory-streams-0.1.3.tgz", 137 | "integrity": "sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==", 138 | "requires": { 139 | "readable-stream": "~1.0.2" 140 | } 141 | }, 142 | "p-graph": { 143 | "version": "1.1.2", 144 | "resolved": "https://registry.npmjs.org/p-graph/-/p-graph-1.1.2.tgz", 145 | "integrity": "sha512-GnEEHrOMozk0hCjXBm011oYb3zpaOolxHgqL2s7Od2niGAJKyk/4FZ2VRUAgjqqqoQnZQtwkF6fjGDJkIQTjDQ==" 146 | }, 147 | "package-a": { 148 | "version": "file:packages/package-a" 149 | }, 150 | "package-b": { 151 | "version": "file:packages/package-b" 152 | }, 153 | "readable-stream": { 154 | "version": "1.0.34", 155 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", 156 | "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", 157 | "requires": { 158 | "core-util-is": "~1.0.0", 159 | "inherits": "~2.0.1", 160 | "isarray": "0.0.1", 161 | "string_decoder": "~0.10.x" 162 | } 163 | }, 164 | "string_decoder": { 165 | "version": "0.10.31", 166 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 167 | "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" 168 | }, 169 | "typescript": { 170 | "version": "4.8.4", 171 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 172 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 173 | "dev": true 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-npm", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*" 9 | ] 10 | }, 11 | "dependencies": { 12 | "@microsoft/task-scheduler": "^2.7.0" 13 | }, 14 | "devDependencies": { 15 | "typescript": "^4.0.0" 16 | }, 17 | "engines": { 18 | "npm": "8.x" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-npm/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-pnpm/.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-pnpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "monorepo-pnpm", 4 | "license": "MIT", 5 | "version": "0.1.0", 6 | "dependencies": { 7 | "@microsoft/task-scheduler": "^2.7.0" 8 | }, 9 | "devDependencies": { 10 | "typescript": "^4.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-pnpm/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-pnpm/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "devDependencies": { 6 | "once": "1.4.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-pnpm/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | importers: 4 | 5 | .: 6 | specifiers: 7 | '@microsoft/task-scheduler': ^2.7.0 8 | typescript: ^4.0.0 9 | dependencies: 10 | '@microsoft/task-scheduler': 2.7.1 11 | devDependencies: 12 | typescript: 4.8.4 13 | 14 | packages/package-a: 15 | specifiers: {} 16 | 17 | packages/package-b: 18 | specifiers: 19 | once: 1.4.0 20 | devDependencies: 21 | once: 1.4.0 22 | 23 | packages: 24 | 25 | /@microsoft/task-scheduler/2.7.1: 26 | resolution: {integrity: sha512-xSmX7xgLTtf3LwVOW5HCEL4qrSWYCmPsVzpJ7PTBayN0KA9acScgsLYaDE6tE26N//DtDEW6mi4RteWU4XSrjA==} 27 | dependencies: 28 | memory-streams: 0.1.3 29 | p-graph: 1.1.2 30 | dev: false 31 | 32 | /core-util-is/1.0.3: 33 | resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} 34 | dev: false 35 | 36 | /inherits/2.0.4: 37 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 38 | dev: false 39 | 40 | /isarray/0.0.1: 41 | resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} 42 | dev: false 43 | 44 | /memory-streams/0.1.3: 45 | resolution: {integrity: sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==} 46 | dependencies: 47 | readable-stream: 1.0.34 48 | dev: false 49 | 50 | /once/1.4.0: 51 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 52 | dependencies: 53 | wrappy: 1.0.2 54 | dev: true 55 | 56 | /p-graph/1.1.2: 57 | resolution: {integrity: sha512-GnEEHrOMozk0hCjXBm011oYb3zpaOolxHgqL2s7Od2niGAJKyk/4FZ2VRUAgjqqqoQnZQtwkF6fjGDJkIQTjDQ==} 58 | dev: false 59 | 60 | /readable-stream/1.0.34: 61 | resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} 62 | dependencies: 63 | core-util-is: 1.0.3 64 | inherits: 2.0.4 65 | isarray: 0.0.1 66 | string_decoder: 0.10.31 67 | dev: false 68 | 69 | /string_decoder/0.10.31: 70 | resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} 71 | dev: false 72 | 73 | /typescript/4.8.4: 74 | resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} 75 | engines: {node: '>=4.2.0'} 76 | hasBin: true 77 | dev: true 78 | 79 | /wrappy/1.0.2: 80 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 81 | dev: true 82 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-pnpm/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-pnpm/common/config/rush/command-line.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", 3 | 4 | "commands": [ 5 | { 6 | "commandKind": "bulk", 7 | "name": "compile", 8 | "enableParallelism": false, 9 | "summary": "Build all packages in the monorepo." 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-pnpm/common/config/rush/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | "@rush-temp/package-a": "file:projects/package-a.tgz" 3 | "@rush-temp/package-b": "file:projects/package-b.tgz" 4 | once: 1.4.0 5 | lockfileVersion: 5.1 6 | packages: 7 | /once/1.4.0: 8 | dependencies: 9 | wrappy: 1.0.2 10 | dev: false 11 | resolution: 12 | integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 13 | /wrappy/1.0.2: 14 | dev: false 15 | resolution: 16 | integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 17 | "file:projects/package-a.tgz": 18 | dev: false 19 | name: "@rush-temp/package-a" 20 | resolution: 21 | integrity: sha512-0sPqBQ5dHga1XHbqFOc2tNJiEJjst3Fus5wzKY0e0Mp/LpLzFQ85Ks6sHtDLRmJ7Y+Q7gIUUz7NMGgW3EKG5gA== 22 | tarball: "file:projects/package-a.tgz" 23 | version: 0.0.0 24 | "file:projects/package-b.tgz": 25 | dependencies: 26 | once: 1.4.0 27 | dev: false 28 | name: "@rush-temp/package-b" 29 | resolution: 30 | integrity: sha512-2VlRDgFYQ3ZwWLRMb2BWVk5Lrm4Rj9X45xxB1f7RSV3JQLrfGf0/EbxTuyRK2KqnxwKSAoKP8d2E3CyRbbHa8A== 31 | tarball: "file:projects/package-b.tgz" 32 | version: 0.0.0 33 | registry: "" 34 | specifiers: 35 | "@rush-temp/package-a": "file:./projects/package-a.tgz" 36 | "@rush-temp/package-b": "file:./projects/package-b.tgz" 37 | once: 1.4.0 38 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-pnpm/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-pnpm/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "devDependencies": { 6 | "once": "1.4.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-pnpm/rush.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", 3 | 4 | "rushVersion": "5.23.2", 5 | "pnpmVersion": "4.14.0", 6 | 7 | "projects": [ 8 | { 9 | "packageName": "package-a", 10 | "projectFolder": "packages/package-a" 11 | }, 12 | { 13 | "packageName": "package-b", 14 | "projectFolder": "packages/package-b" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-yarn/common/config/rush/command-line.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", 3 | 4 | "commands": [ 5 | { 6 | "commandKind": "bulk", 7 | "name": "compile", 8 | "enableParallelism": false, 9 | "summary": "Build all packages in the monorepo." 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-yarn/common/config/rush/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@rush-temp/package-a@file:./projects/package-a.tgz": 6 | version "0.0.0" 7 | resolved "file:./projects/package-a.tgz" 8 | 9 | "@rush-temp/package-b@file:./projects/package-b.tgz": 10 | version "0.0.0" 11 | resolved "file:./projects/package-b.tgz" 12 | dependencies: 13 | once "1.4.0" 14 | 15 | once@1.4.0: 16 | version "1.4.0" 17 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 18 | dependencies: 19 | wrappy "1" 20 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 21 | 22 | wrappy@1: 23 | version "1.0.2" 24 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 25 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 26 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-yarn/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-yarn/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "devDependencies": { 6 | "once": "1.4.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-yarn/rush.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", 3 | 4 | "rushVersion": "5.23.2", 5 | "yarnVersion": "1.17.3", 6 | 7 | "projects": [ 8 | { 9 | "packageName": "package-a", 10 | "projectFolder": "packages/package-a" 11 | }, 12 | { 13 | "packageName": "package-b", 14 | "projectFolder": "packages/package-b" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-rush-yarn/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | pnpm@^6.0: 6 | version "6.32.4" 7 | resolved "https://registry.yarnpkg.com/pnpm/-/pnpm-6.32.4.tgz#74d486f3563d8e4476141b43af18dd08c9291961" 8 | integrity sha512-rOG+VpOzs6g/MR5HWc8KTlLAx3ljdRJCMQwSg1DE/hzAaqF/Y2zIHH0u6dZw/XnRb9w1U8rOs9MJT9jMt7e+Qw== 9 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-shorthand/individual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "individual", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-shorthand/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-shorthand", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "monorepo-shorthand", 9 | "version": "0.1.0", 10 | "license": "MIT", 11 | "workspaces": [ 12 | "packages/*", 13 | "individual" 14 | ], 15 | "engines": { 16 | "npm": "8.x" 17 | } 18 | }, 19 | "individual": { 20 | "version": "0.1.0", 21 | "license": "MIT" 22 | }, 23 | "node_modules/individual": { 24 | "resolved": "individual", 25 | "link": true 26 | }, 27 | "node_modules/package-a": { 28 | "resolved": "packages/package-a", 29 | "link": true 30 | }, 31 | "node_modules/package-b": { 32 | "resolved": "packages/package-b", 33 | "link": true 34 | }, 35 | "packages/package-a": { 36 | "version": "0.1.0", 37 | "license": "MIT" 38 | }, 39 | "packages/package-b": { 40 | "version": "0.1.0", 41 | "license": "MIT" 42 | } 43 | }, 44 | "dependencies": { 45 | "individual": { 46 | "version": "file:individual" 47 | }, 48 | "package-a": { 49 | "version": "file:packages/package-a" 50 | }, 51 | "package-b": { 52 | "version": "file:packages/package-b" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-shorthand/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-shorthand", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/*", 8 | "individual" 9 | ], 10 | "engines": { 11 | "npm": "8.x" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-shorthand/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo-shorthand/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*" 9 | ] 10 | }, 11 | "devDependencies": { 12 | "is-number": "^7.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo/packages/package-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo/packages/package-b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "license": "MIT", 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/jest/__fixtures__/monorepo/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | is-number@^7.0.0: 6 | version "7.0.0" 7 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 8 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 9 | -------------------------------------------------------------------------------- /scripts/jest/debugTests.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const jest = require("jest"); 3 | const path = require("path"); 4 | 5 | const args = process.argv.slice(2); 6 | 7 | function findPackageRoot() { 8 | let cwd = process.cwd(); 9 | const root = path.parse(cwd).root; 10 | 11 | while (cwd !== root) { 12 | if (fs.existsSync(path.join(cwd, "package.json"))) return cwd; 13 | cwd = path.dirname(cwd); 14 | } 15 | } 16 | const packagePath = findPackageRoot(); 17 | if (!packagePath) { 18 | throw new Error("Could not find package.json relative to " + process.cwd()); 19 | } 20 | 21 | console.log(`Starting Jest debugging at: ${packagePath}`); 22 | 23 | jest.run(["--runInBand", "--watch", "--testTimeout=999999999", ...args], packagePath); 24 | -------------------------------------------------------------------------------- /scripts/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types').Config.InitialOptions} */ 2 | module.exports = { 3 | roots: ["/src"], 4 | transform: { 5 | "^.+\\.tsx?$": "ts-jest", 6 | }, 7 | testRegex: "(/__tests__/.*(\\.|/)(test|spec))\\.tsx?$", 8 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 9 | modulePathIgnorePatterns: ["/src/__fixtures__"], 10 | passWithNoTests: true, 11 | preset: "ts-jest", 12 | setupFilesAfterEnv: [require.resolve("./setupTests.ts")], 13 | }; 14 | -------------------------------------------------------------------------------- /scripts/jest/setupFixture.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs-extra"; 3 | import tmp from "tmp"; 4 | import { spawnSync, SpawnSyncOptions } from "child_process"; 5 | 6 | // tmp is supposed to be able to clean up automatically, but this doesn't always work within jest. 7 | // So we attempt to use its built-in cleanup mechanisms, but tests should ideally do their own cleanup too. 8 | tmp.setGracefulCleanup(); 9 | 10 | // Temp directories are created under tempRoot.name with incrementing numeric sub-directories 11 | let tempRoot: tmp.DirResult | undefined; 12 | let tempNumber = 0; 13 | 14 | const fixturesRoot = path.join(__dirname, "__fixtures__"); 15 | 16 | /** 17 | * Create a git repo in a temp directory, optionally containing the fixture files from `fixtureName`. 18 | * Be sure to call `cleanupFixtures()` after all tests to clean up temp directories. 19 | */ 20 | export function setupFixture(fixtureName?: string) { 21 | let fixturePath: string | undefined; 22 | if (fixtureName) { 23 | fixturePath = path.join(fixturesRoot, fixtureName); 24 | if (!fs.existsSync(fixturePath)) { 25 | throw new Error(`Couldn't find fixture "${fixtureName}" under "${fixturesRoot}"`); 26 | } 27 | } 28 | 29 | if (!tempRoot) { 30 | // Create a shared root temp directory for fixture files 31 | tempRoot = tmp.dirSync({ unsafeCleanup: true }); // clean up even if files are left 32 | } 33 | 34 | // Make the directory and git init 35 | const cwd = path.join(tempRoot.name, String(tempNumber++), fixturePath ? path.basename(fixturePath) : ""); 36 | 37 | fs.mkdirpSync(cwd); 38 | basicGit(["init"], { cwd }); 39 | basicGit(["config", "user.name", "test user"], { cwd }); 40 | basicGit(["config", "user.email", "test@test.email"], { cwd }); 41 | 42 | // Ensure GPG signing doesn't interfere with tests 43 | basicGit(["config", "commit.gpgsign", "false"], { cwd }); 44 | 45 | // Make the 'main' branch the default in the test repo 46 | // ensure that the configuration for this repo does not collide 47 | // with any global configuration the user had made, so we have 48 | // a 'fixed' value for our tests, regardless of user configuration 49 | basicGit(["symbolic-ref", "HEAD", "refs/heads/main"], { cwd }); 50 | basicGit(["config", "init.defaultBranch", "main"], { cwd }); 51 | 52 | // Copy and commit the fixture if requested 53 | if (fixturePath) { 54 | fs.copySync(fixturePath, cwd, { filter: (src) => !/[/\\](node_modules|temp|.rush)([/\\]|$)/.test(src) }); 55 | basicGit(["add", "."], { cwd }); 56 | basicGit(["commit", "-m", "test"], { cwd }); 57 | } 58 | 59 | return cwd; 60 | } 61 | 62 | /** 63 | * `tmp` is not always reliable about cleanup even with appropriate options, so it's recommended to 64 | * call this function in `afterAll`. 65 | */ 66 | export function cleanupFixtures() { 67 | if (tempRoot) { 68 | tempRoot.removeCallback(); 69 | tempRoot = undefined; 70 | } 71 | } 72 | 73 | export function setupPackageJson(cwd: string, packageJson: Record = {}) { 74 | const pkgJsonPath = path.join(cwd, "package.json"); 75 | let oldPackageJson: Record | undefined; 76 | if (fs.existsSync(pkgJsonPath)) { 77 | oldPackageJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")); 78 | } 79 | fs.writeFileSync(pkgJsonPath, JSON.stringify({ ...oldPackageJson, ...packageJson }, null, 2)); 80 | } 81 | 82 | export function setupLocalRemote(cwd: string, remoteName: string, fixtureName?: string) { 83 | // Create a separate repo and configure it as a remote 84 | const remoteCwd = setupFixture(fixtureName); 85 | const remoteUrl = remoteCwd.replace(/\\/g, "/"); 86 | basicGit(["remote", "add", remoteName, remoteUrl], { cwd }); 87 | basicGit(["config", "pull.rebase", "false"], { cwd }); 88 | basicGit(["pull", "-X", "ours", "origin", "main", "--allow-unrelated-histories"], { cwd }); 89 | // Configure url in package.json 90 | setupPackageJson(cwd, { repository: { url: remoteUrl, type: "git" } }); 91 | } 92 | 93 | /** 94 | * Very basic git wrapper that throws on error. 95 | * (Can't use the helper methods from `workspace-tools-git` to avoid a circular dependency.) 96 | */ 97 | function basicGit(args: string[], options: { cwd: string } & SpawnSyncOptions) { 98 | const result = spawnSync("git", args, options); 99 | if (result.status !== 0) { 100 | throw new Error(`git ${args.join(" ")} failed with ${result.status}\n\n${result.stderr.toString()}`); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /scripts/jest/setupTests.ts: -------------------------------------------------------------------------------- 1 | // Set timeout to 30 seconds 2 | jest.setTimeout(30 * 1000); 3 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ws-tools/scripts", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "MIT", 6 | "bin": { 7 | "ws-tools-scripts": "bin/ws-tools-scripts.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc --noEmit" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scripts/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020"], 5 | "module": "commonjs", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true, 12 | "types": ["node", "jest"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "noEmit": true 7 | }, 8 | "include": ["."], 9 | "exclude": ["node_modules", "**/__fixtures__"] 10 | } 11 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPointStrategy": "packages", 4 | "entryPoints": ["packages/*"], 5 | "name": "workspace-tools" 6 | } 7 | --------------------------------------------------------------------------------