├── .editorconfig
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── codeql.yml
│ ├── dependency-review.yml
│ ├── gh_pages.yml
│ ├── integration.yml
│ ├── lock-maintenance.yml
│ ├── scorecards.yml
│ └── stale.yml
├── .gitignore
├── .mocharc.cjs
├── .prettierignore
├── .prettierignore-doc
├── .prettierrc
├── .vscode
├── launch.json
└── settings.json
├── .xo-config.json
├── LICENSE
├── eslint.config.js
├── jsdoc.json
├── package-lock.json
├── package.json
├── readme.md
├── src
├── actions
│ ├── fs.ts
│ ├── help.ts
│ ├── lifecycle.ts
│ ├── package-json.ts
│ ├── spawn-command.ts
│ └── user.ts
├── constants.ts
├── generator.ts
├── index.ts
├── questions.d.ts
├── types-utils.d.ts
├── types.d.ts
└── util
│ ├── deprecate.js
│ ├── prompt-suggestion.ts
│ └── storage.ts
├── test
├── base.test.ts
├── deprecate.test.ts
├── environment.test.ts
├── fixtures
│ ├── config.json
│ ├── conflict.js
│ ├── dummy-project
│ │ ├── .yo-rc.json
│ │ └── subdir
│ │ │ └── .gitkeep
│ ├── file-conflict.txt
│ ├── file-contains-utf8.yml
│ ├── foo-copy.js
│ ├── foo-template.js
│ ├── foo.js
│ ├── generator-defaults
│ │ ├── app
│ │ │ ├── index.js
│ │ │ ├── options.js
│ │ │ ├── prompts.js
│ │ │ └── templates
│ │ │ │ └── foo-template.js
│ │ └── package.json
│ ├── generator-mocha
│ │ ├── main.js
│ │ └── package.json
│ ├── options-generator
│ │ ├── main.js
│ │ └── package.json
│ ├── testFile.tar.gz
│ └── yeoman-logo.png
├── fs.test.ts
├── generators-compose-workflow.test.ts
├── generators.test.ts
├── integration.test.ts
├── package-json.test.ts
├── prompt-suggestion.test.ts
├── spawn-command.test.ts
├── storage.test.ts
├── user.test.ts
└── utils.ts
├── tsconfig.json
├── tsconfig.spec.json
└── vitest.config.ts
/.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 | [*.md]
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.js text eol=lf
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: needs triage
6 | assignees: ''
7 | ---
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: needs triage, enhancement
6 | assignees: ''
7 | ---
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 5
8 | ignore:
9 | - dependency-name: '@types/node'
10 | versions: ['>=17']
11 | - dependency-name: '*'
12 | update-types: ['version-update:semver-minor', 'version-update:semver-patch']
13 | groups:
14 | vitest:
15 | patterns:
16 | - '@vitest/*'
17 | - 'vitest'
18 |
19 | - package-ecosystem: 'github-actions'
20 | directory: '/'
21 | schedule:
22 | interval: 'weekly'
23 | open-pull-requests-limit: 5
24 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: NPM Test
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - 'dependabot/**'
7 | pull_request:
8 | branches:
9 | - '*'
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | build:
16 | runs-on: ${{ matrix.os }}
17 |
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | os: [ubuntu-latest, windows-latest, macos-latest]
22 | node-version: [20, 22, 23, 24]
23 |
24 | steps:
25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
26 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | - run: npm ci
30 | - run: npm test
31 |
--------------------------------------------------------------------------------
/.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/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27 | with:
28 | path: source
29 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
30 | with:
31 | ref: gh-pages
32 | path: yeoman-generator-doc
33 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
34 | with:
35 | node-version: 20
36 | - run: npm ci
37 | working-directory: source
38 | - name: Cleanup
39 | working-directory: yeoman-generator-doc
40 | run: rm *.html
41 | - name: Generate doc
42 | run: npm run doc
43 | working-directory: source
44 | env:
45 | DOC_FOLDER: ${{ github.event.inputs.path }}
46 | - name: Detect version
47 | run: echo "::set-output name=version::$(node -e "console.log(require('./package.json').version);")"
48 | working-directory: source
49 | id: version
50 | - name: Create commit
51 | working-directory: yeoman-generator-doc
52 | if: always()
53 | run: |
54 | git add .
55 | git config --local user.email "github-actions[bot]@users.noreply.github.com"
56 | git config --local user.name "Github Actions"
57 | git diff --cached
58 | git commit -a -m "Update api for ${{steps.version.outputs.version}}" || true
59 | - name: Create Pull Request
60 | if: always()
61 | id: cpr
62 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
63 | with:
64 | token: ${{ secrets.GITHUB_TOKEN }}
65 | commit-message: 'Update api for ${{steps.version.outputs.version}}'
66 | title: 'Update api for ${{steps.version.outputs.version}}'
67 | body: |
68 | Update api docs
69 | labels: automated pr
70 | branch: gh-pages-master
71 | path: yeoman-generator-doc
72 | - name: Check outputs
73 | run: |
74 | echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}"
75 | echo "Pull Request Number - ${{ steps.cpr.outputs.pr_number }}"
76 |
--------------------------------------------------------------------------------
/.github/workflows/integration.yml:
--------------------------------------------------------------------------------
1 | name: Integration Build
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - '*'
7 | - 'dependabot/**'
8 | pull_request:
9 | branches:
10 | - '*'
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | build:
17 | runs-on: ${{ matrix.os }}
18 |
19 | strategy:
20 | matrix:
21 | os: [ubuntu-latest]
22 | node-version: [20]
23 |
24 | steps:
25 | - name: Checkout yeoman-test
26 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27 | with:
28 | repository: yeoman/yeoman-test
29 | path: yeoman-test
30 |
31 | - name: Checkout yeoman-generator
32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
33 | with:
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 | - name: Run yeoman-test test
46 | run: |
47 | cd $GITHUB_WORKSPACE/yeoman-test
48 | mv package.json package.json.original
49 | jq 'del(.peerDependencies)' package.json.original > package.json
50 | cat package.json
51 | npm install
52 | npm install ${{ github.repository }}#$GITHUB_SHA yeoman/environment#main
53 | npm test
54 |
55 | - name: Run yeoman-generator test
56 | if: always()
57 | run: |
58 | cd $GITHUB_WORKSPACE/yeoman-generator
59 | npm ci
60 | npm install yeoman/yeoman-test#main yeoman/environment#main
61 | npm test
62 |
63 | - name: Run yeoman-environment test
64 | if: always()
65 | run: |
66 | cd $GITHUB_WORKSPACE/yeoman-environment
67 | npm ci
68 | npm install yeoman-generator-7@${{ github.repository }}#$GITHUB_SHA yeoman/yeoman-test#main
69 | npm test
70 |
--------------------------------------------------------------------------------
/.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: 24
20 | cache: npm
21 | - name: Create commit
22 | run: |
23 | rm package-lock.json
24 | npm install
25 | - name: Create Pull Request
26 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
27 | with:
28 | token: ${{ secrets.GITHUB_TOKEN }}
29 | commit-message: 'Bump transitional dependencies'
30 | committer: 'Github Actions <41898282+github-actions[bot]@users.noreply.github.com>'
31 | author: 'Github Actions <41898282+github-actions[bot]@users.noreply.github.com>'
32 | title: 'Bump transitional dependencies'
33 | body: Transitional dependencies bump.
34 | labels: 'dependencies'
35 |
--------------------------------------------------------------------------------
/.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@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
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: 30
21 | days-before-close: 5
22 | stale-issue-label: 'stale'
23 | exempt-issue-labels: 'not stale'
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | .nyc_output
4 | yarn.lock
5 | .project
6 | dist
7 | test/**/package-lock.json
8 |
--------------------------------------------------------------------------------
/.mocharc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extension: ['test.ts'],
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: 120
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/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Mocha All",
8 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
9 | "args": ["--timeout", "999999", "--colors", "${workspaceFolder}/test"],
10 | "console": "integratedTerminal",
11 | "internalConsoleOptions": "neverOpen"
12 | },
13 | {
14 | "type": "node",
15 | "request": "launch",
16 | "name": "Mocha Current File",
17 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
18 | "args": ["--timeout", "999999", "--colors", "${file}"],
19 | "console": "integratedTerminal",
20 | "internalConsoleOptions": "neverOpen"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "debug.javascript.terminalOptions": {
3 | "skipFiles": ["node_modules/**", "dist/**"]
4 | },
5 | "mochaExplorer.logpanel": true,
6 | "mochaExplorer.files": ["test/*.test.ts"],
7 | "mochaExplorer.ui": "tdd",
8 | "mochaExplorer.configFile": ".mocharc.cjs",
9 | "mochaExplorer.require": "esmocha/mocha",
10 | "mochaExplorer.nodeArgv": ["--loader=esmocha/loader"]
11 | }
12 |
--------------------------------------------------------------------------------
/.xo-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "space": true,
3 | "envs": ["es2020", "node", "mocha"],
4 | "prettier": true,
5 | "ignores": ["test/fixtures"],
6 | "printWidth": 120,
7 | "parserOptions": {
8 | "project": "./tsconfig.spec.json"
9 | },
10 | "rules": {
11 | "@typescript-eslint/no-unsafe-argument": "off",
12 | "@typescript-eslint/no-unsafe-assignment": "off",
13 | "@typescript-eslint/no-unsafe-call": "off",
14 | "@typescript-eslint/no-unsafe-return": "off",
15 | "no-await-in-loop": "off",
16 | "prefer-spread": "off"
17 | },
18 | "overrides": [
19 | {
20 | "files": "test/*",
21 | "rules": {
22 | "max-nested-callbacks": "off",
23 | "prefer-rest-params": "off",
24 | "unicorn/prefer-spread": "off",
25 | "unicorn/no-array-callback-reference": "off",
26 | "unicorn/no-object-as-default-parameter": "off",
27 | "unicorn/no-this-assignment": "off",
28 | "@typescript-eslint/no-empty-function": "off",
29 | "@typescript-eslint/no-unused-expressions": "off",
30 | "@typescript-eslint/restrict-plus-operands": "off",
31 | "@typescript-eslint/naming-convention": "off"
32 | }
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Google
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 | 2. Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import configs from '@yeoman/eslint';
3 | import { config } from 'typescript-eslint';
4 |
5 | export default config(...configs, {
6 | rules: {
7 | 'unicorn/prevent-abbreviations': 'off',
8 | 'unicorn/no-thenable': 'off',
9 | 'unicorn/prefer-event-target': 'off',
10 | 'unicorn/no-object-as-default-parameter': 'off',
11 | '@typescript-eslint/consistent-type-imports': 'error',
12 | 'prefer-arrow-callback': 'error',
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/jsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "include": ["readme.md", "lib"],
4 | "includePattern": ".+\\.js(doc)?$"
5 | },
6 | "opts": {
7 | "recurse": true,
8 | "destination": "../yeoman-generator-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": "Generator",
19 | "footerText": "BSD license Copyright (c) Google",
20 | "default": {
21 | "includeDate": false
22 | }
23 | },
24 | "plugins": ["plugins/markdown"]
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yeoman-generator",
3 | "version": "8.0.0-beta.5",
4 | "description": "Rails-inspired generator system that provides scaffolding for your apps",
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/generator",
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 | "./typed": {
29 | "types": "./dist/index.d.js",
30 | "import": "./dist/index.js"
31 | }
32 | },
33 | "main": "./dist/index.js",
34 | "types": "./dist/index.d.js",
35 | "files": [
36 | "dist"
37 | ],
38 | "scripts": {
39 | "build": "tsc && npm run copy-types",
40 | "copy-types": "cpy \"src/**/*.d.(|c|m)ts\" dist/",
41 | "coverage": "c8 report --reporter=text-lcov | coveralls",
42 | "doc": "npm run doc:generate && npm run doc:fix && npm run doc:prettier",
43 | "doc:fix": "sed -i -e 's:^[[:space:]]*[[:space:]]*$::g' $npm_package_config_doc_path$DOC_FOLDER/global.html || true",
44 | "doc:generate": "jsdoc -c jsdoc.json -d $npm_package_config_doc_path$DOC_FOLDER",
45 | "doc:prettier": "prettier $npm_package_config_doc_path$DOC_FOLDER --write --ignore-path .prettierignore-doc",
46 | "fix": "eslint . --fix && prettier . --write",
47 | "prepare": "npm run build",
48 | "pretest": "eslint . && prettier . --check && npm run build",
49 | "test": "vitest run --coverage"
50 | },
51 | "config": {
52 | "doc_path": "../yeoman-generator-doc/"
53 | },
54 | "dependencies": {
55 | "@types/lodash-es": "^4.17.12",
56 | "@yeoman/namespace": "^1.0.1",
57 | "chalk": "^5.4.1",
58 | "debug": "^4.4.1",
59 | "execa": "^9.5.3",
60 | "latest-version": "^9.0.0",
61 | "lodash-es": "^4.17.21",
62 | "mem-fs-editor": "^11.1.4",
63 | "minimist": "^1.2.8",
64 | "read-package-up": "^11.0.0",
65 | "semver": "^7.7.2",
66 | "simple-git": "^3.27.0",
67 | "sort-keys": "^5.1.0",
68 | "text-table": "^0.2.0",
69 | "type-fest": "^4.41.0"
70 | },
71 | "devDependencies": {
72 | "@types/debug": "^4.1.12",
73 | "@types/ejs": "^3.1.5",
74 | "@types/minimist": "^1.2.5",
75 | "@types/semver": "^7.7.0",
76 | "@types/sinon": "^17.0.4",
77 | "@types/text-table": "^0.2.5",
78 | "@vitest/coverage-v8": "^3.1.3",
79 | "@yeoman/adapter": "^2.1.1",
80 | "@yeoman/eslint": "0.2.0",
81 | "@yeoman/transform": "^2.1.0",
82 | "cpy-cli": "^5.0.0",
83 | "ejs": "^3.1.10",
84 | "eslint": "9.12.0",
85 | "inquirer": "^12.6.1",
86 | "jsdoc": "^4.0.4",
87 | "prettier": "3.5.3",
88 | "prettier-plugin-packagejson": "2.5.12",
89 | "sinon": "^20.0.0",
90 | "typescript": "5.8.3",
91 | "vitest": "^3.1.3",
92 | "yeoman-assert": "^3.1.1",
93 | "yeoman-environment": "^5.0.0-beta.0",
94 | "yeoman-test": "^10.1.1"
95 | },
96 | "peerDependencies": {
97 | "@types/node": ">=18.18.5",
98 | "@yeoman/types": "^1.1.1",
99 | "mem-fs": "^4.0.0"
100 | },
101 | "peerDependenciesMeta": {
102 | "@types/node": {
103 | "optional": true
104 | }
105 | },
106 | "engines": {
107 | "node": "^20.17.0 || >=22.9.0"
108 | },
109 | "overrides": {
110 | "yeoman-test": {
111 | "yeoman-generator": "file:."
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Generator [](http://badge.fury.io/js/yeoman-generator) [](https://github.com/yeoman/generator/actions/workflows/integration.yml) [](https://coveralls.io/r/yeoman/generator) [](https://gitter.im/yeoman/yeoman)
2 |
3 | > Rails-inspired generator system that provides scaffolding for your apps
4 |
5 | 
6 |
7 | ## Getting Started
8 |
9 | If you're interested in writing your own Yeoman generator we recommend reading [the official getting started guide](http://yeoman.io/authoring/). The guide covers all the basics you need to get started.
10 |
11 | A generator can be as complex as you want it to be. It can simply copy a bunch of boilerplate files, or it can be more advanced asking the user's preferences to scaffold a tailor made project. This decision is up to you.
12 |
13 | The fastest way to get started is to use [generator-generator](https://github.com/yeoman/generator-generator), a Yeoman generator to generate a Yeoman generator.
14 |
15 | After reading the getting started guide, you might want to read the code source or visit our [API documentation](http://yeoman.io/generator/) for a list of all methods available.
16 |
17 | [API documentation for v4.x](https://yeoman.github.io/generator/4.x).
18 |
19 | ### Debugging
20 |
21 | See the [debugging guide](http://yeoman.io/authoring/debugging.html).
22 |
23 | ## Contributing
24 |
25 | We love contributors! See our [contribution guideline](http://yeoman.io/contributing/) to get started.
26 |
27 | ## Sponsors
28 |
29 | Love Yeoman work and community? Help us keep it alive by donating funds to cover project expenses!
30 | [[Become a sponsor](https://opencollective.com/yeoman#support)]
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | ## License
124 |
125 | [BSD license](http://opensource.org/licenses/bsd-license.php)
126 | Copyright (c) Google
127 |
--------------------------------------------------------------------------------
/src/actions/fs.ts:
--------------------------------------------------------------------------------
1 | /* eslint max-params: [1, 6] */
2 | import assert from 'node:assert';
3 | import { type CopyOptions, type MemFsEditor } from 'mem-fs-editor';
4 | import type { Data as TemplateData, Options as TemplateOptions } from 'ejs';
5 | import type { OverloadParameters, OverloadReturnType } from '../types-utils.js';
6 | import type { BaseGenerator } from '../generator.js';
7 |
8 | export type Template = {
9 | /**
10 | * Template file, absolute or relative to templatePath().
11 | */
12 | source: string;
13 | /**
14 | * Conditional if the template should be written.
15 | * @param TemplateData
16 | * @param Generator
17 | * @returns
18 | */
19 | when?: (data: D, generator: G) => boolean;
20 | /**
21 | * Destination, absolute or relative to destinationPath().
22 | */
23 | destination?: string;
24 | /**
25 | * Mem-fs-editor copy options
26 | */
27 | copyOptions?: CopyOptions;
28 | /**
29 | * Ejs data
30 | */
31 | templateData?: TemplateData;
32 | /**
33 | * Ejs options
34 | */
35 | templateOptions?: TemplateOptions;
36 | };
37 |
38 | export type Templates = Array>;
39 |
40 | function applyToFirstStringArg(
41 | customizer: (arg1: string) => string,
42 | args: Type,
43 | ): Type {
44 | args[0] = Array.isArray(args[0]) ? args[0].map(arg => customizer(arg)) : customizer(args[0]);
45 | return args;
46 | }
47 |
48 | function applyToFirstAndSecondStringArg(
49 | customizer1: (arg1: string) => string,
50 | customizer2: (arg1: string) => string,
51 | args: Type,
52 | ): Type {
53 | args = applyToFirstStringArg(customizer1, args);
54 | args[1] = customizer2(args[1]);
55 | return args;
56 | }
57 |
58 | export class FsMixin {
59 | fs!: MemFsEditor;
60 |
61 | /**
62 | * Read file from templates folder.
63 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
64 | * Shortcut for this.fs!.read(this.templatePath(filepath))
65 | */
66 | readTemplate(
67 | this: BaseGenerator,
68 | ...args: OverloadParameters
69 | ): OverloadReturnType {
70 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
71 | // @ts-expect-error
72 | return this.fs.read(...applyToFirstStringArg(this.templatePath.bind(this), args));
73 | }
74 |
75 | /**
76 | * Copy file from templates folder to destination folder.
77 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
78 | * Shortcut for this.fs!.copy(this.templatePath(from), this.destinationPath(to))
79 | */
80 | copyTemplate(
81 | this: BaseGenerator,
82 | ...args: OverloadParameters
83 | ): OverloadReturnType {
84 | const [from, to, options = {}, ...remaining] = args;
85 |
86 | return this.fs.copy(
87 | from,
88 | this.destinationPath(to),
89 | { fromBasePath: this.templatePath(), ...options },
90 | ...remaining,
91 | );
92 | }
93 |
94 | /**
95 | * Copy file from templates folder to destination folder.
96 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
97 | * Shortcut for this.fs!.copy(this.templatePath(from), this.destinationPath(to))
98 | */
99 | async copyTemplateAsync(
100 | this: BaseGenerator,
101 | ...args: OverloadParameters
102 | ): OverloadReturnType {
103 | return this.fs.copyAsync(
104 | ...applyToFirstAndSecondStringArg(this.templatePath.bind(this), this.destinationPath.bind(this), args),
105 | );
106 | }
107 |
108 | /**
109 | * Read file from destination folder
110 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
111 | * Shortcut for this.fs!.read(this.destinationPath(filepath)).
112 | */
113 | readDestination(
114 | this: BaseGenerator,
115 | ...args: OverloadParameters
116 | ): OverloadReturnType {
117 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
118 | // @ts-expect-error
119 | return this.fs.read(...applyToFirstStringArg(this.destinationPath.bind(this), args));
120 | }
121 |
122 | /**
123 | * Read JSON file from destination folder
124 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
125 | * Shortcut for this.fs!.readJSON(this.destinationPath(filepath)).
126 | */
127 | readDestinationJSON(
128 | this: BaseGenerator,
129 | ...args: OverloadParameters
130 | ): OverloadReturnType {
131 | return this.fs.readJSON(...applyToFirstStringArg(this.destinationPath.bind(this), args));
132 | }
133 |
134 | /**
135 | * Write file to destination folder
136 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
137 | * Shortcut for this.fs!.write(this.destinationPath(filepath)).
138 | */
139 | writeDestination(
140 | this: BaseGenerator,
141 | ...args: OverloadParameters
142 | ): OverloadReturnType {
143 | return this.fs.write(...applyToFirstStringArg(this.destinationPath.bind(this), args));
144 | }
145 |
146 | /**
147 | * Write json file to destination folder
148 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
149 | * Shortcut for this.fs!.writeJSON(this.destinationPath(filepath)).
150 | */
151 | writeDestinationJSON(
152 | this: BaseGenerator,
153 | ...args: OverloadParameters
154 | ): OverloadReturnType {
155 | return this.fs.writeJSON(...applyToFirstStringArg(this.destinationPath.bind(this), args));
156 | }
157 |
158 | /**
159 | * Delete file from destination folder
160 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
161 | * Shortcut for this.fs!.delete(this.destinationPath(filepath)).
162 | */
163 | deleteDestination(
164 | this: BaseGenerator,
165 | ...args: OverloadParameters
166 | ): OverloadReturnType {
167 | return this.fs.delete(...applyToFirstStringArg(this.destinationPath.bind(this), args));
168 | }
169 |
170 | /**
171 | * Copy file from destination folder to another destination folder.
172 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
173 | * Shortcut for this.fs!.copy(this.destinationPath(from), this.destinationPath(to)).
174 | */
175 | copyDestination(
176 | this: BaseGenerator,
177 | ...args: OverloadParameters
178 | ): OverloadReturnType {
179 | const [from, to, options = {}, ...remaining] = args;
180 |
181 | return this.fs.copy(
182 | from,
183 | this.destinationPath(to),
184 | { fromBasePath: this.destinationPath(), ...options },
185 | ...remaining,
186 | );
187 | }
188 |
189 | /**
190 | * Move file from destination folder to another destination folder.
191 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
192 | * Shortcut for this.fs!.move(this.destinationPath(from), this.destinationPath(to)).
193 | */
194 | moveDestination(
195 | this: BaseGenerator,
196 | ...args: OverloadParameters
197 | ): OverloadReturnType {
198 | const [from, to, options, ...remaining] = args;
199 |
200 | return this.fs.move(
201 | from,
202 | this.destinationPath(to),
203 | { fromBasePath: this.destinationPath(), ...options },
204 | ...remaining,
205 | );
206 | }
207 |
208 | /**
209 | * Exists file on destination folder.
210 | * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
211 | * Shortcut for this.fs!.exists(this.destinationPath(filepath)).
212 | */
213 | existsDestination(
214 | this: BaseGenerator,
215 | ...args: OverloadParameters
216 | ): OverloadReturnType {
217 | return this.fs.exists(...applyToFirstStringArg(this.destinationPath.bind(this), args));
218 | }
219 |
220 | /**
221 | * Copy a template from templates folder to the destination.
222 | *
223 | * @param source - template file, absolute or relative to templatePath().
224 | * @param destination - destination, absolute or relative to destinationPath().
225 | * @param templateData - ejs data
226 | * @param templateOptions - ejs options
227 | * @param copyOptions - mem-fs-editor copy options
228 | */
229 | renderTemplate(
230 | this: BaseGenerator,
231 | source: string | string[] = '',
232 | destination: string | string[] = source,
233 | templateData?: string | D,
234 | templateOptions?: TemplateOptions,
235 | copyOptions?: CopyOptions,
236 | ) {
237 | if (templateData === undefined || typeof templateData === 'string') {
238 | templateData = this._templateData(templateData);
239 | }
240 |
241 | templateOptions = { context: this, ...templateOptions };
242 |
243 | source = Array.isArray(source) ? source : [source];
244 | const templatePath = this.templatePath(...source);
245 | destination = Array.isArray(destination) ? destination : [destination];
246 | const destinationPath = this.destinationPath(...destination);
247 |
248 | this.fs.copyTpl(templatePath, destinationPath, templateData as TemplateData, templateOptions, {
249 | fromBasePath: this.templatePath(),
250 | ...copyOptions,
251 | });
252 | }
253 |
254 | /**
255 | * Copy a template from templates folder to the destination.
256 | *
257 | * @param source - template file, absolute or relative to templatePath().
258 | * @param destination - destination, absolute or relative to destinationPath().
259 | * @param templateData - ejs data
260 | * @param templateOptions - ejs options
261 | * @param copyOptions - mem-fs-editor copy options
262 | */
263 | async renderTemplateAsync(
264 | this: BaseGenerator,
265 | source: string | string[] = '',
266 | destination: string | string[] = source,
267 | templateData?: string | D,
268 | templateOptions?: TemplateOptions,
269 | copyOptions?: CopyOptions,
270 | ) {
271 | if (templateData === undefined || typeof templateData === 'string') {
272 | templateData = this._templateData(templateData);
273 | }
274 |
275 | templateOptions = { context: this, ...templateOptions };
276 |
277 | source = Array.isArray(source) ? source : [source];
278 | const templatePath = this.templatePath(...source);
279 | destination = Array.isArray(destination) ? destination : [destination];
280 | const destinationPath = this.destinationPath(...destination);
281 |
282 | return this.fs.copyTplAsync(templatePath, destinationPath, templateData as TemplateData, templateOptions, {
283 | fromBasePath: this.templatePath(),
284 | ...copyOptions,
285 | });
286 | }
287 |
288 | /**
289 | * Copy templates from templates folder to the destination.
290 | */
291 | renderTemplates(
292 | this: BaseGenerator,
293 | templates: Templates,
294 | templateData?: string | D,
295 | ) {
296 | assert(Array.isArray(templates), 'Templates must an array');
297 | if (templateData === undefined || typeof templateData === 'string') {
298 | templateData = this._templateData(templateData);
299 | }
300 |
301 | for (const template of templates) {
302 | const { templateData: eachData = templateData, source, destination } = template;
303 | if (!template.when || template.when(eachData as D, this)) {
304 | this.renderTemplate(source, destination, eachData, template.templateOptions, {
305 | fromBasePath: this.templatePath(),
306 | ...template.copyOptions,
307 | });
308 | }
309 | }
310 | }
311 |
312 | /**
313 | * Copy templates from templates folder to the destination.
314 | *
315 | * @param templates - template file, absolute or relative to templatePath().
316 | * @param templateData - ejs data
317 | */
318 | async renderTemplatesAsync(
319 | this: BaseGenerator,
320 | templates: Templates,
321 | templateData?: string | D,
322 | ) {
323 | assert(Array.isArray(templates), 'Templates must an array');
324 | if (templateData === undefined || typeof templateData === 'string') {
325 | templateData = this._templateData(templateData);
326 | }
327 |
328 | return Promise.all(
329 | templates.map(async template => {
330 | const { templateData: eachData = templateData, source, destination } = template;
331 | if (!template.when || template.when(eachData as D, this)) {
332 | return this.renderTemplateAsync(source, destination, eachData, template.templateOptions, {
333 | fromBasePath: this.templatePath(),
334 | ...template.copyOptions,
335 | });
336 | }
337 |
338 | return;
339 | }),
340 | );
341 | }
342 |
343 | /**
344 | * Utility method to get a formatted data for templates.
345 | *
346 | * @param path - path to the storage key.
347 | * @return data to be passed to the templates.
348 | */
349 | _templateData(this: BaseGenerator, path?: string): D {
350 | if (path) {
351 | return this.config.getPath(path) as any as D;
352 | }
353 |
354 | const allConfig: D = this.config.getAll() as D;
355 | if (this.generatorConfig) {
356 | Object.assign(allConfig as any, this.generatorConfig.getAll());
357 | }
358 |
359 | if (this.instanceConfig) {
360 | Object.assign(allConfig as any, this.instanceConfig.getAll());
361 | }
362 |
363 | return allConfig;
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/src/actions/help.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import fs from 'node:fs';
3 | import table from 'text-table';
4 | import type { ArgumentSpec, CliOptionSpec } from '../types.js';
5 | import type { BaseGenerator } from '../generator.js';
6 |
7 | function formatArg(config: ArgumentSpec) {
8 | let arg = `<${config.name}>`;
9 |
10 | if (!config.required) {
11 | arg = `[${arg}]`;
12 | }
13 |
14 | return arg;
15 | }
16 |
17 | export class HelpMixin {
18 | declare readonly _options: Record;
19 | declare readonly _arguments: ArgumentSpec[];
20 |
21 | /**
22 | * Tries to get the description from a USAGE file one folder above the
23 | * source root otherwise uses a default description
24 | *
25 | * @return Help message of the generator
26 | */
27 | help(this: BaseGenerator): string {
28 | const filepath = path.resolve(this.sourceRoot(), '../USAGE');
29 | const exists = fs.existsSync(filepath);
30 |
31 | let out = ['Usage:', ` ${this.usage()}`, ''];
32 |
33 | // Build options
34 | if (Object.keys(this._options).length > 0) {
35 | out = [...out, 'Options:', this.optionsHelp(), ''];
36 | }
37 |
38 | // Build arguments
39 | if (this._arguments.length > 0) {
40 | out = [...out, 'Arguments:', this.argumentsHelp(), ''];
41 | }
42 |
43 | // Append USAGE file is any
44 | if (exists) {
45 | out.push(fs.readFileSync(filepath, 'utf8'));
46 | }
47 |
48 | return out.join('\n');
49 | }
50 |
51 | /**
52 | * Output usage information for this given generator, depending on its arguments
53 | * or options
54 | *
55 | * @return Usage information of the generator
56 | */
57 | usage(this: BaseGenerator): string {
58 | const options = Object.keys(this._options).length > 0 ? '[options]' : '';
59 | let name = this._namespace;
60 | let args = '';
61 |
62 | if (this._arguments.length > 0) {
63 | args = `${this._arguments.map(arg => formatArg(arg)).join(' ')} `;
64 | }
65 |
66 | name = name.replace(/^yeoman:/, '');
67 | let out = `yo ${name} ${args}${options}`;
68 |
69 | if (this.description) {
70 | out += `\n\n${this.description}`;
71 | }
72 |
73 | return out;
74 | }
75 |
76 | /**
77 | * Simple setter for custom `description` to append on help output.
78 | *
79 | * @param description
80 | */
81 | desc(this: BaseGenerator, description: string) {
82 | this.description = description || '';
83 | return this;
84 | }
85 |
86 | /**
87 | * Get help text for arguments
88 | * @returns Text of options in formatted table
89 | */
90 | argumentsHelp(this: BaseGenerator): string {
91 | const rows = this._arguments.map(config => {
92 | return [
93 | '',
94 | config.name ?? '',
95 | config.description ? `# ${config.description}` : '',
96 | config.type ? `Type: ${config.type.name}` : '',
97 | `Required: ${config.required}`,
98 | ];
99 | });
100 |
101 | return table(rows);
102 | }
103 |
104 | /**
105 | * Get help text for options
106 | * @returns Text of options in formatted table
107 | */
108 | optionsHelp(this: BaseGenerator): string {
109 | const rows = Object.values(this._options)
110 | .filter((opt: any) => !opt.hide)
111 | .map((opt: any) => {
112 | return [
113 | '',
114 | opt.alias ? `-${opt.alias}, ` : '',
115 | `--${opt.name}`,
116 | opt.description ? `# ${opt.description}` : '',
117 | opt.default !== undefined && opt.default !== '' ? `Default: ${opt.default}` : '',
118 | ];
119 | });
120 |
121 | return table(rows);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/actions/lifecycle.ts:
--------------------------------------------------------------------------------
1 | import { dirname, isAbsolute, resolve as pathResolve, relative } from 'node:path';
2 | import { pathToFileURL } from 'node:url';
3 | import { createRequire } from 'node:module';
4 | import { Transform } from 'node:stream';
5 | import { stat } from 'node:fs/promises';
6 | import createDebug from 'debug';
7 | import type { BaseGenerator, ComposeOptions as EnvironmentComposeOptions, GetGeneratorOptions } from '@yeoman/types';
8 | import { toNamespace } from '@yeoman/namespace';
9 | import { type FileTransform, isFileTransform } from 'mem-fs';
10 | import { type MemFsEditorFile } from 'mem-fs-editor';
11 | import { isFilePending } from 'mem-fs-editor/state';
12 | import type { BaseOptions, ComposeOptions, GeneratorPipelineOptions, Priority, Task, TaskOptions } from '../types.js';
13 | import type Generator from '../index.js';
14 | import type BaseGeneratorImpl from '../generator.js';
15 |
16 | const debug = createDebug('yeoman:generator');
17 |
18 | type TaskStatus = {
19 | cancelled: boolean;
20 | timestamp: Date;
21 | };
22 |
23 | // Ensure a prototype method is a candidate run by default
24 | const methodIsValid = function (name: string) {
25 | return !name.startsWith('_') && name !== 'constructor';
26 | };
27 |
28 | export abstract class TasksMixin {
29 | // Queues map: generator's queue name => grouped-queue's queue name (custom name)
30 | readonly _queues!: Record;
31 |
32 | customLifecycle?: boolean;
33 | runningState?: { namespace: string; queueName: string; methodName: string };
34 | _taskStatus?: TaskStatus;
35 |
36 | /**
37 | * Register priorities for this generator
38 | */
39 | registerPriorities(this: BaseGeneratorImpl, priorities: Priority[]) {
40 | priorities = priorities.filter(({ priorityName, edit, ...priority }) => {
41 | if (edit) {
42 | const queue = this._queues[priorityName];
43 | if (!queue) {
44 | throw new Error(`Error editing priority ${priorityName}, not found`);
45 | }
46 |
47 | Object.assign(queue, { ...priority, edit: undefined });
48 | }
49 |
50 | return !edit;
51 | });
52 |
53 | const customPriorities = priorities.map(customPriority => ({ ...customPriority }));
54 | // Sort customPriorities, a referenced custom queue must be added before the one that reference it.
55 | customPriorities.sort((a, b) => {
56 | if (a.priorityName === b.priorityName) {
57 | throw new Error(`Duplicate custom queue ${a.priorityName}`);
58 | }
59 |
60 | if (a.priorityName === b.before) {
61 | return -1;
62 | }
63 |
64 | if (b.priorityName === a.before) {
65 | return 1;
66 | }
67 |
68 | return 0;
69 | });
70 |
71 | for (const customQueue of customPriorities) {
72 | customQueue.queueName = customQueue.queueName ?? `${this._namespace}#${customQueue.priorityName}`;
73 | debug(`Registering custom queue ${customQueue.queueName}`);
74 | this._queues[customQueue.priorityName] = customQueue;
75 |
76 | const beforeQueue = customQueue.before ? this._queues[customQueue.before].queueName : undefined;
77 | this.env.addPriority(customQueue.queueName, beforeQueue);
78 | }
79 | }
80 |
81 | /**
82 | * Schedule methods on a run queue.
83 | *
84 | * @param method: Method to be scheduled or object with function properties.
85 | * @param methodName Name of the method (task) to be scheduled.
86 | * @param queueName Name of the queue to be scheduled on.
87 | * @param reject Reject callback.
88 | */
89 | queueMethod(method: Task['method'], methodName: string, queueName: Task['queueName'], reject: Task['reject']): void;
90 | queueMethod(
91 | method: Record,
92 | methodName: string | Task['reject'],
93 | reject?: Task['reject'],
94 | ): void;
95 | queueMethod(
96 | this: BaseGeneratorImpl,
97 | method: Task['method'] | Record,
98 | methodName: string | Task['reject'],
99 | queueName?: Task['queueName'] | Task['reject'],
100 | reject?: Task['reject'],
101 | ): void {
102 | if (typeof queueName === 'function') {
103 | reject = queueName;
104 | queueName = undefined;
105 | } else {
106 | queueName = queueName ?? 'default';
107 | }
108 |
109 | if (typeof method !== 'function') {
110 | if (typeof methodName === 'function') {
111 | reject = methodName;
112 | methodName = undefined;
113 | }
114 |
115 | this.queueTaskGroup(method, {
116 | queueName: methodName,
117 | reject,
118 | });
119 | return;
120 | }
121 |
122 | this.queueTask({
123 | method,
124 | taskName: methodName as string,
125 | queueName,
126 | reject,
127 | });
128 | }
129 |
130 | /**
131 | * Schedule tasks from a group on a run queue.
132 | *
133 | * @param taskGroup: Object containing tasks.
134 | * @param taskOptions options.
135 | */
136 | queueTaskGroup(this: BaseGeneratorImpl, taskGroup: Record, taskOptions: TaskOptions): void {
137 | for (const task of this.extractTasksFromGroup(taskGroup, taskOptions)) {
138 | this.queueTask(task);
139 | }
140 | }
141 |
142 | /**
143 | * Get task sources property descriptors.
144 | */
145 | getTaskSourcesPropertyDescriptors(this: BaseGeneratorImpl): any {
146 | if (this.features.inheritTasks) {
147 | const queueNames = Object.keys(this._queues);
148 | let currentPrototype = Object.getPrototypeOf(this);
149 | let propertyDescriptors = [];
150 | while (currentPrototype) {
151 | propertyDescriptors.unshift(...Object.entries(Object.getOwnPropertyDescriptors(currentPrototype)));
152 | currentPrototype = Object.getPrototypeOf(currentPrototype);
153 | }
154 |
155 | const { taskPrefix = '' } = this.features;
156 | propertyDescriptors = propertyDescriptors.filter(
157 | ([name]) => name.startsWith(taskPrefix) && queueNames.includes(name.slice(taskPrefix.length)),
158 | );
159 | return Object.fromEntries(propertyDescriptors);
160 | }
161 |
162 | return Object.getOwnPropertyDescriptors(Object.getPrototypeOf(this));
163 | }
164 |
165 | /**
166 | * Extract tasks from a priority.
167 | *
168 | * @param name: The method name to schedule.
169 | */
170 | extractTasksFromPriority(this: BaseGeneratorImpl, name: string, taskOptions: TaskOptions = {}): Task[] {
171 | const priority = this._queues[name];
172 | taskOptions = {
173 | ...priority,
174 | cancellable: true,
175 | run: false,
176 | ...taskOptions,
177 | };
178 |
179 | if (taskOptions.auto && priority && priority.skip) {
180 | return [];
181 | }
182 |
183 | const { taskPrefix = this.features.taskPrefix ?? '' } = taskOptions;
184 | const propertyName = `${taskPrefix}${name}`;
185 | const property = taskOptions.taskOrigin
186 | ? Object.getOwnPropertyDescriptor(taskOptions.taskOrigin, propertyName)
187 | : this.getTaskSourcesPropertyDescriptors()[propertyName];
188 |
189 | if (!property) return [];
190 |
191 | const item: Task['method'] = property.value ?? property.get?.call(this);
192 |
193 | // Name points to a function; single task
194 | if (typeof item === 'function') {
195 | return [{ ...taskOptions, taskName: name, method: item }];
196 | }
197 |
198 | if (!item || !priority) {
199 | return [];
200 | }
201 |
202 | return this.extractTasksFromGroup(item, taskOptions);
203 | }
204 |
205 | /**
206 | * Extract tasks from group.
207 | *
208 | * @param group Task group.
209 | * @param taskOptions options.
210 | */
211 | extractTasksFromGroup(
212 | this: BaseGeneratorImpl,
213 | group: Record,
214 | taskOptions: TaskOptions,
215 | ): Task[] {
216 | return Object.entries(group)
217 | .map(([taskName, method]) => {
218 | if (typeof method !== 'function' || !methodIsValid(taskName)) return;
219 | return {
220 | ...taskOptions,
221 | method,
222 | taskName,
223 | };
224 | })
225 | .filter(Boolean) as Task[];
226 | }
227 |
228 | /**
229 | * Schedule a generator's method on a run queue.
230 | *
231 | * @param name: The method name to schedule.
232 | * @param taskOptions options.
233 | */
234 | queueOwnTask(this: BaseGeneratorImpl, name: string, taskOptions: TaskOptions): void {
235 | for (const task of this.extractTasksFromPriority(name, taskOptions)) this.queueTask(task);
236 | }
237 |
238 | /**
239 | * Get task names.
240 | */
241 | getTaskNames(this: BaseGeneratorImpl): string[] {
242 | const methods = Object.keys(this.getTaskSourcesPropertyDescriptors());
243 | let validMethods = methods.filter(method => methodIsValid(method));
244 | const { taskPrefix } = this.features;
245 |
246 | validMethods = taskPrefix
247 | ? validMethods.filter(method => method.startsWith(taskPrefix)).map(method => method.slice(taskPrefix.length))
248 | : validMethods.filter(method => !method.startsWith('#'));
249 |
250 | if (this.features.tasksMatchingPriority) {
251 | const queueNames = Object.keys(this._queues);
252 | validMethods = validMethods.filter(method => queueNames.includes(method));
253 | }
254 |
255 | return validMethods;
256 | }
257 |
258 | /**
259 | * Schedule every generator's methods on a run queue.
260 | */
261 | queueOwnTasks(this: BaseGeneratorImpl, taskOptions: TaskOptions): void {
262 | this._running = true;
263 | this._taskStatus = { cancelled: false, timestamp: new Date() };
264 |
265 | const validMethods = this.getTaskNames();
266 | if (validMethods.length === 0 && this._prompts.length === 0 && !this.customLifecycle) {
267 | throw new Error('This Generator is empty. Add at least one method for it to run.');
268 | }
269 |
270 | this.emit('before:queueOwnTasks');
271 |
272 | if (this._prompts.length > 0) {
273 | this.queueTask({
274 | method: async () => (this as any).prompt(this._prompts, this.config),
275 | taskName: 'Prompt registered questions',
276 | queueName: 'prompting',
277 | cancellable: true,
278 | });
279 |
280 | if (validMethods.length === 0) {
281 | this.queueTask({
282 | method: () => {
283 | (this as any).renderTemplate();
284 | },
285 | taskName: 'Empty generator: copy templates',
286 | queueName: 'writing',
287 | cancellable: true,
288 | });
289 | }
290 | }
291 |
292 | for (const methodName of validMethods) this.queueOwnTask(methodName, taskOptions);
293 |
294 | this.emit('queueOwnTasks');
295 | }
296 |
297 | /**
298 | * Schedule tasks on a run queue.
299 | *
300 | * @param task: Task to be queued.
301 | */
302 | queueTask(this: BaseGeneratorImpl, task: Task): void {
303 | const { queueName = 'default', taskName: methodName, run, once } = task;
304 |
305 | const { _taskStatus: taskStatus, _namespace: namespace } = this;
306 |
307 | debug(`Queueing ${namespace}#${methodName} with options %o`, { ...task, method: undefined });
308 | this.env.queueTask(queueName, async () => this.executeTask(task, undefined, taskStatus), {
309 | once: once ? methodName : undefined,
310 | startQueue: run,
311 | });
312 | }
313 |
314 | /**
315 | * Execute a task.
316 | *
317 | * @param task: Task to be executed.
318 | * @param args: Task arguments.
319 | * @param taskStatus.
320 | */
321 | async executeTask(
322 | this: BaseGeneratorImpl,
323 | task: Task,
324 | args = task.args,
325 | taskStatus: TaskStatus | undefined = this._taskStatus,
326 | ): Promise {
327 | const { reject, queueName = 'default', taskName: methodName, method } = task;
328 | const { _namespace: namespace } = this;
329 |
330 | debug(`Running ${namespace}#${methodName}`);
331 | this.emit(`method:${methodName}`);
332 | const taskCancelled = task.cancellable && taskStatus?.cancelled;
333 | if (taskCancelled) {
334 | return;
335 | }
336 | const [priorityName, priority] =
337 | Object.entries(this._queues).find(([_, queue]) => queue.queueName === queueName) ?? [];
338 | args ??= priority?.args ?? this.args;
339 | try {
340 | args = typeof args === 'function' ? args(this as any) : args;
341 | } catch (error: any) {
342 | throw new Error(`Error while building arguments for ${namespace}#${methodName}: ${error.message}`, {
343 | cause: error,
344 | });
345 | }
346 | this.runningState = { namespace, queueName, methodName };
347 | try {
348 | await method.apply(this, args);
349 | delete this.runningState;
350 | const eventName = `done$${namespace || 'unknownnamespace'}#${methodName}`;
351 | debug(`Done event ${eventName}`);
352 | this.env.emit(eventName, {
353 | namespace,
354 | generator: this,
355 | queueName,
356 | priorityName,
357 | });
358 | } catch (error: unknown) {
359 | const errorMessage = `An error occured while running ${namespace}#${methodName}`;
360 | if (this.log?.error) {
361 | this.log.error(errorMessage);
362 | } else {
363 | debug(errorMessage);
364 | }
365 |
366 | if (reject) {
367 | debug('Rejecting task promise, queue will continue normally');
368 | reject(error);
369 | return;
370 | }
371 |
372 | throw error;
373 | } finally {
374 | delete this.runningState;
375 | }
376 | }
377 |
378 | /**
379 | * Ignore cancellable tasks.
380 | */
381 | cancelCancellableTasks(this: BaseGeneratorImpl): void {
382 | this._running = false;
383 | // Task status references is registered at each running task
384 | if (this._taskStatus) {
385 | this._taskStatus.cancelled = true;
386 | }
387 |
388 | // Create a new task status.
389 | delete this._taskStatus;
390 | }
391 |
392 | /**
393 | * Queue generator tasks.
394 | */
395 | async queueTasks(this: BaseGeneratorImpl): Promise {
396 | const thisAny = this as any;
397 | const thisPrototype = Object.getPrototypeOf(thisAny);
398 |
399 | let beforeQueueCallback: (() => Promise) | undefined;
400 | if (this.features.taskPrefix) {
401 | // We want beforeQueue if beforeQueue belongs to the object or to the imediatelly extended class.
402 | beforeQueueCallback =
403 | Object.hasOwn(thisAny, 'beforeQueue') || Object.hasOwn(thisPrototype, 'beforeQueue')
404 | ? thisAny.beforeQueue
405 | : undefined;
406 | }
407 |
408 | if (!beforeQueueCallback) {
409 | // Fallback to _beforeQueue,
410 | beforeQueueCallback =
411 | Object.hasOwn(thisAny, '_beforeQueue') || Object.hasOwn(thisPrototype, '_beforeQueue')
412 | ? thisAny._beforeQueue
413 | : undefined;
414 | }
415 |
416 | if (beforeQueueCallback) {
417 | await beforeQueueCallback.call(this);
418 | }
419 |
420 | await this._queueTasks();
421 | }
422 |
423 | async _queueTasks(this: BaseGeneratorImpl): Promise {
424 | debug(`Queueing generator ${this._namespace} with generator version ${this.yoGeneratorVersion}`);
425 | this.queueOwnTasks({ auto: true });
426 | }
427 |
428 | /**
429 | * Start the generator again.
430 | */
431 | startOver(this: BaseGeneratorImpl, options?: BaseOptions): void {
432 | this.cancelCancellableTasks();
433 | if (options) {
434 | Object.assign(this.options, options);
435 | }
436 |
437 | this.queueOwnTasks({ auto: true });
438 | }
439 |
440 | /**
441 | * Compose this generator with another one.
442 | * @param generator The path to the generator module or an object (see examples)
443 | * @param args Arguments passed to the Generator
444 | * @param options The options passed to the Generator
445 | * @param immediately Boolean whether to queue the Generator immediately
446 | * @return The composed generator
447 | *
448 | * @example Using a peerDependency generator
449 | * await this.composeWith('bootstrap', { sass: true });
450 | *
451 | * @example Using a direct dependency generator
452 | * await this.composeWith(path.resolve(_dirname, 'generator-bootstrap/app/main.js'), { sass: true });
453 | *
454 | * @example Passing a Generator class
455 | * await this.composeWith({ Generator: MyGenerator, path: '../generator-bootstrap/app/main.js' }, { sass: true });
456 | */
457 | async composeWith(
458 | generator: string | { Generator: any; path: string },
459 | immediately?: boolean,
460 | ): Promise;
461 | async composeWith(generator: string[], immediately?: boolean): Promise;
462 | async composeWith(
463 | generator: string | { Generator: any; path: string },
464 | options: Partial>,
465 | immediately?: boolean,
466 | ): Promise;
467 | async composeWith(
468 | generator: string[],
469 | options: Partial>,
470 | immediately?: boolean,
471 | ): Promise;
472 | async composeWith(
473 | generator: string | { Generator: any; path: string },
474 | args: string[],
475 | options?: Partial>,
476 | immediately?: boolean,
477 | ): Promise;
478 | async composeWith(
479 | generator: string[],
480 | args: string[],
481 | options?: Partial>,
482 | immediately?: boolean,
483 | ): Promise;
484 | async composeWith(
485 | generator: string,
486 | options?: ComposeOptions,
487 | ): Promise;
488 |
489 | async composeWith(
490 | this: BaseGeneratorImpl,
491 | generator: string | string[] | { Generator: any; path: string },
492 | args?:
493 | | string[]
494 | | (Partial> | { arguments?: string[]; args?: string[] })
495 | | boolean
496 | | ComposeOptions,
497 | options?: Partial> | boolean,
498 | immediately = false,
499 | ): Promise {
500 | if (Array.isArray(generator)) {
501 | const generators: Generator[] = [];
502 | for (const each of generator) {
503 | generators.push(await this.composeWith(each, args as any, options as any));
504 | }
505 |
506 | return generators as any;
507 | }
508 |
509 | if (
510 | typeof args === 'object' &&
511 | ('generatorArgs' in args ||
512 | 'generatorOptions' in args ||
513 | 'skipEnvRegister' in args ||
514 | 'forceResolve' in args ||
515 | 'forwardOptions' in args)
516 | ) {
517 | return this.composeWithOptions(generator, args);
518 | }
519 |
520 | let parsedArgs: string[] = [];
521 | let parsedOptions: Partial = {};
522 | if (typeof args === 'boolean') {
523 | return this.composeWithOptions(generator, { schedule: !args });
524 | }
525 |
526 | if (Array.isArray(args)) {
527 | if (typeof options === 'object') {
528 | return this.composeWithOptions(generator, {
529 | generatorArgs: args,
530 | generatorOptions: options,
531 | schedule: !immediately,
532 | });
533 | }
534 |
535 | if (typeof options === 'boolean') {
536 | return this.composeWithOptions(generator, { generatorArgs: args, schedule: !options });
537 | }
538 |
539 | return this.composeWithOptions(generator, { generatorArgs: args });
540 | }
541 |
542 | if (typeof args === 'object') {
543 | parsedOptions = args as any;
544 | parsedArgs = (args as any).arguments ?? (args as any).args ?? [];
545 | if (typeof options === 'boolean') {
546 | immediately = options;
547 | }
548 |
549 | return this.composeWithOptions(generator, {
550 | generatorArgs: parsedArgs,
551 | generatorOptions: parsedOptions as any,
552 | schedule: !immediately,
553 | });
554 | }
555 |
556 | return this.composeWithOptions(generator);
557 | }
558 |
559 | private async composeWithOptions(
560 | this: BaseGeneratorImpl,
561 | generator: string | { Generator: any; path: string },
562 | options: ComposeOptions = {},
563 | ): Promise {
564 | const { forceResolve, skipEnvRegister = false, forwardOptions, ...composeOptions } = options;
565 | const optionsToForward = forwardOptions
566 | ? this.options
567 | : {
568 | skipInstall: this.options.skipInstall,
569 | skipCache: this.options.skipCache,
570 | skipLocalCache: this.options.skipLocalCache,
571 | };
572 |
573 | composeOptions.generatorOptions = {
574 | destinationRoot: this._destinationRoot,
575 | ...optionsToForward,
576 | ...composeOptions.generatorOptions,
577 | } as any;
578 |
579 | if (typeof generator === 'object') {
580 | let generatorFile;
581 | try {
582 | generatorFile = await this.resolveGeneratorPath(generator.path ?? generator.Generator.resolved);
583 | } catch {
584 | // Ignore error
585 | }
586 |
587 | const resolved = generatorFile ?? generator.path ?? generator.Generator.resolved;
588 |
589 | return this.composeLocallyWithOptions({ Generator: generator.Generator, resolved }, composeOptions);
590 | }
591 |
592 | if (skipEnvRegister || isAbsolute(generator) || generator.startsWith('.')) {
593 | const resolved = await this.resolveGeneratorPath(generator);
594 | return this.composeLocallyWithOptions({ resolved }, composeOptions);
595 | }
596 |
597 | const namespace = typeof generator === 'string' ? toNamespace(generator) : undefined;
598 | if (!namespace || forceResolve) {
599 | try {
600 | generator = await this.resolveGeneratorPath(generator);
601 | } catch {
602 | // Ignore error
603 | }
604 | }
605 |
606 | return this.env.composeWith(generator, composeOptions);
607 | }
608 |
609 | private async composeLocallyWithOptions(
610 | this: BaseGeneratorImpl,
611 | { Generator, resolved = Generator.resolved }: { Generator?: any; resolved: string },
612 | options: EnvironmentComposeOptions = {},
613 | ) {
614 | if (!resolved) {
615 | throw new Error('Generator path property is not a string');
616 | }
617 | const generatorNamespace = this.env.namespace(resolved);
618 | const findGenerator = async () => {
619 | const generatorImport = await import(pathToFileURL(resolved).href);
620 | const getFactory = (module: any) =>
621 | module.createGenerator ?? module.default?.createGenerator ?? module.default?.default?.createGenerator;
622 | const factory = getFactory(generatorImport);
623 | if (factory) {
624 | return factory(this.env);
625 | }
626 |
627 | return typeof generatorImport.default === 'function' ? generatorImport.default : generatorImport;
628 | };
629 |
630 | try {
631 | Generator = Generator ?? (await findGenerator());
632 | } catch {
633 | throw new Error('Missing Generator property');
634 | }
635 |
636 | Generator.namespace = generatorNamespace;
637 | Generator.resolved = resolved;
638 | return this.env.composeWith(Generator, options);
639 | }
640 |
641 | private async resolveGeneratorPath(this: BaseGeneratorImpl, maybePath: string) {
642 | // Allows to run a local generator without namespace.
643 | // Resolve the generator absolute path to current generator;
644 | const generatorFile = isAbsolute(maybePath) ? maybePath : pathResolve(dirname(this.resolved), maybePath);
645 | let generatorResolvedFile: string | undefined;
646 | try {
647 | const status = await stat(generatorFile);
648 | if (status.isFile()) {
649 | generatorResolvedFile = generatorFile;
650 | }
651 | } catch {
652 | // Ignore error
653 | }
654 |
655 | if (!generatorResolvedFile) {
656 | // Resolve the generator file.
657 | // Use import.resolve when stable.
658 | generatorResolvedFile = createRequire(import.meta.url).resolve(generatorFile);
659 | }
660 |
661 | return generatorResolvedFile;
662 | }
663 |
664 | async pipeline(
665 | this: BaseGeneratorImpl,
666 | options?: GeneratorPipelineOptions,
667 | ...transforms: Array>
668 | ) {
669 | if (isFileTransform(options)) {
670 | transforms = [options, ...transforms];
671 | options = {};
672 | }
673 |
674 | let filter: ((file: MemFsEditorFile) => boolean) | undefined;
675 | const { disabled, name, pendingFiles = true, filter: passedFilter, ...memFsPipelineOptions } = options ?? {};
676 | if (passedFilter && pendingFiles) {
677 | filter = (file: MemFsEditorFile) => isFilePending(file) && passedFilter(file);
678 | } else {
679 | filter = pendingFiles ? isFilePending : passedFilter;
680 | }
681 |
682 | const { env } = this;
683 | await env.adapter.progress(
684 | async ({ step }) =>
685 | env.sharedFs.pipeline(
686 | { filter, ...memFsPipelineOptions },
687 | ...transforms,
688 | new Transform({
689 | objectMode: true,
690 | transform(file: MemFsEditorFile, _encoding, callback) {
691 | step('Completed', relative(env.logCwd, file.path));
692 | callback(null, file);
693 | },
694 | }),
695 | ),
696 | { disabled, name },
697 | );
698 | }
699 |
700 | /**
701 | * Add a transform stream to the commit stream.
702 | *
703 | * Most usually, these transform stream will be Gulp plugins.
704 | *
705 | * @param streams An array of Transform stream
706 | * or a single one.
707 | * @return This generator
708 | */
709 | queueTransformStream(
710 | this: BaseGeneratorImpl,
711 | options?: GeneratorPipelineOptions & { priorityToQueue?: string },
712 | ...transforms: Array>
713 | ) {
714 | if (isFileTransform(options)) {
715 | transforms = [options, ...transforms];
716 | options = {};
717 | }
718 |
719 | const { priorityToQueue, ...pipelineOptions } = options!;
720 | const getQueueForPriority = (priority: string): string => {
721 | const found = this._queues[priority];
722 | if (!found) {
723 | throw new Error(`Could not find priority '${priority}'`);
724 | }
725 |
726 | return found.queueName ?? found.priorityName;
727 | };
728 |
729 | const queueName = priorityToQueue ? getQueueForPriority(priorityToQueue) : 'transform';
730 |
731 | this.queueTask({
732 | method: async () => this.pipeline(pipelineOptions, ...transforms),
733 | taskName: 'transformStream',
734 | queueName,
735 | });
736 |
737 | return this;
738 | }
739 | }
740 |
--------------------------------------------------------------------------------
/src/actions/package-json.ts:
--------------------------------------------------------------------------------
1 | import latestVersion from 'latest-version';
2 | import type { BaseGenerator } from '../generator.js';
3 |
4 | export class PackageJsonMixin {
5 | /**
6 | * Resolve the dependencies to be added to the package.json.
7 | */
8 | async _resolvePackageJsonDependencies(
9 | this: BaseGenerator,
10 | dependencies: string | string[] | Record,
11 | ): Promise> {
12 | if (typeof dependencies === 'string') {
13 | dependencies = [dependencies];
14 | } else if (typeof dependencies !== 'object') {
15 | throw new TypeError('resolvePackageJsonDependencies requires an object');
16 | }
17 |
18 | const depMap = Array.isArray(dependencies)
19 | ? Object.fromEntries(
20 | dependencies.map(dependency => {
21 | const lastIndex = dependency.lastIndexOf('@');
22 | if (lastIndex > 0) {
23 | const depName = dependency.slice(0, lastIndex);
24 | const version = dependency.slice(lastIndex + 1);
25 | return [depName, version];
26 | }
27 |
28 | return [dependency, undefined];
29 | }),
30 | )
31 | : dependencies;
32 |
33 | return Object.fromEntries(
34 | await Promise.all(
35 | // Make sure to convert empty string too
36 |
37 | Object.entries(depMap).map(async ([pkg, version]) => [pkg, version || (await latestVersion(pkg))]),
38 | ),
39 | );
40 | }
41 |
42 | /**
43 | * Add dependencies to the destination the package.json.
44 | *
45 | * Environment watches for package.json changes at `this.env.cwd`, and triggers an package manager install if it has been committed to disk.
46 | * If package.json is at a different folder, like a changed generator root, propagate it to the Environment like `this.env.cwd = this.destinationPath()`.
47 | *
48 | * @param dependencies
49 | */
50 | async addDependencies(
51 | this: BaseGenerator,
52 | dependencies: string | string[] | Record,
53 | ): Promise> {
54 | dependencies = await this._resolvePackageJsonDependencies(dependencies);
55 | this.packageJson.merge({ dependencies });
56 | return dependencies;
57 | }
58 |
59 | /**
60 | * Add dependencies to the destination the package.json.
61 | *
62 | * Environment watches for package.json changes at `this.env.cwd`, and triggers an package manager install if it has been committed to disk.
63 | * If package.json is at a different folder, like a changed generator root, propagate it to the Environment like `this.env.cwd = this.destinationPath()`.
64 | *
65 | * @param dependencies
66 | */
67 | async addDevDependencies(
68 | this: BaseGenerator,
69 | devDependencies: string | string[] | Record,
70 | ): Promise> {
71 | devDependencies = await this._resolvePackageJsonDependencies(devDependencies);
72 | this.packageJson.merge({ devDependencies });
73 | return devDependencies;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/actions/spawn-command.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type Options as ExecaOptions,
3 | type ResultPromise,
4 | type SyncOptions,
5 | type SyncResult,
6 | execa,
7 | execaCommand,
8 | execaCommandSync,
9 | execaSync,
10 | } from 'execa';
11 | import type { BaseGenerator } from '../generator.js';
12 |
13 | export class SpawnCommandMixin {
14 | /**
15 | * Normalize a command across OS and spawn it (asynchronously).
16 | *
17 | * @param command program to execute
18 | * @param opt execa options options
19 | * @see https://github.com/sindresorhus/execa#execacommandcommand-options
20 | */
21 | spawnCommand(
22 | this: BaseGenerator,
23 | command: string,
24 | opt?: OptionsType,
25 | ): ResultPromise {
26 | opt = { cwd: this.destinationRoot(), ...opt } as OptionsType;
27 | return execaCommand(command, opt) as any;
28 | }
29 |
30 | /**
31 | * Normalize a command across OS and spawn it (asynchronously).
32 | *
33 | * @param command program to execute
34 | * @param args list of arguments to pass to the program
35 | * @param opt execa options options
36 | * @see https://github.com/sindresorhus/execa#execafile-arguments-options
37 | */
38 | spawn(
39 | this: BaseGenerator,
40 | command: string,
41 | args?: readonly string[],
42 | opt?: OptionsType,
43 | ): ResultPromise {
44 | opt = { cwd: this.destinationRoot(), ...opt } as OptionsType;
45 | return execa(command, args, opt) as any;
46 | }
47 |
48 | /**
49 | * Normalize a command across OS and spawn it (synchronously).
50 | *
51 | * @param command program to execute
52 | * @param opt execa options options
53 | * @see https://github.com/sindresorhus/execa#execacommandsynccommand-options
54 | */
55 | spawnCommandSync(
56 | this: BaseGenerator,
57 | command: string,
58 | opt?: OptionsType,
59 | ): SyncResult {
60 | opt = { cwd: this.destinationRoot(), ...opt } as OptionsType;
61 | return execaCommandSync(command, opt);
62 | }
63 |
64 | /**
65 | * Normalize a command across OS and spawn it (synchronously).
66 | *
67 | * @param command program to execute
68 | * @param args list of arguments to pass to the program
69 | * @param opt execa options options
70 | * @see https://github.com/sindresorhus/execa#execafile-arguments-options
71 | */
72 | spawnSync(
73 | this: BaseGenerator,
74 | command: string,
75 | args?: readonly string[],
76 | opt?: OptionsType,
77 | ): SyncResult {
78 | opt = { cwd: this.destinationRoot(), ...opt } as OptionsType;
79 | return execaSync(command, args, opt);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/actions/user.ts:
--------------------------------------------------------------------------------
1 | import { type SimpleGit } from 'simple-git';
2 |
3 | class GitUtil {
4 | #parent: { get simpleGit(): SimpleGit };
5 |
6 | constructor(parent: { get simpleGit(): SimpleGit }) {
7 | this.#parent = parent;
8 | }
9 |
10 | /**
11 | * Retrieves user's name from Git in the global scope or the project scope
12 | * (it'll take what Git will use in the current context)
13 | * @return {Promise} configured git name or undefined
14 | */
15 | async name(): Promise {
16 | const { value } = await this.#parent.simpleGit.getConfig('user.name');
17 | return value ?? undefined;
18 | }
19 |
20 | /**
21 | * Retrieves user's email from Git in the global scope or the project scope
22 | * (it'll take what Git will use in the current context)
23 | * @return {Promise} configured git email or undefined
24 | */
25 | async email(): Promise {
26 | const { value } = await this.#parent.simpleGit.getConfig('user.email');
27 | return value ?? undefined;
28 | }
29 | }
30 |
31 | export abstract class GitMixin {
32 | _git?: GitUtil;
33 |
34 | get git(): GitUtil {
35 | if (!this._git) {
36 | this._git = new GitUtil(this as any);
37 | }
38 |
39 | return this._git;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const DESTINATION_ROOT_CHANGE_EVENT = 'destinationRootChange';
2 |
3 | export const requiredEnvironmentVersion = '4.0.0-rc.0';
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import process from 'node:process';
2 | import { type SimpleGit, simpleGit } from 'simple-git';
3 | import { BaseGenerator } from './generator.js';
4 | import type { BaseFeatures, BaseOptions } from './types.js';
5 | import { DESTINATION_ROOT_CHANGE_EVENT } from './constants.js';
6 |
7 | export type * from './types.js';
8 | export type * from './questions.js';
9 | export type * from './util/storage.js';
10 | export { default as Storage } from './util/storage.js';
11 |
12 | export default class Generator<
13 | C extends Record = Record,
14 | O extends BaseOptions = BaseOptions,
15 | F extends BaseFeatures = BaseFeatures,
16 | > extends BaseGenerator {
17 | _simpleGit?: SimpleGit;
18 |
19 | constructor(...args: any[]) {
20 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
21 | // @ts-expect-error
22 | super(...args);
23 |
24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
25 | // @ts-expect-error
26 | this._queues = {};
27 |
28 | // Add original queues.
29 | for (const queue of BaseGenerator.queues) {
30 | this._queues[queue] = { priorityName: queue, queueName: queue };
31 | }
32 |
33 | // Add custom queues
34 | if (Array.isArray(this._customPriorities)) {
35 | this.registerPriorities(this._customPriorities);
36 | }
37 | }
38 |
39 | get simpleGit(): SimpleGit {
40 | if (!this._simpleGit) {
41 | this._simpleGit = simpleGit({ baseDir: this.destinationPath() }).env({
42 | ...process.env,
43 |
44 | LANG: 'en',
45 | });
46 | this.on(DESTINATION_ROOT_CHANGE_EVENT, () => {
47 | this._simpleGit = undefined;
48 | });
49 | }
50 |
51 | return this._simpleGit;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/questions.d.ts:
--------------------------------------------------------------------------------
1 | import type { PromptAnswers, PromptQuestion as PromptQuestionApi } from '@yeoman/types';
2 | import type Storage from './util/storage.js';
3 |
4 | export type { PromptAnswers } from '@yeoman/types';
5 |
6 | /**
7 | * Represents a question.
8 | */
9 | export type PromptQuestion = PromptQuestionApi & {
10 | name: string;
11 |
12 | /**
13 | * The storage to store the answer.
14 | */
15 | storage?: Storage;
16 |
17 | /**
18 | * A value indicating whether to store the user's previous answer for others projects.
19 | */
20 | store?: boolean;
21 | };
22 |
23 | /**
24 | * Provides options for registering a prompt.
25 | */
26 | export type QuestionRegistrationOptions = PromptQuestion & {
27 | /**
28 | * A value indicating whether an option should be exported for this question.
29 | */
30 | exportOption?: boolean | Record;
31 | };
32 |
33 | /**
34 | * Provides a set of questions.
35 | */
36 | export type PromptQuestions = PromptQuestion | Array>; // | Observable>;
37 |
--------------------------------------------------------------------------------
/src/types-utils.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * https://github.com/microsoft/TypeScript/issues/14107#issuecomment-1146738780
3 | */
4 | type OverloadProps = Pick;
5 |
6 | type OverloadUnionRecursive = TOverload extends (
7 | ...args: infer TArgs
8 | ) => infer TReturn
9 | ? // Prevent infinite recursion by stopping recursion when TPartialOverload
10 | // has accumulated all of the TOverload signatures.
11 | TPartialOverload extends TOverload
12 | ? never
13 | :
14 | | OverloadUnionRecursive<
15 | TPartialOverload & TOverload,
16 | TPartialOverload & ((...args: TArgs) => TReturn) & OverloadProps
17 | >
18 | | ((...args: TArgs) => TReturn)
19 | : never;
20 |
21 | type OverloadUnion any> = Exclude<
22 | OverloadUnionRecursive<
23 | // The "() => never" signature must be hoisted to the "front" of the
24 | // intersection, for two reasons: a) because recursion stops when it is
25 | // encountered, and b) it seems to prevent the collapse of subsequent
26 | // "compatible" signatures (eg. "() => void" into "(a?: 1) => void"),
27 | // which gives a direct conversion to a union.
28 | (() => never) & TOverload
29 | >,
30 | TOverload extends () => never ? never : () => never
31 | >;
32 |
33 | // Inferring a union of parameter tuples or return types is now possible.
34 | export type OverloadParameters any> = Parameters>;
35 | export type OverloadReturnType any> = ReturnType>;
36 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BaseGenerator,
3 | ComposeOptions as EnvironmentComposeOptions,
4 | GeneratorFeatures as FeaturesApi,
5 | GeneratorOptions as OptionsApi,
6 | ProgressOptions,
7 | } from '@yeoman/types';
8 | import type { PipelineOptions } from 'mem-fs';
9 | import type { MemFsEditorFile } from 'mem-fs-editor';
10 | import type { JsonValue } from 'type-fest';
11 | import type Storage from './util/storage.js';
12 | import type Generator from './index.js';
13 |
14 | export type StorageValue = JsonValue;
15 | export type GeneratorPipelineOptions = PipelineOptions & ProgressOptions & { pendingFiles?: boolean };
16 |
17 | /**
18 | * Queue options.
19 | */
20 | type QueueOptions = {
21 | /** Name of the queue. */
22 | queueName?: string;
23 |
24 | /** Execute only once by namespace and taskName. */
25 | once?: boolean;
26 |
27 | /** Run the queue if not running yet. */
28 | run?: boolean;
29 |
30 | /** Edit a priority */
31 | edit?: boolean;
32 |
33 | /** Queued manually only */
34 | skip?: boolean;
35 |
36 | /** Arguments to pass to tasks */
37 | args?: any[] | ((generator: Generator) => any[]);
38 | };
39 |
40 | /**
41 | * Task options.
42 | */
43 | export type TaskOptions = QueueOptions & {
44 | /** Reject callback. */
45 | reject?: (error: unknown) => void;
46 |
47 | taskPrefix?: string;
48 |
49 | auto?: boolean;
50 |
51 | taskOrigin?: any;
52 |
53 | cancellable?: boolean;
54 | };
55 |
56 | /**
57 | * Priority object.
58 | */
59 | export type Priority = QueueOptions & {
60 | /** Name of the priority. */
61 | priorityName: string;
62 | /** The queue which this priority should be added before. */
63 | before?: string;
64 | };
65 |
66 | /**
67 | * Complete Task object.
68 | */
69 | export type Task = TaskOptions & {
70 | /** Function to be queued. */
71 |
72 | method: (this: TaskContext, ...args: any[]) => unknown | Promise;
73 |
74 | /** Name of the task. */
75 | taskName: string;
76 | };
77 |
78 | export type BaseFeatures = FeaturesApi & {
79 | /** The Generator instance unique identifier. The Environment will ignore duplicated identifiers. */
80 | uniqueBy?: string;
81 |
82 | /** UniqueBy calculation method */
83 | unique?: true | 'argument' | 'namespace';
84 |
85 | /** Only queue methods that matches a priority. */
86 | tasksMatchingPriority?: boolean;
87 |
88 | /** Tasks methods starts with prefix. Allows api methods (non tasks) without prefix. */
89 | taskPrefix?: string;
90 |
91 | /** Provides a custom install task. Environment built-in task will not be executed */
92 | customInstallTask?: boolean | ((...args: any[]) => void | Promise);
93 |
94 | /** Provides a custom commit task. */
95 | customCommitTask?: boolean | ((...args: any[]) => void | Promise);
96 |
97 | /** Disable args/options parsing. Whenever options/arguments are provided parsed like using commander based parsing. */
98 | skipParseOptions?: boolean;
99 |
100 | /** Custom priorities for more fine tuned workflows. */
101 | customPriorities?: Priority[];
102 |
103 | /** Inherit tasks from parent prototypes, implies tasksMatchingPriority */
104 | inheritTasks?: boolean;
105 | };
106 |
107 | export type BaseOptions = OptionsApi & {
108 | destinationRoot?: string;
109 |
110 | skipInstall?: boolean;
111 |
112 | skipCheckEnv?: boolean;
113 |
114 | ignoreVersionCheck?: boolean;
115 |
116 | askAnswered?: boolean;
117 |
118 | localConfigOnly?: boolean;
119 |
120 | skipCache?: boolean;
121 |
122 | skipLocalCache?: boolean;
123 |
124 | description?: string;
125 |
126 | /** @deprecated moved to features */
127 | skipParseOptions?: boolean;
128 |
129 | /** @deprecated moved to features */
130 | customPriorities?: Priority[];
131 | };
132 |
133 | export type ArgumentSpec = {
134 | name: string;
135 |
136 | /** Description for the argument. */
137 | description?: string;
138 |
139 | /** A value indicating whether the argument is required. */
140 | required?: boolean;
141 |
142 | /** A value indicating whether the argument is optional. */
143 | optional?: boolean;
144 |
145 | /** The type of the argument. */
146 | type: typeof String | typeof Number | typeof Array | typeof Object;
147 |
148 | /** The default value of the argument. */
149 | default?: any;
150 | };
151 |
152 | export type CliOptionSpec = {
153 | name: string;
154 |
155 | /** The type of the option. */
156 | type: typeof Boolean | typeof String | typeof Number | ((opt: string) => any);
157 |
158 | required?: boolean;
159 |
160 | /** The option name alias (example `-h` and --help`). */
161 | alias?: string;
162 |
163 | /** The default value. */
164 | default?: any;
165 |
166 | /** The description for the option. */
167 | description?: string;
168 |
169 | /** A value indicating whether the option should be hidden from the help output. */
170 | hide?: boolean;
171 |
172 | /** The storage to persist the option */
173 | storage?: string | Storage;
174 | };
175 |
176 | export type ComposeOptions = EnvironmentComposeOptions & {
177 | skipEnvRegister?: boolean;
178 | forceResolve?: boolean;
179 | forwardOptions?: boolean;
180 | };
181 |
--------------------------------------------------------------------------------
/src/util/deprecate.js:
--------------------------------------------------------------------------------
1 | import { template } from 'lodash-es';
2 | import chalk from 'chalk';
3 |
4 | const deprecate = (message, fn) => {
5 | return function (...args) {
6 | deprecate.log(message);
7 | return Reflect.apply(fn, this, args);
8 | };
9 | };
10 |
11 | deprecate.log = message => {
12 | console.log(chalk.yellow('(!) ') + message);
13 | };
14 |
15 | deprecate.object = (message, object) => {
16 | const messageTpl = template(message);
17 | const mirror = [];
18 |
19 | for (const name of Object.keys(object)) {
20 | const func = object[name];
21 |
22 | if (typeof func !== 'function') {
23 | mirror[name] = func;
24 | continue;
25 | }
26 |
27 | mirror[name] = deprecate(messageTpl({ name }), func);
28 | }
29 |
30 | return mirror;
31 | };
32 |
33 | deprecate.property = (message, object, property) => {
34 | const original = object[property];
35 | Object.defineProperty(object, property, {
36 | get() {
37 | deprecate.log(message);
38 | return original;
39 | },
40 | });
41 | };
42 |
43 | export default deprecate;
44 |
--------------------------------------------------------------------------------
/src/util/prompt-suggestion.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import type { PromptAnswers, PromptQuestion } from '../questions.js';
3 | import type Storage from './storage.js';
4 |
5 | const getChoices = (question: PromptQuestion) => {
6 | if (question.type === 'list') {
7 | return question.choices;
8 | }
9 |
10 | if (question.type === 'checkbox') {
11 | return question.choices;
12 | }
13 |
14 | if (question.type === 'expand') {
15 | return question.choices;
16 | }
17 |
18 | if (question.type === 'rawlist') {
19 | return question.choices;
20 | }
21 |
22 | return;
23 | };
24 |
25 | /**
26 | * Returns the default value for a checkbox
27 | *
28 | * @param question Inquirer prompt item
29 | * @param defaultValue The stored default value
30 | * @return Default value to set
31 | * @private
32 | */
33 | const getCheckboxDefault = (question: any, defaultValue: any) => {
34 | // For simplicity we uncheck all boxes and use .default to set the active ones
35 | for (const choice of question.choices) {
36 | if (typeof choice === 'object') {
37 | choice.checked = false;
38 | }
39 | }
40 |
41 | return defaultValue;
42 | };
43 |
44 | /**
45 | * Returns the default value for a list
46 | *
47 | * @param question Inquirer prompt item
48 | * @param defaultValue The stored default value
49 | * @return Default value to set
50 | * @private
51 | */
52 | const getListDefault = (question: any, defaultValue: any) => {
53 | const choiceValues = question.choices.map((choice: any) => (typeof choice === 'object' ? choice.value : choice));
54 | return choiceValues.indexOf(defaultValue);
55 | };
56 |
57 | /**
58 | * Return true if the answer should be store in
59 | * the global store, otherwise false
60 | *
61 | * @param question Inquirer prompt item
62 | * @param answer The inquirer answer
63 | * @param storeAll Should store default values
64 | * @return Answer to be stored
65 | * @private
66 | */
67 | const storeListAnswer = (question: any, answer: PromptAnswers, storeAll: boolean) => {
68 | const choiceValues = question.choices.map((choice: any) => {
69 | if (Object.prototype.hasOwnProperty.call(choice, 'value')) {
70 | return choice.value;
71 | }
72 |
73 | return choice;
74 | });
75 |
76 | const choiceIndex = choiceValues.indexOf(answer);
77 |
78 | // Check if answer is not equal to default value
79 | if (storeAll || question.default !== choiceIndex) {
80 | return true;
81 | }
82 |
83 | return false;
84 | };
85 |
86 | /**
87 | * Return true if the answer should be store in
88 | * the global store, otherwise false
89 | *
90 | * @param question Inquirer prompt item
91 | * @param answer The inquirer answer
92 | * @param storeAll Should store default values
93 | * @return Answer to be stored
94 | * @private
95 | */
96 | const storeAnswer = (question: any, answer: PromptAnswers, storeAll: boolean) => {
97 | // Check if answer is not equal to default value or is undefined
98 | if (answer !== undefined && (storeAll || question.default !== answer)) {
99 | return true;
100 | }
101 |
102 | return false;
103 | };
104 |
105 | /**
106 | * Prefill the defaults with values from the global store
107 | *
108 | * @param store `.yo-rc-global` global config
109 | * @param questions Original prompt questions
110 | * @return Prompt questions array with prefilled values.
111 | */
112 | export const prefillQuestions = (
113 | store: Storage,
114 | questions: Array>,
115 | ) => {
116 | assert(store, 'A store parameter is required');
117 | assert(questions, 'A questions parameter is required');
118 |
119 | const promptValues = store.get('promptValues') ?? {};
120 |
121 | questions = [questions].flat();
122 |
123 | // Write user defaults back to prompt
124 | return questions.map(question => {
125 | if (question.store !== true) {
126 | return question;
127 | }
128 |
129 | const storedValue = promptValues[question.name as string];
130 |
131 | if (storedValue === undefined || typeof getChoices(question) === 'function') {
132 | // Do not override prompt default when question.choices is a function,
133 | // since can't guarantee that the `storedValue` will even be in the returned choices
134 | return question;
135 | }
136 |
137 | // Override prompt default with the user's default
138 | switch (question.type) {
139 | case 'rawlist':
140 | case 'expand': {
141 | question.default = getListDefault(question, storedValue);
142 | break;
143 | }
144 |
145 | case 'checkbox': {
146 | question.default = getCheckboxDefault(question, storedValue);
147 | break;
148 | }
149 |
150 | default: {
151 | question.default = storedValue;
152 | break;
153 | }
154 | }
155 |
156 | return question;
157 | });
158 | };
159 |
160 | /**
161 | * Store the answers in the global store
162 | *
163 | * @param store `.yo-rc-global` global config
164 | * @param questions Original prompt questions
165 | * @param answers The inquirer answers
166 | * @param storeAll Should store default values
167 | */
168 | export const storeAnswers = (store: Storage, questions: any, answers: PromptAnswers, storeAll: boolean) => {
169 | assert(store, 'A store parameter is required');
170 | assert(answers, 'A answers parameter is required');
171 | assert(questions, 'A questions parameter is required');
172 | assert.ok(typeof answers === 'object', 'answers must be a object');
173 |
174 | storeAll = storeAll || false;
175 | const promptValues = store.get('promptValues') ?? {};
176 |
177 | questions = [questions].flat();
178 |
179 | for (const question of questions) {
180 | if (question.store !== true) {
181 | return;
182 | }
183 |
184 | let saveAnswer;
185 | const key = question.name;
186 | const answer = answers[key];
187 |
188 | switch (question.type) {
189 | case 'rawlist':
190 | case 'expand': {
191 | saveAnswer = storeListAnswer(question, answer, storeAll);
192 | break;
193 | }
194 |
195 | default: {
196 | saveAnswer = storeAnswer(question, answer, storeAll);
197 | break;
198 | }
199 | }
200 |
201 | if (saveAnswer) {
202 | promptValues[key] = answer;
203 | }
204 | }
205 |
206 | if (Object.keys(promptValues).length > 0) {
207 | store.set('promptValues', promptValues);
208 | }
209 | };
210 |
--------------------------------------------------------------------------------
/src/util/storage.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import { cloneDeep, get, merge, set, defaults as setDefaults } from 'lodash-es';
3 | import sortKeys from 'sort-keys';
4 | import type { Get } from 'type-fest';
5 | import type { MemFsEditor } from 'mem-fs-editor';
6 | import type { StorageValue } from '../types.js';
7 |
8 | /**
9 | * Proxy handler for Storage
10 | */
11 | const proxyHandler: ProxyHandler = {
12 | get(storage: Storage, property: string, _receiver: any): StorageValue {
13 | return storage.get(property);
14 | },
15 | set(storage: Storage, property: string, value: any, _receiver: any): boolean {
16 | if (typeof property === 'string') {
17 | storage.set(property, value);
18 | return true;
19 | }
20 |
21 | return false;
22 | },
23 | ownKeys(storage: Storage) {
24 | return Reflect.ownKeys(storage._store);
25 | },
26 | has(storage: Storage, prop: string) {
27 | return storage.get(prop) !== undefined;
28 | },
29 | getOwnPropertyDescriptor(storage: Storage, key: string): PropertyDescriptor {
30 | return {
31 | get: () => this.get!(storage, key, null),
32 | enumerable: true,
33 | configurable: true,
34 | };
35 | },
36 | };
37 |
38 | export type StorageOptions = {
39 | name?: string;
40 | /**
41 | * Set true to treat name as a lodash path.
42 | */
43 | lodashPath?: boolean;
44 | /**
45 | * Set true to disable json object cache.
46 | */
47 | disableCache?: boolean;
48 | /**
49 | * Set true to cleanup cache for every fs change.
50 | */
51 | disableCacheByFile?: boolean;
52 | /**
53 | * Set true to write sorted json.
54 | */
55 | sorted?: boolean;
56 | };
57 |
58 | /**
59 | * Storage instances handle a json file where Generator authors can store data.
60 | *
61 | * The `Generator` class instantiate the storage named `config` by default.
62 | *
63 | * @constructor
64 | * @param name The name of the new storage (this is a namespace)
65 | * @param fs A mem-fs editor instance
66 | * @param configPath The filepath used as a storage.
67 | *
68 | * @example
69 | * class extend Generator {
70 | * writing: function() {
71 | * this.config.set('coffeescript', false);
72 | * }
73 | * }
74 | */
75 | class Storage = Record> {
76 | path: string;
77 | name?: string;
78 | fs: MemFsEditor;
79 | indent: number;
80 | lodashPath: boolean;
81 | disableCache: boolean;
82 | disableCacheByFile: boolean;
83 | sorted: boolean;
84 | existed: boolean;
85 | _cachedStore?: StorageRecord;
86 |
87 | constructor(name: string | undefined, fs: MemFsEditor, configPath: string, options?: StorageOptions);
88 | constructor(fs: MemFsEditor, configPath: string, options?: StorageOptions);
89 | constructor(
90 | name: string | MemFsEditor | undefined,
91 | fs: MemFsEditor | string,
92 | configPath?: string | StorageOptions,
93 | options: StorageOptions = {},
94 | ) {
95 | let editor: MemFsEditor | undefined;
96 | let actualName: string | undefined;
97 | let actualConfigPath: string | undefined;
98 | let actualOptions: StorageOptions = options;
99 |
100 | if (typeof name === 'string') {
101 | actualName = name;
102 | } else if (typeof name === 'object') {
103 | editor = name;
104 | }
105 |
106 | if (typeof fs === 'string') {
107 | actualConfigPath = fs;
108 | } else {
109 | editor = fs;
110 | }
111 |
112 | if (typeof configPath === 'string') {
113 | actualConfigPath = configPath;
114 | } else if (typeof configPath === 'object') {
115 | actualOptions = configPath;
116 | }
117 |
118 | if (!editor) {
119 | throw new Error(`Check parameters`);
120 | }
121 |
122 | assert(actualConfigPath, 'A config filepath is required to create a storage');
123 |
124 | this.path = actualConfigPath;
125 | this.name = actualName ?? actualOptions.name;
126 | this.fs = editor;
127 | this.indent = 2;
128 | this.lodashPath = actualOptions.lodashPath ?? false;
129 | this.disableCache = actualOptions.disableCache ?? false;
130 | this.disableCacheByFile = actualOptions.disableCacheByFile ?? false;
131 | this.sorted = actualOptions.sorted ?? false;
132 |
133 | this.existed = Object.keys(this._store).length > 0;
134 |
135 | this.fs.store.on('change', filename => {
136 | // At mem-fs 1.1.3 filename is not passed to the event.
137 | if (this.disableCacheByFile || (filename && filename !== this.path)) {
138 | return;
139 | }
140 |
141 | delete this._cachedStore;
142 | });
143 | }
144 |
145 | /**
146 | * @protected
147 | * @return the store content
148 | */
149 | readContent(): StorageRecord {
150 | const content = this.fs.readJSON(this.path, {});
151 | if (!content || typeof content !== 'object' || Array.isArray(content)) {
152 | throw new Error(`${this.path} is not a valid Storage`);
153 | }
154 |
155 | return content;
156 | }
157 |
158 | /**
159 | * @protected
160 | * @return the store content
161 | */
162 | writeContent(fullStore: StorageValue): string {
163 | return this.fs.writeJSON(this.path, fullStore, undefined, this.indent);
164 | }
165 |
166 | /**
167 | * Return the current store as JSON object
168 | * @return the store content
169 | * @private
170 | */
171 | get _store(): StorageRecord {
172 | const store = this._cachedStore ?? this.readContent();
173 | if (!this.disableCache) {
174 | this._cachedStore = store;
175 | }
176 |
177 | if (!this.name) {
178 | return store;
179 | }
180 |
181 | return ((this.lodashPath ? get(store, this.name) : store[this.name]) ?? {}) as StorageRecord;
182 | }
183 |
184 | /**
185 | * Persist a configuration to disk
186 | * @param val - current configuration values
187 | * @private
188 | */
189 | _persist(value: StorageRecord) {
190 | if (this.sorted) {
191 | value = sortKeys(value, { deep: true });
192 | }
193 |
194 | let fullStore: StorageRecord;
195 | if (this.name) {
196 | fullStore = this.readContent();
197 | if (this.lodashPath) {
198 | set(fullStore, this.name, value);
199 | } else {
200 | (fullStore as any)[this.name] = value;
201 | }
202 | } else {
203 | fullStore = value;
204 | }
205 |
206 | this.writeContent(fullStore);
207 | }
208 |
209 | /**
210 | * Save a new object of values
211 | */
212 | save(): void {
213 | this._persist(this._store);
214 | }
215 |
216 | /**
217 | * Get a stored value
218 | * @param key The key under which the value is stored.
219 | * @return The stored value. Any JSON valid type could be returned
220 | */
221 | get(key: Key): StorageRecord[Key] {
222 | return this._store[key];
223 | }
224 |
225 | /**
226 | * Get a stored value from a lodash path
227 | * @param path The path under which the value is stored.
228 | * @return The stored value. Any JSON valid type could be returned
229 | */
230 | getPath(path: KeyPath): Get {
231 | return get(this._store, path);
232 | }
233 |
234 | /**
235 | * Get all the stored values
236 | * @return key-value object
237 | */
238 | getAll(): StorageRecord {
239 | return cloneDeep(this._store);
240 | }
241 |
242 | /**
243 | * Assign a key to a value and schedule a save.
244 | * @param key The key under which the value is stored
245 | * @param val Any valid JSON type value (String, Number, Array, Object).
246 | * @return val Whatever was passed in as val.
247 | */
248 | set(value: Partial): StorageRecord;
249 | set(
250 | key: Key,
251 | value?: Value,
252 | ): Value | undefined;
253 | set(key: string | number | Partial, value?: StorageValue): StorageRecord | StorageValue | undefined {
254 | const store = this._store;
255 | let ret: StorageValue | StorageValue | undefined;
256 |
257 | if (typeof key === 'object') {
258 | ret = Object.assign(store, key);
259 | } else if (typeof key === 'string' || typeof key === 'number') {
260 | (store as any)[key] = value;
261 | ret = value;
262 | } else {
263 | throw new TypeError(`key not supported ${typeof key}`);
264 | }
265 |
266 | this._persist(store);
267 | return ret;
268 | }
269 |
270 | /**
271 | * Assign a lodash path to a value and schedule a save.
272 | * @param path The key under which the value is stored
273 | * @param val Any valid JSON type value (String, Number, Array, Object).
274 | * @return val Whatever was passed in as val.
275 | */
276 | setPath(path: KeyPath, value: Get): Get {
277 | assert(typeof value !== 'function', "Storage value can't be a function");
278 |
279 | const store = this._store;
280 | set(store, path, value);
281 | this._persist(store);
282 | return value;
283 | }
284 |
285 | /**
286 | * Delete a key from the store and schedule a save.
287 | * @param key The key under which the value is stored.
288 | */
289 | delete(key: keyof StorageRecord): void {
290 | const store = this._store;
291 |
292 | delete store[key];
293 | this._persist(store);
294 | }
295 |
296 | /**
297 | * Setup the store with defaults value and schedule a save.
298 | * If keys already exist, the initial value is kept.
299 | * @param defaults Key-value object to store.
300 | * @return val Returns the merged options.
301 | */
302 | defaults(defaults: Partial): StorageRecord {
303 | assert(typeof defaults === 'object', 'Storage `defaults` method only accept objects');
304 | const store = setDefaults({}, this._store, defaults);
305 | this._persist(store);
306 | return this.getAll();
307 | }
308 |
309 | /**
310 | * @param defaults Key-value object to store.
311 | * @return val Returns the merged object.
312 | */
313 | merge(source: Partial): StorageRecord {
314 | assert(typeof source === 'object', 'Storage `merge` method only accept objects');
315 | const value = merge({}, this._store, source);
316 | this._persist(value);
317 | return this.getAll();
318 | }
319 |
320 | /**
321 | * Create a child storage.
322 | * @param path - relative path of the key to create a new storage.
323 | * Some paths need to be escaped. Eg: ["dotted.path"]
324 | * @return Returns a new Storage.
325 | */
326 | createStorage>(path: string): Storage;
327 | createStorage(path: KeyPath): Storage>;
328 | createStorage(path: string): Storage {
329 | const childName = this.name ? `${this.name}.${path}` : path;
330 | return new Storage(childName, this.fs, this.path, { lodashPath: true });
331 | }
332 |
333 | /**
334 | * Creates a proxy object.
335 | * @return proxy.
336 | */
337 | createProxy(): StorageRecord {
338 | return new Proxy(this, proxyHandler) as unknown as StorageRecord;
339 | }
340 | }
341 |
342 | export default Storage;
343 |
--------------------------------------------------------------------------------
/test/deprecate.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-expressions */
2 | import assert from 'node:assert';
3 | import { afterEach, beforeEach, describe, it } from 'vitest';
4 | import chalk from 'chalk';
5 | import sinon, { type SinonSpy } from 'sinon';
6 | import deprecate from '../src/util/deprecate.js';
7 |
8 | type SimpleObject = {
9 | foo: number;
10 | functionInObj: (someValue: number) => number;
11 | };
12 |
13 | describe('deprecate()', () => {
14 | let fakeConsoleLog: SinonSpy;
15 | beforeEach(() => {
16 | fakeConsoleLog = sinon.fake();
17 | sinon.replace(console, 'log', fakeConsoleLog);
18 | });
19 |
20 | afterEach(() => {
21 | sinon.restore();
22 | });
23 |
24 | describe('deprecate a function', () => {
25 | let deprecatedLogSpy: SinonSpy;
26 | beforeEach(() => {
27 | deprecatedLogSpy = sinon.fake();
28 | sinon.replace(deprecate, 'log', deprecatedLogSpy);
29 | });
30 |
31 | afterEach(() => {
32 | sinon.restore();
33 | });
34 |
35 | it('the original function is still called', () => {
36 | const originalFunction = sinon.spy();
37 | const deprecatedFunction = deprecate('this is function deprecated', originalFunction);
38 |
39 | deprecatedFunction('baz', 3);
40 | assert.ok(
41 | originalFunction.calledWith('baz', 3),
42 | `original function called with (${originalFunction.lastCall.args[0]}, ${originalFunction.lastCall.args[1]})`,
43 | );
44 | });
45 |
46 | it('a call to deprecate.log(msg) is added', () => {
47 | const originalFunction = sinon.spy();
48 | const deprecatedFunction = deprecate('this is function deprecated', originalFunction);
49 |
50 | originalFunction('bar', 2);
51 | assert.ok(originalFunction.calledWith('bar', 2), 'original function not called with ("bar", 2)');
52 | assert.ok(deprecatedLogSpy.notCalled);
53 |
54 | deprecatedFunction('baz', 3);
55 | assert.ok(deprecatedLogSpy.calledWith('this is function deprecated'));
56 | });
57 | });
58 |
59 | describe('.log', () => {
60 | it('logs the message in yellow, starting with "(!) "', () => {
61 | deprecate.log('this is the message');
62 | assert.ok(fakeConsoleLog.calledWith(`${chalk.yellow('(!) ')}this is the message`));
63 | });
64 | });
65 |
66 | describe('.object()', () => {
67 | let deprecatedLogSpy: SinonSpy;
68 | beforeEach(() => {
69 | deprecatedLogSpy = sinon.fake();
70 | sinon.replace(deprecate, 'log', deprecatedLogSpy);
71 | });
72 |
73 | afterEach(() => {
74 | sinon.restore();
75 | });
76 |
77 | it('deprecates all functions/methods in the object', () => {
78 | type SomeOtherObject = SimpleObject & {
79 | anotherFunction: (someValue: string) => string;
80 | };
81 | const originalObject: SomeOtherObject = {
82 | foo: 1,
83 | functionInObj(someValue: number): number {
84 | return someValue - 1;
85 | },
86 | anotherFunction(someValue: string): string {
87 | return `${someValue} and ${someValue}`;
88 | },
89 | };
90 |
91 | // TODO When deprecate.js is changed to .ts, DeprecatedObject will be defined in deprecate.ts as something like type DeprecatedObject = O;
92 | const deprecatedObject = deprecate.object('<%= name %> is deprecated', originalObject);
93 | // @ts-expect-error The method functionInObj() does exist on deprecatedObject. This should be a DeprecatedObject. When deprecate.js is changed to .ts, this can be implemented and no error will occur here.
94 | deprecatedObject.functionInObj(42);
95 | assert.ok(
96 | deprecatedLogSpy.calledWith('functionInObj is deprecated'),
97 | `last call with args: ${deprecatedLogSpy.lastCall.args[0]}`,
98 | );
99 |
100 | // @ts-expect-error The method anotherFunction() does exist on deprecatedObject. This should be a DeprecatedObject. When deprecate.js is changed to .ts, this can be implemented and no error will occur here.
101 | deprecatedObject.anotherFunction('something');
102 | assert.ok(
103 | deprecatedLogSpy.calledWith('anotherFunction is deprecated'),
104 | `last call with args: ${deprecatedLogSpy.lastCall.args[0]}`,
105 | );
106 | });
107 |
108 | it('properties that are not functions are not changed', () => {
109 | const originalObject: SimpleObject = {
110 | foo: 1,
111 | functionInObj(someValue: number): number {
112 | return someValue - 1;
113 | },
114 | };
115 |
116 | const deprecatedObject = deprecate.object('The function "<%= name %>" is deprecated', originalObject);
117 | // @ts-expect-error The property foo does exist on deprecatedObject. This should be a DeprecatedObject. When deprecate.js is changed to .ts, this can be implemented and no error will occur here.
118 | const fooValue = deprecatedObject.foo;
119 | assert.equal(fooValue, 1);
120 | assert.ok(deprecatedLogSpy.notCalled);
121 | });
122 |
123 | it('property getters and setters are not changed', () => {
124 | type ObjectWithGettersSetters = SimpleObject & {
125 | get bar(): number;
126 | set bar(someValue: number);
127 | };
128 |
129 | const originalObject: ObjectWithGettersSetters = {
130 | foo: 10,
131 | functionInObj(someValue: number): number {
132 | return someValue - 1;
133 | },
134 | get bar(): number {
135 | return this.foo * 2;
136 | },
137 | set bar(someValue: number) {
138 | this.foo = someValue / 2;
139 | },
140 | };
141 |
142 | const deprecatedObject = deprecate.object('The function "<%= name %>" is deprecated', originalObject);
143 | // @ts-expect-error The getter bar does exist on the object. This should be a DeprecatedObject. When deprecate.js is changed to .ts, this can be implemented and no error will occur here.
144 | deprecatedObject.bar;
145 | // @ts-expect-error The setter bar does exist on the object. This should be a DeprecatedObject. When deprecate.js is changed to .ts, this can be implemented and no error will occur here.
146 | deprecatedObject.bar = 7;
147 |
148 | assert.ok(deprecatedLogSpy.notCalled);
149 | });
150 |
151 | it('deprecation message can be a template', () => {
152 | const originalObject: SimpleObject = {
153 | foo: 1,
154 | functionInObj(someValue: number): number {
155 | return someValue - 1;
156 | },
157 | };
158 |
159 | const deprecatedObject = deprecate.object('The function "<%= name %>" is deprecated', originalObject);
160 |
161 | // @ts-expect-error The method functionInObj() does exist on deprecatedObject. This should be a DeprecatedObject. When deprecate.js is changed to .ts, this can be implemented and no error will occur here.
162 | deprecatedObject.functionInObj(42);
163 |
164 | assert.ok(
165 | deprecatedLogSpy.calledWith('The function "functionInObj" is deprecated'),
166 | `last call with args: ${deprecatedLogSpy.lastCall.args[0]}`,
167 | );
168 | });
169 | });
170 |
171 | describe('.property()', () => {
172 | let deprecatedLogSpy: SinonSpy;
173 | beforeEach(() => {
174 | deprecatedLogSpy = sinon.fake();
175 | sinon.replace(deprecate, 'log', deprecatedLogSpy);
176 | });
177 |
178 | afterEach(() => {
179 | sinon.restore();
180 | });
181 |
182 | it('the deprecated message shows when a property is accessed', () => {
183 | const originalObject = {
184 | foo: 1,
185 | };
186 | deprecate.property('foo property is deprecated', originalObject, 'foo');
187 |
188 | // Value is not affected; it remains the same
189 | assert.equal(originalObject.foo, 1);
190 |
191 | assert.ok(
192 | deprecatedLogSpy.calledWith('foo property is deprecated'),
193 | `deprecatedLogSpy called with (${deprecatedLogSpy.lastCall.args[0]})`,
194 | );
195 | });
196 |
197 | it('property getters and setters are deprecated', () => {
198 | type ObjectWithGettersSetters = SimpleObject & {
199 | get bar(): number;
200 | set bar(someValue: number);
201 | };
202 |
203 | const originalObject: ObjectWithGettersSetters = {
204 | foo: 10,
205 | functionInObj(someValue: number): number {
206 | return someValue - 1;
207 | },
208 | get bar(): number {
209 | return this.foo * 2;
210 | },
211 | set bar(someValue: number) {
212 | this.foo = someValue / 2;
213 | },
214 | };
215 |
216 | deprecate.property('bar is deprecated', originalObject, 'bar');
217 | originalObject.bar;
218 | assert.ok(
219 | deprecatedLogSpy.calledWith('bar is deprecated'),
220 | `deprecatedLogSpy called with (${deprecatedLogSpy.lastCall.args[0]})`,
221 | );
222 |
223 | originalObject.bar = 7;
224 | assert.ok(
225 | deprecatedLogSpy.calledWith('bar is deprecated'),
226 | `deprecatedLogSpy called with (${deprecatedLogSpy.lastCall.args[0]})`,
227 | );
228 | });
229 | });
230 | });
231 |
--------------------------------------------------------------------------------
/test/environment.test.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import { TestAdapter } from '@yeoman/adapter/testing';
3 | import { afterAll, beforeAll, describe, it } from 'vitest';
4 | import type { SinonStub } from 'sinon';
5 | import { stub as sinonStub } from 'sinon';
6 | import Environment from 'yeoman-environment';
7 | import helpers from 'yeoman-test';
8 | import Base from './utils.js';
9 |
10 | describe('Generator with environment version', () => {
11 | let env: Environment;
12 | let Dummy: typeof Base;
13 | let dummy: Base;
14 | let getVersionStub: SinonStub;
15 |
16 | beforeAll(async () => {
17 | await helpers.prepareTemporaryDir().run();
18 | });
19 |
20 | describe('mocked 3.0.0', () => {
21 | beforeAll(() => {
22 | env = new Environment({ skipInstall: true, adapter: new TestAdapter() });
23 | env.getVersion = env.getVersion || (() => {});
24 | getVersionStub = sinonStub(env, 'getVersion');
25 |
26 | Dummy = class extends Base {};
27 | dummy = new Dummy(['bar', 'baz', 'bom'], {
28 | foo: false,
29 | something: 'else',
30 | namespace: 'dummy',
31 | env: env,
32 | 'skip-install': true,
33 | skipCheckEnv: true,
34 | });
35 | }, 100_000);
36 |
37 | afterAll(() => {
38 | getVersionStub.restore();
39 | });
40 |
41 | describe('#checkEnvironmentVersion', () => {
42 | describe('without args', () => {
43 | it('returns true', () => {
44 | getVersionStub.returns('3.0.0');
45 | assert.equal(dummy.checkEnvironmentVersion(), true);
46 | });
47 | });
48 |
49 | describe('with required environment', () => {
50 | beforeAll(() => {
51 | getVersionStub.returns('3.0.1');
52 | });
53 |
54 | it('returns true', () => {
55 | assert.equal(dummy.checkEnvironmentVersion('3.0.1'), true);
56 | });
57 |
58 | describe('with ignoreVersionCheck', () => {
59 | beforeAll(() => {
60 | dummy.options.ignoreVersionCheck = true;
61 | });
62 |
63 | afterAll(() => {
64 | dummy.options.ignoreVersionCheck = false;
65 | });
66 |
67 | it('returns true', () => {
68 | getVersionStub.returns('3.0.1');
69 | assert.equal(dummy.checkEnvironmentVersion('3.0.1'), true);
70 | });
71 | });
72 | });
73 |
74 | describe('with greater than required environment', () => {
75 | it('returns true', () => {
76 | getVersionStub.returns('3.0.2');
77 | assert.equal(dummy.checkEnvironmentVersion('3.0.1'), true);
78 | });
79 | });
80 |
81 | describe('with less than required environment', () => {
82 | beforeAll(() => {
83 | getVersionStub.returns('3.0.0');
84 | });
85 |
86 | it('should throw', () => {
87 | assert.throws(
88 | () => dummy.checkEnvironmentVersion('3.0.1'),
89 | /requires yeoman-environment at least 3.0.1, current version is 3.0.0/,
90 | );
91 | });
92 |
93 | describe('with warning', () => {
94 | it('should return false', () => {
95 | assert.equal(dummy.checkEnvironmentVersion('3.0.1', true), false);
96 | });
97 | });
98 |
99 | describe('with ignoreVersionCheck', () => {
100 | beforeAll(() => {
101 | dummy.options.ignoreVersionCheck = true;
102 | });
103 |
104 | afterAll(() => {
105 | dummy.options.ignoreVersionCheck = false;
106 | });
107 |
108 | it('returns false', () => {
109 | assert.equal(dummy.checkEnvironmentVersion('3.0.1'), false);
110 | });
111 | });
112 | });
113 |
114 | describe('with required inquirer', () => {
115 | it('returns true', () => {
116 | getVersionStub.withArgs('inquirer').returns('7.1.0');
117 | assert.equal(dummy.checkEnvironmentVersion('inquirer', '7.1.0'), true);
118 | });
119 | });
120 |
121 | describe('with greater than required inquirer', () => {
122 | it('returns true', () => {
123 | getVersionStub.withArgs('inquirer').returns('7.1.1');
124 | assert.equal(dummy.checkEnvironmentVersion('inquirer', '7.1.0'), true);
125 | });
126 | });
127 |
128 | describe('with less than required inquirer', () => {
129 | beforeAll(() => {
130 | getVersionStub.withArgs('inquirer').returns('7.1.0');
131 | });
132 |
133 | it('throws exception', () => {
134 | assert.throws(
135 | () => dummy.checkEnvironmentVersion('inquirer', '7.1.1'),
136 | /requires inquirer at least 7.1.1, current version is 7.1.0/,
137 | );
138 | });
139 |
140 | describe('with warning', () => {
141 | it('returns false', () => {
142 | assert.equal(dummy.checkEnvironmentVersion('inquirer', '7.1.1', true), false);
143 | });
144 | });
145 |
146 | describe('with ignoreVersionCheck', () => {
147 | beforeAll(() => {
148 | dummy.options.ignoreVersionCheck = true;
149 | });
150 |
151 | afterAll(() => {
152 | dummy.options.ignoreVersionCheck = false;
153 | });
154 |
155 | it('returns false', () => {
156 | assert.equal(dummy.checkEnvironmentVersion('inquirer', '7.1.1'), false);
157 | });
158 | });
159 | });
160 | });
161 |
162 | describe('#prompt with storage', () => {
163 | it('with compatible environment', () => {
164 | getVersionStub.withArgs().returns('3.0.0');
165 | getVersionStub.withArgs('inquirer').returns('7.1.0');
166 | return dummy.prompt([], dummy.config);
167 | });
168 | });
169 | });
170 |
171 | describe('mocked 2.8.1', () => {
172 | beforeAll(() => {
173 | env = new Environment({ skipInstall: true, adapter: new TestAdapter() });
174 | env.getVersion = undefined;
175 |
176 | Dummy = class extends Base {};
177 | dummy = new Dummy(['bar', 'baz', 'bom'], {
178 | foo: false,
179 | something: 'else',
180 | namespace: 'dummy',
181 | env: env,
182 | skipCheckEnv: true,
183 | 'skip-install': true,
184 | });
185 | }, 100_000);
186 |
187 | describe('#checkEnvironmentVersion', () => {
188 | describe('without args', () => {
189 | it('throws exception', () => {
190 | assert.throws(
191 | () => dummy.checkEnvironmentVersion(),
192 | /requires yeoman-environment at least 2.9.0, current version is less than 2.9.0/,
193 | );
194 | });
195 | });
196 |
197 | describe('with ignoreVersionCheck', () => {
198 | beforeAll(() => {
199 | dummy.options.ignoreVersionCheck = true;
200 | });
201 |
202 | afterAll(() => {
203 | dummy.options.ignoreVersionCheck = false;
204 | });
205 |
206 | describe('without args', () => {
207 | it('returns false', () => {
208 | assert.equal(dummy.checkEnvironmentVersion(), false);
209 | });
210 | });
211 |
212 | describe('without less then 3.0.0', () => {
213 | it('returns undefined', () => {
214 | assert.equal(dummy.checkEnvironmentVersion('2.9.0'), false);
215 | });
216 | });
217 | });
218 | });
219 | });
220 | });
221 |
--------------------------------------------------------------------------------
/test/fixtures/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "test": {
3 | "name": "test",
4 | "testFramework": "mocha"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/conflict.js:
--------------------------------------------------------------------------------
1 | var a = 1;
2 |
--------------------------------------------------------------------------------
/test/fixtures/dummy-project/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/test/fixtures/dummy-project/subdir/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeoman/generator/377782ed82aee599761b0ed670a051e863b90801/test/fixtures/dummy-project/subdir/.gitkeep
--------------------------------------------------------------------------------
/test/fixtures/file-conflict.txt:
--------------------------------------------------------------------------------
1 | initial content
2 |
--------------------------------------------------------------------------------
/test/fixtures/file-contains-utf8.yml:
--------------------------------------------------------------------------------
1 | city: 深圳
2 |
--------------------------------------------------------------------------------
/test/fixtures/foo-copy.js:
--------------------------------------------------------------------------------
1 | var foo = 'foo';
2 |
--------------------------------------------------------------------------------
/test/fixtures/foo-template.js:
--------------------------------------------------------------------------------
1 | var <%= foo %> = '<%= foo %>';
2 | <%%= extra %>
3 |
--------------------------------------------------------------------------------
/test/fixtures/foo.js:
--------------------------------------------------------------------------------
1 | var foo = 'foo';
2 |
--------------------------------------------------------------------------------
/test/fixtures/generator-defaults/app/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Example of a generator with options.
4 | //
5 | // It takes a list of arguments (usually CLI args) and a Hash of options
6 | // (CLI options), the context of the function is a `new Generator.Base`
7 | // object, which means that you can use the API as if you were extending
8 | // `Base`.
9 |
10 | import Base from '../../../../dist/index.js';
11 | import options from './options.js';
12 | import prompts from './prompts.js';
13 |
14 | export default class App extends Base {
15 | constructor(args, opts) {
16 | super(args, opts);
17 |
18 | this.option(options);
19 |
20 | this.registerConfigPrompts(prompts);
21 | }
22 | }
23 |
24 | App.namespace = 'options:generator';
25 |
--------------------------------------------------------------------------------
/test/fixtures/generator-defaults/app/options.js:
--------------------------------------------------------------------------------
1 | export default [{ name: 'extra', type: String, storage: 'config' }];
2 |
--------------------------------------------------------------------------------
/test/fixtures/generator-defaults/app/prompts.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | name: 'foo',
4 | type: 'input',
5 | message: 'Test foo value',
6 | default: 'bar',
7 | exportOption: true,
8 | },
9 | ];
10 |
--------------------------------------------------------------------------------
/test/fixtures/generator-defaults/app/templates/foo-template.js:
--------------------------------------------------------------------------------
1 | var <%= foo %> = '<%= foo %>';
2 | <%= extra %>
3 |
--------------------------------------------------------------------------------
/test/fixtures/generator-defaults/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generator-defaults",
3 | "version": "0.0.0",
4 | "author": "",
5 | "type": "module",
6 | "main": "main.js",
7 | "dependencies": {},
8 | "devDependencies": {},
9 | "optionalDependencies": {},
10 | "engines": {
11 | "node": "*"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/fixtures/generator-mocha/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Example of a simple generator.
4 | //
5 | // A raw function that is executed when this generator is resolved.
6 | //
7 | // It takes a list of arguments (usually CLI args) and a Hash of options
8 | // (CLI options), the context of the function is a `new Generator.Base`
9 | // object, which means that you can use the API as if you were extending
10 | // `Base`.
11 | //
12 | // It works with simple generator, if you need to do a bit more complex
13 | // stuff, extends from Generator.Base and defines your generator steps
14 | // in several methods.
15 | import Base from '../../../dist/index.js';
16 |
17 | class Generator extends Base {
18 | notEmpty() {}
19 | }
20 |
21 | Generator.description =
22 | 'Ana add a custom description by adding a `description` property to your function.';
23 | Generator.usage = 'Usage can be used to customize the help output';
24 |
25 | // Namespace is resolved depending on the location of this generator,
26 | // unless you specifically define it.
27 | Generator.namespace = 'mocha:generator';
28 |
29 | export default Generator;
30 |
--------------------------------------------------------------------------------
/test/fixtures/generator-mocha/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generator-mocha",
3 | "version": "0.0.0",
4 | "author": "",
5 | "type": "module",
6 | "main": "main.js",
7 | "dependencies": {},
8 | "devDependencies": {},
9 | "optionalDependencies": {},
10 | "engines": {
11 | "node": "*"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/fixtures/options-generator/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Example of a generator with options.
4 | //
5 | // It takes a list of arguments (usually CLI args) and a Hash of options
6 | // (CLI options), the context of the function is a `new Generator.Base`
7 | // object, which means that you can use the API as if you were extending
8 | // `Base`.
9 |
10 | import Base from '../../../dist/index.js';
11 |
12 | export default class Generator extends Base {
13 | constructor(args, opts) {
14 | super(args, opts);
15 |
16 | this.option('testOption', {
17 | type: Boolean,
18 | desc: 'Testing falsey values for option',
19 | defaults: true,
20 | });
21 | }
22 |
23 | testOption() {
24 | return 'foo';
25 | }
26 | }
27 |
28 | Generator.namespace = 'options:generator';
29 |
--------------------------------------------------------------------------------
/test/fixtures/options-generator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "options-generator",
3 | "version": "0.0.0",
4 | "author": "",
5 | "type": "module",
6 | "main": "main.js",
7 | "dependencies": {},
8 | "devDependencies": {},
9 | "optionalDependencies": {},
10 | "engines": {
11 | "node": "*"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/fixtures/testFile.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeoman/generator/377782ed82aee599761b0ed670a051e863b90801/test/fixtures/testFile.tar.gz
--------------------------------------------------------------------------------
/test/fixtures/yeoman-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeoman/generator/377782ed82aee599761b0ed670a051e863b90801/test/fixtures/yeoman-logo.png
--------------------------------------------------------------------------------
/test/fs.test.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import path from 'node:path';
3 | import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
4 | import { TestAdapter } from '@yeoman/adapter/testing';
5 | import { type SinonStub, stub as sinonStub } from 'sinon';
6 | import type { Data as TemplateData } from 'ejs';
7 | import Environment from 'yeoman-environment';
8 | import { BaseGenerator } from '../src/generator.js';
9 | import Base from './utils.js';
10 |
11 | const randomString = () => Math.random().toString(36).slice(7);
12 | const createEnv = () => new Environment({ skipInstall: true, adapter: new TestAdapter() });
13 |
14 | // Make copyTpl() call argument indices more readable
15 | const ARG_FROM = 0;
16 | const ARG_TO = 1;
17 | const ARG_DATA = 2; // A.k.a. context
18 | const ARG_TPLSETTINGS = 3; // Template settings
19 | const ARG_COPYSETTINGS = 4;
20 |
21 | type FSOpResult = {
22 | name: string;
23 | first?: string;
24 | second?: string;
25 | fromBasePath?: string;
26 | dest: string;
27 | returnsUndefined?: boolean;
28 | };
29 |
30 | let testResults: FSOpResult[] = [];
31 | testResults = [
32 | ...testResults,
33 | { name: 'readTemplate', first: 'templatePath', dest: 'read' },
34 | {
35 | name: 'copyTemplate',
36 | second: 'destinationPath',
37 | fromBasePath: 'templatePath',
38 | dest: 'copy',
39 | },
40 | {
41 | name: 'copyTemplateAsync',
42 | first: 'templatePath',
43 | second: 'destinationPath',
44 | dest: 'copyAsync',
45 | },
46 | { name: 'readDestination', first: 'destinationPath', dest: 'read' },
47 | { name: 'writeDestination', first: 'destinationPath', dest: 'write' },
48 | {
49 | name: 'writeDestinationJSON',
50 | first: 'destinationPath',
51 | dest: 'writeJSON',
52 | },
53 | { name: 'deleteDestination', first: 'destinationPath', dest: 'delete' },
54 | {
55 | name: 'copyDestination',
56 | second: 'destinationPath',
57 | fromBasePath: 'destinationPath',
58 | dest: 'copy',
59 | },
60 | {
61 | name: 'moveDestination',
62 | second: 'destinationPath',
63 | fromBasePath: 'destinationPath',
64 | dest: 'move',
65 | },
66 | { name: 'existsDestination', first: 'destinationPath', dest: 'exists' },
67 | {
68 | name: 'renderTemplate',
69 | first: 'templatePath',
70 | second: 'destinationPath',
71 | dest: 'copyTpl',
72 | returnsUndefined: true,
73 | },
74 | {
75 | name: 'renderTemplateAsync',
76 | first: 'templatePath',
77 | second: 'destinationPath',
78 | dest: 'copyTplAsync',
79 | },
80 | ];
81 |
82 | type BaseGenPaths = Record;
83 |
84 | describe('generators.Base (actions/fs)', () => {
85 | const baseReturns: BaseGenPaths = {
86 | templatePath: `templatePath${randomString()}`,
87 | destinationPath: `destinationPath${randomString()}`,
88 | };
89 | const configGetAll = { foo: 'bar' };
90 | let returns: Record;
91 | let gen: Base;
92 | let base: BaseGenerator;
93 |
94 | beforeAll(() => {
95 | gen = new Base({ env: createEnv(), resolved: 'unknown', help: true });
96 | }, 10_000);
97 |
98 | beforeEach(() => {
99 | returns = {};
100 | base = new BaseGenerator({ namespace: 'foo', help: true, resolved: 'unknown' });
101 |
102 | // Why not use a sinonStub for base.config as is done in #renderTemplate and #renderTemplateAsync below?
103 | // base get config is not being tested in any way below.
104 | // @ts-expect-error Config is a string (not a symbol) and we know it exists on base https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33173
105 | vi.spyOn(base, 'config', 'get').mockReturnValue({
106 | getAll() {
107 | return configGetAll;
108 | },
109 | });
110 |
111 | Object.assign(base, {
112 | templatePath: sinonStub().returns(baseReturns.templatePath),
113 | destinationPath: sinonStub().returns(baseReturns.destinationPath),
114 | renderTemplate: Base.prototype.renderTemplate,
115 | renderTemplateAsync: Base.prototype.renderTemplateAsync,
116 | renderTemplates: Base.prototype.renderTemplates,
117 | renderTemplatesAsync: Base.prototype.renderTemplatesAsync,
118 | checkEnvironmentVersion() {},
119 | fs: {},
120 | });
121 | for (const op of [
122 | 'read',
123 | 'copy',
124 | 'copyAsync',
125 | 'write',
126 | 'writeJSON',
127 | 'delete',
128 | 'move',
129 | 'exists',
130 | 'copyTpl',
131 | 'copyTplAsync',
132 | ]) {
133 | const returnValue = randomString();
134 | base.fs[op] = sinonStub().returns(returnValue);
135 | returns[op] = returnValue;
136 | }
137 | });
138 |
139 | for (const operation of testResults) {
140 | const passedArg1 = randomString();
141 | const passedArg2 = randomString();
142 | const passedArg3: any = {};
143 | const passedArg4 = { foo: 'bar' };
144 |
145 | describe(`#${operation.name}`, () => {
146 | let returnValue: any;
147 | let expectedReturn: string | undefined;
148 | let firstArgumentHandler: SinonStub | undefined;
149 | let secondArgumentHandler: SinonStub;
150 |
151 | beforeEach(async () => {
152 | returnValue = await base[operation.name](passedArg1, passedArg2, passedArg3, passedArg4);
153 |
154 | expectedReturn = operation.returnsUndefined ? undefined : returns[operation.dest];
155 | firstArgumentHandler = operation.first ? base[operation.first] : undefined;
156 | if (operation.second !== undefined && operation.second !== null) {
157 | secondArgumentHandler = base[operation.second];
158 | }
159 | });
160 |
161 | it('exists on the generator', () => {
162 | assert(operation.name in Base.prototype);
163 | });
164 |
165 | it('returns the correct value', () => {
166 | assert.equal(returnValue, expectedReturn);
167 | });
168 |
169 | it('handles the first parameter', () => {
170 | if (firstArgumentHandler) {
171 | assert.equal(firstArgumentHandler.getCall(0).args[0], passedArg1);
172 | }
173 | });
174 |
175 | it.skip('handles the second parameter', () => {
176 | if (operation.second && operation.first === operation.second) {
177 | assert(secondArgumentHandler.calledTwice);
178 | expect(secondArgumentHandler.getCall(1).args[0]).toMatch(passedArg2);
179 | } else if (operation.second) {
180 | assert(secondArgumentHandler.calledOnce);
181 | expect(secondArgumentHandler.getCall(0).args[0]).toMatch(passedArg2);
182 | if (firstArgumentHandler) {
183 | assert(firstArgumentHandler.calledOnce);
184 | }
185 | }
186 | });
187 |
188 | it('calls fs with correct arguments', () => {
189 | const destCall = base.fs[operation.dest];
190 | assert(destCall.calledOnce);
191 | const call = destCall.getCall(0);
192 | // First argument should be the trated first arguments
193 | expect(call.args[0]).toMatch(operation.first ? baseReturns[operation.first] : passedArg1);
194 |
195 | // Second argument should be the trated first arguments
196 | if (operation.second) {
197 | expect(call.args[1]).toMatch(baseReturns[operation.second]);
198 | } else {
199 | expect(call.args[1]).toMatch(passedArg2);
200 | }
201 |
202 | if (operation.dest === 'copy' || operation.dest === 'move') {
203 | expect(call.args[2]).toMatchObject(passedArg3);
204 | } else {
205 | expect(call.args[2]).toMatchObject(passedArg3);
206 | }
207 | expect(call.args[3].foo).toMatch(passedArg4.foo);
208 | if (operation.fromBasePath) {
209 | expect(call.args[2].fromBasePath).toMatch(baseReturns[operation.fromBasePath]);
210 | }
211 | });
212 | });
213 | }
214 |
215 | describe('#renderTemplate', () => {
216 | const getAllReturn = {};
217 | const getPathReturn = { foo: 'bar' };
218 |
219 | beforeEach(() => {
220 | sinonStub(gen, 'sourceRoot').returns('');
221 | sinonStub(gen, 'destinationRoot').returns('');
222 | sinonStub(gen.config, 'getAll').returns(getAllReturn);
223 | sinonStub(gen.config, 'getPath').returns(getPathReturn);
224 |
225 | for (const op of ['copyTpl']) {
226 | const returnValue = randomString();
227 | sinonStub(gen.fs, op).returns(returnValue);
228 | returns[op] = returnValue;
229 | }
230 | });
231 |
232 | afterEach(() => {
233 | gen.sourceRoot.restore();
234 | gen.destinationRoot.restore();
235 | gen.config.getAll.restore();
236 | gen.config.getPath.restore();
237 | for (const op of ['copyTpl']) gen.fs[op].restore();
238 | });
239 |
240 | it('gets default data from config', () => {
241 | gen.renderTemplate('a', 'b');
242 | const { copyTpl } = gen.fs;
243 |
244 | assert(copyTpl.calledOnce);
245 | const firsCall = copyTpl.getCall(0);
246 | assert.equal(firsCall.args[ARG_DATA], getAllReturn);
247 | });
248 |
249 | it('gets data with path from config', () => {
250 | gen.renderTemplate('a', 'b', 'test');
251 | const { copyTpl } = gen.fs;
252 |
253 | assert(copyTpl.calledOnce);
254 | const firsCall = copyTpl.getCall(0);
255 | assert.equal(firsCall.args[ARG_DATA], getPathReturn);
256 | });
257 |
258 | it('concatenates source and destination', () => {
259 | const source = ['a', 'b'];
260 | const destination = ['b', 'a'];
261 | const data = {};
262 |
263 | gen.renderTemplate(source, destination, data);
264 | const { copyTpl } = gen.fs;
265 |
266 | assert(copyTpl.calledOnce);
267 | const firsCall = copyTpl.getCall(0);
268 | assert.equal(firsCall.args[ARG_FROM], path.join(...source));
269 | assert.equal(firsCall.args[ARG_TO], path.join(...destination));
270 | assert.equal(firsCall.args[ARG_DATA], data);
271 | });
272 | });
273 |
274 | describe('#renderTemplateAsync', () => {
275 | const getAllReturn = {};
276 | const getPathReturn = { foo: 'bar' };
277 |
278 | beforeEach(() => {
279 | sinonStub(gen, 'sourceRoot').returns('');
280 | sinonStub(gen, 'destinationRoot').returns('');
281 | sinonStub(gen.config, 'getAll').returns(getAllReturn);
282 | sinonStub(gen.config, 'getPath').returns(getPathReturn);
283 |
284 | for (const op of ['copyTplAsync']) {
285 | const returnValue = randomString();
286 | sinonStub(gen.fs, op).returns(returnValue);
287 | returns[op] = returnValue;
288 | }
289 | });
290 |
291 | afterEach(() => {
292 | gen.sourceRoot.restore();
293 | gen.destinationRoot.restore();
294 | gen.config.getAll.restore();
295 | gen.config.getPath.restore();
296 | for (const op of ['copyTplAsync']) gen.fs[op].restore();
297 | });
298 |
299 | it('gets default data from config', () => {
300 | gen.renderTemplateAsync('a', 'b');
301 | const { copyTplAsync } = gen.fs;
302 |
303 | assert(copyTplAsync.calledOnce);
304 | const firsCall = copyTplAsync.getCall(0);
305 | assert.equal(firsCall.args[ARG_DATA], getAllReturn);
306 | });
307 |
308 | it('gets data with path from config', async () => {
309 | await gen.renderTemplateAsync('a', 'b', 'test');
310 | const { copyTplAsync } = gen.fs;
311 |
312 | assert(copyTplAsync.calledOnce);
313 | const firsCall = copyTplAsync.getCall(0);
314 | assert.equal(firsCall.args[ARG_DATA], getPathReturn);
315 | });
316 |
317 | it('concatenates source and destination', () => {
318 | const source = ['a', 'b'];
319 | const destination = ['b', 'a'];
320 | const data = {};
321 |
322 | gen.renderTemplateAsync(source, destination, data);
323 | const { copyTplAsync } = gen.fs;
324 |
325 | assert(copyTplAsync.calledOnce);
326 | const firsCall = copyTplAsync.getCall(0);
327 | assert.equal(firsCall.args[ARG_FROM], path.join(...source));
328 | assert.equal(firsCall.args[ARG_TO], path.join(...destination));
329 | assert.equal(firsCall.args[ARG_DATA], data);
330 | });
331 | });
332 |
333 | describe('#renderTemplates', () => {
334 | beforeEach(() => {
335 | sinonStub(gen, 'sourceRoot').returns('');
336 | sinonStub(gen, 'destinationRoot').returns('');
337 |
338 | for (const op of ['copyTpl']) {
339 | const returnValue = randomString();
340 | sinonStub(gen.fs, op).returns(returnValue);
341 | returns[op] = returnValue;
342 | }
343 | });
344 |
345 | afterEach(() => {
346 | gen.sourceRoot.restore();
347 | gen.destinationRoot.restore();
348 | for (const op of ['copyTpl']) gen.fs[op].restore();
349 | });
350 |
351 | it('handles 1 template', () => {
352 | const passedArg1 = 'foo';
353 | const data = {};
354 | gen.renderTemplates([{ source: passedArg1 }], data);
355 |
356 | const { copyTpl } = gen.fs;
357 | assert.equal(copyTpl.callCount, 1);
358 |
359 | const firsCall = copyTpl.getCall(0);
360 | assert.equal(firsCall.args[ARG_FROM], passedArg1);
361 | assert.equal(firsCall.args[ARG_TO], passedArg1);
362 | assert.equal(firsCall.args[ARG_DATA], data);
363 | });
364 |
365 | it('handles more than 1 template', () => {
366 | const passedArg1 = 'foo';
367 | const secondCallArg1 = 'bar';
368 | const secondCallArg2 = 'bar2';
369 | const data = {};
370 | const templateOptions = { foo: '123' };
371 | const copyOptions = {};
372 |
373 | gen.renderTemplates(
374 | [
375 | { source: passedArg1 },
376 | {
377 | source: secondCallArg1,
378 | destination: secondCallArg2,
379 | templateOptions,
380 | copyOptions,
381 | },
382 | ],
383 | data,
384 | );
385 |
386 | const { copyTpl } = gen.fs;
387 | assert.equal(copyTpl.callCount, 2);
388 |
389 | const firsCall = copyTpl.getCall(0);
390 | assert.equal(firsCall.args[ARG_FROM], passedArg1);
391 | assert.equal(firsCall.args[ARG_TO], passedArg1);
392 | assert.equal(firsCall.args[ARG_DATA], data);
393 |
394 | const secondCall = copyTpl.getCall(1);
395 | assert.equal(secondCall.args[ARG_FROM], secondCallArg1);
396 | assert.equal(secondCall.args[ARG_TO], secondCallArg2);
397 | assert.equal(secondCall.args[ARG_DATA], data);
398 | assert.equal(secondCall.args[ARG_TPLSETTINGS].foo, templateOptions.foo);
399 | expect(secondCall.args[ARG_COPYSETTINGS]).toMatchObject({ ...copyOptions, fromBasePath: expect.any(String) });
400 | });
401 |
402 | it('skips templates based on when callback', () => {
403 | const passedArg1 = 'foo';
404 | const secondCallArg1 = 'bar';
405 | const secondCallArg2 = 'bar2';
406 | const data = {};
407 | const templateOptions = {};
408 | const copyOptions = {};
409 |
410 | gen.renderTemplates(
411 | [
412 | { source: passedArg1 },
413 | {
414 | source: secondCallArg1,
415 | when: () => false,
416 | destination: secondCallArg2,
417 | templateOptions,
418 | copyOptions,
419 | },
420 | ],
421 | data,
422 | );
423 |
424 | const { copyTpl } = gen.fs;
425 | assert.equal(copyTpl.callCount, 1);
426 |
427 | const firsCall = copyTpl.getCall(0);
428 | assert.equal(firsCall.args[ARG_FROM], passedArg1);
429 | assert.equal(firsCall.args[ARG_TO], passedArg1);
430 | assert.equal(firsCall.args[ARG_DATA], data);
431 | });
432 |
433 | it('passes the data to when callback', () => {
434 | const passedArg1 = 'foo';
435 | const templateData: TemplateData = {};
436 | let receivedData: TemplateData = { name: 'original value' }; // Set this to something so TypeScript doesn't complain that it is used before set
437 |
438 | gen.renderTemplates(
439 | [
440 | {
441 | source: passedArg1,
442 | when(data: TemplateData) {
443 | receivedData = data;
444 | },
445 | },
446 | ],
447 | templateData,
448 | );
449 |
450 | const { copyTpl } = gen.fs;
451 | assert.equal(copyTpl.callCount, 0);
452 |
453 | assert.equal(receivedData, templateData);
454 | });
455 | });
456 |
457 | describe('#renderTemplatesAsync', () => {
458 | beforeEach(() => {
459 | sinonStub(gen, 'sourceRoot').returns('');
460 | sinonStub(gen, 'destinationRoot').returns('');
461 |
462 | for (const op of ['copyTplAsync']) {
463 | const returnValue = randomString();
464 | sinonStub(gen.fs, op).returns(returnValue);
465 | returns[op] = returnValue;
466 | }
467 | });
468 |
469 | afterEach(() => {
470 | gen.sourceRoot.restore();
471 | gen.destinationRoot.restore();
472 | for (const op of ['copyTplAsync']) gen.fs[op].restore();
473 | });
474 |
475 | it('handles 1 template', () => {
476 | const passedArg1 = 'foo';
477 | const data = {};
478 | gen.renderTemplatesAsync([{ source: passedArg1 }], data);
479 |
480 | const { copyTplAsync } = gen.fs;
481 | assert.equal(copyTplAsync.callCount, 1);
482 |
483 | const firsCall = copyTplAsync.getCall(0);
484 | assert.equal(firsCall.args[ARG_FROM], passedArg1);
485 | assert.equal(firsCall.args[ARG_TO], passedArg1);
486 | assert.equal(firsCall.args[ARG_DATA], data);
487 | });
488 |
489 | it('handles more than 1 template', () => {
490 | const passedArg1 = 'foo';
491 | const secondCallArg1 = 'bar';
492 | const secondCallArg2 = 'bar2';
493 | const data = {};
494 | const templateOptions = { foo: '123' };
495 | const copyOptions = {};
496 |
497 | gen.renderTemplatesAsync(
498 | [
499 | { source: passedArg1 },
500 | {
501 | source: secondCallArg1,
502 | destination: secondCallArg2,
503 | templateOptions,
504 | copyOptions,
505 | },
506 | ],
507 | data,
508 | );
509 |
510 | const { copyTplAsync } = gen.fs;
511 | assert.equal(copyTplAsync.callCount, 2);
512 |
513 | const firsCall = copyTplAsync.getCall(0);
514 | assert.equal(firsCall.args[ARG_FROM], passedArg1);
515 | assert.equal(firsCall.args[ARG_TO], passedArg1);
516 | assert.equal(firsCall.args[ARG_DATA], data);
517 |
518 | const secondCall = copyTplAsync.getCall(1);
519 | assert.equal(secondCall.args[ARG_FROM], secondCallArg1);
520 | assert.equal(secondCall.args[ARG_TO], secondCallArg2);
521 | assert.equal(secondCall.args[ARG_DATA], data);
522 | assert.equal(secondCall.args[ARG_TPLSETTINGS].foo, templateOptions.foo);
523 | expect(secondCall.args[ARG_COPYSETTINGS]).toMatchObject({ ...copyOptions, fromBasePath: expect.any(String) });
524 | });
525 |
526 | it('skips templates based on when callback', async () => {
527 | const passedArg1 = 'foo';
528 | const secondCallArg1 = 'bar';
529 | const secondCallArg2 = 'bar2';
530 | const data = {};
531 | const templateOptions = {};
532 | const copyOptions = {};
533 |
534 | await gen.renderTemplatesAsync(
535 | [
536 | { source: passedArg1 },
537 | {
538 | source: secondCallArg1,
539 | when: () => false,
540 | destination: secondCallArg2,
541 | templateOptions,
542 | copyOptions,
543 | },
544 | ],
545 | data,
546 | );
547 |
548 | const { copyTplAsync } = gen.fs;
549 | assert.equal(copyTplAsync.callCount, 1);
550 |
551 | const firsCall = copyTplAsync.getCall(0);
552 | assert.equal(firsCall.args[ARG_FROM], passedArg1);
553 | assert.equal(firsCall.args[ARG_TO], passedArg1);
554 | assert.equal(firsCall.args[ARG_DATA], data);
555 | });
556 |
557 | it('passes the data to when callback', () => {
558 | const passedArg1 = 'foo';
559 | const templateData: TemplateData = {};
560 | let receivedData: TemplateData = { name: 'original value' }; // Set this to something so TypeScript doesn't complain that it is used before set
561 |
562 | gen.renderTemplatesAsync(
563 | [
564 | {
565 | source: passedArg1,
566 | when(data: TemplateData) {
567 | receivedData = data;
568 | },
569 | },
570 | ],
571 | templateData,
572 | );
573 |
574 | const { copyTplAsync } = gen.fs;
575 | assert.equal(copyTplAsync.callCount, 0);
576 |
577 | assert.equal(receivedData, templateData);
578 | });
579 | });
580 | });
581 |
--------------------------------------------------------------------------------
/test/generators-compose-workflow.test.ts:
--------------------------------------------------------------------------------
1 | import { beforeEach, describe, it } from 'vitest';
2 | import { TestAdapter } from '@yeoman/adapter/testing';
3 | import type { SinonSpy } from 'sinon';
4 | import { assert as sinonAssert, spy as sinonSpy } from 'sinon';
5 | import Environment from 'yeoman-environment';
6 | import assert from 'yeoman-assert';
7 | import helpers from 'yeoman-test';
8 | import Base from './utils.js';
9 |
10 | const createEnv = () => new Environment({ skipInstall: true, adapter: new TestAdapter() });
11 |
12 | describe('Multiples generators', () => {
13 | let env: Environment;
14 |
15 | let Dummy: typeof Base;
16 | let dummy: Base;
17 | let dummy2: Base;
18 |
19 | let spyExec: SinonSpy;
20 | let spyExec1: SinonSpy;
21 | let spyInit1: SinonSpy;
22 | let spyWrite1: SinonSpy;
23 | let spyEnd1: SinonSpy;
24 | let spyExec2: SinonSpy;
25 | let spyInit2: SinonSpy;
26 | let spyWrite2: SinonSpy;
27 | let spyEnd2: SinonSpy;
28 | let spyExec3: SinonSpy;
29 |
30 | beforeEach(async () => {
31 | await helpers.prepareTemporaryDir().run();
32 |
33 | env = createEnv();
34 | Dummy = class extends Base {};
35 | spyExec = sinonSpy();
36 | Dummy.prototype.exec = spyExec;
37 | });
38 |
39 | describe('#composeWith() with multiples generators', () => {
40 | beforeEach(() => {
41 | dummy = new Dummy([], {
42 | resolved: 'unknown',
43 | namespace: 'dummy',
44 | env: env,
45 | 'skip-install': true,
46 | 'force-install': true,
47 | 'skip-cache': true,
48 | });
49 |
50 | spyExec1 = sinonSpy();
51 | spyInit1 = sinonSpy();
52 | spyWrite1 = sinonSpy();
53 | spyEnd1 = sinonSpy();
54 |
55 | const GenCompose1 = class extends Base {};
56 | GenCompose1.prototype.exec = spyExec1;
57 | GenCompose1.prototype.initializing = spyInit1;
58 | GenCompose1.prototype.writing = spyWrite1;
59 | GenCompose1.prototype.end = spyEnd1;
60 |
61 | spyExec2 = sinonSpy();
62 | spyInit2 = sinonSpy();
63 | spyWrite2 = sinonSpy();
64 | spyEnd2 = sinonSpy();
65 |
66 | const GenCompose2 = class extends Base {};
67 | GenCompose2.prototype.exec = spyExec2;
68 | GenCompose2.prototype.initializing = spyInit2;
69 | GenCompose2.prototype.writing = spyWrite2;
70 | GenCompose2.prototype.end = spyEnd2;
71 |
72 | env.registerStub(GenCompose1, 'composed:gen');
73 | env.registerStub(GenCompose2, 'composed:gen2');
74 | });
75 |
76 | it('runs multiple composed generators', async () => {
77 | await dummy.composeWith(['composed:gen', 'composed:gen2']);
78 |
79 | const runSpy = sinonSpy(dummy, 'run');
80 |
81 | // I use a setTimeout here just to make sure composeWith() doesn't start the
82 | // generator before the base one is ran.
83 | await dummy.run();
84 | sinonAssert.callOrder(
85 | runSpy,
86 | spyInit1,
87 | spyInit2,
88 | spyExec,
89 | spyExec1,
90 | spyExec2,
91 | spyWrite1,
92 | spyWrite2,
93 | spyEnd1,
94 | spyEnd2,
95 | );
96 | assert(spyInit1.calledAfter(runSpy));
97 | assert(spyInit2.calledAfter(spyInit1));
98 | assert(spyExec1.calledAfter(spyInit2));
99 | assert(spyExec2.calledAfter(spyExec1));
100 | });
101 |
102 | it('runs multiple composed generators (reverse)', async () => {
103 | await dummy.composeWith(['composed:gen2', 'composed:gen']);
104 |
105 | const runSpy = sinonSpy(dummy, 'run');
106 | await dummy.run();
107 |
108 | sinonAssert.callOrder(
109 | runSpy,
110 | spyInit2,
111 | spyInit1,
112 | spyExec,
113 | spyExec2,
114 | spyExec1,
115 | spyWrite2,
116 | spyWrite1,
117 | spyEnd2,
118 | spyEnd1,
119 | );
120 | assert(spyInit2.calledAfter(runSpy));
121 | assert(spyInit1.calledAfter(spyInit2));
122 | assert(spyExec2.calledAfter(spyInit1));
123 | assert(spyExec1.calledAfter(spyExec2));
124 | });
125 |
126 | it('runs 3 composed generators', async () => {
127 | spyExec3 = sinonSpy();
128 | const spyInit3 = sinonSpy();
129 | const GenCompose3 = class extends Base {};
130 | GenCompose3.prototype.exec = spyExec3;
131 | GenCompose3.prototype.initializing = spyInit3;
132 |
133 | env.registerStub(GenCompose3, 'composed:gen3');
134 |
135 | await dummy.composeWith(['composed:gen', 'composed:gen2', 'composed:gen3']);
136 |
137 | const runSpy = sinonSpy(dummy, 'run');
138 | await dummy.run();
139 |
140 | sinonAssert.callOrder(
141 | runSpy,
142 | spyInit1,
143 | spyInit2,
144 | spyInit3,
145 | spyExec,
146 | spyExec1,
147 | spyExec2,
148 | spyExec3,
149 | spyWrite1,
150 | spyWrite2,
151 | spyEnd1,
152 | spyEnd2,
153 | );
154 | assert(spyInit1.calledAfter(runSpy));
155 | assert(spyInit2.calledAfter(spyInit1));
156 | assert(spyInit3.calledAfter(spyInit2));
157 | assert(spyExec1.calledAfter(spyInit3));
158 | assert(spyExec2.calledAfter(spyExec1));
159 | assert(spyExec3.calledAfter(spyExec2));
160 | });
161 |
162 | it('runs multiple composed generators inside a running generator', () =>
163 | new Promise(done => {
164 | const Dummy2 = class extends Dummy {};
165 |
166 | const writingSpy1 = sinonSpy();
167 | const writingSpy2 = sinonSpy();
168 | const endSpy = sinonSpy();
169 | Dummy2.prototype.end = endSpy;
170 |
171 | Dummy2.prototype.writing = {
172 | async compose() {
173 | // Initializing and default is queue and called next (before writingSpy2)
174 | // Writing is queue after already queued functions (after writingSpy2)
175 | await this.composeWith(['composed:gen', 'composed:gen2']);
176 | writingSpy1();
177 | },
178 | writingSpy2() {
179 | writingSpy2();
180 | },
181 | };
182 |
183 | dummy2 = new Dummy2([], {
184 | resolved: 'unknown',
185 | namespace: 'dummy',
186 | env: env,
187 | 'skip-install': true,
188 | 'force-install': true,
189 | 'skip-cache': true,
190 | });
191 |
192 | const runSpy = sinonSpy(dummy2, 'run');
193 |
194 | // I use a setTimeout here just to make sure composeWith() doesn't start the
195 | // generator before the base one is ran.
196 | setTimeout(() => {
197 | dummy2.run().then(() => {
198 | sinonAssert.callOrder(
199 | runSpy,
200 | writingSpy1,
201 | spyInit1,
202 | spyInit2,
203 | spyExec1,
204 | spyExec2,
205 | writingSpy2,
206 | spyWrite1,
207 | spyWrite2,
208 | endSpy,
209 | spyEnd1,
210 | spyEnd2,
211 | );
212 | assert(writingSpy1.calledAfter(runSpy));
213 | assert(spyInit1.calledAfter(writingSpy1));
214 | assert(spyInit2.calledAfter(spyInit1));
215 | assert(spyExec1.calledAfter(spyInit2));
216 | assert(spyExec2.calledAfter(spyExec1));
217 | assert(writingSpy2.calledAfter(spyExec2));
218 | assert(spyWrite1.calledAfter(writingSpy2));
219 | assert(spyWrite2.calledAfter(spyWrite1));
220 | assert(endSpy.calledAfter(spyWrite2));
221 | assert(spyEnd1.calledAfter(endSpy));
222 | assert(spyEnd2.calledAfter(spyEnd1));
223 | done();
224 | });
225 | }, 100);
226 | }));
227 |
228 | it('runs multiple composed generators inside a running generator', () =>
229 | new Promise(done => {
230 | const Dummy2 = class extends Dummy {};
231 |
232 | const writingSpy1 = sinonSpy();
233 | const writingSpy2 = sinonSpy();
234 | const writingSpy3 = sinonSpy();
235 | const endSpy = sinonSpy();
236 |
237 | Dummy2.prototype.end = endSpy;
238 | Dummy2.prototype.writing = {
239 | async compose1() {
240 | // Initializing and default is queue and called next (before writingSpy2)
241 | // Writing is queue after already queued functions (after writingSpy2, compose2, writingSpy3)
242 | await this.composeWith('composed:gen');
243 | writingSpy1();
244 | },
245 | writingSpy2() {
246 | writingSpy2();
247 | },
248 | async compose2() {
249 | await this.composeWith('composed:gen2');
250 | },
251 | writingSpy3() {
252 | writingSpy3();
253 | },
254 | };
255 |
256 | dummy2 = new Dummy2([], {
257 | resolved: 'unknown',
258 | namespace: 'dummy',
259 | env: env,
260 | 'skip-install': true,
261 | 'force-install': true,
262 | 'skip-cache': true,
263 | });
264 |
265 | const runSpy = sinonSpy(dummy2, 'run');
266 |
267 | // I use a setTimeout here just to make sure composeWith() doesn't start the
268 | // generator before the base one is ran.
269 | setTimeout(() => {
270 | dummy2.run().then(() => {
271 | sinonAssert.callOrder(
272 | runSpy,
273 | writingSpy1,
274 | spyInit1,
275 | spyExec1,
276 | writingSpy2,
277 | spyInit2,
278 | spyExec2,
279 | writingSpy3,
280 | spyWrite1,
281 | spyWrite2,
282 | endSpy,
283 | spyEnd1,
284 | spyEnd2,
285 | );
286 | assert(writingSpy1.calledAfter(runSpy));
287 | assert(spyInit1.calledAfter(writingSpy1));
288 | assert(spyExec1.calledAfter(spyInit1));
289 | assert(writingSpy2.calledAfter(spyExec1));
290 | assert(spyInit2.calledAfter(writingSpy2));
291 | assert(spyExec2.calledAfter(spyExec1));
292 | assert(writingSpy3.calledAfter(spyExec2));
293 | assert(spyWrite1.calledAfter(writingSpy3));
294 | assert(spyWrite2.calledAfter(spyWrite1));
295 | assert(endSpy.calledAfter(spyWrite2));
296 | assert(spyEnd1.calledAfter(endSpy));
297 | assert(spyEnd2.calledAfter(spyEnd1));
298 | done();
299 | });
300 | }, 100);
301 | }));
302 | });
303 |
304 | it('#composeWith() inside _beforeQueue', async () => {
305 | const Generator = class extends Base {
306 | async _beforeQueue() {
307 | await this.composeWith('composed:gen2');
308 | }
309 | };
310 | const writingSpy1 = sinonSpy();
311 | Generator.prototype.writing = {
312 | compose1() {
313 | writingSpy1();
314 | },
315 | };
316 |
317 | const Generator2 = class extends Base {
318 | async _beforeQueue() {
319 | await this.composeWith('composed:gen3');
320 | }
321 | };
322 | const writingSpy2 = sinonSpy();
323 | Generator2.prototype.writing = {
324 | compose2() {
325 | writingSpy2();
326 | },
327 | };
328 |
329 | const Generator3 = class extends Base {};
330 | const writingSpy3 = sinonSpy();
331 | Generator3.prototype.writing = {
332 | compose3() {
333 | writingSpy3();
334 | },
335 | };
336 |
337 | env.registerStub(Generator, 'composed:gen');
338 | env.registerStub(Generator2, 'composed:gen2');
339 | env.registerStub(Generator3, 'composed:gen3');
340 |
341 | const dummy = new Generator([], {
342 | resolved: 'unknown',
343 | namespace: 'dummy',
344 | env: env,
345 | 'skip-install': true,
346 | 'force-install': true,
347 | 'skip-cache': true,
348 | });
349 |
350 | await dummy.run();
351 | assert(writingSpy2.calledAfter(writingSpy1));
352 | assert(writingSpy3.calledAfter(writingSpy2));
353 | });
354 | });
355 |
--------------------------------------------------------------------------------
/test/generators.test.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'node:events';
2 | import path from 'node:path';
3 | import os from 'node:os';
4 | import { beforeEach, describe, expect, it, vitest } from 'vitest';
5 | import { TestAdapter } from '@yeoman/adapter/testing';
6 | import Environment from 'yeoman-environment';
7 | import assert from 'yeoman-assert';
8 | import { valid as semverValid } from 'semver';
9 | import Base from './utils.js';
10 |
11 | const NAMESPACE = 'somenamespace';
12 | const createEnv = () => new Environment({ skipInstall: true, adapter: new TestAdapter() });
13 |
14 | describe('Generators module', () => {
15 | let generator: Generator;
16 | let env: Environment;
17 |
18 | beforeEach(() => {
19 | env = createEnv();
20 | });
21 |
22 | describe('Base', () => {
23 | beforeEach(() => {
24 | const Generator = class extends Base {};
25 | Generator.prototype.exec = function () {};
26 | generator = new Generator({
27 | env: env,
28 | namespace: NAMESPACE,
29 | resolved: 'test',
30 | });
31 | });
32 |
33 | it('should expose yoGeneratorVersion', () => {
34 | assert(semverValid(generator.yoGeneratorVersion), `Not valid version ${generator.yoGeneratorVersion as string}`);
35 | });
36 |
37 | it('is an EventEmitter', () =>
38 | new Promise(done => {
39 | assert.ok(generator instanceof EventEmitter);
40 | assert.strictEqual(typeof generator.on, 'function');
41 | assert.strictEqual(typeof generator.emit, 'function');
42 | generator.on('yay-o-man', done);
43 | generator.emit('yay-o-man');
44 | }));
45 |
46 | it('emits done event', () =>
47 | new Promise(done => {
48 | env.on(`done$${NAMESPACE}#exec`, data => {
49 | assert(data.generator === generator);
50 | assert(`done$${NAMESPACE}#exec`.includes(data.namespace));
51 | assert(data.namespace === NAMESPACE);
52 | assert(data.priorityName === 'default');
53 | assert(data.queueName === 'default');
54 | done();
55 | });
56 | generator.run().catch(() => {});
57 | }));
58 | });
59 |
60 | it('without localConfigOnly option', () => {
61 | generator = new Base({
62 | env: env,
63 | resolved: 'test',
64 | });
65 | assert.equal(path.join(os.homedir(), '.yo-rc-global.json'), generator._globalConfig.path);
66 | });
67 |
68 | it('with localConfigOnly option', () => {
69 | generator = new Base({
70 | env: env,
71 | resolved: 'test',
72 | localConfigOnly: true,
73 | });
74 | assert.equal(path.join(env.cwd, '.yo-rc-global.json'), generator._globalConfig.path);
75 | });
76 |
77 | describe('#run', () => {
78 | beforeEach(() => {
79 | const Generator = class extends Base {};
80 | Generator.prototype.throwing = () => {
81 | throw new Error('not thrown');
82 | };
83 |
84 | generator = new Generator({
85 | env: env,
86 | resolved: 'test',
87 | });
88 | });
89 |
90 | it('forwards error to environment', () =>
91 | new Promise(done => {
92 | env.on('error', () => {
93 | done();
94 | });
95 | generator.run().catch(() => {});
96 | }));
97 | });
98 |
99 | describe('#createStorage', () => {
100 | beforeEach(() => {
101 | generator = new Base({
102 | env: env,
103 | resolved: 'test',
104 | localConfigOnly: true,
105 | });
106 | });
107 |
108 | it('with path and name', () => {
109 | const global = path.join(env.cwd, '.yo-rc-global.json');
110 | const customStorage = generator.createStorage(global, '*');
111 | assert.equal(global, customStorage.path);
112 | assert.equal('*', customStorage.name);
113 | });
114 |
115 | it('with path', () => {
116 | const global = path.join(env.cwd, '.yo-rc-global.json');
117 | const customStorage = generator.createStorage(global);
118 | assert.equal(global, customStorage.path);
119 | assert.equal(undefined, customStorage.name);
120 | });
121 | });
122 |
123 | describe('#getContextData', () => {
124 | beforeEach(() => {
125 | generator = new Base({
126 | env: env,
127 | resolved: 'test',
128 | localConfigOnly: true,
129 | });
130 | });
131 |
132 | it('non existing key should throw', () => {
133 | expect(() => generator.getContextData('foo')).toThrow('Context data foo not found and no factory provided');
134 | });
135 |
136 | it('non existing key should use factory if provided', () => {
137 | const data = 'bar';
138 | const factory: () => string = vitest.fn().mockReturnValue(data);
139 | expect(generator.getContextData('foo', { factory })).toBe(data);
140 | expect(factory).toHaveBeenCalled();
141 | });
142 |
143 | it('retrieves the data', () => {
144 | const data = 'bar';
145 | generator._contextMap.set('foo', data);
146 | expect(generator.getContextData('foo')).toBe(data);
147 | });
148 |
149 | it('supports custon context', () => {
150 | const context = 'ctx';
151 | const key = 'foo';
152 | const data = 'bar';
153 | const factory: () => string = vitest.fn().mockReturnValue(data);
154 | expect(generator.getContextData({ context, key }, { factory })).toBe(data);
155 | expect(factory).toHaveBeenCalled();
156 | expect(env.getContextMap(context).get(key)).toBe(data);
157 | });
158 |
159 | it('using replacement option sets new data and retrieves old data', () => {
160 | const key = 'foo';
161 | const data = 'bar';
162 | expect(generator.getContextData(key, { replacement: data })).toBe(undefined);
163 | expect(generator.getContextData(key, { replacement: 'new' })).toBe(data);
164 | });
165 |
166 | it('supports replacement option with custon context', () => {
167 | const context = 'ctx';
168 | const key = 'foo';
169 | const data = 'bar';
170 | expect(generator.getContextData({ context, key }, { replacement: data })).toBe(undefined);
171 | expect(generator.getContextData({ context, key }, { replacement: 'new' })).toBe(data);
172 | expect(env.getContextMap(context).get(key)).toBe('new');
173 | });
174 | });
175 |
176 | it('running standalone', () =>
177 | new Promise(done => {
178 | const Generator = class extends Base {};
179 | try {
180 | new Generator();
181 | } catch (error) {
182 | assert.equal(error.message, 'This generator requires an environment.');
183 | done();
184 | }
185 | }));
186 |
187 | it('running with an empty env', () =>
188 | new Promise(done => {
189 | const Generator = class extends Base {};
190 | try {
191 | new Generator({ env: {} });
192 | } catch (error) {
193 | assert.equal(
194 | error.message,
195 | "Current environment doesn't provides some necessary feature this generator needs.",
196 | );
197 | done();
198 | }
199 | }));
200 | });
201 |
--------------------------------------------------------------------------------
/test/integration.test.ts:
--------------------------------------------------------------------------------
1 | import path, { dirname } from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 | import { beforeAll, describe, it } from 'vitest';
4 | import helpers, { result } from 'yeoman-test';
5 |
6 | const _filename = fileURLToPath(import.meta.url);
7 | const _dirname = dirname(_filename);
8 |
9 | describe('Integration', () => {
10 | beforeAll(async () => {
11 | await helpers
12 | .create(path.join(_dirname, 'fixtures/generator-defaults/app'))
13 | .withAnswers({ foo: 'fooValue' })
14 | .withOptions({ extra: 'extraValue' })
15 | .run();
16 | }, 5000);
17 | it('writes prompt value to foo-template.js', () => {
18 | result.assertFileContent('foo-template.js', "fooValue = 'fooValue");
19 | });
20 | it('writes option value foo-template.js', () => {
21 | result.assertFileContent('foo-template.js', 'extraValue');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/package-json.test.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import { beforeEach, describe, expect, it, vi } from 'vitest';
3 | import { lte as semverLte } from 'semver';
4 | import helpers from 'yeoman-test';
5 | import type { BaseEnvironment } from '@yeoman/types';
6 | import Generator from '../src/index.js';
7 |
8 | describe('Base#package-json', () => {
9 | let generator: Generator;
10 | let env: BaseEnvironment;
11 |
12 | beforeEach(async () => {
13 | const context = helpers.create(Generator);
14 | await context.build();
15 | // eslint-disable-next-line prefer-destructuring
16 | generator = context.generator;
17 | // eslint-disable-next-line prefer-destructuring
18 | env = context.env;
19 | generator.exec = vi.fn();
20 | });
21 |
22 | describe('_resolvePackageJsonDependencies()', () => {
23 | it('should accept empty version and resolve', async ctx => {
24 | if (semverLte(env.getVersion(), '3.1.0')) {
25 | ctx.skip();
26 | }
27 | const dependencies = await generator._resolvePackageJsonDependencies('yeoman-generator');
28 | expect(dependencies['yeoman-generator']).toBeDefined();
29 | });
30 |
31 | it('should accept semver version', async () => {
32 | assert.deepStrictEqual(await generator._resolvePackageJsonDependencies('yeoman-generator@^2'), {
33 | 'yeoman-generator': '^2',
34 | });
35 | });
36 |
37 | it('should accept object and return it', async () => {
38 | const a = { 'yeoman-generator': '^4' };
39 | assert.deepStrictEqual(await generator._resolvePackageJsonDependencies(a), a);
40 | });
41 |
42 | it('should resolve object with empty version and resolve', async ctx => {
43 | if (semverLte(env.getVersion(), '3.1.0')) {
44 | ctx.skip();
45 | }
46 |
47 | const a = { 'yeoman-generator': '' };
48 | const dependencies = await generator._resolvePackageJsonDependencies(a);
49 | expect(dependencies['yeoman-generator']);
50 | });
51 |
52 | it('should accept arrays', async () => {
53 | assert.deepStrictEqual(
54 | await generator._resolvePackageJsonDependencies(['yeoman-generator@^2', 'yeoman-environment@^2']),
55 | {
56 | 'yeoman-generator': '^2',
57 | 'yeoman-environment': '^2',
58 | },
59 | );
60 | });
61 | });
62 |
63 | describe('addDependencies()', () => {
64 | it('should generate dependencies inside package.json', async () => {
65 | await generator.addDependencies('yeoman-generator@^2');
66 | assert.deepStrictEqual(generator.packageJson.getAll(), {
67 | dependencies: { 'yeoman-generator': '^2' },
68 | });
69 | });
70 |
71 | it('should accept object and merge inside package.json', async () => {
72 | await generator.addDependencies({ 'yeoman-generator': '^2' });
73 | assert.deepStrictEqual(generator.packageJson.getAll(), {
74 | dependencies: { 'yeoman-generator': '^2' },
75 | });
76 | });
77 | });
78 |
79 | describe('addDependencies()', () => {
80 | it('should generate dependencies inside package.json', async () => {
81 | await generator.addDevDependencies('yeoman-generator@^2');
82 | assert.deepStrictEqual(generator.packageJson.getAll(), {
83 | devDependencies: { 'yeoman-generator': '^2' },
84 | });
85 | });
86 |
87 | it('should accept object and merge devDependencies inside package.json', async () => {
88 | await generator.addDevDependencies({ 'yeoman-generator': '^2' });
89 | assert.deepStrictEqual(generator.packageJson.getAll(), {
90 | devDependencies: { 'yeoman-generator': '^2' },
91 | });
92 | });
93 | });
94 | }, 10_000);
95 |
--------------------------------------------------------------------------------
/test/prompt-suggestion.test.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import assert from 'node:assert';
3 | import os from 'node:os';
4 | import { rmSync } from 'node:fs';
5 | import { afterEach, beforeEach, describe, it } from 'vitest';
6 | import { TestAdapter } from '@yeoman/adapter/testing';
7 | import inquirer from 'inquirer';
8 | import Environment from 'yeoman-environment';
9 | import { create as createMemFsEditor } from 'mem-fs-editor';
10 | import Storage from '../src/util/storage.js';
11 | import { prefillQuestions, storeAnswers } from '../src/util/prompt-suggestion.js';
12 |
13 | const createEnv = () => new Environment({ skipInstall: true, adapter: new TestAdapter() });
14 | /* eslint max-nested-callbacks: ["warn", 6] */
15 |
16 | describe('PromptSuggestion', () => {
17 | let storePath: string;
18 | let store: Storage;
19 |
20 | beforeEach(() => {
21 | const memFs = createEnv().sharedFs;
22 | const fs = createMemFsEditor(memFs);
23 | storePath = path.join(os.tmpdir(), 'suggestion-config.json');
24 | store = new Storage('suggestion', fs, storePath);
25 | store.set('promptValues', { respuesta: 'foo' });
26 | });
27 |
28 | afterEach(() => {
29 | rmSync(storePath, { force: true });
30 | });
31 |
32 | describe('.prefillQuestions()', () => {
33 | it('require a store parameter', () => {
34 | assert.throws(prefillQuestions.bind(null));
35 | });
36 |
37 | it('require a questions parameter', () => {
38 | assert.throws(prefillQuestions.bind(store));
39 | });
40 |
41 | it('take a questions parameter', () => {
42 | prefillQuestions(store, []);
43 | });
44 |
45 | it('take a question object', () => {
46 | const question = {
47 | name: 'respuesta',
48 | default: 'bar',
49 | store: true,
50 | };
51 | const [result] = prefillQuestions(store, question);
52 | assert.equal(result.default, 'foo');
53 | });
54 |
55 | it('take a question array', () => {
56 | const question = [
57 | {
58 | name: 'respuesta',
59 | default: 'bar',
60 | store: true,
61 | },
62 | ];
63 | const [result] = prefillQuestions(store, question);
64 | assert.equal(result.default, 'foo');
65 | });
66 |
67 | it("don't override default when store is set to false", () => {
68 | const question = {
69 | name: 'respuesta',
70 | default: 'bar',
71 | store: false,
72 | };
73 | const [result] = prefillQuestions(store, question);
74 | assert.equal(result.default, 'bar');
75 | });
76 |
77 | it('override default when store is set to true', () => {
78 | const question = {
79 | name: 'respuesta',
80 | default: 'bar',
81 | store: true,
82 | };
83 | const [result] = prefillQuestions(store, question);
84 | assert.equal(result.default, 'foo');
85 | });
86 |
87 | it('keep inquirer objects', () => {
88 | const question = {
89 | type: 'checkbox',
90 | name: 'respuesta',
91 | default: ['bar'],
92 | store: true,
93 | choices: [new inquirer.Separator('spacer')],
94 | };
95 | const [result] = prefillQuestions(store, question);
96 | assert.ok(result.choices[0] instanceof inquirer.Separator);
97 | });
98 |
99 | describe('take a checkbox', () => {
100 | beforeEach(() => {
101 | store.set('promptValues', {
102 | respuesta: ['foo'],
103 | });
104 | });
105 |
106 | it('override default from an array with objects', () => {
107 | const question = {
108 | type: 'checkbox',
109 | name: 'respuesta',
110 | default: ['bar'],
111 | store: true,
112 | choices: [
113 | {
114 | value: 'foo',
115 | name: 'foo',
116 | },
117 | new inquirer.Separator('spacer'),
118 | {
119 | value: 'bar',
120 | name: 'bar',
121 | },
122 | {
123 | value: 'baz',
124 | name: 'baz',
125 | },
126 | ],
127 | };
128 | const [result] = prefillQuestions(store, question);
129 |
130 | for (const choice of result.choices) {
131 | assert.equal(choice.checked, false);
132 | }
133 |
134 | assert.deepEqual(result.default, ['foo']);
135 | });
136 |
137 | it('override default from an array with strings', () => {
138 | const question = {
139 | type: 'checkbox',
140 | name: 'respuesta',
141 | default: ['bar'],
142 | store: true,
143 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
144 | };
145 | const [result] = prefillQuestions(store, question);
146 | assert.deepEqual(result.default, ['foo']);
147 | });
148 |
149 | describe('with multiple defaults', () => {
150 | beforeEach(() => {
151 | store.set('promptValues', {
152 | respuesta: ['foo', 'bar'],
153 | });
154 | });
155 |
156 | it('from an array with objects', () => {
157 | const question = {
158 | type: 'checkbox',
159 | name: 'respuesta',
160 | default: ['bar'],
161 | store: true,
162 | choices: [
163 | {
164 | value: 'foo',
165 | name: 'foo',
166 | },
167 | new inquirer.Separator('spacer'),
168 | {
169 | value: 'bar',
170 | name: 'bar',
171 | },
172 | {
173 | value: 'baz',
174 | name: 'baz',
175 | },
176 | ],
177 | };
178 | const [result] = prefillQuestions(store, question);
179 |
180 | for (const choice of result.choices) {
181 | assert.equal(choice.checked, false);
182 | }
183 |
184 | assert.deepEqual(result.default, ['foo', 'bar']);
185 | });
186 |
187 | it('from an array with strings', () => {
188 | const question = {
189 | type: 'checkbox',
190 | name: 'respuesta',
191 | default: ['bar'],
192 | store: true,
193 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
194 | };
195 | const [result] = prefillQuestions(store, question);
196 | assert.deepEqual(result.default, ['foo', 'bar']);
197 | });
198 | });
199 | });
200 |
201 | describe('take a checkbox with choices from a function', () => {
202 | beforeEach(() => {
203 | store.set('promptValues', {
204 | respuesta: ['foo'],
205 | });
206 | });
207 |
208 | it('does not override default from an array with objects', () => {
209 | const question = {
210 | type: 'checkbox',
211 | name: 'respuesta',
212 | default: ['bar'],
213 | store: true,
214 | choices: () => [
215 | {
216 | value: 'foo',
217 | name: 'foo',
218 | },
219 | new inquirer.Separator('spacer'),
220 | {
221 | value: 'bar',
222 | name: 'bar',
223 | },
224 | {
225 | value: 'baz',
226 | name: 'baz',
227 | },
228 | ],
229 | };
230 | const [result] = prefillQuestions(store, question);
231 |
232 | assert.deepEqual(result.default, ['bar']);
233 | });
234 |
235 | it('does not override default from an array with strings', () => {
236 | const question = {
237 | type: 'checkbox',
238 | name: 'respuesta',
239 | default: ['bar'],
240 | store: true,
241 | choices: () => ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
242 | };
243 | const [result] = prefillQuestions(store, question);
244 | assert.deepEqual(result.default, ['bar']);
245 | });
246 |
247 | describe('does not override even with multiple defaults', () => {
248 | beforeEach(() => {
249 | store.set('promptValues', {
250 | respuesta: ['foo', 'bar'],
251 | });
252 | });
253 |
254 | it('from an array with objects', () => {
255 | const question = {
256 | type: 'checkbox',
257 | name: 'respuesta',
258 | default: ['bar'],
259 | store: true,
260 | choices: () => [
261 | {
262 | value: 'foo',
263 | name: 'foo',
264 | },
265 | new inquirer.Separator('spacer'),
266 | {
267 | value: 'bar',
268 | name: 'bar',
269 | },
270 | {
271 | value: 'baz',
272 | name: 'baz',
273 | },
274 | ],
275 | };
276 | const [result] = prefillQuestions(store, question);
277 |
278 | assert.deepEqual(result.default, ['bar']);
279 | });
280 |
281 | it('from an array with strings', () => {
282 | const question = {
283 | type: 'checkbox',
284 | name: 'respuesta',
285 | default: ['bar'],
286 | store: true,
287 | choices: () => ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
288 | };
289 | const [result] = prefillQuestions(store, question);
290 | assert.deepEqual(result.default, ['bar']);
291 | });
292 | });
293 | });
294 |
295 | describe('take a rawlist / expand', () => {
296 | beforeEach(() => {
297 | store.set('promptValues', {
298 | respuesta: 'bar',
299 | });
300 | });
301 |
302 | it('override default arrayWithObjects', () => {
303 | const question = {
304 | type: 'rawlist',
305 | name: 'respuesta',
306 | default: 0,
307 | store: true,
308 | choices: [
309 | {
310 | value: 'foo',
311 | name: 'foo',
312 | },
313 | new inquirer.Separator('spacer'),
314 | {
315 | value: 'bar',
316 | name: 'bar',
317 | },
318 | {
319 | value: 'baz',
320 | name: 'baz',
321 | },
322 | ],
323 | };
324 | const [result] = prefillQuestions(store, question);
325 | assert.equal(result.default, 2);
326 | });
327 |
328 | it('override default arrayWithObjects', () => {
329 | const question = {
330 | type: 'rawlist',
331 | name: 'respuesta',
332 | default: 0,
333 | store: true,
334 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
335 | };
336 | const [result] = prefillQuestions(store, question);
337 | assert.equal(result.default, 2);
338 | });
339 | });
340 | });
341 |
342 | describe('.storeAnswers()', () => {
343 | beforeEach(() => {
344 | store.set('promptValues', { respuesta: 'foo' });
345 | });
346 |
347 | it('require a store parameter', () => {
348 | assert.throws(storeAnswers.bind(null));
349 | });
350 |
351 | it('require a question parameter', () => {
352 | assert.throws(storeAnswers.bind(store));
353 | });
354 |
355 | it('require a answer parameter', () => {
356 | assert.throws(storeAnswers.bind(store, []));
357 | });
358 |
359 | it('take a answer parameter', () => {
360 | storeAnswers(store, [], {});
361 | });
362 |
363 | it('take a storeAll parameter', () => {
364 | storeAnswers(store, [], {}, true);
365 | });
366 |
367 | it('store answer in global store', () => {
368 | const question = {
369 | name: 'respuesta',
370 | default: 'bar',
371 | store: true,
372 | };
373 |
374 | const mockAnswers = {
375 | respuesta: 'baz',
376 | };
377 |
378 | prefillQuestions(store, question);
379 | storeAnswers(store, question, mockAnswers);
380 | assert.equal(store.get('promptValues').respuesta, 'baz');
381 | });
382 |
383 | it("don't store default answer in global store", () => {
384 | const question = {
385 | name: 'respuesta',
386 | default: 'bar',
387 | store: true,
388 | };
389 |
390 | const mockAnswers = {
391 | respuesta: 'bar',
392 | };
393 |
394 | store.delete('promptValues');
395 | prefillQuestions(store, question);
396 | storeAnswers(store, question, mockAnswers, false);
397 | assert.equal(store.get('promptValues'), undefined);
398 | });
399 |
400 | it('force store default answer in global store', () => {
401 | const question = {
402 | name: 'respuesta',
403 | default: 'bar',
404 | store: true,
405 | };
406 |
407 | const mockAnswers = {
408 | respuesta: 'bar',
409 | };
410 |
411 | store.delete('promptValues');
412 | prefillQuestions(store, question);
413 | storeAnswers(store, question, mockAnswers, true);
414 | assert.equal(store.get('promptValues').respuesta, 'bar');
415 | });
416 |
417 | it("don't store answer in global store", () => {
418 | const question = {
419 | name: 'respuesta',
420 | default: 'bar',
421 | store: false,
422 | };
423 |
424 | const mockAnswers = {
425 | respuesta: 'baz',
426 | };
427 |
428 | prefillQuestions(store, question);
429 | storeAnswers(store, question, mockAnswers);
430 | assert.equal(store.get('promptValues').respuesta, 'foo');
431 | });
432 |
433 | it('store answer from rawlist type', () => {
434 | const question = {
435 | type: 'rawlist',
436 | name: 'respuesta',
437 | default: 0,
438 | store: true,
439 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
440 | };
441 |
442 | const mockAnswers = {
443 | respuesta: 'baz',
444 | };
445 |
446 | prefillQuestions(store, question);
447 | storeAnswers(store, question, mockAnswers);
448 | assert.equal(store.get('promptValues').respuesta, 'baz');
449 | });
450 |
451 | describe('empty store', () => {
452 | beforeEach(() => {
453 | store.delete('promptValues');
454 | });
455 | it("don't store default answer from rawlist type", () => {
456 | const question = {
457 | type: 'rawlist',
458 | name: 'respuesta',
459 | default: 0,
460 | store: true,
461 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
462 | };
463 |
464 | const mockAnswers = {
465 | respuesta: 'foo',
466 | };
467 |
468 | prefillQuestions(store, question);
469 | storeAnswers(store, question, mockAnswers, false);
470 | assert.equal(store.get('promptValues'), undefined);
471 | });
472 |
473 | it('force store default answer from rawlist type', () => {
474 | const question = {
475 | type: 'rawlist',
476 | name: 'respuesta',
477 | default: 0,
478 | store: true,
479 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'],
480 | };
481 |
482 | const mockAnswers = {
483 | respuesta: 'foo',
484 | };
485 |
486 | prefillQuestions(store, question);
487 | storeAnswers(store, question, mockAnswers, true);
488 | assert.equal(store.get('promptValues').respuesta, 'foo');
489 | });
490 | });
491 |
492 | it('store falsy answer (but not undefined) in global store', () => {
493 | const question = {
494 | name: 'respuesta',
495 | default: true,
496 | store: true,
497 | };
498 |
499 | const mockAnswers = {
500 | respuesta: false,
501 | };
502 |
503 | prefillQuestions(store, question);
504 | storeAnswers(store, question, mockAnswers);
505 | assert.equal(store.get('promptValues').respuesta, false);
506 | });
507 | });
508 | });
509 |
--------------------------------------------------------------------------------
/test/spawn-command.test.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import Generator from '../src/index.js';
3 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4 | import { spy } from 'sinon';
5 | import { execa, execaCommand, execaCommandSync, execaSync } from 'execa';
6 |
7 | vi.mock('execa', () => ({
8 | execa: vi.fn(),
9 | execaSync: vi.fn(),
10 | execaCommand: vi.fn(),
11 | execaCommandSync: vi.fn(),
12 | }));
13 |
14 | describe('generators.Base (actions/spawn-command)', () => {
15 | let testGenerator: Generator;
16 |
17 | beforeEach(async () => {
18 | testGenerator = new Generator({ help: true, namespace: 'foo', resolved: 'unknown' });
19 | testGenerator.destinationRoot = vi.fn().mockReturnValue('some/destination/path');
20 | });
21 |
22 | afterEach(() => {
23 | vi.restoreAllMocks();
24 | });
25 |
26 | describe('#spawnCommand()', () => {
27 | describe('only the command is required', () => {
28 | describe('no args and no options are given', () => {
29 | it('calls execaCommandSync with the command, {stdio: "inherit", cwd: this.destinationRoot()}', () => {
30 | testGenerator.spawnCommand('foo');
31 | expect(execaCommand).toHaveBeenCalledWith('foo', {
32 | cwd: testGenerator.destinationRoot(),
33 | });
34 | });
35 | });
36 | });
37 |
38 | it('opts given are passed to spawnCommand', () => {
39 | const spawnSpy = spy(testGenerator, 'spawnCommand');
40 | testGenerator.spawnCommand('foo', { verbose: true });
41 | assert.ok(spawnSpy.calledWith('foo', { verbose: true }));
42 | });
43 | });
44 |
45 | describe('#spawn() calls execa()', () => {
46 | describe('only the command is required', () => {
47 | describe('no args and no options are given', () => {
48 | it('calls execaSync with the command, args, {stdio: "inherit", cwd: this.destinationRoot()}', () => {
49 | testGenerator.spawn('foo');
50 | expect(execa).toHaveBeenCalledWith('foo', undefined, {
51 | cwd: testGenerator.destinationRoot(),
52 | });
53 | });
54 | });
55 | });
56 |
57 | it('passes any args and opts along to execa()', () => {
58 | // @ts-expect-error We know that spawn exists on the generator. It is added with applyMixins().
59 | testGenerator.spawn('foo', ['arg1', 2, 'the third arg'], { verbose: true });
60 | expect(execa).toHaveBeenCalledWith('foo', ['arg1', 2, 'the third arg'], {
61 | cwd: testGenerator.destinationRoot(),
62 | verbose: true,
63 | });
64 | });
65 |
66 | it('can override default stdio option', () => {
67 | testGenerator.spawn('foo', undefined, { stdio: 'pipe' });
68 | expect(execa).toHaveBeenCalledWith('foo', undefined, {
69 | cwd: testGenerator.destinationRoot(),
70 | stdio: 'pipe',
71 | });
72 | });
73 | });
74 |
75 | describe('#spawnCommandSync()', () => {
76 | describe('only the command is required', () => {
77 | describe('no args and no options are given', () => {
78 | it('calls execaCommandSync with the command, {stdio: "inherit", cwd: this.destinationRoot()}', () => {
79 | testGenerator.spawnCommandSync('foo');
80 | expect(execaCommandSync).toHaveBeenCalledWith('foo', {
81 | cwd: testGenerator.destinationRoot(),
82 | });
83 | });
84 | });
85 | });
86 |
87 | it('opts given are passed to spawnCommandSync', () => {
88 | const spawnSyncSpy = spy(testGenerator, 'spawnCommandSync');
89 | testGenerator.spawnCommandSync('foo', { verbose: true });
90 | assert.ok(spawnSyncSpy.calledWith('foo', { verbose: true }));
91 | });
92 | });
93 |
94 | describe('#spawnSync() calls execaSync', () => {
95 | describe('only the command is required', () => {
96 | describe('no args and no options are given', () => {
97 | it('calls execaSync with the command, args, {stdio: "inherit", cwd: this.destinationRoot()}', () => {
98 | testGenerator.spawnSync('foo');
99 | expect(execaSync).toHaveBeenCalledWith('foo', undefined, {
100 | cwd: testGenerator.destinationRoot(),
101 | });
102 | });
103 | });
104 | });
105 |
106 | it('passes any args and opts along to execaSync()', () => {
107 | testGenerator.spawnSync('foo', ['arg1', 2, 'the third arg'], { verbose: true });
108 | expect(execaSync).toHaveBeenCalledWith('foo', ['arg1', 2, 'the third arg'], {
109 | cwd: testGenerator.destinationRoot(),
110 | verbose: true,
111 | });
112 | });
113 |
114 | it('can override default stdio option', () => {
115 | testGenerator.spawnSync('foo', undefined, { stdio: 'pipe' });
116 | expect(execaSync).toHaveBeenCalledWith('foo', undefined, {
117 | cwd: testGenerator.destinationRoot(),
118 | stdio: 'pipe',
119 | });
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/test/storage.test.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import fs from 'node:fs';
3 | import os from 'node:os';
4 | import path, { dirname } from 'node:path';
5 | import { fileURLToPath } from 'node:url';
6 | import process from 'node:process';
7 | import { type MemFsEditor, create as createMemFsEditor } from 'mem-fs-editor';
8 | import helpers from 'yeoman-test';
9 | import { type Store, create as createMemFs } from 'mem-fs';
10 | import Storage from '../src/util/storage.js';
11 | import { afterEach, beforeEach, describe, it } from 'vitest';
12 |
13 | const _filename = fileURLToPath(import.meta.url);
14 | const _dirname = dirname(_filename);
15 |
16 | const tmpdir: string = path.join(os.tmpdir(), 'yeoman-storage');
17 |
18 | function rm(filepath: string) {
19 | if (fs.existsSync(filepath)) {
20 | fs.unlinkSync(filepath);
21 | }
22 | }
23 |
24 | describe('Storage', () => {
25 | let beforeDir: string;
26 | let storePath: string;
27 | let memFsInstance: Store;
28 | let editor: MemFsEditor;
29 |
30 | beforeEach(async () => {
31 | await helpers.prepareTemporaryDir();
32 | });
33 |
34 | beforeEach(() => {
35 | beforeDir = process.cwd();
36 | storePath = path.join(tmpdir, 'new-config.json');
37 | memFsInstance = createMemFs();
38 | editor = createMemFsEditor(memFsInstance);
39 | });
40 |
41 | afterEach(() => {
42 | if (fs.existsSync(storePath)) {
43 | const json = editor.read(storePath);
44 | assert.ok(json.endsWith('\n'));
45 | assert.ok(!json.endsWith('\n\n'));
46 | rm(storePath);
47 | process.chdir(beforeDir);
48 | }
49 | });
50 |
51 | describe('.constructor()', () => {
52 | it('require a parameter', () => {
53 | assert.throws(() => {
54 | new Storage();
55 | });
56 | });
57 |
58 | it('require at least 2 parameter', () => {
59 | assert.throws(() => {
60 | new Storage({});
61 | });
62 | });
63 |
64 | it('take a path parameter', () => {
65 | const store = new Storage('test', editor, path.join(_dirname, './fixtures/config.json'));
66 | assert.equal(store.get('testFramework'), 'mocha');
67 | assert.ok(store.existed);
68 | });
69 |
70 | it('take a fs and path parameter without name', () => {
71 | const store = new Storage(editor, path.join(_dirname, './fixtures/config.json'));
72 | assert.equal(store.get('test')!.testFramework, 'mocha');
73 | assert.ok(store.existed);
74 | });
75 | });
76 |
77 | it('a config path is required', () => {
78 | assert.throws(() => {
79 | new Storage('yo', editor);
80 | });
81 | });
82 |
83 | describe('#get()', () => {
84 | let store: Storage;
85 |
86 | beforeEach(() => {
87 | store = new Storage('test', editor, storePath);
88 | store.set('foo', 'bar');
89 | });
90 |
91 | it('namespace each store sharing the same store file', () => {
92 | const localStore = new Storage('foobar', editor, storePath);
93 | localStore.set('foo', 'something else');
94 | assert.equal(store.get('foo'), 'bar');
95 | });
96 |
97 | beforeEach(() => {
98 | store.set('testFramework', 'mocha');
99 | store.set('name', 'test');
100 | });
101 |
102 | it('get values', () => {
103 | assert.equal(store.get('testFramework'), 'mocha');
104 | assert.equal(store.get('name'), 'test');
105 | });
106 | });
107 |
108 | describe('#set()', () => {
109 | let store: Storage;
110 |
111 | beforeEach(() => {
112 | store = new Storage('test', editor, storePath);
113 | store.set('foo', 'bar');
114 | });
115 |
116 | it('set values', () => {
117 | store.set('name', 'Yeoman!');
118 | assert.equal(store.get('name'), 'Yeoman!');
119 | });
120 |
121 | it('set multiple values at once', () => {
122 | store.set({ foo: 'bar', john: 'doe' });
123 | assert.equal(store.get('foo'), 'bar');
124 | assert.equal(store.get('john'), 'doe');
125 | });
126 |
127 | it('throws when invalid JSON values are passed', function () {
128 | assert.throws(store.set.bind(this, 'foo', () => {}));
129 | });
130 |
131 | it('save on each changes', () => {
132 | store.set('foo', 'bar');
133 | assert.equal(editor.readJSON(storePath).test.foo, 'bar');
134 | store.set('foo', 'oo');
135 | assert.equal(editor.readJSON(storePath).test.foo, 'oo');
136 | });
137 |
138 | describe('@return', () => {
139 | beforeEach(() => {
140 | storePath = path.join(tmpdir, 'setreturn.json');
141 | store = new Storage('test', editor, storePath);
142 | });
143 |
144 | afterEach(() => {
145 | rm(storePath);
146 | });
147 |
148 | it('the saved value (with key)', () => {
149 | assert.equal(store.set('name', 'Yeoman!'), 'Yeoman!');
150 | });
151 |
152 | it('the saved value (without key)', () => {
153 | assert.deepEqual(store.set({ foo: 'bar', john: 'doe' }), {
154 | foo: 'bar',
155 | john: 'doe',
156 | });
157 | });
158 |
159 | it('the saved value (update values)', () => {
160 | store.set({ foo: 'bar', john: 'doe' });
161 | assert.deepEqual(store.set({ foo: 'moo' }), {
162 | foo: 'moo',
163 | john: 'doe',
164 | });
165 | });
166 | });
167 |
168 | describe('when multiples instances share the same file', () => {
169 | let store: Storage;
170 | let store2: Storage;
171 |
172 | beforeEach(() => {
173 | store = new Storage('test', editor, storePath);
174 | store.set('foo', 'bar');
175 | store2 = new Storage('test2', editor, storePath);
176 | });
177 |
178 | it('only update modified namespace', () => {
179 | store2.set('bar', 'foo');
180 | store.set('foo', 'bar');
181 |
182 | const json = editor.readJSON(storePath);
183 | assert.equal(json.test.foo, 'bar');
184 | assert.equal(json.test2.bar, 'foo');
185 | });
186 | });
187 |
188 | describe('when multiples instances share the same namespace', () => {
189 | let store: Storage;
190 | let store2: Storage;
191 |
192 | beforeEach(() => {
193 | store = new Storage('test', editor, storePath);
194 | store.set('foo', 'bar');
195 | store2 = new Storage('test', editor, storePath);
196 | });
197 |
198 | it('only update modified namespace', () => {
199 | store2.set('bar', 'foo');
200 | store.set('foo', 'bar');
201 |
202 | assert.equal(store2.get('foo'), 'bar');
203 | assert.equal(store.get('bar'), 'foo');
204 |
205 | const json = editor.readJSON(storePath);
206 | assert.equal(json.test.foo, 'bar');
207 | assert.equal(json.test.bar, 'foo');
208 | });
209 | });
210 | });
211 |
212 | describe('#getAll()', () => {
213 | let store: Storage;
214 |
215 | beforeEach(() => {
216 | store = new Storage('test', editor, storePath);
217 | store.set('foo', 'bar');
218 | });
219 |
220 | beforeEach(() => {
221 | store.set({ foo: 'bar', john: 'doe' });
222 | });
223 |
224 | it('get all values', () => {
225 | assert.deepEqual(store.getAll().foo, 'bar');
226 | });
227 |
228 | it('does not return a reference to the inner store', () => {
229 | store.getAll().foo = 'uhoh';
230 | assert.equal(store.getAll().foo, 'bar');
231 | });
232 | });
233 |
234 | describe('#delete()', () => {
235 | let store: Storage;
236 |
237 | beforeEach(() => {
238 | store = new Storage('test', editor, storePath);
239 | store.set('foo', 'bar');
240 | store.set('name', 'test');
241 | });
242 |
243 | it('delete value', () => {
244 | store.delete('name');
245 | assert.equal(store.get('name'), undefined);
246 | });
247 | });
248 |
249 | describe('#defaults()', () => {
250 | let store: Storage;
251 |
252 | beforeEach(() => {
253 | store = new Storage('test', editor, storePath);
254 | store.set('foo', 'bar');
255 | store.set('val1', 1);
256 | });
257 |
258 | it('set defaults values if not predefined', () => {
259 | store.defaults({ val1: 3, val2: 4 });
260 |
261 | assert.equal(store.get('val1'), 1);
262 | assert.equal(store.get('val2'), 4);
263 | });
264 |
265 | it('require an Object as argument', () => {
266 | assert.throws(store.defaults.bind(store, 'foo'));
267 | });
268 |
269 | describe('@return', () => {
270 | beforeEach(() => {
271 | storePath = path.join(tmpdir, 'defaultreturn.json');
272 | store = new Storage('test', editor, storePath);
273 | store.set('val1', 1);
274 | store.set('foo', 'bar');
275 | });
276 |
277 | afterEach(() => {
278 | rm(storePath);
279 | });
280 |
281 | it('the saved value when passed an empty object', () => {
282 | assert.deepEqual(store.defaults({}), { foo: 'bar', val1: 1 });
283 | });
284 |
285 | it('the saved value when passed the same key', () => {
286 | assert.deepEqual(store.defaults({ foo: 'baz' }), {
287 | foo: 'bar',
288 | val1: 1,
289 | });
290 | });
291 |
292 | it('the saved value when passed new key', () => {
293 | assert.deepEqual(store.defaults({ food: 'pizza' }), {
294 | foo: 'bar',
295 | val1: 1,
296 | food: 'pizza',
297 | });
298 | });
299 | });
300 | });
301 |
302 | describe('#merge()', () => {
303 | let store: Storage;
304 |
305 | beforeEach(() => {
306 | store = new Storage('test', editor, storePath);
307 | store.set('foo', 'bar');
308 | store.set('val1', 1);
309 | });
310 |
311 | it('should merge values if not predefined', () => {
312 | store.merge({ val1: 3, val2: 4 });
313 |
314 | assert.strictEqual(store.get('val1'), 3);
315 | assert.strictEqual(store.get('val2'), 4);
316 | });
317 |
318 | it('should require an Object as argument', () => {
319 | assert.throws(store.defaults.bind(store, 'foo'));
320 | });
321 |
322 | describe('@return', () => {
323 | beforeEach(() => {
324 | storePath = path.join(tmpdir, 'defaultreturn.json');
325 | store = new Storage('test', editor, storePath);
326 | store.set('val1', 1);
327 | store.set('foo', 'bar');
328 | });
329 |
330 | afterEach(() => {
331 | rm(storePath);
332 | });
333 |
334 | it('should return the original object', () => {
335 | assert.deepStrictEqual(store.merge({}), { foo: 'bar', val1: 1 });
336 | });
337 |
338 | it('should return an object with replaced values', () => {
339 | assert.deepStrictEqual(store.merge({ foo: 'baz' }), {
340 | foo: 'baz',
341 | val1: 1,
342 | });
343 | });
344 |
345 | it('should return an object with new values', () => {
346 | assert.deepStrictEqual(store.merge({ food: 'pizza' }), {
347 | foo: 'bar',
348 | val1: 1,
349 | food: 'pizza',
350 | });
351 | });
352 | });
353 | });
354 |
355 | describe('with namespace', () => {
356 | let store: Storage;
357 |
358 | beforeEach(() => {
359 | store = new Storage('test', editor, storePath);
360 | store.set('foo', 'bar');
361 | });
362 |
363 | it('stores sharing the same store file with and without namespace', () => {
364 | const localstore = new Storage(editor, storePath);
365 | localstore.set('test', { bar: 'foo' });
366 | assert.equal(store.get('bar'), 'foo');
367 | });
368 | });
369 |
370 | describe('#getPath() & #setPath()', () => {
371 | let store: Storage;
372 |
373 | beforeEach(() => {
374 | store = new Storage('test', editor, storePath);
375 | store.set('foo', 'bar');
376 | });
377 |
378 | it('#getPath() & #setPath()', () => {
379 | store.set('name', { name: 'test' });
380 | assert.ok(store.getPath('name'));
381 | assert.equal(store.getPath('name.name'), 'test');
382 | assert.equal(store.setPath('name.name', 'changed'), 'changed');
383 | assert.equal(store.getPath('name.name'), 'changed');
384 | assert.equal(store.get('name').name, 'changed');
385 | });
386 | });
387 |
388 | describe('#getStorage()', () => {
389 | let store: Storage;
390 |
391 | beforeEach(() => {
392 | store = new Storage('test', editor, storePath);
393 | store.set('foo', 'bar');
394 | });
395 | describe('with a path safe string', () => {
396 | let pathStore: Storage;
397 | beforeEach(() => {
398 | pathStore = store.createStorage('path');
399 | });
400 |
401 | it('should get and set value', () => {
402 | assert.equal(pathStore.setPath('name', 'initial'), 'initial');
403 | assert.equal(store.get('path').name, 'initial');
404 | assert.equal(store.getPath('path').name, 'initial');
405 | store.set('path', { name: 'test' });
406 | assert.equal(pathStore.get('name'), 'test');
407 | pathStore.set('name', 'changed');
408 | assert.equal(store.get('path').name, 'changed');
409 | });
410 | });
411 | describe('with a path unsafe string', () => {
412 | const keyName = 'path.key';
413 | let pathStore: Storage;
414 |
415 | beforeEach(() => {
416 | pathStore = store.createStorage(`["${keyName}"]`);
417 | });
418 |
419 | it('should get and set value', () => {
420 | assert.equal(pathStore.setPath('name', 'initial'), 'initial');
421 | assert.equal(store.get(keyName).name, 'initial');
422 | assert.equal(store.getPath(`["${keyName}"]`).name, 'initial');
423 | store.set(keyName, { name: 'test' });
424 | assert.equal(pathStore.get('name'), 'test');
425 | pathStore.set('name', 'changed');
426 | assert.equal(store.get(keyName).name, 'changed');
427 | });
428 | });
429 | });
430 |
431 | describe('.constructor() with lodashPath', () => {
432 | let store: Storage;
433 | let pathStore: Storage;
434 |
435 | beforeEach(() => {
436 | store = new Storage('test', editor, storePath);
437 | store.set('foo', 'bar');
438 | pathStore = new Storage('test.path', editor, storePath, { lodashPath: true });
439 | });
440 |
441 | it('get and set value', () => {
442 | assert.equal(pathStore.setPath('name', 'initial'), 'initial');
443 | assert.equal(store.get('path').name, 'initial');
444 | store.set('path', { name: 'test' });
445 | assert.equal(pathStore.get('name'), 'test');
446 | pathStore.set('name', 'changed');
447 | assert.equal(store.get('path').name, 'changed');
448 | });
449 | });
450 |
451 | describe('#createProxy()', () => {
452 | let store: Storage;
453 | let proxy;
454 | beforeEach(() => {
455 | store = new Storage('test', editor, storePath);
456 | store.set('foo', 'bar');
457 | proxy = store.createProxy();
458 | });
459 |
460 | it('sets values', () => {
461 | proxy.name = 'Yeoman!';
462 | assert.equal(store.get('name'), 'Yeoman!');
463 | });
464 |
465 | it('sets multiple values at once', () => {
466 | Object.assign(proxy, { foo: 'bar', john: 'doe' });
467 | assert.equal(store.get('foo'), 'bar');
468 | assert.equal(store.get('john'), 'doe');
469 | });
470 |
471 | it('gets values', () => {
472 | store.set('name', 'Yeoman!');
473 | assert.equal(proxy.name, 'Yeoman!');
474 | });
475 |
476 | it('works with spread operator', () => {
477 | store.set({ foo: 'bar', john: 'doe' });
478 |
479 | const spread = { ...proxy };
480 | assert.equal(spread.foo, 'bar');
481 | assert.equal(spread.john, 'doe');
482 | });
483 |
484 | it('works with in operator', () => {
485 | store.set({ foo: 'bar', john: 'doe' });
486 | assert('foo' in proxy);
487 | assert(!('foo2' in proxy));
488 | });
489 |
490 | it('works with deepEquals', () => {
491 | store.set({ foo: 'bar', john: 'doe' });
492 | assert.deepStrictEqual({ ...proxy }, { foo: 'bar', john: 'doe' });
493 | });
494 | });
495 |
496 | describe('caching', () => {
497 | let store: Storage;
498 |
499 | beforeEach(() => {
500 | store = new Storage('test', editor, storePath);
501 | store.set('foo', 'bar');
502 |
503 | // Make sure the cache is loaded.
504 | // on instantiation a change event is emitted when the file loads to mem-fs
505 | store.get('foo');
506 | });
507 |
508 | it('should load', () => {
509 | assert(store._cachedStore);
510 | });
511 |
512 | it('should not load when disabled', () => {
513 | const store = new Storage('test', editor, storePath, {
514 | disableCache: true,
515 | });
516 | assert(store._cachedStore === undefined);
517 | store.get('foo');
518 | assert(store._cachedStore === undefined);
519 | });
520 |
521 | it('cleanups when the file changes', () => {
522 | editor.writeJSON(store.path, {});
523 | assert(store._cachedStore === undefined);
524 | });
525 |
526 | it("doesn't cleanup when another file changes", () => {
527 | editor.write('a.txt', 'anything');
528 | assert(store._cachedStore);
529 | });
530 |
531 | it('cleanups when per file cache is disabled and another file changes', () => {
532 | editor.writeJSON(store.path, { disableCacheByFile: true });
533 | editor.write('a.txt', 'anything');
534 | assert(store._cachedStore === undefined);
535 | });
536 |
537 | // Compatibility for mem-fs <= 1.1.3
538 | it('cleanups when change event argument is undefined', () => {
539 | memFsInstance.emit('change');
540 | assert(store._cachedStore === undefined);
541 | });
542 | });
543 |
544 | describe('non sorted store', () => {
545 | let store: Storage;
546 |
547 | beforeEach(() => {
548 | store = new Storage('test', editor, storePath);
549 | store.set('foo', 'bar');
550 | store.set('bar', 'foo');
551 | store.set('array', [3, 2, 1]);
552 | });
553 | it('should write non sorted file', () => {
554 | assert.strictEqual(
555 | editor.read(storePath),
556 | `{
557 | "test": {
558 | "foo": "bar",
559 | "bar": "foo",
560 | "array": [
561 | 3,
562 | 2,
563 | 1
564 | ]
565 | }
566 | }
567 | `,
568 | );
569 | });
570 | });
571 |
572 | describe('sorted store', () => {
573 | let store: Storage;
574 |
575 | beforeEach(() => {
576 | store = new Storage('test', editor, storePath, {
577 | sorted: true,
578 | });
579 | store.set('foo', 'bar');
580 | store.set('bar', 'foo');
581 | store.set('array', [3, 2, 1]);
582 | store.set('object', { b: 'shouldBeLast', a: 'shouldBeFirst' });
583 | });
584 | it('should write sorted file', () => {
585 | assert.strictEqual(
586 | editor.read(storePath),
587 | `{
588 | "test": {
589 | "array": [
590 | 3,
591 | 2,
592 | 1
593 | ],
594 | "bar": "foo",
595 | "foo": "bar",
596 | "object": {
597 | "a": "shouldBeFirst",
598 | "b": "shouldBeLast"
599 | }
600 | }
601 | }
602 | `,
603 | );
604 | });
605 | });
606 | });
607 |
--------------------------------------------------------------------------------
/test/user.test.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert';
2 | import { beforeEach, describe, it } from 'vitest';
3 | import { simpleGit } from 'simple-git';
4 | import helpers from 'yeoman-test';
5 | import Generator from '../src/index.js';
6 |
7 | /* eslint max-nested-callbacks: ["warn", 5] */
8 |
9 | describe('Base#user', () => {
10 | let user: Generator;
11 |
12 | beforeEach(async () => {
13 | const context = helpers.create(Generator);
14 | await context.build();
15 | user = context.generator;
16 | const git = simpleGit();
17 | await git.init().addConfig('user.name', 'Yeoman').addConfig('user.email', 'yo@yeoman.io');
18 | });
19 |
20 | describe('.git', () => {
21 | describe('.name()', () => {
22 | it('is the name used by git', async () => {
23 | assert.equal(await user.git.name(), 'Yeoman');
24 | });
25 | });
26 |
27 | describe('.email()', () => {
28 | it('is the email used by git', async () => {
29 | assert.equal(await user.git.email(), 'yo@yeoman.io');
30 | });
31 | });
32 | });
33 |
34 | describe.skip('.github', () => {
35 | describe('.username()', () => {
36 | /*
37 | Fetch mocking is not working as expected
38 | beforeEach(() => {
39 | nock('https://api.github.com')
40 | .filteringPath(/q=[^&]*\/g, 'q=XXX')
41 | .get('/search/users?q=XXX')
42 | .times(1)
43 | .reply(200, {
44 | items: [{ login: 'mockname' }],
45 | });
46 | });
47 |
48 | afterEach(() => {
49 | nock.restore();
50 | });
51 | */
52 |
53 | it('is the username used by GitHub', async () => {
54 | assert.equal(await user.github.username(), 'mockname');
55 | });
56 | });
57 | });
58 | }, 10_000);
59 |
--------------------------------------------------------------------------------
/test/utils.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url';
2 | import Base from '../src/index.js';
3 |
4 | const _filename = fileURLToPath(import.meta.url);
5 |
6 | export default class BaseTest extends Base {
7 | constructor(
8 | options: Omit & Partial>,
9 | features?: Base['features'],
10 | );
11 | constructor(
12 | args: string[],
13 | options?: Omit &
14 | Partial>,
15 | features?: Base['features'],
16 | );
17 | constructor(...args: any[]) {
18 | const optIndex = Array.isArray(args[0]) ? 1 : 0;
19 | args[optIndex] = args[optIndex] ?? {};
20 | args[optIndex].resolved = args[optIndex].resolved ?? _filename;
21 | args[optIndex].namespace = args[optIndex].namespace ?? 'yeoman:testnamespace';
22 | super(...args);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/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": [
7 | "ES2022"
8 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
9 | "types": ["node", "inquirer"],
10 |
11 | /* Modules */
12 | "module": "node16" /* Specify what module code is generated. */,
13 | "rootDir": "./src" /* Specify the root folder within your source files. */,
14 | "resolveJsonModule": true,
15 |
16 | /* JavaScript Support */
17 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
18 |
19 | /* Emit */
20 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
21 | "declaration": true,
22 |
23 | /* Interop Constraints */
24 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
25 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
26 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
27 |
28 | /* Type Checking */
29 | "strict": true /* Enable all strict type-checking options. */,
30 | "noImplicitAny": true,
31 |
32 | /* Completeness */
33 | "skipLibCheck": false /* Skip type checking all .d.ts files. */
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*"],
3 | "exclude": [],
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "types": []
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | pool: 'vmForks',
6 | dangerouslyIgnoreUnhandledErrors: true,
7 | },
8 | });
9 |
--------------------------------------------------------------------------------