├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── gh_pages.yml │ ├── integration.yml │ ├── lock-maintenance.yml │ ├── scorecards.yml │ ├── stale.yml │ └── triage.yml ├── .gitignore ├── .mocharc.cjs ├── .prettierignore ├── .prettierignore-doc ├── .prettierrc ├── .vscode └── settings.json ├── bin └── bin.cjs ├── eslint.config.js ├── jsdoc.json ├── license ├── package-lock.json ├── package.json ├── readme.md ├── src ├── cli │ ├── index.ts │ └── utils.ts ├── commands.ts ├── commit.ts ├── composed-store.ts ├── constants.ts ├── environment-base.ts ├── environment-full.ts ├── generator-lookup.ts ├── index.ts ├── module-lookup.ts ├── package-manager.ts ├── store.ts └── util │ ├── command.ts │ ├── namespace.ts │ ├── resolve.ts │ └── util.ts ├── test ├── __snapshots__ │ └── resolver.js.snap ├── command.js ├── environment.js ├── fixtures │ ├── binary-diff │ │ └── file-contains-utf8.yml │ ├── conflicter │ │ ├── file-conflict.txt │ │ ├── foo-template.js │ │ ├── foo.js │ │ ├── testFile.tar.gz │ │ └── yeoman-logo.png │ ├── generator-commands │ │ ├── generators │ │ │ ├── arguments │ │ │ │ └── index.js │ │ │ └── options │ │ │ │ └── index.js │ │ └── package.json │ ├── generator-common-js │ │ ├── generators │ │ │ └── cjs │ │ │ │ └── index.cjs │ │ └── package.json │ ├── generator-environment-extend │ │ ├── app │ │ │ └── index.js │ │ └── package.json │ ├── generator-esm │ │ ├── generators │ │ │ ├── app │ │ │ │ └── index.js │ │ │ ├── create-inherited │ │ │ │ └── index.js │ │ │ ├── create │ │ │ │ └── index.js │ │ │ └── mjs │ │ │ │ └── index.mjs │ │ └── package.json │ ├── generator-extend │ │ ├── package.json │ │ └── support │ │ │ └── index.js │ ├── generator-mocha │ │ ├── index.js │ │ └── package.json │ ├── generator-module-lib-gen │ │ ├── lib │ │ │ └── generators │ │ │ │ └── app │ │ │ │ └── index.js │ │ └── package.json │ ├── generator-module-root │ │ ├── app │ │ │ └── index.js │ │ └── package.json │ ├── generator-module │ │ ├── generators │ │ │ └── app │ │ │ │ └── index.js │ │ └── package.json │ ├── generator-no-constructor │ │ ├── generators │ │ │ └── app │ │ │ │ └── index.js │ │ └── package.json │ ├── generator-scoped │ │ ├── app.js │ │ ├── app.json │ │ ├── app │ │ │ ├── index.js │ │ │ └── scaffold │ │ │ │ └── index.js │ │ ├── package.json │ │ └── package │ │ │ ├── index.js │ │ │ └── nodefile.node │ ├── generator-simple │ │ ├── index.js │ │ └── package.json │ ├── generator-ts-js │ │ ├── generators │ │ │ └── app │ │ │ │ ├── index.js │ │ │ │ └── index.ts │ │ └── package.json │ ├── generator-ts │ │ ├── generators │ │ │ └── app │ │ │ │ └── index.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── help.txt │ ├── lookup-custom │ │ └── package.json │ ├── lookup-project │ │ ├── package-lock.json │ │ ├── package.json │ │ └── subdir │ │ │ └── package.json │ ├── package-manager │ │ ├── npm │ │ │ └── package-lock.json │ │ ├── pnpm │ │ │ └── pnpm-lock.yaml │ │ └── yarn │ │ │ └── yarn.lock │ └── yo-resolve │ │ ├── .yo-resolve │ │ └── sub │ │ └── .yo-resolve ├── generator-features.js ├── generator-versions.js ├── helpers.js ├── package-manager.js ├── plugins.js ├── resolver.js └── store.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [Makefile] 11 | indent_style = tab 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.cjs text eol=lf 4 | *.ts text eol=lf 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: '@types/node' 10 | versions: ['>=17'] 11 | - dependency-name: 'typescript' 12 | update-types: ['version-update:semver-patch'] 13 | - dependency-name: '*' 14 | update-types: ['version-update:semver-minor', 'version-update:semver-patch'] 15 | - package-ecosystem: 'github-actions' 16 | directory: '/' 17 | schedule: 18 | interval: 'weekly' 19 | open-pull-requests-limit: 4 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: NPM Test 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.head_ref || (github.ref == 'refs/heads/main' && github.sha) || github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | push: 7 | branches-ignore: 8 | - 'dependabot/**' 9 | pull_request: 10 | types: [closed, opened, synchronize, reopened] 11 | branches: 12 | - '*' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | if: >- 20 | github.event.action != 'closed' 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | os: [ubuntu-latest, windows-latest, macos-latest] 28 | node-version: [20] 29 | steps: 30 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 31 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: 'npm' 35 | - run: npm ci 36 | - run: npm test 37 | env: 38 | CI: true 39 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: ['main'] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ['main'] 20 | schedule: 21 | - cron: '0 0 * * 1' 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ['javascript', 'typescript'] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 72 | with: 73 | category: '/language:${{matrix.language}}' 74 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: 'Checkout Repository' 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - name: 'Dependency Review' 22 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 23 | -------------------------------------------------------------------------------- /.github/workflows/gh_pages.yml: -------------------------------------------------------------------------------- 1 | name: Update api docs 2 | on: 3 | push: 4 | branches: 5 | - docs 6 | release: 7 | types: [published] 8 | workflow_dispatch: 9 | inputs: 10 | path: 11 | description: 'Destination path to generate' 12 | required: false 13 | 14 | permissions: 15 | contents: write # for peter-evans/create-pull-request to create branch 16 | pull-requests: write # for peter-evans/create-pull-request to create a PR 17 | 18 | jobs: 19 | build: 20 | permissions: 21 | contents: write # for peter-evans/create-pull-request to create branch 22 | pull-requests: write # for peter-evans/create-pull-request to create a PR 23 | name: Update api docs 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 27 | - run: npm install -g npm@latest 28 | continue-on-error: true 29 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 30 | with: 31 | path: source 32 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | ref: gh-pages 35 | path: yeoman-environment-doc 36 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 37 | with: 38 | path: ~/.npm 39 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-gh-pages 40 | ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}- 41 | - run: npm ci 42 | working-directory: source 43 | - run: npm config set yeoman-environment:doc_path ../yeoman-environment-doc/${{ github.event.inputs.path }} 44 | working-directory: source 45 | - run: npm run doc 46 | working-directory: source 47 | - name: Create commit 48 | working-directory: yeoman-environment-doc 49 | if: always() 50 | run: | 51 | git add . 52 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 53 | git config --local user.name "Github Actions" 54 | git commit -a -m "Update api for ${{github.event.release.name}}" || true 55 | - name: Create Pull Request 56 | if: always() 57 | id: cpr 58 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 59 | with: 60 | token: ${{ secrets.GITHUB_TOKEN }} 61 | commit-message: 'Update api for ${{github.event.release.name}}' 62 | title: 'Update api for ${{github.event.release.name}}' 63 | body: | 64 | Update api docs 65 | labels: automated pr 66 | branch: gh-pages-master 67 | path: yeoman-environment-doc 68 | - name: Check outputs 69 | run: | 70 | echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}" 71 | echo "Pull Request Number - ${{ steps.cpr.outputs.pr_number }}" 72 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration Build 2 | 3 | on: 4 | # push: 5 | # branches-ignore: 6 | # - 'dependabot/**' 7 | pull_request: 8 | branches: 9 | - 'ignore' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | node-version: [20.x] 22 | 23 | steps: 24 | - name: Checkout yeoman-test 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | repository: yeoman/yeoman-test 28 | path: yeoman-test 29 | 30 | - name: Checkout yeoman-generator 31 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | with: 33 | repository: yeoman/generator 34 | path: yeoman-generator 35 | 36 | - name: Checkout yeoman-environment 37 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 38 | with: 39 | path: yeoman-environment 40 | 41 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 42 | with: 43 | node-version: ${{ matrix.node-version }} 44 | 45 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 46 | with: 47 | path: ~/.npm 48 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-integration 49 | restore-keys: | 50 | ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}- 51 | 52 | - name: Run yeoman-test test 53 | run: | 54 | cd $GITHUB_WORKSPACE/yeoman-test 55 | npm ci 56 | npm install yeoman/generator#main 57 | npm install ${{ github.repository }}#$GITHUB_REF 58 | npm test 59 | 60 | - name: Run yeoman-generator test 61 | if: always() 62 | run: | 63 | cd $GITHUB_WORKSPACE/yeoman-generator 64 | npm ci 65 | npm install yeoman/yeoman-test#main 66 | npm install ${{ github.repository }}#$GITHUB_REF 67 | npm test 68 | 69 | - name: Run yeoman-environment test 70 | if: always() 71 | run: | 72 | cd $GITHUB_WORKSPACE/yeoman-environment 73 | npm ci 74 | npm install yeoman/yeoman-test#main 75 | npm install yeoman/generator#main 76 | npm test 77 | -------------------------------------------------------------------------------- /.github/workflows/lock-maintenance.yml: -------------------------------------------------------------------------------- 1 | name: Package lock maintenance 2 | on: 3 | workflow_dispatch: 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | permissions: 11 | contents: write # for peter-evans/create-pull-request to create branch 12 | pull-requests: write # for peter-evans/create-pull-request to create a PR 13 | name: Bump transitional dependencies 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 18 | with: 19 | node-version: '22' 20 | cache: 'npm' 21 | - name: Create commit 22 | run: | 23 | rm package-lock.json 24 | npm install 25 | npm install # make sure package-lock.json is correct 26 | - name: Create Pull Request 27 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | commit-message: 'Bump transitional dependencies' 31 | committer: 'Github Actions <41898282+github-actions[bot]@users.noreply.github.com>' 32 | author: 'Github Actions <41898282+github-actions[bot]@users.noreply.github.com>' 33 | title: 'Bump transitional dependencies' 34 | body: Transitional dependencies bump. 35 | labels: 'dependencies' 36 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ['main'] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | # To allow GraphQL ListCommits to work 32 | issues: read 33 | pull-requests: read 34 | # To detect SAST tools 35 | checks: read 36 | 37 | steps: 38 | - name: 'Checkout code' 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | persist-credentials: false 42 | 43 | - name: 'Run analysis' 44 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 49 | # - you want to enable the Branch-Protection check on a *public* repository, or 50 | # - you are installing Scorecards on a *private* repository 51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 53 | 54 | # Public repositories: 55 | # - Publish results to OpenSSF REST API for easy access by consumers 56 | # - Allows the repository to include the Scorecard badge. 57 | # - See https://github.com/ossf/scorecard-action#publishing-results. 58 | # For private repositories: 59 | # - `publish_results` will always be set to `false`, regardless 60 | # of the value entered here. 61 | publish_results: true 62 | 63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 64 | # format to the repository Actions tab. 65 | - name: 'Upload artifact' 66 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 67 | with: 68 | name: SARIF file 69 | path: results.sarif 70 | retention-days: 5 71 | 72 | # Upload the results to GitHub's code scanning dashboard. 73 | - name: 'Upload to code-scanning' 74 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 75 | with: 76 | sarif_file: results.sarif 77 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | stale: 11 | permissions: 12 | issues: write # for actions/stale to close stale issues 13 | pull-requests: write # for actions/stale to close stale PRs 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: 'This issue is stale because it has been open with no activity. Remove stale label or comment or this will be closed' 20 | days-before-stale: 90 21 | days-before-pr-stale: 180 22 | days-before-close: 5 23 | stale-issue-label: 'stale' 24 | exempt-issue-labels: 'not stale' 25 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: Triage issues 2 | on: 3 | issues: 4 | types: [opened] 5 | jobs: 6 | apply-label: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 10 | with: 11 | github-token: ${{secrets.GITHUB_TOKEN}} 12 | script: | 13 | github.issues.addLabels({...context.issue, labels: ['needs triage']}) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | /.project 5 | dist 6 | test/**/package-lock.json 7 | -------------------------------------------------------------------------------- /.mocharc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parallel: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | .nyc_output 4 | **/fixtures/** 5 | -------------------------------------------------------------------------------- /.prettierignore-doc: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | **/scripts/** 4 | **/styles/** 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | # Prettier configuration 2 | 3 | printWidth: 140 4 | singleQuote: true 5 | tabWidth: 2 6 | useTabs: false 7 | trailingComma: all 8 | 9 | # js and ts rules: 10 | arrowParens: avoid 11 | 12 | # jsx and tsx rules: 13 | bracketSameLine: false 14 | bracketSpacing: true 15 | 16 | endOfLine: auto 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug.javascript.terminalOptions": { 3 | "skipFiles": ["node_modules/**", "dist/**"] 4 | }, 5 | "mochaExplorer.logpanel": true, 6 | "mochaExplorer.files": ["test/*.js"], 7 | "mochaExplorer.ui": "tdd", 8 | "mochaExplorer.configFile": ".mocharc.cjs", 9 | "mochaExplorer.require": "esmocha/mocha", 10 | "mochaExplorer.nodeArgv": ["--loader=esmocha/loader"] 11 | } 12 | -------------------------------------------------------------------------------- /bin/bin.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | (async function () { 3 | await import('../dist/cli/index.js'); 4 | })(); 5 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import configs from '@yeoman/eslint'; 3 | import { config } from 'typescript-eslint'; 4 | 5 | export default config( 6 | ...configs, 7 | { ignores: ['test/fixtures/'] }, 8 | { 9 | rules: { 10 | '@typescript-eslint/no-this-alias': 'off', 11 | 'no-undef': 'off', 12 | 'prefer-destructuring': 'off', 13 | 'unicorn/no-array-for-each': 'off', 14 | 'unicorn/no-array-push-push': 'off', 15 | 'unicorn/no-array-reduce': 'off', 16 | 'unicorn/no-this-assignment': 'off', 17 | 'unicorn/prefer-spread': 'off', 18 | 'unicorn/prevent-abbreviations': 'off', 19 | }, 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["readme.md", "lib"], 4 | "includePattern": ".+\\.js(doc)?$" 5 | }, 6 | "opts": { 7 | "recurse": true, 8 | "destination": "../yeoman-environment-doc", 9 | "template": "node_modules/tui-jsdoc-template", 10 | "package": "package.json" 11 | }, 12 | "templates": { 13 | "logo": { 14 | "url": "https://raw.githubusercontent.com/yeoman/yeoman.github.io/source/app/assets/img/logo.png", 15 | "width": "127px", 16 | "height": "14px" 17 | }, 18 | "name": "yeoman-environment", 19 | "footerText": "BSD license Copyright (c) Google", 20 | "default": { 21 | "includeDate": false 22 | } 23 | }, 24 | "plugins": ["plugins/markdown"] 25 | } 26 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright Google 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yeoman-environment", 3 | "version": "5.0.0-beta.0", 4 | "description": "Handles the lifecyle and bootstrapping of generators in a specific environment", 5 | "keywords": [ 6 | "development", 7 | "dev", 8 | "build", 9 | "tool", 10 | "cli", 11 | "scaffold", 12 | "scaffolding", 13 | "generate", 14 | "generator", 15 | "yeoman", 16 | "app" 17 | ], 18 | "homepage": "http://yeoman.io", 19 | "repository": "yeoman/environment", 20 | "license": "BSD-2-Clause", 21 | "author": "Yeoman", 22 | "type": "module", 23 | "exports": { 24 | ".": { 25 | "types": "./dist/index.d.js", 26 | "default": "./dist/index.js" 27 | }, 28 | "./package.json": "./package.json" 29 | }, 30 | "main": "./dist/index.js", 31 | "types": "./dist/index.d.js", 32 | "bin": { 33 | "yoe": "bin/bin.cjs" 34 | }, 35 | "files": [ 36 | "bin", 37 | "dist" 38 | ], 39 | "scripts": { 40 | "prebuild": "rimraf dist", 41 | "build": "tsc && npm run copy-types", 42 | "copy-types": "cpy \"src/**/*.d.(|c|m)ts\" dist/", 43 | "doc": "npm run doc:generate && npm run doc:fix && npm run doc:prettier", 44 | "doc:fix": "sed -i -e 's:^[[:space:]]*[[:space:]]*$::g' $npm_package_config_doc_path/global.html || true", 45 | "doc:generate": "jsdoc -c jsdoc.json -d $npm_package_config_doc_path", 46 | "doc:prettier": "prettier $npm_package_config_doc_path --write --ignore-path .prettierignore-doc", 47 | "fix": "eslint --fix && prettier . --write", 48 | "prepare": "npm run build", 49 | "pretest": "eslint && prettier . --check", 50 | "test": "c8 esmocha --forbid-only", 51 | "test-base": "c8 esmocha test/environment*.js test/store.js test/util.js test/adapter.js", 52 | "test-environment": "c8 esmocha test/environment.js", 53 | "test-generators": "c8 esmocha test/generators.js", 54 | "test-resolver": "c8 esmocha test/resolver.js" 55 | }, 56 | "config": { 57 | "doc_path": "./yeoman-environment-doc" 58 | }, 59 | "dependencies": { 60 | "@yeoman/adapter": "^2.1.1", 61 | "@yeoman/conflicter": "^2.4.0", 62 | "@yeoman/namespace": "^1.0.1", 63 | "@yeoman/transform": "^2.1.0", 64 | "@yeoman/types": "^1.6.0", 65 | "arrify": "^3.0.0", 66 | "chalk": "^5.4.1", 67 | "commander": "^13.1.0", 68 | "debug": "^4.4.0", 69 | "execa": "^9.5.3", 70 | "fly-import": "^0.4.1", 71 | "globby": "^14.1.0", 72 | "grouped-queue": "^2.0.0", 73 | "locate-path": "^7.2.0", 74 | "lodash-es": "^4.17.21", 75 | "mem-fs": "^4.1.2", 76 | "mem-fs-editor": "^11.1.4", 77 | "semver": "^7.7.2", 78 | "slash": "^5.1.0", 79 | "untildify": "^5.0.0", 80 | "which-package-manager": "^1.0.1" 81 | }, 82 | "devDependencies": { 83 | "@types/debug": "^4.1.12", 84 | "@types/lodash-es": "^4.17.12", 85 | "@types/node": "^18.19.100", 86 | "@types/semver": "^7.7.0", 87 | "@yeoman/eslint": "0.2.0", 88 | "c8": "^10.1.3", 89 | "cpy-cli": "^5.0.0", 90 | "eslint": "9.12.0", 91 | "esmocha": "^3.0.0", 92 | "fs-extra": "^11.3.0", 93 | "jsdoc": "^4.0.4", 94 | "prettier": "3.5.3", 95 | "prettier-plugin-packagejson": "^2.5.12", 96 | "rimraf": "^6.0.1", 97 | "sinon": "^20.0.0", 98 | "sinon-test": "^3.1.6", 99 | "strip-ansi": "^7.1.0", 100 | "typescript": "5.8.3", 101 | "yeoman-assert": "^3.1.1", 102 | "yeoman-environment": "file:./", 103 | "yeoman-generator-2": "npm:yeoman-generator@^2.0.5", 104 | "yeoman-generator-4": "npm:yeoman-generator@^4.13.0", 105 | "yeoman-generator-5": "npm:yeoman-generator@^5.10.0", 106 | "yeoman-generator-6": "npm:yeoman-generator@^6.0.1", 107 | "yeoman-generator-7": "npm:yeoman-generator@^7.0.0", 108 | "yeoman-generator-8": "npm:yeoman-generator@^8.0.0-beta.0", 109 | "yeoman-test": "^10.1.1" 110 | }, 111 | "peerDependencies": { 112 | "@yeoman/types": "^1.1.1", 113 | "mem-fs": "^4.0.0" 114 | }, 115 | "engines": { 116 | "node": "^20.17.0 || >=22.9.0" 117 | }, 118 | "overrides": { 119 | "yeoman-generator-2": { 120 | "chalk": "^4.1.0", 121 | "dargs": "^7.0.0", 122 | "debug": "^4.1.1", 123 | "execa": "^5.1.1", 124 | "github-username": "^6.0.0", 125 | "lodash": "^4.17.11", 126 | "mem-fs-editor": "^9.0.0", 127 | "minimist": "^1.2.5", 128 | "pacote": "^15.2.0", 129 | "read-pkg-up": "^7.0.1", 130 | "run-async": "^2.0.0", 131 | "semver": "^7.2.1", 132 | "shelljs": "^0.8.5", 133 | "sort-keys": "^4.2.0", 134 | "text-table": "^0.2.0", 135 | "yeoman-environment": "file:./" 136 | }, 137 | "yeoman-generator-4": { 138 | "chalk": "^4.1.0", 139 | "dargs": "^7.0.0", 140 | "debug": "^4.1.1", 141 | "execa": "^5.1.1", 142 | "github-username": "^6.0.0", 143 | "lodash": "^4.17.11", 144 | "mem-fs-editor": "^9.0.0", 145 | "minimist": "^1.2.5", 146 | "pacote": "^15.2.0", 147 | "read-pkg-up": "^7.0.1", 148 | "run-async": "^2.0.0", 149 | "semver": "^7.2.1", 150 | "shelljs": "^0.8.5", 151 | "sort-keys": "^4.2.0", 152 | "text-table": "^0.2.0", 153 | "yeoman-environment": "file:./" 154 | }, 155 | "yeoman-generator-5": { 156 | "chalk": "^4.1.0", 157 | "dargs": "^7.0.0", 158 | "debug": "^4.1.1", 159 | "execa": "^5.1.1", 160 | "github-username": "^6.0.0", 161 | "lodash": "^4.17.11", 162 | "mem-fs-editor": "^9.0.0", 163 | "minimist": "^1.2.5", 164 | "pacote": "^15.2.0", 165 | "read-pkg-up": "^7.0.1", 166 | "run-async": "^2.0.0", 167 | "semver": "^7.2.1", 168 | "shelljs": "^0.8.5", 169 | "sort-keys": "^4.2.0", 170 | "text-table": "^0.2.0", 171 | "yeoman-environment": "file:./" 172 | }, 173 | "yeoman-generator-6": { 174 | "yeoman-environment": "file:./" 175 | }, 176 | "yeoman-generator-7": { 177 | "yeoman-environment": "file:./" 178 | }, 179 | "yeoman-generator-8": { 180 | "yeoman-environment": "file:./" 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Yeoman Environment 2 | 3 | [![npm](https://badge.fury.io/js/yeoman-environment.svg)](http://badge.fury.io/js/yeoman-environment) [![NPM Test](https://github.com/yeoman/environment/actions/workflows/ci.yml/badge.svg)](https://github.com/yeoman/environment/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/yeoman/environment/badge.svg?branch=master)](https://coveralls.io/github/yeoman/environment?branch=master) [![Gitter](https://img.shields.io/badge/Gitter-Join_the_Yeoman_chat_%E2%86%92-00d06f.svg)](https://gitter.im/yeoman/yeoman) 4 | 5 | > Handles the lifecycle and bootstrapping of generators in a specific environment 6 | 7 | It provides a high-level API to discover, create and run generators, as well as further tuning of where and how a generator is resolved. 8 | 9 | ## Install 10 | 11 | ``` 12 | $ npm install yeoman-environment 13 | ``` 14 | 15 | ## Usage 16 | 17 | Full documentation available [here](http://yeoman.io/authoring/integrating-yeoman.html). 18 | 19 | ```js 20 | import { createEnv } from 'yeoman-environment'; 21 | 22 | // The #lookup() method will search the user computer for installed generators 23 | // The search if done from the current working directory 24 | await env.lookup(); 25 | await env.run('angular', { skipInstall: true }); 26 | ``` 27 | 28 | For advance usage, see [our API documentation for latest yeoman-environment](http://yeoman.github.io/environment). 29 | 30 | [API documentation for yeoman-environment v2.x](http://yeoman.github.io/environment/2.x). 31 | 32 | ## License 33 | 34 | BSD-2-Clause © Yeoman 35 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import process from 'node:process'; 3 | import { dirname, resolve } from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | import YeomanCommand, { addEnvironmentOptions } from '../util/command.js'; 6 | import { createEnv as createEnvironment } from '../index.js'; 7 | import { environmentAction, printGroupedGenerator } from './utils.js'; 8 | 9 | const program = new YeomanCommand(); 10 | 11 | const packageJson = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json')).toString()); 12 | 13 | program.version(packageJson.version).allowExcessArguments(false).enablePositionalOptions(); 14 | 15 | addEnvironmentOptions( 16 | program 17 | .command('run ') 18 | .description('Run a generator') 19 | .argument('', 'Generator to run') 20 | .passThroughOptions() 21 | .allowUnknownOption() 22 | .allowExcessArguments(true) 23 | .action(environmentAction) 24 | .usage('[environment options] [generator-options]'), 25 | ); 26 | 27 | program 28 | .command('find') 29 | .description('Find installed generators') 30 | .action(async () => { 31 | const environment = createEnvironment(); 32 | printGroupedGenerator(await environment.lookup()); 33 | }); 34 | 35 | program 36 | .command('list') 37 | .description('List generators available to be used') 38 | .action(async () => { 39 | const environment = createEnvironment(); 40 | await environment.lookup(); 41 | printGroupedGenerator(Object.values(environment.getGeneratorsMeta())); 42 | }); 43 | 44 | try { 45 | await program.parseAsync(process.argv); 46 | } catch (error) { 47 | console.log(error); 48 | // eslint-disable-next-line unicorn/no-process-exit 49 | process.exit(1); 50 | } 51 | -------------------------------------------------------------------------------- /src/cli/utils.ts: -------------------------------------------------------------------------------- 1 | import { requireNamespace } from '@yeoman/namespace'; 2 | import type { BaseGeneratorMeta } from '@yeoman/types'; 3 | import { groupBy } from 'lodash-es'; 4 | import createLogger from 'debug'; 5 | import { createEnv as createEnvironment } from '../index.js'; 6 | import type YeomanCommand from '../util/command.js'; 7 | 8 | const debug = createLogger('yeoman:yoe'); 9 | 10 | export const printGroupedGenerator = (generators: BaseGeneratorMeta[]) => { 11 | const grouped = groupBy(generators, 'packagePath'); 12 | for (const [packagePath, group] of Object.entries(grouped)) { 13 | const namespace = requireNamespace(group[0].namespace); 14 | console.log(` ${namespace.packageNamespace} at ${packagePath}`); 15 | for (const generator of group) { 16 | const generatorNamespace = requireNamespace(generator.namespace); 17 | console.log(` :${generatorNamespace.generator || 'app'}`); 18 | } 19 | 20 | console.log(''); 21 | } 22 | 23 | console.log(`${generators.length} generators`); 24 | }; 25 | 26 | /** 27 | * @param {string} generatorNamespace 28 | * @param {*} options 29 | * @param {*} command 30 | * @returns 31 | */ 32 | export const environmentAction = async function (this: YeomanCommand, generatorNamespace: string, options: any, command: any) { 33 | debug('Handling operands %o', generatorNamespace); 34 | if (!generatorNamespace) { 35 | return; 36 | } 37 | 38 | const environment = createEnvironment({ ...options, command: this }); 39 | this.env = environment; 40 | await environment.lookupLocalPackages(); 41 | 42 | return environment.execute(generatorNamespace, command.args.splice(1)); 43 | }; 44 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import { type BaseGeneratorConstructor, type GeneratorMeta } from '@yeoman/types'; 2 | import YeomanCommand, { addEnvironmentOptions } from './util/command.js'; 3 | import { createEnv as createEnvironment } from './index.js'; 4 | 5 | export type CommandPreparation = { 6 | resolved?: string; 7 | command?: YeomanCommand; 8 | generator?: BaseGeneratorConstructor; 9 | namespace?: string; 10 | }; 11 | 12 | /** 13 | * Prepare a commander instance for cli support. 14 | * 15 | * @param {Command} command - Command to be prepared 16 | * @param generatorPath - Generator to create Command 17 | * @return {Command} return command 18 | */ 19 | export const prepareGeneratorCommand = async ({ 20 | command = addEnvironmentOptions(new YeomanCommand()), 21 | resolved, 22 | generator, 23 | namespace, 24 | }: CommandPreparation) => { 25 | const environment = createEnvironment(); 26 | let meta: GeneratorMeta; 27 | if (generator && namespace) { 28 | meta = environment.register(generator, { namespace, resolved }); 29 | } else if (resolved) { 30 | meta = environment.register(resolved, { namespace }); 31 | } else { 32 | throw new Error(`A generator with namespace or a generator path is required`); 33 | } 34 | 35 | command.env = environment; 36 | command.registerGenerator(await meta.instantiateHelp()); 37 | command.action(async function (this: YeomanCommand) { 38 | let rootCommand: YeomanCommand = this; 39 | while (rootCommand.parent) { 40 | rootCommand = rootCommand.parent as YeomanCommand; 41 | } 42 | 43 | const generator = await meta.instantiate(this.args, this.opts()); 44 | await environment.runGenerator(generator); 45 | }); 46 | return command; 47 | }; 48 | 49 | /** 50 | * Prepare a commander instance for cli support. 51 | * 52 | * @param generatorPaht - Generator to create Command 53 | * @return Return a Command instance 54 | */ 55 | export const prepareCommand = async (options: CommandPreparation) => { 56 | options.command = options.command ?? new YeomanCommand(); 57 | addEnvironmentOptions(options.command); 58 | return prepareGeneratorCommand(options); 59 | }; 60 | -------------------------------------------------------------------------------- /src/commit.ts: -------------------------------------------------------------------------------- 1 | import type { InputOutputAdapter } from '@yeoman/types'; 2 | import { type ConflicterOptions, createConflicterTransform, createYoResolveTransform, forceYoFiles } from '@yeoman/conflicter'; 3 | import createdLogger from 'debug'; 4 | import type { Store } from 'mem-fs'; 5 | import { type MemFsEditorFile } from 'mem-fs-editor'; 6 | import { createCommitTransform } from 'mem-fs-editor/transform'; 7 | import { isFilePending } from 'mem-fs-editor/state'; 8 | 9 | const debug = createdLogger('yeoman:environment:commit'); 10 | 11 | /** 12 | * Commits the MemFs to the disc. 13 | */ 14 | export const commitSharedFsTask = async ({ 15 | adapter, 16 | conflicterOptions, 17 | sharedFs, 18 | }: { 19 | adapter: InputOutputAdapter; 20 | conflicterOptions?: ConflicterOptions; 21 | sharedFs: Store; 22 | }) => { 23 | debug('Running commitSharedFsTask'); 24 | await sharedFs.pipeline( 25 | { filter: (file: MemFsEditorFile) => isFilePending(file) || file.path.endsWith('.yo-resolve') }, 26 | createYoResolveTransform(), 27 | forceYoFiles(), 28 | createConflicterTransform(adapter, conflicterOptions), 29 | createCommitTransform(), 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/composed-store.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto'; 2 | import { toNamespace } from '@yeoman/namespace'; 3 | import type { BaseGenerator, Logger } from '@yeoman/types'; 4 | import createdLogger from 'debug'; 5 | 6 | const debug = createdLogger('yeoman:environment:composed-store'); 7 | 8 | type UniqueFeatureType = 'customCommitTask' | 'customInstallTask'; 9 | 10 | export class ComposedStore { 11 | private readonly log?; 12 | private readonly generators: Record = {}; 13 | private readonly uniqueByPathMap = new Map>(); 14 | private readonly uniqueGloballyMap = new Map(); 15 | 16 | constructor({ log }: { log?: Logger } = {}) { 17 | this.log = log; 18 | } 19 | 20 | get customCommitTask() { 21 | return this.findUniqueFeature('customCommitTask'); 22 | } 23 | 24 | get customInstallTask() { 25 | return this.findUniqueFeature('customInstallTask'); 26 | } 27 | 28 | getGenerators(): Record { 29 | return { ...this.generators }; 30 | } 31 | 32 | addGenerator(generator: BaseGenerator) { 33 | const { features = (generator as any).getFeatures?.() ?? {} } = generator; 34 | let { uniqueBy } = features; 35 | const { uniqueGlobally } = features; 36 | 37 | let identifier = uniqueBy; 38 | if (!uniqueBy) { 39 | const { namespace } = generator.options; 40 | const instanceId = crypto.randomBytes(20).toString('hex'); 41 | let namespaceDefinition = toNamespace(namespace); 42 | if (namespaceDefinition) { 43 | namespaceDefinition = namespaceDefinition.with({ instanceId }); 44 | uniqueBy = namespaceDefinition.id; 45 | identifier = namespaceDefinition.namespace; 46 | } else { 47 | uniqueBy = `${namespace}#${instanceId}`; 48 | identifier = namespace; 49 | } 50 | } 51 | 52 | const generatorRoot = generator.destinationRoot(); 53 | const uniqueByMap = uniqueGlobally ? this.uniqueGloballyMap : this.getUniqueByPathMap(generatorRoot); 54 | if (uniqueByMap.has(uniqueBy)) { 55 | return { uniqueBy, identifier, added: false, generator: uniqueByMap.get(uniqueBy) }; 56 | } 57 | 58 | uniqueByMap.set(uniqueBy, generator); 59 | 60 | this.generators[uniqueGlobally ? uniqueBy : `${generatorRoot}#${uniqueBy}`] = generator; 61 | return { identifier, added: true, generator }; 62 | } 63 | 64 | getUniqueByPathMap(root: string): Map { 65 | if (!this.uniqueByPathMap.has(root)) { 66 | this.uniqueByPathMap.set(root, new Map()); 67 | } 68 | 69 | return this.uniqueByPathMap.get(root)!; 70 | } 71 | 72 | findFeature(featureName: string): Array<{ generatorId: string; feature: any }> { 73 | return Object.entries(this.generators) 74 | .map(([generatorId, generator]) => { 75 | const { features = (generator as any).getFeatures?.() } = generator; 76 | const feature = features?.[featureName]; 77 | return feature ? { generatorId, feature } : undefined; 78 | }) 79 | .filter(Boolean) as any; 80 | } 81 | 82 | private findUniqueFeature(featureName: UniqueFeatureType) { 83 | const providedFeatures = this.findFeature(featureName); 84 | if (providedFeatures.length > 0) { 85 | if (providedFeatures.length > 1) { 86 | this.log?.info?.( 87 | `Multiple ${featureName} tasks found (${providedFeatures.map(({ generatorId }) => generatorId).join(', ')}). Using the first.`, 88 | ); 89 | } 90 | 91 | const [{ generatorId, feature }] = providedFeatures; 92 | debug(`Feature ${featureName} provided by ${generatorId}`); 93 | return feature; 94 | } 95 | 96 | return; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const UNKNOWN_NAMESPACE = 'unknownnamespace'; 2 | 3 | export const UNKNOWN_RESOLVED = 'unknown'; 4 | 5 | export const defaultQueues = [ 6 | 'environment:run', 7 | 'initializing', 8 | 'prompting', 9 | 'configuring', 10 | 'default', 11 | 'writing', 12 | 'transform', 13 | 'conflicts', 14 | 'environment:conflicts', 15 | 'install', 16 | 'end', 17 | ]; 18 | -------------------------------------------------------------------------------- /src/environment-base.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'node:events'; 2 | import { createRequire } from 'node:module'; 3 | import { basename, isAbsolute, join, relative, resolve } from 'node:path'; 4 | import process from 'node:process'; 5 | import { realpathSync } from 'node:fs'; 6 | import { QueuedAdapter, type TerminalAdapterOptions } from '@yeoman/adapter'; 7 | import type { 8 | ApplyTransformsOptions, 9 | BaseEnvironment, 10 | BaseEnvironmentOptions, 11 | BaseGenerator, 12 | BaseGeneratorConstructor, 13 | BaseGeneratorMeta, 14 | ComposeOptions, 15 | GeneratorMeta, 16 | GetGeneratorConstructor, 17 | GetGeneratorOptions, 18 | InputOutputAdapter, 19 | InstantiateOptions, 20 | LookupGeneratorMeta, 21 | } from '@yeoman/types'; 22 | import { type Store as MemFs, create as createMemFs } from 'mem-fs'; 23 | import { type MemFsEditorFile } from 'mem-fs-editor'; 24 | import { FlyRepository } from 'fly-import'; 25 | import createdLogger from 'debug'; 26 | // @ts-expect-error grouped-queue don't have types 27 | import GroupedQueue from 'grouped-queue'; 28 | import { isFilePending } from 'mem-fs-editor/state'; 29 | import { type FilePipelineTransform, filePipeline, transform } from '@yeoman/transform'; 30 | import { type YeomanNamespace, toNamespace } from '@yeoman/namespace'; 31 | import chalk from 'chalk'; 32 | import { type ConflicterOptions } from '@yeoman/conflicter'; 33 | import { defaults, pick } from 'lodash-es'; 34 | import { ComposedStore } from './composed-store.js'; 35 | import Store from './store.js'; 36 | import type YeomanCommand from './util/command.js'; 37 | import { asNamespace, defaultLookups } from './util/namespace.js'; 38 | import { type LookupOptions, lookupGenerators } from './generator-lookup.js'; 39 | import { UNKNOWN_NAMESPACE, UNKNOWN_RESOLVED, defaultQueues } from './constants.js'; 40 | import { resolveModulePath } from './util/resolve.js'; 41 | import { commitSharedFsTask } from './commit.js'; 42 | import { packageManagerInstallTask } from './package-manager.js'; 43 | import { splitArgsFromString as splitArgumentsFromString } from './util/util.js'; 44 | 45 | const require = createRequire(import.meta.url); 46 | 47 | const ENVIRONMENT_VERSION = require('../package.json').version; 48 | 49 | const debug = createdLogger('yeoman:environment'); 50 | 51 | export type EnvironmentLookupOptions = LookupOptions & { 52 | /** Add a scope to the namespace if there is no scope */ 53 | registerToScope?: string; 54 | /** Customize the namespace to be registered */ 55 | customizeNamespace?: (ns?: string) => string | undefined; 56 | }; 57 | 58 | export type EnvironmentOptions = BaseEnvironmentOptions & 59 | Omit & { 60 | adapter?: InputOutputAdapter; 61 | logCwd?: string; 62 | command?: YeomanCommand; 63 | yeomanRepository?: string; 64 | arboristRegistry?: string; 65 | nodePackageManager?: string; 66 | }; 67 | 68 | const getInstantiateOptions = (arguments_?: any, options?: any): InstantiateOptions => { 69 | if (Array.isArray(arguments_) || typeof arguments_ === 'string') { 70 | return { generatorArgs: splitArgumentsFromString(arguments_), generatorOptions: options }; 71 | } 72 | 73 | if (arguments_ !== undefined) { 74 | if ('generatorOptions' in arguments_ || 'generatorArgs' in arguments_) { 75 | return arguments_; 76 | } 77 | 78 | if ('options' in arguments_ || 'arguments' in arguments_ || 'args' in arguments_) { 79 | const { 80 | args: insideArguments, 81 | arguments: generatorArguments = insideArguments, 82 | options: generatorOptions, 83 | ...remainingOptions 84 | } = arguments_; 85 | return { generatorArgs: splitArgumentsFromString(generatorArguments), generatorOptions: generatorOptions ?? remainingOptions }; 86 | } 87 | } 88 | 89 | return { generatorOptions: options }; 90 | }; 91 | 92 | const getComposeOptions = (...varargs: any[]): ComposeOptions => { 93 | if (varargs.filter(Boolean).length === 0) return {}; 94 | 95 | const [arguments_, options, composeOptions] = varargs; 96 | if (typeof arguments_ === 'boolean') { 97 | return { schedule: arguments_ }; 98 | } 99 | 100 | let generatorArguments; 101 | let generatorOptions; 102 | if (arguments_ !== undefined) { 103 | if (Array.isArray(arguments_)) { 104 | generatorArguments = arguments_; 105 | } else if (typeof arguments_ === 'string') { 106 | generatorArguments = splitArgumentsFromString(String(arguments_)); 107 | } else if (typeof arguments_ === 'object') { 108 | if ('generatorOptions' in arguments_ || 'generatorArgs' in arguments_ || 'schedule' in arguments_) { 109 | return arguments_; 110 | } 111 | 112 | generatorOptions = arguments_; 113 | } 114 | } 115 | 116 | if (typeof options === 'boolean') { 117 | return { generatorArgs: generatorArguments, generatorOptions, schedule: options }; 118 | } 119 | 120 | generatorOptions = generatorOptions ?? options; 121 | 122 | if (typeof composeOptions === 'boolean') { 123 | return { generatorArgs: generatorArguments, generatorOptions, schedule: composeOptions }; 124 | } 125 | 126 | return {}; 127 | }; 128 | 129 | /** 130 | * Copy and remove null and undefined values 131 | * @param object 132 | * @returns 133 | */ 134 | 135 | export function removePropertiesWithNullishValues(object: Record): Record { 136 | return Object.fromEntries(Object.entries(object).filter(([_key, value]) => value !== undefined && value !== null)); 137 | } 138 | 139 | // eslint-disable-next-line unicorn/prefer-event-target 140 | export default class EnvironmentBase extends EventEmitter implements BaseEnvironment { 141 | cwd: string; 142 | logCwd: string; 143 | adapter: QueuedAdapter; 144 | sharedFs: MemFs; 145 | conflicterOptions?: ConflicterOptions; 146 | 147 | protected readonly options: EnvironmentOptions; 148 | protected readonly aliases: Array<{ match: RegExp; value: string }> = []; 149 | protected store: Store; 150 | protected command?: YeomanCommand; 151 | protected runLoop: GroupedQueue; 152 | protected composedStore: ComposedStore; 153 | protected lookups: string[]; 154 | protected sharedOptions: Record; 155 | protected repository: FlyRepository; 156 | protected experimental: boolean; 157 | protected _rootGenerator?: BaseGenerator; 158 | protected compatibilityMode?: false | 'v4'; 159 | 160 | private contextStore: Map> = new Map(); 161 | 162 | constructor(options: EnvironmentOptions = {}) { 163 | super(); 164 | 165 | this.setMaxListeners(100); 166 | 167 | const { 168 | cwd = process.cwd(), 169 | logCwd = cwd, 170 | sharedFs = createMemFs(), 171 | command, 172 | yeomanRepository, 173 | arboristRegistry, 174 | sharedOptions = {}, 175 | experimental, 176 | console: adapterConsole, 177 | stdin, 178 | stderr, 179 | stdout, 180 | adapter = new QueuedAdapter({ console: adapterConsole, stdin, stdout, stderr }), 181 | ...remainingOptions 182 | } = options; 183 | 184 | this.options = remainingOptions; 185 | this.adapter = adapter as QueuedAdapter; 186 | this.cwd = resolve(cwd); 187 | this.logCwd = logCwd; 188 | this.store = new Store(this as BaseEnvironment); 189 | this.command = command; 190 | 191 | this.runLoop = new GroupedQueue(defaultQueues, false); 192 | this.composedStore = new ComposedStore({ log: this.adapter.log }); 193 | this.sharedFs = sharedFs as MemFs; 194 | 195 | // Each composed generator might set listeners on these shared resources. Let's make sure 196 | // Node won't complain about event listeners leaks. 197 | this.runLoop.setMaxListeners(0); 198 | this.sharedFs.setMaxListeners(0); 199 | 200 | this.lookups = defaultLookups; 201 | 202 | this.sharedOptions = sharedOptions; 203 | 204 | // Create a default sharedData. 205 | this.sharedOptions.sharedData = this.sharedOptions.sharedData ?? {}; 206 | 207 | // Pass forwardErrorToEnvironment to generators. 208 | this.sharedOptions.forwardErrorToEnvironment = false; 209 | 210 | this.repository = new FlyRepository({ 211 | repositoryPath: yeomanRepository ?? `${this.cwd}/.yo-repository`, 212 | arboristConfig: { 213 | registry: arboristRegistry, 214 | }, 215 | }); 216 | 217 | this.experimental = experimental || process.argv.includes('--experimental'); 218 | 219 | this.alias(/^([^:]+)$/, '$1:app'); 220 | } 221 | 222 | findFeature(featureName: string): Array<{ generatorId: string; feature: any }> { 223 | return this.composedStore.findFeature(featureName); 224 | } 225 | 226 | async applyTransforms(transformStreams: FilePipelineTransform[], options: ApplyTransformsOptions = {}): Promise { 227 | const { 228 | streamOptions = { filter: file => isFilePending(file) }, 229 | stream = this.sharedFs.stream(streamOptions), 230 | name = 'Transforming', 231 | } = options; 232 | 233 | if (!Array.isArray(transformStreams)) { 234 | transformStreams = [transformStreams]; 235 | } 236 | 237 | await this.adapter.progress( 238 | async ({ step }) => { 239 | await filePipeline(stream, [ 240 | ...(transformStreams as any), 241 | transform(file => { 242 | step('Completed', relative(this.logCwd, file.path)); 243 | // eslint-disable-next-line unicorn/no-useless-undefined 244 | return undefined; 245 | }), 246 | ]); 247 | }, 248 | { name, disabled: !(options?.log ?? true) }, 249 | ); 250 | } 251 | 252 | /** 253 | * @param namespaceOrPath 254 | * @return the generator meta registered under the namespace 255 | */ 256 | async findMeta(namespaceOrPath: string | YeomanNamespace): Promise { 257 | // Stop the recursive search if nothing is left 258 | if (!namespaceOrPath) { 259 | return; 260 | } 261 | 262 | const parsed = toNamespace(namespaceOrPath); 263 | if (typeof namespaceOrPath !== 'string' || parsed) { 264 | const ns = parsed!.namespace; 265 | return this.store.getMeta(ns) ?? this.store.getMeta(this.alias(ns)); 266 | } 267 | 268 | const maybeMeta = this.store.getMeta(namespaceOrPath) ?? this.store.getMeta(this.alias(namespaceOrPath)); 269 | if (maybeMeta) { 270 | return maybeMeta; 271 | } 272 | 273 | try { 274 | const resolved = await resolveModulePath(namespaceOrPath); 275 | if (resolved) { 276 | return this.store.add({ resolved, namespace: this.namespace(resolved) }); 277 | } 278 | } catch { 279 | // ignore error 280 | } 281 | 282 | return undefined; 283 | } 284 | 285 | /** 286 | * Get a single generator from the registered list of generators. The lookup is 287 | * based on generator's namespace, "walking up" the namespaces until a matching 288 | * is found. Eg. if an `angular:common` namespace is registered, and we try to 289 | * get `angular:common:all` then we get `angular:common` as a fallback (unless 290 | * an `angular:common:all` generator is registered). 291 | * 292 | * @param namespaceOrPath 293 | * @return the generator registered under the namespace 294 | */ 295 | async get( 296 | namespaceOrPath: string | YeomanNamespace, 297 | ): Promise { 298 | const meta = await this.findMeta(namespaceOrPath); 299 | return meta?.importGenerator() as Promise; 300 | } 301 | 302 | /** 303 | * Create is the Generator factory. It takes a namespace to lookup and optional 304 | * hash of options, that lets you define `arguments` and `options` to 305 | * instantiate the generator with. 306 | * 307 | * An error is raised on invalid namespace. 308 | * 309 | * @param namespaceOrPath 310 | * @param instantiateOptions 311 | * @return The instantiated generator 312 | */ 313 | async create( 314 | namespaceOrPath: string | GetGeneratorConstructor, 315 | instantiateOptions?: InstantiateOptions, 316 | ): Promise; 317 | async create( 318 | namespaceOrPath: string | GetGeneratorConstructor, 319 | ...arguments_: any[] 320 | ): Promise { 321 | let constructor; 322 | const namespace = typeof namespaceOrPath === 'string' ? toNamespace(namespaceOrPath) : undefined; 323 | 324 | const checkGenerator = (Generator: any) => { 325 | const generatorNamespace = Generator?.namespace; 326 | if (namespace && generatorNamespace !== namespace.namespace && generatorNamespace !== UNKNOWN_NAMESPACE) { 327 | // Update namespace object in case of aliased namespace. 328 | try { 329 | namespace.namespace = Generator.namespace; 330 | } catch { 331 | // Invalid namespace can be aliased to a valid one. 332 | } 333 | } 334 | 335 | if (typeof Generator !== 'function') { 336 | throw new TypeError( 337 | `${chalk.red(`You don't seem to have a generator with the name “${namespace?.generatorHint}” installed.`)}\n` + 338 | `But help is on the way:\n\n` + 339 | `You can see available generators via ${chalk.yellow('npm search yeoman-generator')} or via ${chalk.yellow( 340 | 'http://yeoman.io/generators/', 341 | )}. \n` + 342 | `Install them with ${chalk.yellow(`npm install ${namespace?.generatorHint}`)}.\n\n` + 343 | `To see all your installed generators run ${chalk.yellow('yo --generators')}. ` + 344 | `Adding the ${chalk.yellow('--help')} option will also show subgenerators. \n\n` + 345 | `If ${chalk.yellow('yo')} cannot find the generator, run ${chalk.yellow('yo doctor')} to troubleshoot your system.`, 346 | ); 347 | } 348 | 349 | return Generator; 350 | }; 351 | 352 | if (typeof namespaceOrPath !== 'string') { 353 | return this.instantiate(checkGenerator(namespaceOrPath), ...arguments_); 354 | } 355 | 356 | if (typeof namespaceOrPath === 'string') { 357 | const meta = await this.findMeta(namespaceOrPath); 358 | constructor = await meta?.importGenerator(); 359 | if (namespace && !constructor) { 360 | // Await this.lookupLocalNamespaces(namespace); 361 | // constructor = await this.get(namespace); 362 | } 363 | 364 | if (constructor) { 365 | (constructor as any)._meta = meta; 366 | } 367 | } else { 368 | constructor = namespaceOrPath; 369 | } 370 | 371 | return this.instantiate(checkGenerator(constructor), ...arguments_); 372 | } 373 | 374 | /** 375 | * Instantiate a Generator with metadatas 376 | * 377 | * @param generator Generator class 378 | * @param instantiateOptions 379 | * @return The instantiated generator 380 | */ 381 | async instantiate( 382 | generator: GetGeneratorConstructor, 383 | instantiateOptions?: InstantiateOptions, 384 | ): Promise; 385 | async instantiate(constructor: GetGeneratorConstructor, ...arguments_: any[]): Promise { 386 | const composeOptions = arguments_.length > 0 ? (getInstantiateOptions(...arguments_) as InstantiateOptions) : {}; 387 | const { namespace = UNKNOWN_NAMESPACE, resolved = UNKNOWN_RESOLVED, _meta } = constructor as any; 388 | const environmentOptions = { env: this, resolved, namespace }; 389 | const generator = new constructor(composeOptions.generatorArgs ?? [], { 390 | ...this.sharedOptions, 391 | ...composeOptions.generatorOptions, 392 | ...environmentOptions, 393 | } as unknown as GetGeneratorOptions); 394 | 395 | (generator as any)._meta = _meta; 396 | (generator as any)._environmentOptions = { 397 | ...this.options, 398 | ...this.sharedOptions, 399 | ...environmentOptions, 400 | }; 401 | 402 | if (!composeOptions.generatorOptions?.help && generator._postConstruct) { 403 | await generator._postConstruct(); 404 | } 405 | 406 | return generator as unknown as G; 407 | } 408 | 409 | /** 410 | * @protected 411 | * Compose with the generator. 412 | * 413 | * @param {String} namespaceOrPath 414 | * @return {Generator} The instantiated generator or the singleton instance. 415 | */ 416 | async composeWith( 417 | generator: string | GetGeneratorConstructor, 418 | composeOptions?: ComposeOptions, 419 | ): Promise; 420 | async composeWith( 421 | generator: string | GetGeneratorConstructor, 422 | ...arguments_: any[] 423 | ): Promise { 424 | const options = getComposeOptions(...arguments_) as ComposeOptions; 425 | const { schedule: passedSchedule = true, ...instantiateOptions } = options; 426 | 427 | const generatorInstance = await this.create(generator, instantiateOptions); 428 | // Convert to function to keep type compatibility with old @yeoman/types where schedule is boolean only 429 | const schedule: (gen: G) => boolean = typeof passedSchedule === 'function' ? passedSchedule : () => passedSchedule; 430 | return this.queueGenerator(generatorInstance, { schedule: schedule(generatorInstance) }); 431 | } 432 | 433 | /** 434 | * Given a String `filepath`, tries to figure out the relative namespace. 435 | * 436 | * ### Examples: 437 | * 438 | * this.namespace('backbone/all/index.js'); 439 | * // => backbone:all 440 | * 441 | * this.namespace('generator-backbone/model'); 442 | * // => backbone:model 443 | * 444 | * this.namespace('backbone.js'); 445 | * // => backbone 446 | * 447 | * this.namespace('generator-mocha/backbone/model/index.js'); 448 | * // => mocha:backbone:model 449 | * 450 | * @param {String} filepath 451 | * @param {Array} lookups paths 452 | */ 453 | namespace(filepath: string, lookups = this.lookups) { 454 | return asNamespace(filepath, { lookups }); 455 | } 456 | 457 | /** 458 | * Returns the environment or dependency version. 459 | * @param {String} packageName - Module to get version. 460 | * @return {String} Environment version. 461 | */ 462 | getVersion(): string; 463 | getVersion(dependency: string): string | undefined; 464 | getVersion(packageName?: string): string | undefined { 465 | if (packageName && packageName !== 'yeoman-environment') { 466 | try { 467 | return require(`${packageName}/package.json`).version; 468 | } catch { 469 | return undefined; 470 | } 471 | } 472 | 473 | return ENVIRONMENT_VERSION; 474 | } 475 | 476 | /** 477 | * Queue generator run (queue itself tasks). 478 | * 479 | * @param {Generator} generator Generator instance 480 | * @param {boolean} [schedule=false] Whether to schedule the generator run. 481 | * @return {Generator} The generator or singleton instance. 482 | */ 483 | async queueGenerator(generator: G, queueOptions?: { schedule?: boolean }): Promise { 484 | const schedule = typeof queueOptions === 'boolean' ? queueOptions : (queueOptions?.schedule ?? false); 485 | const { added, identifier, generator: composedGenerator } = this.composedStore.addGenerator(generator); 486 | if (!added) { 487 | debug(`Using existing generator for namespace ${identifier}`); 488 | return composedGenerator as G; 489 | } 490 | 491 | this.emit('compose', identifier, generator); 492 | this.emit(`compose:${identifier}`, generator); 493 | 494 | const runGenerator = async () => { 495 | if (generator.queueTasks) { 496 | // Generator > 5 497 | this.once('run', () => generator.emit('run')); 498 | this.once('end', () => generator.emit('end')); 499 | await generator.queueTasks(); 500 | return; 501 | } 502 | 503 | if (!(generator.options as any).forwardErrorToEnvironment) { 504 | generator.on('error', (error: any) => this.emit('error', error)); 505 | } 506 | 507 | (generator as any).promise = (generator as any).run(); 508 | }; 509 | 510 | if (schedule) { 511 | this.queueTask('environment:run', async () => runGenerator()); 512 | } else { 513 | await runGenerator(); 514 | } 515 | 516 | return generator; 517 | } 518 | 519 | /** 520 | * Get the first generator that was queued to run in this environment. 521 | * 522 | * @return {Generator} generator queued to run in this environment. 523 | */ 524 | rootGenerator(): G { 525 | return this._rootGenerator as G; 526 | } 527 | 528 | async runGenerator(generator: BaseGenerator): Promise { 529 | generator = await this.queueGenerator(generator); 530 | 531 | this.compatibilityMode = generator.queueTasks ? false : 'v4'; 532 | this._rootGenerator = this._rootGenerator ?? generator; 533 | 534 | return this.start(generator.options); 535 | } 536 | 537 | /** 538 | * Registers a specific `generator` to this environment. This generator is stored under 539 | * provided namespace, or a default namespace format if none if available. 540 | * 541 | * @param name - Filepath to the a generator or a npm package name 542 | * @param namespace - Namespace under which register the generator (optional) 543 | * @param packagePath - PackagePath to the generator npm package (optional) 544 | * @return environment - This environment 545 | */ 546 | register(filePath: string, meta?: Partial | undefined): GeneratorMeta; 547 | register(generator: unknown, meta: BaseGeneratorMeta): GeneratorMeta; 548 | register(pathOrStub: unknown, meta?: Partial | BaseGeneratorMeta, ...arguments_: any[]): GeneratorMeta { 549 | if (typeof pathOrStub === 'string') { 550 | if (typeof meta === 'object') { 551 | return this.registerGeneratorPath(pathOrStub, meta.namespace, meta.packagePath); 552 | } 553 | 554 | // Backward compatibility 555 | return this.registerGeneratorPath(pathOrStub, meta, ...arguments_); 556 | } 557 | 558 | if (pathOrStub) { 559 | if (typeof meta === 'object') { 560 | return this.registerStub(pathOrStub, meta.namespace!, meta.resolved, meta.packagePath); 561 | } 562 | 563 | // Backward compatibility 564 | return this.registerStub(pathOrStub, meta as unknown as string, ...arguments_); 565 | } 566 | 567 | throw new TypeError('You must provide a generator name to register.'); 568 | } 569 | 570 | /** 571 | * Queue tasks 572 | * @param {string} priority 573 | * @param {(...args: any[]) => void | Promise} task 574 | * @param {{ once?: string, startQueue?: boolean }} [options] 575 | */ 576 | queueTask( 577 | priority: string, 578 | task: () => void | Promise, 579 | options?: { once?: string | undefined; startQueue?: boolean | undefined } | undefined, 580 | ): void { 581 | this.runLoop.add( 582 | priority, 583 | async (done: () => Record, stop: (argument: any) => Record) => { 584 | try { 585 | await task(); 586 | done(); 587 | } catch (error) { 588 | stop(error); 589 | } 590 | }, 591 | { 592 | once: options?.once, 593 | run: options?.startQueue ?? false, 594 | }, 595 | ); 596 | } 597 | 598 | /** 599 | * Add priority 600 | * @param {string} priority 601 | * @param {string} [before] 602 | */ 603 | addPriority(priority: string, before?: string | undefined): void { 604 | if (this.runLoop.queueNames.includes(priority)) { 605 | return; 606 | } 607 | 608 | this.runLoop.addSubQueue(priority, before); 609 | } 610 | 611 | /** 612 | * Search for generators and their sub generators. 613 | * 614 | * A generator is a `:lookup/:name/index.js` file placed inside an npm package. 615 | * 616 | * Defaults lookups are: 617 | * - ./ 618 | * - generators/ 619 | * - lib/generators/ 620 | * 621 | * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be 622 | * registered as `dummy:yo` generator. 623 | */ 624 | async lookup(options?: EnvironmentLookupOptions): Promise { 625 | const { 626 | registerToScope, 627 | customizeNamespace = (ns: string) => ns, 628 | lookups = this.lookups, 629 | ...remainingOptions 630 | } = options ?? { localOnly: false }; 631 | options = { 632 | ...remainingOptions, 633 | lookups, 634 | }; 635 | 636 | const generators: LookupGeneratorMeta[] = []; 637 | await lookupGenerators(options, ({ packagePath, filePath, lookups }) => { 638 | let repositoryPath = join(packagePath, '..'); 639 | if (basename(repositoryPath).startsWith('@')) { 640 | // Scoped package 641 | repositoryPath = join(repositoryPath, '..'); 642 | } 643 | 644 | let namespace = customizeNamespace(asNamespace(relative(repositoryPath, filePath), { lookups })); 645 | try { 646 | const resolved = realpathSync(filePath); 647 | if (!namespace) { 648 | namespace = customizeNamespace(asNamespace(resolved, { lookups })); 649 | } 650 | 651 | namespace = namespace!; 652 | if (registerToScope && !namespace.startsWith('@')) { 653 | namespace = `@${registerToScope}/${namespace}`; 654 | } 655 | 656 | const meta = this.store.add({ namespace, packagePath, resolved }); 657 | if (meta) { 658 | generators.push({ 659 | ...meta, 660 | registered: true, 661 | }); 662 | return Boolean(options?.singleResult); 663 | } 664 | } catch (error) { 665 | console.error('Unable to register %s (Error: %s)', filePath, error); 666 | } 667 | 668 | generators.push({ 669 | resolved: filePath, 670 | namespace: namespace!, 671 | packagePath, 672 | registered: false, 673 | }); 674 | 675 | return false; 676 | }); 677 | 678 | return generators; 679 | } 680 | 681 | /** 682 | * Verify if a package namespace already have been registered. 683 | * 684 | * @param packageNS - namespace of the package. 685 | * @return true if any generator of the package has been registered 686 | */ 687 | isPackageRegistered(packageNamespace: string): boolean { 688 | const registeredPackages = this.getRegisteredPackages(); 689 | return registeredPackages.includes(packageNamespace) || registeredPackages.includes(this.alias(packageNamespace).split(':', 2)[0]); 690 | } 691 | 692 | /** 693 | * Get all registered packages namespaces. 694 | * 695 | * @return array of namespaces. 696 | */ 697 | getRegisteredPackages(): string[] { 698 | return this.store.getPackagesNS(); 699 | } 700 | 701 | /** 702 | * Returns stored generators meta 703 | * @param namespace 704 | */ 705 | getGeneratorMeta(namespace: string): GeneratorMeta | undefined { 706 | const meta = this.store.getMeta(namespace) ?? this.store.getMeta(this.alias(namespace)); 707 | if (!meta) { 708 | return; 709 | } 710 | 711 | return { ...meta } as GeneratorMeta; 712 | } 713 | 714 | /** 715 | * Get or create an alias. 716 | * 717 | * Alias allows the `get()` and `lookup()` methods to search in alternate 718 | * filepath for a given namespaces. It's used for example to map `generator-*` 719 | * npm package to their namespace equivalent (without the generator- prefix), 720 | * or to default a single namespace like `angular` to `angular:app` or 721 | * `angular:all`. 722 | * 723 | * Given a single argument, this method acts as a getter. When both name and 724 | * value are provided, acts as a setter and registers that new alias. 725 | * 726 | * If multiple alias are defined, then the replacement is recursive, replacing 727 | * each alias in reverse order. 728 | * 729 | * An alias can be a single String or a Regular Expression. The finding is done 730 | * based on .match(). 731 | * 732 | * @param {String|RegExp} match 733 | * @param {String} value 734 | * 735 | * @example 736 | * 737 | * env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1'); 738 | * env.alias(/^([^:]+)$/, '$1:app'); 739 | * env.alias(/^([^:]+)$/, '$1:all'); 740 | * env.alias('foo'); 741 | * // => generator-foo:all 742 | */ 743 | alias(match: string | RegExp, value: string): this; 744 | alias(value: string): string; 745 | alias(match: string | RegExp, value?: string): string | this { 746 | if (match && value) { 747 | this.aliases.push({ 748 | match: match instanceof RegExp ? match : new RegExp(`^${match}$`), 749 | value, 750 | }); 751 | return this; 752 | } 753 | 754 | if (typeof match !== 'string') { 755 | throw new TypeError('string is required'); 756 | } 757 | 758 | const aliases = [...this.aliases].reverse(); 759 | 760 | return aliases.reduce((resolved, alias) => { 761 | if (!alias.match.test(resolved)) { 762 | return resolved; 763 | } 764 | 765 | return resolved.replace(alias.match, alias.value); 766 | }, match); 767 | } 768 | 769 | /** 770 | * Watch for package.json and queue package manager install task. 771 | */ 772 | public watchForPackageManagerInstall({ 773 | cwd, 774 | queueTask, 775 | installTask, 776 | }: { 777 | cwd?: string; 778 | queueTask?: boolean; 779 | installTask?: (nodePackageManager: string | undefined, defaultTask: () => Promise) => void | Promise; 780 | } = {}) { 781 | if (cwd && !installTask) { 782 | throw new Error(`installTask is required when using a custom cwd`); 783 | } 784 | 785 | const npmCwd = cwd ?? this.cwd; 786 | 787 | const queueInstallTask = () => { 788 | this.queueTask( 789 | 'install', 790 | async () => { 791 | if (this.compatibilityMode === 'v4') { 792 | debug('Running in generator < 5 compatibility. Package manager install is done by the generator.'); 793 | return; 794 | } 795 | 796 | const { adapter, sharedFs: memFs } = this; 797 | const { skipInstall, nodePackageManager } = this.options; 798 | await packageManagerInstallTask({ 799 | adapter, 800 | memFs, 801 | packageJsonLocation: npmCwd, 802 | skipInstall, 803 | nodePackageManager, 804 | customInstallTask: installTask ?? this.composedStore.customInstallTask, 805 | }); 806 | }, 807 | { once: `package manager install ${npmCwd}` }, 808 | ); 809 | }; 810 | 811 | this.sharedFs.on('change', file => { 812 | if (file === join(npmCwd, 'package.json')) { 813 | queueInstallTask(); 814 | } 815 | }); 816 | 817 | if (queueTask) { 818 | queueInstallTask(); 819 | } 820 | } 821 | 822 | /** 823 | * Start Environment queue 824 | * @param {Object} options - Conflicter options. 825 | */ 826 | protected async start(options: any) { 827 | return new Promise((resolve, reject) => { 828 | Object.assign(this.options, removePropertiesWithNullishValues(pick(options, ['skipInstall', 'nodePackageManager']))); 829 | this.logCwd = options.logCwd ?? this.logCwd; 830 | this.conflicterOptions = pick(defaults({}, this.options, options), ['force', 'bail', 'ignoreWhitespace', 'dryRun', 'skipYoResolve']); 831 | this.conflicterOptions.cwd = this.logCwd; 832 | 833 | this.queueCommit(); 834 | this.queueTask('install', () => { 835 | // Postpone watchForPackageManagerInstall to install priority since env's cwd can be changed by generators 836 | this.watchForPackageManagerInstall({ queueTask: true }); 837 | }); 838 | 839 | /* 840 | * Listen to errors and reject if emmited. 841 | * Some cases the generator relied at the behavior that the running process 842 | * would be killed if an error is thrown to environment. 843 | * Make sure to not rely on that behavior. 844 | */ 845 | this.on('error', async error => { 846 | this.runLoop.pause(); 847 | await this.adapter.onIdle?.(); 848 | reject(error); 849 | this.adapter.close(); 850 | }); 851 | 852 | this.once('end', async () => { 853 | await this.adapter.onIdle?.(); 854 | resolve(); 855 | this.adapter.close(); 856 | }); 857 | 858 | /* 859 | * For backward compatibility 860 | */ 861 | this.on('generator:reject', error => { 862 | this.emit('error', error); 863 | }); 864 | 865 | /* 866 | * For backward compatibility 867 | */ 868 | this.on('generator:resolve', () => { 869 | this.emit('end'); 870 | }); 871 | 872 | this.runLoop.on('error', (error: any) => { 873 | this.emit('error', error); 874 | }); 875 | 876 | this.runLoop.on('paused', () => { 877 | this.emit('paused'); 878 | }); 879 | 880 | /* If runLoop has ended, the environment has ended too. */ 881 | this.runLoop.once('end', () => { 882 | this.emit('end'); 883 | }); 884 | 885 | this.emit('run'); 886 | this.runLoop.start(); 887 | }); 888 | } 889 | 890 | /** 891 | * Queue environment's commit task. 892 | */ 893 | protected queueCommit() { 894 | const queueCommit = () => { 895 | debug('Queueing conflicts task'); 896 | this.queueTask( 897 | 'environment:conflicts', 898 | async () => { 899 | debug('Adding queueCommit listener'); 900 | // Conflicter can change files add listener before commit task. 901 | const changedFileHandler = (filePath: string) => { 902 | const file = this.sharedFs.get(filePath); 903 | if (isFilePending(file)) { 904 | queueCommit(); 905 | this.sharedFs.removeListener('change', changedFileHandler); 906 | } 907 | }; 908 | 909 | this.sharedFs.on('change', changedFileHandler); 910 | 911 | debug('Running conflicts'); 912 | const { customCommitTask = async () => commitSharedFsTask(this) } = this.composedStore; 913 | if (typeof customCommitTask === 'function') { 914 | await customCommitTask(); 915 | } else { 916 | debug('Ignoring commit, custom commit was provided'); 917 | } 918 | }, 919 | { 920 | once: 'write memory fs to disk', 921 | }, 922 | ); 923 | }; 924 | 925 | queueCommit(); 926 | } 927 | 928 | /** 929 | * Registers a specific `generator` to this environment. This generator is stored under 930 | * provided namespace, or a default namespace format if none if available. 931 | * 932 | * @param name - Filepath to the a generator or a npm package name 933 | * @param namespace - Namespace under which register the generator (optional) 934 | * @param packagePath - PackagePath to the generator npm package (optional) 935 | * @return environment - This environment 936 | */ 937 | protected registerGeneratorPath(generatorPath: string, namespace?: string, packagePath?: string): GeneratorMeta { 938 | if (typeof generatorPath !== 'string') { 939 | throw new TypeError('You must provide a generator name to register.'); 940 | } 941 | 942 | if (!isAbsolute(generatorPath)) { 943 | throw new Error(`An absolute path is required to register`); 944 | } 945 | 946 | namespace = namespace ?? this.namespace(generatorPath); 947 | 948 | if (!namespace) { 949 | throw new Error('Unable to determine namespace.'); 950 | } 951 | 952 | // Generator is already registered and matches the current namespace. 953 | const generatorMeta = this.store.getMeta(namespace); 954 | if (generatorMeta && generatorMeta.resolved === generatorPath) { 955 | return generatorMeta; 956 | } 957 | 958 | const meta = this.store.add({ namespace, resolved: generatorPath, packagePath }); 959 | 960 | debug('Registered %s (%s) on package %s (%s)', namespace, generatorPath, meta.packageNamespace, packagePath); 961 | return meta; 962 | } 963 | 964 | /** 965 | * Register a stubbed generator to this environment. This method allow to register raw 966 | * functions under the provided namespace. `registerStub` will enforce the function passed 967 | * to extend the Base generator automatically. 968 | * 969 | * @param Generator - A Generator constructor or a simple function 970 | * @param namespace - Namespace under which register the generator 971 | * @param resolved - The file path to the generator 972 | * @param packagePath - The generator's package path 973 | */ 974 | protected registerStub(Generator: any, namespace: string, resolved = UNKNOWN_RESOLVED, packagePath?: string): GeneratorMeta { 975 | if (typeof Generator !== 'function' && typeof Generator.createGenerator !== 'function') { 976 | throw new TypeError('You must provide a stub function to register.'); 977 | } 978 | 979 | if (typeof namespace !== 'string') { 980 | throw new TypeError('You must provide a namespace to register.'); 981 | } 982 | 983 | const meta = this.store.add({ namespace, resolved, packagePath }, Generator); 984 | 985 | debug('Registered %s (%s) on package (%s)', namespace, resolved, packagePath); 986 | return meta; 987 | } 988 | 989 | /** 990 | * @experimental 991 | * Get a map to store shared data, usually a generator root path to share a map by path. 992 | */ 993 | getContextMap(key: string, factory = () => new Map()): Map { 994 | if (this.contextStore.has(key)) { 995 | return this.contextStore.get(key)!; 996 | } 997 | 998 | const context = factory(); 999 | this.contextStore.set(key, context); 1000 | return context; 1001 | } 1002 | } 1003 | -------------------------------------------------------------------------------- /src/environment-full.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'node:crypto'; 2 | import { join, resolve } from 'node:path'; 3 | import type { BaseGeneratorConstructor, InputOutputAdapter } from '@yeoman/types'; 4 | import { type YeomanNamespace, requireNamespace, toNamespace } from '@yeoman/namespace'; 5 | import { flyImport } from 'fly-import'; 6 | import { defaults, pick, uniq } from 'lodash-es'; 7 | import { valid } from 'semver'; 8 | import { type LookupOptions } from './generator-lookup.js'; 9 | import YeomanCommand from './util/command.js'; 10 | import EnvironmentBase, { type EnvironmentOptions } from './environment-base.js'; 11 | import { splitArgsFromString as splitArgumentsFromString } from './util/util.js'; 12 | 13 | class FullEnvironment extends EnvironmentBase { 14 | constructor(options?: EnvironmentOptions); 15 | constructor(options: EnvironmentOptions = {}, adapterCompat?: InputOutputAdapter) { 16 | if (adapterCompat) { 17 | options.adapter = adapterCompat; 18 | } 19 | 20 | super(options); 21 | 22 | this.loadSharedOptions(this.options); 23 | if (this.sharedOptions.skipLocalCache === undefined) { 24 | this.sharedOptions.skipLocalCache = true; 25 | } 26 | } 27 | 28 | /** 29 | * Load options passed to the Generator that should be used by the Environment. 30 | * 31 | * @param {Object} options 32 | */ 33 | loadEnvironmentOptions(options: EnvironmentOptions) { 34 | const environmentOptions = pick(options, ['skipInstall', 'nodePackageManager']); 35 | defaults(this.options, environmentOptions); 36 | return environmentOptions; 37 | } 38 | 39 | /** 40 | * Load options passed to the Environment that should be forwarded to the Generator. 41 | * 42 | * @param {Object} options 43 | */ 44 | loadSharedOptions(options: EnvironmentOptions) { 45 | const optionsToShare = pick(options, [ 46 | 'skipInstall', 47 | 'forceInstall', 48 | 'skipCache', 49 | 'skipLocalCache', 50 | 'skipParseOptions', 51 | 'localConfigOnly', 52 | 'askAnswered', 53 | ]); 54 | Object.assign(this.sharedOptions, optionsToShare); 55 | return optionsToShare; 56 | } 57 | 58 | /** 59 | * @protected 60 | * Outputs the general help and usage. Optionally, if generators have been 61 | * registered, the list of available generators is also displayed. 62 | * 63 | * @param {String} name 64 | */ 65 | help(name = 'init') { 66 | const out = [ 67 | 'Usage: :binary: GENERATOR [args] [options]', 68 | '', 69 | 'General options:', 70 | " --help # Print generator's options and usage", 71 | ' -f, --force # Overwrite files that already exist', 72 | '', 73 | 'Please choose a generator below.', 74 | '', 75 | ]; 76 | 77 | const ns = this.namespaces(); 78 | 79 | const groups: Record = {}; 80 | for (const namespace of ns) { 81 | const base = namespace.split(':')[0]; 82 | 83 | if (!groups[base]) { 84 | groups[base] = []; 85 | } 86 | 87 | groups[base].push(namespace); 88 | } 89 | 90 | for (const key of Object.keys(groups).sort()) { 91 | const group = groups[key]; 92 | 93 | if (group.length > 0) { 94 | out.push('', key.charAt(0).toUpperCase() + key.slice(1)); 95 | } 96 | 97 | for (const ns of groups[key]) { 98 | out.push(` ${ns}`); 99 | } 100 | } 101 | 102 | return out.join('\n').replaceAll(':binary:', name); 103 | } 104 | 105 | /** 106 | * @protected 107 | * Returns the list of registered namespace. 108 | * @return {Array} 109 | */ 110 | namespaces() { 111 | return this.store.namespaces(); 112 | } 113 | 114 | /** 115 | * @protected 116 | * Returns stored generators meta 117 | * @return {Object} 118 | */ 119 | getGeneratorsMeta() { 120 | return this.store.getGeneratorsMeta(); 121 | } 122 | 123 | /** 124 | * @protected 125 | * Get registered generators names 126 | * 127 | * @return {Array} 128 | */ 129 | getGeneratorNames() { 130 | return uniq(Object.keys(this.getGeneratorsMeta()).map(namespace => toNamespace(namespace)?.packageNamespace)); 131 | } 132 | 133 | /** 134 | * Get last added path for a namespace 135 | * 136 | * @param {String} - namespace 137 | * @return {String} - path of the package 138 | */ 139 | getPackagePath(namespace: string) { 140 | if (namespace.includes(':')) { 141 | const generator = this.getGeneratorMeta(namespace); 142 | return generator?.packagePath; 143 | } 144 | 145 | const packagePaths = this.getPackagePaths(namespace) || []; 146 | return packagePaths[0]; 147 | } 148 | 149 | /** 150 | * Get paths for a namespace 151 | * 152 | * @param - namespace 153 | * @return array of paths. 154 | */ 155 | getPackagePaths(namespace: string) { 156 | return ( 157 | this.store.getPackagesPaths()[namespace] || this.store.getPackagesPaths()[requireNamespace(this.alias(namespace)).packageNamespace] 158 | ); 159 | } 160 | 161 | /** 162 | * Generate a command for the generator and execute. 163 | * 164 | * @param {string} generatorNamespace 165 | * @param {string[]} args 166 | */ 167 | async execute(generatorNamespace: string, arguments_ = []) { 168 | const namespace = requireNamespace(generatorNamespace); 169 | if (!(await this.get(namespace.namespace))) { 170 | await this.lookup({ 171 | packagePatterns: [namespace.generatorHint], 172 | singleResult: true, 173 | }); 174 | } 175 | 176 | if (!(await this.get(namespace.namespace))) { 177 | await this.installLocalGenerators({ 178 | [namespace.generatorHint]: namespace.semver, 179 | }); 180 | } 181 | 182 | const namespaceCommand = this.command ? this.command.command(namespace.namespace) : new YeomanCommand(); 183 | namespaceCommand.usage('[generator-options]'); 184 | 185 | // Instantiate the generator for options 186 | const generator = await this.create(namespace.namespace, { generatorArgs: [], generatorOptions: { help: true } }); 187 | namespaceCommand.registerGenerator(generator); 188 | 189 | (namespaceCommand as any)._parseCommand([], arguments_); 190 | return this.run([namespace.namespace, ...namespaceCommand.args], { 191 | ...namespaceCommand.opts(), 192 | }); 193 | } 194 | 195 | async requireGenerator(namespace: string): Promise { 196 | if (namespace === undefined) { 197 | try { 198 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 199 | // @ts-ignore 200 | // eslint-disable-next-line import-x/no-unresolved 201 | const { default: Generator } = await import('yeoman-generator'); 202 | return Generator; 203 | } catch { 204 | // ignore error 205 | } 206 | 207 | const { default: Generator } = await flyImport('yeoman-generator'); 208 | return Generator; 209 | } 210 | 211 | // Namespace is a version 212 | if (valid(namespace)) { 213 | // Create a hash to install any version range in the local repository 214 | const hash = createHash('shake256', { outputLength: 2 }).update(namespace, 'utf8').digest('hex'); 215 | 216 | const { default: Generator } = await flyImport(`@yeoman/generator-impl-${hash}@npm:yeoman-generator@${namespace}`); 217 | return Generator; 218 | } 219 | 220 | return this.get(namespace); 221 | } 222 | 223 | /** 224 | * Install generators at the custom local repository and register. 225 | * 226 | * @param {Object} packages - packages to install key(packageName): value(versionRange). 227 | * @return {Boolean} - true if the install succeeded. 228 | */ 229 | async installLocalGenerators(packages: Record) { 230 | const entries = Object.entries(packages); 231 | const specs = entries.map(([packageName, version]) => `${packageName}${version ? `@${version}` : ''}`); 232 | const installResult = await this.repository.install(specs); 233 | const failToInstall = installResult.find(result => !result.path); 234 | if (failToInstall) { 235 | throw new Error(`Fail to install ${failToInstall.pkgid}`); 236 | } 237 | 238 | await this.lookup({ packagePaths: installResult.map(result => result.path) as string[] }); 239 | return true; 240 | } 241 | 242 | /** 243 | * Lookup and register generators from the custom local repository. 244 | * 245 | * @param {String[]} [packagesToLookup='generator-*'] - packages to lookup. 246 | */ 247 | async lookupLocalPackages(packagesToLookup = ['generator-*']) { 248 | await this.lookup({ 249 | packagePatterns: packagesToLookup, 250 | npmPaths: [join(this.repository.repositoryPath, 'node_modules')], 251 | }); 252 | } 253 | 254 | /** 255 | * Lookup and register generators from the custom local repository. 256 | * 257 | * @private 258 | * @param {YeomanNamespace[]} namespacesToLookup - namespaces to lookup. 259 | * @return {Promise} List of generators 260 | */ 261 | async lookupLocalNamespaces(namespacesToLookup: string | string[]) { 262 | if (!namespacesToLookup) { 263 | return []; 264 | } 265 | 266 | namespacesToLookup = Array.isArray(namespacesToLookup) ? namespacesToLookup : [namespacesToLookup]; 267 | const namespaces = namespacesToLookup.map(ns => requireNamespace(ns)); 268 | // Keep only those packages that has a compatible version. 269 | return this.lookupLocalPackages(namespaces.map(ns => ns.generatorHint)); 270 | } 271 | 272 | /** 273 | * Search for generators or sub generators by namespace. 274 | * 275 | * @private 276 | * @param {boolean|Object} [options] options passed to lookup. Options singleResult, 277 | * filePatterns and packagePatterns can be overridden 278 | * @return {Array|Object} List of generators 279 | */ 280 | async lookupNamespaces(namespaces: string | string[], options: LookupOptions = {}) { 281 | if (!namespaces) { 282 | return []; 283 | } 284 | 285 | namespaces = Array.isArray(namespaces) ? namespaces : [namespaces]; 286 | const namespacesObjs = namespaces.map(ns => requireNamespace(ns)); 287 | const options_ = namespacesObjs.map(ns => { 288 | const nsOptions: LookupOptions = { packagePatterns: [ns.generatorHint] }; 289 | if (ns.generator) { 290 | // Build filePatterns to look specifically for the namespace. 291 | const genPath = ns.generator.split(':').join('/'); 292 | let filePatterns = [`${genPath}/index.?s`, `${genPath}.?s`]; 293 | const lookups = options.lookups ?? this.lookups; 294 | filePatterns = lookups.flatMap(prefix => filePatterns.map(pattern => join(prefix, pattern))); 295 | nsOptions.filePatterns = filePatterns; 296 | nsOptions.singleResult = true; 297 | } 298 | 299 | return nsOptions; 300 | }); 301 | return Promise.all(options_.flatMap(async opt => this.lookup({ ...opt, ...options }))); 302 | } 303 | 304 | /** 305 | * Load or install namespaces based on the namespace flag 306 | * 307 | * @private 308 | * @param {String|Array} - namespaces 309 | * @return {boolean} - true if every required namespace was found. 310 | */ 311 | async prepareEnvironment(namespaces: string | string[]) { 312 | namespaces = Array.isArray(namespaces) ? namespaces : [namespaces]; 313 | let missing = namespaces.map(ns => requireNamespace(ns)); 314 | const updateMissing = async () => { 315 | const entries = await Promise.all(missing.map(async ns => [ns, await this.get(ns)])); 316 | missing = entries.filter(([_ns, gen]) => Boolean(gen)).map(([ns]) => ns) as YeomanNamespace[]; 317 | }; 318 | 319 | await updateMissing(); 320 | 321 | // Install missing 322 | const toInstall: Record = Object.fromEntries(missing.map(ns => [ns.generatorHint, ns.semver])); 323 | 324 | await this.installLocalGenerators(toInstall); 325 | 326 | await updateMissing(); 327 | 328 | if (missing.length === 0) { 329 | return true; 330 | } 331 | 332 | throw new Error(`Error preparing environment for ${missing.map(ns => ns.complete).join(',')}`); 333 | } 334 | 335 | /** 336 | * Tries to locate and run a specific generator. The lookup is done depending 337 | * on the provided arguments, options and the list of registered generators. 338 | * 339 | * When the environment was unable to resolve a generator, an error is raised. 340 | * 341 | * @param {String|Array} args 342 | * @param {Object} [options] 343 | */ 344 | async run(arguments_?: string[], options?: any) { 345 | arguments_ = Array.isArray(arguments_) ? arguments_ : splitArgumentsFromString(arguments_ as unknown as string); 346 | options = { ...options }; 347 | 348 | let name = arguments_.shift(); 349 | if (!name) { 350 | throw new Error('Must provide at least one argument, the generator namespace to invoke.'); 351 | } 352 | 353 | if (name.startsWith('.')) { 354 | name = resolve(name); 355 | } 356 | 357 | this.loadEnvironmentOptions(options); 358 | 359 | if (this.experimental && !this.getGeneratorMeta(name)) { 360 | try { 361 | await this.prepareEnvironment(name); 362 | } catch { 363 | // ignore error 364 | } 365 | } 366 | 367 | const generator = await this.create(name, { 368 | generatorArgs: arguments_, 369 | generatorOptions: { 370 | ...options, 371 | initialGenerator: true, 372 | }, 373 | }); 374 | 375 | if (options.help) { 376 | console.log((generator as any).help()); 377 | return; 378 | } 379 | 380 | return this.runGenerator(generator); 381 | } 382 | } 383 | 384 | export default FullEnvironment; 385 | -------------------------------------------------------------------------------- /src/generator-lookup.ts: -------------------------------------------------------------------------------- 1 | import { extname, isAbsolute, join, posix } from 'node:path'; 2 | import { pathToFileURL } from 'node:url'; 3 | import { type LookupOptions as LookupOptionsApi } from '@yeoman/types'; 4 | import { requireNamespace, toNamespace } from '@yeoman/namespace'; 5 | import { type ModuleLookupOptions, findPackagesIn, getNpmPaths, moduleLookupSync } from './module-lookup.js'; 6 | import { asNamespace, defaultLookups } from './util/namespace.js'; 7 | 8 | export type LookupOptions = LookupOptionsApi & 9 | ModuleLookupOptions & { 10 | lookups?: string[]; 11 | }; 12 | 13 | type LookupMeta = { filePath: string; packagePath: string; lookups: string[] }; 14 | 15 | export const defaultExtensions = ['.ts', '.cts', '.mts', '.js', '.cjs', '.mjs']; 16 | 17 | /** 18 | * Search for generators and their sub generators. 19 | * 20 | * A generator is a `:lookup/:name/index.js` file placed inside an npm package. 21 | * 22 | * Defaults lookups are: 23 | * - ./ 24 | * - generators/ 25 | * - lib/generators/ 26 | * 27 | * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be 28 | * registered as `dummy:yo` generator. 29 | * 30 | * @param {boolean|Object} [options] 31 | * @param {boolean} [options.localOnly = false] - Set true to skip lookups of 32 | * globally-installed generators. 33 | * @param {string|Array} [options.packagePaths] - Paths to look for generators. 34 | * @param {string|Array} [options.npmPaths] - Repository paths to look for generators packages. 35 | * @param {string|Array} [options.filePatterns='*\/index.js'] - File pattern to look for. 36 | * @param {string|Array} [options.packagePatterns='generator-*'] - Package pattern to look for. 37 | * @param {boolean} [options.singleResult=false] - Set true to stop lookup on the first match. 38 | * @param {Number} [options.globbyDeep] - Deep option to be passed to globby. 39 | * @return {Promise} List of generators 40 | */ 41 | export async function lookupGenerators(options: LookupOptions = {}, register?: (meta: LookupMeta) => boolean) { 42 | const { lookups = defaultLookups } = options; 43 | options = { 44 | // Js generators should be after, last will override registered one. 45 | filePatterns: lookups.flatMap(prefix => defaultExtensions.map(extension => `${prefix}/*/index${extension}`)), 46 | filterPaths: false, 47 | packagePatterns: ['generator-*'], 48 | reverse: !options.singleResult, 49 | ...options, 50 | }; 51 | 52 | return moduleLookupSync(options, ({ packagePath, files }) => { 53 | files = [...files].sort((a, b) => { 54 | return defaultExtensions.indexOf(extname(a)) - defaultExtensions.indexOf(extname(b)); 55 | }); 56 | for (const filePath of files) { 57 | const registered = register?.({ filePath, packagePath, lookups }); 58 | if (options.singleResult && registered) { 59 | return filePath; 60 | } 61 | } 62 | 63 | return; 64 | }); 65 | } 66 | 67 | /** 68 | * Lookup for a specific generator. 69 | * 70 | * @param {String} namespace 71 | * @param {Object} [options] 72 | * @param {Boolean} [options.localOnly=false] - Set true to skip lookups of 73 | * globally-installed generators. 74 | * @param {Boolean} [options.packagePath=false] - Set true to return the package 75 | * path instead of generators file. 76 | * @param {Boolean} [options.singleResult=true] - Set false to return multiple values. 77 | * @return {String} generator 78 | */ 79 | export function lookupGenerator(namespace: string, options?: ModuleLookupOptions & { packagePath?: boolean; generatorPath?: boolean }) { 80 | options = typeof options === 'boolean' ? { localOnly: options } : (options ?? {}); 81 | options.singleResult = options.singleResult ?? true; 82 | 83 | options.filePatterns = options.filePatterns ?? defaultLookups.map(prefix => join(prefix, '*/index.{js,ts}')); 84 | const ns = requireNamespace(namespace); 85 | options.packagePatterns = options.packagePatterns ?? [ns.generatorHint]; 86 | 87 | options.npmPaths = options.npmPaths ?? getNpmPaths({ localOnly: options.localOnly }).reverse(); 88 | options.packagePatterns = options.packagePatterns ?? ['generator-*']; 89 | options.packagePaths = options.packagePaths ?? findPackagesIn(options.npmPaths, options.packagePatterns); 90 | 91 | let paths: string[] | string | undefined = options.singleResult ? undefined : []; 92 | moduleLookupSync(options, ({ files, packagePath }) => { 93 | for (const filename of files) { 94 | const fileNs = asNamespace(filename, { lookups: defaultLookups }); 95 | const ns = toNamespace(fileNs); 96 | if (namespace === fileNs || (options!.packagePath && namespace === ns?.packageNamespace)) { 97 | // Version 2.6.0 returned pattern instead of modulePath for options.packagePath 98 | const returnPath = options!.packagePath ? packagePath : options!.generatorPath ? posix.join(filename, '../../') : filename; 99 | if (options!.singleResult) { 100 | paths = returnPath; 101 | return filename; 102 | } 103 | 104 | (paths as string[]).push(returnPath); 105 | } 106 | } 107 | 108 | return; 109 | }); 110 | 111 | if (options.singleResult) { 112 | const generatorPath = paths as unknown as string; 113 | return generatorPath && isAbsolute(generatorPath) ? pathToFileURL(generatorPath).toString() : generatorPath; 114 | } 115 | 116 | return paths!.map(gen => (isAbsolute(gen) ? pathToFileURL(gen).toString() : gen)); 117 | } 118 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { type EnvironmentOptions } from './environment-base.js'; 2 | import Environment from './environment-full.js'; 3 | 4 | export { default } from './environment-full.js'; 5 | export { default as EnvironmentBase } from './environment-base.js'; 6 | 7 | export const createEnv = (options?: EnvironmentOptions) => new Environment(options); 8 | 9 | export * from './commands.js'; 10 | export * from './util/command.js'; 11 | export * from './package-manager.js'; 12 | export * from './commit.js'; 13 | export { lookupGenerator } from './generator-lookup.js'; 14 | -------------------------------------------------------------------------------- /src/module-lookup.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, lstatSync, readFileSync } from 'node:fs'; 2 | import { delimiter, dirname, join, resolve, sep } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import process from 'node:process'; 5 | import arrify from 'arrify'; 6 | import { compact, uniq } from 'lodash-es'; 7 | import { type Options as GlobbyOptions, globbySync } from 'globby'; 8 | import slash from 'slash'; 9 | import createdLogger from 'debug'; 10 | import { execaOutput } from './util/util.js'; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = dirname(__filename); 14 | 15 | const PROJECT_ROOT = join(__dirname, '..'); 16 | 17 | const PACKAGE_NAME_PATTERN = [JSON.parse(readFileSync(join(PROJECT_ROOT, 'package.json')).toString()).name]; 18 | 19 | const win32 = process.platform === 'win32'; 20 | const nvm = process.env.NVM_HOME; 21 | 22 | const debug = createdLogger('yeoman:environment'); 23 | 24 | export type ModuleLookupOptions = { 25 | /** Set true to skip lookups of globally-installed generators */ 26 | localOnly?: boolean; 27 | /** Paths to look for generators */ 28 | packagePaths?: string[]; 29 | /** Repository paths to look for generators packages */ 30 | npmPaths?: string[]; 31 | /** File pattern to look for */ 32 | filePatterns?: string[]; 33 | /** The package patterns to look for */ 34 | packagePatterns?: string[]; 35 | /** A value indicating whether the lookup should be stopped after finding the first result */ 36 | singleResult?: boolean; 37 | filterPaths?: boolean; 38 | /** Set true reverse npmPaths/packagePaths order */ 39 | reverse?: boolean; 40 | /** The `deep` option to pass to `globby` */ 41 | globbyDeep?: number; 42 | globbyOptions?: any; 43 | }; 44 | 45 | /** 46 | * Search for npm packages. 47 | */ 48 | export function moduleLookupSync( 49 | options: ModuleLookupOptions, 50 | find: (argument: { files: string[]; packagePath: string }) => string | undefined, 51 | ) { 52 | debug('Running lookup with options: %o', options); 53 | options = { ...options }; 54 | options.filePatterns = arrify(options.filePatterns ?? 'package.json').map(filePattern => slash(filePattern)); 55 | 56 | if (options.packagePaths) { 57 | options.packagePaths = arrify(options.packagePaths); 58 | if (options.reverse) { 59 | options.packagePaths = options.packagePaths.reverse(); 60 | } 61 | } else { 62 | options.npmPaths = options.npmPaths ?? getNpmPaths(options); 63 | if (options.reverse && Array.isArray(options.npmPaths)) { 64 | options.npmPaths = options.npmPaths.reverse(); 65 | } 66 | 67 | options.packagePatterns = arrify(options.packagePatterns ?? PACKAGE_NAME_PATTERN).map(packagePattern => slash(packagePattern)); 68 | options.packagePaths = findPackagesIn(options.npmPaths, options.packagePatterns); 69 | } 70 | 71 | debug('Lookup calculated options: %o', options); 72 | 73 | const modules = []; 74 | for (const packagePath of options.packagePaths) { 75 | if (!existsSync(packagePath) || (!lstatSync(packagePath).isDirectory() && !lstatSync(packagePath).isSymbolicLink())) { 76 | continue; 77 | } 78 | 79 | const files = globbySync(options.filePatterns, { 80 | cwd: packagePath, 81 | absolute: true, 82 | ...options.globbyOptions, 83 | } as GlobbyOptions); 84 | 85 | const filePath = find({ files, packagePath }); 86 | if (filePath) { 87 | return [{ filePath, packagePath }]; 88 | } 89 | 90 | for (const filePath of files) { 91 | modules.push({ filePath, packagePath }); 92 | } 93 | } 94 | 95 | return modules; 96 | } 97 | 98 | /** 99 | * Search npm for every available generators. 100 | * Generators are npm packages who's name start with `generator-` and who're placed in the 101 | * top level `node_module` path. They can be installed globally or locally. 102 | * 103 | * @method 104 | * 105 | * @param searchPaths List of search paths 106 | * @param packagePatterns Pattern of the packages 107 | * @param globbyOptions 108 | * @return List of the generator modules path 109 | */ 110 | export function findPackagesIn(searchPaths: string[], packagePatterns: string[], globbyOptions?: any): any[] { 111 | searchPaths = arrify(searchPaths) 112 | .filter(Boolean) 113 | .map(npmPath => resolve(npmPath)); 114 | let modules: any[] = []; 115 | for (const root of searchPaths) { 116 | if (!existsSync(root) || (!lstatSync(root).isDirectory() && !lstatSync(root).isSymbolicLink())) { 117 | continue; 118 | } 119 | 120 | // Some folders might not be readable to the current user. For those, we add a try 121 | // catch to handle the error gracefully as globby doesn't have an option to skip 122 | // restricted folders. 123 | try { 124 | modules = modules.concat( 125 | globbySync(packagePatterns, { 126 | cwd: root, 127 | onlyDirectories: true, 128 | expandDirectories: false, 129 | absolute: true, 130 | deep: 0, 131 | ...globbyOptions, 132 | } as GlobbyOptions), 133 | ); 134 | 135 | // To limit recursive lookups into non-namespace folders within globby, 136 | // fetch all namespaces in root, then search each namespace separately 137 | // for generator modules 138 | const scopes = globbySync(['@*'], { 139 | cwd: root, 140 | onlyDirectories: true, 141 | expandDirectories: false, 142 | absolute: true, 143 | deep: 0, 144 | ...globbyOptions, 145 | } as GlobbyOptions); 146 | 147 | for (const scope of scopes) { 148 | modules = modules.concat( 149 | globbySync(packagePatterns, { 150 | cwd: scope, 151 | onlyDirectories: true, 152 | expandDirectories: false, 153 | absolute: true, 154 | deep: 0, 155 | ...globbyOptions, 156 | } as GlobbyOptions), 157 | ); 158 | } 159 | } catch (error) { 160 | debug('Could not access %s (%s)', root, error); 161 | } 162 | } 163 | 164 | return modules; 165 | } 166 | 167 | /** 168 | * Get the npm lookup directories (`node_modules/`) 169 | * 170 | * @method 171 | * 172 | * @param {boolean|Object} [options] 173 | * @param {boolean} [options.localOnly = false] - Set true to skip lookups of 174 | * globally-installed generators. 175 | * @param {boolean} [options.filterPaths = false] - Remove paths that don't ends 176 | * with a supported path (don't touch at NODE_PATH paths). 177 | * @return {Array} lookup paths 178 | */ 179 | export function getNpmPaths(options: { localOnly?: boolean; filterPaths?: boolean } = {}): string[] { 180 | // Resolve signature where options is boolean (localOnly). 181 | if (typeof options === 'boolean') { 182 | options = { localOnly: options }; 183 | } 184 | 185 | // Start with the local paths. 186 | let paths = getLocalNpmPaths(); 187 | 188 | // Append global paths, unless they should be excluded. 189 | if (!options.localOnly) { 190 | paths = paths.concat(getGlobalNpmPaths(options.filterPaths)); 191 | } 192 | 193 | return uniq(paths); 194 | } 195 | 196 | /** 197 | * Get the local npm lookup directories 198 | * @private 199 | * @return {Array} lookup paths 200 | */ 201 | function getLocalNpmPaths(): string[] { 202 | const paths: string[] = []; 203 | 204 | // Walk up the CWD and add `node_modules/` folder lookup on each level 205 | process 206 | .cwd() 207 | .split(sep) 208 | .forEach((part, index, parts) => { 209 | let lookup = join(...parts.slice(0, index + 1), 'node_modules'); 210 | 211 | if (!win32) { 212 | lookup = `/${lookup}`; 213 | } 214 | 215 | paths.push(lookup); 216 | }); 217 | 218 | return uniq(paths.reverse()); 219 | } 220 | 221 | /** 222 | * Get the global npm lookup directories 223 | * Reference: https://nodejs.org/api/modules.html 224 | * @private 225 | * @return {Array} lookup paths 226 | */ 227 | function getGlobalNpmPaths(filterPaths = true): string[] { 228 | let paths: string[] = []; 229 | 230 | // Node.js will search in the following list of GLOBAL_FOLDERS: 231 | // 1: $HOME/.node_modules 232 | // 2: $HOME/.node_libraries 233 | // 3: $PREFIX/lib/node 234 | const filterValidNpmPath = function (path: string, ignore = false): string[] { 235 | return ignore ? [path] : ['/node_modules', '/.node_modules', '/.node_libraries', '/node'].some(dir => path.endsWith(dir)) ? [path] : []; 236 | }; 237 | 238 | // Default paths for each system 239 | if (nvm && process.env.NVM_HOME) { 240 | paths.push(join(process.env.NVM_HOME, process.version, 'node_modules')); 241 | } else if (win32 && process.env.APPDATA) { 242 | paths.push(join(process.env.APPDATA, 'npm/node_modules')); 243 | } else { 244 | paths.push('/usr/lib/node_modules', '/usr/local/lib/node_modules'); 245 | } 246 | 247 | // Add NVM prefix directory 248 | if (process.env.NVM_PATH) { 249 | paths.push(join(dirname(process.env.NVM_PATH), 'node_modules')); 250 | } 251 | 252 | // Adding global npm directories 253 | // We tried using npm to get the global modules path, but it haven't work out 254 | // because of bugs in the parseable implementation of `ls` command and mostly 255 | // performance issues. So, we go with our best bet for now. 256 | if (process.env.NODE_PATH) { 257 | paths = compact(process.env.NODE_PATH.split(delimiter)).concat(paths); 258 | } 259 | 260 | // Global node_modules should be 4 or 2 directory up this one (most of the time) 261 | // Ex: /usr/another_global/node_modules/yeoman-denerator/node_modules/yeoman-environment/lib (1 level dependency) 262 | paths.push(...filterValidNpmPath(join(PROJECT_ROOT, '../../..'), !filterPaths)); 263 | // Ex: /usr/another_global/node_modules/yeoman-environment/lib (installed directly) 264 | paths.push(join(PROJECT_ROOT, '..')); 265 | 266 | // Get yarn global directory and infer the module paths from there 267 | const yarnBase = execaOutput('yarn', ['global', 'dir']); 268 | if (yarnBase) { 269 | paths.push(resolve(yarnBase, 'node_modules')); 270 | paths.push(resolve(yarnBase, '../link/')); 271 | } 272 | 273 | // Get npm global prefix and infer the module paths from there 274 | const globalInstall = execaOutput('npm', ['root', '-g']); 275 | if (globalInstall) { 276 | paths.push(resolve(globalInstall)); 277 | } 278 | 279 | // Adds support for generator resolving when yeoman-generator has been linked 280 | if (process.argv[1]) { 281 | paths.push(...filterValidNpmPath(join(dirname(process.argv[1]), '../..'), !filterPaths)); 282 | } 283 | 284 | return uniq(paths.filter(Boolean).reverse()); 285 | } 286 | -------------------------------------------------------------------------------- /src/package-manager.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'node:path'; 2 | import createdLogger from 'debug'; 3 | import { whichPackageManager } from 'which-package-manager'; 4 | import { execa } from 'execa'; 5 | import type { MemFsEditorFile } from 'mem-fs-editor'; 6 | import { type InputOutputAdapter } from '@yeoman/types'; 7 | import { type Store } from 'mem-fs'; 8 | 9 | const debug = createdLogger('yeoman:environment:package-manager'); 10 | 11 | export type PackageManagerInstallTaskOptions = { 12 | memFs: Store; 13 | packageJsonLocation: string; 14 | adapter: InputOutputAdapter; 15 | nodePackageManager?: string; 16 | customInstallTask?: boolean | ((nodePackageManager: string | undefined, defaultTask: () => Promise) => void | Promise); 17 | skipInstall?: boolean; 18 | }; 19 | 20 | /** 21 | * Executes package manager install. 22 | * - checks if package.json was committed. 23 | * - uses a preferred package manager or try to detect. 24 | * @return {Promise} Promise true if the install execution suceeded. 25 | */ 26 | /* 27 | const { customInstallTask } = this.composedStore; 28 | packageJsonFile: join(this.cwd, 'package.json'); 29 | 30 | */ 31 | export async function packageManagerInstallTask({ 32 | memFs, 33 | packageJsonLocation, 34 | customInstallTask, 35 | adapter, 36 | nodePackageManager, 37 | skipInstall, 38 | }: PackageManagerInstallTaskOptions) { 39 | debug('Running packageManagerInstallTask'); 40 | packageJsonLocation = resolve(packageJsonLocation); 41 | /** 42 | * @private 43 | * Get the destination package.json file. 44 | * @return {Vinyl | undefined} a Vinyl file. 45 | */ 46 | function getDestinationPackageJson() { 47 | return memFs.get(join(packageJsonLocation, 'package.json')); 48 | } 49 | 50 | /** 51 | * @private 52 | * Get the destination package.json commit status. 53 | * @return {boolean} package.json commit status. 54 | */ 55 | function isDestinationPackageJsonCommitted() { 56 | const file = getDestinationPackageJson(); 57 | return file.committed; 58 | } 59 | 60 | if (!getDestinationPackageJson()) { 61 | return false; 62 | } 63 | 64 | if (customInstallTask && typeof customInstallTask !== 'function') { 65 | debug('Install disabled by customInstallTask'); 66 | return false; 67 | } 68 | 69 | if (!isDestinationPackageJsonCommitted()) { 70 | adapter.log(` 71 | No change to package.json was detected. No package manager install will be executed.`); 72 | return false; 73 | } 74 | 75 | adapter.log(` 76 | Changes to package.json were detected.`); 77 | 78 | if (skipInstall) { 79 | adapter.log(`Skipping package manager install. 80 | `); 81 | return false; 82 | } 83 | 84 | let packageManagerName = nodePackageManager ?? (await whichPackageManager({ cwd: packageJsonLocation })); 85 | 86 | const execPackageManager = async () => { 87 | if (!packageManagerName) { 88 | packageManagerName = 'npm'; 89 | adapter.log('Error detecting the package manager. Falling back to npm.'); 90 | } 91 | 92 | if (!['npm', 'yarn', 'pnpm', 'bun'].includes(packageManagerName)) { 93 | adapter.log(`${packageManagerName} is not a supported package manager. Run it by yourself.`); 94 | return false; 95 | } 96 | 97 | adapter.log(` 98 | Running ${packageManagerName} install for you to install the required dependencies.`); 99 | await execa(packageManagerName, ['install'], { stdio: 'inherit', cwd: packageJsonLocation }); 100 | return true; 101 | }; 102 | 103 | if (customInstallTask) { 104 | return customInstallTask(packageManagerName, execPackageManager); 105 | } 106 | 107 | return execPackageManager(); 108 | } 109 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'node:url'; 2 | import { extname, join } from 'node:path'; 3 | import { createRequire } from 'node:module'; 4 | import { toNamespace } from '@yeoman/namespace'; 5 | import type { BaseEnvironment, BaseGeneratorMeta, GeneratorMeta, GetGeneratorConstructor } from '@yeoman/types'; 6 | import createDebug from 'debug'; 7 | 8 | const debug = createDebug('yeoman:environment:store'); 9 | const require = createRequire(import.meta.url); 10 | 11 | /** 12 | * The Generator store 13 | * This is used to store generator (npm packages) reference and instantiate them when 14 | * requested. 15 | * @constructor 16 | * @private 17 | */ 18 | export default class Store { 19 | private readonly _meta: Record = {}; 20 | // Store packages paths by ns 21 | private readonly _packagesPaths: Record = {}; 22 | // Store packages ns 23 | private readonly _packagesNS: string[] = []; 24 | 25 | constructor(private readonly environment: BaseEnvironment) {} 26 | 27 | /** 28 | * Store a module under the namespace key 29 | * @param meta 30 | * @param generator - A generator module or a module path 31 | */ 32 | add(meta: M, Generator?: unknown): GeneratorMeta & M { 33 | if (typeof meta.resolved === 'string') { 34 | if (extname(meta.resolved)) { 35 | meta.resolved = join(meta.resolved); 36 | } else { 37 | try { 38 | // Resolve if meta.resolved is a package path. 39 | meta.resolved = require.resolve(meta.resolved); 40 | } catch { 41 | // Import must be a file, append index.js to directories 42 | meta.resolved = join(meta.resolved, 'index.js'); 43 | } 44 | } 45 | } 46 | 47 | if (meta.packagePath) { 48 | meta.packagePath = join(meta.packagePath); 49 | } 50 | 51 | let importModule: (() => Promise) | undefined; 52 | if (!Generator) { 53 | if (!meta.resolved) { 54 | throw new Error(`Generator Stub or resolved path is required for ${meta.namespace}`); 55 | } 56 | 57 | importModule = async () => import(pathToFileURL(meta.resolved!).href); 58 | } 59 | 60 | let importPromise: any; 61 | const importGenerator = async () => { 62 | if (importPromise) { 63 | Generator = await importPromise; 64 | } 65 | 66 | if (importModule && !Generator) { 67 | importPromise = importModule(); 68 | Generator = await importPromise; 69 | } 70 | 71 | const factory = this.getFactory(Generator); 72 | if (typeof factory === 'function') { 73 | importPromise = factory(this.environment); 74 | Generator = await importPromise; 75 | } 76 | 77 | return this._getGenerator(Generator, meta); 78 | }; 79 | 80 | const instantiate = async (arguments_: string[] = [], options: any = {}) => 81 | this.environment.instantiate(await importGenerator(), { generatorArgs: arguments_, generatorOptions: options }); 82 | const instantiateHelp = async () => instantiate([], { help: true }); 83 | const { packageNamespace } = toNamespace(meta.namespace) ?? {}; 84 | 85 | const generatorMeta = { 86 | ...meta, 87 | importGenerator, 88 | importModule, 89 | instantiate, 90 | instantiateHelp, 91 | packageNamespace, 92 | }; 93 | this._meta[meta.namespace] = generatorMeta; 94 | 95 | if (packageNamespace) { 96 | this.addPackageNamespace(packageNamespace); 97 | if (meta.packagePath) { 98 | this.addPackage(packageNamespace, meta.packagePath); 99 | } 100 | } 101 | 102 | return generatorMeta; 103 | } 104 | 105 | /** 106 | * Get the module registered under the given namespace 107 | * @param {String} namespace 108 | * @return {Module} 109 | */ 110 | async get(namespace: string): Promise { 111 | return this.getMeta(namespace)?.importGenerator(); 112 | } 113 | 114 | /** 115 | * Get the module registered under the given namespace 116 | * @param {String} namespace 117 | * @return {Module} 118 | */ 119 | getMeta(namespace: string): GeneratorMeta | undefined { 120 | return this._meta[namespace]; 121 | } 122 | 123 | /** 124 | * Returns the list of registered namespace. 125 | * @return {Array} Namespaces array 126 | */ 127 | namespaces() { 128 | return Object.keys(this._meta); 129 | } 130 | 131 | /** 132 | * Get the stored generators meta data 133 | * @return {Object} Generators metadata 134 | */ 135 | getGeneratorsMeta() { 136 | return this._meta; 137 | } 138 | 139 | /** 140 | * Store a package under the namespace key 141 | * @param {String} packageNS - The key under which the generator can be retrieved 142 | * @param {String} packagePath - The package path 143 | */ 144 | addPackage(packageNS: string, packagePath: string) { 145 | if (this._packagesPaths[packageNS]) { 146 | // Yo environment allows overriding, so the last added has preference. 147 | if (this._packagesPaths[packageNS][0] !== packagePath) { 148 | const packagePaths = this._packagesPaths[packageNS]; 149 | debug( 150 | 'Overriding a package with namespace %s and path %s, with path %s', 151 | packageNS, 152 | this._packagesPaths[packageNS][0], 153 | packagePath, 154 | ); 155 | // Remove old packagePath 156 | const index = packagePaths.indexOf(packagePath); 157 | if (index > -1) { 158 | packagePaths.splice(index, 1); 159 | } 160 | 161 | packagePaths.splice(0, 0, packagePath); 162 | } 163 | } else { 164 | this._packagesPaths[packageNS] = [packagePath]; 165 | } 166 | } 167 | 168 | /** 169 | * Get the stored packages namespaces with paths. 170 | * @return {Object} Stored packages namespaces with paths. 171 | */ 172 | getPackagesPaths() { 173 | return this._packagesPaths; 174 | } 175 | 176 | /** 177 | * Store a package ns 178 | * @param {String} packageNS - The key under which the generator can be retrieved 179 | */ 180 | addPackageNamespace(packageNS: string) { 181 | if (!this._packagesNS.includes(packageNS)) { 182 | this._packagesNS.push(packageNS); 183 | } 184 | } 185 | 186 | /** 187 | * Get the stored packages namespaces. 188 | * @return {Array} Stored packages namespaces. 189 | */ 190 | 191 | getPackagesNS(): string[] { 192 | return this._packagesNS; 193 | } 194 | 195 | private getFactory(module: any) { 196 | // CJS is imported in default, for backward compatibility we support a Generator exported as `module.exports = { default }` 197 | return module.createGenerator ?? module.default?.createGenerator ?? module.default?.default?.createGenerator; 198 | } 199 | 200 | private _getGenerator(module: any, meta: BaseGeneratorMeta) { 201 | const Generator = module.default?.default ?? module.default ?? module; 202 | if (typeof Generator !== 'function') { 203 | throw new TypeError("The generator doesn't provide a constructor."); 204 | } 205 | 206 | Object.assign(Generator, meta); 207 | return Generator; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/util/command.ts: -------------------------------------------------------------------------------- 1 | import { Command, Option } from 'commander'; 2 | import type BaseEnvironment from '../environment-base.js'; 3 | 4 | export default class YeomanCommand extends Command { 5 | env?: BaseEnvironment; 6 | 7 | override createCommand(name?: string) { 8 | return new YeomanCommand(name); 9 | } 10 | 11 | /** 12 | * Override addOption to register a negative alternative for every option. 13 | * @param {Option} option 14 | * @return {YeomanCommand} this; 15 | */ 16 | addOption(option: Option) { 17 | if (!option.long || option.required || option.optional) { 18 | return super.addOption(option); 19 | } 20 | 21 | if (option.negate) { 22 | // Add a affirmative option for negative boolean options. 23 | // Should be done before, because commander adds a non working affirmative by itself. 24 | super.addOption(new Option(option.long.replace(/^--no-/, '--')).hideHelp()); 25 | } 26 | 27 | const result = super.addOption(option); 28 | if (!option.negate) { 29 | // Add a hidden negative option for affirmative boolean options. 30 | super.addOption(new Option(option.long.replace(/^--/, '--no-')).hideHelp()); 31 | } 32 | 33 | return result; 34 | } 35 | 36 | /** 37 | * Load Generator options into a commander instance. 38 | * 39 | * @param {Generator} generator - Generator 40 | * @return {Command} return command 41 | */ 42 | registerGenerator(generator: any) { 43 | return this.addGeneratorOptions(generator._options).addGeneratorArguments(generator._arguments); 44 | } 45 | 46 | /** 47 | * Register arguments using generator._arguments structure. 48 | * @param {object[]} generatorArgs 49 | * @return {YeomanCommand} this; 50 | */ 51 | addGeneratorArguments(generatorArguments: any[] = []) { 52 | if (!generatorArguments || generatorArguments.length === 0) { 53 | return this; 54 | } 55 | 56 | const arguments_ = generatorArguments 57 | .map(argument => { 58 | const argumentName = argument.type === Array ? `${argument.name}...` : argument.name; 59 | return argument.required ? `<${argumentName}>` : `[${argumentName}]`; 60 | }) 61 | .join(' '); 62 | this.arguments(arguments_); 63 | return this; 64 | } 65 | 66 | /** 67 | * Register options using generator._options structure. 68 | * @param {object} options 69 | * @param {string} blueprintOptionDescription - description of the blueprint that adds the option 70 | * @return {YeomanCommand} this; 71 | */ 72 | addGeneratorOptions(options: Record) { 73 | options = options || {}; 74 | for (const [key, value] of Object.entries(options)) { 75 | this._addGeneratorOption(key, value); 76 | } 77 | 78 | return this; 79 | } 80 | 81 | _addGeneratorOption(optionName: string, optionDefinition: any, additionalDescription = '') { 82 | if (optionName === 'help') { 83 | return; 84 | } 85 | 86 | const longOption = `--${optionName}`; 87 | const existingOption = (this as any)._findOption(longOption); 88 | if ((this as any)._findOption(longOption)) { 89 | return existingOption; 90 | } 91 | 92 | let cmdString = ''; 93 | if (optionDefinition.alias) { 94 | cmdString = `-${optionDefinition.alias}, `; 95 | } 96 | 97 | cmdString = `${cmdString}${longOption}`; 98 | if (optionDefinition.type === String) { 99 | cmdString = optionDefinition.required === false ? `${cmdString} [value]` : `${cmdString} `; 100 | } else if (optionDefinition.type === Array) { 101 | cmdString = optionDefinition.required === false ? `${cmdString} [value...]` : `${cmdString} `; 102 | } 103 | 104 | return this.addOption( 105 | new Option(cmdString, `${optionDefinition.description}${additionalDescription}`) 106 | .default(optionDefinition.default) 107 | .hideHelp(optionDefinition.hide), 108 | ); 109 | } 110 | } 111 | 112 | /* Add Environment options */ 113 | export const addEnvironmentOptions = (command = new YeomanCommand()) => 114 | command 115 | .option('--cwd', 'Path to use as current dir') 116 | /* Environment options */ 117 | .option('--skip-install', 'Do not automatically install dependencies', false) 118 | /* Generator options */ 119 | .option('--skip-cache', 'Do not remember prompt answers', false) 120 | .option('--local-config-only', 'Generate .yo-rc-global.json locally', false) 121 | .option('--ask-answered', 'Show prompts for already configured options', false) 122 | /* Conflicter options */ 123 | .option('--force', 'Override every file', false) 124 | .option('--dry-run', 'Print conflicts', false) 125 | .option('--whitespace', 'Whitespace changes will not trigger conflicts', false) 126 | .option('--bail', 'Fail on first conflict', false) 127 | .option('--skip-yo-resolve', 'Ignore .yo-resolve files', false) 128 | /* Hidden options, used for api */ 129 | .addOption(new Option('--skip-local-cache', 'Skip local answers cache').default(true).hideHelp()) 130 | .addOption(new Option('--skip-parse-options', 'Skip legacy options parsing').default(false).hideHelp()) 131 | .addOption(new Option('--experimental', 'Experimental features').default(false).hideHelp()) 132 | .addOption(new Option('--log-cwd', 'Path for log purpose').hideHelp()); 133 | -------------------------------------------------------------------------------- /src/util/namespace.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'node:path'; 2 | import slash from 'slash'; 3 | import { escapeRegExp, findLast } from 'lodash-es'; 4 | 5 | type AsNamespaceOptions = { 6 | lookups?: string[]; 7 | }; 8 | 9 | export const defaultLookups = ['.', 'generators', 'lib/generators', 'dist/generators']; 10 | 11 | /** 12 | * Given a String `filepath`, tries to figure out the relative namespace. 13 | * 14 | * ### Examples: 15 | * 16 | * this.namespace('backbone/all/index.js'); 17 | * // => backbone:all 18 | * 19 | * this.namespace('generator-backbone/model'); 20 | * // => backbone:model 21 | * 22 | * this.namespace('backbone.js'); 23 | * // => backbone 24 | * 25 | * this.namespace('generator-mocha/backbone/model/index.js'); 26 | * // => mocha:backbone:model 27 | * 28 | * @param filepath 29 | * @param lookups paths 30 | */ 31 | export const asNamespace = (filepath: string, { lookups = defaultLookups }: AsNamespaceOptions): string => { 32 | if (!filepath) { 33 | throw new Error('Missing file path'); 34 | } 35 | 36 | // Normalize path 37 | let ns = slash(filepath); 38 | 39 | // Ignore path before latest node_modules 40 | const nodeModulesPath = '/node_modules/'; 41 | if (ns.includes(nodeModulesPath)) { 42 | ns = ns.slice(ns.lastIndexOf(nodeModulesPath) + nodeModulesPath.length); 43 | } 44 | 45 | // Cleanup extension and normalize path for differents OS 46 | const parsed = parse(ns); 47 | ns = parsed.dir ? `${parsed.dir}/${parsed.name}` : parsed.name; 48 | 49 | // Sort lookups by length so biggest are removed first 50 | const nsLookups = [...lookups, '..'] 51 | .map(found => slash(found)) 52 | .sort((a, b) => a.split('/').length - b.split('/').length) 53 | .reverse(); 54 | 55 | // If `ns` contains a lookup dir in its path, remove it. 56 | for (const lookup of nsLookups) { 57 | // Only match full directory (begin with leading slash or start of input, end with trailing slash) 58 | ns = ns.replaceAll(new RegExp(`(?:/|^)${escapeRegExp(lookup)}(?=/)`, 'g'), ''); 59 | } 60 | 61 | const folders = ns.split('/'); 62 | const scope = findLast(folders, folder => folder.startsWith('@')); 63 | 64 | // Cleanup `ns` from unwanted parts and then normalize slashes to `:` 65 | ns = ns 66 | .replaceAll('//', '') // Remove double `/` 67 | .replace(/(.*generator-)/, '') // Remove before `generator-` 68 | .replace(/\/(index|main)$/, '') // Remove `/index` or `/main` 69 | .replace(/^\//, '') // Remove leading `/` 70 | .replaceAll(/\/+/g, ':'); // Replace slashes by `:` 71 | 72 | if (scope) { 73 | ns = `${scope}/${ns}`; 74 | } 75 | 76 | return ns; 77 | }; 78 | -------------------------------------------------------------------------------- /src/util/resolve.ts: -------------------------------------------------------------------------------- 1 | import { dirname, extname, join, normalize, resolve, sep } from 'node:path'; 2 | import { realpath, stat } from 'node:fs/promises'; 3 | import untildify from 'untildify'; 4 | import { locatePath } from 'locate-path'; 5 | import { defaultExtensions } from '../generator-lookup.js'; 6 | 7 | /** 8 | * Resolve a module path 9 | * @param specifier - Filepath or module name 10 | * @return - The resolved path leading to the module 11 | */ 12 | export async function resolveModulePath(specifier: string, resolvedOrigin?: string) { 13 | let maybeResolved = specifier; 14 | if (maybeResolved.startsWith('.')) { 15 | if (resolvedOrigin) { 16 | maybeResolved = resolve(dirname(resolvedOrigin), '..', maybeResolved); 17 | } else { 18 | throw new Error(`Specifier ${maybeResolved} could not be calculated`); 19 | } 20 | } 21 | 22 | maybeResolved = untildify(maybeResolved); 23 | maybeResolved = normalize(maybeResolved); 24 | 25 | if (extname(maybeResolved) === '') { 26 | maybeResolved += sep; 27 | } 28 | 29 | try { 30 | let specStat = await stat(maybeResolved); 31 | if (specStat.isSymbolicLink()) { 32 | specStat = await stat(await realpath(maybeResolved)); 33 | } 34 | 35 | if (specStat.isFile()) { 36 | return maybeResolved; 37 | } 38 | 39 | if (specStat.isDirectory()) { 40 | return await locatePath(defaultExtensions.map(extension => `index${extension}`).map(file => join(maybeResolved, file))); 41 | } 42 | } catch { 43 | // ignore error 44 | } 45 | 46 | throw new Error(`Error resolving ${specifier}`); 47 | } 48 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | import { execaSync } from 'execa'; 2 | 3 | export const execaOutput = (cmg: string, arguments_: string[]): string | undefined => { 4 | try { 5 | const { failed, stdout } = execaSync(cmg, arguments_, { encoding: 'utf8' }); 6 | if (!failed) { 7 | return stdout; 8 | } 9 | } catch { 10 | // ignore error 11 | } 12 | 13 | return; 14 | }; 15 | 16 | /** 17 | * Two-step argument splitting function that first splits arguments in quotes, 18 | * and then splits up the remaining arguments if they are not part of a quote. 19 | */ 20 | export function splitArgsFromString(argumentsString: string | string[]): string[] { 21 | if (Array.isArray(argumentsString)) { 22 | return argumentsString; 23 | } 24 | 25 | let result: string[] = []; 26 | if (!argumentsString) { 27 | return result; 28 | } 29 | 30 | const quoteSeparatedArguments = argumentsString.split(/("[^"]*")/).filter(Boolean); 31 | for (const argument of quoteSeparatedArguments) { 32 | if (argument.includes('"')) { 33 | result.push(argument.replaceAll('"', '')); 34 | } else { 35 | result = result.concat(argument.trim().split(' ')); 36 | } 37 | } 38 | 39 | return result; 40 | } 41 | -------------------------------------------------------------------------------- /test/command.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import path, { dirname } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { createRequire } from 'node:module'; 5 | import { stub } from 'sinon'; 6 | import { beforeEach, describe, it } from 'esmocha'; 7 | import { prepareCommand } from '../src/commands.js'; 8 | import Environment from '../src/index.js'; 9 | 10 | const require = createRequire(import.meta.url); 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = dirname(__filename); 13 | 14 | describe('environment (command)', () => { 15 | describe('#execute() with options', () => { 16 | let environment; 17 | 18 | beforeEach(async () => { 19 | environment = new Environment([], { skipInstall: true, dryRun: true }); 20 | environment.adapter.log = stub(); 21 | await environment.register(path.join(__dirname, 'fixtures/generator-commands/generators/options')); 22 | }); 23 | 24 | describe('generator with options', () => { 25 | describe('without options', () => { 26 | let generator; 27 | beforeEach(async () => { 28 | await environment.execute('commands:options'); 29 | const generators = Object.values(environment.composedStore.getGenerators()); 30 | assert(generators.length === 1); 31 | generator = generators[0]; 32 | }); 33 | 34 | it('should parse options correctly', () => { 35 | assert.strictEqual(generator.options.bool, undefined); 36 | assert.strictEqual(generator.options.boolDefault, true); 37 | 38 | assert.strictEqual(generator.options.string, undefined); 39 | assert.strictEqual(generator.options.stringDefault, 'defaultValue'); 40 | }); 41 | }); 42 | 43 | describe('with options', () => { 44 | let generator; 45 | beforeEach(async () => { 46 | await environment.execute('commands:options', [ 47 | '--bool', 48 | '--no-bool-default', 49 | '--string', 50 | 'customValue', 51 | '--string-default', 52 | 'newValue', 53 | ]); 54 | 55 | const generators = Object.values(environment.composedStore.getGenerators()); 56 | assert(generators.length === 1); 57 | generator = generators[0]; 58 | }); 59 | 60 | it('should parse options correctly', () => { 61 | assert.strictEqual(generator.options.bool, true); 62 | assert.strictEqual(generator.options.boolDefault, false); 63 | 64 | assert.strictEqual(generator.options.string, 'customValue'); 65 | assert.strictEqual(generator.options.stringDefault, 'newValue'); 66 | }); 67 | }); 68 | 69 | describe('using aliases', () => { 70 | let generator; 71 | beforeEach(async () => { 72 | await environment.execute('commands:options', ['-b', '-s', 'customValue']); 73 | const generators = Object.values(environment.composedStore.getGenerators()); 74 | assert(generators.length === 1); 75 | generator = generators[0]; 76 | }); 77 | 78 | it('should parse options correctly', () => { 79 | assert.strictEqual(generator.options.bool, true); 80 | assert.strictEqual(generator.options.string, 'customValue'); 81 | }); 82 | }); 83 | }); 84 | }); 85 | 86 | describe('#execute() with arguments', () => { 87 | let environment; 88 | 89 | beforeEach(() => { 90 | environment = new Environment([], { skipInstall: true, dryRun: true }); 91 | environment.adapter.log = stub(); 92 | environment.register(path.join(__dirname, 'fixtures/generator-commands/generators/arguments')); 93 | }); 94 | 95 | describe('generator with arguments', () => { 96 | describe('without arguments', () => { 97 | let generator; 98 | beforeEach(async () => { 99 | await environment.execute('commands:arguments'); 100 | const generators = Object.values(environment.composedStore.getGenerators()); 101 | assert(generators.length === 1); 102 | generator = generators[0]; 103 | }); 104 | 105 | it('should parse arguments correctly', () => { 106 | assert.deepStrictEqual(generator._args, []); 107 | }); 108 | }); 109 | 110 | describe('with arguments', () => { 111 | let generator; 112 | beforeEach(async () => { 113 | await environment.execute('commands:arguments', ['foo']); 114 | const generators = Object.values(environment.composedStore.getGenerators()); 115 | assert(generators.length === 1); 116 | generator = generators[0]; 117 | }); 118 | 119 | it('should parse arguments correctly', () => { 120 | assert.deepStrictEqual(generator._args, ['foo']); 121 | }); 122 | 123 | it('should load arguments into options', () => { 124 | assert.strictEqual(generator.options.name, 'foo'); 125 | }); 126 | }); 127 | }); 128 | }); 129 | 130 | describe('#prepareCommand()', () => { 131 | describe('generator with arguments', () => { 132 | describe('passing bar argument', () => { 133 | let generator; 134 | let environment; 135 | 136 | beforeEach(async () => { 137 | const command = await prepareCommand({ 138 | resolved: require.resolve('./fixtures/generator-commands/generators/arguments/index.js'), 139 | }); 140 | await command.parseAsync(['node', 'yo', 'bar']); 141 | 142 | environment = command.env; 143 | const generators = Object.values(environment.composedStore.getGenerators()); 144 | assert(generators.length === 1); 145 | generator = generators[0]; 146 | }); 147 | 148 | it('should parse arguments correctly', () => { 149 | assert.deepStrictEqual(generator._args, ['bar']); 150 | }); 151 | }); 152 | }); 153 | describe('generator with options', () => { 154 | describe('passing options', () => { 155 | let generator; 156 | let environment; 157 | 158 | beforeEach(async () => { 159 | const command = await prepareCommand({ resolved: require.resolve('./fixtures/generator-commands/generators/options/index.js') }); 160 | await command.parseAsync([ 161 | 'node', 162 | 'yo', 163 | '--bool', 164 | '--no-bool-default', 165 | '--string', 166 | 'customValue', 167 | '--string-default', 168 | 'newValue', 169 | ]); 170 | 171 | environment = command.env; 172 | const generators = Object.values(environment.composedStore.getGenerators()); 173 | assert(generators.length === 1); 174 | generator = generators[0]; 175 | }); 176 | 177 | it('should parse options correctly', () => { 178 | assert.strictEqual(generator.options.bool, true); 179 | assert.strictEqual(generator.options.boolDefault, false); 180 | 181 | assert.strictEqual(generator.options.string, 'customValue'); 182 | assert.strictEqual(generator.options.stringDefault, 'newValue'); 183 | }); 184 | }); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /test/fixtures/binary-diff/file-contains-utf8.yml: -------------------------------------------------------------------------------- 1 | city: 深圳 2 | -------------------------------------------------------------------------------- /test/fixtures/conflicter/file-conflict.txt: -------------------------------------------------------------------------------- 1 | initial content 2 | -------------------------------------------------------------------------------- /test/fixtures/conflicter/foo-template.js: -------------------------------------------------------------------------------- 1 | var <%= foo %> = '<%= foo %>'; 2 | <%%= extra %> 3 | -------------------------------------------------------------------------------- /test/fixtures/conflicter/foo.js: -------------------------------------------------------------------------------- 1 | var foo = 'foo'; 2 | -------------------------------------------------------------------------------- /test/fixtures/conflicter/testFile.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeoman/environment/ebf34ed51479dd0d413e12b9253d5f05e9ff56eb/test/fixtures/conflicter/testFile.tar.gz -------------------------------------------------------------------------------- /test/fixtures/conflicter/yeoman-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeoman/environment/ebf34ed51479dd0d413e12b9253d5f05e9ff56eb/test/fixtures/conflicter/yeoman-logo.png -------------------------------------------------------------------------------- /test/fixtures/generator-commands/generators/arguments/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | module.exports = class extends Generator { 3 | constructor(args, options = {}) { 4 | super(args, options); 5 | 6 | this.argument('name', { 7 | type: String, 8 | required: false, 9 | }); 10 | } 11 | 12 | empty() {} 13 | }; -------------------------------------------------------------------------------- /test/fixtures/generator-commands/generators/options/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | module.exports = class extends Generator { 3 | constructor(args, options = {}) { 4 | super(args, options); 5 | 6 | options = options || {}; 7 | 8 | this.option('bool', { 9 | type: Boolean, 10 | alias: 'b', 11 | }); 12 | 13 | this.option('bool-default', { 14 | type: Boolean, 15 | defaults: true, 16 | }); 17 | 18 | this.option('string', { 19 | type: String, 20 | alias: 's', 21 | }); 22 | 23 | this.option('string-default', { 24 | type: String, 25 | defaults: 'defaultValue', 26 | }); 27 | } 28 | 29 | empty() {} 30 | }; -------------------------------------------------------------------------------- /test/fixtures/generator-commands/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-commands", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-common-js/generators/cjs/index.cjs: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | 3 | class NewGenerator extends Generator { 4 | _postConstruct() {} 5 | default() {} 6 | }; 7 | 8 | module.exports = NewGenerator; 9 | -------------------------------------------------------------------------------- /test/fixtures/generator-common-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-common-js", 4 | "version": "0.0.0", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/generator-environment-extend/app/index.js: -------------------------------------------------------------------------------- 1 | import { lookupGenerator } from '../../../../src/generator-lookup.js'; 2 | const maybeGenerator = await import(lookupGenerator('dummy:app')); 3 | const Generator = maybeGenerator.default ?? maybeGenerator; 4 | export default class extends Generator {}; 5 | -------------------------------------------------------------------------------- /test/fixtures/generator-environment-extend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-environment-extend", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "type": "module", 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/generator-esm/generators/app/index.js: -------------------------------------------------------------------------------- 1 | import Generator from 'yeoman-generator-7'; 2 | 3 | class NewGenerator extends Generator { 4 | _postConstruct() {} 5 | default() {} 6 | }; 7 | 8 | export default NewGenerator; 9 | -------------------------------------------------------------------------------- /test/fixtures/generator-esm/generators/create-inherited/index.js: -------------------------------------------------------------------------------- 1 | export async function createGenerator(env) { 2 | const ParentGenerator = await env.requireGenerator('esm:create'); 3 | return class NewGenerator extends ParentGenerator { 4 | default() { 5 | super.mockedDefault(); 6 | } 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/generator-esm/generators/create/index.js: -------------------------------------------------------------------------------- 1 | export async function createGenerator(env) { 2 | const ParentGenerator = await env.requireGenerator('mocked-generator'); 3 | return class NewGenerator extends ParentGenerator { 4 | default() { 5 | super.mockedDefault(); 6 | } 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/generator-esm/generators/mjs/index.mjs: -------------------------------------------------------------------------------- 1 | import Generator from 'yeoman-generator-7'; 2 | 3 | class NewGenerator extends Generator { 4 | default() {} 5 | }; 6 | 7 | export default NewGenerator; 8 | -------------------------------------------------------------------------------- /test/fixtures/generator-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "type":"module", 4 | "name": "generator-esm", 5 | "version": "0.0.0", 6 | "exports": "./index.js", 7 | "dependencies": {}, 8 | "devDependencies": {}, 9 | "optionalDependencies": {}, 10 | "engines": { 11 | "node": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/generator-extend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-extend", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-extend/support/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Generator = require('yeoman-generator-5'); 3 | module.exports = class extends Generator {}; 4 | -------------------------------------------------------------------------------- /test/fixtures/generator-mocha/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | 3 | class NewGenerator extends Generator { 4 | default() { 5 | console.log('Executing NewGenerator generator', this.arguments); 6 | } 7 | }; 8 | 9 | NewGenerator.name = 'You can name your generator'; 10 | NewGenerator.description = 'Ana add a custom description by adding a `description` property to your function.'; 11 | NewGenerator.usage = 'Usage can be used to customize the help output'; 12 | 13 | // namespace is resolved depending on the location of this generator, 14 | // unless you specifically define it. 15 | NewGenerator.namespace = 'mocha:generator'; 16 | 17 | module.exports = NewGenerator; 18 | -------------------------------------------------------------------------------- /test/fixtures/generator-mocha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-mocha", 4 | "version": "0.0.0", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/generator-module-lib-gen/lib/generators/app/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | exports.default = class extends Generator { 3 | exec() { 4 | 5 | } 6 | }; -------------------------------------------------------------------------------- /test/fixtures/generator-module-lib-gen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-module-lib-gen", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-module-root/app/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | exports.default = class extends Generator { 3 | exec() { 4 | 5 | } 6 | }; -------------------------------------------------------------------------------- /test/fixtures/generator-module-root/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-module", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-module/generators/app/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | exports.default = class extends Generator { 3 | exec() { 4 | 5 | } 6 | }; -------------------------------------------------------------------------------- /test/fixtures/generator-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-module", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-no-constructor/generators/app/index.js: -------------------------------------------------------------------------------- 1 | exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/generator-no-constructor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-module", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-scoped/app.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | module.exports = class extends Generator {}; 3 | -------------------------------------------------------------------------------- /test/fixtures/generator-scoped/app.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/generator-scoped/app/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | module.exports = class extends Generator {}; 3 | -------------------------------------------------------------------------------- /test/fixtures/generator-scoped/app/scaffold/index.js: -------------------------------------------------------------------------------- 1 | var Generator = require('yeoman-generator-5'); 2 | module.exports = class extends Generator {}; 3 | -------------------------------------------------------------------------------- /test/fixtures/generator-scoped/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-scoped", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-scoped/package/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeoman/environment/ebf34ed51479dd0d413e12b9253d5f05e9ff56eb/test/fixtures/generator-scoped/package/index.js -------------------------------------------------------------------------------- /test/fixtures/generator-scoped/package/nodefile.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeoman/environment/ebf34ed51479dd0d413e12b9253d5f05e9ff56eb/test/fixtures/generator-scoped/package/nodefile.node -------------------------------------------------------------------------------- /test/fixtures/generator-simple/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (args, options) => { 2 | console.log('Executing generator with', args, options); 3 | }; 4 | 5 | module.exports.name = 'You can name your generator'; 6 | module.exports.description = 'And add a custom description by adding a `description` property to your function.'; 7 | module.exports.usage = 'Usage can be used to customize the help output'; 8 | -------------------------------------------------------------------------------- /test/fixtures/generator-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-simple", 4 | "version": "0.0.0", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/generator-ts-js/generators/app/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (args, options) => { 2 | console.log('Executing generator with', args, options); 3 | }; 4 | 5 | module.exports.name = 'You can name your generator'; 6 | module.exports.description = 'And add a custom description by adding a `description` property to your function.'; 7 | module.exports.usage = 'Usage can be used to customize the help output'; 8 | -------------------------------------------------------------------------------- /test/fixtures/generator-ts-js/generators/app/index.ts: -------------------------------------------------------------------------------- 1 | module.exports = (args, options) => { 2 | console.log('Executing generator with', args, options); 3 | }; 4 | 5 | module.exports.name = 'You can name your generator'; 6 | module.exports.description = 'And add a custom description by adding a `description` property to your function.'; 7 | module.exports.usage = 'Usage can be used to customize the help output'; 8 | -------------------------------------------------------------------------------- /test/fixtures/generator-ts-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-ts-js", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-ts/generators/app/index.ts: -------------------------------------------------------------------------------- 1 | const Generator = require("yeoman-generator-5"); 2 | 3 | class DummyTsGenerator extends Generator { 4 | constructor(args, opts){ 5 | super(args, opts); 6 | } 7 | 8 | exec() { 9 | this.env.done = true; 10 | } 11 | } 12 | 13 | module.exports = DummyTsGenerator; 14 | -------------------------------------------------------------------------------- /test/fixtures/generator-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "generator-ts", 4 | "version": "0.0.0", 5 | "dependencies": {}, 6 | "devDependencies": {}, 7 | "optionalDependencies": {}, 8 | "engines": { 9 | "node": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/generator-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "noImplicitAny": false 3 | } -------------------------------------------------------------------------------- /test/fixtures/help.txt: -------------------------------------------------------------------------------- 1 | Usage: init GENERATOR [args] [options] 2 | 3 | General options: 4 | --help # Print generator's options and usage 5 | -f, --force # Overwrite files that already exist 6 | 7 | Please choose a generator below. 8 | 9 | 10 | Extend 11 | extend:support 12 | 13 | Simple 14 | simple 15 | -------------------------------------------------------------------------------- /test/fixtures/lookup-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookup-dummy-env" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/lookup-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookup-dummy-env", 3 | "dependencies": { 4 | "generator-commonjs": "*", 5 | "generator-dummy": "~0.1.0", 6 | "generator-jquery": "^1.2.3" 7 | }, 8 | "overrides": { 9 | "graceful-fs": "^4.2.10" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/lookup-project/subdir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookup-subdir-dummy-env", 3 | "dependencies": { 4 | "generator-dummy": "~0.1.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/package-manager/npm/package-lock.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/package-manager/pnpm/pnpm-lock.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeoman/environment/ebf34ed51479dd0d413e12b9253d5f05e9ff56eb/test/fixtures/package-manager/pnpm/pnpm-lock.yaml -------------------------------------------------------------------------------- /test/fixtures/package-manager/yarn/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeoman/environment/ebf34ed51479dd0d413e12b9253d5f05e9ff56eb/test/fixtures/package-manager/yarn/yarn.lock -------------------------------------------------------------------------------- /test/fixtures/yo-resolve/.yo-resolve: -------------------------------------------------------------------------------- 1 | root-to-skip skip 2 | */match-to-skip skip -------------------------------------------------------------------------------- /test/fixtures/yo-resolve/sub/.yo-resolve: -------------------------------------------------------------------------------- 1 | sub-to-skip skip 2 | sub2-to-force=force -------------------------------------------------------------------------------- /test/generator-features.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { stub } from 'sinon'; 3 | import { after, afterEach, before, beforeEach, describe, esmocha, expect, it } from 'esmocha'; 4 | import helpers, { getCreateEnv as getCreateEnvironment } from './helpers.js'; 5 | import { greaterThan5 } from './generator-versions.js'; 6 | 7 | const { commitSharedFsTask } = await esmocha.mock('../src/commit.js', import('../src/commit.js')); 8 | const { packageManagerInstallTask } = await esmocha.mock('../src/package-manager.js', import('../src/package-manager.js')); 9 | const { execa } = await esmocha.mock('execa', import('execa')); 10 | const { default: BasicEnvironment } = await import('../src/environment-base.js'); 11 | 12 | for (const generatorVersion of greaterThan5) { 13 | const { default: Generator } = await import(generatorVersion); 14 | class FeaturesGenerator extends Generator {} 15 | 16 | describe(`environment (generator-features) using ${generatorVersion}`, () => { 17 | afterEach(() => { 18 | esmocha.resetAllMocks(); 19 | }); 20 | after(() => { 21 | esmocha.reset(true); 22 | }); 23 | 24 | describe('customCommitTask feature', () => { 25 | describe('without customInstallTask', () => { 26 | beforeEach(async () => { 27 | await helpers 28 | .run('custom-commit', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) }) 29 | .withOptions({ skipInstall: true }) 30 | .withGenerators([[helpers.createMockedGenerator(Generator), { namespace: 'custom-commit:app' }]]); 31 | }); 32 | 33 | it('should call commitSharedFs', () => { 34 | expect(commitSharedFsTask).toHaveBeenCalledTimes(1); 35 | }); 36 | }); 37 | 38 | describe('with true customCommitTask', () => { 39 | let runContext; 40 | before(async () => { 41 | runContext = helpers 42 | .create('custom-commit', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) }) 43 | .withOptions({ skipInstall: true }) 44 | .withGenerators([ 45 | [ 46 | helpers.createMockedGenerator( 47 | class extends FeaturesGenerator { 48 | constructor(arguments_, options) { 49 | super(arguments_, options, { customCommitTask: true }); 50 | } 51 | }, 52 | ), 53 | { namespace: 'custom-commit:app' }, 54 | ], 55 | ]); 56 | await runContext.run(); 57 | }); 58 | 59 | it('should not call commitSharedFs', () => { 60 | expect(commitSharedFsTask).not.toHaveBeenCalled(); 61 | }); 62 | }); 63 | 64 | describe('with function customCommitTask', () => { 65 | let runContext; 66 | let customCommitTask; 67 | before(async () => { 68 | customCommitTask = stub(); 69 | runContext = helpers 70 | .create('custom-commit') 71 | .withOptions({ skipInstall: true }) 72 | .withGenerators([ 73 | [ 74 | helpers.createMockedGenerator( 75 | class extends FeaturesGenerator { 76 | constructor(arguments_, options) { 77 | super(arguments_, options, { customCommitTask }); 78 | } 79 | }, 80 | ), 81 | { namespace: 'custom-commit:app' }, 82 | ], 83 | ]) 84 | .withEnvironment(environment => { 85 | environment.commitSharedFs = stub().returns(Promise.resolve()); 86 | }); 87 | await runContext.run(); 88 | }); 89 | 90 | it('should not call commitSharedFs', () => { 91 | assert.equal(runContext.env.commitSharedFs.callCount, 0, 'should not have been called'); 92 | }); 93 | 94 | it('should call customCommitTask', () => { 95 | assert.equal(customCommitTask.callCount, 1, 'should have been called'); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('customInstallTask feature', () => { 101 | describe('without customInstallTask', () => { 102 | let runContext; 103 | beforeEach(async () => { 104 | runContext = helpers 105 | .create('custom-install', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) }) 106 | .withOptions({ skipInstall: false }) 107 | .withGenerators([ 108 | [ 109 | class extends FeaturesGenerator { 110 | packageJsonTask() { 111 | this.packageJson.set({ name: 'foo' }); 112 | } 113 | }, 114 | { namespace: 'custom-install:app' }, 115 | ], 116 | ]); 117 | await runContext.run(); 118 | }); 119 | 120 | it('should call packageManagerInstallTask', () => { 121 | expect(packageManagerInstallTask).toHaveBeenCalledTimes(1); 122 | expect(packageManagerInstallTask).toHaveBeenCalledWith( 123 | expect.not.objectContaining({ 124 | customInstallTask: expect.any(Function), 125 | }), 126 | ); 127 | }); 128 | }); 129 | 130 | describe('v4 compatibility', () => { 131 | let runContext; 132 | beforeEach(async () => { 133 | runContext = helpers 134 | .create('custom-install', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) }) 135 | .withOptions({ skipInstall: false }) 136 | .withGenerators([ 137 | [ 138 | class extends FeaturesGenerator { 139 | packageJsonTask() { 140 | this.env.compatibilityMode = 'v4'; 141 | this.packageJson.set({ name: 'foo' }); 142 | } 143 | }, 144 | { namespace: 'custom-install:app' }, 145 | ], 146 | ]); 147 | await runContext.run(); 148 | }); 149 | 150 | it('should not call packageManagerInstallTask', () => { 151 | expect(packageManagerInstallTask).not.toHaveBeenCalled(); 152 | }); 153 | }); 154 | 155 | describe('with true customInstallTask', () => { 156 | let runContext; 157 | before(async () => { 158 | runContext = helpers 159 | .create('custom-install', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) }) 160 | .withOptions({ skipInstall: false }) 161 | .withGenerators([ 162 | [ 163 | class extends FeaturesGenerator { 164 | constructor(arguments_, options) { 165 | super(arguments_, options, { customInstallTask: true }); 166 | } 167 | 168 | packageJsonTask() { 169 | this.packageJson.set({ name: 'foo' }); 170 | } 171 | }, 172 | { namespace: 'custom-install:app' }, 173 | ], 174 | ]); 175 | await runContext.run(); 176 | }); 177 | 178 | it('should not call execa', () => { 179 | expect(execa).not.toHaveBeenCalled(); 180 | }); 181 | }); 182 | 183 | describe('with function customInstallTask', () => { 184 | let customInstallTask; 185 | before(async () => { 186 | customInstallTask = stub(); 187 | await helpers 188 | .run('custom-install') 189 | .withOptions({ skipInstall: false }) 190 | .withGenerators([ 191 | [ 192 | class extends FeaturesGenerator { 193 | constructor(arguments_, options) { 194 | super(arguments_, options, { customInstallTask }); 195 | } 196 | 197 | packageJsonTask() { 198 | this.packageJson.set({ name: 'foo' }); 199 | } 200 | }, 201 | { namespace: 'custom-install:app' }, 202 | ], 203 | ]); 204 | }); 205 | 206 | it('should call customInstallTask', () => { 207 | assert.equal(customInstallTask.callCount, 1, 'should have been called'); 208 | }); 209 | 210 | it('should forward preferred pm', () => { 211 | assert.equal(customInstallTask.getCall(0).args[0], null); 212 | }); 213 | 214 | it('should forward default execution callback', () => { 215 | assert.equal(typeof customInstallTask.getCall(0).args[1], 'function'); 216 | }); 217 | }); 218 | 219 | describe('with function customInstallTask and custom path', () => { 220 | let runContext; 221 | let customInstallTask; 222 | let installTask; 223 | beforeEach(async () => { 224 | customInstallTask = stub(); 225 | installTask = (pm, defaultTask) => defaultTask(pm); 226 | runContext = helpers 227 | .create('custom-install', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) }) 228 | .withOptions({ skipInstall: false }) 229 | .withGenerators([ 230 | [ 231 | class extends FeaturesGenerator { 232 | constructor(arguments_, options) { 233 | super(arguments_, options, { customInstallTask }); 234 | this.destinationRoot(this.destinationPath('foo')); 235 | this.env.watchForPackageManagerInstall({ 236 | cwd: this.destinationPath(), 237 | installTask, 238 | }); 239 | } 240 | 241 | packageJsonTask() { 242 | this.packageJson.set({ name: 'foo' }); 243 | } 244 | }, 245 | { namespace: 'custom-install:app' }, 246 | ], 247 | ]); 248 | await runContext.run(); 249 | }); 250 | 251 | it('should not call customInstallTask', () => { 252 | assert.equal(customInstallTask.callCount, 0, 'should not have been called'); 253 | }); 254 | 255 | it('should call packageManagerInstallTask twice', () => { 256 | expect(packageManagerInstallTask).toHaveBeenCalledTimes(2); 257 | expect(packageManagerInstallTask).toHaveBeenNthCalledWith( 258 | 2, 259 | expect.objectContaining({ 260 | customInstallTask, 261 | }), 262 | ); 263 | expect(packageManagerInstallTask).toHaveBeenNthCalledWith( 264 | 1, 265 | expect.objectContaining({ 266 | customInstallTask: installTask, 267 | }), 268 | ); 269 | }); 270 | }); 271 | }); 272 | }); 273 | } 274 | -------------------------------------------------------------------------------- /test/generator-versions.js: -------------------------------------------------------------------------------- 1 | export const generator2 = 'yeoman-generator-2'; 2 | export const generator4 = 'yeoman-generator-4'; 3 | export const generator5 = 'yeoman-generator-5'; 4 | export const generator6 = 'yeoman-generator-6'; 5 | export const generator7 = 'yeoman-generator-7'; 6 | 7 | export const allVersions = [generator6, generator5, generator4, generator2]; 8 | const legacyVersions = new Set([generator2, generator4]); 9 | export const isLegacyVersion = version => legacyVersions.has(version); 10 | 11 | const greaterThan6 = new Set([generator6, generator7]); 12 | export const isGreaterThan6 = version => greaterThan6.has(version); 13 | 14 | export const greaterThan5 = new Set([generator5, ...greaterThan6]); 15 | export const isGreaterThan5 = version => greaterThan5.has(version); 16 | 17 | export const importGenerator = async generatorVersion => { 18 | /* 19 | TODO use dynamic install works for yeoman-generator@4, but not for v2 20 | if (isLegacyVersion(generatorVersion)) { 21 | const version = generatorVersion.split('-')[2]; 22 | console.log(version); 23 | const hash = createHash('shake256', { outputLength: 2 }).update(version, 'utf8').digest('hex'); 24 | console.log(hash); 25 | const { default: generator } = await flyImport(`@yeoman/generator-impl-${hash}@npm:yeoman-generator@${version}`); 26 | return generator; 27 | } 28 | */ 29 | 30 | const { default: generator } = await import(generatorVersion); 31 | return generator; 32 | }; 33 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import { createHelpers } from 'yeoman-test'; 2 | import Environment from '../src/index.js'; 3 | 4 | export const getCreateEnv = 5 | Environment => 6 | (arguments_, ...others) => 7 | Array.isArray(arguments_) ? new Environment(...others) : new Environment(arguments_, ...others); 8 | 9 | export default createHelpers({ 10 | createEnv: getCreateEnv(Environment), 11 | }); 12 | -------------------------------------------------------------------------------- /test/package-manager.js: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { stub } from 'sinon'; 4 | import { after, afterEach, beforeEach, describe, esmocha, expect, it } from 'esmocha'; 5 | 6 | const { execa } = await esmocha.mock('execa', import('execa')); 7 | const { whichPackageManager } = await esmocha.mock('which-package-manager', import('which-package-manager')); 8 | 9 | const { packageManagerInstallTask } = await import('../src/package-manager.js'); 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = dirname(__filename); 13 | 14 | const changesToPackageJson = ` 15 | Changes to package.json were detected.`; 16 | const skippingInstall = `Skipping package manager install. 17 | `; 18 | const runningPackageManager = pm => ` 19 | Running ${pm} install for you to install the required dependencies.`; 20 | 21 | describe('environment (package-manager)', () => { 22 | let adapter; 23 | let memFs; 24 | let packageJsonLocation; 25 | 26 | beforeEach(() => { 27 | adapter = { log: esmocha.fn() }; 28 | execa.mockReturnValue(); 29 | memFs = { get: esmocha.fn() }; 30 | packageJsonLocation = path.join(__dirname, 'fixtures', 'package-manager', 'npm'); 31 | whichPackageManager.mockResolvedValue('npm'); 32 | }); 33 | 34 | afterEach(() => { 35 | esmocha.resetAllMocks(); 36 | }); 37 | after(() => { 38 | esmocha.reset(); 39 | }); 40 | 41 | describe('#packageManagerInstallTask()', () => { 42 | describe('without a package.json', async () => { 43 | beforeEach(() => packageManagerInstallTask({ adapter, memFs, packageJsonLocation })); 44 | 45 | it('should not log', () => { 46 | expect(adapter.log).not.toBeCalled(); 47 | }); 48 | 49 | it('should not call spawnCommand', () => { 50 | expect(execa).not.toBeCalled(); 51 | }); 52 | }); 53 | 54 | describe('with a package.json', async () => { 55 | describe('when package.json was not committed', () => { 56 | beforeEach(async () => { 57 | memFs.get.mockReturnValue({ committed: false }); 58 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation }); 59 | }); 60 | 61 | it('should log', () => { 62 | expect(adapter.log).toBeCalledTimes(1); 63 | expect(adapter.log).toHaveBeenNthCalledWith( 64 | 1, 65 | ` 66 | No change to package.json was detected. No package manager install will be executed.`, 67 | ); 68 | }); 69 | 70 | it('should not call spawnCommand', () => { 71 | expect(execa).not.toBeCalled(); 72 | }); 73 | }); 74 | 75 | describe('when package.json was committed', () => { 76 | beforeEach(async () => { 77 | memFs.get = stub().returns({ committed: true }); 78 | }); 79 | 80 | describe('with skipInstall', () => { 81 | beforeEach(async () => { 82 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation, skipInstall: true }); 83 | }); 84 | 85 | it('should log', async () => { 86 | expect(adapter.log).toBeCalledTimes(2); 87 | expect(adapter.log).toHaveBeenNthCalledWith(1, changesToPackageJson); 88 | expect(adapter.log).toHaveBeenNthCalledWith(2, skippingInstall); 89 | }); 90 | 91 | it('should not call spawnCommand', () => { 92 | expect(execa).not.toBeCalled(); 93 | }); 94 | }); 95 | 96 | describe('with npm', () => { 97 | beforeEach(async () => { 98 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation }); 99 | }); 100 | 101 | it('should log', async () => { 102 | expect(adapter.log).toBeCalledTimes(2); 103 | expect(adapter.log).toHaveBeenNthCalledWith(1, changesToPackageJson); 104 | expect(adapter.log).toHaveBeenNthCalledWith(2, runningPackageManager('npm')); 105 | }); 106 | 107 | it('should execute npm', () => { 108 | expect(execa).toBeCalled(); 109 | expect(execa).toBeCalledWith('npm', ['install'], expect.any(Object)); 110 | }); 111 | }); 112 | 113 | describe('with yarn', () => { 114 | beforeEach(async () => { 115 | whichPackageManager.mockResolvedValue('yarn'); 116 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation }); 117 | }); 118 | 119 | it('should log', async () => { 120 | expect(adapter.log).toBeCalledTimes(2); 121 | expect(adapter.log).toHaveBeenNthCalledWith(1, changesToPackageJson); 122 | expect(adapter.log).toHaveBeenNthCalledWith(2, runningPackageManager('yarn')); 123 | }); 124 | 125 | it('should execute yarn', () => { 126 | expect(execa).toBeCalled(); 127 | expect(execa).toBeCalledWith('yarn', ['install'], expect.any(Object)); 128 | }); 129 | }); 130 | 131 | describe('with pnpm', () => { 132 | beforeEach(async () => { 133 | whichPackageManager.mockResolvedValue('pnpm'); 134 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation }); 135 | }); 136 | 137 | it('should log', async () => { 138 | expect(adapter.log).toBeCalledTimes(2); 139 | expect(adapter.log).toHaveBeenNthCalledWith(1, changesToPackageJson); 140 | expect(adapter.log).toHaveBeenNthCalledWith(2, runningPackageManager('pnpm')); 141 | }); 142 | 143 | it('should execute pnpm', () => { 144 | expect(execa).toBeCalled(); 145 | expect(execa).toBeCalledWith('pnpm', ['install'], expect.any(Object)); 146 | }); 147 | }); 148 | 149 | describe('with bun', () => { 150 | beforeEach(async () => { 151 | whichPackageManager.mockResolvedValue('bun'); 152 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation }); 153 | }); 154 | 155 | it('should log', async () => { 156 | expect(adapter.log).toBeCalledTimes(2); 157 | expect(adapter.log).toHaveBeenNthCalledWith(1, changesToPackageJson); 158 | expect(adapter.log).toHaveBeenNthCalledWith(2, runningPackageManager('bun')); 159 | }); 160 | 161 | it('should execute bun', () => { 162 | expect(execa).toBeCalled(); 163 | expect(execa).toBeCalledWith('bun', ['install'], expect.any(Object)); 164 | }); 165 | }); 166 | 167 | describe('with an unsupported package manager', () => { 168 | beforeEach(async () => { 169 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation, nodePackageManager: 'foo' }); 170 | }); 171 | 172 | it('should log', async () => { 173 | expect(adapter.log).toBeCalledTimes(2); 174 | expect(adapter.log).toHaveBeenNthCalledWith(1, changesToPackageJson); 175 | expect(adapter.log).toHaveBeenNthCalledWith(2, 'foo is not a supported package manager. Run it by yourself.'); 176 | }); 177 | 178 | it('should not call spawnCommand', () => { 179 | expect(execa).not.toBeCalled(); 180 | }); 181 | }); 182 | 183 | describe('error detecting package manager', () => { 184 | beforeEach(async () => { 185 | whichPackageManager.mockResolvedValue(); 186 | await packageManagerInstallTask({ adapter, memFs, packageJsonLocation }); 187 | }); 188 | 189 | it('should log', async () => { 190 | expect(adapter.log).toBeCalledTimes(3); 191 | expect(adapter.log).toHaveBeenNthCalledWith(1, changesToPackageJson); 192 | expect(adapter.log).toHaveBeenNthCalledWith(2, 'Error detecting the package manager. Falling back to npm.'); 193 | expect(adapter.log).toHaveBeenNthCalledWith(3, runningPackageManager('npm')); 194 | }); 195 | 196 | it('should not call spawnCommand', () => { 197 | expect(execa).toBeCalledWith('npm', ['install'], expect.any(Object)); 198 | }); 199 | }); 200 | }); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /test/plugins.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import os from 'node:os'; 3 | import path from 'node:path'; 4 | import process from 'node:process'; 5 | import { mkdirSync, rmSync } from 'node:fs'; 6 | import { afterEach, beforeEach, describe, it } from 'esmocha'; 7 | import Environment from '../src/index.js'; 8 | 9 | const tmpdir = path.join(os.tmpdir(), 'yeoman-environment/light'); 10 | 11 | describe('Generators plugin', () => { 12 | beforeEach(function () { 13 | mkdirSync(tmpdir, { recursive: true }); 14 | this.cwd = process.cwd(); 15 | process.chdir(tmpdir); 16 | }); 17 | 18 | afterEach(function () { 19 | this.timeout(40_000); 20 | process.chdir(this.cwd); 21 | rmSync(tmpdir, { recursive: true }); 22 | }); 23 | 24 | for (const extended of [undefined, 'super:app']) { 25 | describe(`#run ${extended}`, () => { 26 | beforeEach(async function () { 27 | this.timeout(300_000); 28 | delete this.execValue; 29 | 30 | this.env = new Environment({ skipInstall: true, experimental: true }); 31 | 32 | const self = this; 33 | const superGenerator = { 34 | async createGenerator(environment) { 35 | const Generator = await environment.requireGenerator(); 36 | return class extends Generator { 37 | exec() {} 38 | }; 39 | }, 40 | }; 41 | this.env.registerStub(superGenerator, 'super:app'); 42 | 43 | const dummy = { 44 | async createGenerator(environment) { 45 | return class extends (await environment.requireGenerator(extended)) { 46 | exec() { 47 | self.execValue = 'done'; 48 | } 49 | }; 50 | }, 51 | }; 52 | this.env.registerStub(dummy, 'dummy:app'); 53 | }); 54 | 55 | it(`runs generators plugin with requireGenerator value ${extended}`, function () { 56 | this.timeout(100_000); 57 | const self = this; 58 | return this.env.run('dummy:app').then(() => { 59 | assert.equal(self.execValue, 'done'); 60 | }); 61 | }); 62 | }); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /test/resolver.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-await-expression-member */ 2 | import path, { dirname, join, relative } from 'node:path'; 3 | import assert from 'node:assert'; 4 | import { fileURLToPath } from 'node:url'; 5 | import { createRequire } from 'node:module'; 6 | import process from 'node:process'; 7 | import fs from 'fs-extra'; 8 | import { after, afterEach, before, beforeEach, describe, expect, it } from 'esmocha'; 9 | import { execaSync } from 'execa'; 10 | import slash from 'slash'; 11 | import Environment from '../src/index.js'; 12 | import { execaOutput } from '../src/util/util.js'; 13 | import { findPackagesIn, getNpmPaths } from '../src/module-lookup.js'; 14 | import { lookupGenerator } from '../src/generator-lookup.js'; 15 | 16 | const require = createRequire(import.meta.url); 17 | 18 | const __filename = fileURLToPath(import.meta.url); 19 | const __dirname = dirname(__filename); 20 | 21 | const globalLookupTest = () => (process.env.NODE_PATH ? it : xit); 22 | 23 | const toRelativeMeta = meta => 24 | Object.fromEntries( 25 | Object.entries(meta).map(([namespace, meta]) => { 26 | return [ 27 | namespace, 28 | { ...meta, packagePath: slash(relative(__dirname, meta.packagePath)), resolved: slash(relative(__dirname, meta.resolved)) }, 29 | ]; 30 | }), 31 | ); 32 | 33 | const linkGenerator = (generator, scope) => { 34 | const nodeModulesPath = path.resolve('node_modules'); 35 | if (!fs.existsSync(nodeModulesPath)) { 36 | fs.mkdirSync(nodeModulesPath); 37 | } 38 | 39 | let destination = path.join(nodeModulesPath, generator); 40 | if (scope) { 41 | const scopeDir = path.join(nodeModulesPath, scope); 42 | destination = path.join(scopeDir, generator); 43 | if (!fs.existsSync(scopeDir)) { 44 | fs.mkdirSync(scopeDir, { recursive: true }); 45 | } 46 | } 47 | 48 | if (!fs.existsSync(destination)) { 49 | fs.symlinkSync(path.resolve(path.join(__dirname, 'fixtures', generator)), path.resolve(destination), 'dir'); 50 | } 51 | }; 52 | 53 | const unlinkGenerator = (generator, scope) => { 54 | let destination = path.resolve(path.join('node_modules', generator)); 55 | let scopeDir; 56 | if (scope) { 57 | scopeDir = path.resolve(path.join('node_modules', scope)); 58 | destination = path.join(scopeDir, generator); 59 | } 60 | 61 | if (fs.existsSync(destination)) { 62 | fs.unlinkSync(destination); 63 | } 64 | 65 | if (scopeDir && fs.existsSync(scopeDir)) { 66 | fs.rmdirSync(scopeDir); 67 | } 68 | }; 69 | 70 | const projectRoot = path.join(__dirname, 'fixtures/lookup-project'); 71 | const customProjectRoot = path.join(__dirname, 'fixtures/lookup-custom'); 72 | const subDirRoot = path.join(projectRoot, 'subdir'); 73 | 74 | describe('Environment Resolver', async function () { 75 | this.timeout(100_000); 76 | 77 | before(function () { 78 | this.timeout(500_000); 79 | this.cwd = process.cwd(); 80 | 81 | if (!fs.existsSync(projectRoot)) { 82 | fs.mkdirSync(projectRoot); 83 | } 84 | 85 | process.chdir(projectRoot); 86 | if (!fs.existsSync(path.join(projectRoot, 'node_modules'))) { 87 | execaSync('npm', ['ci']); 88 | execaSync('npm', ['install', '-g', 'generator-dummytest', 'generator-dummy', '--no-package-lock']); 89 | } 90 | }); 91 | 92 | beforeEach(() => { 93 | this.NODE_PATH = process.env.NODE_PATH; 94 | delete process.env.NODE_PATH; 95 | this.NVM_PATH = process.env.NVM_PATH; 96 | delete process.env.NVM_PATH; 97 | }); 98 | 99 | afterEach(function () { 100 | process.env.NODE_PATH = this.NODE_PATH; 101 | process.env.NVM_PATH = this.NVM_PATH; 102 | }); 103 | 104 | after(function () { 105 | process.chdir(this.cwd); 106 | }); 107 | 108 | describe('#lookup()', async () => { 109 | let lookupOptions; 110 | 111 | before(() => { 112 | linkGenerator('generator-extend'); 113 | linkGenerator('generator-scoped', '@dummyscope'); 114 | linkGenerator('generator-esm'); 115 | linkGenerator('generator-common-js'); 116 | linkGenerator('generator-ts'); 117 | linkGenerator('generator-ts-js'); 118 | }); 119 | 120 | after(() => { 121 | unlinkGenerator('generator-extend'); 122 | unlinkGenerator('generator-scoped', '@dummyscope'); 123 | unlinkGenerator('generator-esm'); 124 | unlinkGenerator('generator-common-js'); 125 | unlinkGenerator('generator-ts'); 126 | unlinkGenerator('generator-ts-js'); 127 | }); 128 | 129 | beforeEach(async function () { 130 | this.env = new Environment(); 131 | assert.equal(this.env.namespaces().length, 0, 'ensure env is empty'); 132 | await this.env.lookup({ ...lookupOptions, localOnly: true }); 133 | }); 134 | 135 | it('should register expected generators', async function () { 136 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 137 | 138 | // Register local generators 139 | assert.ok(await this.env.get('dummy:app')); 140 | assert.ok(await this.env.get('dummy:yo')); 141 | 142 | assert.ok((await this.env.get('dummy:app')).packagePath.endsWith(join('node_modules/generator-dummy'))); 143 | assert.ok((await this.env.get('dummy:app')).packagePath.endsWith(join('node_modules/generator-dummy'))); 144 | 145 | // Registers local ts generators 146 | assert.ok(await this.env.get('ts:app')); 147 | 148 | // Registers local common js generators with cjs extension 149 | assert.ok(await this.env.get('common-js:cjs')); 150 | 151 | // Registers local esm generators with js extension 152 | assert.ok(await this.env.get('ts:app')); 153 | 154 | // Registers local esm generators with mjs extension 155 | assert.ok(await this.env.get('esm:mjs')); 156 | 157 | // Js generators takes precedence 158 | assert.equal(await this.env.get('ts-js:app'), require('./fixtures/generator-ts-js/generators/app/index.js')); 159 | 160 | // Register generators in scoped packages 161 | assert.ok(await this.env.get('@dummyscope/scoped:app')); 162 | 163 | // Register non-dependency local generator 164 | assert.ok(await this.env.get('jquery:app')); 165 | 166 | // Register symlinked generators 167 | assert.ok(await this.env.get('extend:support')); 168 | }); 169 | 170 | globalLookupTest()('register global generators', async function () { 171 | assert.ok(await this.env.get('dummytest:app')); 172 | assert.ok(await this.env.get('dummytest:controller')); 173 | }); 174 | 175 | describe("when there's ancestor node_modules/ folder", async () => { 176 | before(() => { 177 | process.chdir(subDirRoot); 178 | execaSync('npm', ['install', '--no-package-lock']); 179 | }); 180 | 181 | after(() => { 182 | process.chdir(projectRoot); 183 | fs.rmdirSync(path.join(subDirRoot, 'node_modules'), { 184 | recursive: true, 185 | }); 186 | }); 187 | 188 | beforeEach(async function () { 189 | this.env = new Environment(); 190 | assert.equal(this.env.namespaces().length, 0, 'ensure env is empty'); 191 | await this.env.lookup({ localOnly: true }); 192 | }); 193 | 194 | it('should register expected generators', async function () { 195 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 196 | }); 197 | 198 | it('register generators in ancestor node_modules directory', async function () { 199 | assert.ok(await this.env.get('jquery:app')); 200 | }); 201 | 202 | it('local generators are prioritized over ancestor', async function () { 203 | const { resolved } = await this.env.get('dummy:app'); 204 | assert.ok(resolved.includes('subdir'), `Couldn't find 'subdir' in ${resolved}`); 205 | }); 206 | }); 207 | 208 | describe('when node_modules is a symlink', async () => { 209 | before(() => { 210 | if (!fs.existsSync(path.resolve('orig'))) { 211 | fs.ensureDirSync(path.resolve('orig')); 212 | fs.moveSync(path.resolve('node_modules'), path.resolve('orig/node_modules')); 213 | fs.ensureSymlinkSync(path.resolve('orig/node_modules'), path.resolve('node_modules')); 214 | } 215 | }); 216 | after(() => { 217 | if (fs.existsSync(path.resolve('orig'))) { 218 | fs.removeSync(path.resolve('node_modules')); 219 | fs.moveSync(path.resolve('orig/node_modules'), path.resolve('node_modules')); 220 | fs.removeSync(path.resolve('orig')); 221 | } 222 | }); 223 | 224 | it('should register expected generators', async function () { 225 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 226 | 227 | // Register local generators 228 | assert.ok(await this.env.get('dummy:app')); 229 | assert.ok(await this.env.get('dummy:yo')); 230 | 231 | assert.ok((await this.env.get('dummy:app')).packagePath.endsWith(join('node_modules/generator-dummy'))); 232 | assert.ok((await this.env.get('dummy:app')).packagePath.endsWith(join('node_modules/generator-dummy'))); 233 | 234 | // Registers local ts generators 235 | assert.ok(await this.env.get('ts:app')); 236 | 237 | // Js generators takes precedence 238 | assert.equal(await this.env.get('ts-js:app'), require('./fixtures/generator-ts-js/generators/app/index.js')); 239 | 240 | // Register generators in scoped packages 241 | assert.ok(await this.env.get('@dummyscope/scoped:app')); 242 | 243 | // Register non-dependency local generator 244 | assert.ok(await this.env.get('jquery:app')); 245 | 246 | // Local generators prioritized over global 247 | const { resolved } = await this.env.get('dummy:app'); 248 | assert.ok(resolved.includes('lookup-project'), `Couldn't find 'lookup-project' in ${resolved}`); 249 | 250 | // Register symlinked generators 251 | assert.ok(await this.env.get('extend:support')); 252 | }); 253 | 254 | globalLookupTest()('register global generators', async function () { 255 | assert.ok(await this.env.get('dummytest:app')); 256 | assert.ok(await this.env.get('dummytest:controller')); 257 | }); 258 | }); 259 | 260 | describe('when modules repository is not called node_modules', async () => { 261 | let lookupOptionsBackup; 262 | let customRepositoryPath; 263 | before(() => { 264 | customRepositoryPath = path.resolve('orig'); 265 | lookupOptionsBackup = lookupOptions; 266 | lookupOptions = { npmPaths: [customRepositoryPath] }; 267 | if (!fs.existsSync(customRepositoryPath)) { 268 | fs.moveSync(path.resolve('node_modules'), customRepositoryPath); 269 | } 270 | }); 271 | after(() => { 272 | lookupOptions = lookupOptionsBackup; 273 | if (fs.existsSync(path.resolve('orig'))) { 274 | fs.moveSync(customRepositoryPath, path.resolve('node_modules')); 275 | } 276 | }); 277 | 278 | it('should register expected generators', async function () { 279 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 280 | 281 | // Register local generators 282 | assert.ok(await this.env.get('dummy:app')); 283 | assert.ok(await this.env.get('dummy:yo')); 284 | 285 | assert.ok((await this.env.get('dummy:app')).packagePath.endsWith(join('/generator-dummy'))); 286 | assert.ok((await this.env.get('dummy:app')).packagePath.endsWith(join('/generator-dummy'))); 287 | 288 | // Registers local ts generators', async function () { 289 | assert.ok(await this.env.get('ts:app')); 290 | 291 | // Js generators takes precedence', async function () { 292 | assert.equal(await this.env.get('ts-js:app'), require('./fixtures/generator-ts-js/generators/app/index.js')); 293 | 294 | // Register generators in scoped packages', async function () { 295 | assert.ok(await this.env.get('@dummyscope/scoped:app')); 296 | 297 | // Local generators prioritized over global 298 | const { resolved } = await this.env.get('dummy:app'); 299 | assert.ok(resolved.includes('orig'), `Couldn't find 'lookup-project' in ${resolved}`); 300 | 301 | // Register symlinked generators 302 | assert.ok(await this.env.get('extend:support')); 303 | }); 304 | }); 305 | 306 | describe('when localOnly argument is true', async () => { 307 | beforeEach(async function () { 308 | this.env = new Environment(); 309 | assert.equal(this.env.namespaces().length, 0, 'ensure env is empty'); 310 | await this.env.lookup({ localOnly: true }); 311 | this.env.alias('dummy-alias', 'dummy'); 312 | }); 313 | 314 | it('should register expected generators', async function () { 315 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 316 | 317 | // Register local generators 318 | assert.ok(await this.env.get('dummy:app')); 319 | assert.ok(await this.env.get('dummy:yo')); 320 | assert.ok(this.env.isPackageRegistered('dummy')); 321 | assert.ok(this.env.isPackageRegistered('dummy-alias')); 322 | 323 | // Register generators in scoped packages 324 | assert.ok(await this.env.get('@dummyscope/scoped:app')); 325 | 326 | // Register non-dependency local generator 327 | assert.ok(await this.env.get('jquery:app')); 328 | 329 | // Register symlinked generators 330 | assert.ok(await this.env.get('extend:support')); 331 | }); 332 | 333 | globalLookupTest()('does not register global generators', async function () { 334 | assert.ok(!this.env.get('dummytest:app')); 335 | assert.ok(!this.env.get('dummytest:controller')); 336 | }); 337 | }); 338 | 339 | describe('when options.localOnly argument is true', async () => { 340 | beforeEach(async function () { 341 | this.env = new Environment(); 342 | assert.equal(this.env.namespaces().length, 0, 'ensure env is empty'); 343 | await this.env.lookup({ localOnly: true }); 344 | }); 345 | 346 | it('should register expected generators', async function () { 347 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 348 | 349 | // Register local generators 350 | assert.ok(await this.env.get('dummy:app')); 351 | assert.ok(await this.env.get('dummy:yo')); 352 | 353 | // Register generators in scoped packages 354 | assert.ok(await this.env.get('@dummyscope/scoped:app')); 355 | 356 | // Register non-dependency local generator 357 | assert.ok(await this.env.get('jquery:app')); 358 | 359 | // Register symlinked generators 360 | assert.ok(await this.env.get('extend:support')); 361 | }); 362 | 363 | globalLookupTest()('does not register global generators', async function () { 364 | assert.ok(!this.env.get('dummytest:app')); 365 | assert.ok(!this.env.get('dummytest:controller')); 366 | }); 367 | }); 368 | }); 369 | 370 | describe('#lookup() with options', async () => { 371 | before(() => { 372 | process.chdir(customProjectRoot); 373 | 374 | linkGenerator('generator-scoped', '@scoped'); 375 | linkGenerator('generator-module-lib-gen'); 376 | linkGenerator('generator-module'); 377 | linkGenerator('generator-module-root'); 378 | }); 379 | 380 | beforeEach(function () { 381 | this.env = new Environment(); 382 | }); 383 | 384 | after(() => { 385 | unlinkGenerator('generator-scoped', '@scoped'); 386 | unlinkGenerator('generator-module-lib-gen'); 387 | unlinkGenerator('generator-module'); 388 | unlinkGenerator('generator-module-root'); 389 | 390 | process.chdir(projectRoot); 391 | }); 392 | 393 | it('with packagePaths', async function () { 394 | await this.env.lookup({ localOnly: true, packagePaths: ['node_modules/generator-module'] }); 395 | 396 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 397 | 398 | assert.ok(await this.env.get('module:app')); 399 | assert.ok(this.env.getRegisteredPackages().length === 1); 400 | }); 401 | 402 | it('with customizeNamespace', async function () { 403 | await this.env.lookup({ 404 | localOnly: true, 405 | packagePaths: ['node_modules/generator-module'], 406 | customizeNamespace: ns => ns.replace('module', 'custom'), 407 | }); 408 | 409 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 410 | 411 | assert.ok(await this.env.get('custom:app')); 412 | assert.ok(this.env.getRegisteredPackages().length === 1); 413 | }); 414 | 415 | it('with scope and packagePaths', async function () { 416 | await this.env.lookup({ 417 | localOnly: true, 418 | packagePaths: ['node_modules/generator-module', 'node_modules/@scoped/generator-scoped'], 419 | registerToScope: 'test', 420 | }); 421 | 422 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 423 | 424 | assert.ok(await this.env.get('@test/module:app')); 425 | assert.ok(await this.env.get('@scoped/scoped:app')); 426 | assert.ok(this.env.getRegisteredPackages().length === 2); 427 | }); 428 | 429 | it('with 2 packagePaths', async function () { 430 | await this.env.lookup({ 431 | localOnly: true, 432 | packagePaths: ['node_modules/generator-module', 'node_modules/generator-module-root'], 433 | }); 434 | 435 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 436 | 437 | assert.ok(await this.env.get('module:app')); 438 | assert.ok(await this.env.get('module-root:app')); 439 | assert.ok(this.env.getRegisteredPackages().length === 2); 440 | }); 441 | 442 | it('with 3 packagePaths', async function () { 443 | await this.env.lookup({ 444 | localOnly: true, 445 | packagePaths: ['node_modules/generator-module', 'node_modules/generator-module-root', 'node_modules/generator-module-lib-gen'], 446 | }); 447 | 448 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 449 | 450 | assert.ok(await this.env.get('module:app')); 451 | assert.ok(await this.env.get('module-root:app')); 452 | assert.ok(await this.env.get('module-lib-gen:app')); 453 | assert.ok(this.env.getRegisteredPackages().length === 3); 454 | }); 455 | 456 | it('with scoped packagePaths', async function () { 457 | await this.env.lookup({ 458 | localOnly: true, 459 | packagePaths: [ 460 | 'node_modules/generator-module', 461 | 'node_modules/generator-module-root', 462 | 'node_modules/generator-module-lib-gen', 463 | 'node_modules/@scoped/generator-scoped', 464 | ], 465 | }); 466 | 467 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 468 | 469 | assert.ok(await this.env.get('module:app')); 470 | assert.ok(await this.env.get('module-root:app')); 471 | assert.ok(await this.env.get('module-lib-gen:app')); 472 | assert.ok(await this.env.get('@scoped/scoped:app')); 473 | assert.ok(this.env.getRegisteredPackages().length === 4); 474 | }); 475 | 476 | it('with npmPaths', async function () { 477 | await this.env.lookup({ npmPaths: ['node_modules'] }); 478 | 479 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 480 | 481 | assert.ok(await this.env.get('module:app')); 482 | assert.ok(await this.env.get('module-root:app')); 483 | assert.ok(await this.env.get('module-lib-gen:app')); 484 | assert.ok(await this.env.get('@scoped/scoped:app')); 485 | assert.ok(this.env.getRegisteredPackages().length === 4); 486 | }); 487 | 488 | it('with sub-sub-generators filePatterns', async function () { 489 | await this.env.lookup({ 490 | localOnly: true, 491 | npmPaths: ['node_modules'], 492 | filePatterns: ['*/*/index.js'], 493 | }); 494 | 495 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 496 | 497 | assert.ok(await this.env.get('@scoped/scoped:app:scaffold')); 498 | }); 499 | 500 | it('with packagePatterns', async function () { 501 | await this.env.lookup({ 502 | localOnly: true, 503 | npmPaths: ['node_modules'], 504 | packagePatterns: ['generator-module', 'generator-module-root'], 505 | }); 506 | 507 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 508 | 509 | assert.ok(await this.env.get('module:app')); 510 | assert.ok(await this.env.get('module-root:app')); 511 | assert.ok(this.env.getRegisteredPackages().length === 2); 512 | }); 513 | 514 | it('with sub-sub-generators and packagePaths', async function () { 515 | await this.env.lookup({ 516 | localOnly: true, 517 | packagePaths: ['node_modules/@scoped/generator-scoped'], 518 | filePatterns: ['*/*/index.js'], 519 | }); 520 | 521 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 522 | 523 | assert.ok(await this.env.get('@scoped/scoped:app:scaffold')); 524 | }); 525 | 526 | it('with sub-sub-generators and packagePatterns', async function () { 527 | await this.env.lookup({ 528 | localOnly: true, 529 | npmPaths: ['node_modules'], 530 | packagePatterns: ['generator-scoped'], 531 | filePatterns: ['*/*/index.js'], 532 | }); 533 | 534 | expect(toRelativeMeta(this.env.getGeneratorsMeta())).toMatchSnapshot(); 535 | 536 | assert.ok(await this.env.get('@scoped/scoped:app:scaffold')); 537 | }); 538 | }); 539 | 540 | describe('#lookupNamespaces()', async () => { 541 | before(() => { 542 | process.chdir(customProjectRoot); 543 | 544 | linkGenerator('generator-scoped', '@scoped'); 545 | linkGenerator('generator-module-lib-gen'); 546 | linkGenerator('generator-module'); 547 | linkGenerator('generator-module-root'); 548 | }); 549 | 550 | beforeEach(function () { 551 | this.env = new Environment({ experimental: true }); 552 | }); 553 | 554 | after(() => { 555 | unlinkGenerator('generator-scoped', '@scoped'); 556 | unlinkGenerator('generator-module-lib-gen'); 557 | unlinkGenerator('generator-module'); 558 | unlinkGenerator('generator-module-root'); 559 | 560 | process.chdir(projectRoot); 561 | fs.rmdirSync(path.join(customProjectRoot, 'node_modules'), { 562 | recursive: true, 563 | }); 564 | }); 565 | 566 | it('with 1 namespace', async function () { 567 | await this.env.lookupNamespaces('module:app', { localOnly: true, npmPaths: ['node_modules'] }); 568 | assert.ok(await this.env.get('module:app')); 569 | assert.ok(this.env.getRegisteredPackages().length === 1); 570 | }); 571 | 572 | it('with 2 namespaces', async function () { 573 | await this.env.lookupNamespaces(['module:app', 'module-root:app'], { 574 | localOnly: true, 575 | npmPaths: ['node_modules'], 576 | }); 577 | assert.ok(await this.env.get('module:app')); 578 | assert.ok(await this.env.get('module-root:app')); 579 | assert.ok(this.env.getRegisteredPackages().length === 2); 580 | }); 581 | 582 | it('with sub-sub-generators', async function () { 583 | await this.env.lookupNamespaces('@scoped/scoped:app:scaffold', { 584 | localOnly: true, 585 | npmPaths: ['node_modules'], 586 | }); 587 | assert.ok(await this.env.get('@scoped/scoped:app:scaffold')); 588 | assert.ok(this.env.getRegisteredPackages().length === 1); 589 | }); 590 | }); 591 | 592 | describe('#getNpmPaths()', async () => { 593 | beforeEach(function () { 594 | this.bestBet = path.join(__dirname, '../../../..'); 595 | this.bestBet2 = path.join(path.dirname(process.argv[1]), '../..'); 596 | this.env = new Environment(); 597 | }); 598 | 599 | describe('with NODE_PATH', async () => { 600 | beforeEach(() => { 601 | process.env.NODE_PATH = '/some/dummy/path'; 602 | }); 603 | 604 | it('walk up the CWD lookups dir', async function () { 605 | const paths = getNpmPaths({ localOnly: false, filterPaths: false }); 606 | assert.equal(paths[0], path.join(process.cwd(), 'node_modules')); 607 | assert.equal(paths[1], path.join(process.cwd(), '../node_modules')); 608 | }); 609 | 610 | it('append NODE_PATH', async function () { 611 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).includes(process.env.NODE_PATH)); 612 | }); 613 | }); 614 | 615 | describe('without NODE_PATH', async () => { 616 | it('walk up the CWD lookups dir', async function () { 617 | const paths = getNpmPaths({ localOnly: false, filterPaths: false }); 618 | assert.equal(paths[0], path.join(process.cwd(), 'node_modules')); 619 | const prevdir = process.cwd().split(path.sep).slice(0, -1).join(path.sep); 620 | assert.equal(paths[1], path.join(prevdir, 'node_modules')); 621 | }); 622 | 623 | it('append best bet if NODE_PATH is unset', async function () { 624 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).includes(this.bestBet)); 625 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).includes(this.bestBet2)); 626 | }); 627 | 628 | it('append default NPM dir depending on your OS', async function () { 629 | if (process.platform === 'win32') { 630 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).includes(path.join(process.env.APPDATA, 'npm/node_modules'))); 631 | } else { 632 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).includes('/usr/lib/node_modules')); 633 | } 634 | }); 635 | }); 636 | 637 | describe('with NVM_PATH', async () => { 638 | beforeEach(() => { 639 | process.env.NVM_PATH = '/some/dummy/path'; 640 | }); 641 | 642 | it('walk up the CWD lookups dir', async function () { 643 | const paths = getNpmPaths({ localOnly: false, filterPaths: false }); 644 | assert.equal(paths[0], path.join(process.cwd(), 'node_modules')); 645 | assert.equal(paths[1], path.join(process.cwd(), '../node_modules')); 646 | }); 647 | 648 | it('append NVM_PATH', async function () { 649 | assert( 650 | getNpmPaths({ localOnly: false, filterPaths: false }).includes(path.join(path.dirname(process.env.NVM_PATH), 'node_modules')), 651 | ); 652 | }); 653 | }); 654 | 655 | describe('without NVM_PATH', async () => { 656 | it('walk up the CWD lookups dir', async function () { 657 | const paths = getNpmPaths({ localOnly: false, filterPaths: false }); 658 | assert.equal(paths[0], path.join(process.cwd(), 'node_modules')); 659 | assert.equal(paths[1], path.join(process.cwd(), '../node_modules')); 660 | }); 661 | 662 | it('append best bet if NVM_PATH is unset', async function () { 663 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).includes(path.join(this.bestBet, 'node_modules'))); 664 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).includes(this.bestBet2)); 665 | }); 666 | }); 667 | 668 | describe('when localOnly argument is true', async () => { 669 | it('walk up the CWD lookups dir', async function () { 670 | const paths = getNpmPaths({ localOnly: false, filterPaths: false }); 671 | assert.equal(paths[0], path.join(process.cwd(), 'node_modules')); 672 | assert.equal(paths[1], path.join(process.cwd(), '../node_modules')); 673 | }); 674 | 675 | it('does not append NODE_PATH', async function () { 676 | process.env.NODE_PATH = '/some/dummy/path'; 677 | assert(!getNpmPaths({ localOnly: true, filterPaths: false }).includes(process.env.NODE_PATH)); 678 | }); 679 | 680 | it('does not append NVM_PATH', async function () { 681 | process.env.NVM_PATH = '/some/dummy/path'; 682 | assert( 683 | !getNpmPaths({ localOnly: true, filterPaths: false }).includes(path.join(path.dirname(process.env.NVM_PATH), 'node_modules')), 684 | ); 685 | }); 686 | 687 | it('does not append best bet', async function () { 688 | assert(!getNpmPaths({ localOnly: true, filterPaths: false }).includes(this.bestBet)); 689 | }); 690 | 691 | it('does not append default NPM dir depending on your OS', async function () { 692 | if (process.platform === 'win32') { 693 | assert(!getNpmPaths({ localOnly: true, filterPaths: false }).includes(path.join(process.env.APPDATA, 'npm/node_modules'))); 694 | } else { 695 | assert(!getNpmPaths({ localOnly: true, filterPaths: false }).includes('/usr/lib/node_modules')); 696 | } 697 | }); 698 | }); 699 | 700 | describe('with npm global prefix', async () => { 701 | it('append npm modules path depending on your OS', async function () { 702 | const npmPrefix = execaOutput('npm', ['prefix', '-g']); 703 | if (process.platform === 'win32') { 704 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).indexOf(path.resolve(npmPrefix, 'node_modules')) > 0); 705 | } else { 706 | assert(getNpmPaths({ localOnly: false, filterPaths: false }).indexOf(path.resolve(npmPrefix, 'lib/node_modules')) > 0); 707 | } 708 | }); 709 | }); 710 | }); 711 | 712 | describe('#findPackagesIn()', async () => { 713 | before(() => { 714 | linkGenerator('generator-scoped', '@dummyscope'); 715 | }); 716 | 717 | after(() => { 718 | unlinkGenerator('generator-scoped', '@dummyscope'); 719 | }); 720 | 721 | beforeEach(function () { 722 | this.env = new Environment(); 723 | }); 724 | 725 | describe('when passing package patterns without scope', async () => { 726 | it('finds it', async function () { 727 | const packageToFind = 'generator-dummy'; 728 | const actual = findPackagesIn(['node_modules'], [packageToFind]); 729 | assert.equal(actual.length, 1); 730 | assert.ok(actual[0].endsWith(packageToFind)); 731 | }); 732 | }); 733 | 734 | describe('when passing package patterns with scope', async () => { 735 | it('finds it', async function () { 736 | const packageToFind = '@dummyscope/generator-scoped'; 737 | const actual = findPackagesIn(['node_modules'], [packageToFind]); 738 | assert.equal(actual.length, 1); 739 | assert.ok(actual[0].endsWith(packageToFind)); 740 | }); 741 | }); 742 | }); 743 | 744 | describe('#lookupGenerator()', async () => { 745 | before(() => { 746 | process.chdir(customProjectRoot); 747 | 748 | linkGenerator('generator-extend'); 749 | linkGenerator('generator-scoped', '@dummyscope'); 750 | linkGenerator('generator-module'); 751 | }); 752 | 753 | after(() => { 754 | unlinkGenerator('generator-extend'); 755 | unlinkGenerator('generator-scoped', '@dummyscope'); 756 | unlinkGenerator('generator-module'); 757 | 758 | process.chdir(projectRoot); 759 | fs.rmdirSync(path.join(customProjectRoot, 'node_modules'), { 760 | recursive: true, 761 | }); 762 | }); 763 | 764 | describe('Find generator', async () => { 765 | it('Scoped lookup', async () => { 766 | const modulePath = lookupGenerator('@dummyscope/scoped:app'); 767 | assert.ok(modulePath.endsWith('node_modules/@dummyscope/generator-scoped/app/index.js')); 768 | const packagePath = lookupGenerator('@dummyscope/scoped:app', { packagePath: true }); 769 | assert.ok(packagePath.endsWith('node_modules/@dummyscope/generator-scoped')); 770 | }); 771 | it('Lookup', async () => { 772 | const modulePath = lookupGenerator('extend:support'); 773 | assert.ok(modulePath.endsWith('node_modules/generator-extend/support/index.js')); 774 | 775 | const packagePath = lookupGenerator('extend:support', { 776 | packagePath: true, 777 | }); 778 | const packagePath3 = lookupGenerator('extend', { 779 | packagePath: true, 780 | }); 781 | assert.ok(packagePath.endsWith('node_modules/generator-extend')); 782 | assert.ok(packagePath3.endsWith('node_modules/generator-extend')); 783 | }); 784 | it('Module Lookup', async () => { 785 | const modulePath = lookupGenerator('module:app'); 786 | assert.ok(modulePath.endsWith('node_modules/generator-module/generators/app/index.js'), modulePath); 787 | 788 | const packagePath = lookupGenerator('module:app', { 789 | packagePath: true, 790 | }); 791 | assert.ok(packagePath.endsWith('node_modules/generator-module'), packagePath); 792 | 793 | const generatorPath = lookupGenerator('module:app', { 794 | generatorPath: true, 795 | }); 796 | assert.ok(generatorPath.endsWith('node_modules/generator-module/generators/'), generatorPath); 797 | }); 798 | }); 799 | }); 800 | 801 | describe('#lookupGenerator() with multiple option', async () => { 802 | before(() => { 803 | process.chdir(customProjectRoot); 804 | 805 | this.chdirRoot = path.join(customProjectRoot, 'node_modules/foo'); 806 | this.chdirRootNodeModule = path.join(this.chdirRoot, 'node_modules'); 807 | this.multipleModuleGenerator = path.join(this.chdirRootNodeModule, 'generator-module'); 808 | 809 | fs.mkdirSync(this.chdirRoot, { recursive: true }); 810 | linkGenerator('generator-module'); 811 | process.chdir(this.chdirRoot); 812 | linkGenerator('generator-module'); 813 | }); 814 | 815 | after(() => { 816 | unlinkGenerator('generator-module'); 817 | process.chdir(customProjectRoot); 818 | unlinkGenerator('generator-module'); 819 | process.chdir(projectRoot); 820 | 821 | fs.rmdirSync(path.join(customProjectRoot, 'node_modules'), { 822 | recursive: true, 823 | }); 824 | }); 825 | 826 | describe('Find generator', async () => { 827 | it('Module Lookup', async () => { 828 | const modulePath = lookupGenerator('module:app'); 829 | assert.ok(modulePath.endsWith('node_modules/generator-module/generators/app/index.js')); 830 | 831 | const multiplePath = lookupGenerator('module:app', { 832 | singleResult: false, 833 | }); 834 | assert.equal(multiplePath.length, 2); 835 | assert.ok(multiplePath[0].endsWith('lookup-custom/node_modules/generator-module/generators/app/index.js')); 836 | assert.ok(multiplePath[1].endsWith('lookup-custom/node_modules/foo/node_modules/generator-module/generators/app/index.js')); 837 | 838 | const multiplePath2 = lookupGenerator('module:app', { 839 | singleResult: false, 840 | }); 841 | assert.equal(multiplePath2.length, 2); 842 | assert.ok(multiplePath2[0].endsWith('lookup-custom/node_modules/generator-module/generators/app/index.js')); 843 | assert.ok(multiplePath2[1].endsWith('lookup-custom/node_modules/foo/node_modules/generator-module/generators/app/index.js')); 844 | }); 845 | }); 846 | }); 847 | 848 | describe('Enviroment with a generator extended by environment lookup', async () => { 849 | before(() => { 850 | linkGenerator('generator-environment-extend'); 851 | }); 852 | 853 | after(() => { 854 | unlinkGenerator('generator-environment-extend'); 855 | }); 856 | 857 | describe('Find generator', async () => { 858 | it('Generator extended by environment lookup', async () => { 859 | this.env = new Environment(); 860 | assert.equal(this.env.namespaces().length, 0, 'ensure env is empty'); 861 | await this.env.lookup(); 862 | assert.ok(await this.env.get('environment-extend:app')); 863 | assert.ok(await this.env.create('environment-extend:app')); 864 | }); 865 | }); 866 | }); 867 | }); 868 | -------------------------------------------------------------------------------- /test/store.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { createRequire } from 'node:module'; 3 | import path, { dirname, join } from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | import { beforeEach, describe, it } from 'esmocha'; 6 | import Store from '../src/store.js'; 7 | 8 | const require = createRequire(import.meta.url); 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | describe('Store', async () => { 13 | beforeEach(async function () { 14 | this.store = new Store(); 15 | }); 16 | 17 | describe('#add() / #get()', async () => { 18 | beforeEach(async function () { 19 | this.modulePath = path.join(__dirname, 'fixtures/generator-mocha'); 20 | this.module = require(this.modulePath); 21 | }); 22 | 23 | describe('storing as module', async () => { 24 | beforeEach(async function () { 25 | this.store.add({ namespace: 'foo:module', resolved: '/foo/path' }, this.module); 26 | this.outcome = await this.store.get('foo:module'); 27 | }); 28 | 29 | it('store and return the module', async function () { 30 | assert.equal(this.outcome, this.module); 31 | }); 32 | 33 | it('assign meta data to the module', async function () { 34 | assert.equal(this.outcome.namespace, 'foo:module'); 35 | assert.equal(this.outcome.resolved, join('/foo/path/index.js')); 36 | }); 37 | }); 38 | 39 | describe('storing as module path', async () => { 40 | beforeEach(async function () { 41 | this.store.add({ namespace: 'foo:path', resolved: this.modulePath }); 42 | this.outcome = await this.store.get('foo:path'); 43 | }); 44 | 45 | it('store and returns the required module', async function () { 46 | assert.notEqual(this.outcome, this.modulePath); 47 | assert.equal(this.outcome.usage, 'Usage can be used to customize the help output'); 48 | }); 49 | 50 | it('assign meta data to the module', async function () { 51 | assert.equal(this.outcome.resolved, join(this.modulePath, 'index.js')); 52 | assert.equal(this.outcome.namespace, 'foo:path'); 53 | }); 54 | }); 55 | }); 56 | 57 | describe('#namespaces()', async () => { 58 | beforeEach(async function () { 59 | this.store.add({ namespace: 'foo' }, {}); 60 | this.store.add({ namespace: 'lab' }, {}); 61 | }); 62 | 63 | it('return stored module namespaces', async function () { 64 | assert.deepEqual(this.store.namespaces(), ['foo', 'lab']); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | "compilerOptions": { 4 | /* Language and Environment */ 5 | "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 6 | "lib": ["ES2022"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 7 | "types": ["node"], 8 | 9 | /* Modules */ 10 | "module": "node16" /* Specify what module code is generated. */, 11 | "rootDir": "./src" /* Specify the root folder within your source files. */, 12 | 13 | /* Emit */ 14 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 15 | "declaration": true, 16 | "sourceMap": false, 17 | 18 | /* Interop Constraints */ 19 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 20 | 21 | /* Type Checking */ 22 | "strict": true /* Enable all strict type-checking options. */, 23 | "noImplicitAny": true, 24 | 25 | /* Completeness */ 26 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 27 | } 28 | } 29 | --------------------------------------------------------------------------------