diff --git a/.codeclimate.yml b/.codeclimate.yml index 520557c190..b2e84c02f2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,7 +1,12 @@ +version: "2" # required to adjust maintainability checks languages: - JavaScript: true + JavaScript: true + +checks: + file-lines: + enabled: false exclude_paths: - - "tests/acceptance/*" - - "tests/fixtures/*" - - "tests/unit/*" + - "**/tests" + - "**/tmp" + - "**/node_modules/" diff --git a/.editorconfig b/.editorconfig index 9606f54f2b..4f60915653 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,6 @@ root = true - [*] end_of_line = lf charset = utf-8 @@ -12,3 +11,6 @@ trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 2 + +[*.hbs] +insert_final_newline = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..93806b25f4 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,17 @@ +/bin/ +/blueprints/**/files/ +/packages/*-blueprint/files/ +/common-tmp/ +/coverage/ +/docs/build/ +/node_modules/ +/tmp/ +tests/fixtures/ + +/lib/broccoli/app-*.js +/lib/broccoli/test-support-*.js +/lib/broccoli/tests-*.js +/lib/broccoli/vendor-*.js + +!.* +/.git/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..149aa285c8 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,69 @@ +'use strict'; + +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 2020, + }, + extends: ['eslint:recommended', 'plugin:n/recommended', 'prettier'], + env: { + browser: false, + node: true, + es6: true, + }, + globals: {}, + rules: { + /*** Possible Errors ***/ + + 'no-console': 'off', + 'no-template-curly-in-string': 'error', + 'no-unsafe-negation': 'error', + + /*** Best Practices ***/ + + curly: 'error', + eqeqeq: 'error', + 'guard-for-in': 'off', + 'no-caller': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-new': 'off', + 'no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + }, + ], + 'wrap-iife': 'off', + yoda: 'error', + + /*** Strict Mode ***/ + + strict: ['error', 'global'], + + /*** Variables ***/ + + 'no-undef': 'error', + 'no-unused-vars': 'error', + 'no-use-before-define': ['error', 'nofunc'], + + /*** Stylistic Issues ***/ + + camelcase: 'error', + 'new-cap': ['error', { properties: false }], + 'no-array-constructor': 'error', + 'no-bitwise': 'error', + 'no-lonely-if': 'error', + 'no-plusplus': 'off', + 'no-unneeded-ternary': 'error', + + /*** ECMAScript 6 ***/ + + 'no-useless-computed-key': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-template': 'error', + 'symbol-description': 'error', + }, +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..9e9d563833 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# force unix line endings on all files, needed for Windows npm publishing +# https://github.com/ember-cli/ember-cli/issues/5342 +# this may be a temporary solution, as it may affect the dev flow for Windows +# contributors that aren't npm publishers +# another solution may be https://www.npmjs.com/package/npm-text-auto +* text eol=lf +*.png binary diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..8b4960a173 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +Thank you for taking the time to open an issue! + +For bug reports please include the following section in your issue details. +If you include instructions on how to reproduce the bug or a failing test case +it will make it easier for us to track down the issue you're having. + +--- + +Output from `ember version --verbose && npm --version && yarn --version && pnpm --version`: + +``` +[Replace this line with the output.] +``` diff --git a/.github/actions/setup-environment/action.yml b/.github/actions/setup-environment/action.yml new file mode 100644 index 0000000000..5d495498b5 --- /dev/null +++ b/.github/actions/setup-environment/action.yml @@ -0,0 +1,20 @@ +name: Setup environment +description: Setup environment + +inputs: + node-version: + default: "20" + description: Node version + +runs: + using: composite + + steps: + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + cache: pnpm + node-version: ${{ inputs.node-version }} + + - run: pnpm install + shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..785ba5d52b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: monthly + time: "03:00" + open-pull-requests-limit: 10 + versioning-strategy: increase + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + time: "03:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..d2c753be05 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,162 @@ +name: CI + +on: + push: + branches: + - master + - beta + - release + - next + - "v*" + - "lts-*" + paths-ignore: + - "**.md" + pull_request: + paths-ignore: + - "**.md" + - "!packages/**.md" + - "!tests/**.md" + workflow_dispatch: + merge_group: + schedule: + - cron: "0 3 * * *" # daily, at 3am + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + SKIP_YARN_COREPACK_CHECK: "0" + +jobs: + linting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + - run: pnpm lint + + basic-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + - run: pnpm test + + basic-tests-matrix: + needs: + - linting + - basic-tests + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + node-version: + - 20 + - 22 + - 24 + os: + - macos-latest + - ubuntu-latest + - windows-latest + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + with: + node-version: ${{ matrix.node-version }} + - run: pnpm test + + slow-tests: + needs: + - linting + - basic-tests + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + node-version: + - 20 + - 22 + - 24 + os: + - macos-latest + - ubuntu-latest + - windows-latest + # manual sharding to make CI faster + test-file: + - addon-smoke + - brocfile-smoke + - nested-addons-smoke + - new + - preprocessor-smoke + - smoke + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + with: + node-version: ${{ matrix.node-version }} + - name: Work around windows short path alias # https://github.com/actions/runner-images/issues/712 + if: runner.os == 'Windows' + run: new-item D:\temp -ItemType Directory; echo "TEMP=D:\temp" >> $env:GITHUB_ENV + - run: pnpm test tests/acceptance/${{ matrix.test-file }}-test-slow.js + + feature-flags: + needs: + - linting + - basic-tests + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + feature-flag: + - ENABLE_ALL_EXPERIMENTS + - EMBROIDER + - CLASSIC + - VITE + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + - run: pnpm test:all + env: + "EMBER_CLI_${{ matrix.feature-flag }}": true + + deprecations-broken-test: + name: Deprecations "broken" + runs-on: ubuntu-latest + needs: [basic-tests, linting] + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + - name: Run Tests with Deprecations as Errors + env: + OVERRIDE_DEPRECATION_VERSION: "22.0.0" + run: pnpm test:all + + notify: + name: Notify Discord + runs-on: ubuntu-latest + needs: + - linting + - basic-tests + - basic-tests-matrix + - slow-tests + - feature-flags + - deprecations-broken-test + if: ${{ !cancelled() && contains(github.ref, 'cron') == true }} + steps: + - uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.CORE_TOOLING_WEBHOOK }} + status: "Failure" + title: "ember-cli Nightly CI" + color: 0xcc0000 + url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + username: GitHub Actions diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000000..0a04a0ea3a --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,26 @@ +name: Code Coverage + +on: + push: + branches: + - master + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + - name: Test && Report to Code Climate + uses: paambaati/codeclimate-action@v5.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: pnpm test:cover + coverageLocations: "coverage/lcov.info:lcov" + + - name: Coveralls + uses: coverallsapp/github-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..d02a73c2e8 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,66 @@ +name: "Docs" + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + required: true + type: string + description: 'Specify the released version of ember-cli to use to generate and deploy documentation. Should be full tag name, including the leading "v"' + +env: + GIT_NAME: "github-actions[bot]" + GIT_EMAIL: "github-actions+bot@users.noreply.github.com" + +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.version || github.ref }} + - uses: ./.github/actions/setup-environment + - run: pnpm docs + - name: Upload docs artifact + uses: actions/upload-artifact@v3 + with: + name: docs (${{ github.event.inputs.version || github.ref }}) + path: docs/build + deploy: + name: Deploy docs + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v6 + with: + repository: "ember-learn/ember-cli.github.io" + ssh-key: ${{ secrets.DOCS_DEPLOY_KEY }} + branch: master + - name: Download docs artifact + uses: actions/download-artifact@v3 + with: + name: docs (${{ github.event.inputs.version || github.ref }}) + path: api + - name: Commit + run: | + git config --local user.email "${{ env.GIT_EMAIL }}" + git config --local user.name "${{ env.GIT_NAME }}" + git add api + COMMIT_MESSAGE="Update docs for ${{ github.event.inputs.version || github.ref }} + + --- + + This commit was automatically generated by the ember-cli documentation update process. + + Commit: $GITHUB_REPOSITORY@$GITHUB_SHA + Script: https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/.github/workflows/docs.yml + Logs: https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA/checks" + + git commit --allow-empty -m "$COMMIT_MESSAGE" + - name: Push + run: | + git push -f origin HEAD:refs/heads/master diff --git a/.github/workflows/plan-alpha-release.yml b/.github/workflows/plan-alpha-release.yml new file mode 100644 index 0000000000..124d87fea1 --- /dev/null +++ b/.github/workflows/plan-alpha-release.yml @@ -0,0 +1,72 @@ +name: Plan Alpha Release +on: + workflow_dispatch: + push: + branches: + - main + - master + pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + types: + - labeled + - unlabeled + +concurrency: + group: plan-release # only the latest one of these should ever be running + cancel-in-progress: true + +jobs: + should-run-release-plan-prepare: + name: Should we run release-plan prepare? + runs-on: ubuntu-latest + outputs: + should-prepare: ${{ steps.should-prepare.outputs.should-prepare }} + steps: + - uses: release-plan/actions/should-prepare-release@v1 + with: + ref: "master" + id: should-prepare + + create-prepare-release-pr: + name: Create Prepare Release PR + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: should-run-release-plan-prepare + permissions: + contents: write + issues: read + pull-requests: write + if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true' + steps: + - uses: release-plan/actions/prepare@v1 + name: Run release-plan prepare + with: + ref: "master" + env: + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} + id: explanation + + # this is needed because our fixtures are highly dependent on the exact version of ember-cli in the monorepo + # and release-plan updates the version during the plan phase + - name: "Update blueprint to use new ember-cli version" + # if you're planning to test this locally and you're on a mac then you should use `gsed` because bsd sed has different + # arguments. You can install gsed with `brew install gnu-sed` + run: | + sed -i 's/"ember-cli".*/"ember-cli": "~'$(jq -r .version package.json)'",/' packages/app-blueprint/files/package.json + + - uses: peter-evans/create-pull-request@v8 + name: Create Prepare Release PR + with: + commit-message: "Prepare Alpha Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'" + labels: "internal" + sign-commits: true + branch: releaseplan-preview + title: Prepare Alpha Release ${{ steps.explanation.outputs.new-version }} + # this doesn't use secrets.GITHUB_TOKEN because we want CI to run on the PR that it opens. + # See: https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow + token: ${{ secrets.RELEASE_PLAN_GH_PAT }} + body: | + This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 + + ----------------------------------------- + + ${{ steps.explanation.outputs.text }} diff --git a/.github/workflows/plan-beta-release.yml b/.github/workflows/plan-beta-release.yml new file mode 100644 index 0000000000..9bf1f450df --- /dev/null +++ b/.github/workflows/plan-beta-release.yml @@ -0,0 +1,71 @@ +name: Plan Beta Release +on: + workflow_dispatch: + push: + branches: + - beta + pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + types: + - labeled + - unlabeled + +concurrency: + group: plan-release-beta # only the latest one of these should ever be running + cancel-in-progress: true + +jobs: + should-run-release-plan-prepare: + name: Should we run release-plan prepare? + runs-on: ubuntu-latest + outputs: + should-prepare: ${{ steps.should-prepare.outputs.should-prepare }} + steps: + - uses: release-plan/actions/should-prepare-release@v1 + with: + ref: "beta" + id: should-prepare + + create-prepare-release-pr: + name: Create Prepare Release PR + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: should-run-release-plan-prepare + permissions: + contents: write + issues: read + pull-requests: write + if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true' + steps: + - uses: release-plan/actions/prepare@v1 + name: Run release-plan prepare + with: + ref: "beta" + env: + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} + id: explanation + + # this is needed because our fixtures are highly dependent on the exact version of ember-cli in the monorepo + # and release-plan updates the version during the plan phase + - name: "Update blueprint to use new ember-cli version" + # if you're planning to test this locally and you're on a mac then you should use `gsed` because bsd sed has different + # arguments. You can install gsed with `brew install gnu-sed` + run: | + sed -i 's/"ember-cli".*/"ember-cli": "~'$(jq -r .version package.json)'",/' packages/app-blueprint/files/package.json + + - uses: peter-evans/create-pull-request@v8 + name: Create Prepare Release PR + with: + commit-message: "Prepare Beta Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'" + labels: "internal" + sign-commits: true + branch: release-preview-beta + title: Prepare Beta Release ${{ steps.explanation.outputs.new-version }} + # this doesn't use secrets.GITHUB_TOKEN because we want CI to run on the PR that it opens. + # See: https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow + token: ${{ secrets.RELEASE_PLAN_GH_PAT }} + body: | + This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 + + ----------------------------------------- + + ${{ steps.explanation.outputs.text }} diff --git a/.github/workflows/plan-stable-release.yml b/.github/workflows/plan-stable-release.yml new file mode 100644 index 0000000000..dd22967314 --- /dev/null +++ b/.github/workflows/plan-stable-release.yml @@ -0,0 +1,71 @@ +name: Plan Stable Release +on: + workflow_dispatch: + push: + branches: + - release + pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + types: + - labeled + - unlabeled + +concurrency: + group: plan-release-stable # only the latest one of these should ever be running + cancel-in-progress: true + +jobs: + should-run-release-plan-prepare: + name: Should we run release-plan prepare? + runs-on: ubuntu-latest + outputs: + should-prepare: ${{ steps.should-prepare.outputs.should-prepare }} + steps: + - uses: release-plan/actions/should-prepare-release@v1 + with: + ref: "release" + id: should-prepare + + create-prepare-release-pr: + name: Create Prepare Release PR + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: should-run-release-plan-prepare + permissions: + contents: write + issues: read + pull-requests: write + if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true' + steps: + - uses: release-plan/actions/prepare@v1 + name: Run release-plan prepare + with: + ref: "release" + env: + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} + id: explanation + + # this is needed because our fixtures are highly dependent on the exact version of ember-cli in the monorepo + # and release-plan updates the version during the plan phase + - name: "Update blueprint to use new ember-cli version" + # if you're planning to test this locally and you're on a mac then you should use `gsed` because bsd sed has different + # arguments. You can install gsed with `brew install gnu-sed` + run: | + sed -i 's/"ember-cli".*/"ember-cli": "~'$(jq -r .version package.json)'",/' packages/app-blueprint/files/package.json + + - uses: peter-evans/create-pull-request@v8 + name: Create Prepare Release PR + with: + commit-message: "Prepare Stable Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'" + labels: "internal" + sign-commits: true + branch: release-preview-stable + title: Prepare Stable Release ${{ steps.explanation.outputs.new-version }} + # this doesn't use secrets.GITHUB_TOKEN because we want CI to run on the PR that it opens. + # See: https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow + token: ${{ secrets.RELEASE_PLAN_GH_PAT }} + body: | + This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 + + ----------------------------------------- + + ${{ steps.explanation.outputs.text }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..b8d1bf34a2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,50 @@ +# Every push to any of the primary branches (master, beta, release) with .release-plan.json modified, +# runs release-plan publish. + +name: Publish + +on: + workflow_dispatch: + push: + branches: + - main + - master + - release + - beta + paths: + - ".release-plan.json" + +concurrency: + group: publish-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + publish: + name: "NPM Publish" + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Publish to NPM + # pass --github-prerelease when we are only branch other than release + run: | + if [ ${{ github.ref }} = "refs/heads/release" ]; then + pnpm release-plan publish --publish-branch=release + elif [ ${{ github.ref }} = "refs/heads/beta" ]; then + pnpm release-plan publish --github-prerelease --publish-branch=beta + else + pnpm release-plan publish --github-prerelease + fi + env: + GITHUB_AUTH: ${{ secrets.RELEASE_PLAN_GH_PAT }} diff --git a/.github/workflows/sync-output-repos.yml b/.github/workflows/sync-output-repos.yml new file mode 100644 index 0000000000..c80a3fdb22 --- /dev/null +++ b/.github/workflows/sync-output-repos.yml @@ -0,0 +1,112 @@ +# This Workflow requires a GITHUB_AUTH token that can push to the editor output repos +# - https://github.com/ember-cli/ember-addon-output +# - https://github.com/ember-cli/ember-new-output +# - https://github.com/ember-cli/editor-output +# +# NOTE: +# ember-addon-output and ember-new-output have tags for each release, as well as branches +# for each lts, master (beta), and stable (release) +# +# editor-output has a branch per-editor / scenario. +# so branches form the pattern ${service}-{addon|app}-output{-typescript ?} +name: Sync Output Repos + +on: + # Manual run + workflow_dispatch: + inputs: + version: + required: true + type: string + description: 'Specify the released version of ember-cli to use to generate / update the output repos. Should be full semver version, and without a leading "v"' + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#running-your-workflow-only-when-a-push-of-specific-tags-occurs + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushbranchestagsbranches-ignoretags-ignore + push: + # Only trigger for ember-cli package tags (format: v{version}-ember-cli) + # Avoids spurious runs from other package tags in this repo (e.g. v*-@ember-tooling/*) + tags: + - "v*-ember-cli" + +# https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables +# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context +# +# GITHUB_REF - github.ref - refs/tags/ +# GITHUB_REF_NAME - github.ref_name - +# GITHUB_REF_TYPE - github.ref_type - branch or tag + +env: + GIT_NAME: "github-actions[bot]" + GIT_EMAIL: "github-actions+bot@users.noreply.github.com" + +jobs: + verify-inputs: + name: "Verify Inputs" + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.determine.outputs.tag }} + + steps: + - id: determine + run: | + if [[ "${{ github.event.inputs.version }}" != "" ]]; then + # Manual workflow_dispatch with an explicit version string (no leading 'v'). + # Construct the full tag name expected by the scripts. + TAG="v${{ github.event.inputs.version }}-ember-cli" + elif [[ "${{ github.ref_type }}" == "tag" ]]; then + # Tag-push trigger: github.ref_name is already the full tag name + # (e.g. v7.1.0-alpha.2-ember-cli). Use it directly — do NOT append + # "-ember-cli" again, or the version parsing in the scripts will be wrong. + TAG="${{ github.ref_name }}" + else + echo "Could not determine tag / version" + echo "" + echo "github.ref_name = ${{ github.ref_name }}" + echo "github.ref_type = ${{ github.ref_type }}" + echo "event.inputs.version = ${{ github.event.inputs.version }}" + exit 1; + fi + + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + push-addon-app: + name: "Push to ${{ matrix.blueprint }} output repo" + runs-on: ubuntu-latest + needs: [verify-inputs] + strategy: + fail-fast: false + matrix: + blueprint: ["addon", "app"] + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + - name: "Configure Git" + run: | + git config --global user.name "${{ env.GIT_NAME }}" + git config --global user.email "${{ env.GIT_EMAIL }}" + - run: node ./dev/update-output-repos.js ${{ needs.verify-inputs.outputs.tag }} + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + BLUEPRINT: ${{ matrix.blueprint }} + + push-editors: + name: "Push to editor output repos (${{ matrix.variant }})" + runs-on: ubuntu-latest + needs: [verify-inputs] + strategy: + fail-fast: false + matrix: + variant: ["javascript", "typescript"] + + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-environment + - name: "Configure Git" + run: | + git config --global user.name "${{ env.GIT_NAME }}" + git config --global user.email "${{ env.GIT_EMAIL }}" + - name: Publish ${{ matrix.variant }} branches + run: node ./dev/update-editor-output-repos.js ${{ needs.verify-inputs.outputs.tag }} + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + VARIANT: ${{ matrix.variant }} diff --git a/.gitignore b/.gitignore index 02ca6c107d..08908ef00d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ -node_modules -npm-debug.log -tmp* -_posts -_site -common-tmp +/common-tmp/ +/coverage/ +/.nyc_output/ +/docs/build/ +/node_modules/ +/packages/*/node_modules/ +/tmp/ +*.log *.tgz -docs/build -.node_modules-tmp -.bower_components-tmp -coverage \ No newline at end of file +*.pem +.DS_Store +.eslintcache diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index 837151e08e..0000000000 --- a/.jshintignore +++ /dev/null @@ -1,6 +0,0 @@ -tmp/* -node_modules/* -coverage/* -blueprints/**/files/**/*.js -tests/fixtures/* -common-tmp/* diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 0114f2e059..0000000000 --- a/.jshintrc +++ /dev/null @@ -1,45 +0,0 @@ -{ - "predef": [ - "console", - "it", - "describe", - "beforeEach", - "afterEach", - "before", - "after", - "-Promise" - ], - "expr": true, - "proto": true, - "strict": true, - "indent": 2, - "camelcase": true, - "node": true, - "browser": false, - "boss": true, - "curly": true, - "latedef": "nofunc", - "debug": false, - "devel": false, - "eqeqeq": true, - "evil": true, - "forin": false, - "immed": false, - "laxbreak": false, - "newcap": true, - "noarg": true, - "noempty": false, - "quotmark": true, - "nonew": false, - "nomen": false, - "onevar": false, - "plusplus": false, - "regexp": false, - "undef": true, - "unused": true, - "sub": true, - "trailing": true, - "white": false, - "eqnull": true, - "esnext": true -} diff --git a/.mocharc.js b/.mocharc.js new file mode 100644 index 0000000000..fe9f368085 --- /dev/null +++ b/.mocharc.js @@ -0,0 +1,20 @@ +'use strict'; + +const ciInfo = require('ci-info'); +let reporter = process.env.MOCHA_REPORTER || (ciInfo.isCI ? 'tap' : 'spec'); +const chaiJestSnapshot = require('chai-jest-snapshot'); + +module.exports = { + timeout: 5000, + reporter, + reporterOption: { + maxDiffSize: Infinity, + }, + retries: 2, + rootHooks: { + beforeEach() { + chaiJestSnapshot.resetSnapshotRegistry(); + chaiJestSnapshot.configureUsingMochaContext(this); + }, + }, +}; diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..d4b152ec6c --- /dev/null +++ b/.npmignore @@ -0,0 +1,15 @@ +/.github/ +/assets/ +/dev/ +/docs/* +/docs/*/ +/docs/build/* +/docs/build/*/ +!docs/build/data.json +/tests/* +/tests/*/ +!/tests/helpers/ +/tmp/ +/.* +*.log +/pnpm-lock.yaml diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 0000000000..fc19e97195 --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,5 @@ +{ + "include": ["bin/**/*.js", "lib/**/*.js", "blueprints/**/*.js"], + + "exclude": ["blueprints/*/files/**"] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..6f25d4a089 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,15 @@ +/.nyc_output/ +/blueprints/*/files/ +/coverage/ +/docs/build/ +/lib/broccoli/app-*.js +/lib/broccoli/test-support-*.js +/lib/broccoli/tests-*.js +/lib/broccoli/vendor-*.js +/tests/fixtures/ +/tmp/ +/CHANGELOG.md +/packages/addon-blueprint/files +/packages/app-blueprint/files +/packages/blueprint-blueprint/files +/pnpm-lock.yaml diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000..b9cd63afa3 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + overrides: [ + { + files: ['*.js', 'bin/ember'], + options: { + singleQuote: true, + }, + }, + ], + printWidth: 120, + trailingComma: 'es5', +}; diff --git a/.release-plan.json b/.release-plan.json new file mode 100644 index 0000000000..07156a572d --- /dev/null +++ b/.release-plan.json @@ -0,0 +1,96 @@ +{ + "solution": { + "ember-cli": { + "impact": "minor", + "oldVersion": "7.2.0-alpha.0", + "newVersion": "7.2.0-alpha.1", + "tagName": "alpha", + "constraints": [ + { + "impact": "minor", + "reason": "Appears in changelog section :rocket: Enhancement" + }, + { + "impact": "patch", + "reason": "Has dependency `workspace:*` on @ember-tooling/classic-build-addon-blueprint" + }, + { + "impact": "patch", + "reason": "Has dependency `workspace:*` on @ember-tooling/classic-build-app-blueprint" + }, + { + "impact": "patch", + "reason": "Has dependency `workspace:*` on @ember-tooling/blueprint-model" + }, + { + "impact": "patch", + "reason": "Appears in changelog section :bug: Bug Fix" + }, + { + "impact": "patch", + "reason": "Appears in changelog section :house: Internal" + } + ], + "pkgJSONPath": "./package.json" + }, + "@ember-tooling/classic-build-addon-blueprint": { + "impact": "minor", + "oldVersion": "7.2.0-alpha.0", + "newVersion": "7.2.0-alpha.1", + "tagName": "alpha", + "constraints": [ + { + "impact": "minor", + "reason": "Appears in changelog section :rocket: Enhancement" + }, + { + "impact": "patch", + "reason": "Has dependency `workspace:*` on @ember-tooling/blueprint-model" + }, + { + "impact": "patch", + "reason": "Appears in changelog section :house: Internal" + } + ], + "pkgJSONPath": "./packages/addon-blueprint/package.json" + }, + "@ember-tooling/classic-build-app-blueprint": { + "impact": "minor", + "oldVersion": "7.2.0-alpha.0", + "newVersion": "7.2.0-alpha.1", + "tagName": "alpha", + "constraints": [ + { + "impact": "minor", + "reason": "Appears in changelog section :rocket: Enhancement" + }, + { + "impact": "patch", + "reason": "Has dependency `workspace:*` on @ember-tooling/blueprint-model" + }, + { + "impact": "patch", + "reason": "Appears in changelog section :house: Internal" + } + ], + "pkgJSONPath": "./packages/app-blueprint/package.json" + }, + "@ember-tooling/blueprint-blueprint": { + "oldVersion": "0.3.0" + }, + "@ember-tooling/blueprint-model": { + "impact": "minor", + "oldVersion": "0.6.3", + "newVersion": "0.7.0", + "tagName": "latest", + "constraints": [ + { + "impact": "minor", + "reason": "Appears in changelog section :rocket: Enhancement" + } + ], + "pkgJSONPath": "./packages/blueprint-model/package.json" + } + }, + "description": "## Release (2026-06-30)\n\n* ember-cli 7.2.0-alpha.1 (minor)\n* @ember-tooling/classic-build-addon-blueprint 7.2.0-alpha.1 (minor)\n* @ember-tooling/classic-build-app-blueprint 7.2.0-alpha.1 (minor)\n* @ember-tooling/blueprint-model 0.7.0 (minor)\n\n#### :rocket: Enhancement\n* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `@ember-tooling/blueprint-model`\n * [#11032](https://github.com/ember-cli/ember-cli/pull/11032) Prepare 7.2 alpha ([@mansona](https://github.com/mansona))\n* `@ember-tooling/classic-build-addon-blueprint`\n * [#11035](https://github.com/ember-cli/ember-cli/pull/11035) Add placeholder for documenting exports from an addon ([@kategengler](https://github.com/kategengler))\n* `ember-cli`, `@ember-tooling/blueprint-model`\n * [#10672](https://github.com/ember-cli/ember-cli/pull/10672) Separate blueprint model so it can be used outside of ember-cli ([@mansona](https://github.com/mansona))\n\n#### :bug: Bug Fix\n* `ember-cli`\n * [#11027](https://github.com/ember-cli/ember-cli/pull/11027) Fix error when blueprint is missing keyword ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n * [#11028](https://github.com/ember-cli/ember-cli/pull/11028) [BUGFIX release] fix require(esm) of blueprint indexes ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :house: Internal\n* `ember-cli`\n * [#11037](https://github.com/ember-cli/ember-cli/pull/11037) bring test fixtures in line with updates from addon readme ([@void-mAlex](https://github.com/void-mAlex))\n * [#11038](https://github.com/ember-cli/ember-cli/pull/11038) fix running tests on blueprint markdown changes ([@mansona](https://github.com/mansona))\n * [#10880](https://github.com/ember-cli/ember-cli/pull/10880) stop using internal package cache in the addon smoke test and rely on pnpm caching ([@mansona](https://github.com/mansona))\n * [#11033](https://github.com/ember-cli/ember-cli/pull/11033) Merge release into beta ([@mansona](https://github.com/mansona))\n* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`\n * [#11029](https://github.com/ember-cli/ember-cli/pull/11029) Prepare Beta Release ([@mansona](https://github.com/mansona))\n\n#### Committers: 4\n- Alex ([@void-mAlex](https://github.com/void-mAlex))\n- Chris Manson ([@mansona](https://github.com/mansona))\n- Katie Gengler ([@kategengler](https://github.com/kategengler))\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n" +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 99d16afa81..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -sudo: false -env: - - NODE_VERSION=0.10 - - NODE_VERSION=0.12 - - NODE_VERSION=iojs - -os: - - linux -script: npm run-script test-all:cover -before_install: - - test $TRAVIS_OS_NAME = "osx" && brew update && brew install nvm && source $(brew --prefix nvm)/nvm.sh && brew install phantomjs || test $TRAVIS_OS_NAME = "linux" - - nvm install $NODE_VERSION - - npm config set spin false -install: - - node --version - - npm --version - - git --version - - npm install --no-optional - -after_script: - - cat coverage/lcov.info | codeclimate - - cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js diff --git a/ADDON_HOOKS.md b/ADDON_HOOKS.md deleted file mode 100644 index 057ce9301c..0000000000 --- a/ADDON_HOOKS.md +++ /dev/null @@ -1,464 +0,0 @@ -Table of Contents: - -1. [config](#config) -- [blueprintsPath](#blueprintspath) -- [includedCommands](#includedcommands) -- [serverMiddleware](#servermiddleware) -- [postBuild](#postbuild) -- [preBuild](#prebuild) -- [buildError](#builderror) -- [included](#included) -- [setupPreprocessorRegistry](#setuppreprocessorregistry) -- [postprocessTree](#postprocesstree) -- [lintTree](#linttree) -- [contentFor](#contentfor) -- [treeFor](#treefor) - 1. [treeForApp](#treefor-cont) - - [treeForStyles](#treefor-cont) - - [treeForTemplates](#treefor-cont) - - [treeForAddon](#treefor-cont) - - [treeForVendor](#treefor-cont) - - [treeForTestSupport](#treefor-cont) - - [treeForPublic](#treefor-cont) - -For each hook we'll cover the following (if applicable): - -- Received arguments -- Source -- Default implementation -- Uses -- Examples - -Compendium is largely based of a talk by [@rwjblue](https://github.com/rwjblue) which can be found [here](https://www.youtube.com/watch?v=e1l07N0ukzY&feature=youtu.be&t=1h40m53s). - - -## Config - -Augments the applications configuration settings. Object returned from this hook is merged with the application's configuration object. Application's configuration always take precedence. - -**Received arguments:** - - - env - name of current environment (ie "development") - - baseConfig - Initial application config - -**Source:** [lib/models/addon.js:485](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/models/addon.js#L485) - -**Default implementation:** - -```js -Addon.prototype.config = function (env, baseConfig) { - var configPath = path.join(this.root, 'config', 'environment.js'); - - if (existsSync(configPath)) { - var configGenerator = require(configPath); - - return configGenerator(env, baseConfig); - } -}; -``` - -**Uses:** - -- Modifying configuration options (see list of defaults [here](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/broccoli/ember-app.js#L96)) - - For example - - `minifyJS` - - `storeConfigInMeta` - - `es3Safe` - - et, al - -**Examples:** - -- Setting `storeConfigInMeta` to false in [ember-cli-rails-addon](https://github.com/rondale-sc/ember-cli-rails-addon/blob/v0.0.11/index.js#L24) - - -## blueprintsPath - -Tells the application where your blueprints exist. - -**Received arguments:** None - -**Source:** [lib/models/addon.js:457](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/models/addon.js#L457) - -**Default implementation:** - -```js -Addon.prototype.blueprintsPath = function() { - var blueprintPath = path.join(this.root, 'blueprints'); - - if (existsSync(blueprintPath)) { - return blueprintPath; - } -}; -``` - -**Uses:** - -- Let application know where blueprints exists. - -**Examples:** - -- [ember-cli-coffeescript](https://github.com/kimroen/ember-cli-coffeescript/blob/v0.9.1/index.js#L26) - - -## includedCommands - -Allows the specification of custom addon commands. Expects you to return an object whose key is the name of the command and value is the command instance. - -**Received arguments:** None - -**Source:** [lib/models/project.js:388](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/models/project.js#L388) - -**Default implementation:** None - -**Uses:** - -- Include custom commands into consuming application - -**Examples:** - -- [ember-cli-cordova](https://github.com/poetic/ember-cli-cordova/blob/v0.0.14/index.js#L19) - -```js - // https://github.com/rwjblue/ember-cli-divshot/blob/v0.1.6/index.js - includedCommands: function() { - return { - 'divshot': require('./lib/commands/divshot') - }; - } -``` - - -## serverMiddleware - -Designed to manipulate requests in development mode. - -**Received arguments:** - - options (eg express_instance, project, watcher, environment) - -**Source:** [lib/tasks/server/express-server.js:64](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/tasks/server/express-server.js#L64) - -**Default implementation:** None - -**Uses:** - -- Tacking on headers to each request -- Modifying the request object - -*Note:* that this should only be used in development, and if you need the same behavior in production you'll need to configure your server. - - -**Examples:** - -- [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/blob/v0.3.0/index.js#L25) - -- [history-support-addon](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/tasks/server/middleware/history-support/index.js#L13) - - -## postBuild - -Gives access to the result of the tree, and the location of the output. - -**Received arguments:** - -- Result object from broccoli build - - `result.directory` - final output path - -**Source:** [lib/models/builder.js:117](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/models/builder.js#L117) - -**Default implementation:** None - -**Uses:** - -- Slow tree listing -- May be used to manipulate your project after build has happened -- Opportunity to symlink or copy files elsewhere. - -**Examples:** - -- [ember-cli-rails-addon](https://github.com/rondale-sc/ember-cli-rails-addon/blob/v0.0.11/index.js#L47) - - In this case we are using this in tandem with a rails middleware to remove a lock file. This allows our ruby gem to block incoming requests until after the build happens reliably. - - -## preBuild - -Hook called before build takes place. - -**Received arguments:** - -**Source:** [lib/models/builder.js:112](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/models/builder.js#L112) - -**Default implementation:** None - -**Uses:** - -**Examples:** - -- [ember-cli-rails-addon](https://github.com/rondale-sc/ember-cli-rails-addon/blob/v0.0.11/index.js#L41) - - In this case we are using this in tandem with a rails middleware to create a lock file. - *[See postBuild]* - - -## buildError - -buildError hook will be called on when an error occurs during the -preBuild or postBuild hooks for addons, or when builder#build -fails - -**Received arguments:** - -- The error that was caught during the processes listed above - -**Source:** [lib/models/builder.js:119](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/models/builder.js#L119) - -**Default implementation:** None - -**Uses:** - -- Custom error handling during build process - -**Examples:** - -- [ember-cli-rails-addon](https://github.com/rondale-sc/ember-cli-rails-addon/blob/v0.0.11/index.js#L19) - - -## included - -Usually used to import assets into the application. - -**Received arguments:** - -- `EmberApp` instance [see ember-app.js](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/broccoli/ember-app.js) - -**Source:** [lib/broccoli/ember-app.js:268](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/broccoli/ember-app.js#L268) - -**Default implementation:** None - -**Uses:** - -- including vendor files -- setting configuration options - -*Note:* Any options set in the consuming application will override the addon. - -**Examples:** - -```js -// https://github.com/yapplabs/ember-colpick/blob/master/index.js -included: function colpick_included(app) { - this._super.included.apply(this, arguments); - - var colpickPath = path.join(app.bowerDirectory, 'colpick'); - - this.app.import(path.join(colpickPath, 'js', 'colpick.js')); - this.app.import(path.join(colpickPath, 'css', 'colpick.css')); -} -``` - -- [ember-cli-rails-addon](https://github.com/rondale-sc/ember-cli-rails-addon/blob/v0.0.11/index.js#L23) - - -## setupPreprocessorRegistry - -Used to add preprocessors to the preprocessor registry. This is often used by addons like [ember-cli-htmlbars](https://github.com/ember-cli/ember-cli-htmlbars) -and [ember-cli-coffeescript](https://github.com/kimroen/ember-cli-coffeescript) to add a `template` or `js` preprocessor to the registry. - -**Received arguments** - -- `type` either `"self"` or `"parent"` -- `registry` the registry to be set up - -**Source:** [lib/preprocessors:7](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/preprocessors.js#L7) - -**Default implementation:** None - -**Uses:** - -- Adding preprocessors to the registry. - -**Examples:** - -```js -// https://github.com/ember-cli/ember-cli-htmlbars/blob/master/ember-addon-main.js -setupPreprocessorRegistry: function(type, registry) { - var addonContext = this; - - registry.add('template', { - name: 'ember-cli-htmlbars', - ext: 'hbs', - toTree: function(tree) { - return htmlbarsCompile(tree, addonContext.htmlbarsOptions()); - } - }); -} -``` - - -## postprocessTree - -**Received arguments:** - -- post processing type (eg all) -- receives tree after build -- receives tree for a given type after preprocessors (like HTMLBars or babel) run. - -available types: - -* js -* template -* all -* css -* test - -**Source:** [lib/broccoli/ember-app.js:313](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/broccoli/ember-app.js#L313) - -**Default implementation:** None - - -## preprocessTree - -**Received arguments:** - -- type of tree (eg template, js) -- receives tree for a given type before preprocessors (like HTMLBars or babel) run. - -available types: - -* js -* template -* css -* test - -**Default implementation:** None - -**Uses:** - -- removing / adding files from the build. - -**Examples:** - -- [broccoli-asset-rev](https://github.com/rickharrison/broccoli-asset-rev/blob/c82c3580855554a31f7d6600b866aecf69cdaa6d/index.js#L29) - - -## lintTree - -Return value is merged into the **tests** tree. This lets you inject -linter output as test results. - -**Received arguments:** - -- tree type ('app', 'tests', or 'addon') -- tree of Javascript files - -**Source:** [lib/broccoli/ember-app.js:335](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/broccoli/ember-app.js#L335) - -**Default implementation:** None - -**Uses:** - -- JSHint -- any other form of automated test generation that turns code into tests - -**Examples:** - -- [ember-cli-qunit](https://github.com/ember-cli/ember-cli-qunit/blob/v0.3.8/index.js#L94) -- [ember-cli-mocha](https://github.com/ef4/ember-cli-mocha/blob/ec5a7cd064aabbfe47fbcb3389383f80cde8b668/index.js#L83-L89) - - - -## contentFor - -Allow addons to implement contentFor method to add string output into the associated {{content-for 'foo'}} section in index.html - -**Received arguments:** - -- type -- config - -**Source:** [lib/broccoli/ember-app.js:1167](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/broccoli/ember-app.js#L1167) - -**Default implementation:** None - -**Uses:** - -- For instance, to inject analytics code into index.html - -**Examples:** - -- [ember-cli-google-analytics](https://github.com/pgrippi/ember-cli-google-analytics/blob/v1.3.1/index.js#L79) - - -## treeFor - -Return value is merged with application tree of same type - -**Received arguments:** - -- returns given type of tree (eg app, vendor, bower) - -**Source:** [lib/broccoli/ember-app.js:296](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/broccoli/ember-app.js#L296) - -**Default implementation:** - -```js -var mergeTrees = require('broccoli-merge-trees'); - -Addon.prototype.treeFor = function treeFor(name) { - this._requireBuildPackages(); - - var tree; - var trees = []; - - if (tree = this._treeFor(name)) { - trees.push(tree); - } - - if (this.isDevelopingAddon() && this.app.hinting && name === 'app') { - trees.push(this.jshintAddonTree()); - } - - return mergeTrees(trees.filter(Boolean)); -}; -``` - -**Uses:** - -- manipulating trees at build time - -**Examples:** - - -# treeFor (cont...) - -Instead of overriding `treeFor` and acting only if the tree you receive matches the one you need EmberCLI has custom hooks for the following Broccoli trees - -- treeForApp -- treeForStyles -- treeForTemplates -- treeForAddon -- treeForVendor -- treeForTestSupport -- treeForPublic - - -## isDevelopingAddon - -Allows to mark the addon as developing, triggering live-reload in the project the addon is linked to - -**Received arguments:** None - -**Default implementation:** None - -**Uses:** - -- Working on projects with internal addons - -**Examples:** - -```js - // addon index.js - isDevelopingAddon: function() { - return true; - } -``` - -See more [here](https://github.com/ember-cli/ember-cli/blob/v0.1.15/lib/models/addon.js#L62). diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc849b89a..9a45e1d247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2039 +1,4419 @@ # ember-cli Changelog -### 1.13.8 +## Release (2026-06-30) + +* ember-cli 7.2.0-alpha.1 (minor) +* @ember-tooling/classic-build-addon-blueprint 7.2.0-alpha.1 (minor) +* @ember-tooling/classic-build-app-blueprint 7.2.0-alpha.1 (minor) +* @ember-tooling/blueprint-model 0.7.0 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `@ember-tooling/blueprint-model` + * [#11032](https://github.com/ember-cli/ember-cli/pull/11032) Prepare 7.2 alpha ([@mansona](https://github.com/mansona)) +* `@ember-tooling/classic-build-addon-blueprint` + * [#11035](https://github.com/ember-cli/ember-cli/pull/11035) Add placeholder for documenting exports from an addon ([@kategengler](https://github.com/kategengler)) +* `ember-cli`, `@ember-tooling/blueprint-model` + * [#10672](https://github.com/ember-cli/ember-cli/pull/10672) Separate blueprint model so it can be used outside of ember-cli ([@mansona](https://github.com/mansona)) + +#### :bug: Bug Fix +* `ember-cli` + * [#11027](https://github.com/ember-cli/ember-cli/pull/11027) Fix error when blueprint is missing keyword ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + * [#11028](https://github.com/ember-cli/ember-cli/pull/11028) [BUGFIX release] fix require(esm) of blueprint indexes ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### :house: Internal +* `ember-cli` + * [#11037](https://github.com/ember-cli/ember-cli/pull/11037) bring test fixtures in line with updates from addon readme ([@void-mAlex](https://github.com/void-mAlex)) + * [#11038](https://github.com/ember-cli/ember-cli/pull/11038) fix running tests on blueprint markdown changes ([@mansona](https://github.com/mansona)) + * [#10880](https://github.com/ember-cli/ember-cli/pull/10880) stop using internal package cache in the addon smoke test and rely on pnpm caching ([@mansona](https://github.com/mansona)) + * [#11033](https://github.com/ember-cli/ember-cli/pull/11033) Merge release into beta ([@mansona](https://github.com/mansona)) +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#11029](https://github.com/ember-cli/ember-cli/pull/11029) Prepare Beta Release ([@mansona](https://github.com/mansona)) + +#### Committers: 4 +- Alex ([@void-mAlex](https://github.com/void-mAlex)) +- Chris Manson ([@mansona](https://github.com/mansona)) +- Katie Gengler ([@kategengler](https://github.com/kategengler)) +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +## Release (2026-05-13) + +* ember-cli 7.1.0-alpha.3 (minor) +* @ember-tooling/classic-build-addon-blueprint 7.1.0-alpha.2 (patch) +* @ember-tooling/classic-build-app-blueprint 7.1.0-alpha.3 (patch) +* @ember-tooling/blueprint-model 0.6.3 (patch) + +#### :rocket: Enhancement +* `ember-cli` + * [#10610](https://github.com/ember-cli/ember-cli/pull/10610) use semver-deprecate instead of internal code ([@mansona](https://github.com/mansona)) + * [#11008](https://github.com/ember-cli/ember-cli/pull/11008) update babel-remove-types to v2 ([@mansona](https://github.com/mansona)) + * [#11009](https://github.com/ember-cli/ember-cli/pull/11009) update configstore to v8 ([@mansona](https://github.com/mansona)) + +#### :bug: Bug Fix +* `ember-cli`, `@ember-tooling/blueprint-model` + * [#11020](https://github.com/ember-cli/ember-cli/pull/11020) Update diff to latest v8.x ([@mkszepp](https://github.com/mkszepp)) + +#### :house: Internal +* `ember-cli` + * [#11017](https://github.com/ember-cli/ember-cli/pull/11017) Add Sync Output Repos check to release instructions ([@kategengler](https://github.com/kategengler)) + * [#11016](https://github.com/ember-cli/ember-cli/pull/11016) fix: sync-output-repos workflow failing on tag pushes ([@Copilot](https://github.com/apps/copilot-swe-agent)) + * [#10999](https://github.com/ember-cli/ember-cli/pull/10999) update RELEASE with update-blueprint-deps commands ([@mansona](https://github.com/mansona)) + +#### Committers: 4 +- Chris Manson ([@mansona](https://github.com/mansona)) +- Copilot [Bot] ([@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)) +- Katie Gengler ([@kategengler](https://github.com/kategengler)) +- Markus Sanin ([@mkszepp](https://github.com/mkszepp)) + +## Release (2026-04-26) + +* ember-cli 7.1.0-alpha.2 (minor) +* @ember-tooling/classic-build-app-blueprint 7.1.0-alpha.2 (minor) + +#### :rocket: Enhancement +* `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#11006](https://github.com/ember-cli/ember-cli/pull/11006) update ember-welcome-page to v8 in app blueprint ([@mansona](https://github.com/mansona)) + * [#11005](https://github.com/ember-cli/ember-cli/pull/11005) update ember-cli-deprecation-workflow to v4 ([@mansona](https://github.com/mansona)) + * [#11003](https://github.com/ember-cli/ember-cli/pull/11003) update @ember/optional-features to v3 ([@mansona](https://github.com/mansona)) + +#### Committers: 1 +- Chris Manson ([@mansona](https://github.com/mansona)) + +## Release (2026-04-24) + +* ember-cli 7.1.0-alpha.1 (minor) +* @ember-tooling/classic-build-addon-blueprint 7.1.0-alpha.1 (minor) +* @ember-tooling/classic-build-app-blueprint 7.1.0-alpha.1 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#11001](https://github.com/ember-cli/ember-cli/pull/11001) Prepare 7.1 Alpha ([@mansona](https://github.com/mansona)) + +#### Committers: 1 +- Chris Manson ([@mansona](https://github.com/mansona)) + +## Release (2026-04-21) + +* ember-cli 7.0.0-beta.1 (minor) +* @ember-tooling/classic-build-addon-blueprint 7.0.0-beta.1 (minor) +* @ember-tooling/classic-build-app-blueprint 7.0.0-beta.1 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10997](https://github.com/ember-cli/ember-cli/pull/10997) Prepare 7.0 Beta ([@mansona](https://github.com/mansona)) + +#### Committers: 1 +- Chris Manson ([@mansona](https://github.com/mansona)) + +## Release (2026-04-09) + +* ember-cli 6.12.0 (minor) +* @ember-tooling/classic-build-addon-blueprint 6.12.0 (minor) +* @ember-tooling/classic-build-app-blueprint 6.12.0 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10993](https://github.com/ember-cli/ember-cli/pull/10993) Promote Beta and update all dependencies for 6.12 release ([@mansona](https://github.com/mansona)) +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10939](https://github.com/ember-cli/ember-cli/pull/10939) Add warpDrive support to app-blueprint ([@Copilot](https://github.com/apps/copilot-swe-agent)) +* `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10969](https://github.com/ember-cli/ember-cli/pull/10969) Update ember-cli-htmlbars to ^7.0.0 in app-blueprint ([@Copilot](https://github.com/apps/copilot-swe-agent)) + +#### :bug: Bug Fix +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10976](https://github.com/ember-cli/ember-cli/pull/10976) Enable `use-ember-modules` in blueprint optional-features.json ([@Copilot](https://github.com/apps/copilot-swe-agent)) +* `ember-cli` + * [#10941](https://github.com/ember-cli/ember-cli/pull/10941) Downgrade isbinaryfile ([@mansona](https://github.com/mansona)) +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10932](https://github.com/ember-cli/ember-cli/pull/10932) Remove tracked-built-ins (it comes built in with ember-source 6.8+) ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### :house: Internal +* `ember-cli` + * [#10990](https://github.com/ember-cli/ember-cli/pull/10990) bump node on publish.yml and stop updating npm ([@mansona](https://github.com/mansona)) + * [#10982](https://github.com/ember-cli/ember-cli/pull/10982) Update publish.yml to use PAT so that output repos workflow will run ([@kategengler](https://github.com/kategengler)) + * [#10967](https://github.com/ember-cli/ember-cli/pull/10967) Replace `temp` package with Node.js built-in `fs.mkdtemp` ([@Copilot](https://github.com/apps/copilot-swe-agent)) + * [#10931](https://github.com/ember-cli/ember-cli/pull/10931) update Release.md ([@mansona](https://github.com/mansona)) + * [#10945](https://github.com/ember-cli/ember-cli/pull/10945) update release-plan for OIDC ([@mansona](https://github.com/mansona)) +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10947](https://github.com/ember-cli/ember-cli/pull/10947) bump in-range versions ([@mansona](https://github.com/mansona)) + +#### Committers: 4 +- Chris Manson ([@mansona](https://github.com/mansona)) +- Copilot [Bot] ([@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)) +- Katie Gengler ([@kategengler](https://github.com/kategengler)) +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +## Release (2026-03-29) + +* ember-cli 6.11.2 (patch) +* @ember-tooling/classic-build-addon-blueprint 6.11.2 (patch) +* @ember-tooling/classic-build-app-blueprint 6.11.2 (patch) + +#### :bug: Bug Fix +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10977](https://github.com/ember-cli/ember-cli/pull/10977) Backport: Enable use-ember-modules in blueprint optional-features.json ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent)) + +#### Committers: 1 +- NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent)) + +## Release (2026-03-29) + +* ember-cli 6.11.1 (patch) +* @ember-tooling/classic-build-addon-blueprint 6.11.1 (patch) +* @ember-tooling/classic-build-app-blueprint 6.11.1 (patch) + +#### :bug: Bug Fix +* `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10975](https://github.com/ember-cli/ember-cli/pull/10975) Backport: Update ember-cli-htmlbars to ^7.0.0 in app-blueprint ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent)) +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10974](https://github.com/ember-cli/ember-cli/pull/10974) Backport: Remove tracked-built-ins (it comes built in with ember-source 6.8+) ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent)) +* `ember-cli` + * [#10972](https://github.com/ember-cli/ember-cli/pull/10972) Support ember-source (ESM) -- without addon vendor paths ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### Committers: 2 +- NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent)) +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +## Release (2026-02-17) + +* ember-cli 6.11.0 (minor) +* @ember-tooling/classic-build-addon-blueprint 6.11.0 (minor) +* @ember-tooling/classic-build-app-blueprint 6.11.0 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-app-blueprint` + * [#10960](https://github.com/ember-cli/ember-cli/pull/10960) Promote Beta and update all dependencies for 6.11 release ([@mansona](https://github.com/mansona)) +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10929](https://github.com/ember-cli/ember-cli/pull/10929) Prepare 6.11-beta ([@mansona](https://github.com/mansona)) + * [#10919](https://github.com/ember-cli/ember-cli/pull/10919) Prepare 6.11-alpha ([@mansona](https://github.com/mansona)) + +#### Committers: 1 +- Chris Manson ([@mansona](https://github.com/mansona)) + +## Release (2026-02-09) + +* ember-cli 6.10.2 (patch) + +#### :bug: Bug Fix +* `ember-cli` + * [#10949](https://github.com/ember-cli/ember-cli/pull/10949) [backport release] remove unused isbinaryfile from ember-cli package ([@mansona](https://github.com/mansona)) + * [#10940](https://github.com/ember-cli/ember-cli/pull/10940) [bugfix release] remove fixturify-project from dependencies ([@mansona](https://github.com/mansona)) + +#### :house: Internal +* `ember-cli` + * [#10952](https://github.com/ember-cli/ember-cli/pull/10952) add correct --publish-branch to pnpm publish ([@mansona](https://github.com/mansona)) + * [#10951](https://github.com/ember-cli/ember-cli/pull/10951) Fix PR name for stable release-plan pull request ([@mansona](https://github.com/mansona)) + * [#10950](https://github.com/ember-cli/ember-cli/pull/10950) [backport release] update release-plan for OIDC ([@mansona](https://github.com/mansona)) + +#### Committers: 1 +- Chris Manson ([@mansona](https://github.com/mansona)) + +## Release (2026-02-08) + +* ember-cli 6.10.1 (patch) + +#### :bug: Bug Fix +* `ember-cli` + * [#10949](https://github.com/ember-cli/ember-cli/pull/10949) [backport release] remove unused isbinaryfile from ember-cli package ([@mansona](https://github.com/mansona)) + * [#10940](https://github.com/ember-cli/ember-cli/pull/10940) [bugfix release] remove fixturify-project from dependencies ([@mansona](https://github.com/mansona)) + +#### :house: Internal +* `ember-cli` + * [#10951](https://github.com/ember-cli/ember-cli/pull/10951) Fix PR name for stable release-plan pull request ([@mansona](https://github.com/mansona)) + * [#10950](https://github.com/ember-cli/ember-cli/pull/10950) [backport release] update release-plan for OIDC ([@mansona](https://github.com/mansona)) + +#### Committers: 1 +- Chris Manson ([@mansona](https://github.com/mansona)) + +## Release (2026-01-23) + +* ember-cli 6.10.0 (minor) +* @ember-tooling/classic-build-addon-blueprint 6.10.0 (minor) +* @ember-tooling/classic-build-app-blueprint 6.10.0 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10923](https://github.com/ember-cli/ember-cli/pull/10923) Promote Beta and update all dependencies for 6.10 release ([@mansona](https://github.com/mansona)) + * [#10887](https://github.com/ember-cli/ember-cli/pull/10887) Update `chalk` dependency to latest ([@bertdeblock](https://github.com/bertdeblock)) +* `ember-cli` + * [#10906](https://github.com/ember-cli/ember-cli/pull/10906) Even more dependency updates ([@bertdeblock](https://github.com/bertdeblock)) + * [#10892](https://github.com/ember-cli/ember-cli/pull/10892) More dependency updates ([@bertdeblock](https://github.com/bertdeblock)) + * [#10890](https://github.com/ember-cli/ember-cli/pull/10890) Update various dependencies ([@bertdeblock](https://github.com/bertdeblock)) + * [#10870](https://github.com/ember-cli/ember-cli/pull/10870) Upgrade broccoli ([@kategengler](https://github.com/kategengler)) + +#### :bug: Bug Fix +* `ember-cli` + * [#10886](https://github.com/ember-cli/ember-cli/pull/10886) Update required Node version to v20.19.0 ([@bertdeblock](https://github.com/bertdeblock)) +* `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10876](https://github.com/ember-cli/ember-cli/pull/10876) bump minimum node version to 20.19 ([@mansona](https://github.com/mansona)) + +#### :memo: Documentation +* `ember-cli` + * [#10874](https://github.com/ember-cli/ember-cli/pull/10874) Update RELEASE.md ([@mansona](https://github.com/mansona)) + +#### :house: Internal +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint` + * [#10891](https://github.com/ember-cli/ember-cli/pull/10891) Update `prettier` + setup ([@bertdeblock](https://github.com/bertdeblock)) +* `ember-cli` + * [#10878](https://github.com/ember-cli/ember-cli/pull/10878) stop using internal package cache for smoke-test-slow ([@mansona](https://github.com/mansona)) + * [#10863](https://github.com/ember-cli/ember-cli/pull/10863) Missed a script -- fix updating output repos ([@kategengler](https://github.com/kategengler)) + * [#10862](https://github.com/ember-cli/ember-cli/pull/10862) Fix typo in output repo workflow ([@kategengler](https://github.com/kategengler)) + * [#10861](https://github.com/ember-cli/ember-cli/pull/10861) Fix output repo generation with new tag format ([@kategengler](https://github.com/kategengler)) + +#### Committers: 3 +- Bert De Block ([@bertdeblock](https://github.com/bertdeblock)) +- Chris Manson ([@mansona](https://github.com/mansona)) +- Katie Gengler ([@kategengler](https://github.com/kategengler)) + +## Release (2025-12-11) + +* ember-cli 6.9.1 (patch) + +#### :bug: Bug Fix +* `ember-cli` + * [#10888](https://github.com/ember-cli/ember-cli/pull/10888) Upgrade broccoli ([@kategengler](https://github.com/kategengler)) + +#### Committers: 1 +- Katie Gengler ([@kategengler](https://github.com/kategengler)) + +## Release (2025-12-10) + +* ember-cli 6.9.0 (minor) +* @ember-tooling/classic-build-addon-blueprint 6.9.0 (minor) +* @ember-tooling/classic-build-app-blueprint 6.9.0 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-app-blueprint` + * [#10885](https://github.com/ember-cli/ember-cli/pull/10885) Promote Beta and update all dependencies for 6.9 release ([@mansona](https://github.com/mansona)) +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10854](https://github.com/ember-cli/ember-cli/pull/10854) Prepare Beta Release ([@mansona](https://github.com/mansona)) + +#### :house: Internal +* `ember-cli` + * [#10809](https://github.com/ember-cli/ember-cli/pull/10809) Update RELEASE ([@mansona](https://github.com/mansona)) + * [#10813](https://github.com/ember-cli/ember-cli/pull/10813) Document need for future vite experiment test cleanup ([@ef4](https://github.com/ef4)) + +#### Committers: 2 +- Chris Manson ([@mansona](https://github.com/mansona)) +- Edward Faulkner ([@ef4](https://github.com/ef4)) + +## Release (2025-11-29) + +* ember-cli 6.8.1 (patch) + +#### :bug: Bug Fix +* `ember-cli` + * [#10860](https://github.com/ember-cli/ember-cli/pull/10860) [BUGFIX release]: Enter the WatchDetector branch of the build command when EMBROIDER_PREBUILD is present ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### Committers: 1 +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +## Release (2025-10-14) + +* ember-cli 6.8.0 (minor) +* @ember-tooling/classic-build-addon-blueprint 6.8.0 (minor) +* @ember-tooling/classic-build-app-blueprint 6.8.0 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10853](https://github.com/ember-cli/ember-cli/pull/10853) Promote Beta and update all dependencies for 6.8 release ([@mansona](https://github.com/mansona)) + * [#10831](https://github.com/ember-cli/ember-cli/pull/10831) [bugfix beta] enable `--strict` by default to match new app blueprint ([@mansona](https://github.com/mansona)) + * [#10808](https://github.com/ember-cli/ember-cli/pull/10808) Prepare 6.8 Beta ([@mansona](https://github.com/mansona)) +* `ember-cli` + * [#10844](https://github.com/ember-cli/ember-cli/pull/10844) [beta] Error when `ember (generate|destroy) (http-proxy|http-mock|server)` is used in a Vite-based project ([@kategengler](https://github.com/kategengler)) + * [#10802](https://github.com/ember-cli/ember-cli/pull/10802) enable Vite by default ([@mansona](https://github.com/mansona)) + * [#10804](https://github.com/ember-cli/ember-cli/pull/10804) Support a `--ts` alias for the `addon`, `init` and `new` commands ([@bertdeblock](https://github.com/bertdeblock)) + * [#10781](https://github.com/ember-cli/ember-cli/pull/10781) Add new `VITE` experiment to generate app with new Vite blueprint ([@pichfl](https://github.com/pichfl)) + * [#10785](https://github.com/ember-cli/ember-cli/pull/10785) Depracate passing filenames and globs to `init` ([@pichfl](https://github.com/pichfl)) + * [#10776](https://github.com/ember-cli/ember-cli/pull/10776) Add deprecation warning for the `--embroider` argument ([@pichfl](https://github.com/pichfl)) +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10791](https://github.com/ember-cli/ember-cli/pull/10791) update the format of the ember-cli-update.json ([@mansona](https://github.com/mansona)) + +#### :bug: Bug Fix +* `ember-cli` + * [#10846](https://github.com/ember-cli/ember-cli/pull/10846) [beta bugfix] allow build --watch only in EMBROIDER_PREBUILD ([@mansona](https://github.com/mansona)) + * [#10826](https://github.com/ember-cli/ember-cli/pull/10826) move resolution of @ember/app-blueprint to prevent loading latest ([@mansona](https://github.com/mansona)) + * [#10782](https://github.com/ember-cli/ember-cli/pull/10782) update heimdall-fs-monitor ([@mansona](https://github.com/mansona)) +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10803](https://github.com/ember-cli/ember-cli/pull/10803) Add "ember-blueprint" to keywords in `package.json` for the classic blueprints ([@pichfl](https://github.com/pichfl)) +* `@ember-tooling/classic-build-app-blueprint` + * [#10798](https://github.com/ember-cli/ember-cli/pull/10798) Add import from ember-data breakage/deprecation ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) +* `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10707](https://github.com/ember-cli/ember-cli/pull/10707) Enabled recommended configs from eslint-plugin-n and eslint-plugin-qunit ([@ijlee2](https://github.com/ijlee2)) + +#### :memo: Documentation +* `ember-cli` + * [#10843](https://github.com/ember-cli/ember-cli/pull/10843) Further contextualize help output and error when options and commands that are no longer supported in Vite-based projects are used ([@kategengler](https://github.com/kategengler)) + * [#10840](https://github.com/ember-cli/ember-cli/pull/10840) [beta] fix help for vite-based projects ([@kategengler](https://github.com/kategengler)) + * [#10835](https://github.com/ember-cli/ember-cli/pull/10835) Update deprecation message for --embroider option ([@kategengler](https://github.com/kategengler)) + +#### :house: Internal +* `ember-cli`, `@ember-tooling/classic-build-app-blueprint` + * [#10847](https://github.com/ember-cli/ember-cli/pull/10847) Prepare Beta Release ([@mansona](https://github.com/mansona)) + * [#10825](https://github.com/ember-cli/ember-cli/pull/10825) Prepare Beta Release ([@mansona](https://github.com/mansona)) + * [#10820](https://github.com/ember-cli/ember-cli/pull/10820) Prepare Beta Release ([@mansona](https://github.com/mansona)) +* `ember-cli` + * [#10845](https://github.com/ember-cli/ember-cli/pull/10845) [beta] update @ember/app-blueprint to latest beta ([@mansona](https://github.com/mansona)) + * [#10833](https://github.com/ember-cli/ember-cli/pull/10833) [bugfix beta] bump the @ember/app-blueprint version ([@mansona](https://github.com/mansona)) + * [#10823](https://github.com/ember-cli/ember-cli/pull/10823) fix incorrect ember-cli-update version in tests ([@mansona](https://github.com/mansona)) + * [#10819](https://github.com/ember-cli/ember-cli/pull/10819) update @ember/app-blueprint beta version ([@mansona](https://github.com/mansona)) + * [#10806](https://github.com/ember-cli/ember-cli/pull/10806) skip build watch tests when vite is enabled ([@mansona](https://github.com/mansona)) + * [#10790](https://github.com/ember-cli/ember-cli/pull/10790) Reorganize tests for `new` and `addon` commands ([@pichfl](https://github.com/pichfl)) + * [#10783](https://github.com/ember-cli/ember-cli/pull/10783) remove unused changelog script ([@mansona](https://github.com/mansona)) + * [#10764](https://github.com/ember-cli/ember-cli/pull/10764) fix double CI run on release-plan PR ([@mansona](https://github.com/mansona)) + * [#10750](https://github.com/ember-cli/ember-cli/pull/10750) Add more notes to the Release.md ([@mansona](https://github.com/mansona)) +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10799](https://github.com/ember-cli/ember-cli/pull/10799) Prepare Alpha Release ([@mansona](https://github.com/mansona)) + * [#10778](https://github.com/ember-cli/ember-cli/pull/10778) Prepare Alpha Release ([@mansona](https://github.com/mansona)) + * [#10756](https://github.com/ember-cli/ember-cli/pull/10756) Prepare Alpha Release ([@mansona](https://github.com/mansona)) + * [#10763](https://github.com/ember-cli/ember-cli/pull/10763) Prepare 6.8-alpha ([@mansona](https://github.com/mansona)) + +#### Committers: 6 +- Bert De Block ([@bertdeblock](https://github.com/bertdeblock)) +- Chris Manson ([@mansona](https://github.com/mansona)) +- Florian Pichler ([@pichfl](https://github.com/pichfl)) +- Isaac Lee ([@ijlee2](https://github.com/ijlee2)) +- Katie Gengler ([@kategengler](https://github.com/kategengler)) +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +## Release (2025-10-08) + +* ember-cli 6.7.2 (patch) +* @ember-tooling/classic-build-addon-blueprint 6.7.1 (patch) +* @ember-tooling/classic-build-app-blueprint 6.7.2 (patch) + +#### :bug: Bug Fix +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10838](https://github.com/ember-cli/ember-cli/pull/10838) Add package license metadata to match repository ([@davidtaylorhq](https://github.com/davidtaylorhq)) + +#### Committers: 1 +- David Taylor ([@davidtaylorhq](https://github.com/davidtaylorhq)) + +## Release (2025-10-06) + +* ember-cli 6.7.1 (patch) +* @ember-tooling/classic-build-app-blueprint 6.7.1 (patch) + +#### :bug: Bug Fix +* `ember-cli` + * [#10834](https://github.com/ember-cli/ember-cli/pull/10834) [bugfix release] Fix tests using wrong versions ([@mansona](https://github.com/mansona)) +* `@ember-tooling/classic-build-app-blueprint` + * [#10824](https://github.com/ember-cli/ember-cli/pull/10824) [bugfix release] fix app-blueprint being considered a blueprint ([@mansona](https://github.com/mansona)) + +#### Committers: 1 +- Chris Manson ([@mansona](https://github.com/mansona)) + +## Release (2025-09-06) + +* ember-cli 6.7.0 (minor) +* @ember-tooling/classic-build-addon-blueprint 6.7.0 (minor) +* @ember-tooling/classic-build-app-blueprint 6.7.0 (minor) + +#### :rocket: Enhancement +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10796](https://github.com/ember-cli/ember-cli/pull/10796) Promote Beta and update all dependencies for 6.7 release ([@mansona](https://github.com/mansona)) + * [#10742](https://github.com/ember-cli/ember-cli/pull/10742) Drop node 18, add node 24 ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### :bug: Bug Fix +* `ember-cli` + * [#10794](https://github.com/ember-cli/ember-cli/pull/10794) Fix potential NPE ([@boris-petrov](https://github.com/boris-petrov)) + +#### :memo: Documentation +* `ember-cli` + * [#10724](https://github.com/ember-cli/ember-cli/pull/10724) Update RELEASE.md to fully document new release-plan setup ([@mansona](https://github.com/mansona)) + +#### :house: Internal +* `ember-cli` + * [#10801](https://github.com/ember-cli/ember-cli/pull/10801) update github-changelog ([@mansona](https://github.com/mansona)) + * [#10761](https://github.com/ember-cli/ember-cli/pull/10761) update release-plan ([@mansona](https://github.com/mansona)) + * [#10748](https://github.com/ember-cli/ember-cli/pull/10748) update blueprint version of ember-cli as part of release plan ([@mansona](https://github.com/mansona)) + * [#10747](https://github.com/ember-cli/ember-cli/pull/10747) only use the PAT for the PR creation in release-plan ([@mansona](https://github.com/mansona)) + * [#10744](https://github.com/ember-cli/ember-cli/pull/10744) run tests on the release-plan PR ([@mansona](https://github.com/mansona)) + * [#10735](https://github.com/ember-cli/ember-cli/pull/10735) remove unnecessary project override ([@mansona](https://github.com/mansona)) +* `ember-cli`, `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint` + * [#10800](https://github.com/ember-cli/ember-cli/pull/10800) fix release-plan for stable release ([@mansona](https://github.com/mansona)) + * [#10736](https://github.com/ember-cli/ember-cli/pull/10736) update all sub-packages to have the @ember-tooling prefix ([@mansona](https://github.com/mansona)) + * [#10671](https://github.com/ember-cli/ember-cli/pull/10671) Implement the built in app, addon, and blueprint blueprints by package lookup ([@mansona](https://github.com/mansona)) +* `@ember-tooling/classic-build-addon-blueprint`, `@ember-tooling/classic-build-app-blueprint`, `ember-cli` + * [#10738](https://github.com/ember-cli/ember-cli/pull/10738) add a repository for each of the packages ([@mansona](https://github.com/mansona)) + +#### Committers: 3 +- Boris Petrov ([@boris-petrov](https://github.com/boris-petrov)) +- Chris Manson ([@mansona](https://github.com/mansona)) +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +## Release (2025-07-29) + +* ember-cli 6.6.0 (minor) + +#### :rocket: Enhancement +* `ember-cli` + * [#10749](https://github.com/ember-cli/ember-cli/pull/10749) Update all dependencies for 6.6 release ([@mansona](https://github.com/mansona)) + * [#10751](https://github.com/ember-cli/ember-cli/pull/10751) [BETA BACKPORT] Backport drop node 18 ([@mansona](https://github.com/mansona)) + * [#10701](https://github.com/ember-cli/ember-cli/pull/10701) [ENHANCEMENT] Support Bun ([@hexadecy](https://github.com/hexadecy)) + * [#10664](https://github.com/ember-cli/ember-cli/pull/10664) [ENHANCEMENT] Remove ember-fetch ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### :bug: Bug Fix +* `ember-cli` + * [#10711](https://github.com/ember-cli/ember-cli/pull/10711) fix yuidoc generation on publish ([@mansona](https://github.com/mansona)) + * [#10702](https://github.com/ember-cli/ember-cli/pull/10702) [Bugfix]: Update ember-data and unify the versions between @ and non-@ ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### :house: Internal +* `ember-cli` + * [#10699](https://github.com/ember-cli/ember-cli/pull/10699) Start using release-plan for releasing ember-cli ([@mansona](https://github.com/mansona)) + * [#10670](https://github.com/ember-cli/ember-cli/pull/10670) testing: don't generate temp directories in the repo ([@mansona](https://github.com/mansona)) + +#### Committers: 3 +- Chris Manson ([@mansona](https://github.com/mansona)) +- Michel Couillard ([@hexadecy](https://github.com/hexadecy)) +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +## Release (2025-06-16) + +* ember-cli 6.5.0 (minor) + +#### :rocket: Enhancement +* `ember-cli` + * [#10721](https://github.com/ember-cli/ember-cli/pull/10721) Prepare 6.5.0 release ([@mansona](https://github.com/mansona)) + * [#10705](https://github.com/ember-cli/ember-cli/pull/10705) add `--strict` flag for new app and addon generation ([@mansona](https://github.com/mansona)) + +#### :bug: Bug Fix +* `ember-cli` + * [#10684](https://github.com/ember-cli/ember-cli/pull/10684) [BUGFIX] Remove deprecated `@types/eslint__js` dependency from `app` blueprint ([@gvdp](https://github.com/gvdp)) + +#### :house: Internal +* `ember-cli` + * [#10726](https://github.com/ember-cli/ember-cli/pull/10726) use patched github-changelog for properly rolling up beta changelog ([@mansona](https://github.com/mansona)) + * [#10722](https://github.com/ember-cli/ember-cli/pull/10722) prevent clashes between release-plan branches ([@mansona](https://github.com/mansona)) + * [#10719](https://github.com/ember-cli/ember-cli/pull/10719) [Backport release]: start using release-plan to release ember-cli ([@mansona](https://github.com/mansona)) + * [#10718](https://github.com/ember-cli/ember-cli/pull/10718) Prepare Beta Release v6.5.0-beta.2 ([@github-actions[bot]](https://github.com/apps/github-actions)) + * [#10717](https://github.com/ember-cli/ember-cli/pull/10717) pass --publish-branch to release-plan publish ([@mansona](https://github.com/mansona)) + * [#10716](https://github.com/ember-cli/ember-cli/pull/10716) Prepare Beta Release v6.5.0-beta.1 ([@github-actions[bot]](https://github.com/apps/github-actions)) + * [#10715](https://github.com/ember-cli/ember-cli/pull/10715) [Backport beta]: Set up release-plan ([@mansona](https://github.com/mansona)) + +#### Committers: 3 +- Chris Manson ([@mansona](https://github.com/mansona)) +- [@github-actions[bot]](https://github.com/apps/github-actions) +- [@gvdp](https://github.com/gvdp) + +## v6.4.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.3.0...v6.4.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.3.0...v6.4.0) + +#### Changelog + +- [#10598](https://github.com/ember-cli/ember-cli/pull/10598) [INTERNAL] Remove "locked versions for pre-v1.0.0 packages" tests [@bertdeblock](https://github.com/bertdeblock) +- [#10620](https://github.com/ember-cli/ember-cli/pull/10620) [BUGFIX] Handle errors gracefully when proxy target is down [@dwickern](https://github.com/dwickern) +- [#10641](https://github.com/ember-cli/ember-cli/pull/10641) [Bugfix] Fix missing lint dependency for typescript projects [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10659](https://github.com/ember-cli/ember-cli/pull/10659) [BUGFIX] Improve de-typing `.gts` files [@bertdeblock](https://github.com/bertdeblock) +- [#10661](https://github.com/ember-cli/ember-cli/pull/10661) [ENHANCEMENT] Update `ember-try` to v4 in blueprints [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v6.3.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.3.0...v6.3.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.3.0...v6.3.1) + +#### Changelog + +- [#10685](https://github.com/ember-cli/ember-cli/pull/10685) Add configuration to ember-cli-build to opt new projects out of the deprecated behavior of the Store class extending EmberObject [@kategengler](https://github.com/kategengler) + +Thank you to all who took the time to contribute! + +## v6.3.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.2.0...v6.3.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.2.0...v6.3.0) + +#### Changelog + +- [#10644](https://github.com/ember-cli/ember-cli/pull/10644) [BUGFIX beta] `--no-ember-data` fixes [@Windvis](https://github.com/Windvis) +- [#10647](https://github.com/ember-cli/ember-cli/pull/10647) [Bugfix beta]: Update ember-page-title to latest (v9) [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10668](https://github.com/ember-cli/ember-cli/pull/10668) Update the `--no-ember-data` fixtures [@Windvis](https://github.com/Windvis) +- [#10646](https://github.com/ember-cli/ember-cli/pull/10646) [Bugfix release]: Fix eslint parser for js when using --typescript [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10633](https://github.com/ember-cli/ember-cli/pull/10633) [BUGFIX release] Update all EmberData deps to stable [@Windvis](https://github.com/Windvis) +- [#10643](https://github.com/ember-cli/ember-cli/pull/10643) Remove unmaintained ember-cli-lodash-subset in favor of requiring functions directly from lodash [@kategengler](https://github.com/kategengler) +- [#10638](https://github.com/ember-cli/ember-cli/pull/10638) [BUGFIX release] Fix ember-data configuration again [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10534](https://github.com/ember-cli/ember-cli/pull/10534) [CLEANUP] Clean up `outputPaths` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10538](https://github.com/ember-cli/ember-cli/pull/10538) [CLEANUP] Clean up Travis CI deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10586](https://github.com/ember-cli/ember-cli/pull/10586) [INTERNAL] Add tests to ensure no linting errors post generating a new app or addon [@bertdeblock](https://github.com/bertdeblock) +- [#10587](https://github.com/ember-cli/ember-cli/pull/10587) [BUGFIX] Fix including `ember-source` types for v1 addons [@bertdeblock](https://github.com/bertdeblock) +- [#10589](https://github.com/ember-cli/ember-cli/pull/10589) [ENHANCEMENT] Deprecate v1 addon `contentFor` types [RFC 1044] [@bertdeblock](https://github.com/bertdeblock) +- [#10592](https://github.com/ember-cli/ember-cli/pull/10592) [BUGFIX] Fix ESLint config for v1 addons [@bertdeblock](https://github.com/bertdeblock) +- [#10593](https://github.com/ember-cli/ember-cli/pull/10593) [CLEANUP] Clean up old `broccoli-builder` fallback [@bertdeblock](https://github.com/bertdeblock) +- [#10594](https://github.com/ember-cli/ember-cli/pull/10594) [CLEANUP] Clean up old `heimdalljs` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10595](https://github.com/ember-cli/ember-cli/pull/10595) [ENHANCEMENT] Update `@glimmer/component` to v2 in blueprints [@bertdeblock](https://github.com/bertdeblock) +- [#10596](https://github.com/ember-cli/ember-cli/pull/10596) [ENHANCEMENT] Vanilla Prettier setup in blueprints [RFC 1055] [@bertdeblock](https://github.com/bertdeblock) +- [#10597](https://github.com/ember-cli/ember-cli/pull/10597) [CLEANUP] Clean up remaining Travis fixtures [@bertdeblock](https://github.com/bertdeblock) +- [#10599](https://github.com/ember-cli/ember-cli/pull/10599) [INTERNAL] Bump `content-tag` to v3 [@SergeAstapov](https://github.com/SergeAstapov) +- [#10612](https://github.com/ember-cli/ember-cli/pull/10612) [BUGFIX release]: tsconfig.json referenced paths to types instead of imports [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10613](https://github.com/ember-cli/ember-cli/pull/10613) [ENHANCEMENT] Support `--ember-data` / `--no-ember-data` flags when creating a new app [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10615](https://github.com/ember-cli/ember-cli/pull/10615) [ENHANCEMENT] Simplify Prettier config in blueprints [@bendemboski](https://github.com/bendemboski) +- [#10616](https://github.com/ember-cli/ember-cli/pull/10616) [INTERNAL] Add test to ensure all package files are parseable [@bertdeblock](https://github.com/bertdeblock) +- [#10617](https://github.com/ember-cli/ember-cli/pull/10617) [ENHANCEMENT] Use `staticInvokables` in the `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10618](https://github.com/ember-cli/ember-cli/pull/10618) [INTERNAL] Avoid output for `deprecate` tests [@bertdeblock](https://github.com/bertdeblock) +- [#10619](https://github.com/ember-cli/ember-cli/pull/10619) [BUGFIX] Only remove type imports when removing the types from `.gts` files in blueprints [@Windvis](https://github.com/Windvis) +- [#10621](https://github.com/ember-cli/ember-cli/pull/10621) [ENHANCEMENT] Bump `@ember/test-helpers` to v5.1.0 in blueprints [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v6.2.3 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.2.2...v6.2.3) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.2.2...v6.2.3) + +#### Changelog + +- [#10646](https://github.com/ember-cli/ember-cli/pull/10646) [Bugfix release]: Fix eslint parser for js when using --typescript [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v6.2.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.2.1...v6.2.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.2.1...v6.2.2) + +#### Changelog + +- [#10633](https://github.com/ember-cli/ember-cli/pull/10633) [BUGFIX release] Update all EmberData deps to stable [@Windvis](https://github.com/Windvis) +- [#10643](https://github.com/ember-cli/ember-cli/pull/10643) Remove unmaintained ember-cli-lodash-subset in favor of requiring functions directly from lodash [@kategengler](https://github.com/kategengler) + +Thank you to all who took the time to contribute! + +## v6.2.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.2.0...v6.2.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.2.0...v6.2.1) + +#### Changelog + +- [#10638](https://github.com/ember-cli/ember-cli/pull/10638) [BUGFIX release] Fix ember-data configuration again [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v6.2.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.1.0...v6.2.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.1.0...v6.2.0) + +#### Changelog + +- [#10496](https://github.com/ember-cli/ember-cli/pull/10496) [CLEANUP] Clean up support for incorrect values for `BROCCOLI_VIZ` env var [@bertdeblock](https://github.com/bertdeblock) +- [#10555](https://github.com/ember-cli/ember-cli/pull/10555) [ENHANCEMENT] Bump `pnpm/action-setup` to v4 in `app` and `addon` blueprints [@SergeAstapov](https://github.com/SergeAstapov) +- [#10562](https://github.com/ember-cli/ember-cli/pull/10562) [ENHANCEMENT] Allow creating apps and addons everywhere [@bertdeblock](https://github.com/bertdeblock) +- [#10577](https://github.com/ember-cli/ember-cli/pull/10577) [ENHANCEMENT] Remove `@ember/string` from `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10578](https://github.com/ember-cli/ember-cli/pull/10578) [ENHANCEMENT] Test against Node v22 [@bertdeblock](https://github.com/bertdeblock) +- [#10579](https://github.com/ember-cli/ember-cli/pull/10579) [INTERNAL] Update `sort-package-json` [@bertdeblock](https://github.com/bertdeblock) +- [#10580](https://github.com/ember-cli/ember-cli/pull/10580) [ENHANCEMENT] Update LTS versions in blueprints [@bertdeblock](https://github.com/bertdeblock) +- [#10583](https://github.com/ember-cli/ember-cli/pull/10583) [ENHANCEMENT] Update `app` blueprint to support `ember-qunit` v9 [@ef4](https://github.com/ef4) +- [#10585](https://github.com/ember-cli/ember-cli/pull/10585) [INTERNAL] Support `WRITE_FIXTURES` in more test files [@ef4](https://github.com/ef4) + +Thank you to all who took the time to contribute! + +## v6.1.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.0.0...v6.1.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.0.0...v6.1.0) + +#### Changelog + +- [#10563](https://github.com/ember-cli/ember-cli/pull/10563) [Backport release]: update @ember/test-helpers. #10522 [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10564](https://github.com/ember-cli/ember-cli/pull/10564) [BUGFIX] Add missing package for TS eslint config [@mkszepp](https://github.com/mkszepp) +- [#10514](https://github.com/ember-cli/ember-cli/pull/10514) [backport release] use fork of remove-types with config: false [@mansona](https://github.com/mansona) +- [#10515](https://github.com/ember-cli/ember-cli/pull/10515) Use colors for the concurrently prefixes in package.json [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10516](https://github.com/ember-cli/ember-cli/pull/10516) Use ESLint 9 and Flat Config [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10521](https://github.com/ember-cli/ember-cli/pull/10521) Make tests easier to run, closes #10520 [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10525](https://github.com/ember-cli/ember-cli/pull/10525) Upgrade concurrently, closes #10524 [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10526](https://github.com/ember-cli/ember-cli/pull/10526) Update ember-resolver, closes #10523 [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10527](https://github.com/ember-cli/ember-cli/pull/10527) Update @ember/test-helpers, closes #10522 [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10530](https://github.com/ember-cli/ember-cli/pull/10530) Update ember-load-initializers to v3 [@mkszepp](https://github.com/mkszepp) +- [#10531](https://github.com/ember-cli/ember-cli/pull/10531) Update stylelint and depended package [@mkszepp](https://github.com/mkszepp) +- [#10535](https://github.com/ember-cli/ember-cli/pull/10535) Upgrade eslint-plugin-ember [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v6.0.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v6.0.0...v6.0.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v6.0.0...v6.0.1) + +#### Changelog + +- [#10563](https://github.com/ember-cli/ember-cli/pull/10563) [Backport release]: update @ember/test-helpers. #10522 [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v6.0.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.12.0...v6.0.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.12.0...v6.0.0) + +#### Changelog + +- [#10559](https://github.com/ember-cli/ember-cli/pull/10559) [ENHANCEMENT] Make deprecations throw when the `until` for `ember-cli` has passed [@kategengler](https://github.com/kategengler) +- [#10453](https://github.com/ember-cli/ember-cli/pull/10453) [ENHANCEMENT] Allow specifying no CI provider [@deanylev](https://github.com/deanylev) +- [#10505](https://github.com/ember-cli/ember-cli/pull/10505) use our fork of remove-types with config: false [@mansona](https://github.com/mansona) +- [#10506](https://github.com/ember-cli/ember-cli/pull/10506) [ENHANCEMENT] Use the official types in the blueprints [@Windvis](https://github.com/Windvis) +- [#10507](https://github.com/ember-cli/ember-cli/pull/10507) Official types in blueprints amendments [@Windvis](https://github.com/Windvis) + +Thank you to all who took the time to contribute! + +## v5.12.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.11.0...v5.12.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.11.0...v5.12.0) + +#### Changelog + +- [#10483](https://github.com/ember-cli/ember-cli/pull/10483) Bump @embroider/* dependencies [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10486](https://github.com/ember-cli/ember-cli/pull/10486) Bump ember-template-lint [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10481](https://github.com/ember-cli/ember-cli/pull/10481) Bump ember-cli-app-version [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10485](https://github.com/ember-cli/ember-cli/pull/10485) Bump eslint-plugin-ember [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10484](https://github.com/ember-cli/ember-cli/pull/10484) Bump ember-resolver [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10479](https://github.com/ember-cli/ember-cli/pull/10479) Bump @typescript-eslint dependencies to latest [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10482](https://github.com/ember-cli/ember-cli/pull/10482) Bump qunit-dom [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10480](https://github.com/ember-cli/ember-cli/pull/10480) Bump @ember/string [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10499](https://github.com/ember-cli/ember-cli/pull/10499) [ENHANCEMENT] Update `testem` [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v5.11.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.10.0...v5.11.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.10.0...v5.11.0) + +#### Changelog + +- [#10474](https://github.com/ember-cli/ember-cli/pull/10474) Improve dx when WelcomePage is present [@ef4](https://github.com/ef4) +- [#10475](https://github.com/ember-cli/ember-cli/pull/10475) Document WRITE_FIXTURES [@kategengler](https://github.com/kategengler) +- [#10476](https://github.com/ember-cli/ember-cli/pull/10476) Bump content-tag to v2 [@SergeAstapov](https://github.com/SergeAstapov) + +Thank you to all who took the time to contribute! + +## v5.10.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.9.0...v5.10.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.9.0...v5.10.0) + +#### Changelog + +- [#10464](https://github.com/ember-cli/ember-cli/pull/10464) Specified the locale in setupIntl() [@ijlee2](https://github.com/ijlee2) + +Thank you to all who took the time to contribute! + +## v5.9.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.8.1...v5.9.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.8.1...v5.9.0) + +#### Changelog + +- [#10446](https://github.com/ember-cli/ember-cli/pull/10446) [ENHANCEMENT] Format markdown files in blueprints with Prettier [@bertdeblock](https://github.com/bertdeblock) +- [#10450](https://github.com/ember-cli/ember-cli/pull/10450) [ENHANCEMENT] Remove warning when encountering a `.js` file when generating a TS blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10452](https://github.com/ember-cli/ember-cli/pull/10452) [BUGFIX] Make sure to use the correct package manager in concurrently scripts [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v5.8.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.8.0...v5.8.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.8.0...v5.8.1) + +#### Changelog + +- [#10458](https://github.com/ember-cli/ember-cli/pull/10458) Use Lodash's `_.template` instead of `lodash.template` package [@gorner](https://github.com/gorner) + +Thank you to all who took the time to contribute! + +## v5.8.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.7.0...v5.8.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.7.0...v5.8.0) + +#### Changelog + +- [#10418](https://github.com/ember-cli/ember-cli/pull/10418) [ENHANCEMENT] Use `content-tag` to parse GTS in blueprints [@IgnaceMaes](https://github.com/IgnaceMaes) +- [#10432](https://github.com/ember-cli/ember-cli/pull/10432) Filter out tsconfig.declarations.json correctly [@bendemboski](https://github.com/bendemboski) +- [#10436](https://github.com/ember-cli/ember-cli/pull/10436) stop using wyvox/action-setup-pnpm [@mansona](https://github.com/mansona) +- [#10437](https://github.com/ember-cli/ember-cli/pull/10437) [ENHANCEMENT] Update LTS scenarios in `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10438](https://github.com/ember-cli/ember-cli/pull/10438) [ENHANCEMENT] Add `declarations` folder to `.eslintignore` file in `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10439](https://github.com/ember-cli/ember-cli/pull/10439) [ENHANCEMENT] Add tsconfig files to `.npmignore` file in `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v5.7.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.6.0...v5.7.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.6.0...v5.7.0) + +#### Changelog + +- [#10440](https://github.com/ember-cli/ember-cli/pull/10440) Update optional-features.json [@achambers](https://github.com/achambers) +- [#10445](https://github.com/ember-cli/ember-cli/pull/10445) Unpin Node 18 for ci [@kategengler](https://github.com/kategengler) +- [#10425](https://github.com/ember-cli/ember-cli/pull/10425) fix GitHub Action floating deps scenario for PNPM [@jelhan](https://github.com/jelhan) + +Thank you to all who took the time to contribute! + +## v5.6.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.5.0...v5.6.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.5.0...v5.6.0) + +#### Changelog + +- [#10394](https://github.com/ember-cli/ember-cli/pull/10394) [ENHANCEMENT] automatically select a port by default with `ember serve` [@sportshead](https://github.com/sportshead) +- [#10404](https://github.com/ember-cli/ember-cli/pull/10404) Add a workflow to deploy api docs to ember-learn/ember-cli.github.io [@kategengler](https://github.com/kategengler) +- [#10405](https://github.com/ember-cli/ember-cli/pull/10405) Update to deploy to the master branch and also correct a comment [@kategengler](https://github.com/kategengler) + +Thank you to all who took the time to contribute! + +## v5.5.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.4.1...v5.5.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.4.1...v5.5.0) + +#### Changelog + +- [#10332](https://github.com/ember-cli/ember-cli/pull/10332) [ENHANCEMENT] Support converting gts files in blueprint [@IgnaceMaes](https://github.com/IgnaceMaes) +- [#10350](https://github.com/ember-cli/ember-cli/pull/10350) [ENHANCEMENT] Deprecate Travis CI support [@bertdeblock](https://github.com/bertdeblock) +- [#10370](https://github.com/ember-cli/ember-cli/pull/10370) When generating a new app with --embroider use all optimisation flags [@mansona](https://github.com/mansona) +- [#10393](https://github.com/ember-cli/ember-cli/pull/10393) [ENHANCEMENT] feat: add skip-install alias to skip-npm [@IgnaceMaes](https://github.com/IgnaceMaes) +- [#10403](https://github.com/ember-cli/ember-cli/pull/10403) Fix some docs that were showing up weirdly in generated api docs [@kategengler](https://github.com/kategengler) +- [#9514](https://github.com/ember-cli/ember-cli/pull/9514) [ENHANCEMENT] Use packager commands in `CONTRIBUTING.md` and `README.md` files in `app` and `addon` blueprints [@elwayman02](https://github.com/elwayman02) + +Thank you to all who took the time to contribute! + +## v5.4.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.4.1...v5.4.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.4.1...v5.4.2) + +#### Changelog + +- [#10458](https://github.com/ember-cli/ember-cli/pull/10458) Use Lodash's `_.template` instead of `lodash.template` package [@gorner](https://github.com/gorner) + +Thank you to all who took the time to contribute! + +## v5.4.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.4.0...v5.4.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.4.0...v5.4.1) + +#### Changelog + +- [#10402](https://github.com/ember-cli/ember-cli/pull/10402) [BUGFIX release] use import type in helpers/index.ts :: typechecking in new apps otherwise fails [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v5.4.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.3.0...v5.4.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.3.0...v5.4.0) + +#### Changelog + +- [#10388](https://github.com/ember-cli/ember-cli/pull/10388) [CLEANUP] Drop support for Node v16 [@Shishouille](https://github.com/Shishouille) +- [#10345](https://github.com/ember-cli/ember-cli/pull/10345) [BUGFIX beta] App blueprint may not have explicit-any in ember-data types [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10351](https://github.com/ember-cli/ember-cli/pull/10351) [ENHANCEMENT] Remove `ember-lts-4.4` scenario from `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10352](https://github.com/ember-cli/ember-cli/pull/10352) [ENHANCEMENT] Add official support for Node.js v20 [@bertdeblock](https://github.com/bertdeblock) +- [#10353](https://github.com/ember-cli/ember-cli/pull/10353) [ENHANCEMENT] Remove all telemetry [@bertdeblock](https://github.com/bertdeblock) +- [#10354](https://github.com/ember-cli/ember-cli/pull/10354) [INTERNAL] Remove `@babel/core` as a dependency [@bertdeblock](https://github.com/bertdeblock) +- [#10368](https://github.com/ember-cli/ember-cli/pull/10368) [ENHANCEMENT] Streamline package-manager CLI options [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v5.3.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.2.0...v5.3.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.2.0...v5.3.0) + +#### Changelog + +- [#10346](https://github.com/ember-cli/ember-cli/pull/10346) [BUGFIX release] App blueprint may not have explicit-any in ember-data types [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10349](https://github.com/ember-cli/ember-cli/pull/10349) [BUGFIX release] Add `@babel/core` to `app` and `addon` blueprints [@bertdeblock](https://github.com/bertdeblock) +- [#10162](https://github.com/ember-cli/ember-cli/pull/10162) [ENHANCEMENT] Deprecate `outputPaths` build option [@bertdeblock](https://github.com/bertdeblock) +- [#10187](https://github.com/ember-cli/ember-cli/pull/10187) [ENHANCEMENT] Remove Node version checking [@bertdeblock](https://github.com/bertdeblock) +- [#10249](https://github.com/ember-cli/ember-cli/pull/10249) Serve app on root url without trailing slash [@mmun](https://github.com/mmun) +- [#10311](https://github.com/ember-cli/ember-cli/pull/10311) [ENHANCEMENT] Add v4.12 LTS scenario to `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10316](https://github.com/ember-cli/ember-cli/pull/10316) [BUGFIX] Remove `auto` as a possible value for `locationType` in `config` declaration [@bertdeblock](https://github.com/bertdeblock) +- [#10319](https://github.com/ember-cli/ember-cli/pull/10319) Use pnpm-action from org [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10331](https://github.com/ember-cli/ember-cli/pull/10331) [ENHANCEMENT] Exclude `@ember/string` from `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10335](https://github.com/ember-cli/ember-cli/pull/10335) Update ci.yml to trigger on merge queue [@locks](https://github.com/locks) +- [#10337](https://github.com/ember-cli/ember-cli/pull/10337) remove EMBER_CLI_PNPM [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10338](https://github.com/ember-cli/ember-cli/pull/10338) [INTERNAL] Remove `PNPM` experiment from CI matrix [@bertdeblock](https://github.com/bertdeblock) +- [#10341](https://github.com/ember-cli/ember-cli/pull/10341) [ENHANCEMENT] Remove reference to `ember-mocha` in `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#8578](https://github.com/ember-cli/ember-cli/pull/8578) By default make ember test to pick ports automatically [@SparshithNR](https://github.com/SparshithNR) + +Thank you to all who took the time to contribute! + +## v5.2.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.2.0...v5.2.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.2.0...v5.2.1) + +#### Changelog + +- [#10346](https://github.com/ember-cli/ember-cli/pull/10346) [BUGFIX release] App blueprint may not have explicit-any in ember-data types [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10349](https://github.com/ember-cli/ember-cli/pull/10349) [BUGFIX release] Add `@babel/core` to `app` and `addon` blueprints [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v5.2.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.1.0...v5.2.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.1.0...v5.2.0) + +#### Changelog + +- [#10283](https://github.com/ember-cli/ember-cli/pull/10283) Refactor `--typescript` support in blueprints [@simonihmig](https://github.com/simonihmig) + +Thank you to all who took the time to contribute! + +## v5.1.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v5.0.0...v5.1.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v5.0.0...v5.1.0) + +#### Changelog + +- [#10300](https://github.com/ember-cli/ember-cli/pull/10300) [BUGFIX] Do not try to wire up Testem unless a test framework is detected [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10256](https://github.com/ember-cli/ember-cli/pull/10256) [ENHANCEMENT] Align `hbs` templates generated by `app` blueprint with Prettier defaults [@jelhan](https://github.com/jelhan) +- [#10276](https://github.com/ember-cli/ember-cli/pull/10276) [BUGFIX] Widen peer dependency range for ember-source [@jrjohnson](https://github.com/jrjohnson) +- [#10279](https://github.com/ember-cli/ember-cli/pull/10279) Updated ember-welcome-page to v7.0.2 [@ijlee2](https://github.com/ijlee2) +- [#10287](https://github.com/ember-cli/ember-cli/pull/10287) Implementation of RFC 907 - pnpm support [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v5.0.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.12.1...v5.0.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.12.1...v5.0.0) + +#### Changelog + +- [#10244](https://github.com/ember-cli/ember-cli/pull/10244) Update ember-cli-preprocess-registry and add ember-cli-clean-css to the blueprints [@kategengler](https://github.com/kategengler) +- [#10276](https://github.com/ember-cli/ember-cli/pull/10276) Widen peer dependency range for ember-source [@jrjohnson](https://github.com/jrjohnson) +- [#10019](https://github.com/ember-cli/ember-cli/pull/10019) [CLEANUP] Drop support for the `baseURL` environment option [@bertdeblock](https://github.com/bertdeblock) +- [#10106](https://github.com/ember-cli/ember-cli/pull/10106) [CLEANUP] Remove deprecated `PACKAGER` experiment [@bertdeblock](https://github.com/bertdeblock) +- [#10175](https://github.com/ember-cli/ember-cli/pull/10175) [CLEANUP] Clean up `blacklist-whitelist-build-options` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10176](https://github.com/ember-cli/ember-cli/pull/10176) [CLEANUP] Clean up `vendor-shim-blueprint` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10177](https://github.com/ember-cli/ember-cli/pull/10177) [CLEANUP] Clean up `ember-cli-jshint-support` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10178](https://github.com/ember-cli/ember-cli/pull/10178) [CLEANUP] Remove all Bower entries from ignore files in `app` and `addon` blueprints [@bertdeblock](https://github.com/bertdeblock) +- [#10182](https://github.com/ember-cli/ember-cli/pull/10182) [CLEANUP] Clean up `project.bower-dependencies` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10183](https://github.com/ember-cli/ember-cli/pull/10183) [CLEANUP] Clean up `project.bower-directory` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10184](https://github.com/ember-cli/ember-cli/pull/10184) [CLEANUP] Clean up `blueprint.add-bower-package-to-project` and `blueprint.add-bower-packages-to-project` deprecations [@bertdeblock](https://github.com/bertdeblock) +- [#10185](https://github.com/ember-cli/ember-cli/pull/10185) [CLEANUP] Drop support for the `EMBER_CLI_ERROR_ON_INVALID_ADDON` env flag [@bertdeblock](https://github.com/bertdeblock) +- [#10186](https://github.com/ember-cli/ember-cli/pull/10186) [CLEANUP] Drop support for installing Bower packages [@bertdeblock](https://github.com/bertdeblock) +- [#10193](https://github.com/ember-cli/ember-cli/pull/10193) [CLEANUP] Drop support for `ember-cli-babel` v5 and v6 [@bertdeblock](https://github.com/bertdeblock) +- [#10194](https://github.com/ember-cli/ember-cli/pull/10194) [CLEANUP] Drop support for `ember-cli-shims` [@bertdeblock](https://github.com/bertdeblock) +- [#10195](https://github.com/ember-cli/ember-cli/pull/10195) [CLEANUP] Remove automatic inclusion of jQuery in legacy Ember versions [@bertdeblock](https://github.com/bertdeblock) +- [#10196](https://github.com/ember-cli/ember-cli/pull/10196) [INTERNAL] Remove unused private `_legacyAddonCompile` method on `EmberApp` class [@bertdeblock](https://github.com/bertdeblock) +- [#10197](https://github.com/ember-cli/ember-cli/pull/10197) [CLEANUP] Remove default `minifyJS` options [@bertdeblock](https://github.com/bertdeblock) +- [#10211](https://github.com/ember-cli/ember-cli/pull/10211) Automate output repos [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#10217](https://github.com/ember-cli/ember-cli/pull/10217) [CLEANUP] Remove `ember-resolver` fallback [@bertdeblock](https://github.com/bertdeblock) +- [#10218](https://github.com/ember-cli/ember-cli/pull/10218) [CLEANUP] Drop support for including `handlebars.js` via Bower [@bertdeblock](https://github.com/bertdeblock) +- [#10219](https://github.com/ember-cli/ember-cli/pull/10219) [CLEANUP] Drop support for npm versions below v5.7.1 [@bertdeblock](https://github.com/bertdeblock) +- [#10220](https://github.com/ember-cli/ember-cli/pull/10220) [CLEANUP] Drop support for including Ember builds via Bower [@bertdeblock](https://github.com/bertdeblock) +- [#10221](https://github.com/ember-cli/ember-cli/pull/10221) [CLEANUP] Clean up `building-bower-packages` deprecation [@bertdeblock](https://github.com/bertdeblock) +- [#10222](https://github.com/ember-cli/ember-cli/pull/10222) [CLEANUP] Drop support for Node v14 [@bertdeblock](https://github.com/bertdeblock) +- [#10223](https://github.com/ember-cli/ember-cli/pull/10223) [CLEANUP] Drop support for finding addons by their `index.js` name [@bertdeblock](https://github.com/bertdeblock) +- [#10224](https://github.com/ember-cli/ember-cli/pull/10224) [CLEANUP] Drop support for checking if Bower components are installed [@bertdeblock](https://github.com/bertdeblock) +- [#10225](https://github.com/ember-cli/ember-cli/pull/10225) [CLEANUP] Drop support for the `EMBER_CLI_IGNORE_ADDON_NAME_MISMATCH` env flag [@bertdeblock](https://github.com/bertdeblock) +- [#10226](https://github.com/ember-cli/ember-cli/pull/10226) [INTERNAL] Remove all remaining Bower references [@bertdeblock](https://github.com/bertdeblock) +- [#10227](https://github.com/ember-cli/ember-cli/pull/10227) [INTERNAL] Remove JSHint reference [@bertdeblock](https://github.com/bertdeblock) +- [#10229](https://github.com/ember-cli/ember-cli/pull/10229) Update blueprint ignore files [@bertdeblock](https://github.com/bertdeblock) +- [#10231](https://github.com/ember-cli/ember-cli/pull/10231) [INTERNAL] Remove npm version check in `ember new` test [@bertdeblock](https://github.com/bertdeblock) +- [#10232](https://github.com/ember-cli/ember-cli/pull/10232) [CLEANUP] Remove Babel fallback for addons [@bertdeblock](https://github.com/bertdeblock) +- [#10245](https://github.com/ember-cli/ember-cli/pull/10245) change guarding condition for output repos [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v4.12.3 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.12.2...v4.12.3) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.12.2...v4.12.3) + +#### Changelog + +- [#10458](https://github.com/ember-cli/ember-cli/pull/10458) Use Lodash's `_.template` instead of `lodash.template` package [@gorner](https://github.com/gorner) + +Thank you to all who took the time to contribute! + +## v4.12.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.12.1...v4.12.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.12.1...v4.12.2) + +#### Changelog + +- [#10314](https://github.com/ember-cli/ember-cli/pull/10314) [BUGFIX] Do not try to wire up Testem unless a test framework is dete… [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v4.12.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.12.0...v4.12.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.12.0...v4.12.1) + +#### Changelog + +- [#10251](https://github.com/ember-cli/ember-cli/pull/10251) [BUGFIX release] Remove `stylelint-config-prettier` from `app` blueprint [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v4.12.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.11.0...v4.12.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.11.0...v4.12.0) + +#### Changelog + +- [#10039](https://github.com/ember-cli/ember-cli/pull/10039) [CLEANUP] Remove unused test fixtures [@bertdeblock](https://github.com/bertdeblock) +- [#10040](https://github.com/ember-cli/ember-cli/pull/10040) [CLEANUP] Remove MU-related debug code [@bertdeblock](https://github.com/bertdeblock) +- [#10091](https://github.com/ember-cli/ember-cli/pull/10091) [BUGFIX] Fix looking up the default blueprint for scoped addons [@GendelfLugansk](https://github.com/GendelfLugansk) +- [#10107](https://github.com/ember-cli/ember-cli/pull/10107) [INTERNAL] Remove old `uninstall:npm` command [@bertdeblock](https://github.com/bertdeblock) +- [#10122](https://github.com/ember-cli/ember-cli/pull/10122) [ENHANCEMENT] Add Stylelint setup to `app` and `addon` blueprints [RFC 814] [@bmish](https://github.com/bmish) +- [#10142](https://github.com/ember-cli/ember-cli/pull/10142) [ENHANCEMENT] Update ESLint to v8 in `app` and `addon` blueprints [@bertdeblock](https://github.com/bertdeblock) +- [#10157](https://github.com/ember-cli/ember-cli/pull/10157) [ENHANCEMENT] Exclude `ember-cli-terser` when generating apps using the `--embroider` option [@bertdeblock](https://github.com/bertdeblock) +- [#10159](https://github.com/ember-cli/ember-cli/pull/10159) [ENHANCEMENT] Exclude `ember-cli-sri` when generating apps using the `--embroider` option [@bertdeblock](https://github.com/bertdeblock) +- [#10161](https://github.com/ember-cli/ember-cli/pull/10161) [ENHANCEMENT] Remove NPM version checking [@bertdeblock](https://github.com/bertdeblock) +- [#10166](https://github.com/ember-cli/ember-cli/pull/10166) [INTERNAL] Remove unused dependencies [@bertdeblock](https://github.com/bertdeblock) +- [#10169](https://github.com/ember-cli/ember-cli/pull/10169) [ENHANCEMENT] Remove `app.import` comment in `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10172](https://github.com/ember-cli/ember-cli/pull/10172) [PERFORMANCE] Lazy require heavier packages [@bertdeblock](https://github.com/bertdeblock) +- [#10173](https://github.com/ember-cli/ember-cli/pull/10173) [ENHANCEMENT] Don't delete the newly-generated app directory when an error occurs [@ef4](https://github.com/ef4) +- [#10174](https://github.com/ember-cli/ember-cli/pull/10174) [INTERNAL] Clean up removal of `bower.json` and `package.json` files in `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10180](https://github.com/ember-cli/ember-cli/pull/10180) [ENHANCEMENT] Update `ember-welcome-page` to v7 in `app` blueprint [@ijlee2](https://github.com/ijlee2) +- [#10188](https://github.com/ember-cli/ember-cli/pull/10188) [ENHANCEMENT] Update ESLint parser `ecmaVersion` to `latest` in `app` blueprint [@elwayman02](https://github.com/elwayman02) +- [#10189](https://github.com/ember-cli/ember-cli/pull/10189) [ENHANCEMENT] Add v4.8 LTS to `addon` blueprint - Remove v3.28 LTS and `ember-classic` scenario [@bertdeblock](https://github.com/bertdeblock) +- [#10192](https://github.com/ember-cli/ember-cli/pull/10192) [BUGFIX] The `addon` command should throw when no addon name is provided [@bertdeblock](https://github.com/bertdeblock) +- [#10198](https://github.com/ember-cli/ember-cli/pull/10198) [INTERNAL] Output TypeScript apps and addons for StackBlitz [@NullVoxPopuli](https://github.com/NullVoxPopuli) + +Thank you to all who took the time to contribute! + +## v4.11.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.10.0...v4.11.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.10.0...v4.11.0) + +#### Changelog + +- [#10103](https://github.com/ember-cli/ember-cli/pull/10103) Update `markdown-it-terminal` to v0.4.0 (resolve `markdown-it` vulnerability) [@bertdeblock](https://github.com/bertdeblock) +- [#10109](https://github.com/ember-cli/ember-cli/pull/10109) [RFC 811] Add `ember-modifier` dependency to app blueprint [@SergeAstapov](https://github.com/SergeAstapov) +- [#10110](https://github.com/ember-cli/ember-cli/pull/10110) [RFC 812] Add `tracked-built-ins` dependency to app blueprint [@SergeAstapov](https://github.com/SergeAstapov) + +Thank you to all who took the time to contribute! + +## v4.10.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.9.2...v4.10.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.9.2...v4.10.0) + +#### Changelog + +- [#10048](https://github.com/ember-cli/ember-cli/pull/10048) Make addAddonsToProject support creating a new project with custom target directory [@simonihmig](https://github.com/simonihmig) +- [#10054](https://github.com/ember-cli/ember-cli/pull/10054) [ENHANCEMENT] Conditionally apply ESLint parser options in `app` and `addon` blueprints [@Windvis](https://github.com/Windvis) +- [#10060](https://github.com/ember-cli/ember-cli/pull/10060) [ENHANCEMENT] Replace `eslint-plugin-node` with `eslint-plugin-n` in blueprints [@Windvis](https://github.com/Windvis) +- [#10062](https://github.com/ember-cli/ember-cli/pull/10062) [ENHANCEMENT] Update Prettier config in blueprints to only use single quotes for `.js` and `.ts` files [@Windvis](https://github.com/Windvis) +- [#10132](https://github.com/ember-cli/ember-cli/pull/10132) Add @ember/string as a dependency of projects [@kategengler](https://github.com/kategengler) + +Thank you to all who took the time to contribute! + +## v4.9.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.9.1...v4.9.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.9.1...v4.9.2) + +#### Changelog + +- [#10108](https://github.com/ember-cli/ember-cli/pull/10108) [BUGFIX release] Correctly instantiate server watcher [@bertdeblock](https://github.com/bertdeblock) + +## v4.9.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.9.0...v4.9.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.9.0...v4.9.1) + +#### Changelog + +- [#10101](https://github.com/ember-cli/ember-cli/pull/10101) [BUGFIX release] Correctly instantiate `Watcher` instance when running `ember test --serve` [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v4.9.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.8.0...v4.9.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.8.0...v4.9.0) + +#### Changelog + +- [#10015](https://github.com/ember-cli/ember-cli/pull/10015) Update `glob` to v8 [@bertdeblock](https://github.com/bertdeblock) +- [#10016](https://github.com/ember-cli/ember-cli/pull/10016) Fix indentation in `.ember-cli` file in `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10017](https://github.com/ember-cli/ember-cli/pull/10017) [ENHANCEMENT] Disable prototype extensions by default in `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10018](https://github.com/ember-cli/ember-cli/pull/10018) Trap unhandled failures [@ef4](https://github.com/ef4) +- [#10020](https://github.com/ember-cli/ember-cli/pull/10020) [INTERNAL] Fix typos in `serve` command test [@bertdeblock](https://github.com/bertdeblock) +- [#10021](https://github.com/ember-cli/ember-cli/pull/10021) [CLEANUP] Drop support for using `usePods: true` and the `--pod` flag simultaneously [@bertdeblock](https://github.com/bertdeblock) +- [#10022](https://github.com/ember-cli/ember-cli/pull/10022) [ENHANCEMENT] Use `concurrently` instead of `npm-run-all` in `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10024](https://github.com/ember-cli/ember-cli/pull/10024) [ENHANCEMENT] Add `ember-source` to `peerDependencies` in `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10025](https://github.com/ember-cli/ember-cli/pull/10025) [ENHANCEMENT] Update NPM version constraints [@bertdeblock](https://github.com/bertdeblock) +- [#10026](https://github.com/ember-cli/ember-cli/pull/10026) [ENHANCEMENT] Display info message when running the `lint:fix` script post blueprint generation [@bertdeblock](https://github.com/bertdeblock) +- [#10038](https://github.com/ember-cli/ember-cli/pull/10038) Update `filesize` to v10 [@bertdeblock](https://github.com/bertdeblock) +- [#10041](https://github.com/ember-cli/ember-cli/pull/10041) [INTERNAL] Remove end year from copyright notice [@bertdeblock](https://github.com/bertdeblock) +- [#10049](https://github.com/ember-cli/ember-cli/pull/10049) [ENHANCEMENT] Remove the `config/environment.js` file from the `addon` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10050](https://github.com/ember-cli/ember-cli/pull/10050) [ENHANCEMENT] Remove `vendor` folder from `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#10051](https://github.com/ember-cli/ember-cli/pull/10051) [ENHANCEMENT] Move `ember-try.js` config file to `tests/dummy/config/ember-try.js` for addons [@bertdeblock](https://github.com/bertdeblock) +- [#10053](https://github.com/ember-cli/ember-cli/pull/10053) Add support for node ESM addons [@hjdivad](https://github.com/hjdivad) +- [#9824](https://github.com/ember-cli/ember-cli/pull/9824) [RFC 638] Interactive way to create new Ember apps and addons [@bertdeblock](https://github.com/bertdeblock) +- [#9972](https://github.com/ember-cli/ember-cli/pull/9972) [ENHANCEMENT] Add support for `--typescript` flag to `app` and `addon` blueprints [@simonihmig](https://github.com/simonihmig) + +Thank you to all who took the time to contribute! + +## v4.8.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.7.0...v4.8.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.7.0...v4.8.0) + +#### Changelog + +- [#10014](https://github.com/ember-cli/ember-cli/pull/10014) [BUGFIX release] Make sure newly installed addons are discovered when running `ember install` [@bertdeblock](https://github.com/bertdeblock) +- [#9920](https://github.com/ember-cli/ember-cli/pull/9920) [BUGFIX] Make sure a blueprint’s options object and project instance are always available for all public hooks [@bertdeblock](https://github.com/bertdeblock) +- [#9945](https://github.com/ember-cli/ember-cli/pull/9945) [ENHANCEMENT/BREAKING] Add Node v18 to `engines` in `app` and `addon` blueprint (removes support for Node v17) [@bertdeblock](https://github.com/bertdeblock) +- [#9946](https://github.com/ember-cli/ember-cli/pull/9946) [INTERNAL] Unskip `package-info-cache` tests [@bertdeblock](https://github.com/bertdeblock) +- [#9951](https://github.com/ember-cli/ember-cli/pull/9951) Update `js-yaml` to v4 [@bertdeblock](https://github.com/bertdeblock) +- [#9952](https://github.com/ember-cli/ember-cli/pull/9952) Update `walk-sync` to v3 [@bertdeblock](https://github.com/bertdeblock) +- [#9971](https://github.com/ember-cli/ember-cli/pull/9971) Add Ember 4.4 LTS to addon blueprint, remove 3.24 [@simonihmig](https://github.com/simonihmig) +- [#9975](https://github.com/ember-cli/ember-cli/pull/9975) [ENHANCEMENT] Fix `prefer-const` lint violations in `app` and `addon` blueprints [@bmish](https://github.com/bmish) +- [#9987](https://github.com/ember-cli/ember-cli/pull/9987) [BUGFIX] Handle rebuild failures without exiting [@bendemboski](https://github.com/bendemboski) +- [#9988](https://github.com/ember-cli/ember-cli/pull/9988) [BUGFIX] - Address npm-run-all and Yarn 3 conflict & Removed warning [@christianarty](https://github.com/christianarty) + +Thank you to all who took the time to contribute! + +## v4.7.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.6.0...v4.7.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.6.0...v4.7.0) + +#### Changelog + +- [#10009](https://github.com/ember-cli/ember-cli/pull/10009) [BUGFIX] Handle rebuild failures without exiting [@bendemboski](https://github.com/bendemboski) + +Thank you to all who took the time to contribute! + +## v4.6.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.5.0...v4.6.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.5.0...v4.6.0) + +#### Changelog + +- [#9890](https://github.com/ember-cli/ember-cli/pull/9890) Make sure addons are discovered only once [@wagenet](https://github.com/wagenet) +- [#9770](https://github.com/ember-cli/ember-cli/pull/9770) Include the addon’s name in the warning when a core command is overridden [@davecombs](https://github.com/davecombs) +- [#9863](https://github.com/ember-cli/ember-cli/pull/9863) [INTERNAL] Add `.prettierignore` file to Ember CLI repo [@bertdeblock](https://github.com/bertdeblock) +- [#9872](https://github.com/ember-cli/ember-cli/pull/9872) [INTERNAL] Use native classes for tasks in test suite [@bertdeblock](https://github.com/bertdeblock) +- [#9885](https://github.com/ember-cli/ember-cli/pull/9885) [CLEANUP] Remove `ie 11` from default targets [@bertdeblock](https://github.com/bertdeblock) +- [#9898](https://github.com/ember-cli/ember-cli/pull/9898) Remove deprecated `addonJsFiles` method on `addon` model [@bertdeblock](https://github.com/bertdeblock) +- [#9899](https://github.com/ember-cli/ember-cli/pull/9899) Remove deprecated internal `silent` error [@bertdeblock](https://github.com/bertdeblock) +- [#9900](https://github.com/ember-cli/ember-cli/pull/9900) Remove unused CLI error class [@bertdeblock](https://github.com/bertdeblock) +- [#9902](https://github.com/ember-cli/ember-cli/pull/9902) Remove support for `ember-cli-inject-live-reload` < v1.10.0 [@bertdeblock](https://github.com/bertdeblock) +- [#9903](https://github.com/ember-cli/ember-cli/pull/9903) Deprecate `vendor-shim` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9904](https://github.com/ember-cli/ember-cli/pull/9904) Update Node compatibility warning [@bertdeblock](https://github.com/bertdeblock) +- [#9906](https://github.com/ember-cli/ember-cli/pull/9906) Drop support for Node v12 [@bertdeblock](https://github.com/bertdeblock) +- [#9907](https://github.com/ember-cli/ember-cli/pull/9907) Remove ESLint config file from `server` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9908](https://github.com/ember-cli/ember-cli/pull/9908) Remove support for finding an addon by its unscoped name [@bertdeblock](https://github.com/bertdeblock) +- [#9909](https://github.com/ember-cli/ember-cli/pull/9909) Deprecate support for `ember-cli-jshint` [@bertdeblock](https://github.com/bertdeblock) +- [#9917](https://github.com/ember-cli/ember-cli/pull/9917) update beta deps [@kellyselden](https://github.com/kellyselden) +- [#9919](https://github.com/ember-cli/ember-cli/pull/9919) Clean up `ember-addon` object in package file when destroying an in-repo addon [@bertdeblock](https://github.com/bertdeblock) +- [#9935](https://github.com/ember-cli/ember-cli/pull/9935) Update dev changelog script [@kellyselden](https://github.com/kellyselden) +- [#9938](https://github.com/ember-cli/ember-cli/pull/9938) [INTERNAL] Fix internal `sequence` util [@bertdeblock](https://github.com/bertdeblock) +- [#9939](https://github.com/ember-cli/ember-cli/pull/9939) Update `fs-extra` to v10 [@bertdeblock](https://github.com/bertdeblock) +- [#9937](https://github.com/ember-cli/ember-cli/pull/9937) [INTERNAL] Remove old `deprecate` utility [@bertdeblock](https://github.com/bertdeblock) +- [#9941](https://github.com/ember-cli/ember-cli/pull/9941) Update `filesize` to v9 [@bertdeblock](https://github.com/bertdeblock) +- [#9942](https://github.com/ember-cli/ember-cli/pull/9942) Update `isbinaryfile` to v5 [@bertdeblock](https://github.com/bertdeblock) +- [#9944](https://github.com/ember-cli/ember-cli/pull/9944) Add support for Node v18 [@ddzz](https://github.com/ddzz) +- [#9947](https://github.com/ember-cli/ember-cli/pull/9947) [DOC] Update EOL date for Node v16 in `Node Support` doc [@bertdeblock](https://github.com/bertdeblock) +- [#9953](https://github.com/ember-cli/ember-cli/pull/9953) Update `resolve-package-path` to v4 [@bertdeblock](https://github.com/bertdeblock) +- [#9954](https://github.com/ember-cli/ember-cli/pull/9954) Update `jsdom` to v20 [@bertdeblock](https://github.com/bertdeblock) +- [#9969](https://github.com/ember-cli/ember-cli/pull/9969) update ember source beta [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + +## v4.5.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.4.0...v4.5.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.4.0...v4.5.0) + +#### Changelog + +- [#9760](https://github.com/ember-cli/ember-cli/pull/9760) Add `timeout-minutes` to GitHub CI jobs in `app` and `addon` blueprints [@mansona](https://github.com/mansona) +- [#9777](https://github.com/ember-cli/ember-cli/pull/9777) [DOC] Add bower note [@jenweber](https://github.com/jenweber) +- [#9778](https://github.com/ember-cli/ember-cli/pull/9778) Prune lodash dependencies [@locks](https://github.com/locks) +- [#9785](https://github.com/ember-cli/ember-cli/pull/9785) Generate correct directory name in `README.md` and `CONTRIBUTING.md` files [@bertdeblock](https://github.com/bertdeblock) +- [#9805](https://github.com/ember-cli/ember-cli/pull/9805) Add Node v16 to `Node Support` docs [@bertdeblock](https://github.com/bertdeblock) +- [#9823](https://github.com/ember-cli/ember-cli/pull/9823) [RFC 801] Deprecate `blacklist` and `whitelist` build options [@bertdeblock](https://github.com/bertdeblock) +- [#9825](https://github.com/ember-cli/ember-cli/pull/9825) Remove `ember-export-application-global` addon from `app` blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9848](https://github.com/ember-cli/ember-cli/pull/9848) Update `ember-cli-dependency-checker` to v3.3.1 [@gnclmorais](https://github.com/gnclmorais) +- [#9857](https://github.com/ember-cli/ember-cli/pull/9857) Use `createBuilder` instead of deprecated `buildOutput` in test suite [@geneukum](https://github.com/geneukum) +- [#9858](https://github.com/ember-cli/ember-cli/pull/9858) Remove `EXTEND_PROTOTYPES` object in the app's `config/environment.js` file [@bertdeblock](https://github.com/bertdeblock) +- [#9859](https://github.com/ember-cli/ember-cli/pull/9859) Update `git.io` URLs [@bertdeblock](https://github.com/bertdeblock) +- [#9860](https://github.com/ember-cli/ember-cli/pull/9860) Add inline comment RE: `runningTests` variable [@bertdeblock](https://github.com/bertdeblock) +- [#9886](https://github.com/ember-cli/ember-cli/pull/9886) Remove deletion of `@ember/jquery` in addon blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9906](https://github.com/ember-cli/ember-cli/pull/9906) Drop support for Node v12 [@bertdeblock](https://github.com/bertdeblock) +- [#9909](https://github.com/ember-cli/ember-cli/pull/9909) Deprecate support for `ember-cli-jshint` [@bertdeblock](https://github.com/bertdeblock) +- [#9914](https://github.com/ember-cli/ember-cli/pull/9914) Temporarily skip failing ember new test for npm versions <= v6.0.0 [@bertdeblock](https://github.com/bertdeblock) +- [#9770](https://github.com/ember-cli/ember-cli/pull/9770) Include the addon’s name in the warning when a core command is overridden [@davecombs](https://github.com/davecombs) +- [#9890](https://github.com/ember-cli/ember-cli/pull/9890) Make sure addons are discovered only once [@wagenet](https://github.com/wagenet) +- [#9898](https://github.com/ember-cli/ember-cli/pull/9898) Remove deprecated `addonJsFiles` method on `addon` model [@bertdeblock](https://github.com/bertdeblock) +- [#9900](https://github.com/ember-cli/ember-cli/pull/9900) Remove unused CLI error class [@bertdeblock](https://github.com/bertdeblock) +- [#9917](https://github.com/ember-cli/ember-cli/pull/9917) update beta deps [@kellyselden](https://github.com/kellyselden) +- [#9919](https://github.com/ember-cli/ember-cli/pull/9919) Clean up `ember-addon` object in package file when destroying an in-repo addon [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v4.4.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.3.0...v4.4.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.3.0...v4.4.0) + +#### Changelog + +- [#9611](https://github.com/ember-cli/ember-cli/pull/9611) use more standard markdown for addon readme [@mansona](https://github.com/mansona) +- [#9818](https://github.com/ember-cli/ember-cli/pull/9818) Update actions/checkout action to v3 [@SergeAstapov](https://github.com/SergeAstapov) +- [#9819](https://github.com/ember-cli/ember-cli/pull/9819) Update actions/setup-node action to v3 [@SergeAstapov](https://github.com/SergeAstapov) +- [#9822](https://github.com/ember-cli/ember-cli/pull/9822) Update `since.available` and `since.enabled` versions for Bower deprecations [@bertdeblock](https://github.com/bertdeblock) +- [#9850](https://github.com/ember-cli/ember-cli/pull/9850) Fix contents of addon `.gitignore` file [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v4.3.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.2.0...v4.3.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.2.0...v4.3.0) + +#### Changelog + +- [#9707](https://github.com/ember-cli/ember-cli/pull/9707) [RFC 772] Deprecate Bower support [@bertdeblock](https://github.com/bertdeblock) +- [#9387](https://github.com/ember-cli/ember-cli/pull/9387) Add support for specifying a path for the generate command [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#9638](https://github.com/ember-cli/ember-cli/pull/9638) Update ember-cli's own dependencies to latest [@rwjblue](https://github.com/rwjblue) +- [#9680](https://github.com/ember-cli/ember-cli/pull/9680) Ignore default folder name used by broccoli-debug [@notmessenger](https://github.com/notmessenger) +- [#9683](https://github.com/ember-cli/ember-cli/pull/9683) chore: replace json-stable-stringify with safe-stable-stringify [@BridgeAR](https://github.com/BridgeAR) +- [#9769](https://github.com/ember-cli/ember-cli/pull/9769) Update markdown-it to v12.3.2 to address vulnerabiliity [@locks](https://github.com/locks) +- [#9781](https://github.com/ember-cli/ember-cli/pull/9781) [ENHANCEMENT] Remove `X-UA-Compatible` meta tag for IE browser [@bobisjan](https://github.com/bobisjan) +- [#9790](https://github.com/ember-cli/ember-cli/pull/9790) Upgrade broccoli-merge-trees [@locks](https://github.com/locks) +- [#9803](https://github.com/ember-cli/ember-cli/pull/9803) [RFC 637] Customizable test setups [@bertdeblock](https://github.com/bertdeblock) +- [#9804](https://github.com/ember-cli/ember-cli/pull/9804) Fix formatting of CI file in app and addon blueprints [@bertdeblock](https://github.com/bertdeblock) +- [#9817](https://github.com/ember-cli/ember-cli/pull/9817) update beta deps [@kellyselden](https://github.com/kellyselden) +- [#9830](https://github.com/ember-cli/ember-cli/pull/9830) update deps before release [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + +## v4.2.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.1.1...v4.2.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.1.1...v4.2.0) + +#### Changelog + +- [#9681](https://github.com/ember-cli/ember-cli/pull/9681) Update URL to Ember CLI website everywhere [@bertdeblock](https://github.com/bertdeblock) +- [#9726](https://github.com/ember-cli/ember-cli/pull/9726) Cancel stale workflows when starting a new one. [@rwjblue](https://github.com/rwjblue) +- [#9731](https://github.com/ember-cli/ember-cli/pull/9731) Add an `assert` and a `deprecate` utility [@bertdeblock](https://github.com/bertdeblock) +- [#9753](https://github.com/ember-cli/ember-cli/pull/9753) Upgrade to `ember-template-lint` v4 in blueprint [@bmish](https://github.com/bmish) + +Thank you to all who took the time to contribute! + +## v4.1.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.1.0...v4.1.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.1.0...v4.1.1) + +#### Changelog + +- [#9737](https://github.com/ember-cli/ember-cli/pull/9737) Remove jQuery integration scenario from ember-try [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#9739](https://github.com/ember-cli/ember-cli/pull/9739) Change blueprint to generate apps using 'history' location [@kategengler](https://github.com/kategengler) +- [#9762](https://github.com/ember-cli/ember-cli/pull/9762) Update @embroider/* packages to 1.0.0. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v4.1.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.0.1...v4.1.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.0.1...v4.1.0) + +#### Changelog + +- [#9700](https://github.com/ember-cli/ember-cli/pull/9700) [Chore] Update .npmignore to ignore .github and docs folders [@SergeAstapov](https://github.com/SergeAstapov) +- [#9729](https://github.com/ember-cli/ember-cli/pull/9729) is-language-code@^3.1.0 [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + +## v4.0.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v4.0.0...v4.0.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v4.0.0...v4.0.1) + +#### Changelog + +- [#9675](https://github.com/ember-cli/ember-cli/pull/9675) Fix using `pnpm` install inadvertently [@balinterdi](https://github.com/balinterdi) + +Thank you to all who took the time to contribute! + +## v4.0.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.28.4...v4.0.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.28.4...v4.0.0) + +#### Changelog + +- [#9679](https://github.com/ember-cli/ember-cli/pull/9679) Bump ember-page-title from v6.2.2 to v7.0.0 [@raido](https://github.com/raido) +- [#9694](https://github.com/ember-cli/ember-cli/pull/9694) test in node 16 LTS [@kellyselden](https://github.com/kellyselden) +- [#9696](https://github.com/ember-cli/ember-cli/pull/9696) commands/init: Fix `--yarn` usage [@Turbo87](https://github.com/Turbo87) +- [#9659](https://github.com/ember-cli/ember-cli/pull/9659) Ensure `ember-classic` ember-try scenario uses Ember 3.x [@rwjblue](https://github.com/rwjblue) +- [#9661](https://github.com/ember-cli/ember-cli/pull/9661) Set default CI config blueprints to run all builds [@elwayman02](https://github.com/elwayman02) +- [#9666](https://github.com/ember-cli/ember-cli/pull/9666) Remove IE11 comments from `config/target.js` in app blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9667](https://github.com/ember-cli/ember-cli/pull/9667) Update eslint-plugin-qunit to v7 in blueprint [@bmish](https://github.com/bmish) +- [#9670](https://github.com/ember-cli/ember-cli/pull/9670) Don't emit an error when the `lint:fix` script fails post blueprint generation [@bertdeblock](https://github.com/bertdeblock) +- [#9574](https://github.com/ember-cli/ember-cli/pull/9574) Update link to Discord in README.md [@MelSumner](https://github.com/MelSumner) +- [#9613](https://github.com/ember-cli/ember-cli/pull/9613) Fix test that started failing with v2.17.0 of qunit [@kategengler](https://github.com/kategengler) +- [#9579](https://github.com/ember-cli/ember-cli/pull/9579) Add `--ci-provider` option to `ember new` and `ember addon` [@snewcomer](https://github.com/snewcomer) +- [#9618](https://github.com/ember-cli/ember-cli/pull/9618) Reload `_packageInfo` as part of `reloadPkg` [@brendenpalmer](https://github.com/brendenpalmer) +- [#9563](https://github.com/ember-cli/ember-cli/pull/9563) Add `pnpm` support to `ember install` command [@Turbo87](https://github.com/Turbo87) +- [#9580](https://github.com/ember-cli/ember-cli/pull/9580) Add `.lint-todo` to prettier ignore [@elwayman02](https://github.com/elwayman02) +- [#9589](https://github.com/ember-cli/ember-cli/pull/9589) Add link to visualizer to perf guide [@mehulkar](https://github.com/mehulkar) +- [#9595](https://github.com/ember-cli/ember-cli/pull/9595) Add support for `addons.exclude` and `addons.include` options [@bertdeblock](https://github.com/bertdeblock) +- [#9623](https://github.com/ember-cli/ember-cli/pull/9623) Update app/addon blueprints to use ember-auto-import@2 [@rwjblue](https://github.com/rwjblue) +- [#9619](https://github.com/ember-cli/ember-cli/pull/9619) Update watch-detector to 1.0.1 [@colenso](https://github.com/colenso) +- [#9605](https://github.com/ember-cli/ember-cli/pull/9605) Properly set `loglevel` flag for npm [@jrvidal](https://github.com/jrvidal) +- [#9609](https://github.com/ember-cli/ember-cli/pull/9609) Ignore additional `ember-try` files for apps and addons [@bertdeblock](https://github.com/bertdeblock) +- [#9644](https://github.com/ember-cli/ember-cli/pull/9644) Default `ember new` and `ember addon` to use GitHub Actions [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.28.4 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.28.3...v3.28.4) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.28.3...v3.28.4) + +#### Changelog + +- [#9694](https://github.com/ember-cli/ember-cli/pull/9694) test in node 16 LTS [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + +## v3.28.3 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.28.2...v3.28.3) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.28.2...v3.28.3) + +#### Changelog + +- [#9670](https://github.com/ember-cli/ember-cli/pull/9670) Don't emit an error when the `lint:fix` script fails post blueprint generation [@bertdeblock](https://github.com/bertdeblock) + +Thank you to all who took the time to contribute! + +## v3.28.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.28.1...v3.28.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.28.1...v3.28.2) + +#### Changelog + +- [#9659](https://github.com/ember-cli/ember-cli/pull/9659) Ensure `ember-classic` ember-try scenario uses Ember 3.x [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.28.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.28.0...v3.28.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.28.0...v3.28.1) + +#### Changelog + +- [#9618](https://github.com/ember-cli/ember-cli/pull/9618) Ensure discovered addons are refreshed after `ember install` (fix usage of default blueprints) [@brendenpalmer](https://github.com/brendenpalmer) + +## v4.0.0-beta.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.28.0...v4.0.0-beta.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.28.0...v4.0.0-beta.1) + +#### Changelog + +- [#9574](https://github.com/ember-cli/ember-cli/pull/9574) Update link to Discord in README.md [@MelSumner](https://github.com/MelSumner) +- [#9613](https://github.com/ember-cli/ember-cli/pull/9613) Fix test that started failing with v2.17.0 of qunit [@kategengler](https://github.com/kategengler) +- [#9579](https://github.com/ember-cli/ember-cli/pull/9579) Add `--ci-provider` option to `ember new` and `ember addon` [@snewcomer](https://github.com/snewcomer) +- [#9618](https://github.com/ember-cli/ember-cli/pull/9618) Reload `_packageInfo` as part of `reloadPkg` [@brendenpalmer](https://github.com/brendenpalmer) +- [#9563](https://github.com/ember-cli/ember-cli/pull/9563) Add `pnpm` support to `ember install` command [@Turbo87](https://github.com/Turbo87) +- [#9580](https://github.com/ember-cli/ember-cli/pull/9580) Add `.lint-todo` to prettier ignore [@elwayman02](https://github.com/elwayman02) +- [#9589](https://github.com/ember-cli/ember-cli/pull/9589) Add link to visualizer to perf guide [@mehulkar](https://github.com/mehulkar) +- [#9595](https://github.com/ember-cli/ember-cli/pull/9595) Add support for `addons.exclude` and `addons.include` options [@bertdeblock](https://github.com/bertdeblock) +- [#9623](https://github.com/ember-cli/ember-cli/pull/9623) Update app/addon blueprints to use ember-auto-import@2 [@rwjblue](https://github.com/rwjblue) +- [#9619](https://github.com/ember-cli/ember-cli/pull/9619) Update watch-detector to 1.0.1 [@colenso](https://github.com/colenso) +- [#9627](https://github.com/ember-cli/ember-cli/pull/9627) Update app & addon blueprint dependencies to latest [@rwjblue](https://github.com/rwjblue) +- [#9605](https://github.com/ember-cli/ember-cli/pull/9605) Properly set `loglevel` flag for npm [@jrvidal](https://github.com/jrvidal) +- [#9609](https://github.com/ember-cli/ember-cli/pull/9609) Ignore additional `ember-try` files for apps and addons [@bertdeblock](https://github.com/bertdeblock) +- [#9644](https://github.com/ember-cli/ember-cli/pull/9644) Default `ember new` and `ember addon` to use GitHub Actions [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.28.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.27.0...v3.28.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.27.0...v3.28.0) + +#### Changelog + +- [#9505](https://github.com/ember-cli/ember-cli/pull/9505) Pass `realPath` as `root` rather than the dirname for `addonMainPath` [@brendenpalmer](https://github.com/brendenpalmer) +- [#9507](https://github.com/ember-cli/ember-cli/pull/9507) Add a new config, `ember-addon.projectRoot`, to specify the location of the project [@brendenpalmer](https://github.com/brendenpalmer) +- [#9530](https://github.com/ember-cli/ember-cli/pull/9530) Drop Node 10 support [@rwjblue](https://github.com/rwjblue) +- [#9487](https://github.com/ember-cli/ember-cli/pull/9487) Add support for creating a single addon instance per bundle root (which enables dramatically reducing the total number of addon instances) [@davecombs](https://github.com/davecombs) +- [#9524](https://github.com/ember-cli/ember-cli/pull/9524) Update CONTRIBUTING.md to reference cli.emberjs.com [@loganrosen](https://github.com/loganrosen) +- [#9533](https://github.com/ember-cli/ember-cli/pull/9533) Ensure package-info objects are stable when they represent the same addon [@brendenpalmer](https://github.com/brendenpalmer) +- [#9538](https://github.com/ember-cli/ember-cli/pull/9538) ensure backwards compatibility is maintained with `packageRoot` and `root` [@brendenpalmer](https://github.com/brendenpalmer) +- [#9539](https://github.com/ember-cli/ember-cli/pull/9539) avoid setting `root` as `realPath` from the package-info object [@brendenpalmer](https://github.com/brendenpalmer) +- [#9537](https://github.com/ember-cli/ember-cli/pull/9537) Implement LCA host/host addons logic in `ember-cli` [@brendenpalmer](https://github.com/brendenpalmer) +- [#9540](https://github.com/ember-cli/ember-cli/pull/9540) Use relative override paths in blueprint ESLint config [@loganrosen](https://github.com/loganrosen) +- [#9542](https://github.com/ember-cli/ember-cli/pull/9542) Add validation checks for addon instance bundle caching [@brendenpalmer](https://github.com/brendenpalmer) +- [#9543](https://github.com/ember-cli/ember-cli/pull/9543) Add ability to specify a custom `ember-addon.perBundleAddonCacheUtil` utility [@brendenpalmer](https://github.com/brendenpalmer) +- [#9562](https://github.com/ember-cli/ember-cli/pull/9562) Update `addon-proxy` to support Embroider [@brendenpalmer](https://github.com/brendenpalmer) +- [#9565](https://github.com/ember-cli/ember-cli/pull/9565) Drop Node 10 support in blueprint engine spec [@elwayman02](https://github.com/elwayman02) +- [#9568](https://github.com/ember-cli/ember-cli/pull/9568) [BUGFIX release] Skip babel for qunit with embroider [@ctjhoa](https://github.com/ctjhoa) + +Thank you to all who took the time to contribute! + + +## v3.27.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.26.0...v3.27.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.26.0-beta.2...v3.27.0) + +#### Changelog + +- [#9504](https://github.com/ember-cli/ember-cli/pull/9504) Update minimum version of broccoli-concat to address a major issue with cache invalidation [@brendenpalmer](https://github.com/brendenpalmer) +- [#9535](https://github.com/ember-cli/ember-cli/pull/9535) Disable Embroider by default. [@rwjblue](https://github.com/rwjblue) +- [#9557](https://github.com/ember-cli/ember-cli/pull/9557) Update app and addon blueprint dependencies to latest. [@rwjblue](https://github.com/rwjblue) +- [#9558](https://github.com/ember-cli/ember-cli/pull/9558) Switch from `octane` template lint config to `recommended` [@bmish](https://github.com/bmish) +- [#9453](https://github.com/ember-cli/ember-cli/pull/9453) Prevent "yarn-error.log" files being published for addons [@bertdeblock](https://github.com/bertdeblock) +- [#9392](https://github.com/ember-cli/ember-cli/pull/9392) / [#9484](https://github.com/ember-cli/ember-cli/pull/9484) Add eslint-plugin-qunit to blueprint [@bmish](https://github.com/bmish) +- [#9454](https://github.com/ember-cli/ember-cli/pull/9454) / [#9492](https://github.com/ember-cli/ember-cli/pull/9492) Add --embroider as an option for new and init [@thoov](https://github.com/thoov) +- [#9456](https://github.com/ember-cli/ember-cli/pull/9456) Add `.*/` to eslint ignore [@chancancode](https://github.com/chancancode) +- [#9469](https://github.com/ember-cli/ember-cli/pull/9469) Run `lint:fix` script automatically after blueprint generation [@rpemberton](https://github.com/rpemberton) +- [#9480](https://github.com/ember-cli/ember-cli/pull/9480) Refactor getPort to only check required port [@Cartmanishere](https://github.com/Cartmanishere) +- [#9485](https://github.com/ember-cli/ember-cli/pull/9485) Add Ember 3.24 LTS to ember-try configuration [@bertdeblock](https://github.com/bertdeblock) +- [#9488](https://github.com/ember-cli/ember-cli/pull/9488) Update supported Ember version in addon blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9490](https://github.com/ember-cli/ember-cli/pull/9490) Prevent window.Ember deprecation on Ember 3.27+. [@rwjblue](https://github.com/rwjblue) +- [#9491](https://github.com/ember-cli/ember-cli/pull/9491) Update supported Ember CLI version in addon blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9495](https://github.com/ember-cli/ember-cli/pull/9495) Enable Embroider by default for new projects [@thoov](https://github.com/thoov) +- [#9500](https://github.com/ember-cli/ember-cli/pull/9500) Fix `lint:fix` script for Windows users [@lupestro](https://github.com/lupestro) + +Thank you to all who took the time to contribute! + + +## v3.26.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.26.0...v3.26.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.26.0...v3.26.1) + +#### Changelog + +- [#9504](https://github.com/ember-cli/ember-cli/pull/9504) Update `broccoli-concat` to avoid a cache invalidation problem in files larger than 10000 characters. [@brendenpalmer](https://github.com/brendenpalmer) + +Thank you to all who took the time to contribute! + +## v3.26.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.25.3...v3.26.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.25.3...v3.26.0) + +#### Changelog + +- [#9473](https://github.com/ember-cli/ember-cli/pull/9473) Issue a better error message for add-on's missing an entry point (e.g. invalid `ember-addon.main` path) [@ef4](https://github.com/ef4) +- [#9437](https://github.com/ember-cli/ember-cli/pull/9437) Add Prettier files to ".npmignore" file in addon blueprint [@bertdeblock](https://github.com/bertdeblock) +- [#9436](https://github.com/ember-cli/ember-cli/pull/9436) Enable Embroider test scenario for addons [@thoov](https://github.com/thoov) +- [#9435](https://github.com/ember-cli/ember-cli/pull/9435) Use "lint:fix" script in app and addon README files [@bertdeblock](https://github.com/bertdeblock) +- [#9451](https://github.com/ember-cli/ember-cli/pull/9451) update blueprint deps [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + + +## v3.25.3 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.25.2...v3.25.3) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.25.2...v3.25.3) + +#### Changelog + +- [#9490](https://github.com/ember-cli/ember-cli/pull/9490) Prevent `window.Ember` deprecation when testing (for Ember 3.27+) [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.25.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.25.1...v3.25.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.25.1...v3.25.2) + +#### Changelog + +- [#9473](https://github.com/ember-cli/ember-cli/pull/9473) Issue a better error message for add-on's missing an entry point (e.g. invalid `ember-addon.main` path) [@ef4](https://github.com/ef4) + +Thank you to all who took the time to contribute! + + +## v3.25.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.25.0...v3.25.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.25.0...v3.25.1) + +#### Changelog + +- [#9467](https://github.com/ember-cli/ember-cli/pull/9467) Defer `The tests file was not loaded.` warning until after `DOMContentLoaded` [@ef4](https://github.com/ef4) + + +Thank you to all who took the time to contribute! + +## v3.25.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.24.0...v3.25.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.24.0...v3.25.0) + +#### Changelog + +- [#9450](https://github.com/ember-cli/ember-cli/pull/9450) update blueprint deps [@kellyselden](https://github.com/kellyselden) +- Update `ember-data` and `ember-source` to 3.25.0-beta [@kellyselden](https://github.com/kellyselden) / [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.24.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.23.0...v3.24.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.23.0...v3.24.0) + +#### Changelog + +- [#9410](https://github.com/ember-cli/ember-cli/pull/9410) Add `.eslintcache` to `.gitignore` for applications and addons [@simonihmig](https://github.com/simonihmig) +- [#9425](https://github.com/ember-cli/ember-cli/pull/9425) Update blueprint dependecies to latest. [@rwjblue](https://github.com/rwjblue) +- [#9372](https://github.com/ember-cli/ember-cli/pull/9372) / [#9382](https://github.com/ember-cli/ember-cli/pull/9382) Add `ember-page-title` to app blueprint [@raido](https://github.com/raido) +- [#9391](https://github.com/ember-cli/ember-cli/pull/9391) / [#9407](https://github.com/ember-cli/ember-cli/pull/9407) Add `prettier` to blueprint [@bmish](https://github.com/bmish) +- [#9402](https://github.com/ember-cli/ember-cli/pull/9402) Prevent build cycles when app is within a watched dir [@ef4](https://github.com/ef4) +- [#9403](https://github.com/ember-cli/ember-cli/pull/9403) Update blueprint to eslint-plugin-ember v10 [@bmish](https://github.com/bmish) +- [#9340](https://github.com/ember-cli/ember-cli/pull/9340) / [#9371](https://github.com/ember-cli/ember-cli/pull/9371) Update blueprints with new testing configuration [@scalvert](https://github.com/scalvert) + + +Thank you to all who took the time to contribute! + +## v3.23.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.22.0...v3.23.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.22.0...v3.23.0) + +#### Changelog + +- [#9369](https://github.com/ember-cli/ember-cli/pull/9369) / [#9406](https://github.com/ember-cli/ember-cli/pull/9406) Update blueprint dependencies to latest. [@rwjblue](https://github.com/rwjblue) +- [#9361](https://github.com/ember-cli/ember-cli/pull/9361) / [#9364](https://github.com/ember-cli/ember-cli/pull/9364) / [#9365](https://github.com/ember-cli/ember-cli/pull/9365) / [#9368](https://github.com/ember-cli/ember-cli/pull/9368) Update dependencies to latest. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.22.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.21.0...v3.22.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.21.0...v3.22.0) + +#### Changelog + +- [#9325](https://github.com/ember-cli/ember-cli/pull/9325) Update dependencies for 3.22 beta series. [@rwjblue](https://github.com/rwjblue) +- [#9325](https://github.com/ember-cli/ember-cli/pull/9325) Update to `eslint-plugin-ember@9.0.0`. [@rwjblue](https://github.com/rwjblue) +- [#9336](https://github.com/ember-cli/ember-cli/pull/9336) Fixup internal test harness fixturify-project helper. [@rwjblue](https://github.com/rwjblue) +- [#9338](https://github.com/ember-cli/ember-cli/pull/9338) Remove requirement to have `loader.js`. [@rwjblue](https://github.com/rwjblue) +- [#9343](https://github.com/ember-cli/ember-cli/pull/9343) Fix yuidoc for private APIs [@jenweber](https://github.com/jenweber) +- [#9359](https://github.com/ember-cli/ember-cli/pull/9359) Upgrade to tiny-lr v2.0.0 [@elwayman02](https://github.com/elwayman02) +- [#9360](https://github.com/ember-cli/ember-cli/pull/9360) Update blueprint dependencies to latest version. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.21.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.21.1...v3.21.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.21.1...v3.21.2) + +#### Changelog + +- [#9327](https://github.com/ember-cli/ember-cli/pull/9327) Update addon `README.md` to indicate Ember 3.16 minimum. [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + +## v3.21.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.21.0...v3.21.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.21.0...v3.21.1) + +#### Changelog + +- [#9321](https://github.com/ember-cli/ember-cli/pull/9321) Add missing `ember-lts-3.20` matrix build to CI configuration. [@kellyselden](https://github.com/kellyselden) +- [#9323](https://github.com/ember-cli/ember-cli/pull/9323) Remove errant `ember-lts-3.12` matrix build from CI configuration. [@rwjblue](https://github.com/rwjblue) +- [#9324](https://github.com/ember-cli/ember-cli/pull/9324) Fix transpilation issues with modern browsers by migrating from `ember-cli-uglify` to `ember-cli-terser` [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.21.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.20.1...v3.21.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.20.1...v3.21.0) + +#### Changelog + +- [#9305](https://github.com/ember-cli/ember-cli/pull/9305) Update blueprint dependencies to latest versions. [@rwjblue](https://github.com/rwjblue) +- [#9306](https://github.com/ember-cli/ember-cli/pull/9306) Ensure that `outputReady` receives the final output directory. [@rwjblue](https://github.com/rwjblue) +- [#9308](https://github.com/ember-cli/ember-cli/pull/9308) Add Ember 3.20 LTS to ember-try configuration. [@rwjblue](https://github.com/rwjblue) +- [#9309](https://github.com/ember-cli/ember-cli/pull/9309) Update blueprint dependencies to latest [@rwjblue](https://github.com/rwjblue) +- [#9310](https://github.com/ember-cli/ember-cli/pull/9310) Drop Ember 3.12 from default addon testing matrix. [@rwjblue](https://github.com/rwjblue) +- [#9259](https://github.com/ember-cli/ember-cli/pull/9259) Implement [emberjs/rfcs#635](https://github.com/emberjs/rfcs/blob/master/text/0635-ember-new-lang.md): `ember new --lang` [@josephdsumner](https://github.com/josephdsumner) +- [#9299](https://github.com/ember-cli/ember-cli/pull/9299) Remove explicit `yarn install` in blueprint generated `.travis.yml` (use the Travis CI default of `yarn install --frozen-lockfile`) [@kellyselden](https://github.com/kellyselden) +- [#9289](https://github.com/ember-cli/ember-cli/pull/9289) Update blueprint dependencies / devDependencies [@rwjblue](https://github.com/rwjblue) + +## v3.20.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.20.1...v3.20.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.20.1...v3.20.2) + +#### Changelog + +- [#9321](https://github.com/ember-cli/ember-cli/pull/9321) Add missing `ember-lts-3.20` invocation to CI [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + +## v3.20.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.20.0...v3.20.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.20.0...v3.20.1) + +#### Changelog + +- [#9308](https://github.com/ember-cli/ember-cli/pull/9308) Add Ember 3.20 LTS to ember-try configuration. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.20.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.19.0...v3.20.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.19.0...v3.20.0) + +#### Changelog + +- [#9211](https://github.com/ember-cli/ember-cli/pull/9211) bugfix: processAppMiddlewares - check server middleware files [@lifeart](https://github.com/lifeart) +- [#9215](https://github.com/ember-cli/ember-cli/pull/9215) Handle unexpected errors in development mode proxy [@houfeng0923](https://github.com/houfeng0923) +- [#9238](https://github.com/ember-cli/ember-cli/pull/9238) Add config/ember-cli-update.json to app and addon blueprints. [@rwjblue](https://github.com/rwjblue) +- [#9262](https://github.com/ember-cli/ember-cli/pull/9262) Refactor release process. [@rwjblue](https://github.com/rwjblue) +- [#9264](https://github.com/ember-cli/ember-cli/pull/9264) refactor: use Boolean constructor to cast variable in config/targets.js blueprint [@bmish](https://github.com/bmish) +- [032e9a8851af869c7e0cf5ef8c3d930ade38b6c1](https://github.com/ember-cli/ember-cli/commit/032e9a8851af869c7e0cf5ef8c3d930ade38b6c1) Merge branch 'master' into default-blueprint-absolute-imports [@dfreeman](https://github.com/dfreeman) +- [#9273](https://github.com/ember-cli/ember-cli/pull/9273) Avoid relative imports in the default blueprint [@dfreeman](https://github.com/dfreeman) +- [#9277](https://github.com/ember-cli/ember-cli/pull/9277) Allow `ember install` to work with Yarn v2 [@caassandra](https://github.com/caassandra) +- [#9280](https://github.com/ember-cli/ember-cli/pull/9280) Remove `ember-default` ember-try scenario [@mehulkar](https://github.com/mehulkar) +- [#9281](https://github.com/ember-cli/ember-cli/pull/9281) Update blueprint dependencies to latest versions. [@rwjblue](https://github.com/rwjblue) +- [#9282](https://github.com/ember-cli/ember-cli/pull/9282) Deprecate `PACKAGER` experiment. [@rwjblue](https://github.com/rwjblue) +- [#9283](https://github.com/ember-cli/ember-cli/pull/9283) Remove macOS from CI matrix for slow/acceptance tests. [@rwjblue](https://github.com/rwjblue) +- [#9284](https://github.com/ember-cli/ember-cli/pull/9284) Drop support for Node 13. [@rwjblue](https://github.com/rwjblue) +- [56461f26a9b81833f424bf1a23c7ce502d35a43b](https://github.com/ember-cli/ember-cli/commit/56461f26a9b81833f424bf1a23c7ce502d35a43b) Merge branch 'master' into master [@caassandra](https://github.com/caassandra) +- [#9286](https://github.com/ember-cli/ember-cli/pull/9286) Remove unused `lib/utilities/symbol.js` [@IzzatN](https://github.com/IzzatN) +- [#9287](https://github.com/ember-cli/ember-cli/pull/9287) Remove redundant guard in `Addon.prototype.moduleName` [@IzzatN](https://github.com/IzzatN) + +Thank you to all who took the time to contribute! + +## v3.19.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.18.0...v3.19.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.19.0...v3.19.0) + +#### Changelog + +- [#9239](https://github.com/ember-cli/ember-cli/pull/9239) Update app / addon dependencies to latest. [@rwjblue](https://github.com/rwjblue) +- [#9205](https://github.com/ember-cli/ember-cli/pull/9205) Pass options to middleware [@mrloop](https://github.com/mrloop) +- [#9209](https://github.com/ember-cli/ember-cli/pull/9209) Create small development script to update blueprint dependencies. [@rwjblue](https://github.com/rwjblue) +- [#9212](https://github.com/ember-cli/ember-cli/pull/9212) Ensure that the captured exit is released. [@rwjblue](https://github.com/rwjblue) +- [#9218](https://github.com/ember-cli/ember-cli/pull/9218) Update eslint to 7.0.0. [@rwjblue](https://github.com/rwjblue) +- [#9219](https://github.com/ember-cli/ember-cli/pull/9219) Add ability to pass `--filter` to `dev/update-blueprint-dependencies.js` [@rwjblue](https://github.com/rwjblue) +- [#9240](https://github.com/ember-cli/ember-cli/pull/9240) Ensure `ember serve` property waits for the serve task. [@rwjblue](https://github.com/rwjblue) +- [#9242](https://github.com/ember-cli/ember-cli/pull/9242) Move travis configuration from trusty to xenial [@Gaurav0](https://github.com/Gaurav0) +- [#7538](https://github.com/ember-cli/ember-cli/pull/7538) Fix `configPath` caching [@kanongil](https://github.com/kanongil) +- [#8258](https://github.com/ember-cli/ember-cli/pull/8258) Tweak `isDevelopingAddon` error message [@stefanpenner](https://github.com/stefanpenner) +- [#8813](https://github.com/ember-cli/ember-cli/pull/8813) Update NPM version check to avoid double `npm install` when using `npm@5.7.1` or higher. [@deepan83](https://github.com/deepan83) +- [#9126](https://github.com/ember-cli/ember-cli/pull/9126) chore: fix init help text with the right description [@rajasegar](https://github.com/rajasegar) +- [#9132](https://github.com/ember-cli/ember-cli/pull/9132) Convert commands to use async/await syntax [@locks](https://github.com/locks) +- [#9134](https://github.com/ember-cli/ember-cli/pull/9134) [DOC] Update locals hook example [@locks](https://github.com/locks) +- [#9146](https://github.com/ember-cli/ember-cli/pull/9146) Convert express-server task to async await [@locks](https://github.com/locks) +- [#9147](https://github.com/ember-cli/ember-cli/pull/9147) Convert serve task to async await [@locks](https://github.com/locks) +- [#9148](https://github.com/ember-cli/ember-cli/pull/9148) Convert npm-task task to async/await syntax [@locks](https://github.com/locks) +- [#9149](https://github.com/ember-cli/ember-cli/pull/9149) Update blueprint dependencies to latest [@bmish](https://github.com/bmish) +- [#9157](https://github.com/ember-cli/ember-cli/pull/9157) Convert insert-into-file to async/await syntax [@locks](https://github.com/locks) +- [#9158](https://github.com/ember-cli/ember-cli/pull/9158) Convert clean-remove to async/await syntax [@locks](https://github.com/locks) +- [#9163](https://github.com/ember-cli/ember-cli/pull/9163) Convert in-option-generate-test to async/await syntax [@locks](https://github.com/locks) +- [#9183](https://github.com/ember-cli/ember-cli/pull/9183) Ensure processed addon styles are not doubly-included in vendor.css [@bantic](https://github.com/bantic) + +## v3.18.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.17.0...v3.18.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.17.0...v3.18.0) + +#### Changelog + +- [#9063](https://github.com/ember-cli/ember-cli/pull/9063) Fix typo in `Blueprint` documentation. [@bartocc](https://github.com/bartocc) +- [#9068](https://github.com/ember-cli/ember-cli/pull/9068) Adds link to CLI commands doc from README [@entendu](https://github.com/entendu) +- [#9070](https://github.com/ember-cli/ember-cli/pull/9070) Fix a number of causes of unhandled rejections (and ensure tests fail when unhandled rejection occurs). [@stefanpenner](https://github.com/stefanpenner) +- [#9072](https://github.com/ember-cli/ember-cli/pull/9072) Ensure errors during build are properly reported to the console. [@stefanpenner](https://github.com/stefanpenner) +- [#9092](https://github.com/ember-cli/ember-cli/pull/9092) Update `ember-source` and `ember-data` to 3.18 betas. [@rwjblue](https://github.com/rwjblue) +- [#9097](https://github.com/ember-cli/ember-cli/pull/9097) Update production dependencies to latest. [@rwjblue](https://github.com/rwjblue) +- [#9108](https://github.com/ember-cli/ember-cli/pull/9108) Cleanup of async in `CLI` / `Builder` while digging into issues around progress clean up. [@rwjblue](https://github.com/rwjblue) +- [#9188](https://github.com/ember-cli/ember-cli/pull/9188) Add Node 14 to CI [@rwjblue](https://github.com/rwjblue) +- [#9208](https://github.com/ember-cli/ember-cli/pull/9208) Update blueprint dependencies to latest versions. [@rwjblue](https://github.com/rwjblue) +- [#9090](https://github.com/ember-cli/ember-cli/pull/9183) Ensure processed addon styles are not doubly-included in vendor.css [@bantic](https://github.com/bantic) + +Thank you to all who took the time to contribute! + +## v3.17.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.16.1...v3.17.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.16.1...v3.17.0) + +#### Changelog + +- [#9045](https://github.com/ember-cli/ember-cli/pull/9045) Add final newline in CONTRIBUTING.md [@kellyselden](https://github.com/kellyselden) +- [c30ed27181257ab4319b3a06134e13067ac1e76e](https://github.com/ember-cli/ember-cli/commit/c30ed27181257ab4319b3a06134e13067ac1e76e) Handle a number of unhandled rejections scenarios [@stefanpenner](https://github.com/stefanpenner) +- [c377300bb21485faf0137ce69b54a10b3a458828](https://github.com/ember-cli/ember-cli/commit/c377300bb21485faf0137ce69b54a10b3a458828) Publish yuidoc json as a part of npm package [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [0a8d7a18b5f27147f2cec5574625e53784841601](https://github.com/ember-cli/ember-cli/commit/0a8d7a18b5f27147f2cec5574625e53784841601) Consistently 'use strict'; for our node js files [@kellyselden](https://github.com/kellyselden) +- [64e635c48c76f177769ca73eb9a228149ffbd863](https://github.com/ember-cli/ember-cli/commit/64e635c48c76f177769ca73eb9a228149ffbd863) Ensure buildFailures are reported correctly [@stefanpenner](https://github.com/stefanpenner) +- [#9037](https://github.com/ember-cli/ember-cli/pull/9037) Update Ember and Ember Data to 3.17 betas. [@rwjblue](https://github.com/rwjblue) +- [#9039](https://github.com/ember-cli/ember-cli/pull/9039) Remove long enabled EMBER_CLI_SYSTEM_TEMP experiment. [@rwjblue](https://github.com/rwjblue) +- [#9038](https://github.com/ember-cli/ember-cli/pull/9038) Remove EMBER_CLI_DELAYED_TRANSPILATION experiment. [@rwjblue](https://github.com/rwjblue) +- [#9040](https://github.com/ember-cli/ember-cli/pull/9040) Remove MODULE_UNIFICATION experiment. [@rwjblue](https://github.com/rwjblue) +- [#9009](https://github.com/ember-cli/ember-cli/pull/9009) Use `eslint` and `ember-template-lint` directly (no longer lint during builds/rebuilds by default) [@dcyriller](https://github.com/dcyriller) +- [#9041](https://github.com/ember-cli/ember-cli/pull/9041) Remove usage of RSVP. [@rwjblue](https://github.com/rwjblue) +- [#9042](https://github.com/ember-cli/ember-cli/pull/9042) Include API documentation `yuidoc` JSON output when publishing [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [#9045](https://github.com/ember-cli/ember-cli/pull/9045) Add final newline in CONTRIBUTING.md [@kellyselden](https://github.com/kellyselden) + +## v3.16.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.16.1...v3.16.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.16.1...v3.16.2) + +#### Changelog + +- [#9090](https://github.com/ember-cli/ember-cli/pull/9183) Ensure processed addon styles are not doubly-included in vendor.css [@bantic](https://github.com/bantic) + +## v3.16.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.16.0...v3.16.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.16.0...v3.16.1) + +#### Changelog + +- [#9090](https://github.com/ember-cli/ember-cli/pull/9090) Backports of critical bugfixes to LTS (3.16) [@rwjblue](https://github.com/rwjblue) +- [#9045](https://github.com/ember-cli/ember-cli/pull/9045) Add final newline in CONTRIBUTING.md [@kellyselden](https://github.com/kellyselden) +- [c30ed27181257ab4319b3a06134e13067ac1e76e](https://github.com/ember-cli/ember-cli/commit/c30ed27181257ab4319b3a06134e13067ac1e76e) Handle a number of unhandled rejections scenarios [@stefanpenner](https://github.com/stefanpenner) +- [c377300bb21485faf0137ce69b54a10b3a458828](https://github.com/ember-cli/ember-cli/commit/c377300bb21485faf0137ce69b54a10b3a458828) Publish yuidoc json as a part of npm package [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [0a8d7a18b5f27147f2cec5574625e53784841601](https://github.com/ember-cli/ember-cli/commit/0a8d7a18b5f27147f2cec5574625e53784841601) Consistently 'use strict'; for our node js files [@kellyselden](https://github.com/kellyselden) +- [64e635c48c76f177769ca73eb9a228149ffbd863](https://github.com/ember-cli/ember-cli/commit/64e635c48c76f177769ca73eb9a228149ffbd863) Ensure buildFailures are reported correctly [@stefanpenner](https://github.com/stefanpenner) + +Thank you to all who took the time to contribute! + +## v3.16.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.15.2...v3.16.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.15.2...v3.16.0) + +#### Changelog + +- [#8905](https://github.com/ember-cli/ember-cli/pull/8905) Use production environment for `npm run build` / `yarn build` by default [@pichfl](https://github.com/pichfl) +- [#8930](https://github.com/ember-cli/ember-cli/pull/8930) / [#8929](https://github.com/ember-cli/ember-cli/pull/8929) Drop Node 11 support [@SergeAstapov](https://github.com/SergeAstapov) +- [#8932](https://github.com/ember-cli/ember-cli/pull/8932) Add Node.js 13 to test matrix [@SergeAstapov](https://github.com/SergeAstapov) +- [#8941](https://github.com/ember-cli/ember-cli/pull/8941) feat(blueprint): resolve remote blueprints via package manager [@buschtoens](https://github.com/buschtoens) +- [#8944](https://github.com/ember-cli/ember-cli/pull/8944) Travis.yml: Remove deprecated `sudo: false` option [@tniezurawski](https://github.com/tniezurawski) +- [#8943](https://github.com/ember-cli/ember-cli/pull/8943) Travis.yml: use fast_finish instead of undocumented fail_fast [@tniezurawski](https://github.com/tniezurawski) +- [#8962](https://github.com/ember-cli/ember-cli/pull/8962) Drop Ember 3.8 and add Ember 3.16 scenarios in default `config/ember-try.js`. [@kellyselden](https://github.com/kellyselden) +- [#8986](https://github.com/ember-cli/ember-cli/pull/8986) Increase testem browser timeout. [@rwjblue](https://github.com/rwjblue) +- [#9012](https://github.com/ember-cli/ember-cli/pull/9012) Drop support for Node v8 [@jrjohnson](https://github.com/jrjohnson) +- [#9013](https://github.com/ember-cli/ember-cli/pull/9013) Remove useless line break in `.editorconfig` file [@dcyriller](https://github.com/dcyriller) +- [#9023](https://github.com/ember-cli/ember-cli/pull/9023) Update to use Ember + Ember Data 3.16. [@rwjblue](https://github.com/rwjblue) +- [#9026](https://github.com/ember-cli/ember-cli/pull/9026) Add @glimmer/tracking to default blueprint. [@rwjblue](https://github.com/rwjblue) +- [#9028](https://github.com/ember-cli/ember-cli/pull/9028) Update minimum versions of app / addon blueprint dependencies. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.15.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.15.1...v3.15.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.15.1...v3.15.2) + +#### Changelog + +- [#8924](https://github.com/ember-cli/ember-cli/pull/8924) Fix named UMD imports [@kellyselden](https://github.com/kellyselden) +- [#9015](https://github.com/ember-cli/ember-cli/pull/9015) Allow failure of Node 8 CI jobs. [@rwjblue](https://github.com/rwjblue) +- [#9014](https://github.com/ember-cli/ember-cli/pull/9014) Avoid errors when `ui` is not present and a warning will be emitted. [@tmquinn](https://github.com/tmquinn) + +Thank you to all who took the time to contribute! + +## v3.15.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.15.0...v3.15.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.15.0...v3.15.1) + +#### Changelog + +- [#8977](https://github.com/ember-cli/ember-cli/pull/8977) Fix invalid syntax with ember-classic ember-try scenario. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.15.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.14.0...v3.15.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.14.0...v3.15.0) + +#### Changelog + +- [#8963](https://github.com/ember-cli/ember-cli/pull/8963) Remove `app/templates/components` [@chancancode](https://github.com/chancancode) +- [#8964](https://github.com/ember-cli/ember-cli/pull/8964) Add support for `ember new @scope-here/name-here`. [@rwjblue](https://github.com/rwjblue) +- [#8965](https://github.com/ember-cli/ember-cli/pull/8965) Update ember-resolver to v7.0.0. [@rwjblue](https://github.com/rwjblue) +- [#8971](https://github.com/ember-cli/ember-cli/pull/8971) Add an ember-try scenario for Ember "classic" (pre-octane). [@rwjblue](https://github.com/rwjblue) +- [#8972](https://github.com/ember-cli/ember-cli/pull/8972) Update ember-data to 3.15.0. [@rwjblue](https://github.com/rwjblue) +- [#8933](https://github.com/ember-cli/ember-cli/pull/8933) Remove `app/resolver.js` in favor of importing in `app/app.js` [@rwjblue](https://github.com/rwjblue) +- [#8945](https://github.com/ember-cli/ember-cli/pull/8945) Fix issue with addon `.travis.yml` configuration when using `npm` [@kellyselden](https://github.com/kellyselden) +- [#8946](https://github.com/ember-cli/ember-cli/pull/8946) Drop testing of ember-source@3.4 in the addon blueprints ember-try config [@kellyselden](https://github.com/kellyselden) +- [#8946](https://github.com/ember-cli/ember-cli/pull/8946) Add testing of ember-source@3.12 in the addon blueprints ember-try config [@kellyselden](https://github.com/kellyselden) +- [#8959](https://github.com/ember-cli/ember-cli/pull/8959) Fix issue with addon discovery when npm/yarn leave empty directories in resolvable locations [@stefanpenner](https://github.com/stefanpenner) +- [#8961](https://github.com/ember-cli/ember-cli/pull/8961) Prepare for Octane release in 3.15 [@rwjblue](https://github.com/rwjblue) + * Adds `ember` property to `package.json` to implement [emberjs/rfcs#558](https://github.com/emberjs/rfcs/pull/558) + * Adds `@glimmer/component@1.0.0` as a development dependency for both apps and addons + * Updates `ember-try` to at least 1.4.0 in order to support `config/ember-try.js` scenarios with `ember` `package.json` property (mentioned in emberjs/rfcs#558) + * Enables Octane related optional features + * Updates ember-template-lint configuration to use `octane` preset + * Update to ember-source@3.15 stable + * Updates all packages in the application blueprint to their latest version +- [#8827](https://github.com/ember-cli/ember-cli/pull/8827) Remove module-unification blueprints [@dcyriller](https://github.com/dcyriller) +- [#8878](https://github.com/ember-cli/ember-cli/pull/8878) Adds flag to throw an error for invalid addon locations [@tmquinn](https://github.com/tmquinn) +- [#8906](https://github.com/ember-cli/ember-cli/pull/8906) Enable broccoli memoization by default in Ember-CLI [@SparshithNR](https://github.com/SparshithNR) +- [#8917](https://github.com/ember-cli/ember-cli/pull/8917) Update CI configuration for applications using `npm` to run a "floating dependencies" test. [@kellyselden](https://github.com/kellyselden) +- [#8926](https://github.com/ember-cli/ember-cli/pull/8926) Add `application` to invalid names [@kennethlarsen](https://github.com/kennethlarsen) + +Thank you to all who took the time to contribute! + +## v3.14.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.13.2...v3.14.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.13.2...v3.14.0) + +#### Changelog + +- [#8875](https://github.com/ember-cli/ember-cli/pull/8875) Fix ember-cli-htmlbars-inline-precompile deprecation [@HeroicEric](https://github.com/HeroicEric) +- [#8882](https://github.com/ember-cli/ember-cli/pull/8882) Simplify "Get started" message for `ember new` [@dcyriller](https://github.com/dcyriller) +- [#8899](https://github.com/ember-cli/ember-cli/pull/8899) Don't reload the config for instrumentation [@pzuraq](https://github.com/pzuraq) +- [#8900](https://github.com/ember-cli/ember-cli/pull/8900) Include `legacyDecorators` eslint option by default [@pzuraq](https://github.com/pzuraq) +- [#8901](https://github.com/ember-cli/ember-cli/pull/8901) Merge `config/environment.js`'s `EmberENV` configuration with any pre-existing `EmberENV` [@chancancode](https://github.com/chancancode) +- [#8910](https://github.com/ember-cli/ember-cli/pull/8910) Update TravisCI config for `ember new` to restrict CI runs to `master` branch and pull requests [@kellyselden](https://github.com/kellyselden) +- [#8915](https://github.com/ember-cli/ember-cli/pull/8915) Revert changes made to enable "octane" as the default for `ember new` [@rwjblue](https://github.com/rwjblue) +- [#8916](https://github.com/ember-cli/ember-cli/pull/8916) Update ember-source and ember-data to 3.14.x [@rwjblue](https://github.com/rwjblue) +- [#8853](https://github.com/ember-cli/ember-cli/pull/8853) Update ember-resolver to 5.3.0. [@rwjblue](https://github.com/rwjblue) +- [#8812](https://github.com/ember-cli/ember-cli/pull/8812) Clarify installation error message [@jrjohnson](https://github.com/jrjohnson) +- [#8820](https://github.com/ember-cli/ember-cli/pull/8820) Issue deprecation when enabling MODULE_UNIFICATION flag. [@rwjblue](https://github.com/rwjblue) + +## v3.13.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.13.1...v3.13.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.13.1...v3.13.2) + +#### Changelog + +- [#8875](https://github.com/ember-cli/ember-cli/pull/8875) Fix ember-cli-htmlbars-inline-precompile deprecation [@HeroicEric](https://github.com/HeroicEric) +- [#8882](https://github.com/ember-cli/ember-cli/pull/8882) Simplify "Get started" message [@dcyriller](https://github.com/dcyriller) +- [#8901](https://github.com/ember-cli/ember-cli/pull/8901) Merge `config/environment.js`'s `EmberENV` configuration with any pre-existing `EmberENV` [@chancancode](https://github.com/chancancode) + +Thank you to all who took the time to contribute! + +## v3.13.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.13.0...v3.13.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.13.0...v3.13.1) + +#### Changelog + +- [#8857](https://github.com/ember-cli/ember-cli/pull/8857) Tweaks to release scripts. [@rwjblue](https://github.com/rwjblue) +- [#8862](https://github.com/ember-cli/ember-cli/pull/8862) Adjust message for when a new app is created [@dcyriller](https://github.com/dcyriller) + +Thank you to all who took the time to contribute! + +## v3.13.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.12.0...v3.13.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.12.0...v3.13.0) + +#### Changelog + +- [#8797](https://github.com/ember-cli/ember-cli/pull/8797) Update heimdalljs-fs-monitor to 0.2.3. [@rwjblue](https://github.com/rwjblue) +- [#8798](https://github.com/ember-cli/ember-cli/pull/8798) Update blueprint reference for ember-source to 3.13.0-beta.2. [@rwjblue](https://github.com/rwjblue) +- [#8814](https://github.com/ember-cli/ember-cli/pull/8814) Drop Node 11 from CI. [@rwjblue](https://github.com/rwjblue) +- [#8816](https://github.com/ember-cli/ember-cli/pull/8816) Update app and addon blueprints to latest version of packages. [@rwjblue](https://github.com/rwjblue) +- [#8834](https://github.com/ember-cli/ember-cli/pull/8834) Ensure addon tree is scoped to addon name before compiling templates. [@rwjblue](https://github.com/rwjblue) +- [fd7268b59ddcddca849762a4923c14655da47188](https://github.com/ember-cli/ember-cli/commit/fd7268b59ddcddca849762a4923c14655da47188) Update watch-detector to 1.0.0. [@rwjblue](https://github.com/rwjblue) +- [#8850](https://github.com/ember-cli/ember-cli/pull/8850) Update broccoli dependencies/devDependencies to latest. [@rwjblue](https://github.com/rwjblue) +- [#8851](https://github.com/ember-cli/ember-cli/pull/8851) Update Ember ecosystem packages to latest version. [@rwjblue](https://github.com/rwjblue) +- [#8853](https://github.com/ember-cli/ember-cli/pull/8853) Update ember-resolver to 5.3.0. [@rwjblue](https://github.com/rwjblue) +- [#8642](https://github.com/ember-cli/ember-cli/pull/8642) Use system temp for ember test [@ef4](https://github.com/ef4) +- [#8650](https://github.com/ember-cli/ember-cli/pull/8650) [BUGFIX] reset resolve-package-path caches in PackageInfoCache._clear() [@jamescdavis](https://github.com/jamescdavis) +- [#8633](https://github.com/ember-cli/ember-cli/pull/8633) Refactor template build pipeline to enable co-located templates. [@rwjblue](https://github.com/rwjblue) +- [#8616](https://github.com/ember-cli/ember-cli/pull/8616) [ENHANCEMENT] Gather hardware information when creating instrumentation summaries [@benblank](https://github.com/benblank) +- [#8676](https://github.com/ember-cli/ember-cli/pull/8676) Track the time taken to collect hardware metrics [@benblank](https://github.com/benblank) +- [#8678](https://github.com/ember-cli/ember-cli/pull/8678) give ember-cli a progress indicator [@stefanpenner](https://github.com/stefanpenner) +- [#8588](https://github.com/ember-cli/ember-cli/pull/8588) [dx] Detail app / addon creation messages [@dcyriller](https://github.com/dcyriller) +- [#8687](https://github.com/ember-cli/ember-cli/pull/8687) Add .git directory to npmignore [@rwwagner90](https://github.com/rwwagner90) +- [#8701](https://github.com/ember-cli/ember-cli/pull/8701) Add ember-cli-htmlbars to default addon dependencies [@haochuan](https://github.com/haochuan) +- [#8747](https://github.com/ember-cli/ember-cli/pull/8747) Update Windows documentation link [@loganrosen](https://github.com/loganrosen) +- [#8772](https://github.com/ember-cli/ember-cli/pull/8772) fix typos :) [@aspala](https://github.com/aspala) +- [#8564](https://github.com/ember-cli/ember-cli/pull/8564) Adds babel-eslint as the default ESlint parser [@pzuraq](https://github.com/pzuraq) + +Thank you to all who took the time to contribute! + +## v3.12.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.12.0...v3.12.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.12.0...v3.12.1) + +#### Community Contributions + +- [#8797](https://github.com/ember-cli/ember-cli/pull/8797) Update heimdalljs-fs-monitor to 0.2.3. [@rwjblue](https://github.com/rwjblue) +- [#8959](https://github.com/ember-cli/ember-cli/pull/8959) Ensure `node_modules/*` directories without a `package.json` are not considered as part of the addon discovery process [@stefanpenner](https://github.com/stefanpenner) + +Thank you to all who took the time to contribute! + +## v3.12.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.11.0...v3.12.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.11.0...v3.12.0) + +#### Community Contributions + +- [#8753](https://github.com/ember-cli/ember-cli/pull/8753) Quote empty strings in printCommand [@chancancode](https://github.com/chancancode) +- [#8774](https://github.com/ember-cli/ember-cli/pull/8774) Remove --disable-gpu flag when starting headless chrome [@stefanpenner](https://github.com/stefanpenner) + +## v3.11.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.10.0...v3.11.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.10.0...v3.11.0) + +#### Community Contributions + +- [#8659](https://github.com/ember-cli/ember-cli/pull/8659) ecmaVersion 2015 is out of date [@kellyselden](https://github.com/kellyselden) +- [#8660](https://github.com/ember-cli/ember-cli/pull/8660) start testing ember-lts-3.8 in ember-try [@kellyselden](https://github.com/kellyselden) +- [#8662](https://github.com/ember-cli/ember-cli/pull/8662) use async/await in ember-try config [@kellyselden](https://github.com/kellyselden) +- [#8664](https://github.com/ember-cli/ember-cli/pull/8664) fix README ember min version [@kellyselden](https://github.com/kellyselden) +- [#8674](https://github.com/ember-cli/ember-cli/pull/8674) ensure we use Promise.prototype.finally polyfil for node 8 compat [@stefanpenner](https://github.com/stefanpenner) +- [#8675](https://github.com/ember-cli/ember-cli/pull/8675) [beta] Gather hardware information when creating instrumentation summaries. [@stefanpenner](https://github.com/stefanpenner) +- [#8679](https://github.com/ember-cli/ember-cli/pull/8679) Adding change logging for backwards compatibility [@thoov](https://github.com/thoov) +- [#8680](https://github.com/ember-cli/ember-cli/pull/8680) [Fixes #8677] ensure watcher parity [@stefanpenner](https://github.com/stefanpenner) +- [#8595](https://github.com/ember-cli/ember-cli/pull/8595) `Project#config` should use `EMBER_ENV` as default environment when none is passed in [@nlfurniss](https://github.com/nlfurniss) +- [#8604](https://github.com/stefanpenner/ember-cli/pull/8604) CONTRIBUTING: Clarify the way to start working on the repo. [@MonsieurDart](https://github.com/MonsieurDart) +- [#8621](https://github.com/ember-cli/ember-cli/pull/8621) project.findAddonByName was intended to be public [@stefanpenner](https://github.com/stefanpenner) +- [#8697](https://github.com/ember-cli/ember-cli/pull/8697) Update to Broccoli 3.1. [@thoov](https://github.com/thoov) +- [#8692](https://github.com/ember-cli/ember-cli/pull/8692) Allow `prettier` usage on `app/index.html` [@lougreenwood](https://github.com/lougreenwood) + +Thank you to all who took the time to contribute! + +## v3.10.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.10.0...v3.10.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.10.0...v3.10.1) + +#### Community Contributions + +- [#8645](https://github.com/ember-cli/ember-cli/pull/8645) Update addon and application blueprints to account for Node 6 support being removed. [@kellyselden](https://github.com/kellyselden) +- [#8631](https://github.com/ember-cli/ember-cli/pull/8631) Add CI testing for Node 12. [@rwjblue](https://github.com/rwjblue) + +## v3.10.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.9.0...v3.10.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.9.0...v3.10.0) + +#### Community Contributions + +- [#8631](https://github.com/ember-cli/ember-cli/pull/8631) Add CI testing for Node 12. [@rwjblue](https://github.com/rwjblue) +- [#8563](https://github.com/ember-cli/ember-cli/pull/8563) Drop Node 6 support [@Turbo87](https://github.com/Turbo87) +- [#8566](https://github.com/ember-cli/ember-cli/pull/8566) Modernize `build-watch.js` and `build-watch-test.js` [@xg-wang](https://github.com/xg-wang) +- [#8569](https://github.com/ember-cli/ember-cli/pull/8569) Modernize `bower-install-test.js` [@RichardOtvos](https://github.com/RichardOtvos) +- [#8565](https://github.com/ember-cli/ember-cli/pull/8565) Modernize `git-init.js` and `git-init-test.js` [@xg-wang](https://github.com/xg-wang) +- [#8572](https://github.com/ember-cli/ember-cli/pull/8572) Use eslint-plugin-node v8 in blueprints [@kellyselden](https://github.com/kellyselden) +- [#8205](https://github.com/ember-cli/ember-cli/pull/8205) Run eslint-plugin-node on apps [@kellyselden](https://github.com/kellyselden) +- [#8606](https://github.com/ember-cli/ember-cli/pull/8606) Modernize `models/instrumentation-test.js` [@Semeia-io](https://github.com/Semeia-io) +- [#8607](https://github.com/ember-cli/ember-cli/pull/8607) Modernize `models/addon-test.js` [@Semeia-io](https://github.com/Semeia-io) +- [#8462](https://github.com/ember-cli/ember-cli/pull/8462) blueprints: Update `ember-cli-eslint` to v5.1.0 [@Turbo87](https://github.com/Turbo87) +- [#8461](https://github.com/ember-cli/ember-cli/pull/8461) blueprints: Update `ember-welcome-page` to v4.0.0 [@Turbo87](https://github.com/Turbo87) +- [#8460](https://github.com/ember-cli/ember-cli/pull/8460) blueprints: Update `ember-qunit` to v4.4.1 [@Turbo87](https://github.com/Turbo87) +- [#8396](https://github.com/ember-cli/ember-cli/pull/8396) blueprints: Update dependencies [@mistahenry](https://github.com/mistahenry) +- [#8470](https://github.com/ember-cli/ember-cli/pull/8470) Remove obsolete `BROCCOLI_2` experiment [@Turbo87](https://github.com/Turbo87) +- [#8469](https://github.com/ember-cli/ember-cli/pull/8469) Move all package related path resolution to `resolve-package-path` [@stefanpenner](https://github.com/stefanpenner) +- [#8515](https://github.com/ember-cli/ember-cli/pull/8515) Corrected tiny typo in JSDoc [@rbarbey](https://github.com/rbarbey) +- [#8517](https://github.com/ember-cli/ember-cli/pull/8517) Add `--output-path` to test command [@step2yeung](https://github.com/step2yeung) +- [#8528](https://github.com/ember-cli/ember-cli/pull/8528) Ensure packager respects source map config when concatting [@stefanpenner](https://github.com/stefanpenner) +- [#8540](https://github.com/ember-cli/ember-cli/pull/8540) Fixed broken npm link documentation link [@yohanmishkin](https://github.com/yohanmishkin) + +Thank you to all who took the time to contribute! + + +## v3.9.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.8.2...v3.9.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.8.2...v3.9.0) + +#### Community Contributions + +- [#8444](https://github.com/ember-cli/ember-cli/pull/8444) Ensure Node 11 does not issue warning [@jeanduplessis](https://github.com/jeanduplessis) +- [#8425](https://github.com/ember-cli/ember-cli/pull/8425) Update Broccoli website URL [@hakilebara](https://github.com/hakilebara) +- [#8383](https://github.com/ember-cli/ember-cli/pull/8383) Update `ember-welcome-page` usage to angle brackets [@locks](https://github.com/locks) +- [#8435](https://github.com/ember-cli/ember-cli/pull/8435) Don't add extra slash in dist paths [@knownasilya](https://github.com/knownasilya) +- [#8358](https://github.com/ember-cli/ember-cli/pull/8358) In MU apps, exclude TS test files from the app JS file [@ppcano](https://github.com/ppcano) +- [#8373](https://github.com/ember-cli/ember-cli/pull/8373) package-info-cache: Add `heimdalljs-logger` logging [@Turbo87](https://github.com/Turbo87) +- [#8379](https://github.com/ember-cli/ember-cli/pull/8379) Fix the module path for MU non-acceptance tests [@ppcano](https://github.com/ppcano) +- [#8387](https://github.com/ember-cli/ember-cli/pull/8387) Fix non-acceptance tests for MU addons [@ppcano](https://github.com/ppcano) +- [#8289](https://github.com/ember-cli/ember-cli/pull/8289) Include addon styles for MU apps [@ppcano](https://github.com/ppcano) +- [#8399](https://github.com/ember-cli/ember-cli/pull/8399) Improve jQuery deprecation message [@simonihmig](https://github.com/simonihmig) +- [#8397](https://github.com/ember-cli/ember-cli/pull/8397) Update packages [@btecu](https://github.com/btecu) +- [#8432](https://github.com/ember-cli/ember-cli/pull/8432) Fix how MU blueprints fetches `ember-source` [@ppcano](https://github.com/ppcano) +- [#8433](https://github.com/ember-cli/ember-cli/pull/8433) MU blueprints: enable `EMBER_MODULE_UNIFICATION` feature flag [@ppcano](https://github.com/ppcano) +- [#8414](https://github.com/ember-cli/ember-cli/pull/8414) `preprocessTemplates` is called only once in MU layout [@ppcano](https://github.com/ppcano) +- [#8434](https://github.com/ember-cli/ember-cli/pull/8434) Fix comment on the `environment.js` blueprint files [@ppcano](https://github.com/ppcano) + +Thank you to all who took the time to contribute! + +## v3.8.3 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.8.2...v3.8.3) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.8.2...v3.8.3) + +#### Community Contributions + +- [#8631](https://github.com/ember-cli/ember-cli/pull/8631) Add CI testing for Node 12. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.8.2 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.8.1...v3.8.2) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.8.1...v3.8.2) + +#### Community Contributions + +- [#8482](https://github.com/ember-cli/ember-cli/pull/8482) Update `ember-ajax` in blueprints and tests [@boris-petrov](https://github.com/boris-petrov) +- [#8370](https://github.com/ember-cli/ember-cli/pull/8370) Use `moduleName()` for templates [@pzuraq](https://github.com/pzuraq) +- [#8556](https://github.com/ember-cli/ember-cli/pull/8556) Ensure packager respects source map config when concatting [@stefanpenner](https://github.com/stefanpenner) + +Thank you to all who took the time to contribute! + + +## v3.8.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.8.0...v3.8.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.8.0...v3.8.1) + +#### Community Contributions + +- [#8261](https://github.com/ember-cli/ember-cli/pull/8261) add server/**/*.js to eslint node files for app [@kellyselden](https://github.com/kellyselden) +- [#8467](https://github.com/ember-cli/ember-cli/pull/8467) Ensure npm version is available during ember new / ember install foo. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + + +## v3.8.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.7.1...v3.8.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.7.1...v3.8.0) + +#### Community Contributions + +- [#8175](https://github.com/ember-cli/ember-cli/pull/8175) Do not watch `tests` directory when tests are disabled [@f1sherman](https://github.com/f1sherman) +- [#8052](https://github.com/ember-cli/ember-cli/pull/8052) Use non-greedy pattern for `{{content-for}}` [@mpirio](https://github.com/mpirio) +- [#8325](https://github.com/ember-cli/ember-cli/pull/8325) Add contributing guidelines section to Docs [@jamesgeorge007](https://github.com/jamesgeorge007) +- [#8326](https://github.com/ember-cli/ember-cli/pull/8326) package.json: Move `fixturify` to `devDependencies` [@Turbo87](https://github.com/Turbo87) +- [#8329](https://github.com/ember-cli/ember-cli/pull/8329) livereload-server: Fix logger output [@Turbo87](https://github.com/Turbo87) +- [#8328](https://github.com/ember-cli/ember-cli/pull/8328) tasks/server: Remove obsolete `exists-sync` dependency declaration [@Turbo87](https://github.com/Turbo87) +- [#8313](https://github.com/ember-cli/ember-cli/pull/8313) Update ember-ajax to v4.x [@maxwondercorn](https://github.com/maxwondercorn) +- [#8309](https://github.com/ember-cli/ember-cli/pull/8309) blueprints/addon: Add Contributing section [@knownasilya](https://github.com/knownasilya) +- [#8327](https://github.com/ember-cli/ember-cli/pull/8327) Improve `ember-cli` entry file [@Turbo87](https://github.com/Turbo87) +- [#8330](https://github.com/ember-cli/ember-cli/pull/8330) blueprints/app/gitignore: Ignore Yarn PnP files [@Turbo87](https://github.com/Turbo87) +- [#8331](https://github.com/ember-cli/ember-cli/pull/8331) Update `ember-cli-dependency-checker` to v3.1.0 [@Turbo87](https://github.com/Turbo87) +- [#8323](https://github.com/ember-cli/ember-cli/pull/8323) Dynamically fetch the `ember-source` version for MU blueprints [@ppcano](https://github.com/ppcano) +- [#8427](https://github.com/ember-cli/ember-cli/pull/8427) Fix install warnings for @babel/core [@jrjohnson](https://github.com/jrjohnson) + +Thank you to all who took the time to contribute! + + +## v3.7.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.7.0...v3.7.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.7.0...v3.7.1) + +#### Community Contributions + +- [#8357](https://github.com/ember-cli/ember-cli/pull/8357) blueprints/addon: Fix incorrect job formatting in `.travis.yml` config [@buschtoens](https://github.com/buschtoens) + +Thank you to all who took the time to contribute! + + +## v3.7.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.6.1...v3.7.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.6.1...v3.7.0) + +#### Community Contributions + +- [#8262](https://github.com/ember-cli/ember-cli/pull/8262) spelling fix [@jfdnc](https://github.com/jfdnc) +- [#8264](https://github.com/ember-cli/ember-cli/pull/8264) Remove redundant _requireBuildPackages and format comments [@xg-wang](https://github.com/xg-wang) +- [#8275](https://github.com/ember-cli/ember-cli/pull/8275) Fix dead link in CONTRIBUTING.md [@hakilebara](https://github.com/hakilebara) +- [#8279](https://github.com/ember-cli/ember-cli/pull/8279) CHANGELOG: Drop releases before v2.8.0 [@Turbo87](https://github.com/Turbo87) +- [#8286](https://github.com/ember-cli/ember-cli/pull/8286) Provide a compatibility section in addon READMEs [@kennethlarsen](https://github.com/kennethlarsen) +- [#8296](https://github.com/ember-cli/ember-cli/pull/8296) Add ember-source@3.4 LTS ember-try scenario. [@rwjblue](https://github.com/rwjblue) +- [#8297](https://github.com/ember-cli/ember-cli/pull/8297) Remove last usage of Babel 6. [@rwjblue](https://github.com/rwjblue) +- [#8299](https://github.com/ember-cli/ember-cli/pull/8299) remove ember-lts-2.16 ember-try scenario [@kellyselden](https://github.com/kellyselden) +- [#8308](https://github.com/ember-cli/ember-cli/pull/8308) ignore .env* files [@kellyselden](https://github.com/kellyselden) +- [#8351](https://github.com/ember-cli/ember-cli/pull/8351) Update Ember and Ember Data to v3.7.0 [@Turbo87](https://github.com/Turbo87) +- [#8352](https://github.com/ember-cli/ember-cli/pull/8352) Fix `relative-module-paths` caching [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +## v3.6.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.6.0...v3.6.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.6.0...v3.6.1) + +#### Community Contributions + +- [#8288](https://github.com/ember-cli/ember-cli/pull/8288) Add `ember-default` scenario to `ember-try` config again [@bendemboski](https://github.com/bendemboski) +- [#8296](https://github.com/ember-cli/ember-cli/pull/8296) Add ember-source@3.4 LTS ember-try scenario. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + + +## v3.6.0 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.5.1...v3.6.0) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.5.1...v3.6.0) + +#### Community Contributions + +- [#7958](https://github.com/ember-cli/ember-cli/pull/7958) Gather doc files in docs directory [@dcyriller](https://github.com/dcyriller) +- [#8032](https://github.com/ember-cli/ember-cli/pull/8032) Update minimum broccoli-viz version [@gandalfar](https://github.com/gandalfar) +- [#7974](https://github.com/ember-cli/ember-cli/pull/7974) Prevent double builds in CI for branches pushed by owner [@rwjblue](https://github.com/rwjblue) +- [#8096](https://github.com/ember-cli/ember-cli/pull/8096) Use regex to parse /livereload urls [@SparshithNR](https://github.com/SparshithNR) +- [#8086](https://github.com/ember-cli/ember-cli/pull/8086) Prefer walk-sync for AssetPrinterSize (speeds things up) [@stefanpenner](https://github.com/stefanpenner) +- [#8108](https://github.com/ember-cli/ember-cli/pull/8108) Put `package-info-cache` warnings under `DEBUG` control [@dcombslinkedin](https://github.com/dcombslinkedin) +- [#8203](https://github.com/ember-cli/ember-cli/pull/8203) Move contribution info away from `README` to `CONTRIBUTING` [@kennethlarsen/feature](https://github.com/kennethlarsen/feature) +- [#8147](https://github.com/ember-cli/ember-cli/pull/8147) Bump ember-cli-babel@7 in application blueprint [@SergeAstapov](https://github.com/SergeAstapov) +- [#8142](https://github.com/ember-cli/ember-cli/pull/8142) Fix yarn test failures [@abhilashlr](https://github.com/abhilashlr) +- [#8138](https://github.com/ember-cli/ember-cli/pull/8138) Remove ember-ajax from addon blueprint [@initram](https://github.com/initram) +- [#8143](https://github.com/ember-cli/ember-cli/pull/8143) adding fix to use NULL nodeModuleList to optimize when dealing with non-addons [@dcombslinkedin](https://github.com/dcombslinkedin) +- [#8179](https://github.com/ember-cli/ember-cli/pull/8179) Do not include `.jshintrc` and `.eslintrc` when generating `lib` or `packages` [@ppcano](https://github.com/ppcano) +- [#8162](https://github.com/ember-cli/ember-cli/pull/8162) remove any /* eslint-env node */ [@kellyselden](https://github.com/kellyselden) +- [#8174](https://github.com/ember-cli/ember-cli/pull/8174) Fix links in Addon API docs header [@dfreeman](https://github.com/dfreeman) +- [#8171](https://github.com/ember-cli/ember-cli/pull/8171) Add `--ssl` options to `ember test` [@nathanhammond](https://github.com/nathanhammond) +- [#8165](https://github.com/ember-cli/ember-cli/pull/8165) Remove unused "ember-default" scenario [@kellyselden](https://github.com/kellyselden) +- [#8202](https://github.com/ember-cli/ember-cli/pull/8202) Specify explicit ember-cli version in project update instructions [@nickschot](https://github.com/nickschot) +- [#8277](https://github.com/ember-cli/ember-cli/pull/8277) DefaultPackager: Move `addon-test-support` out of the `tests` folder [@Turbo87](https://github.com/Turbo87) +- [#8278](https://github.com/ember-cli/ember-cli/pull/8278) DefaultPackager: Fix addon preprocessing [@Turbo87](https://github.com/Turbo87) +- [#8287](https://github.com/ember-cli/ember-cli/pull/8287) blueprints/app: Update Ember and Ember Data to v3.6.0 [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +## v3.5.1 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.5.0...v3.5.1) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.5.0...v3.5.1) + +#### Community Contributions + +- [#8127](https://github.com/ember-cli/ember-cli/pull/8127) Fix eslint errors in new app [@Gaurav0](https://github.com/Gaurav0) +- [#8130](https://github.com/ember-cli/ember-cli/pull/8130) Use regex to parse /livereload urls [@rwjblue](https://github.com/rwjblue) +- [#8141](https://github.com/ember-cli/ember-cli/pull/8141) Use `debug` for `package-info-cache` messages [@stefanpenner](https://github.com/stefanpenner) +- [#8150](https://github.com/ember-cli/ember-cli/pull/8150) Fix `toTree()` with custom paths [@wagenet](https://github.com/wagenet) + +Thank you to all who took the time to contribute! + + +## v3.5.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.4.3...v3.5.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.4.3...v3.5.0) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#8079](https://github.com/ember-cli/ember-cli/pull/8079) add .template-lintrc.js to npmignore [@kellyselden](https://github.com/kellyselden) +- [#8083](https://github.com/ember-cli/ember-cli/pull/8083) Catch InvalidNodeError for Broccoli 2.0 and fallback to broccoli-builder [@oligriffiths](https://github.com/oligriffiths) +- [#8117](https://github.com/ember-cli/ember-cli/pull/8117) Do not ignore dotfiles in ESLint [@Gaurav0](https://github.com/Gaurav0) +- [#7365](https://github.com/ember-cli/ember-cli/pull/7365) Migrate from ember-cli-qunit to ember-qunit. [@rwjblue](https://github.com/rwjblue) +- [#8062](https://github.com/ember-cli/ember-cli/pull/8062) Add `yarn.lock` to `.npmignore` [@Turbo87](https://github.com/Turbo87) +- [#8064](https://github.com/ember-cli/ember-cli/pull/8064) Update `qunit-dom` to v0.8.0 [@Turbo87](https://github.com/Turbo87) +- [#8067](https://github.com/ember-cli/ember-cli/pull/8067) Less restrictive blueprints - change how addons are identified [@scalvert](https://github.com/scalvert) +- [#8068](https://github.com/ember-cli/ember-cli/pull/8068) Enable BROCCOLI_2 and SYSTEM_TEMP experiments by default. [@rwjblue](https://github.com/rwjblue) +- [#8069](https://github.com/ember-cli/ember-cli/pull/8069) Adding back feature to customization of serveURL [@SparshithNR](https://github.com/SparshithNR) +- [#7798](https://github.com/ember-cli/ember-cli/pull/7798) Add Broccoli 2.0 support [@oligriffiths](https://github.com/oligriffiths) +- [#7916](https://github.com/ember-cli/ember-cli/pull/7916) Support node-http-proxy timeout options [@jboler](https://github.com/jboler) +- [#7937](https://github.com/ember-cli/ember-cli/pull/7937) Update fallback default browser targets from IE9 -> IE11. [@arthirm](https://github.com/arthirm) +- [#7984](https://github.com/ember-cli/ember-cli/pull/7984) Reject when command args validation fails [@zonkyio](https://github.com/zonkyio) +- [#7946](https://github.com/ember-cli/ember-cli/pull/7946) upgrade ember-cli-htmlbars to 3.x [@stefanpenner](https://github.com/stefanpenner) +- [#8000](https://github.com/ember-cli/ember-cli/pull/8000) Adding `--in` option to `ember generate` and `ember destroy` to allow blueprint generation for in repo addons in custom paths. [@scalvert](https://github.com/scalvert) +- [#8028](https://github.com/ember-cli/ember-cli/pull/8028) Add `public` to the list of disallowed application names [@twokul](https://github.com/twokul) + +Thank you to all who took the time to contribute! + + +## v3.4.4 + +#### Blueprint Changes + +- [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.4.3...v3.4.4) +- [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.4.3...v3.4.4) + +#### Community Contributions + +- [#8277](https://github.com/ember-cli/ember-cli/pull/8277) DefaultPackager: Move `addon-test-support` out of the `tests` folder [@Turbo87](https://github.com/Turbo87) +- [#8278](https://github.com/ember-cli/ember-cli/pull/8278) DefaultPackager: Fix addon preprocessing [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +## v3.4.3 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.4.2...v3.4.3) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember new` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.4.2...v3.4.3) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#8044](https://github.com/ember-cli/ember-cli/pull/8044) Fix livereload issues when SSL is enabled. [@SparshithNR](https://github.com/SparshithNR) +- [#8046](https://github.com/ember-cli/ember-cli/pull/8046) Do not to fail to build if `tests` folder is not present [@twokul](https://github.com/twokul) +- [#8047](https://github.com/ember-cli/ember-cli/pull/8047) Fix `app.import` transforms for tests [@twokul](https://github.com/twokul) +- [#8048](https://github.com/ember-cli/ember-cli/pull/8048) Make sure app content always "wins" over addon content. [@twokul](https://github.com/twokul) +- [#8057](https://github.com/ember-cli/ember-cli/pull/8057) Ensure livereload support does not break proxied websockets. [@rwjblue](https://github.com/rwjblue) +- [#8058](https://github.com/ember-cli/ember-cli/pull/8058) Tweak invalid / missing package log output to be more actionable [@dcombslinkedin](https://github.com/dcombslinkedin) + +Thank you to all who took the time to contribute! + +## v3.4.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.4.1...v3.4.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.4.1...v3.4.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#8024](https://github.com/ember-cli/ember-cli/pull/8024) [BUGFIX] Remove 2.12 scenario from travis.yml [@cibernox](https://github.com/cibernox) +- [#8033](https://github.com/ember-cli/ember-cli/pull/8033) Restore `styles` behaviour [@twokul](https://github.com/twokul) +- [#8038](https://github.com/ember-cli/ember-cli/pull/8038) Ensure livereload proxy is scoped to only live reload prefix. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.4.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.3.0...v3.4.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.3.0...v3.4.1) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7791](https://github.com/ember-cli/ember-cli/pull/7791) Add Node 10 to support matrix [@stefanpenner](https://github.com/stefanpenner) +- [#7803](https://github.com/ember-cli/ember-cli/pull/7803) Migrate to using Travis stages [@rwjblue](https://github.com/rwjblue) +- [#7808](https://github.com/ember-cli/ember-cli/pull/7808) Drop Node 4 support [@Turbo87](https://github.com/Turbo87) +- [#7947](https://github.com/ember-cli/ember-cli/pull/7947) Add support for in repo addons in non-conventional directories [@scalvert](https://github.com/scalvert) +- [#7954](https://github.com/ember-cli/ember-cli/pull/7954) Add template linting [@rwjblue](https://github.com/rwjblue) +- [#7956](https://github.com/ember-cli/ember-cli/pull/7956) Embrace stages in CI [@rwjblue](https://github.com/rwjblue) +- [#7977](https://github.com/ember-cli/ember-cli/pull/7977) Use existing build from specified path [@SparshithNR](https://github.com/SparshithNR) +- [#7997](https://github.com/ember-cli/ember-cli/pull/7997) Update `ember-data` and `ember-source` to 3.4.0 [@btecu](https://github.com/btecu) +- [#8019](https://github.com/ember-cli/ember-cli/pull/8019) Revert "[PERF] Speed up package info cache" [@rwjblue](https://github.com/rwjblue) +- [#8013](https://github.com/ember-cli/ember-cli/pull/8013) Fix SASS compilation issues [@twokul](https://github.com/twokul) + +Thank you to all who took the time to contribute! + +## v3.4.0 + +This version was unpublished from NPM because the published tar file contained a +bug. NPM is write-only (that is you can unpublish a package but you cannot +re-publish it with the same version) and that leaves us with only one option: +publish a new version. We effectively fast-forwarding to `3.4.1`. + +## v3.4.0-beta.3 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7952](https://github.com/ember-cli/ember-cli/pull/7952) [BUGFIX release] Fix crash when tree objects are used in config [@kanongil](https://github.com/kanongil) +- [#7962](https://github.com/ember-cli/ember-cli/pull/7962) Dependency updates... [@rwjblue](https://github.com/rwjblue) +- [#7965](https://github.com/ember-cli/ember-cli/pull/7965) Don’t produce duplicate warnings when invoking findAddonByName with a… [@stefanpenner](https://github.com/ember-cli) +- [#7966](https://github.com/ember-cli/ember-cli/pull/7966) adding flag to stop repeated dumping of same package errors [@dcombslinkedin](https://github.com/dcombslinkedin) +- [#7969](https://github.com/ember-cli/ember-cli/pull/7969) Update addon.js [@stefanpenner](https://github.com/ember-cli) +- [#7970](https://github.com/ember-cli/ember-cli/pull/7970) Fix findAddonByName in beta. [@rwjblue](https://github.com/rwjblue) +- [#7977](https://github.com/ember-cli/ember-cli/pull/7977) Use existing build from specified path, --path option added [@SparshithNR](https://github.com/SparshithNR) +- [#7979](https://github.com/ember-cli/ember-cli/pull/7979) Stabilize + Lock-down add-on discovery order. [@stefanpenner](https://github.com/ember-cli) +- [#7983](https://github.com/ember-cli/ember-cli/pull/7983) Remove unneeded logging. [@stefanpenner](https://github.com/ember-cli) + +Thank you to all who took the time to contribute! + +## v3.4.0-beta.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.4.0-beta.1...v3.4.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.4.0-beta.1...v3.4.0-beta.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7902](https://github.com/ember-cli/ember-cli/pull/7902) add .eslintignore to .npmignore [@kellyselden](https://github.com/kellyselden) +- [#7904](https://github.com/ember-cli/ember-cli/pull/7904) further unify all our `.*ignore` files [@kellyselden](https://github.com/kellyselden) +- [#7905](https://github.com/ember-cli/ember-cli/pull/7905) Fix release branch tests [@kellyselden](https://github.com/kellyselden) +- [#7917](https://github.com/ember-cli/ember-cli/pull/7917) Adding entries for the package-info-cache work to the contribution list [@dcombslinkedin](https://github.com/dcombslinkedin) +- [#7940](https://github.com/ember-cli/ember-cli/pull/7940) Enable LiveReload and normal app server to share a single port. [@SparshithNR](https://github.com/SparshithNR) +- [#7947](https://github.com/ember-cli/ember-cli/pull/7947) Adding support for in repo addons in non-conventional directories [@scalvert](https://github.com/scalvert) +- [#7949](https://github.com/ember-cli/ember-cli/pull/7949) Upgrade blueprint dependency versions [@stefanpenner](https://github.com/stefanpenner) +- [#7950](https://github.com/ember-cli/ember-cli/pull/7950) Better handle ambiguity between package.json and index.js name field. [@rwjblue](https://github.com/rwjblue) + - Changes the addon blueprint to ensure name property just references the package.json's name + - Introduce a hard error when locally developing an addon whose `package.json` name differs from `index.js` name. + - Updates `Project.prototype.findAddonByName` / `Addon.prototype.findAddonByName` to handle the changes to mentioned above. Specifically, `findAddonByName` will prefer exact matches to the `package.json` name over exact matches in `index.js` name. +- [#7954](https://github.com/ember-cli/ember-cli/pull/7954) Add template linting. [@rwjblue](https://github.com/rwjblue) +- [#7956](https://github.com/ember-cli/ember-cli/pull/7956) Embrace stages in CI. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## v3.4.0-beta.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.3.0...v3.4.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.3.0...v3.4.0-beta.1) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7792](https://github.com/ember-cli/ember-cli/pull/7792) Add node 10 to test matrix [@stefanpenner](https://github.com/stefanpenner) +- [#7801](https://github.com/ember-cli/ember-cli/pull/7801) Update node support policy docs. [@rwjblue](https://github.com/rwjblue) +- [#7804](https://github.com/ember-cli/ember-cli/pull/7804) removing extra `app` folder check and error message [@stonecircle](https://github.com/stonecircle) + +Thank you to all who took the time to contribute! + +## v3.3.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.2.0...v3.3.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.2.0...v3.3.0) +- Core Contributors + + No changes required + +## v3.2.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.1.4...v3.2.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.1.4...v3.2.0) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7560](https://github.com/ember-cli/ember-cli/pull/7560) Migrate to new format for mode specific arguments in testem config. [@rwjblue](https://github.com/rwjblue) +- [#7621](https://github.com/ember-cli/ember-cli/pull/7621) Take newer symlink-or-copy [@ef4](https://github.com/ef4) +- [#7698](https://github.com/ember-cli/ember-cli/pull/7698) Update `ember-cli-qunit` dependency [@CodingItWrong](https://github.com/CodingItWrong) +- [#7729](https://github.com/ember-cli/ember-cli/pull/7729) package-info-cache (fast package caching) [@dcombslinkedin](https://github.com/dcombslinkedin) +- [#7809](https://github.com/ember-cli/ember-cli/pull/7809) Do not attempt to compress server sent events. [@rwjblue](https://github.com/rwjblue) +- [#7811](https://github.com/ember-cli/ember-cli/pull/7811) replace TRAVIS with CI [@kellyselden](https://github.com/kellyselden) +- [#7813](https://github.com/ember-cli/ember-cli/pull/7813) speed up addon initialization by using PackageInfoCache instead of Addon.lookup [@dcombslinkedin](https://github.com/dcombslinkedin) +- [#7833](https://github.com/ember-cli/ember-cli/pull/7833) blueprints/addon: Add `yarn.lock` file to `.npmignore` [@Turbo87](https://github.com/Turbo87) +- [#7836](https://github.com/ember-cli/ember-cli/pull/7836) tests: Increase timeout for linting tests [@Turbo87](https://github.com/Turbo87) +- [#7857](https://github.com/ember-cli/ember-cli/pull/7857) Filter out blacklisted addons before calling included hook [@dnachev](https://github.com/dnachev) +- [#7880](https://github.com/ember-cli/ember-cli/pull/7880) testem: Improve Chrome command line flags [@stefanpenner](https://github.com/stefanpenner) + +Thank you to all who took the time to contribute! + +## v3.2.0-beta.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.2.0-beta.1...v3.2.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.2.0-beta.1...v3.2.0-beta.2) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7792](https://github.com/ember-cli/ember-cli/pull/7792) Add node 10 to test matrix [@stefanpenner](https://github.com/stefanpenner) +- [#7801](https://github.com/ember-cli/ember-cli/pull/7801) Update node support policy docs. [@rwjblue](https://github.com/rwjblue) +- [#7804](https://github.com/ember-cli/ember-cli/pull/7804) removing extra `app` folder check and error message [@stonecircle](https://github.com/stonecircle) + +Thank you to all who took the time to contribute! + + +## v3.2.0-beta.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.1.2...v3.2.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.1.2...v3.2.0-beta.1) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7490](https://github.com/ember-cli/ember-cli/pull/7490) Module Unification Addons [@rwjblue](https://github.com/rwjblue) +- [#7605](https://github.com/ember-cli/ember-cli/pull/7605) blueprints/app: Add `qunit-dom` dependency by default [@Turbo87](https://github.com/Turbo87) +- [#7501](https://github.com/ember-cli/ember-cli/pull/7501) add delayed transpilation [@kellyselden](https://github.com/kellyselden) +- [#7634](https://github.com/ember-cli/ember-cli/pull/7634) Addon._treeFor optimization [@kellyselden](https://github.com/kellyselden) +- [#7635](https://github.com/ember-cli/ember-cli/pull/7635) Packaged Bower [@twokul](https://github.com/twokul) +- [#7637](https://github.com/ember-cli/ember-cli/pull/7637) More comprehensive detect if ember-cli is being run within CI or not. [@stefanpenner](https://github.com/ember-cli) +- [#7641](https://github.com/ember-cli/ember-cli/pull/7641) update console-ui [@kellyselden](https://github.com/kellyselden) +- [#7665](https://github.com/ember-cli/ember-cli/pull/7665) Package config [@twokul](https://github.com/twokul) +- [#7661](https://github.com/ember-cli/ember-cli/pull/7661) Double linting test timeout [@ro0gr](https://github.com/ro0gr) +- [#7659](https://github.com/ember-cli/ember-cli/pull/7659) Exclude addon-test-support from eslint node files [@kolybasov](https://github.com/kolybasov) +- [#7660](https://github.com/ember-cli/ember-cli/pull/7660) improve logic for if addon is module-unification [@iezer](https://github.com/iezer) +- [#7658](https://github.com/ember-cli/ember-cli/pull/7658) Module Unification Addon blueprint [@cibernox](https://github.com/cibernox) +- [#7654](https://github.com/ember-cli/ember-cli/pull/7654) Package Vendor [@twokul](https://github.com/twokul) +- [#7650](https://github.com/ember-cli/ember-cli/pull/7650) compile all addons at once optimization [@kellyselden](https://github.com/kellyselden) +- [#7655](https://github.com/ember-cli/ember-cli/pull/7655) Package tests [@twokul](https://github.com/twokul) +- [#7649](https://github.com/ember-cli/ember-cli/pull/7649) only use the standard compilers to compile addon code [@kellyselden](https://github.com/kellyselden) +- [#7731](https://github.com/ember-cli/ember-cli/pull/7731) restore `addon-import` logic [@GavinJoyce](https://github.com/GavinJoyce) +- [#7671](https://github.com/ember-cli/ember-cli/pull/7671) Include _super call in example of Addon.included [@jacobq](https://github.com/jacobq) +- [#7667](https://github.com/ember-cli/ember-cli/pull/7667) MU addons must generate a MU dummy app [@cibernox](https://github.com/cibernox) +- [#7662](https://github.com/ember-cli/ember-cli/pull/7662) Remove redundant checks [@twokul](https://github.com/twokul) +- [#7664](https://github.com/ember-cli/ember-cli/pull/7664) Support serving wasm with application/wasm [@stefanpenner](https://github.com/ember-cli) +- [#7668](https://github.com/ember-cli/ember-cli/pull/7668) Only watch test/dummy/app on addons if it exist [@cibernox](https://github.com/cibernox) +- [#7739](https://github.com/ember-cli/ember-cli/pull/7739) remove config caching [@GavinJoyce](https://github.com/GavinJoyce) +- [#7708](https://github.com/ember-cli/ember-cli/pull/7708) Update default broccoli-asset-rev [@ef4](https://github.com/ef4) +- [#7676](https://github.com/ember-cli/ember-cli/pull/7676) Deprecate ember-cli-babel 5.x [@raytiley](https://github.com/raytiley) +- [#7679](https://github.com/ember-cli/ember-cli/pull/7679) Update init to be src/ friendly [@mixonic](https://github.com/mixonic) +- [#7674](https://github.com/ember-cli/ember-cli/pull/7674) Package Public [@twokul](https://github.com/twokul) +- [#7678](https://github.com/ember-cli/ember-cli/pull/7678) Use a recent release of Ember canary for MU [@stefanpenner](https://github.com/ember-cli) +- [#7742](https://github.com/ember-cli/ember-cli/pull/7742) Package Javascript [@twokul](https://github.com/twokul) +- [#7702](https://github.com/ember-cli/ember-cli/pull/7702) Don't run `addon-import` blueprint if `project.isModuleUnification()` [@GavinJoyce](https://github.com/GavinJoyce) +- [#7685](https://github.com/ember-cli/ember-cli/pull/7685) fix the shims messaging [@kellyselden](https://github.com/kellyselden) +- [#7711](https://github.com/ember-cli/ember-cli/pull/7711) remove exists-sync, use fs.existsSync [@stefanpenner](https://github.com/stefanpenner) +- [#7762](https://github.com/ember-cli/ember-cli/pull/7762) Remove default value for `ember addon --yarn` [@lennyburdette](https://github.com/lennyburdette) +- [#7724](https://github.com/ember-cli/ember-cli/pull/7724) github package was renamed @octokit/rest [@kellyselden](https://github.com/kellyselden) +- [#7758](https://github.com/ember-cli/ember-cli/pull/7758) Allowing cwd to be passed to testem without being overridden by ember-cli [@arthirm](https://github.com/arthirm) +- [#7737](https://github.com/ember-cli/ember-cli/pull/7737) Fix issues identified with "delayed transpilation" system. [@rwjblue](https://github.com/rwjblue) +- [#7735](https://github.com/ember-cli/ember-cli/pull/7735) Disable MU by default [@stefanpenner](https://github.com/stefanpenner) +- [#7753](https://github.com/ember-cli/ember-cli/pull/7753) Update `qunit-dom` dependency [@Turbo87](https://github.com/Turbo87) +- [#7764](https://github.com/ember-cli/ember-cli/pull/7764) Skip running custom "*-addon" blueprint if module unification [@GavinJoyce](https://github.com/GavinJoyce) +- [#7767](https://github.com/ember-cli/ember-cli/pull/7767) Upgrade ember-load-initializers [@GavinJoyce](https://github.com/GavinJoyce) +- [#7768](https://github.com/ember-cli/ember-cli/pull/7768) Don't try to use default addon-import if app is MU [@GavinJoyce](https://github.com/GavinJoyce) +- [#7769](https://github.com/ember-cli/ember-cli/pull/7769) add blueprints to linting coverage [@kellyselden](https://github.com/kellyselden) +- [#7771](https://github.com/ember-cli/ember-cli/pull/7771) forgot .eslintignore in MU [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + + +## v3.1.4 + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.1.3...v3.1.4) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.1.3...v3.1.4) + +#### Community Contributions + +- [#7801](https://github.com/ember-cli/ember-cli/pull/7801) Update node support policy docs. [@rwjblue](https://github.com/rwjblue) +- [#7809](https://github.com/ember-cli/ember-cli/pull/7809) Do not attempt to compress server sent events. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + + +## v3.1.3 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.1.2...v3.1.3) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.1.2...v3.1.3) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7769](https://github.com/ember-cli/ember-cli/pull/7769) add blueprints to linting coverage [@kellyselden](https://github.com/kellyselden) +- [#7771](https://github.com/ember-cli/ember-cli/pull/7771) forgot .eslintignore in MU [@kellyselden](https://github.com/kellyselden) +- [#7792](https://github.com/ember-cli/ember-cli/pull/7792) Add node 10 to test matrix [@stefanpenner](https://github.com/stefanpenner) + +Thank you to all who took the time to contribute! + + +## v3.1.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.1.1...v3.1.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.1.1...v3.1.2) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7749](https://github.com/ember-cli/ember-cli/pull/7749) remove trailing comma [@kellyselden](https://github.com/kellyselden) +- [#7752](https://github.com/ember-cli/ember-cli/pull/7752) Fix test fixtures [@Turbo87](https://github.com/Turbo87) +- [#7759](https://github.com/ember-cli/ember-cli/pull/7759) Ensure css is minified correctly [@twokul](https://github.com/twokul) + +Thank you to all who took the time to contribute! + + +## v3.1.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.1.0...v3.1.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.1.0...v3.1.1) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7746](https://github.com/ember-cli/ember-cli/pull/7746) Revert "arthirm/testem-bug-fix" [@Turbo87](https://github.com/Turbo87) + + +## v3.1.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.0.3...v3.1.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.0.3...v3.1.0) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7670](https://github.com/ember-cli/ember-cli/pull/7670) Support serving wasm with application/wasm [@rwjblue](https://github.com/rwjblue) +- [#7683](https://github.com/ember-cli/ember-cli/pull/7683) Revert "EmberApp: Remove deprecated `contentFor()` hooks" [@stefanpenner](https://github.com/ember-c +li) +- [#7691](https://github.com/ember-cli/ember-cli/pull/7691) Add support for .npmrc for blueprints [@thoov](https://github.com/thoov) +- [#7694](https://github.com/ember-cli/ember-cli/pull/7694) [BACKPORT release] Ensure config() memoizing is considers if the [@stefanpenner](https://github.com/ +ember-cli) +- [#7719](https://github.com/ember-cli/ember-cli/pull/7719) reorder ember-cli-build.js in blueprint [@kellyselden](https://github.com/kellyselden) +- [#7720](https://github.com/ember-cli/ember-cli/pull/7720) assert no filters matched [@kellyselden](https://github.com/kellyselden) +- [#7721](https://github.com/ember-cli/ember-cli/pull/7721) update eslint-plugin-node for addons [@stefanpenner](https://github.com/ember-cli) +- [#7728](https://github.com/ember-cli/ember-cli/pull/7728) Passing defaultOptions to testem to prevent the cwd and config_dir set in testem.js from being ov +erridden by ember-cli [@arthirm](https://github.com/arthirm) +- [#7732](https://github.com/ember-cli/ember-cli/pull/7732) Merge pull request #7728 from arthirm/testem-bug-fix [@arthirm](https://github.com/arthirm) +- [#7736](https://github.com/ember-cli/ember-cli/pull/7736) add addon-test-support/index.js to eslint glob bug mitigation [@kellyselden](https://github.com/k +ellyselden) + +Thank you to all who took the time to contribute! + + +## v3.0.4 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v3.0.3...v3.0.4) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v3.0.3...v3.0.4) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7746](https://github.com/ember-cli/ember-cli/pull/7746) Revert "arthirm/testem-bug-fix" [@Turbo87](https://github.com/Turbo87) + + +## v3.0.3 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7719](https://github.com/ember-cli/ember-cli/pull/7719) reorder ember-cli-build.js in blueprint [@kellyselden](https://github.com/kellyselden) +- [#7720](https://github.com/ember-cli/ember-cli/pull/7720) assert no filters matched [@kellyselden](https://github.com/kellyselden) +- [#7721](https://github.com/ember-cli/ember-cli/pull/7721) update eslint-plugin-node for addons [@stefanpenner](https://github.com/ember-cli) +- [#7728](https://github.com/ember-cli/ember-cli/pull/7728) Passing defaultOptions to testem to prevent the cwd and config_dir set in testem.js from being overridden by ember-cli [@arthirm](https://github.com/arthirm) +- [#7736](https://github.com/ember-cli/ember-cli/pull/7736) add addon-test-support/index.js to eslint glob bug mitigation [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + +## v3.0.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7691](https://github.com/ember-cli/ember-cli/pull/7691) Add support for .npmrc for blueprints [@thoov](https://github.com/thoov) +- [#7694](https://github.com/ember-cli/ember-cli/pull/7694) [BACKPORT release] Ensure config() memoizing is considers if the [@stefanpenner](https://github.com/ember-cli) + +Thank you to all who took the time to contribute! + +## v3.0.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7626](https://github.com/ember-cli/ember-cli/pull/7626) Fix 'const' declarations in non-strict mode are not supported [@Turbo87](https://github.com/Turbo87) +- [#7627](https://github.com/ember-cli/ember-cli/pull/7627) Fixing test fixtures from 3.0.0 release [@Turbo87](https://github.com/Turbo87) +- [#7670](https://github.com/ember-cli/ember-cli/pull/7670) Support serving wasm with application/wasm [@rwjblue](https://github.com/rwjblue) +- [#7683](https://github.com/ember-cli/ember-cli/pull/7683) Revert "EmberApp: Remove deprecated `contentFor()` hooks" [@stefanpenner](https://github.com/ember-cli) + +Thank you to all who took the time to contribute! + +## v3.0.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.18.2...v3.0.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.18.2...v3.0.0) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7566](https://github.com/ember-cli/ember-cli/pull/7566) testem: Use `--no-sandbox` on TravisCI [@Turbo87](https://github.com/Turbo87) +- [#7569](https://github.com/ember-cli/ember-cli/pull/7569) mark "lib" folder as node style in eslint for apps [@kellyselden](https://github.com/kellyselden) +- [#7589](https://github.com/ember-cli/ember-cli/pull/7589) [BACKPORT release] upgrade testem [@stefanpenner](https://github.com/ember-cli) +- [#7594](https://github.com/ember-cli/ember-cli/pull/7594) Install optional dependencies when creating a new project [@stefanpenner](https://github.com/ember-cli) +- [#7610](https://github.com/ember-cli/ember-cli/pull/7610) Change isMainVendorFile check [@twokul](https://github.com/twokul/twokul) +- [#7447](https://github.com/ember-cli/ember-cli/pull/7447) Remove ember-cli-legacy-blueprints. [@rwjblue](https://github.com/rwjblue) +- [#7528](https://github.com/ember-cli/ember-cli/pull/7528) EmberApp: Overwrite `app/config/environment` in `tests.js` [@Turbo87](https://github.com/Turbo87) +- [#7536](https://github.com/ember-cli/ember-cli/pull/7536) Avoid bower usage in config/ember-try.js. [@rwjblue](https://github.com/rwjblue) +- [#7546](https://github.com/ember-cli/ember-cli/pull/7546) Remove unused testing helper files. [@rwjblue](https://github.com/rwjblue) +- [#7548](https://github.com/ember-cli/ember-cli/pull/7548) Make async/await work nicely by default. [@rwjblue](https://github.com/rwjblue) +- [#7549](https://github.com/ember-cli/ember-cli/pull/7549) Use `sudo: required` to work around issue in CI. [@rwjblue](https://github.com/rwjblue) +- [#7553](https://github.com/ember-cli/ember-cli/pull/7553) Remove embertest from ESLint configuration. [@rwjblue](https://github.com/rwjblue) +- [#7554](https://github.com/ember-cli/ember-cli/pull/7554) Make ember-try a direct addon dependency. [@rwjblue](https://github.com/rwjblue) +- [#7522](https://github.com/ember-cli/ember-cli/pull/7522) utilities: Remove deprecated `deprecateUI()` function [@Turbo87](https://github.com/Turbo87) +- [#7502](https://github.com/ember-cli/ember-cli/pull/7502) Cleanup and correct node-support.md. [@stefanpenner](https://github.com/ember-cli) +- [#7487](https://github.com/ember-cli/ember-cli/pull/7487) [BUGFIX] give `ember new` error messages consistent color [@GavinJoyce](https://github.com/GavinJoyce) +- [#7479](https://github.com/ember-cli/ember-cli/pull/7479) Improve default addon README [@Turbo87](https://github.com/Turbo87) +- [#7512](https://github.com/ember-cli/ember-cli/pull/7512) fix alpha ordering in npmignore [@stefanpenner](https://github.com/ember-cli) +- [#7520](https://github.com/ember-cli/ember-cli/pull/7520) Remove deprecated commands [@Turbo87](https://github.com/Turbo87) +- [#7523](https://github.com/ember-cli/ember-cli/pull/7523) Remove deprecated code from `EmberApp` class [@Turbo87](https://github.com/Turbo87) +- [#7524](https://github.com/ember-cli/ember-cli/pull/7524) Remove deprecated `Blueprint` code [@Turbo87](https://github.com/Turbo87) +- [#7525](https://github.com/ember-cli/ember-cli/pull/7525) Remove deprecated `Project` and `Addon` code [@Turbo87](https://github.com/Turbo87) +- [#7527](https://github.com/ember-cli/ember-cli/pull/7527) EmberApp: Remove deprecated `contentFor()` hooks [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +## v2.18.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.18.1...v2.18.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.18.1...v2.18.2) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7569](https://github.com/ember-cli/ember-cli/pull/7569) mark "lib" folder as node style in eslint for apps [@kellyselden](https://github.com/kellyselden) +- [#7589](https://github.com/ember-cli/ember-cli/pull/7589) upgrade testem [@stefanpenner](https://github.com/stefanpenner) +- [#7594](https://github.com/ember-cli/ember-cli/pull/7594) Install optional dependencies when creating a new project [@tomdale](https://github.com/tomdale) + +Thank you to all who took the time to contribute! + + +## v2.18.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.18.0...v2.18.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.18.0...v2.18.1) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7566](https://github.com/ember-cli/ember-cli/pull/7566) testem: Use `--no-sandbox` on TravisCI [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +## v2.18.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.17.2...v2.18.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.17.2...v2.18.0) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7489](https://github.com/ember-cli/ember-cli/pull/7489) Fix regression with scoped package name mismatches [@rwwagner90](https://github.com/rwwagner90) +- [#7507](https://github.com/ember-cli/ember-cli/pull/7507) Ensure testing honors config/environment settings. [@rwjblue](https://github.com/rwjblue) +- [#7513](https://github.com/ember-cli/ember-cli/pull/7513) fix alpha ordering in npmignore [@kellyselden](https://github.com/kellyselden) +- [#7516](https://github.com/ember-cli/ember-cli/pull/7516) Fix `ember new --yarn` not using yarn [@Turbo87](https://github.com/Turbo87) +- [#7529](https://github.com/ember-cli/ember-cli/pull/7529) Backport & fixup linting changes. [@rwjblue](https://github.com/rwjblue) +- [#7474](https://github.com/ember-cli/ember-cli/pull/7474) Give plugins and extends their own lines [@rwwagner90](https://github.com/rwwagner90) +- [#7475](https://github.com/ember-cli/ember-cli/pull/7475) don't treat strings as regex in insertIntoFile [@kellyselden](https://github.com/kellyselden) +- [#7477](https://github.com/ember-cli/ember-cli/pull/7477) Restore `separator: '\n;'` to vendor JS concat [@kellyselden/lenny](https://github.com/kellyselden/lenny) +- [#7478](https://github.com/ember-cli/ember-cli/pull/7478) Remove obsolete CONFIG_CACHING feature flag [@Turbo87](https://github.com/Turbo87) +- [#7481](https://github.com/ember-cli/ember-cli/pull/7481) NpmInstallTask: `useYarn` from constructor args [@lennyburdette](https://github.com/lennyburdette) +- [#7395](https://github.com/ember-cli/ember-cli/pull/7395) Make "testdouble" dependency optional in MockProcess class [@ro0gr](https://github.com/ro0gr) +- [#7382](https://github.com/ember-cli/ember-cli/pull/7382) add option to not create file [@kellyselden](https://github.com/kellyselden) +- [#7385](https://github.com/ember-cli/ember-cli/pull/7385) remove node 7 testing [@kellyselden](https://github.com/kellyselden) +- [#6955](https://github.com/ember-cli/ember-cli/pull/6955) Discover dependencies of npm-linked addons [@ef4](https://github.com/ef4) +- [#7164](https://github.com/ember-cli/ember-cli/pull/7164) Fix generate command when both usePods option and --pod argument is used [@emrekutlu](https://github.com/emrekutlu) +- [#7428](https://github.com/ember-cli/ember-cli/pull/7428) Fix bad recursion in ember-cli-shims detection [@cibernox](https://github.com/cibernox) +- [#7419](https://github.com/ember-cli/ember-cli/pull/7419) Delete crossdomain.xml [@sandstrom](https://github.com/sandstrom) +- [#7424](https://github.com/ember-cli/ember-cli/pull/7424) Adding documentation for experiments [@sangm](https://github.com/sangm) +- [#7414](https://github.com/ember-cli/ember-cli/pull/7414) Fixes Project#hasDependencies to only check for dependencies instead … [@MiguelMadero/mmadero](https://github.com/MiguelMadero/mmadero) +- [#7406](https://github.com/ember-cli/ember-cli/pull/7406) Remove livereload url from output [@topaxi](https://github.com/topaxi) +- [#7401](https://github.com/ember-cli/ember-cli/pull/7401) Resolve node modules correctly [@ef4](https://github.com/ef4) +- [#7443](https://github.com/ember-cli/ember-cli/pull/7443) Use `overrides` for a single `.eslintrc.js`. [@rwjblue](https://github.com/rwjblue) +- [#7435](https://github.com/ember-cli/ember-cli/pull/7435) add ember-try ignores to npmignore [@kellyselden](https://github.com/kellyselden) +- [#7432](https://github.com/ember-cli/ember-cli/pull/7432) Avoid publishing massive temp folder leaked by ember-try [@ef4](https://github.com/ef4) +- [#7438](https://github.com/ember-cli/ember-cli/pull/7438) skip uninstall if no matching package is installed [@makepanic](https://github.com/makepanic) +- [#7455](https://github.com/ember-cli/ember-cli/pull/7455) Add eslint-plugin-ember to default linting config. [@rwjblue](https://github.com/rwjblue) +- [#7456](https://github.com/ember-cli/ember-cli/pull/7456) Use fs-extra's `ensureDir` to avoid race condition in `mk-tmp-dir-in`. [@rwjblue](https://github.com/rwjblue) +- [#7457](https://github.com/ember-cli/ember-cli/pull/7457) Avoid directly requiring `blueprints/app/files/package.json`. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + + +## 2.17.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.17.1...v2.17.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.17.1...v2.17.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7489](https://github.com/ember-cli/ember-cli/pull/7489) Fix regression with scoped package name mismatches [@rwwagner90](https://github.com/rwwagner90) +- [#7507](https://github.com/ember-cli/ember-cli/pull/7507) Ensure testing honors config/environment settings. [@rwjblue](https://github.com/rwjblue) +- [#7513](https://github.com/ember-cli/ember-cli/pull/7513) fix alpha ordering in npmignore [@kellyselden](https://github.com/kellyselden) +- [#7516](https://github.com/ember-cli/ember-cli/pull/7516) Fix `ember new --yarn` not using yarn [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +## 2.17.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.17.0...v2.17.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.17.0...v2.17.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7475](https://github.com/ember-cli/ember-cli/pull/7475) don't treat strings as regex in insertIntoFile [@kellyselden](https://github.com/kellyselden) +- [#7477](https://github.com/ember-cli/ember-cli/pull/7477) Restore `separator: '\n;'` to vendor JS concat [@lennyburdette](https://github.com/lennyburdette) +- [#7478](https://github.com/ember-cli/ember-cli/pull/7478) Remove obsolete CONFIG_CACHING feature flag [@Turbo87](https://github.com/Turbo87) +- [#7481](https://github.com/ember-cli/ember-cli/pull/7481) NpmInstallTask: `useYarn` from constructor args [@lennyburdette](https://github.com/lennyburdette) + +Thank you to all who took the time to contribute! + + +## 2.17.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.16.2...v2.17.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.16.2...v2.17.0) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7232](https://github.com/ember-cli/ember-cli/pull/7232) don't compress responses with the x-no-compression response header [@akatov](https://github.com/akatov) +- [#7272](https://github.com/ember-cli/ember-cli/pull/7272) Fixes `undefined` values in merged aliases [@twokul](https://github.com/twokul) +- [#7342](https://github.com/ember-cli/ember-cli/pull/7342) Updating testem.js for the app blueprint [@scalvert](https://github.com/scalvert) +- [#7360](https://github.com/ember-cli/ember-cli/pull/7360) "server" -> "serve" in package.json blueprint [@kellyselden](https://github.com/kellyselden) +- [#7353](https://github.com/ember-cli/ember-cli/pull/7353) remove ember-cli-shim warning [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#7369](https://github.com/ember-cli/ember-cli/pull/7369) Fix issue with linting within an addon without an `app/` directory. [@rwjblue](https://github.com/rwjblue) +- [#7372](https://github.com/ember-cli/ember-cli/pull/7372) Fix travis.yml in addon blueprint [@simonihmig](https://github.com/simonihmig) +- [#7377](https://github.com/ember-cli/ember-cli/pull/7377) Invoke transform registeration before included hook is called. [@kratiahuja](https://github.com/kratiahuja) +- [#7378](https://github.com/ember-cli/ember-cli/pull/7378) Ensure test-support and addon-test-support are linted. [@rwjblue](https://github.com/rwjblue) +- [#7381](https://github.com/ember-cli/ember-cli/pull/7381) Changes default Chrome remote debugging port. [@Oreoz](https://github.com/Oreoz) +- [#7409](https://github.com/ember-cli/ember-cli/pull/7409) cherry pick #7382 into beta [@kellyselden](https://github.com/kellyselden) +- [#7416](https://github.com/ember-cli/ember-cli/pull/7416) Add support for Node 9. [@rwjblue](https://github.com/rwjblue) +- [#7417](https://github.com/ember-cli/ember-cli/pull/7417) Issue warning for Node 7. [@rwjblue](https://github.com/rwjblue) +- [#7427](https://github.com/ember-cli/ember-cli/pull/7427) Remove emoji alias [@tristanpemble](https://github.com/tristanpemble) +- [#7430](https://github.com/ember-cli/ember-cli/pull/7430) ember-try: Add `useYarn` flag if necessary [@Turbo87](https://github.com/Turbo87) +- [#7436](https://github.com/ember-cli/ember-cli/pull/7436) retire 2.8, introduce 2.16 [@kellyselden](https://github.com/kellyselden) +- [#7437](https://github.com/ember-cli/ember-cli/pull/7437) Update to ember-cli-qunit@4.1.1. [@rwjblue](https://github.com/rwjblue) +- [#7439](https://github.com/ember-cli/ember-cli/pull/7439) Cherry pick #7432 and #7435 to release [@kellyselden](https://github.com/kellyselden) +- [#7449](https://github.com/ember-cli/ember-cli/pull/7449) Update `ember-cli-shims` to v1.2.0 [@Turbo87](https://github.com/Turbo87) +- [#7460](https://github.com/ember-cli/ember-cli/pull/7460) remove trailing comma from testem.js [@stefanpenner](https://github.com/ember-cli) + +Thank you to all who took the time to contribute! + + +## 2.17.0-beta.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.17.0-beta.1...v2.17.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.17.0-beta.1...v2.17.0-beta.2) +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7353](https://github.com/ember-cli/ember-cli/pull/7353) remove ember-cli-shim warning [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [#7369](https://github.com/ember-cli/ember-cli/pull/7369) Fix issue with linting within an addon without an `app/` directory. [@rwjblue](https://github.com/rwjblue) +- [#7372](https://github.com/ember-cli/ember-cli/pull/7372) Fix travis.yml in addon blueprint [@simonihmig](https://github.com/simonihmig) +- [#7377](https://github.com/ember-cli/ember-cli/pull/7377) Invoke transform registeration before included hook is called. [@kratiahuja](https://github.com/kratiahuja) +- [#7378](https://github.com/ember-cli/ember-cli/pull/7378) Ensure test-support and addon-test-support are linted. [@rwjblue](https://github.com/rwjblue) +- [#7381](https://github.com/ember-cli/ember-cli/pull/7381) Changes default Chrome remote debugging port. [@Oreoz](https://github.com/Oreoz) +- [#7409](https://github.com/ember-cli/ember-cli/pull/7409) cherry pick #7382 into beta [@kellyselden](https://github.com/kellyselden) +- [#7416](https://github.com/ember-cli/ember-cli/pull/7416) Add support for Node 9. [@rwjblue](https://github.com/rwjblue) +- [#7417](https://github.com/ember-cli/ember-cli/pull/7417) Issue warning for Node 7. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + + +## 2.17.0-beta.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.16.0...v2.17.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.16.0...v2.17.0-beta.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7344](https://github.com/ember-cli/ember-cli/pull/7344) Update mocha to the latest version 🚀 [@stefanpenner/greenkeeper](https://github.com/ember-cli/greenkeeper) +- [#7322](https://github.com/ember-cli/ember-cli/pull/7322) [INTERNAL] Update NPM to npm [@kimroen](https://github.com/kimroen) +- [#7232](https://github.com/ember-cli/ember-cli/pull/7232) ENHANCEMENT - don't compress responses with the x-no-compression response header [@akatov](https://github.com/akatov) +- [#7272](https://github.com/ember-cli/ember-cli/pull/7272) Fixes `undefined` values in merged aliases [@twokul](https://github.com/twokul) +- [#7317](https://github.com/ember-cli/ember-cli/pull/7317) [INTERNAL] Introduce `broccoli-assembler` [@twokul](https://github.com/twokul) +- [#7338](https://github.com/ember-cli/ember-cli/pull/7338) [INTERNAL] Port test helpers to class syntax [@twokul](https://github.com/twokul) +- [#7340](https://github.com/ember-cli/ember-cli/pull/7340) bump `rsvp` [@bekzod](https://github.com/bekzod) +- [#7342](https://github.com/ember-cli/ember-cli/pull/7342) Updating testem.js for the app blueprint [@scalvert](https://github.com/scalvert) +- [#7345](https://github.com/ember-cli/ember-cli/pull/7345) correct `rsvp` version in yarn.lock [@bekzod](https://github.com/bekzod) +- [#7360](https://github.com/ember-cli/ember-cli/pull/7360) "server" -> "serve" in package.json blueprint [@kellyselden](https://github.com/kellyselden) +- [#7363](https://github.com/ember-cli/ember-cli/pull/7363) Update to ember-cli-qunit@4.1.0-beta.1. [@rwjblue](https://github.com/rwjblue) +- [#7367](https://github.com/ember-cli/ember-cli/pull/7367) Bump ember-source to 2.17.0-beta.1. [@rwjblue](https://github.com/rwjblue) + + +Thank you to all who took the time to contribute! + +## 2.16.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.16.1...v2.16.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.16.1...v2.16.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7372](https://github.com/ember-cli/ember-cli/pull/7372) [BUGFIX] Fix travis.yml in addon blueprint [@simonihmig](https://github.com/simonihmig) +- [#7377](https://github.com/ember-cli/ember-cli/pull/7377) [BUGFIX] Invoke transform registeration before included hook is called. [@kratiahuja](https://github.com/kratiahuja) + +Thank you to all who took the time to contribute! + +## 2.16.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.16.0...v2.16.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.16.0...v2.16.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7369](https://github.com/ember-cli/ember-cli/pull/7369) Fix issue with linting within an addon without an `app/` directory. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! + +## 2.16.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.15.1...v2.16.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.15.1...v2.16.0) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7341](https://github.com/ember-cli/ember-cli/pull/7341) tasks/npm-task: Adjust version constraints to not warn for npm@5 [@Turbo87](https://github.com/Turbo87) +- [#7346](https://github.com/ember-cli/ember-cli/pull/7346) Use "ci" mode for testem.js [@Turbo87](https://github.com/Turbo87) +- [#7348](https://github.com/ember-cli/ember-cli/pull/7348) Update "ember-cli-uglify" to v2.0.0 [@Turbo87](https://github.com/Turbo87) +- [#7349](https://github.com/ember-cli/ember-cli/pull/7349) Limit allowed concurrency in CI environments. [@rwjblue](https://github.com/rwjblue) +- [#7361](https://github.com/ember-cli/ember-cli/pull/7361) Update "ember-data" to v2.16.2 [@Turbo87](https://github.com/Turbo87) +- [#7364](https://github.com/ember-cli/ember-cli/pull/7364) preserve final newline in addon's package.json [@kellyselden](https://github.com/kellyselden) +- [#7316](https://github.com/ember-cli/ember-cli/pull/7316) Bump blueprint to Ember Data 2.15.0 [@locks](https://github.com/locks) +- [#7320](https://github.com/ember-cli/ember-cli/pull/7320) move "private" key in package.json [@kellyselden](https://github.com/kellyselden) +- [#7333](https://github.com/ember-cli/ember-cli/pull/7333) models/project: Use deep cloning instead of freezing the config [@Turbo87](https://github.com/Turbo87) +- [#7178](https://github.com/ember-cli/ember-cli/pull/7178) readme npm/yarn updates with tests [@kellyselden](https://github.com/kellyselden) +- [#6908](https://github.com/ember-cli/ember-cli/pull/6908) Several fixes for the module unification feature flag [@mixonic](https://github.com/mixonic) +- [#7137](https://github.com/ember-cli/ember-cli/pull/7137) add in-app testing page reference [@kellyselden](https://github.com/kellyselden) +- [#7086](https://github.com/ember-cli/ember-cli/pull/7086) Mock process [@ro0gr](https://github.com/ro0gr) +- [#7108](https://github.com/ember-cli/ember-cli/pull/7108) remove unnecessary `.push` [@bekzod](https://github.com/bekzod) +- [#7033](https://github.com/ember-cli/ember-cli/pull/7033) Ensure addon blueprint calls `this.filesPath` [@status200](https://github.com/status200) +- [#7109](https://github.com/ember-cli/ember-cli/pull/7109) Fix `ember install` for scoped packages [@ef4](https://github.com/ef4) +- [#6963](https://github.com/ember-cli/ember-cli/pull/6963) Preserve header key case when serving with proxy [@jpadilla](https://github.com/jpadilla) +- [#7119](https://github.com/ember-cli/ember-cli/pull/7119) added `app` directory for linting [@bekzod](https://github.com/bekzod) +- [#7074](https://github.com/ember-cli/ember-cli/pull/7074) Fix eslint warning on generated config/environment.js [@morhook](https://github.com/morhook) +- [#7065](https://github.com/ember-cli/ember-cli/pull/7065) Set the basePort for livereload from 49153 -> 7020 [@eriktrom](https://github.com/eriktrom) +- [#7239](https://github.com/ember-cli/ember-cli/pull/7239) Remove private `_mergeTrees` function [@twokul](https://github.com/twokul) +- [#7221](https://github.com/ember-cli/ember-cli/pull/7221) Bumps `broccoli-builder` version to include stack traces fix [@twokul](https://github.com/twokul) +- [#7233](https://github.com/ember-cli/ember-cli/pull/7233) Convert blueprints to use modules and bump ember-cli-babel [@rwwagner90](https://github.com/rwwagner90) +- [#7235](https://github.com/ember-cli/ember-cli/pull/7235) bump `ember-cli-lodash-subset` [@bekzod](https://github.com/bekzod) +- [#7227](https://github.com/ember-cli/ember-cli/pull/7227) Don't merge `emberCLITree` twice [@twokul](https://github.com/twokul) +- [#7294](https://github.com/ember-cli/ember-cli/pull/7294) Fix --test-port description [@akashdsouza](https://github.com/akashdsouza) +- [#7259](https://github.com/ember-cli/ember-cli/pull/7259) Convert MarkdownColor to class syntax [@locks](https://github.com/locks) +- [#7244](https://github.com/ember-cli/ember-cli/pull/7244) Using shorthands for functions [@twokul](https://github.com/twokul) +- [#7248](https://github.com/ember-cli/ember-cli/pull/7248) Bump `amd-name-resolver` version to enable parallel babel transpile [@mikrostew](https://github.com/mikrostew) +- [#7245](https://github.com/ember-cli/ember-cli/pull/7245) Add API to allow addons to define and use custom transform with app.import [@kratiahuja](https://github.com/kratiahuja) +- [#7266](https://github.com/ember-cli/ember-cli/pull/7266) Fix JSON format of asset sizes report [@simplabs](https://github.com/simplabs) +- [#7264](https://github.com/ember-cli/ember-cli/pull/7264) Introduces Bundler [@twokul](https://github.com/twokul) +- [#7262](https://github.com/ember-cli/ember-cli/pull/7262) Convert to classes [@twokul](https://github.com/twokul) +- [#7261](https://github.com/ember-cli/ember-cli/pull/7261) double test timeout for install-test-slow [@ro0gr](https://github.com/ro0gr) +- [#7275](https://github.com/ember-cli/ember-cli/pull/7275) Allow server middleware to answer non-get (POST/PATCH...) requests [@cibernox](https://github.com/cibernox) +- [#7269](https://github.com/ember-cli/ember-cli/pull/7269) Extract vendor generation into bundler [@twokul](https://github.com/twokul) +- [#7292](https://github.com/ember-cli/ember-cli/pull/7292) Add Documentation Link and Supported Versions [@CrshOverride](https://github.com/CrshOverride) +- [#7296](https://github.com/ember-cli/ember-cli/pull/7296) Drop un-needed Ember import [@mixonic](https://github.com/mixonic) +- [#7300](https://github.com/ember-cli/ember-cli/pull/7300) Refactor Custom Transformation logic [@sangm](https://github.com/sangm) +- [#7303](https://github.com/ember-cli/ember-cli/pull/7303) Introduces a way to debug application/add-on trees [@twokul](https://github.com/twokul) +- [#7314](https://github.com/ember-cli/ember-cli/pull/7314) Removes babel module transform [@twokul](https://github.com/twokul) +- [#7315](https://github.com/ember-cli/ember-cli/pull/7315) fix image uri [@xg-wang](https://github.com/xg-wang) + +Thank you to all who took the time to contribute! + + +## 2.15.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.15.0...v2.15.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.15.0...v2.15.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7316](https://github.com/ember-cli/ember-cli/pull/7316) Bump blueprint to Ember Data 2.15.0 [@locks](https://github.com/locks) +- [#7320](https://github.com/ember-cli/ember-cli/pull/7320) move "private" key in package.json [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + + +## 2.15.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.14.2...v2.15.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.14.2...v2.15.0) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7286](https://github.com/ember-cli/ember-cli/pull/7286) Update `amd-name-resolver` version to enable parallel babel transpile [@mikrostew](https://github.com/mikrostew) +- [#7309](https://github.com/ember-cli/ember-cli/pull/7309) model/project: Freeze app config before caching it [@Turbo87](https://github.com/Turbo87) +- [#7310](https://github.com/ember-cli/ember-cli/pull/7310) models/project: Hide app config caching behind CONFIG_CACHING feature flag [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +## 2.15.0-beta.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.15.0-beta.1...v2.15.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.15.0-beta.1...v2.15.0-beta.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7210](https://github.com/ember-cli/ember-cli/pull/7210) Ember try remove test [@kellyselden](https://github.com/kellyselden) +- [#7186](https://github.com/ember-cli/ember-cli/pull/7186) node 8 [@stefanpenner](https://github.com/stefanpenner) +- [#7136](https://github.com/ember-cli/ember-cli/pull/7136) ember test work with both —server and —path [@stefanpenner](https://github.com/stefanpenner) +- [#7224](https://github.com/ember-cli/ember-cli/pull/7224) context issue fix [@bekzod](https://github.com/bekzod) +- [#7206](https://github.com/ember-cli/ember-cli/pull/7206) release] remove MODEL_FACTORY_INJECTIONS [@kellyselden](https://github.com/kellyselden) +- [#7205](https://github.com/ember-cli/ember-cli/pull/7205) release] 2 12 lts testing [@kellyselden](https://github.com/kellyselden) +- [#7193](https://github.com/ember-cli/ember-cli/pull/7193) cherry pick "install npm 4 in addon travis using npm" [@kellyselden](https://github.com/kellyselden) +- [#7194](https://github.com/ember-cli/ember-cli/pull/7194) stay in sync with editorconfig and other blueprints regarding newlines [@kellyselden](https://github.com/kellyselden) +- [#7204](https://github.com/ember-cli/ember-cli/pull/7204) release] explain node 4 in addons [@kellyselden](https://github.com/kellyselden) +- [#7208](https://github.com/ember-cli/ember-cli/pull/7208) Fixes typo in babel transpilation options [@pzuraq/bugfix](https://github.com/pzuraq/bugfix) +- [#7231](https://github.com/ember-cli/ember-cli/pull/7231) Don't merge `emberCLITree` twice [@twokul](https://github.com/twokul) +- [#7246](https://github.com/ember-cli/ember-cli/pull/7246) cherry-pick "Bumps `broccoli-builder` version to include stack traces fix [@twokul](https://github.com/twokul) +- [#7270](https://github.com/ember-cli/ember-cli/pull/7270) Cache Project model config. [@stefanpenner](https://github.com/ember-cli) +- [#7273](https://github.com/ember-cli/ember-cli/pull/7273) Asset sizes [@simplabs](https://github.com/simplabs) + +Thank you to all who took the time to contribute! + + +## 2.15.0-beta.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.14.0...v2.15.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.14.0...v2.15.0-beta.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#6988](https://github.com/ember-cli/ember-cli/pull/6988) update addon lts testing [@kellyselden](https://github.com/kellyselden) +- [#7132](https://github.com/ember-cli/ember-cli/pull/7132) Bump ember-cli-eslint [@rwwagner90](https://github.com/rwwagner90) +- [#7026](https://github.com/ember-cli/ember-cli/pull/7026) explain node 4 in addons [@kellyselden](https://github.com/kellyselden) +- [#7002](https://github.com/ember-cli/ember-cli/pull/7002) update from npm 2 when using node 4 [@kellyselden](https://github.com/kellyselden) +- [#7014](https://github.com/ember-cli/ember-cli/pull/7014) fixup #6941 [@stefanpenner](https://github.com/stefanpenner) +- [#7025](https://github.com/ember-cli/ember-cli/pull/7025) remove MODEL_FACTORY_INJECTIONS [@stefanpenner](https://github.com/stefanpenner) +- [#7003](https://github.com/ember-cli/ember-cli/pull/7003) Allow node 7.x on Windows [@btecu](https://github.com/btecu) +- [#7090](https://github.com/ember-cli/ember-cli/pull/7090) Documentation around error propagation & version bumps [@twokul](https://github.com/twokul) +- [#7048](https://github.com/ember-cli/ember-cli/pull/7048) Update yarn.lock with latest allowed dependencies. [@rwjblue](https://github.com/rwjblue) +- [#7046](https://github.com/ember-cli/ember-cli/pull/7046) Pass only packages to npm uninstall task that exist [@raido](https://github.com/raido) +- [#7041](https://github.com/ember-cli/ember-cli/pull/7041) Revert rawMode to original value during windows signals cleanup [@ro0gr](https://github.com/ro0gr) +- [#7045](https://github.com/ember-cli/ember-cli/pull/7045) Make app.import() work with files inside `node_modules` [@Turbo87](https://github.com/Turbo87) +- [#7032](https://github.com/ember-cli/ember-cli/pull/7032) BUGFIX Corrected a typo in Windows elevation test error message. [@jpschober](https://github.com/jpschober) +- [#7068](https://github.com/ember-cli/ember-cli/pull/7068) Remove reference to "lib/ext/promise" from docs [@ro0gr](https://github.com/ro0gr) +- [#7057](https://github.com/ember-cli/ember-cli/pull/7057) Use https in references to emberjs website [@ahmadsoe](https://github.com/ahmadsoe) +- [#7056](https://github.com/ember-cli/ember-cli/pull/7056) fix_typos [@fixTypos](https://github.com/fixTypos) +- [#7064](https://github.com/ember-cli/ember-cli/pull/7064) remove the implied npm install and test from travis [@kellyselden](https://github.com/kellyselden) +- [#7054](https://github.com/ember-cli/ember-cli/pull/7054) Allow imports from scoped packages [@dfreeman](https://github.com/dfreeman) +- [#7150](https://github.com/ember-cli/ember-cli/pull/7150) fix typo [@stefanpenner](https://github.com/stefanpenner) +- [#7102](https://github.com/ember-cli/ember-cli/pull/7102) use `.test` instead of `.match` when appropriate [@bekzod](https://github.com/bekzod) +- [#7095](https://github.com/ember-cli/ember-cli/pull/7095) loggers `let` => `const` [@bekzod](https://github.com/bekzod) +- [#7084](https://github.com/ember-cli/ember-cli/pull/7084) change var to let in ARCHITECTURE.md [@ro0gr](https://github.com/ro0gr) +- [#7100](https://github.com/ember-cli/ember-cli/pull/7100) use native `Object.assign` [@bekzod](https://github.com/bekzod) +- [#7101](https://github.com/ember-cli/ember-cli/pull/7101) use `.reduce` in `addonPackages` [@bekzod](https://github.com/bekzod) +- [#7094](https://github.com/ember-cli/ember-cli/pull/7094) concat instead of `unshift each` in `_processedExternalTree` [@bekzod](https://github.com/bekzod) +- [#7080](https://github.com/ember-cli/ember-cli/pull/7080) Node support doc [@stefanpenner](https://github.com/stefanpenner) +- [#7099](https://github.com/ember-cli/ember-cli/pull/7099) use native `[].any` and `Object.keys` [@bekzod](https://github.com/bekzod) +- [#7096](https://github.com/ember-cli/ember-cli/pull/7096) removed redundant `self` references [@bekzod](https://github.com/bekzod) +- [#7081](https://github.com/ember-cli/ember-cli/pull/7081) update deps [@stefanpenner](https://github.com/stefanpenner) +- [#7093](https://github.com/ember-cli/ember-cli/pull/7093) use arrow function in `discoverFromDependencies` [@bekzod](https://github.com/bekzod) +- [#7085](https://github.com/ember-cli/ember-cli/pull/7085) remove "Aligned require statements" style guide [@ro0gr](https://github.com/ro0gr) +- [#7092](https://github.com/ember-cli/ember-cli/pull/7092) avoid extra iteration, use `reduce` instead of `map/filter` combination [@bekzod](https://github.com/bekzod) +- [#7161](https://github.com/ember-cli/ember-cli/pull/7161) Use headless chrome in addon build config [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [#7123](https://github.com/ember-cli/ember-cli/pull/7123) double mocha-eslint test timeout [@ro0gr](https://github.com/ro0gr) +- [#7118](https://github.com/ember-cli/ember-cli/pull/7118) cleanup `appAndDependencies` [@bekzod](https://github.com/bekzod) +- [#7105](https://github.com/ember-cli/ember-cli/pull/7105) use `const` where appropriate [@bekzod](https://github.com/bekzod) +- [#7106](https://github.com/ember-cli/ember-cli/pull/7106) cleanup tangled promise [@bekzod](https://github.com/bekzod) +- [#7114](https://github.com/ember-cli/ember-cli/pull/7114) explain the old code in bin/ember [@kellyselden](https://github.com/kellyselden) +- [#7104](https://github.com/ember-cli/ember-cli/pull/7104) cleanup `addon/dependencies` [@bekzod](https://github.com/bekzod) +- [#7107](https://github.com/ember-cli/ember-cli/pull/7107) cleanup promise chain [@bekzod](https://github.com/bekzod) +- [#7113](https://github.com/ember-cli/ember-cli/pull/7113) simplified promise chain in `git-init` [@bekzod](https://github.com/bekzod) +- [#7110](https://github.com/ember-cli/ember-cli/pull/7110) convert to RSVP promise inside `utilities/execa` [@bekzod](https://github.com/bekzod) +- [#7152](https://github.com/ember-cli/ember-cli/pull/7152) Remove redundant chrome installation since appveyor has latest chrome [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [#7151](https://github.com/ember-cli/ember-cli/pull/7151) Change `broccoli-middleware` to `1.0.0` :tada: [@twokul](https://github.com/twokul) +- [#7148](https://github.com/ember-cli/ember-cli/pull/7148) Replace phantom.js usage with headless chrome [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [#7147](https://github.com/ember-cli/ember-cli/pull/7147) Add Replacer to FileInfo [@stefanpenner](https://github.com/stefanpenner) +- [#7133](https://github.com/ember-cli/ember-cli/pull/7133) ember-cli-dependency-checker major version bump [@kellyselden](https://github.com/kellyselden) +- [#7175](https://github.com/ember-cli/ember-cli/pull/7175) mention chrome is required now [@kellyselden](https://github.com/kellyselden) +- [#7160](https://github.com/ember-cli/ember-cli/pull/7160) link + integrity is currently causing double loads [@stefanpenner](https://github.com/stefanpenner) +- [#7153](https://github.com/ember-cli/ember-cli/pull/7153) Update yarn.lock [@rwjblue](https://github.com/rwjblue) +- [#7180](https://github.com/ember-cli/ember-cli/pull/7180) install npm 4 in addon travis using npm [@kellyselden](https://github.com/kellyselden) +- [#7167](https://github.com/ember-cli/ember-cli/pull/7167) Upgrade testem to allow browser_args in testem.json [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [#7169](https://github.com/ember-cli/ember-cli/pull/7169) Travis multiple blank line cleanup and if block code consolidation [@kellyselden](https://github.com/kellyselden) +- [#7173](https://github.com/ember-cli/ember-cli/pull/7173) Removed unused dependencies [@t-sauer](https://github.com/t-sauer) +- [#7177](https://github.com/ember-cli/ember-cli/pull/7177) verify npm/yarn logic in travis files [@kellyselden](https://github.com/kellyselden) +- [#7181](https://github.com/ember-cli/ember-cli/pull/7181) node 8 but with npm4 [@stefanpenner](https://github.com/stefanpenner) +- [#7195](https://github.com/ember-cli/ember-cli/pull/7195) fix --non-interactive test regression [@kellyselden](https://github.com/kellyselden) +- [#7196](https://github.com/ember-cli/ember-cli/pull/7196) fix a bad merge conflict resolution [@kellyselden](https://github.com/kellyselden) +- [#7197](https://github.com/ember-cli/ember-cli/pull/7197) use newer yarn on travis [@kellyselden](https://github.com/kellyselden) +- [#7200](https://github.com/ember-cli/ember-cli/pull/7200) verify welcome page logic [@kellyselden](https://github.com/kellyselden) + +Thank you to all who took the time to contribute! + + +## 2.14.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.14.1...v2.14.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.14.1...v2.14.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7273](https://github.com/ember-cli/ember-cli/pull/7273) Fix --json option for asset sizes command [@simplabs](https://github.com/simplabs) + +Thank you to all who took the time to contribute! + + +## 2.14.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.14.0...v2.14.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.14.0...v2.14.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7186](https://github.com/ember-cli/ember-cli/pull/7186) [release] node 8 [@stefanpenner](https://github.com/stefanpenner) +- [#7193](https://github.com/ember-cli/ember-cli/pull/7193) cherry pick "install npm 4 in addon travis using npm" [@kellyselden](https://github.com/kellyselden) +- [#7194](https://github.com/ember-cli/ember-cli/pull/7194) stay in sync with editorconfig and other blueprints regarding newlines [@kellyselden](https://github.com/kellyselden) +- [#7204](https://github.com/ember-cli/ember-cli/pull/7204) [bugfix release] explain node 4 in addons [@kellyselden](https://github.com/kellyselden) +- [#7205](https://github.com/ember-cli/ember-cli/pull/7205) [bugfix release] 2 12 lts testing [@kellyselden](https://github.com/kellyselden) +- [#7206](https://github.com/ember-cli/ember-cli/pull/7206) [bugfix release] remove MODEL_FACTORY_INJECTIONS [@kellyselden](https://github.com/kellyselden) +- [#7208](https://github.com/ember-cli/ember-cli/pull/7208) bugfix(legacy-addons): Fixes typo in babel transpilation options [@pzuraq](https://github.com/pzuraq) +- [#7210](https://github.com/ember-cli/ember-cli/pull/7210) [bugfix release] Ember try remove test [@kellyselden](https://github.com/kellyselden) +- [#7246](https://github.com/ember-cli/ember-cli/pull/7246) [BUGFIX release] cherry-pick "Bumps `broccoli-builder` version to include stack traces fix" [@twokul](https://github.com/twokul) + +## 2.14.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.3...v2.14.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.3...v2.14.0) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#6937](https://github.com/ember-cli/ember-cli/pull/6937) various blueprint cleanup and consistency [@kellyselden](https://github.com/kellyselden) +- [#6862](https://github.com/ember-cli/ember-cli/pull/6862) Update minimum ember-try version. [@rwjblue](https://github.com/rwjblue) +- [#6932](https://github.com/ember-cli/ember-cli/pull/6932) make blueprint files public [@kellyselden](https://github.com/kellyselden) +- [#6874](https://github.com/ember-cli/ember-cli/pull/6874) Add .eslintrc.js files to blueprints [@rwwagner90](https://github.com/rwwagner90) +- [#6868](https://github.com/ember-cli/ember-cli/pull/6868) Add --welcome option to `new` and `init` so that it can be skipped with --no-welcome [@romulomachado](https://github.com/romulomachado) +- [#6873](https://github.com/ember-cli/ember-cli/pull/6873) Add ~ to ember-cli in package.json in blueprints [@rwwagner90](https://github.com/rwwagner90) +- [#6934](https://github.com/ember-cli/ember-cli/pull/6934) missed node 4 - es6 updates in blueprints [@kellyselden](https://github.com/kellyselden) +- [#6890](https://github.com/ember-cli/ember-cli/pull/6890) Replace lib/utilities/DAG.js with dag-map package [@rwwagner90](https://github.com/rwwagner90) +- [#6888](https://github.com/ember-cli/ember-cli/pull/6888) Print out `yarn install` when yarn.lock file is present [@samdemaeyer](https://github.com/samdemaeyer) +- [#6883](https://github.com/ember-cli/ember-cli/pull/6883) broccoli/ember-app: Make app/index.html optional [@Turbo87](https://github.com/Turbo87) +- [#6886](https://github.com/ember-cli/ember-cli/pull/6886) Handle addon constructor errors gracefully [@jsturgis](https://github.com/jsturgis) +- [#6889](https://github.com/ember-cli/ember-cli/pull/6889) Use const/let in all blueprints [@simonihmig](https://github.com/simonihmig) +- [#6938](https://github.com/ember-cli/ember-cli/pull/6938) Add ESLint config to "server" and "lib" blueprints [@kellyselden](https://github.com/kellyselden) +- [#6910](https://github.com/ember-cli/ember-cli/pull/6910) [BUGFIX] Add yuidocs for the addon:init method [@mattmarcum](https://github.com/mattmarcum) +- [#6896](https://github.com/ember-cli/ember-cli/pull/6896) Removed all references to Bower in blueprint README. [@michielboekhoff](https://github.com/michielboekhoff) +- [#6903](https://github.com/ember-cli/ember-cli/pull/6903) remove npm experiment refs [@tylerturdenpants](https://github.com/tylerturdenpants) +- [#6907](https://github.com/ember-cli/ember-cli/pull/6907) Ignore files created by Ember-Try [@elwayman02](https://github.com/elwayman02) +- [#6898](https://github.com/ember-cli/ember-cli/pull/6898) Update ember-export-application-global to babel@6 version. [@rwjblue](https://github.com/rwjblue) +- [#6915](https://github.com/ember-cli/ember-cli/pull/6915) Run YUIDoc on single `it` [@sduquej](https://github.com/sduquej) +- [#6912](https://github.com/ember-cli/ember-cli/pull/6912) Stop creating recursive symlink (addon requiring itself) [@clekstro](https://github.com/clekstro) +- [#6911](https://github.com/ember-cli/ember-cli/pull/6911) Fix dirty git state [@clekstro](https://github.com/clekstro) +- [#6966](https://github.com/ember-cli/ember-cli/pull/6966) ENHANCEMENT: throw when converting `npm install foo` to `yarn install foo` [@pichfl](https://github.com/pichfl) +- [#6940](https://github.com/ember-cli/ember-cli/pull/6940) remove lint filter [@kellyselden](https://github.com/kellyselden) +- [#6936](https://github.com/ember-cli/ember-cli/pull/6936) use RSVP.resolve shorthand [@kellyselden](https://github.com/kellyselden) +- [#6919](https://github.com/ember-cli/ember-cli/pull/6919) Do not use `chalk.white` when displaying asset sizes [@lucasmazza](https://github.com/lucasmazza) +- [#6939](https://github.com/ember-cli/ember-cli/pull/6939) replace ': function(' with '(' [@kellyselden](https://github.com/kellyselden) +- [#6935](https://github.com/ember-cli/ember-cli/pull/6935) use our string style since converted from json [@kellyselden](https://github.com/kellyselden) +- [#6942](https://github.com/ember-cli/ember-cli/pull/6942) object shorthand blueprint cleanup [@kellyselden](https://github.com/kellyselden) +- [#6984](https://github.com/ember-cli/ember-cli/pull/6984) Fix perf-guide to have correct file names for build visualization [@kratiahuja](https://github.com/kratiahuja) +- [#7007](https://github.com/ember-cli/ember-cli/pull/7007) Updated npm version for ember-data to use ~ instead of ^ [@fushi](https://github.com/fushi) +- [#7038](https://github.com/ember-cli/ember-cli/pull/7038) Update "ember-cli-htmlbars" [@stefanpenner](https://github.com/stefanpenner) +- [#7059](https://github.com/ember-cli/ember-cli/pull/7059) Addon#setupPreprocessorRegistry should be invoked after `addon.app` is set. [@stefanpenner](https://github.com/stefanpenner) +- [#7130](https://github.com/ember-cli/ember-cli/pull/7130) yarn: Use --non-interactive flag [@Turbo87](https://github.com/Turbo87) +- [#7191](https://github.com/ember-cli/ember-cli/pull/7191) blueprints/app: Update "ember-source" and "ember-data" to v2.14.0 [@Turbo87](https://github.com/Turbo87) +- [#7192](https://github.com/ember-cli/ember-cli/pull/7192) tests/acceptance: Delete broken "ember generate http-proxy" test [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +### 2.14.0-beta.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.14.0-beta.1...v2.14.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.14.0-beta.1...v2.14.0-beta.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#7007](https://github.com/ember-cli/ember-cli/pull/7007) Updated npm version for ember-data to use ~ instead of ^ [@fushi](https://github.com/fushi) +- [#6996](https://github.com/ember-cli/ember-cli/pull/6996) Update to non-beta version of ember-cli-qunit. [@rwjblue](https://github.com/rwjblue) +- [#6991](https://github.com/ember-cli/ember-cli/pull/6991) cleanup [@stefanpenner](https://github.com/stefanpenner) +- [#7009](https://github.com/ember-cli/ember-cli/pull/7009) fix extra new line and easier to read indentation [@Turbo87](https://github.com/Turbo87) +- [#7011](https://github.com/ember-cli/ember-cli/pull/7011) npmTask should throw when trying to convert `npm install foo` to `yarn install foo` [@Turbo87](https://github.com/Turbo87) +- [#7015](https://github.com/ember-cli/ember-cli/pull/7015) Do not set committer for the initial git commit [@Turbo87](https://github.com/Turbo87) +- [#7023](https://github.com/ember-cli/ember-cli/pull/7023) Allow broccoli-babel-transpiler to float with SemVer. [@rwjblue](https://github.com/rwjblue) +- [#7028](https://github.com/ember-cli/ember-cli/pull/7028) add yarn missing default comment [@kellyselden](https://github.com/kellyselden) +- [#7036](https://github.com/ember-cli/ember-cli/pull/7036) Ensure `lintTree` results cannot clobber tests. [@rwjblue](https://github.com/rwjblue) +- [#7038](https://github.com/ember-cli/ember-cli/pull/7038) Update "ember-cli-htmlbars" [@stefanpenner](https://github.com/stefanpenner) +- [#7049](https://github.com/ember-cli/ember-cli/pull/7049) Prevent warnings from broccoli-babel-transpiler. [@rwjblue](https://github.com/rwjblue) +- [#7051](https://github.com/ember-cli/ember-cli/pull/7051) Corrected a typo in Windows elevation test error message. [@Turbo87](https://github.com/Turbo87) +- [#7052](https://github.com/ember-cli/ember-cli/pull/7052) Pass only package to npm uninstall task that exist [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +### 2.14.0-beta.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.0...v2.14.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.0...v2.14.0-beta.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#6918](https://github.com/ember-cli/ember-cli/pull/6918) Update markdown-it-terminal to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) +- [#6862](https://github.com/ember-cli/ember-cli/pull/6862) Update minimum ember-try version. [@rwjblue](https://github.com/rwjblue) +- [#6859](https://github.com/ember-cli/ember-cli/pull/6859) Update fs-extra to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) +- [#6937](https://github.com/ember-cli/ember-cli/pull/6937) various blueprint cleanup and consistency [@kellyselden](https://github.com/kellyselden) +- [#6874](https://github.com/ember-cli/ember-cli/pull/6874) Add .eslintrc.js files to blueprints [@rwwagner90](https://github.com/rwwagner90) +- [#6868](https://github.com/ember-cli/ember-cli/pull/6868) Add --welcome option to `new` and `init` so that it can be skipped with --no-welcome [@romulomachado](https://github.com/romulomachado) +- [#6873](https://github.com/ember-cli/ember-cli/pull/6873) Add ~ to ember-cli in package.json in blueprints [@rwwagner90](https://github.com/rwwagner90) +- [#6932](https://github.com/ember-cli/ember-cli/pull/6932) make blueprint files public [@kellyselden](https://github.com/kellyselden) +- [#6890](https://github.com/ember-cli/ember-cli/pull/6890) Replace lib/utilities/DAG.js with dag-map package [@rwwagner90](https://github.com/rwwagner90) +- [#6888](https://github.com/ember-cli/ember-cli/pull/6888) Print out `yarn install` when yarn.lock file is present [@samdemaeyer](https://github.com/samdemaeyer) +- [#6883](https://github.com/ember-cli/ember-cli/pull/6883) broccoli/ember-app: Make app/index.html optional [@Turbo87](https://github.com/Turbo87) +- [#6886](https://github.com/ember-cli/ember-cli/pull/6886) Handle addon constructor errors gracefully [@jsturgis](https://github.com/jsturgis) +- [#6889](https://github.com/ember-cli/ember-cli/pull/6889) Use const/let in all blueprints [@simonihmig](https://github.com/simonihmig) +- [#6940](https://github.com/ember-cli/ember-cli/pull/6940) remove lint filter [@kellyselden](https://github.com/kellyselden) +- [#6910](https://github.com/ember-cli/ember-cli/pull/6910) [BUGFIX] Add yuidocs for the addon:init method [@mattmarcum](https://github.com/mattmarcum) +- [#6896](https://github.com/ember-cli/ember-cli/pull/6896) Removed all references to Bower in blueprint README. [@michielboekhoff](https://github.com/michielboekhoff) +- [#6903](https://github.com/ember-cli/ember-cli/pull/6903) remove npm experiment refs [@tylerturdenpants](https://github.com/tylerturdenpants) +- [#6907](https://github.com/ember-cli/ember-cli/pull/6907) Ignore files created by Ember-Try [@elwayman02](https://github.com/elwayman02) +- [#6898](https://github.com/ember-cli/ember-cli/pull/6898) Update ember-export-application-global to babel@6 version. [@rwjblue](https://github.com/rwjblue) +- [#6942](https://github.com/ember-cli/ember-cli/pull/6942) object shorthand blueprint cleanup [@stefanpenner](https://github.com/ember-cli) +- [#6936](https://github.com/ember-cli/ember-cli/pull/6936) use RSVP.resolve shorthand [@kellyselden](https://github.com/kellyselden) +- [#6934](https://github.com/ember-cli/ember-cli/pull/6934) missed node 4 - es6 updates in blueprints [@kellyselden](https://github.com/kellyselden) +- [#6912](https://github.com/ember-cli/ember-cli/pull/6912) Stop creating recursive symlink (addon requiring itself) [@clekstro](https://github.com/clekstro) +- [#6935](https://github.com/ember-cli/ember-cli/pull/6935) use our string style since converted from json [@kellyselden](https://github.com/kellyselden) +- [#6919](https://github.com/ember-cli/ember-cli/pull/6919) Do not use `chalk.white` when displaying asset sizes [@lucasmazza](https://github.com/lucasmazza) +- [#6915](https://github.com/ember-cli/ember-cli/pull/6915) Run YUIDoc on single `it` [@sduquej](https://github.com/sduquej) +- [#6911](https://github.com/ember-cli/ember-cli/pull/6911) Fix dirty git state [@clekstro](https://github.com/clekstro) +- [#6938](https://github.com/ember-cli/ember-cli/pull/6938) Add ESLint config to "server" and "lib" blueprints [@kellyselden](https://github.com/kellyselden) +- [#6939](https://github.com/ember-cli/ember-cli/pull/6939) replace ': function(' with '(' [@kellyselden](https://github.com/kellyselden) +- [#6966](https://github.com/ember-cli/ember-cli/pull/6966) ENHANCEMENT: throw when converting `npm install foo` to `yarn install foo` [@pichfl](https://github.com/pichfl) +- [#6984](https://github.com/ember-cli/ember-cli/pull/6984) Fix perf-guide to have correct file names for build visualization [@kratiahuja](https://github.com/kratiahuja) +- [#6987](https://github.com/ember-cli/ember-cli/pull/6987) Update fs-extra to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) + +Thank you to all who took the time to contribute! + + +### 2.13.3 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/db09559dc922eafdfb6723969b53dfff1a8f5331) - + default ember is now at 1.13.7 (but feel free to upgrade/downgrade as desired) - + default ember-data is now at 1.13.8 (but feel free to upgrade/downgrade as desired) - + for users with very large bower_components directories, rebuild times should improve - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + If you haven't already, please remember to transition your Brocfile.js to ember-cli-build.js. [more details](https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md#brocfile-transition) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.2...v2.13.3) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/aaf7faebf1ca721382d281dd3125b24c7a752c7e) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.2...v2.13.3) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#4599](https://github.com/ember-cli/ember-cli/pull/4599) Update valid-platform-version.js [@stefanpenner](https://github.com/stefanpenner) -- [#4590](https://github.com/ember-cli/ember-cli/pull/4590) Remove `ember update` mention in update-checker [@quaertym](https://github.com/quaertym) -- [#4582](https://github.com/ember-cli/ember-cli/pull/4582) blueprints/app/package.json: Sort scripts alphabetically [@Turbo87](https://github.com/Turbo87) -- [#4577](https://github.com/ember-cli/ember-cli/pull/4577) Adding more help acceptance tests [@kellyselden](https://github.com/kellyselden) -- [#4621](https://github.com/ember-cli/ember-cli/pull/4621) bump funnel, and prefer globs for includes. [@stefanpenner](https://github.com/stefanpenner) -- [#4598](https://github.com/ember-cli/ember-cli/pull/4598) bump timeout, see if iojs on CI becomes happy again [@stefanpenner](https://github.com/stefanpenner) -- [#4596](https://github.com/ember-cli/ember-cli/pull/4596) Bump version of ember-cli-app-version to 0.5.0 [@taras](https://github.com/taras) -- [#4591](https://github.com/ember-cli/ember-cli/pull/4591) Revert "Remove `ember update` mention in update-checker" [@stefanpenner](https://github.com/stefanpenner) -- [#4597](https://github.com/ember-cli/ember-cli/pull/4597) update to ember-cli-qunit v1.0.0 [@stefanpenner](https://github.com/stefanpenner) -- [#4593](https://github.com/ember-cli/ember-cli/pull/4593) Remove ember update mention in update-checker [@quaertym](https://github.com/quaertym) -- [#4628](https://github.com/ember-cli/ember-cli/pull/4628) a couple more tests that weren't being run [@kellyselden](https://github.com/kellyselden) -- [#4606](https://github.com/ember-cli/ember-cli/pull/4606) [TYPO] SRI changelog typo fix [@jonathanKingston](https://github.com/jonathanKingston) -- [#4601](https://github.com/ember-cli/ember-cli/pull/4601) update broccoli-caching-writer [@stefanpenner](https://github.com/stefanpenner); -- [#4630](https://github.com/ember-cli/ember-cli/pull/4630) Update blueprint dependencies [@btecu](https://github.com/btecu) -- [#4611](https://github.com/ember-cli/ember-cli/pull/4611) Update Ember Data dependency to 1.13.8 [@bmac](https://github.com/bmac) -- [#4638](https://github.com/ember-cli/ember-cli/pull/4638) broccoli-plugin now uses annotation, rather then our own convention o… [@stefanpenner](https://github.com/stefanpenner) -- [#4622](https://github.com/ember-cli/ember-cli/pull/4622) Upgrade merge trees [@stefanpenner](https://github.com/stefanpenner) -- [#4625](https://github.com/ember-cli/ember-cli/pull/4625) bump broccoli-caching-writer to 1.1.0 [@kellyselden](https://github.com/kellyselden) -- [#4627](https://github.com/ember-cli/ember-cli/pull/4627) fix test that was never being run [@kellyselden](https://github.com/kellyselden) -- [#4629](https://github.com/ember-cli/ember-cli/pull/4629) broccoli-asset-rev to 2.1.2 [@kellyselden](https://github.com/kellyselden) -- [#4632](https://github.com/ember-cli/ember-cli/pull/4632) Windows CI: Removed npm upgrade [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#4636](https://github.com/ember-cli/ember-cli/pull/4636) Update Ember version to 1.13.7. [@rwjblue](https://github.com/rwjblue) -- [#4637](https://github.com/ember-cli/ember-cli/pull/4637) [INTERNAL] Prefer globs over RegExps as funnel arguments [@dschmidt](https://github.com/dschmidt) -- [#4642](https://github.com/ember-cli/ember-cli/pull/4642) bump broccoli-funnel [@stefanpenner](https://github.com/stefanpenner) -- [#4643](https://github.com/ember-cli/ember-cli/pull/4643) Support a (now deprecated) single-argument use of addBowerPackageToProject [@mike-north](https://github.com/mike-north) +- [#7076](https://github.com/ember-cli/ember-cli/pull/7076) node 8 [@stefanpenner](https://github.com/stefanpenner) +- [#7077](https://github.com/ember-cli/ember-cli/pull/7077) Add reasonable `uglify-js` options. [@rwjblue](https://github.com/rwjblue) Thank you to all who took the time to contribute! -### 1.13.7 + +### 2.13.2 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/6a41c5cd7f0f68e7cf710268376d0349c5b57171) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + If you haven't already, please remember to transition your Brocfile.js to ember-cli-build.js. [more details](https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md#brocfile-transition) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.1...v2.13.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/f6f61d55c31d631203bc5491432b435e2cc807c2) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.1...v2.13.2) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#4558](https://github.com/ember-cli/ember-cli/pull/4558) ensure we apply patches at the right part of the release. [@stefanpenner](https://github.com/ember-cli) -- [#4559](https://github.com/ember-cli/ember-cli/pull/4559) bundle testem [@stefanpenner](https://github.com/ember-cli) -- [#4560](https://github.com/ember-cli/ember-cli/pull/4560) Update ember-qunit to 0.4.9. [@rwjblue](https://github.com/rwjblue) -- [#4561](https://github.com/ember-cli/ember-cli/pull/4561) Upgrade to Broccoli 0.16.5 [@joliss](https://github.com/joliss) -- [#4564](https://github.com/ember-cli/ember-cli/pull/4564) add 1.13.6 diffs to changelog [@kellyselden](https://github.com/kellyselden) -- [#4569](https://github.com/ember-cli/ember-cli/pull/4569) Update Ember to v1.13.6. [@rwjblue](https://github.com/rwjblue) -- [#4572](https://github.com/ember-cli/ember-cli/pull/4572) Update QUnit version to 1.18.0. [@rwjblue](https://github.com/rwjblue) -- [#4589](https://github.com/ember-cli/ember-cli/pull/4589) Fixes issue with smoke test failure. [@rickharrison](https://github.com/rickharrison) +- [#7023](https://github.com/ember-cli/ember-cli/pull/7023) Allow broccoli-babel-transpiler to float with SemVer. [@rwjblue](https://github.com/rwjblue) +- [#7028](https://github.com/ember-cli/ember-cli/pull/7028) add yarn missing default comment [@kellyselden](https://github.com/kellyselden) +- [#7036](https://github.com/ember-cli/ember-cli/pull/7036) Ensure `lintTree` results cannot clobber tests. [@rwjblue](https://github.com/rwjblue) +- [#7049](https://github.com/ember-cli/ember-cli/pull/7049) Prevent warnings from broccoli-babel-transpiler. [@rwjblue](https://github.com/rwjblue) +- [#7051](https://github.com/ember-cli/ember-cli/pull/7051) Corrected a typo in Windows elevation test error message. [@Turbo87](https://github.com/Turbo87) +- [#7052](https://github.com/ember-cli/ember-cli/pull/7052) Pass only package to npm uninstall task that exist [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 1.13.6 + +### 2.13.1 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/c36b2e35b9ef2a66d6f01f360831c6ec9707c5d7) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + If you haven't already, please remember to transition your Brocfile.js to ember-cli-build.js. [more details](https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md#brocfile-transition) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.0...v2.13.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/d77330079ca14a1d0e39383cce87565c1c2d742f) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.0...v2.13.1) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#3239](https://github.com/ember-cli/ember-cli/pull/3239) ENHANCEMENT: Added `--test-port`/`testPort` option to configure test port [@patocallaghan](https://github.com/patocallaghan) -- [#4545](https://github.com/ember-cli/ember-cli/pull/4545) bump es6modules to fix IE8 issue [@stefanpenner](https://github.com/ember-cli) -- [#4549](https://github.com/ember-cli/ember-cli/pull/4549) adding 1.13.5 diff to changelog [@kellyselden](https://github.com/kellyselden) -- [#4553](https://github.com/ember-cli/ember-cli/pull/4553) [Bugfix] addAddonToProject fix for 1.13.5 [@jasonmit](https://github.com/jasonmit) +- [#6991](https://github.com/ember-cli/ember-cli/pull/6991) cleanup [@stefanpenner](https://github.com/stefanpenner) +- [#6996](https://github.com/ember-cli/ember-cli/pull/6996) Update to non-beta version of ember-cli-qunit [@rwjblue](https://github.com/rwjblue) +- [#7009](https://github.com/ember-cli/ember-cli/pull/7009) fix extra new line and easier to read indentation [@Turbo87](https://github.com/Turbo87) +- [#7011](https://github.com/ember-cli/ember-cli/pull/7011) npmTask should throw when trying to convert `npm install foo` to `yarn install foo` [@Turbo87](https://github.com/Turbo87) +- [#7015](https://github.com/ember-cli/ember-cli/pull/7015) Do not set committer for the initial git commit [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 1.13.5 + +### 2.13.0 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/750a6ba374fc8bb2bbb6102fcb9db399dd1c2472) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + If you haven't already, please remember to transition your Brocfile.js to ember-cli-build.js. [more details](https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md#brocfile-transition) - + We now bundle ember.js 1.13.5 and ember-data 1.13.7 by default, but please note you can change these by updating bower.json - + We have included support for [Subresource Integrity (SRI)](http://www.w3.org/TR/SRI) by default, to find out more checkout our site's [SRI section](http://www.ember-cli.com/user-guide/#subresource-integrity) - + Please note: Testem will now error if a specified runner is missing. - + When installing ember-cli, one can use `npm install ember-cli --no-optional` to skip all native dependencies. + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.12.3...v2.13.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/ace30d3fecafee7e27ae5d75254096a08ede2a6c) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.12.3...v2.13.0) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#4471](https://github.com/ember-cli/ember-cli/pull/4471) Make the Examples less confusing [@mdragon](https://github.com/mdragon) -- [#4367](https://github.com/ember-cli/ember-cli/pull/4367) [BUGFIX] Adding check for undefined inRepoAddon option in Blueprint.prototype.\_locals. [@gmurphey](https://github.com/gmurphey) -- [#4411](https://github.com/ember-cli/ember-cli/pull/4411) Removing unknown command from `ember help`. [@gmurphey](https://github.com/gmurphey) -- [#4405](https://github.com/ember-cli/ember-cli/pull/4405) Fix default generated integration test. [@blimmer](https://github.com/blimmer) -- [#4406](https://github.com/ember-cli/ember-cli/pull/4406) [ENHANCEMENT] Let ember install take multiple addons [@DanielOchoa](https://github.com/DanielOchoa) -- [#4478](https://github.com/ember-cli/ember-cli/pull/4478) Store acceptance test application in test context. [@rwjblue](https://github.com/rwjblue) -- [#4413](https://github.com/ember-cli/ember-cli/pull/4413) Add clarity for 0.2.7 to 1.13.x transition (build) [@pixelhandler](https://github.com/pixelhandler) -- [#4416](https://github.com/ember-cli/ember-cli/pull/4416) bump sourcemap-concat version [@kwikPRs](https://github.com/kwikPRs) -- [#4419](https://github.com/ember-cli/ember-cli/pull/4419) Nuke 'ember update' [@jonnii](https://github.com/jonnii) -- [#4488](https://github.com/ember-cli/ember-cli/pull/4488) Update Ember to 1.13.5. [@rwjblue](https://github.com/rwjblue) -- [#4440](https://github.com/ember-cli/ember-cli/pull/4440) EmberAddon Should Merge Defaults [@chadhietala](https://github.com/chadhietala) -- [#4438](https://github.com/ember-cli/ember-cli/pull/4438) Fix Ember CLI project update link [@balinterdi](https://github.com/balinterdi) -- [#4428](https://github.com/ember-cli/ember-cli/pull/4428) Update #watchman message to point to correct url [@supabok](https://github.com/supabok) -- [#4430](https://github.com/ember-cli/ember-cli/pull/4430) Handle a wide variety of bower endpoints [@truenorth](https://github.com/truenorth) -- [#4454](https://github.com/ember-cli/ember-cli/pull/4454) Always use Brocfile (with deprecation messaging) if it exists [@gmurphey](https://github.com/gmurphey) -- [#4452](https://github.com/ember-cli/ember-cli/pull/4452) [ENHANCEMENT] Detect & skip lib install on http-mock gen [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) -- [#4456](https://github.com/ember-cli/ember-cli/pull/4456) Updating getPort logic to use liveReloadHost. Fixes #4455. [@gmurphey](https://github.com/gmurphey) -- [#4443](https://github.com/ember-cli/ember-cli/pull/4443) Prevent live-reload-port collisions (by default) [@stefanpenner](https://github.com/stefanpenner) -- [#4457](https://github.com/ember-cli/ember-cli/pull/4457) Removing extraneous newline from the component-test blueprint [@gmurphey](https://github.com/gmurphey) -- [#4447](https://github.com/ember-cli/ember-cli/pull/4447) Update to Ember 1.13.4. [@rwjblue](https://github.com/rwjblue) -- [#4449](https://github.com/ember-cli/ember-cli/pull/4449) [ENHANCEMENT] Add --skip-router flag to route blueprint generator [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) -- [#4484](https://github.com/ember-cli/ember-cli/pull/4484) Updating route blueprint to write to router when dummy flag is used [@gmurphey](https://github.com/gmurphey) -- [#4468](https://github.com/ember-cli/ember-cli/pull/4468) [fixes #4467] ensure commands that fail do to being run in the wrong … [@stefanpenner](https://github.com/stefanpenner) -- [#4474](https://github.com/ember-cli/ember-cli/pull/4474) [fixes #4328] patch engion.io-client to use any XMLHTTPRequest, but t… [@stefanpenner](https://github.com/stefanpenner) -- [#4462](https://github.com/ember-cli/ember-cli/pull/4462) Make ember destroy and ember generate give a better error when called… [@marcioj](https://github.com/marcioj) -- [#4466](https://github.com/ember-cli/ember-cli/pull/4466) Native deps now gone [@stefanpenner](https://github.com/stefanpenner) -- [#4465](https://github.com/ember-cli/ember-cli/pull/4465) ensure reexporter doesn't introduce instability during builds [@stefanpenner](https://github.com/stefanpenner) -- [#4516](https://github.com/ember-cli/ember-cli/pull/4516) update ember-cli-qunit to mitigate some leaks [@stefanpenner](https://github.com/stefanpenner) -- [#4489](https://github.com/ember-cli/ember-cli/pull/4489) [ENHANCEMENT] Testem v0.9.0 [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#4483](https://github.com/ember-cli/ember-cli/pull/4483) Adding in ember-cli-sri into application package.json by default [@jonathanKingston](https://github.com/jonathanKingston) -- [#4524](https://github.com/ember-cli/ember-cli/pull/4524) Update ember-qunit to 0.4.6. [@rwjblue](https://github.com/rwjblue) -- [#4490](https://github.com/ember-cli/ember-cli/pull/4490) Fix code of conduct markdown formatting [@max](https://github.com/max) -- [#4495](https://github.com/ember-cli/ember-cli/pull/4495) bump ember-export-application-global [@stefanpenner](https://github.com/stefanpenner) -- [#4498](https://github.com/ember-cli/ember-cli/pull/4498) Testem v0.9.0 [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#4514](https://github.com/ember-cli/ember-cli/pull/4514) component-unit blueprint - trim component content by default [@ramybenaroya](https://github.com/ramybenaroya) -- [#4534](https://github.com/ember-cli/ember-cli/pull/4534) add os to version output [@kellyselden](https://github.com/kellyselden) -- [#4536](https://github.com/ember-cli/ember-cli/pull/4536) Remove unnecessary `"use strict";`s in "app.js" [@nathanhammond](https://github.com/nathanhammond) -- [#4538](https://github.com/ember-cli/ember-cli/pull/4540) Updated ember-qunit to 0.4.7 [@stefanpenner](https://github.com/stefanpenner) +- [#6978](https://github.com/ember-cli/ember-cli/pull/6978) Update dependencies to Babel 6 versions. [@rwjblue](https://github.com/rwjblue) +- [#6980](https://github.com/ember-cli/ember-cli/pull/6980) Update ember-ajax to v3.0.0. [@rwjblue](https://github.com/rwjblue) +- [#6983](https://github.com/ember-cli/ember-cli/pull/6983) blueprints: Remove Bower from README [@stefanpenner](https://github.com/ember-cli) +- [#6986](https://github.com/ember-cli/ember-cli/pull/6986) Revert nopt dependency update [@calderas](https://github.com/calderas) +- [#6992](https://github.com/ember-cli/ember-cli/pull/6992) blueprints/app: Update "ember-source" and "ember-data" to v2.13.0 [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 1.13.1 + +### 2.13.0-beta.4 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/f1425c5073a33dfb7ff60d5254fd340046f578bd) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.0-beta.3...v2.13.0-beta.4) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/dc309f7655a2cde4cd81bb75d8f274087e9d82f8) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.0-beta.3...v2.13.0-beta.4) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#4398](https://github.com/ember-cli/ember-cli/pull/4398) [BUGFIX] Fixes #4397 add silentError with deprecation [@trabus](https://github.com/trabus) +- [#6944](https://github.com/ember-cli/ember-cli/pull/6944) Include ember-testing.js when using ember-source [@trentmwillis](https://github.com/trentmwillis) +- [#6961](https://github.com/ember-cli/ember-cli/pull/6961) ensure addon.css is always included [@stefanpenner](https://github.com/stefanpenner) +- [#6968](https://github.com/ember-cli/ember-cli/pull/6968) Configure ESLint to parse ES2017 by default [@cibernox](https://github.com/cibernox) +- [#6969](https://github.com/ember-cli/ember-cli/pull/6969) Remove `.bowerrc` from blueprints [@pichfl](https://github.com/pichfl) Thank you to all who took the time to contribute! -### 1.13.0 + + +### 2.13.0-beta.3 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/e83bf78f9be69a6dcedd5a7e16402c6b874efceb) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + `Brocfile.js` has been deprecated in favor of `ember-cli-build.js`. See [TRANSITION.md](https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md) for details on how to transition your `Brocfile.js` code to `ember-cli-build.js`. - + Components are now generated with integration tests by default instead of unit tests. Component unit tests can still be generated separately with: `ember g component-test foo-bar -unit`. - + Services can now be generated into pod structure. + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.0-beta.2...v2.13.0-beta.3) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/c8496e7574826520ead230009068a6313a9712e4) - + `Brocfile.js` has been deprecated in favor of `ember-cli-build.js`. See [TRANSITION.md](https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md) for details on how to transition your `Brocfile.js` code to `ember-cli-build.js`. - + Blueprints can now be generated into the `tests/dummy/app` folder with the `--dummy` flag. - + Scoped npm dependencies are now supported. + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.0-beta.2...v2.13.0-beta.3) + + No changes required - Core Contributors - + fs.existsSync is deprecated, use exists-sync instead. + + No changes required #### Community Contributions -- [#4378](https://github.com/ember-cli/ember-cli/pull/4378) Update Ember to 1.13.3 [@rwjblue](https://github.com/rwjblue) -- [#4395](https://github.com/ember-cli/ember-cli/pull/4395) Update ember-data to 1.13.5 [@trabus](https://github.com/trabus) -- [#4217](https://github.com/ember-cli/ember-cli/pull/4217) [BUGFIX] generating tests inside addons no longer generates addon export file [@trabus](https://github.com/trabus) -- [#4212](https://github.com/ember-cli/ember-cli/pull/4212) fix friendly test description for transforms [@csantero](https://github.com/csantero) -- [#4214](https://github.com/ember-cli/ember-cli/pull/4214) [BUGFIX] correct relative import path for nested adapters [@trabus](https://github.com/trabus) -- [#4215](https://github.com/ember-cli/ember-cli/pull/4215) extract clean-base-url to its own module [@stefanpenner](https://github.com/stefanpenner) -- [#4197](https://github.com/ember-cli/ember-cli/pull/4197) [BUGFIX] add default for path option in component blueprint locals [@trabus](https://github.com/trabus) -- [#4316](https://github.com/ember-cli/ember-cli/pull/4316) fs.existsSync deprecated, replace with exists-sync [@jasonmit](https://github.com/jasonmit) -- [#4224](https://github.com/ember-cli/ember-cli/pull/4224) extract silent-error to its own addon [@stefanpenner](https://github.com/stefanpenner) -- [#4319](https://github.com/ember-cli/ember-cli/pull/4319) Update tmp.js [@jjmiv](https://github.com/jjmiv) -- [#4228](https://github.com/ember-cli/ember-cli/pull/4228) add 0.2.7 diffs [@kellyselden](https://github.com/kellyselden) -- [#4227](https://github.com/ember-cli/ember-cli/pull/4227) Extract process relative require [@stefanpenner](https://github.com/stefanpenner) -- [#4226](https://github.com/ember-cli/ember-cli/pull/4226) extract node-modules-path as its own module [@stefanpenner](https://github.com/stefanpenner) -- [#4326](https://github.com/ember-cli/ember-cli/pull/4326) Drop unused line from app blueprint [@ef4](https://github.com/ef4) -- [#4254](https://github.com/ember-cli/ember-cli/pull/4254) [BUGFIX] Closes #4253. Add `skipHelp` as an available option to commands. [@DanielOchoa](https://github.com/DanielOchoa) -- [#4249](https://github.com/ember-cli/ember-cli/pull/4249) Passing options to tiny-lr (live reload) for HTTPS support [@dosco](https://github.com/dosco) -- [#4239](https://github.com/ember-cli/ember-cli/pull/4239) Fix JSDoc issues [@Turbo87](https://github.com/Turbo87) -- [#4242](https://github.com/ember-cli/ember-cli/pull/4242) Add devDependencies "up to date" badge to README [@truenorth](https://github.com/truenorth) -- [#4251](https://github.com/ember-cli/ember-cli/pull/4251) [BUGFIX] Fix generated addon acceptance test [@trabus](https://github.com/trabus) -- [#4240](https://github.com/ember-cli/ember-cli/pull/4240) Added ember-cli-release to app/addon devDeps blueprint for simple release cutting [@jayphelps](https://github.com/jayphelps) -- [#4286](https://github.com/ember-cli/ember-cli/pull/4286) [Deprecation] Introduce new build file [@chadhietala](https://github.com/chadhietala) -- [#4280](https://github.com/ember-cli/ember-cli/pull/4280) [ENHANCEMENT] Add pod support for services blueprint [@trabus](https://github.com/trabus) -- [#4272](https://github.com/ember-cli/ember-cli/pull/4272) [ENHANCEMENT] Generate component-tests into `tests/integration` by default [@trabus](https://github.com/trabus) -- [#4261](https://github.com/ember-cli/ember-cli/pull/4261) bump ember-cli-htmlbars [@stefanpenner](https://github.com/stefanpenner) -- [#4266](https://github.com/ember-cli/ember-cli/pull/4266) [fixes #4264] [@stefanpenner](https://github.com/stefanpenner) -- [#4270](https://github.com/ember-cli/ember-cli/pull/4270) [BUGFIX] don't allow ember init to create an application without project name [@dukex/bugfix](https://github.com/dukex/bugfix) -- [#4278](https://github.com/ember-cli/ember-cli/pull/4278) Fix 2 typos in livereload-server-test [@jrobeson](https://github.com/jrobeson) -- [#4271](https://github.com/ember-cli/ember-cli/pull/4271) Adding support for private npm modules in blueprints. Closes #4256. [@gmurphey](https://github.com/gmurphey) -- [#4263](https://github.com/ember-cli/ember-cli/pull/4263) [fixes #4260] postprocessTree hook for templates [@stefanpenner](https://github.com/stefanpenner) -- [#4347](https://github.com/ember-cli/ember-cli/pull/4347) ES3+ and for ES5+ deprecation free keys + forEach [@stefanpenner](https://github.com/stefanpenner) -- [#4292](https://github.com/ember-cli/ember-cli/pull/4292) Cleanup pr4283 [@stefanpenner](https://github.com/stefanpenner) -- [#4284](https://github.com/ember-cli/ember-cli/pull/4284) Update ember-resolver to 0.1.17. [@rwjblue](https://github.com/rwjblue) -- [#4287](https://github.com/ember-cli/ember-cli/pull/4287) [ENHANCEMENT] Add ability to generate blueprints into addon `tests/dummy/app` [@trabus](https://github.com/trabus) -- [#4290](https://github.com/ember-cli/ember-cli/pull/4290) Pass the correct port property to LiveReload server [@jrobeson](https://github.com/jrobeson) -- [#4282](https://github.com/ember-cli/ember-cli/pull/4282) Display the LiveReload server address as url [@jrobeson](https://github.com/jrobeson) -- [#4288](https://github.com/ember-cli/ember-cli/pull/4288) Update ember-cli-htmlbars to 0.7.9. [@rwjblue](https://github.com/rwjblue) -- [#4376](https://github.com/ember-cli/ember-cli/pull/4376) Update initializer-test blueprint [@quaertym](https://github.com/quaertym) -- [#4306](https://github.com/ember-cli/ember-cli/pull/4306) [ENHANCEMENT] Print notification when modifying router.js [@trabus](https://github.com/trabus) -- [#4377](https://github.com/ember-cli/ember-cli/pull/4377) Add a test-page option to the test command [@jrjohnson](https://github.com/jrjohnson) -- [#4341](https://github.com/ember-cli/ember-cli/pull/4341) Update ember-load-initializers to 0.1.5 [@jmurphyau](https://github.com/jmurphyau) -- [#4322](https://github.com/ember-cli/ember-cli/pull/4322) Update ADDON_HOOKS.md [@jjmiv](https://github.com/jjmiv) -- [#4334](https://github.com/ember-cli/ember-cli/pull/4334) Cache processed styles tree to prevent double style builds. [@rwjblue](https://github.com/rwjblue) -- [#4309](https://github.com/ember-cli/ember-cli/pull/4309) [ENHANCEMENT] Name blueprint in generate and destroy output message [@trabus](https://github.com/trabus) -- [#4327](https://github.com/ember-cli/ember-cli/pull/4327) Bring tests jshintrc closer to app jshintrc [@ef4](https://github.com/ef4) -- [#4344](https://github.com/ember-cli/ember-cli/pull/4344) [ENHANCEMENT] Fix typo in test command description. [@fabianrbz](https://github.com/fabianrbz) -- [#4362](https://github.com/ember-cli/ember-cli/pull/4362) extract preprocessor-registry -> ember-cli-preprocessor-registry [@stefanpenner](https://github.com/stefanpenner) -- [#4348](https://github.com/ember-cli/ember-cli/pull/4348) Do not pack ember-cli-build.js [@chadhietala](https://github.com/chadhietala) -- [#4343](https://github.com/ember-cli/ember-cli/pull/4343) bump to ember-resolve 0.1.18 – which fixes deprecations while continu… [@stefanpenner](https://github.com/stefanpenner) -- [#4349](https://github.com/ember-cli/ember-cli/pull/4349) enable both relative and absolute treePaths (npm v3 fix) [@stefanpenner](https://github.com/stefanpenner) -- [#4359](https://github.com/ember-cli/ember-cli/pull/4359) Update helper blueprint to use `Ember.Helper.helper` [@balinterdi](https://github.com/balinterdi) -- [#4354](https://github.com/ember-cli/ember-cli/pull/4354) Upgrade to ember-cli-app-version 0.4.0 [@taras](https://github.com/taras) -- [#4370](https://github.com/ember-cli/ember-cli/pull/4370) Remove unexpected final newline [@treyhunner](https://github.com/treyhunner) -- [#4374](https://github.com/ember-cli/ember-cli/pull/4374) Update appveyor.yml [@stefanpenner](https://github.com/stefanpenner) -- [#4382](https://github.com/ember-cli/ember-cli/pull/4382) Update ember-cli-qunit to 0.3.15. [@rwjblue](https://github.com/rwjblue) -- [#4384](https://github.com/ember-cli/ember-cli/pull/4384) [ENHANCEMENT] Add block-template assertion to generated component integration test [@trabus](https://github.com/trabus) -- [#4385](https://github.com/ember-cli/ember-cli/pull/4385) Dependency updates [@truenorth](https://github.com/truenorth) + +- [#6901](https://github.com/ember-cli/ember-cli/pull/6901) Update ember-welcome-page to use Babel 6. [@rwjblue](https://github.com/rwjblue) +- [#6904](https://github.com/ember-cli/ember-cli/pull/6904) Update ember-cli-qunit to use Babel 6. [@rwjblue](https://github.com/rwjblue) +- [#6905](https://github.com/ember-cli/ember-cli/pull/6905) Update various addons to use Babel 6. [@rwjblue](https://github.com/rwjblue) +- [#6928](https://github.com/ember-cli/ember-cli/pull/6928) Add 🐹 as "ember" alias [@Turbo87](https://github.com/Turbo87) +- [#6929](https://github.com/ember-cli/ember-cli/pull/6929) Backport fixes to release branch [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 0.2.7 + +### 2.13.0-beta.2 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/8cd6f5ee0012d3e4960dd9204c9e459f05babd15) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.13.0-beta.1...v2.13.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/d59788f7a175376a16e8f4890ac40e6eabb7b9dd) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.13.0-beta.1...v2.13.0-beta.2) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#4196](https://github.com/ember-cli/ember-cli/pull/4196) [BUGFIX] Adding fileMapToken __name__ for route-addon blueprint. [@gmurphey](https://github.com/gmurphey) -- [#4203](https://github.com/ember-cli/ember-cli/pull/4203) [BUGFIX] acceptance-test blueprint no longer generates addon re-export in app folder [@trabus](https://github.com/trabus) -- [#4206](https://github.com/ember-cli/ember-cli/pull/4206) [Bugfix] Addon.prototype.compileTemplates should not use deprecated t… [@stefanpenner](https://github.com/stefanpenner) -- [#4207](https://github.com/ember-cli/ember-cli/pull/4207) [fixes #4205] allow null addonTemplates. [@stefanpenner](https://github.com/stefanpenner) -- [#4208](https://github.com/ember-cli/ember-cli/pull/4208) Drop ncp for cpr [@stefanpenner](https://github.com/stefanpenner) -- [#4210](https://github.com/ember-cli/ember-cli/pull/4210) Upgrade ember-try dependency in addon blueprint [@kategengler](https://github.com/kategengler) +- [#6861](https://github.com/ember-cli/ember-cli/pull/6861) Don't generate `addon-config/targets.js` in addons [@cibernox](https://github.com/cibernox) +- [#6871](https://github.com/ember-cli/ember-cli/pull/6871) Use `yarn install --no-lockfile` in travis for addons [@rwwagner90](https://github.com/rwwagner90) +- [#6874](https://github.com/ember-cli/ember-cli/pull/6874) Add .eslintrc.js files to blueprints [@rwwagner90](https://github.com/rwwagner90) +- [#6884](https://github.com/ember-cli/ember-cli/pull/6884) Remove guard in `treeForAddon` around `addon/**/*.js` files. [@rwjblue](https://github.com/rwjblue) +- [#6885](https://github.com/ember-cli/ember-cli/pull/6885) Work around broken bower installation for old npm versions [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 0.2.6 + +### 2.13.0-beta.1 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/734a6b49d4c88ea6431d2793b49477aed70fc220) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + `ember server` can now be started over `https`. Default ssl - certificate and ssl key paths are `ssl/server.crt` and - `ssl/server.key` respectively. Custom paths can be added with - `--ssl-cert` and `--ssl-key` [#3550](https://github.com/ember-cli/ember-cli/issues/3550). - + `ember test` now accepts a `reporter` option, it passes this option to Testem with the reporter to use `[tap|dot|xunit]` [#4106](https://github.com/ember-cli/ember-cli/pull/4106). - + `app/views` is not longer included in the default project blueprint [#4083](https://github.com/ember-cli/ember-cli/pull/4083). - + Added again `podModulePrefix` to `app.js`. We still need - podModulePrefix for the time being, it can be removed again when - the state of pods has been finalized. - + New apps include a `.watchmanconfig` which tells `watchman` to ignoring `tmp` dir [#4101](https://github.com/ember-cli/ember-cli/issues/4101). - + Updated `ember-data` to `1.0.0-beta.18`. Install with `npm install --save-dev ember-data@1.0.0-beta.18`. - + Unit tests for components are now flagged as such [#4177](https://github.com/ember-cli/ember-cli/pull/4177). -- Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/c5db8de5351628f532535f5f6e76e6da8d259299) - + A new hook is available: `treeForAddonTemplates` which allows you to specify the templates tree. For more info on how to use this hook see [the following issue](https://github.com/yapplabs/ember-modal-dialog/issues/34). - + Route blueprint now works within addons [#4152](https://github.com/ember-cli/ember-cli/pull/4152). - + A new generator is available, `ember g route-addon` allows you to create import wrappers for your addon's routes. -- Core Contributors - + We started to merge pull-request as part of the quest to improve code quality, keep them coming! [#3730](https://github.com/ember-cli/ember-cli/issues/3730). + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.12.0...v2.13.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.12.0...v2.13.0-beta.1) + + No changes required +- Core Contributors + + No changes required #### Community Contributions -- [#4143](https://github.com/ember-cli/ember-cli/pull/4143) [BUGFIX] Blueprint.load verify blueprint is in a directory [@trabus](https://github.com/trabus) -- [#4035](https://github.com/ember-cli/ember-cli/pull/4035) Add a verification step to fail the build when tests are filtered with .only [@marcioj](https://github.com/marcioj) -- [#4091](https://github.com/ember-cli/ember-cli/pull/4091) fix name of ember-cli-dependency-checker [@bantic](https://github.com/bantic) -- [#3854](https://github.com/ember-cli/ember-cli/pull/3854) [ENHANCEMENT] install:addon command will show a deprecation message before running the install command. [@DanielOchoa](https://github.com/DanielOchoa) -- [#3550](https://github.com/ember-cli/ember-cli/pull/3550) Add ability to start ember serve on https [@drogus](https://github.com/drogus) -- [#3786](https://github.com/ember-cli/ember-cli/pull/3786) Throw if templating a file fails [@davewasmer](https://github.com/davewasmer) -- [#4026](https://github.com/ember-cli/ember-cli/pull/4026) Revert "Test powershell for appveyor builds" [@stefanpenner](https://github.com/stefanpenner) -- [#4148](https://github.com/ember-cli/ember-cli/pull/4148) extract common SilentError debug/throw logic [@stefanpenner](https://github.com/stefanpenner) -- [#4104](https://github.com/ember-cli/ember-cli/pull/4104) [BUGFIX] Fix custom blueprint options for destroy command [@trabus](https://github.com/trabus) -- [#4106](https://github.com/ember-cli/ember-cli/pull/4106) [ENHANCEMENT] Adding Report option to 'ember test' [@step2yeung](https://github.com/step2yeung) -- [#4155](https://github.com/ember-cli/ember-cli/pull/4155) Updating in-addon and in-repo-addon adapters [@gmurphey](https://github.com/gmurphey) -- [#4123](https://github.com/ember-cli/ember-cli/pull/4123) Remove duplication in lib/utilities/test-info [@quaertym](https://github.com/quaertym) -- [#4114](https://github.com/ember-cli/ember-cli/pull/4114) [Bugfix] 1.4 diff displayed removal before addition. [@stefanpenner](https://github.com/stefanpenner) -- [#4108](https://github.com/ember-cli/ember-cli/pull/4108) Update ember-disable-proxy-controller to 1.0.0 [@cibernox](https://github.com/cibernox) -- [#4116](https://github.com/ember-cli/ember-cli/pull/4116) gzip served files. [@stefanpenner](https://github.com/stefanpenner) -- [#4120](https://github.com/ember-cli/ember-cli/pull/4120) [fixes #4083] remove views dir by default [@stefanpenner](https://github.com/stefanpenner) -- [#4159](https://github.com/ember-cli/ember-cli/pull/4159) Add `treeForAddonTemplates` hook. [@lukemelia](https://github.com/lukemelia) -- [#4142](https://github.com/ember-cli/ember-cli/pull/4142) Installation checker [@stefanpenner](https://github.com/stefanpenner) -- [#4132](https://github.com/ember-cli/ember-cli/pull/4132) Revert "Remove podModulePrefix from app.js" [@trabus](https://github.com/trabus) -- [#4141](https://github.com/ember-cli/ember-cli/pull/4141) ENHANCEMENT More advanced detection of whether outputPath is a parent of the project directory [@catbieber](https://github.com/catbieber) -- [#4138](https://github.com/ember-cli/ember-cli/pull/4138) Extract unknown command [@quaertym](https://github.com/quaertym) -- [#4139](https://github.com/ember-cli/ember-cli/pull/4139) [fixes #4133] warn if helper without `-` is generated [@stefanpenner](https://github.com/stefanpenner) -- [#4124](https://github.com/ember-cli/ember-cli/pull/4124) [ENHANCEMENT] Add watchmanconfig file to blueprints [@mikegrassotti](https://github.com/mikegrassotti) -- [#4152](https://github.com/ember-cli/ember-cli/pull/4152) [ENHANCEMENT] Updating route blueprint to work within addons and create route-addon… [@stefanpenner](https://github.com/stefanpenner) -- [#4157](https://github.com/ember-cli/ember-cli/pull/4157) Add Code Climate config [@chrislopresto](https://github.com/chrislopresto) -- [#4150](https://github.com/ember-cli/ember-cli/pull/4150) Code Quality: npm-install.js, npm-uninstall.js D -> A [@jkarsrud](https://github.com/jkarsrud) -- [#4146](https://github.com/ember-cli/ember-cli/pull/4146) Code Quality: addon.js, project.js D -> C [@jkarsrud](https://github.com/jkarsrud) -- [#4147](https://github.com/ember-cli/ember-cli/pull/4147) Detect ember-cli from deps as well as devDeps [@searls](https://github.com/searls) -- [#4154](https://github.com/ember-cli/ember-cli/pull/4154) remove duplication from normalize entity name [@tyleriguchi](https://github.com/tyleriguchi) -- [#4158](https://github.com/ember-cli/ember-cli/pull/4158) Allow addons to have pod based templates [@pzuraq](https://github.com/pzuraq) -- [#4160](https://github.com/ember-cli/ember-cli/pull/4160) Friendlier comments for Brocfile in addons [@igorT](https://github.com/igorT) -- [#4162](https://github.com/ember-cli/ember-cli/pull/4162) Remove unused variables [@quaertym](https://github.com/quaertym) -- [#4163](https://github.com/ember-cli/ember-cli/pull/4163) Bump ember-data to v1.0.0-beta.18 [@quaertym](https://github.com/quaertym) -- [#4166](https://github.com/ember-cli/ember-cli/pull/4166) upgrade node-require-timings [@stefanpenner](https://github.com/stefanpenner) -- [#4168](https://github.com/ember-cli/ember-cli/pull/4168) Allow internal cli parameters to be configurable by other cli tools [@rodyhaddad](https://github.com/rodyhaddad ) -- [#4177](https://github.com/ember-cli/ember-cli/pull/4177) Flag component unit tests as such [@mixonic](https://github.com/mixonic) -- [#4187](https://github.com/ember-cli/ember-cli/pull/4187) isbinaryfile is used in more the just development [@stefanpenner](https://github.com/ember-clistefanpenner) -- [#4188](https://github.com/ember-cli/ember-cli/pull/4188) Fixed type annotations [@Turbo87](https://github.com/Turbo87) +- [#6795](https://github.com/ember-cli/ember-cli/pull/6795) Cleanup EmberApp class [@Turbo87](https://github.com/Turbo87) +- [#6615](https://github.com/ember-cli/ember-cli/pull/6615) Command interruption [@ro0gr](https://github.com/ro0gr) +- [#6472](https://github.com/ember-cli/ember-cli/pull/6472) add ability to clean up old files in generators [@kellyselden](https://github.com/kellyselden) +- [#6796](https://github.com/ember-cli/ember-cli/pull/6796) Update dependencies to latest versions. [@stefanpenner](https://github.com/ember-cli) +- [#6718](https://github.com/ember-cli/ember-cli/pull/6718) Pass init instrumentation to CLI if we have it [@stefanpenner](https://github.com/ember-cli) +- [#6717](https://github.com/ember-cli/ember-cli/pull/6717) Make instrumentation more resilient to errors [@stefanpenner](https://github.com/ember-cli) +- [#6716](https://github.com/ember-cli/ember-cli/pull/6716) Remove link to transition guide when ember-cli-build.js file is missing [@status200](https://github.com/status200) +- [#6715](https://github.com/ember-cli/ember-cli/pull/6715) Fix build console output when using environment variable [@status200](https://github.com/status200) +- [#6690](https://github.com/ember-cli/ember-cli/pull/6690) BUGFIX #6679 - workaround for tiny-lr not reloading on empty files arguments [@gandalfar](https://github.com/gandalfar) +- [#6617](https://github.com/ember-cli/ember-cli/pull/6617) Remove wasted work around addon's addon trees. [@rwjblue](https://github.com/rwjblue) +- [#6798](https://github.com/ember-cli/ember-cli/pull/6798) Update ember-cli-preprocess-registry to get latest clean-css. [@stefanpenner](https://github.com/ember-cli) +- [#6747](https://github.com/ember-cli/ember-cli/pull/6747) Use EOL to fix one Windows CI failure. [@rwjblue](https://github.com/rwjblue) +- [#6727](https://github.com/ember-cli/ember-cli/pull/6727) remove bower install from travis [@kellyselden](https://github.com/kellyselden) +- [#6745](https://github.com/ember-cli/ember-cli/pull/6745) ensure SIGINT ember serve produces instrumentation [@stefanpenner](https://github.com/ember-cli) +- [#6731](https://github.com/ember-cli/ember-cli/pull/6731) This reverts commit cb6bac632dc8dc1c49b30583f0fa135364c5c408, reversing +changes made to be142aaf7801bf64f4322583c7d82ae7c7066c52. [@rwjblue](https://github.com/rwjblue) +- [#6737](https://github.com/ember-cli/ember-cli/pull/6737) Make project require public [@asakusuma](https://github.com/asakusuma) +- [#6741](https://github.com/ember-cli/ember-cli/pull/6741) addon needs to mirror filesToRemove from app [@kellyselden](https://github.com/kellyselden) +- [#6742](https://github.com/ember-cli/ember-cli/pull/6742) Promote cacheKeyForTree to public API [@trentmwillis](https://github.com/trentmwillis) +- [#6734](https://github.com/ember-cli/ember-cli/pull/6734) chore(package): update broccoli-concat to version 3.1.1 [@stefanpenner](https://github.com/ember-cli) +- [#6739](https://github.com/ember-cli/ember-cli/pull/6739) Remove bower.json files again [@Turbo87](https://github.com/Turbo87) +- [#6728](https://github.com/ember-cli/ember-cli/pull/6728) remove application.hbs newline [@stefanpenner](https://github.com/ember-cli) +- [#6736](https://github.com/ember-cli/ember-cli/pull/6736) start using filesToRemove [@kellyselden](https://github.com/kellyselden) +- [#6748](https://github.com/ember-cli/ember-cli/pull/6748) Use yarn if yarn.lock exists or `--yarn` is used [@Turbo87](https://github.com/Turbo87) +- [#6805](https://github.com/ember-cli/ember-cli/pull/6805) more old file cleanup [@kellyselden](https://github.com/kellyselden) +- [#6789](https://github.com/ember-cli/ember-cli/pull/6789) Support npm packages as `ember new` blueprints [@Turbo87](https://github.com/Turbo87) +- [#6758](https://github.com/ember-cli/ember-cli/pull/6758) Fixes blueprints noop log removals [@gadogado](https://github.com/gadogado) +- [#6768](https://github.com/ember-cli/ember-cli/pull/6768) Normalize end-of-line characters in strings to compare prior to diffing [@koopa](https://github.com/koopa) +- [#6785](https://github.com/ember-cli/ember-cli/pull/6785) Refactor InstallBlueprintTask class [@Turbo87](https://github.com/Turbo87) +- [#6776](https://github.com/ember-cli/ember-cli/pull/6776) Implement targets RFC [@cibernox](https://github.com/cibernox) +- [#6778](https://github.com/ember-cli/ember-cli/pull/6778) Don't print heimdall stack on errors [@stefanpenner](https://github.com/ember-cli) +- [#6766](https://github.com/ember-cli/ember-cli/pull/6766) Remove flagging for `experiments.INSTRUMENTATION`. [@stefanpenner](https://github.com/ember-cli) +- [#6759](https://github.com/ember-cli/ember-cli/pull/6759) Enable instrumentation experiment with public `instrument` method. [@rwjblue](https://github.com/rwjblue) +- [#6756](https://github.com/ember-cli/ember-cli/pull/6756) `yarn upgrade` [@rwjblue](https://github.com/rwjblue) +- [#6754](https://github.com/ember-cli/ember-cli/pull/6754) Interrupt command with an error if no _currentTask [@ro0gr](https://github.com/ro0gr) +- [#6825](https://github.com/ember-cli/ember-cli/pull/6825) EmberApp: Use "src/ui/index.html" if it exists [@Turbo87](https://github.com/Turbo87) +- [#6797](https://github.com/ember-cli/ember-cli/pull/6797) Remove "proxyquire" dependency [@Turbo87](https://github.com/Turbo87) +- [#6792](https://github.com/ember-cli/ember-cli/pull/6792) package.json: Remove "npm" from greenkeeper ignore list [@stefanpenner](https://github.com/ember-cli) +- [#6791](https://github.com/ember-cli/ember-cli/pull/6791) Convert EmberApp and EmberAddon to ES6 classes [@Turbo87](https://github.com/Turbo87) +- [#6840](https://github.com/ember-cli/ember-cli/pull/6840) Add logging for `this.runTask` within commands. [@rwjblue](https://github.com/rwjblue) +- [#6804](https://github.com/ember-cli/ember-cli/pull/6804) Remove missing init instrumentation warning [@stefanpenner](https://github.com/ember-cli) +- [#6800](https://github.com/ember-cli/ember-cli/pull/6800) tests/blueprints: Use arrow functions for callbacks [@Turbo87](https://github.com/Turbo87) +- [#6799](https://github.com/ember-cli/ember-cli/pull/6799) Refactor `capture-exit` usage to avoid releasing exit. [@rwjblue](https://github.com/rwjblue) +- [#6845](https://github.com/ember-cli/ember-cli/pull/6845) Convert more promise chains to coroutines [@Turbo87](https://github.com/Turbo87) +- [#6806](https://github.com/ember-cli/ember-cli/pull/6806) Adds eslint-plugin-mocha [@gadogado](https://github.com/gadogado) +- [#6835](https://github.com/ember-cli/ember-cli/pull/6835) EmberApp: Use "src/ui/styles" over "app/styles" if it exists [@Turbo87](https://github.com/Turbo87) +- [#6824](https://github.com/ember-cli/ember-cli/pull/6824) chore(package): update broccoli-merge-trees to version 1.2.3 [@stefanpenner](https://github.com/ember-cli) +- [#6816](https://github.com/ember-cli/ember-cli/pull/6816) Resolve path when calling UnwatchedDir for Bower [@arthirm](https://github.com/arthirm) +- [#6821](https://github.com/ember-cli/ember-cli/pull/6821) Add tests for EmberApp.index() method [@Turbo87](https://github.com/Turbo87) +- [#6828](https://github.com/ember-cli/ember-cli/pull/6828) Use Babel 6 [@stefanpenner](https://github.com/ember-cli) +- [#6839](https://github.com/ember-cli/ember-cli/pull/6839) Allow `ember new -b foo` to opt-in to yarn by default. [@rwjblue](https://github.com/rwjblue) +- [#6846](https://github.com/ember-cli/ember-cli/pull/6846) tests: Use "chai-as-promised" assertions [@Turbo87](https://github.com/Turbo87) +- [#6847](https://github.com/ember-cli/ember-cli/pull/6847) print the `serving on http://host:port/basePath` after each rebuild [@stefanpenner](https://github.com/ember-cli) +- [#6852](https://github.com/ember-cli/ember-cli/pull/6852) Replace "itr2array" helper with Array.from() [@Turbo87](https://github.com/Turbo87) +- [#6853](https://github.com/ember-cli/ember-cli/pull/6853) tests: Remove unused variables [@Turbo87](https://github.com/Turbo87) +- [#6857](https://github.com/ember-cli/ember-cli/pull/6857) Update testdouble to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) Thank you to all who took the time to contribute! -### 0.2.5 + +### 2.12.3 The following changes are required if you are upgrading from the previous version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/f49b35bbb243b6e3b8e20fb2a9c69a2fa13a6aec) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + package.json - + Upgrade `ember-cli-qunit` to `0.3.13`. - + Make sure that `ember-cli-dependency-checker` is using caret `^1.0.0`. - + bower.json - + Upgrade `ember-qunit` to `0.3.3`. -- Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/8ef831d2df8abad6445ca7bfa732518c6d8777af) - + No changes required -- Core Contributors -+ No changes required - - -#### Community Contributions - -- [#4076](https://github.com/ember-cli/ember-cli/pull/4076) Use caret version for stable dependencies in project blueprint. [@abuiles](https://github.com/abuiles) -- [#4087](https://github.com/ember-cli/ember-cli/pull/4087) Bump ember-cli-qunit to v0.3.13 (ember-qunit@0.3.3). [@rwjblue](https://github.com/rwjblue) - -### 0.2.4 - -The following changes are required if you are upgrading from the previous -version: - -- Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/964c80924d665adfde3ce31acaac5c26b95a1bc0) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + Apps now have [ember-disable-proxy-controllers](https://github.com/cibernox/ember-disable-proxy-controllers) - included by default, this ensures that autogenerated controllers - always are regular `Ember.Controller` instead of the deprecated - proxy ones. This does not affect explicitly created controllers. - + Generated routes always use `this.route` (`this.resource` is no longer used). - + The command `ember install:bower` has been removed. - + Pod components can now be generated outside the - `app/pods/components` (or `app/components` sans podModulePrefix) - folder with the `--path` option. `ember g component foo-bar -p - -path foo` generates into `app/foo/foo-bar/component.js` - + The `ember new` command now has a `--directory` option, allowing - you to generate into a directory that differs from your app - name. `ember new foo -dir bar` generates an app named `foo` into a - directory named `bar`. - + Generated apps no longer have `podModulePrefix` in the config. - + All blueprints have been updated to use shorthand ES6 syntax for importing and exporting. - + package.json - + Upgrade `ember-cli-qunit` to `0.3.12` - + Upgrade `ember-cli-dependency-checker` to `1.0.0` - + bower.json - + Bundled ember `v1.12` - + Upgrade bower.json `ember-qunit` to `0.3.2` for glimmer support. -- Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/4175b66d0911c9ea454daaefb219d11b334f1bab) - + No changes required -- Core Contributors - + No changes required - -#### Community Contributions - -- [#3965](https://github.com/ember-cli/ember-cli/pull/3965) fixup doc generator test [@stefanpenner](https://github.com/stefanpenner) -- [#3822](https://github.com/ember-cli/ember-cli/pull/3822) adding 0.2.3 diffs [@kellyselden](https://github.com/kellyselden) -- [#3384](https://github.com/ember-cli/ember-cli/pull/3384) Test powershell for appveyor builds [@stefanpenner](https://github.com/stefanpenner) -- [#3771](https://github.com/ember-cli/ember-cli/pull/3771) [ENHANCEMENT] Support custom node_module paths [@jakehow](https://github.com/jakehow) -- [#3820](https://github.com/ember-cli/ember-cli/pull/3820) [ENHANCEMENT] Change blueprint command options to type String to avoid nopt transformations [@rodyhaddad](https://github.com/rodyhaddad) -- [#3698](https://github.com/ember-cli/ember-cli/pull/3698) adding docker for linux testing/debugging [@kellyselden](https://github.com/kellyselden) -- [#3973](https://github.com/ember-cli/ember-cli/pull/3973) Config cache unc share [@stefanpenner](https://github.com/stefanpenner) -- [#3836](https://github.com/ember-cli/ember-cli/pull/3836) Suggestion: Adding test coverage to pull requests [@kellyselden](https://github.com/kellyselden) -- [#3827](https://github.com/ember-cli/ember-cli/pull/3827) [BUGFIX] Fixes availableOptions in custom blueprints [@trabus](https://github.com/trabus) -- [#3825](https://github.com/ember-cli/ember-cli/pull/3825) Exclude dist/ from addon npm publishes by default [@jayphelps](https://github.com/jayphelps) -- [#3826](https://github.com/ember-cli/ember-cli/pull/3826) [fixes #3712] rethrow errors in build task [@marcioj](https://github.com/marcioj) -- [#3978](https://github.com/ember-cli/ember-cli/pull/3978) Update broccoli-es6modules [@marcioj](https://github.com/marcioj) -- [#3882](https://github.com/ember-cli/ember-cli/pull/3882) Removes bower install command. [@willrax](https://github.com/willrax) -- [#3869](https://github.com/ember-cli/ember-cli/pull/3869) [BUGFIX] Use posix path for in-repo-addons in package.json [@trabus](https://github.com/trabus) -- [#3846](https://github.com/ember-cli/ember-cli/pull/3846) [BUGFIX] Prevent addon-import blueprint from generating if entity name is undefined [@trabus](https://github.com/trabus) -- [#3851](https://github.com/ember-cli/ember-cli/pull/3851) Use shorthand ES6 syntax for addon -> app re-exports [@jayphelps](https://github.com/jayphelps) -- [#3848](https://github.com/ember-cli/ember-cli/pull/3848) writeError now looks for filename as well as file [@wagenet](https://github.com/wagenet) -- [#3858](https://github.com/ember-cli/ember-cli/pull/3858) move github to normal dependencies to hack around: https://github.com/np... [@stefanpenner](https://github.com/stefanpenner) -- [#3856](https://github.com/ember-cli/ember-cli/pull/3856) Use shorthand ES6 re-export for addon-imports as well, which landed in #3690 [@jayphelps](https://github.com/jayphelps) -- [#3842](https://github.com/ember-cli/ember-cli/pull/3842) Add remove packages [@jonathanKingston](https://github.com/jonathanKingston) -- [#3845](https://github.com/ember-cli/ember-cli/pull/3845) [BUGFIX] Fix ability to generate blueprints (blueprint, http-mock, http-proxy, and tests) inside addons [@trabus](https://github.com/trabus) -- [#3853](https://github.com/ember-cli/ember-cli/pull/3853) Cache `node_modules` and `bower_components` in CI [@seanpdoyle](https://github.com/seanpdoyle) -- [#3994](https://github.com/ember-cli/ember-cli/pull/3994) update sane + broccoli-sane-watcher [@stefanpenner](https://github.com/stefanpenner) -- [#3949](https://github.com/ember-cli/ember-cli/pull/3949) adding a shared folder with host, and fixing git PATH [@kellyselden](https://github.com/kellyselden) -- [#3937](https://github.com/ember-cli/ember-cli/pull/3937) [BUGFIX] Merge app/styles from addons with overwrite: true. Fixes #3930. [@yapplabs](https://github.com/yapplabs) -- [#3895](https://github.com/ember-cli/ember-cli/pull/3895) [Enhancement] PhantomJS 2.0 running on travis-ci [@truenorth](https://github.com/truenorth) -- [#3921](https://github.com/ember-cli/ember-cli/pull/3921) [Enhancement] Ember-try & parallel travis-ci scenario tests for addons [@truenorth](https://github.com/truenorth) -- [#3936](https://github.com/ember-cli/ember-cli/pull/3936) Replace 'this.resource' with 'this.route' in generators [@HeroicEric](https://github.com/HeroicEric) -- [#3946](https://github.com/ember-cli/ember-cli/pull/3946) [ENHANCEMENT] Add host option to `ember test`. [@wangjohn](https://github.com/wangjohn) -- [#3909](https://github.com/ember-cli/ember-cli/pull/3909) test blueprints now use consistent, less-opinionated import style [@jayphelps](https://github.com/jayphelps) -- [#3891](https://github.com/ember-cli/ember-cli/pull/3891) Fixes problem in initializer tests generated in addons [@marcioj](https://github.com/marcioj) -- [#3889](https://github.com/ember-cli/ember-cli/pull/3889) Updating dev folder to help with debugging [@kellyselden](https://github.com/kellyselden) -- [#3916](https://github.com/ember-cli/ember-cli/pull/3916) Disable directory listings on development server [@joliss](https://github.com/joliss) -- [#3887](https://github.com/ember-cli/ember-cli/pull/3887) Bump ember router generator and allow index routes [@abuiles](https://github.com/abuiles) -- [#3915](https://github.com/ember-cli/ember-cli/pull/3915) Always create addon trees when developing an addon [@marcioj](https://github.com/marcioj) -- [#3901](https://github.com/ember-cli/ember-cli/pull/3901) bump blueprints to latest released ember [@stefanpenner](https://github.com/stefanpenner) -- [#3913](https://github.com/ember-cli/ember-cli/pull/3913) Remove podModulePrefix from app.js [@knownasilya](https://github.com/knownasilya) -- [#3945](https://github.com/ember-cli/ember-cli/pull/3945) [ENHANCEMENT] Friendly test names and descriptions [@eccegordo/feature](https://github.com/eccegordo/feature) -- [#3922](https://github.com/ember-cli/ember-cli/pull/3922) Export test output dir via ENV [@ef4](https://github.com/ef4) -- [#3919](https://github.com/ember-cli/ember-cli/pull/3919) Remove connect-restreamer. [@abuiles](https://github.com/abuiles) -- [#4021](https://github.com/ember-cli/ember-cli/pull/4021) Allow custom history location types. [@stefanpenner](https://github.com/stefanpenner) -- [#3950](https://github.com/ember-cli/ember-cli/pull/3950) allow override of os.EOL in tests [@stefanpenner](https://github.com/stefanpenner) -- [#3962](https://github.com/ember-cli/ember-cli/pull/3962) Disable any file watching done by testem [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3968](https://github.com/ember-cli/ember-cli/pull/3968) node-glob doesn’t work with windows shares… [@stefanpenner](https://github.com/stefanpenner) -- [#3951](https://github.com/ember-cli/ember-cli/pull/3951) [ENHANCEMENT] Add --directory flag to `ember new` [@HeroicEric](https://github.com/HeroicEric) -- [#3956](https://github.com/ember-cli/ember-cli/pull/3956) Bump ember-route-generator to match #3936. [@abuiles](https://github.com/abuiles) -- [#3954](https://github.com/ember-cli/ember-cli/pull/3954) [ENHANCEMENT] Generate component pods outside components folder [@trabus](https://github.com/trabus) -- [#3958](https://github.com/ember-cli/ember-cli/pull/3958) Fix indentation in `crossdomain.xml` [@arthurvr](https://github.com/arthurvr) -- [#3967](https://github.com/ember-cli/ember-cli/pull/3967) allow override of os.EOL in tests [@stefanpenner](https://github.com/stefanpenner) -- [#3959](https://github.com/ember-cli/ember-cli/pull/3959) Add `Disallow:` to robots.txt [@arthurvr](https://github.com/arthurvr) -- [#3966](https://github.com/ember-cli/ember-cli/pull/3966) increase timeouts, and use mocha’s inheriting config [@stefanpenner](https://github.com/stefanpenner) -- [#4039](https://github.com/ember-cli/ember-cli/pull/4039) Update ember-qunit to support glimmer [@knownasilya](https://github.com/knownasilya) -- [#4000](https://github.com/ember-cli/ember-cli/pull/4000) use `escape-string-regexp` module [@sindresorhus](https://github.com/sindresorhus) -- [#3975](https://github.com/ember-cli/ember-cli/pull/3975) included modules is no longer needed [@stefanpenner](https://github.com/stefanpenner) -- [#3982](https://github.com/ember-cli/ember-cli/pull/3982) Changed markdown-color blue to bright-blue to be the same on all platforms [@trabus](https://github.com/trabus) -- [#3984](https://github.com/ember-cli/ember-cli/pull/3984) Allow io.js-next in development in 'valid-platform-version' [@laiso](https://github.com/laiso) -- [#3974](https://github.com/ember-cli/ember-cli/pull/3974) Resolve sync [@stefanpenner](https://github.com/stefanpenner) -- [#3976](https://github.com/ember-cli/ember-cli/pull/3976) Relative require [@stefanpenner](https://github.com/stefanpenner) -- [#3996](https://github.com/ember-cli/ember-cli/pull/3996) Warns when npm or bower dependencies aren't installed [@marcioj](https://github.com/marcioj) -- [#4024](https://github.com/ember-cli/ember-cli/pull/4024) Appveyor: Use `run` instead of `run-script` [@knownasilya](https://github.com/knownasilya) -- [#4033](https://github.com/ember-cli/ember-cli/pull/4033) Bump ember-cli-dependency-checker to v0.1.0 [@quaertym](https://github.com/quaertym) -- [#4027](https://github.com/ember-cli/ember-cli/pull/4027) Re-order postBuild hook [@chadhietala](https://github.com/chadhietala) -- [#4008](https://github.com/ember-cli/ember-cli/pull/4008) Disable leek for `ember -v` [@twokul](https://github.com/twokul) -- [#4020](https://github.com/ember-cli/ember-cli/pull/4020) Allowed failures [@stefanpenner](https://github.com/stefanpenner) -- [#4007](https://github.com/ember-cli/ember-cli/pull/4007) Hide Python on appveyor so npm won't build native extentions [@raytiley](https://github.com/raytiley) -- [#4022](https://github.com/ember-cli/ember-cli/pull/4022) Run all tests again [@marcioj](https://github.com/marcioj) -- [#4032](https://github.com/ember-cli/ember-cli/pull/4032) Update ember-cli-qunit to v0.3.2 [@HeroicEric](https://github.com/HeroicEric) -- [#4037](https://github.com/ember-cli/ember-cli/pull/4037) Add ember-disable-proxy-controllers to app blueprint [@cibernox](https://github.com/cibernox) -- [#4046](https://github.com/ember-cli/ember-cli/pull/4046) Upgrade ember-cli-htmlbars to 0.7.6 [@teddyzeenny](https://github.com/teddyzeenny) -- [#4057](https://github.com/ember-cli/ember-cli/pull/4057) [INTERNAL] Fix tests to expect single line qunit import [@trabus](https://github.com/trabus) -- [#4058](https://github.com/ember-cli/ember-cli/pull/4058) Bump ember-cli-dependency-checker to v1.0.0 [@quaertym](https://github.com/quaertym) -- [#4059](https://github.com/ember-cli/ember-cli/pull/4059) Update Ember-data to beta 17 [@cibernox](https://github.com/cibernox) -- [#4065](https://github.com/ember-cli/ember-cli/pull/4065) Update to Ember 1.12.0. [@rwjblue](https://github.com/rwjblue) - -Thank you to all who took the time to contribute! - - -### 0.2.3 - -The following changes are required if you are upgrading from the previous -version: - -- Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/0aaabc98378600e116da0fcc5b75c1a8b00ce541) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + `ember install ` now is the correct way to install an add-on (not `ember install:npm `) - + babel has been upgraded to `5.0.0`, be sure any configuration to babel is updated accordingly - + bundled ember is now 1.11.1 - + when existing test --server, tmp files should once again be correctly cleaned up. -- Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/567f9f2db157ce835c116ffde1567cc8c709ae0c) - + No changes required -- Core Contributors - + No changes required - + Code Climate was added: https://codeclimate.com/github/ember-cli/ember-cli, - we have been making steady progress in improving our code quality and coverage. - As new code enters the system, we should ensure we continue to improve. - -#### Community Contributions - -- [#3782](https://github.com/ember-cli/ember-cli/pull/3782) add OS X as a CI target for travis [@stefanpenner](https://github.com/stefanpenner) -- [#3711](https://github.com/ember-cli/ember-cli/pull/3711) adding changelog diffs [@kellyselden](https://github.com/kellyselden) -- [#3703](https://github.com/ember-cli/ember-cli/pull/3703) [ENHANCEMENT] Add testem --launch option to ember test command [@jrjohnson](https://github.com/jrjohnson) -- [#3598](https://github.com/ember-cli/ember-cli/pull/3598) [ENHANCEMENT] Replace install:addon with install, remove install:bower and install:npm [@DanielOchoa](https://github.com/DanielOchoa) -- [#3690](https://github.com/ember-cli/ember-cli/pull/3690) [ENHANCEMENT] Addon-import support for built-in blueprints [@trabus](https://github.com/trabus) -- [#3700](https://github.com/ember-cli/ember-cli/pull/3700) fixes #3613 - added path.normalize [@swelham](https://github.com/swelham) -- [#3412](https://github.com/ember-cli/ember-cli/pull/3412) Changes match application regex [@twokul](https://github.com/twokul) -- [#3789](https://github.com/ember-cli/ember-cli/pull/3789) Code Quality: ember-app.js F -> D [@kellyselden](https://github.com/kellyselden) -- [#3731](https://github.com/ember-cli/ember-cli/pull/3731) Promise cleanup [@stefanpenner](https://github.com/stefanpenner) -- [#3722](https://github.com/ember-cli/ember-cli/pull/3722) Updated included hook example [@RSSchermer](https://github.com/RSSchermer) -- [#3713](https://github.com/ember-cli/ember-cli/pull/3713) The unbundling [@stefanpenner](https://github.com/stefanpenner) -- [#3725](https://github.com/ember-cli/ember-cli/pull/3725) increase timeouts, and use mocha’s inheriting config strategy to prevent... [@stefanpenner](https://github.com/stefanpenner) -- [#3727](https://github.com/ember-cli/ember-cli/pull/3727) misc cleanup [@stefanpenner](https://github.com/stefanpenner) -- [#3794](https://github.com/ember-cli/ember-cli/pull/3794) BUGFIX fixes vars-on-top error in ESLint [@jonathanKingston](https://github.com/jonathanKingston) -- [#3759](https://github.com/ember-cli/ember-cli/pull/3759) Order bower dependencies alphabetically [@pmdarrow](https://github.com/pmdarrow) -- [#3736](https://github.com/ember-cli/ember-cli/pull/3736) [fixes #3732] configure YAM with the Project.root. [@stefanpenner](https://github.com/stefanpenner) -- [#3743](https://github.com/ember-cli/ember-cli/pull/3743) no longer bundle testem, allow it to drift along semver [@stefanpenner](https://github.com/stefanpenner) -- [#3756](https://github.com/ember-cli/ember-cli/pull/3756) adding a blueprint uninstall test [@kellyselden](https://github.com/kellyselden) -- [#3750](https://github.com/ember-cli/ember-cli/pull/3750) add developer requirements to CONTRIBUTING.md [@jakehow](https://github.com/jakehow) -- [#3748](https://github.com/ember-cli/ember-cli/pull/3748) Fix wording [@jbrown](https://github.com/jbrown) -- [#3740](https://github.com/ember-cli/ember-cli/pull/3740) Remove dead code [@IanVS](https://github.com/IanVS) -- [#3747](https://github.com/ember-cli/ember-cli/pull/3747) code quality refactor of blueprint model [@kellyselden](https://github.com/kellyselden) -- [#3755](https://github.com/ember-cli/ember-cli/pull/3755) return currentURL() rather than path, ref #3719 [@mariogintili](https://github.com/mariogintili) -- [#3800](https://github.com/ember-cli/ember-cli/pull/3800) [fixes #3799] fix jshint [@stefanpenner](https://github.com/stefanpenner) -- [#3780](https://github.com/ember-cli/ember-cli/pull/3780) Upgrade to npm 2.7.6 [@davewasmer](https://github.com/davewasmer) -- [#3775](https://github.com/ember-cli/ember-cli/pull/3775) Code quality blueprint duplicates [@kellyselden](https://github.com/kellyselden) -- [#3762](https://github.com/ember-cli/ember-cli/pull/3762) Improved serializer-test blueprint [@bmac](https://github.com/bmac) -- [#3778](https://github.com/ember-cli/ember-cli/pull/3778) bump to babel 5.0 [@stefanpenner](https://github.com/stefanpenner) -- [#3764](https://github.com/ember-cli/ember-cli/pull/3764) Version bump ember-load-initializers to handle instance initializers [@jasonmit](https://github.com/jasonmit) -- [#3769](https://github.com/ember-cli/ember-cli/pull/3769) Babel 5.0 now separates codeFrame from error.{message, stack} [@stefanpenner](https://github.com/stefanpenner) -- [#3804](https://github.com/ember-cli/ember-cli/pull/3804) increase some timeouts and prefer mocha’s inheriting timers [@stefanpenner](https://github.com/stefanpenner) -- [#3798](https://github.com/ember-cli/ember-cli/pull/3798) adding coverage badge to readme [@stefanpenner](https://github.com/stefanpenner) -- [#3781](https://github.com/ember-cli/ember-cli/pull/3781) bump to a non-vulnerable semver module [@stefanpenner](https://github.com/stefanpenner) -- [#3784](https://github.com/ember-cli/ember-cli/pull/3784) Add ember-try for addons. [@rwjblue](https://github.com/rwjblue) -- [#3795](https://github.com/ember-cli/ember-cli/pull/3795) Code Climate: adding test coverage [@kellyselden](https://github.com/kellyselden) -- [#3797](https://github.com/ember-cli/ember-cli/pull/3797) Update to ember-qunit 0.3.1. [@rwjblue](https://github.com/rwjblue) -- [#3801](https://github.com/ember-cli/ember-cli/pull/3801) moving coverage repo key to travis env variable [@kellyselden](https://github.com/kellyselden) -- [#3802](https://github.com/ember-cli/ember-cli/pull/3802) remove pre-mature process.exit when existing `ember test —server` [@stefanpenner](https://github.com/stefanpenner) -- [#3803](https://github.com/ember-cli/ember-cli/pull/3803) update testem [@stefanpenner](https://github.com/stefanpenner) -- [#3809](https://github.com/ember-cli/ember-cli/pull/3809) Fix url format of isGitRepo [@stefanpenner](https://github.com/stefanpenner) -- [#3812](https://github.com/ember-cli/ember-cli/pull/3812) update ember to 1.11.1 in the default blueprint [@stefanpenner](https://github.com/stefanpenner) - -Thank you to all who took the time to contribute! - -### 0.2.2 - -The following changes are required if you are upgrading from the previous -version: - -- Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/1c47557e629d88ec399786bd3f06995a251e6f0f) - + updated to ember 1.11.0 - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + `ember init` once again works inside an addon. - + error live-reloading now actually works! - + npm WARN for `makeError` and `tmpl` have been fixed - + ember-qunit was updated from `0.2.8` -> `0.3.0`, `this.render()` in a test now no-longer returns a jQuery object. - -- Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/4bb6c82e5411560c6d21517755d6a2276bad9a39) - + Addons now have `ember-disable-prototype-extensions` included by default, - this ensures add-ons are written in a way that works regardless of the - consumers prototype extension preference. - - + the following addon API's in has been deprecated: - * `this.mergeTrees` -> `require('mergeTrees');` - * `this.Funnel` -> `require('broccoli-funnel');` - * `this.pickFiles` -> `require('broccoli-funnel');` - * `this.walkSync` -> `require('walk-sync');` - * `this.transpileModules` -> `require('broccoli-es6modules');` - - Rather then relying on them from ember-cli, add-ons should require them via NPM. - - + We now are using broccoli v0.15.3, which is a backwards compatible upgrade, - but it does expose the new `rebuild` api, that will soon superseed the `read` - api. TL;DR among other things, this paves the path to having a configurable - tmp directory. - - We recommend broccoli-plugin authors to update as soon as they are able to. - - For more information checkout: [new rebuild api](https://github.com/broccolijs/broccoli/blob/master/docs/new-rebuild-api.md) - -- Core Contributors - - + Keep being awesome! - -#### Community Contributions - -- [#3560](https://github.com/ember-cli/ember-cli/pull/3560) fixing the formatting from one line to two [@kellyselden](https://github.com/kellyselden) -- [#3622](https://github.com/ember-cli/ember-cli/pull/3622) [BUGFIX] Fix ember init inside an existing addon [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3469](https://github.com/ember-cli/ember-cli/pull/3469) [ENHANCEMENT] Update component-test test.js blueprint [@simonprev](https://github.com/simonprev) -- [#3565](https://github.com/ember-cli/ember-cli/pull/3565) [BUGFIX] temporarily disable podModulePrefix deprecation [@trabus](https://github.com/trabus) -- [#3601](https://github.com/ember-cli/ember-cli/pull/3601) Allow Node 0.13 in platform deprecation check. [@rwjblue](https://github.com/rwjblue) -- [#3585](https://github.com/ember-cli/ember-cli/pull/3585) [ENHANCEMENT] Add in-repo-addon generate and destroy support [@trabus](https://github.com/trabus) -- [#3674](https://github.com/ember-cli/ember-cli/pull/3674) Update nock dependency [@btecu](https://github.com/btecu) -- [#3636](https://github.com/ember-cli/ember-cli/pull/3636) [fixes #3618] we will add some acceptance tests in this area soon (rushi... [@stefanpenner](https://github.com/stefanpenner) -- [#3634](https://github.com/ember-cli/ember-cli/pull/3634) Resolves #3628 postprocessTree for styles with vendor + app [@jschilli](https://github.com/jschilli) -- [#3630](https://github.com/ember-cli/ember-cli/pull/3630) Fix minor typo's [@QuantumInformation](https://github.com/QuantumInformation) -- [#3631](https://github.com/ember-cli/ember-cli/pull/3631) [Documentation] adding new ember new and ember addon diffs [@kellyselden](https://github.com/kellyselden) -- [#3680](https://github.com/ember-cli/ember-cli/pull/3680) Updates [@stefanpenner](https://github.com/stefanpenner) -- [#3645](https://github.com/ember-cli/ember-cli/pull/3645) Add ember-disable-prototype-extensions to addons by default. [@rwjblue](https://github.com/rwjblue) -- [#3642](https://github.com/ember-cli/ember-cli/pull/3642) Check if style file with project name exists [@btecu](https://github.com/btecu) -- [#3639](https://github.com/ember-cli/ember-cli/pull/3639) Bump ember-data to beta-16.1 [@bmac](https://github.com/bmac) -- [#3682](https://github.com/ember-cli/ember-cli/pull/3682) strip ansi from babel errors for now. [@stefanpenner](https://github.com/stefanpenner) -- [#3655](https://github.com/ember-cli/ember-cli/pull/3655) Uses Ember.keys instead of Object.keys in reexport [@danmcclain](https://github.com/danmcclain) -- [#3646](https://github.com/ember-cli/ember-cli/pull/3646) Add `chai` as dependency. [@rwjblue](https://github.com/rwjblue) -- [#3647](https://github.com/ember-cli/ember-cli/pull/3647) add timeouts until we improve the mocha <-> custom runner timeout stuff [@stefanpenner](https://github.com/stefanpenner) -- [#3648](https://github.com/ember-cli/ember-cli/pull/3648) Update broccoli-sane-watcher. [@rwjblue](https://github.com/rwjblue) -- [#3691](https://github.com/ember-cli/ember-cli/pull/3691) Update Ember to 1.11.0. [@rwjblue](https://github.com/rwjblue) -- [#3675](https://github.com/ember-cli/ember-cli/pull/3675) Restore addon pick files [@stefanpenner](https://github.com/stefanpenner) -- [#3673](https://github.com/ember-cli/ember-cli/pull/3673) Update Broccoli to 0.15.3 [@joliss](https://github.com/joliss) -- [#3672](https://github.com/ember-cli/ember-cli/pull/3672) Use broccoli-funnel instead of broccoli-static-compiler [@joliss](https://github.com/joliss) -- [#3666](https://github.com/ember-cli/ember-cli/pull/3666) Tweaks [@stefanpenner](https://github.com/stefanpenner) -- [#3669](https://github.com/ember-cli/ember-cli/pull/3669) Update dependencies [@btecu](https://github.com/btecu) -- [#3677](https://github.com/ember-cli/ember-cli/pull/3677) Export return value from Router.map (closes #3676). [@abuiles](https://github.com/abuiles) -- [#3681](https://github.com/ember-cli/ember-cli/pull/3681) Deprecate funnel and pickfiles [@stefanpenner](https://github.com/stefanpenner) -- [#3692](https://github.com/ember-cli/ember-cli/pull/3692) Replace lodash-node with lodash [@btecu](https://github.com/btecu) -- [#3696](https://github.com/ember-cli/ember-cli/pull/3696) Update markdown-it and markdown-it-terminal [@stefanpenner](https://github.com/stefanpenner) -- [#3704](https://github.com/ember-cli/ember-cli/pull/3704) Live reload fix [@stefanpenner](https://github.com/stefanpenner) -- [#3705](https://github.com/ember-cli/ember-cli/pull/3705) Fix initial commit message [@xymbol](https://github.com/xymbol) - -Thank you to all who took the time to contribute! - -### 0.2.1 - -The following changes are required if you are upgrading from the previous -version: - -- Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/e4d36aa2ce99ebb288cd596270e7b38da90f535e) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + Mostly just bug-fixes and "Nice things" - + build errors now live-reload and correctly display build failure in the browser. [more-details](https://github.com/ember-cli/ember-cli/pull/3576) -- Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/5d87ed789651b1fbecf9a30d7b82eb86e0629bd2) - + UI is now provided to the AddonDiscovery - + ember-cli-babel is now included in the default blueprint, this is still optional but enabled by default + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.12.2...v2.12.3) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.12.2...v2.12.3) + + No changes required +- Core Contributors + + No changes required #### Community Contributions -- [#3555](https://github.com/ember-cli/ember-cli/pull/3555) [BUGFIX] Generate mixin in addon/mixins when inside an addon project [@trabus](https://github.com/trabus) -- [#3476](https://github.com/ember-cli/ember-cli/pull/3476) Removes initializer mention from service generator help text [@corpulentcoffee](https://github.com/corpulentcoffee) -- [#3433](https://github.com/ember-cli/ember-cli/pull/3433) [ENHANCEMENT] Prevent addon generation in existing ember-cli project [@cbrock](https://github.com/cbrock) -- [#3463](https://github.com/ember-cli/ember-cli/pull/3463) disable visual progress effect in dumb terminals [@jesse-black](https://github.com/jesse-black) -- [#3440](https://github.com/ember-cli/ember-cli/pull/3440) Enforcing newlines in template files results in unwanted Nodes [@jclem](https://github.com/jclem) -- [#3484](https://github.com/ember-cli/ember-cli/pull/3484) Component blueprint only import layout when generated inside addon [@trabus](https://github.com/trabus) -- [#3505](https://github.com/ember-cli/ember-cli/pull/3505) Update testem to 0.7.5 [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3481](https://github.com/ember-cli/ember-cli/pull/3481) BUGFIX Fixes #3472 Check for 'usePods' instead of 'pod'. [@jankrueger](https://github.com/jankrueger) -- [#3493](https://github.com/ember-cli/ember-cli/pull/3493) Fix helper test failing by default [@kimroen](https://github.com/kimroen) -- [#3488](https://github.com/ember-cli/ember-cli/pull/3488) ENHANCEMENT: update-checker.js should use environment http_proxy if detected [@xomaczar](https://github.com/xomaczar) -- [#3483](https://github.com/ember-cli/ember-cli/pull/3483) Ensure that addons pass the `ui` into their AddonDiscovery. [@rwjblue](https://github.com/rwjblue) -- [#3501](https://github.com/ember-cli/ember-cli/pull/3501) [Enhancement] Architecture Diagram [@visheshjoshi](https://github.com/visheshjoshi) -- [#3562](https://github.com/ember-cli/ember-cli/pull/3562) dist can be watched, it really is just tmp that matters. This prevents p... [@stefanpenner](https://github.com/stefanpenner) -- [#3540](https://github.com/ember-cli/ember-cli/pull/3540) [fixes #3520, #3174] disable ES3SafeFilter if babel is present, as babel... [@stefanpenner](https://github.com/stefanpenner) -- [#3508](https://github.com/ember-cli/ember-cli/pull/3508) Update ember-cli-app-version [@btecu](https://github.com/btecu) -- [#3518](https://github.com/ember-cli/ember-cli/pull/3518) [BUGFIX] Add missing bind when server already in use [@bdvholmes](https://github.com/bdvholmes) -- [#3539](https://github.com/ember-cli/ember-cli/pull/3539) add tmp dir to npmignore [@ahmadsoe](https://github.com/ahmadsoe) -- [#3515](https://github.com/ember-cli/ember-cli/pull/3515) [BUGFIX] Fixes nested component generation in addons with correct relative path for template import [@trabus](https://github.com/trabus) -- [#3517](https://github.com/ember-cli/ember-cli/pull/3517) Use node 0.12 on Windows CI [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3535](https://github.com/ember-cli/ember-cli/pull/3535) Update ADDON_HOOKS.md [@ahmadsoe](https://github.com/ahmadsoe) -- [#3533](https://github.com/ember-cli/ember-cli/pull/3533) [BUGFIX] Replace marked with markdown-it [@trabus](https://github.com/trabus) -- [#3583](https://github.com/ember-cli/ember-cli/pull/3583) Updated license copyright date range [@jayphelps](https://github.com/jayphelps) -- [#3546](https://github.com/ember-cli/ember-cli/pull/3546) [ENHANCEMENT] Add podModulePrefix deprecation for generate and destroy commands [@trabus](https://github.com/trabus) -- [#3544](https://github.com/ember-cli/ember-cli/pull/3544) Add links with watchman info to cli output [@felixbuenemann](https://github.com/felixbuenemann) -- [#3545](https://github.com/ember-cli/ember-cli/pull/3545) [BUGFIX] Ensure `package.json` `main` entry point is used for addon lookup. [@rwjblue](https://github.com/rwjblue) -- [#3541](https://github.com/ember-cli/ember-cli/pull/3541) [fixes #3520, #3174] bump es3-safe-recast [@stefanpenner](https://github.com/stefanpenner) -- [#3594](https://github.com/ember-cli/ember-cli/pull/3594) fix broken link [@kellyselden](https://github.com/kellyselden) -- [#3564](https://github.com/ember-cli/ember-cli/pull/3564) Added babel to addons package.json dependencies by default [@jayphelps](https://github.com/jayphelps) -- [#3559](https://github.com/ember-cli/ember-cli/pull/3559) [Documentation] add ref to ember-cli-output and ember-addon-output [@kellyselden](https://github.com/kellyselden) -- [#3571](https://github.com/ember-cli/ember-cli/pull/3571) [BREAKING ENHANCEMENT] Update ember-cli-content-security-policy to v0.4.0 [@sir-dunxalot/enhancement](https://github.com/sir-dunxalot/enhancement) -- [#3572](https://github.com/ember-cli/ember-cli/pull/3572) Specify node version (0.12) for CI [@quaertym](https://github.com/quaertym) -- [#3576](https://github.com/ember-cli/ember-cli/pull/3576) ensure a build-failure is “live-reloaded” to the user. [@stefanpenner](https://github.com/stefanpenner) -- [#3578](https://github.com/ember-cli/ember-cli/pull/3578) Teaches updateChecker about dev builds [@twokul](https://github.com/twokul) -- [#3579](https://github.com/ember-cli/ember-cli/pull/3579) allow for ./server to export express app [@calvinmetcalf](https://github.com/calvinmetcalf) -- [#3581](https://github.com/ember-cli/ember-cli/pull/3581) Resolves #3534 - addon postprocessTrees for styles [@jschilli](https://github.com/jschilli) -- [#3593](https://github.com/ember-cli/ember-cli/pull/3593) add command uninstall:npm [@kellyselden](https://github.com/kellyselden) -- [#3604](https://github.com/ember-cli/ember-cli/pull/3604) Testem update [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3611](https://github.com/ember-cli/ember-cli/pull/3611) Bump ember-data to beta-16 [@bmac](https://github.com/bmac) - -Thank you to all who took the time to contribute! - -### 0.2.0 - -#### Addon Formatting - -Support for addon's without an entry point script (either `index.js` by default or the script specified by ember-addon main in the addon's `package.json` -has been removed. An addon must have at least the following: - -```javascript -module.exports = { - name: "addons-name-here" -}; -``` - -This should *not* pose a problem for the vast majority of addons. - -#### Addon Nesting - -This release updates the way that addons can be nested, and contains some breaking changes in non-default addon configurations. - -Prior versions of Ember CLI maintained a flat addon structure, so that all addons (of any depth) would be added to the consuming -application. This has led to many issues, like the inability to use preprocessors (i.e. ember-cli-htmlbars, ember-cli-sass, etc) -in nested addons. - -For the majority of apps, the update from 0.1.15 to 0.2.0 is non-breaking and should not cause significant concern. - -For addon creators, make sure to update to use the `setupPreprocessorRegistry` hook (documented [here](https://github.com/ember-cli/ember-cli/blob/master/ADDON_HOOKS.md)) -if you need to add a preprocessor to the registry. You can review the update process in -[ember-cli-htmlbars#38](https://github.com/ember-cli/ember-cli-htmlbars/pull/38) or [ember-cli-coffeescript#60](https://github.com/kimroen/ember-cli-coffeescript/pull/60) -which show how to maintain support for both 0.1.x and 0.2.0 in an addon. - -The following changes are required if you are upgrading from the previous version: - -- Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/d3080cd44b2b62cef45e7f723c18c862b7789f9d) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + The 6to5 project has been renamed to Babel. See [the blog post](http://babeljs.io/blog/2015/02/15/not-born-to-die/) for more details. - + The default blueprint has been updated to work with Ember 1.10 by default. - + Update the following packages in your `package.json`: - * Remove `broccoli-ember-hbs-template-compiler`. Uninstall with `npm uninstall --save-dev broccoli-ember-hbs-template-compiler`. - * Remove `ember-cli-6to5`. Uninstall with `npm uninstall --save-dev ember-cli-6to5`. - * Add `ember-cli-babel`. Install with `npm install --save-dev ember-cli-babel`. - * Add `ember-cli-htmlbars`. Install with `npm install --save-dev ember-cli-htmlbars`. - * Updated `ember-cli-qunit` to 0.3.9. Install with `npm install --save-dev ember-cli-qunit@0.3.9`. - * Updated `ember-data` to 1.0.0-beta.15. Install with `npm install --save-dev ember-data@1.0.0-beta.15`. - * Updated `ember-cli-dependency-checker` to 0.0.8. Install with `npm install --save-dev ember-cli-dependency-checker@0.0.8`. - * Updated `ember-cli-app-version` to 0.3.2. Install with `npm install --save-dev ember-cli-app-version@0.3.2`. - + Update the following packages in your `bower.json`: - * Removed `handlebars`. Uninstall with `bower uninstall --save handlebars`. - * Updated `ember` to 1.10.0. Install with `bower install --save ember#1.10.0`. - * Updated `ember-data` to 1.0.0-beta.15. Install with `bower install --save ember-data#1.0.0-beta.15`. - * Updated `ember-cli-test-loader` to 0.1.3. Install with `bower install --save ember-cli-test-loader#0.1.3`. - * Updated `ember-resolver` to 0.1.12. Install with `bower install --save ember-resolver`. - * Updated `loader.js` to 3.2.0. -- Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/c78af207563593e5cb33a9a79d5d249cb134c1f9) - + Usage of the `included` hook to add items to the `registry` will need to be refactored to use the newly added `setupPreprocessorRegistry` hook instead. -- Core Contributors - + No changes required - -#### Community Contributions - -- [#3246](https://github.com/ember-cli/ember-cli/pull/3246) [ENHANCEMENT] Update the service blueprint to use `Ember.Service` (and remove usage of an initializer). [@ohcibi](https://github.com/ohcibi) -- [#3054](https://github.com/ember-cli/ember-cli/pull/3054) [ENHANCEMENT] Updated `loader.js` to the latest version. [@stefanpenner](https://github.com/stefanpenner) -- [#3216](https://github.com/ember-cli/ember-cli/pull/3216) [BUGFIX] Do not default to development asset [@martndemus](https://github.com/martndemus) -- [#3237](https://github.com/ember-cli/ember-cli/pull/3237) [BUGFIX] Blueprint templates with undefined variables should fallback to raw text [@davewasmer](https://github.com/davewasmer) -- [#3288](https://github.com/ember-cli/ember-cli/pull/3288) [ENHANCEMENT] Override default port with `PORT` env var [@knownasilya](https://github.com/knownasilya) -- [#3158](https://github.com/ember-cli/ember-cli/pull/3158) [INTERNAL] add more steps to release.md [@raytiley](https://github.com/raytiley) -- [#3160](https://github.com/ember-cli/ember-cli/pull/3160) [BUGFIX] Don't override the request's path [@dmathieu](https://github.com/dmathieu) -- [#3367](https://github.com/ember-cli/ember-cli/pull/3367) [ENHANCEMENT] Prevent spotlight from indexing `tmp`. [@stefanpenner](https://github.com/stefanpenner) -- [#3336](https://github.com/ember-cli/ember-cli/pull/3336) [ENHANCEMENT] Nested addons should be overrideable from parent. [@rwjblue](https://github.com/rwjblue) -- [#3335](https://github.com/ember-cli/ember-cli/pull/3335) [ENHANCEMENT] Allow shared nested addons to be properly discovered. [@rwjblue](https://github.com/rwjblue) -- [#3312](https://github.com/ember-cli/ember-cli/pull/3312) [BUGFIX] ADDON_HOOKS.md - fixed broken and outdated links [@leandrocp](https://github.com/leandrocp) -- [#3326](https://github.com/ember-cli/ember-cli/pull/3326) [ENHANCEMENT] Print deprecation warning for Node 0.10. [@rwjblue](https://github.com/rwjblue) -- [#3317](https://github.com/ember-cli/ember-cli/pull/3317) [ENHANCEMENT] Remove express & glob from default app package.json. [@rwjblue](https://github.com/rwjblue) -- [#3383](https://github.com/ember-cli/ember-cli/pull/3383) [ENHANCEMENT] Use Ember.HTMLBars by default in new helpers. [@maxwerr](https://github.com/maxwerr) -- [#3355](https://github.com/ember-cli/ember-cli/pull/3355) [ENHANCEMENT] Add `ui` to `Project` and `Addon` instances. [@rwjblue](https://github.com/rwjblue) -- [#3341](https://github.com/ember-cli/ember-cli/pull/3341) [ENHANCEMENT] Improve blueprint help output method (markdown support) [@trabus](https://github.com/trabus) -- [#3349](https://github.com/ember-cli/ember-cli/pull/3349) [BUGFIX] Allow deprecated lookup of invalid packages. [@rwjblue](https://github.com/rwjblue) -- [#3353](https://github.com/ember-cli/ember-cli/pull/3353) [BUGFIX] Allow generated acceptance tests to be in directories [@koriroys](https://github.com/koriroys) -- [#3345](https://github.com/ember-cli/ember-cli/pull/3345) [ENHANCEMENT] Check if blueprint exists before printing help [@trabus](https://github.com/trabus) -- [#3338](https://github.com/ember-cli/ember-cli/pull/3338) [ENHANCEMENT] Update resolver to 0.1.12 [@teddyzeenny](https://github.com/teddyzeenny) -- [#3401](https://github.com/ember-cli/ember-cli/pull/3401) [BUGFIX] Fixes accidental global Error object pollution. [@stefanpenner](https://github.com/stefanpenner) -- [#3363](https://github.com/ember-cli/ember-cli/pull/3363) [ENHANCEMENT] Bump ember-cli-dependency-checker to v0.0.8 [@quaertym](https://github.com/quaertym) -- [#3358](https://github.com/ember-cli/ember-cli/pull/3358) [ENHANCEMENT] CI=true puts the UI into `silent` writeLevel [@stefanpenner](https://github.com/stefanpenner) -- [#3361](https://github.com/ember-cli/ember-cli/pull/3361) [ENHANCEMENT] Update `loader.js` to 3.0.2 [@stefanpenner](https://github.com/stefanpenner) -- [#3356](https://github.com/ember-cli/ember-cli/pull/3356) [ENHANCEMENT] Generate blueprint inside addon generates into addon folder with re-export in app folder [@trabus](https://github.com/trabus) -- [#3378](https://github.com/ember-cli/ember-cli/pull/3378) [ENHANCEMENT] Only generate JSHint warnings for the addon being developed [@teddyzeenny](https://github.com/teddyzeenny) -- [#3375](https://github.com/ember-cli/ember-cli/pull/3375) [ENHANCEMENT] JSHint addon before preprocessing the JS [@teddyzeenny](https://github.com/teddyzeenny) -- [#3373](https://github.com/ember-cli/ember-cli/pull/3373) [ENHANCEMENT] Provide a helpful error when an addon does not have a template compiler. [@rwjblue](https://github.com/rwjblue) -- [#3386](https://github.com/ember-cli/ember-cli/pull/3386) [ENHANCEMENT] Display localhost in console instead of 0.0.0.0. [@rwjblue](https://github.com/rwjblue) -- [#3391](https://github.com/ember-cli/ember-cli/pull/3391) [ENHANCEMENT] Update ember-cli-qunit to 0.3.9. [@rwjblue](https://github.com/rwjblue) -- [#3410](https://github.com/ember-cli/ember-cli/pull/3410) [ENHANCEMENT] Use correct bound helper params for HTMLBars [@jbrown](https://github.com/jbrown) -- [#3428](https://github.com/ember-cli/ember-cli/pull/3428) [BUGFIX] Lock glob and rimraf to prevent EEXISTS errors. [@raytiley](https://github.com/raytiley) -- [#3435](https://github.com/ember-cli/ember-cli/pull/3435) [ENHANCEMENT] Update bundled npm [@stefanpenner](https://github.com/stefanpenner) -- [#3436](https://github.com/ember-cli/ember-cli/pull/3436) [ENHANCEMENT] Update Broccoli to 0.13.6 to provide errors on new API. [@rwjblue](https://github.com/rwjblue) -- [#3438](https://github.com/ember-cli/ember-cli/pull/3438) [BUGFIX] Ensure nested addon registry matches addon order. [@rwjblue](https://github.com/rwjblue) -- [#3456](https://github.com/ember-cli/ember-cli/pull/3456) [BUGFIX] Update ember-cli-app-version to 0.3.2 [@taras](https://github.com/taras) +- [#6986](https://github.com/ember-cli/ember-cli/pull/6986) Revert nopt dependency update [@calderas](https://github.com/calderas) Thank you to all who took the time to contribute! -### 0.2.0-beta.1 -This release updates the way that addons can be nested, and contains some breaking changes in non-default addon configurations. +### 2.12.2 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.12.1...v2.12.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.12.1...v2.12.2) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#6929](https://github.com/ember-cli/ember-cli/pull/6929) Backport fixes to release branch [@Turbo87](https://github.com/Turbo87) +- [#6944](https://github.com/ember-cli/ember-cli/pull/6944) Include ember-testing.js when using ember-source [@trentmwillis](https://github.com/trentmwillis) +- [#6974](https://github.com/ember-cli/ember-cli/pull/6974) Unnecessary "ember-cli-eslint" install [@tylerturdenpants](https://github.com/tylerturdenpants) -Prior versions of Ember CLI maintained a flat addon structure, so that all addons (of any depth) would be added to the consuming -application. This has led to many issues, like the inability to use preprocessors (i.e. ember-cli-htmlbars, ember-cli-sass, etc) -in nested addons. +Thank you to all who took the time to contribute! -For the majority of apps, the update from 0.1.15 to 0.2.0 is non-breaking and should not cause significant concern. -For addon creators, make sure to update to use the `setupPreprocessorRegistry` hook (documented [here](https://github.com/ember-cli/ember-cli/blob/master/ADDON_HOOKS.md)) -if you need to add a preprocessor to the registry. You can review the update process in -[ember-cli-htmlbars#38](https://github.com/ember-cli/ember-cli-htmlbars/pull/38) or [ember-cli-coffeescript#60](https://github.com/kimroen/ember-cli-coffeescript/pull/60) -which show how to maintain support for both 0.1.x and 0.2.0 in an addon. +### 2.12.1 -The following changes are required if you are upgrading from the previous version: +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/d717009d95d75cee1800e8ba9f52c24d117acb12) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + The 6to5 project has been renamed to Babel. See [the blog post](http://babeljs.io/blog/2015/02/15/not-born-to-die/) for more details. - + The default blueprint has been updated to work with Ember 1.10 by default. - + Update the following packages in your `package.json`: - * Remove `broccoli-ember-hbs-template-compiler`. Uninstall with `npm uninstall --save-dev broccoli-ember-hbs-template-compiler`. - * Remove `ember-cli-6to5`. Uninstall with `npm uninstall --save-dev ember-cli-6to5`. - * Add `ember-cli-babel`. Install with `npm install --save-dev ember-cli-babel`. - * Add `ember-cli-htmlbars`. Install with `npm install --save-dev ember-cli-htmlbars`. - * Updated `ember-cli-qunit` to 0.3.8. Install with `npm install --save-dev ember-cli-qunit@0.3.8`. - * Updated `ember-data` to 1.0.0-beta.15. Install with `npm install --save-dev ember-data@1.0.0-beta.15`. - + Update the following packages in your `bower.json`: - * Removed `handlebars`. Uninstall with `bower uninstall --save handlebars`. - * Updated `ember` to 1.10.0. Install with `bower install --save ember#1.10.0`. - * Updated `ember-data` to 1.0.0-beta.15. Install with `bower install --save ember-data#1.0.0-beta.15`. - * Updated `ember-cli-test-loader` to 0.1.3. Install with `bower install --save ember-cli-test-loader#0.1.3`. + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.12.0...v2.12.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/c7e8a2a97ab5d508ea3f586bc97fedffa5763a75) - + Usage of the `included` hook to add items to the `registry` will need to be refactored to use the newly added `setupPreprocessorRegistry` hook instead. + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.12.0...v2.12.1) + + No changes required - Core Contributors + No changes required #### Community Contributions -- [#3166](https://github.com/ember-cli/ember-cli/pull/3166) [BREAKING ENHANCEMENT] Addon discovery and isolation [@lukemelia](https://github.com/lukemelia) / [@chrislopresto](https://github.com/chrislopresto) / [@rwjblue](https://github.com/rwjblue) -- [#3285](https://github.com/ember-cli/ember-cli/pull/3285) [INTERNAL ENHANCEMENT] Update to Testem 0.7 [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3295](https://github.com/ember-cli/ember-cli/pull/3295) [ENHANCEMENT] Update ember-data to 1.0.0-beta.15 [@bmac](https://github.com/bmac) -- [#3297](https://github.com/ember-cli/ember-cli/pull/3297) [ENHANCEMENT] Use ember-cli-babel instead of ember-cli-6to5 [@fivetanley](https://github.com/fivetanley) -- [#3298](https://github.com/ember-cli/ember-cli/pull/3298) [BUGFIX] Update ember-cli-qunit to v0.3.8. [@rwjblue](https://github.com/rwjblue) -- [#3301](https://github.com/ember-cli/ember-cli/pull/3301) [BUGFIX] Only add Handlebars to `vendor.js` if present in `bower.json`. [@rwjblue](https://github.com/rwjblue) +- [#6879](https://github.com/ember-cli/ember-cli/pull/6879) Add .eslintrc.js files to blueprints [@rwwagner90](https://github.com/rwwagner90) +- [#6884](https://github.com/ember-cli/ember-cli/pull/6884) Remove guard in `treeForAddon` around `addon/**/*.js` files. [@rwjblue](https://github.com/rwjblue) +- [#6885](https://github.com/ember-cli/ember-cli/pull/6885) Work around broken bower installation for old npm versions [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 0.1.15 -This release fixes a regression in 0.1.13. See [#3271](https://github.com/ember-cli/ember-cli/issues/3271) for details. +### 2.12.0 -The following changes are required if you are upgrading from the previous version: +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/1f0bc0414b460da9c924e7e750d7bc5639b62f42) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.11.1...v2.12.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/0ba9b5980684c48c063a3d320914db90498f684a) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.11.1...v2.12.0) + No changes required - Core Contributors + No changes required -- [#3271](https://github.com/ember-cli/ember-cli/pull/3271) [HOTFIX] Update broccoli-funnel to v0.2.2. [@rwjblue](https://github.com/rwjblue) +#### Community Contributions + +- [#6669](https://github.com/ember-cli/ember-cli/pull/6669) tasks/bower-install: Fix "bower" lookup [@Turbo87](https://github.com/Turbo87) +- [#6606](https://github.com/ember-cli/ember-cli/pull/6606) Instrumentation [@stefanpenner](https://github.com/ember-cli) +- [#6540](https://github.com/ember-cli/ember-cli/pull/6540) removing jshint reference in blueprints [@kellyselden](https://github.com/kellyselden) +- [#5874](https://github.com/ember-cli/ember-cli/pull/5874) Don't process CSS imports by default [@stefanpenner](https://github.com/ember-cli) +- [#6516](https://github.com/ember-cli/ember-cli/pull/6516) Properly call `preprocessTree` / `postprocessTree` for addons. [@rwjblue](https://github.com/rwjblue) +- [#6627](https://github.com/ember-cli/ember-cli/pull/6627) Lazily require `broccoli-babel-transpiler`. [@rwjblue](https://github.com/rwjblue) +- [#6630](https://github.com/ember-cli/ember-cli/pull/6630) [DOC] Update license year [@cjnething](https://github.com/cjnething) +- [#6626](https://github.com/ember-cli/ember-cli/pull/6626) Flesh out `init` instrumentation. [@stefanpenner](https://github.com/ember-cli) +- [#6629](https://github.com/ember-cli/ember-cli/pull/6629) Enable more ESLint rules [@Turbo87](https://github.com/Turbo87) +- [#6624](https://github.com/ember-cli/ember-cli/pull/6624) Update version of ember-cli-eslint used in new applications. [@rwjblue](https://github.com/rwjblue) +- [#6613](https://github.com/ember-cli/ember-cli/pull/6613) Add missing annotations. [@rwjblue](https://github.com/rwjblue) +- [#6625](https://github.com/ember-cli/ember-cli/pull/6625) Update dependencies previous avoided due to Node 0.12 support. [@rwjblue](https://github.com/rwjblue) +- [#6628](https://github.com/ember-cli/ember-cli/pull/6628) Ensure `beforeRun` is included in `command` instrumentation. [@rwjblue](https://github.com/rwjblue) +- [#6684](https://github.com/ember-cli/ember-cli/pull/6684) [fixes #6672] ensure example clearly indicates promise usage [@stefanpenner](https://github.com/ember-cli) +- [#6641](https://github.com/ember-cli/ember-cli/pull/6641) Properly sort the linting rules in the ES6 section. [@rwjblue](https://github.com/rwjblue) +- [#6639](https://github.com/ember-cli/ember-cli/pull/6639) Disable usage of `var`. [@rwjblue](https://github.com/rwjblue) +- [#6633](https://github.com/ember-cli/ember-cli/pull/6633) Split serving assets into two different in-repo addons [@kratiahuja](https://github.com/kratiahuja) +- [#6640](https://github.com/ember-cli/ember-cli/pull/6640) Enable a few additional ES6 linting rules. [@rwjblue](https://github.com/rwjblue) +- [#6634](https://github.com/ember-cli/ember-cli/pull/6634) Remove "ember-cli-app-version" from "addon" blueprint [@Turbo87](https://github.com/Turbo87) +- [#6631](https://github.com/ember-cli/ember-cli/pull/6631) 🏎 Lazily install "bower" if required [@Turbo87](https://github.com/Turbo87) +- [#6636](https://github.com/ember-cli/ember-cli/pull/6636) Use ES6 features [@Turbo87](https://github.com/Turbo87) +- [#6689](https://github.com/ember-cli/ember-cli/pull/6689) Update fs-extra to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) +- [#6649](https://github.com/ember-cli/ember-cli/pull/6649) Make in-repo-addon blueprint 'use strict'. [@stefanpenner](https://github.com/ember-cli) +- [#6644](https://github.com/ember-cli/ember-cli/pull/6644) Use ES6 classes for internal classes [@Turbo87](https://github.com/Turbo87) +- [#6646](https://github.com/ember-cli/ember-cli/pull/6646) Fix some of the issues in #6623 [@stefanpenner](https://github.com/ember-cli) +- [#6645](https://github.com/ember-cli/ember-cli/pull/6645) Make project.config() public [@simonihmig](https://github.com/simonihmig) +- [#6647](https://github.com/ember-cli/ember-cli/pull/6647) Convert CoreObject classes to ES6 classes extending CoreObject [@Turbo87](https://github.com/Turbo87) +- [#6699](https://github.com/ember-cli/ember-cli/pull/6699) RELEASE: Make code snippet copy-pasta compatible [@Turbo87](https://github.com/Turbo87) +- [#6663](https://github.com/ember-cli/ember-cli/pull/6663) Add stats and logging for addon tree caching opt out [@trentmwillis](https://github.com/trentmwillis) +- [#6655](https://github.com/ember-cli/ember-cli/pull/6655) Update execa to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) +- [#6660](https://github.com/ember-cli/ember-cli/pull/6660) Preserve user errors in instrumentation hook [@stefanpenner](https://github.com/ember-cli) +- [#6652](https://github.com/ember-cli/ember-cli/pull/6652) [BUGFIX] Revert "Remove arbitrary *.js filtering for addon tree." [@nathanhammond](https://github.com/nathanhammond) +- [#6654](https://github.com/ember-cli/ember-cli/pull/6654) blueprints/app: Update "ember-cli-qunit" dependency [@Turbo87](https://github.com/Turbo87) +- [#6674](https://github.com/ember-cli/ember-cli/pull/6674) Update core-object to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) +- [#6685](https://github.com/ember-cli/ember-cli/pull/6685) Revert "remove travis sudo check" [@stefanpenner](https://github.com/ember-cli) +- [#6683](https://github.com/ember-cli/ember-cli/pull/6683) ensure `Task.prototype.run` returns promises [@stefanpenner](https://github.com/ember-cli) +- [#6680](https://github.com/ember-cli/ember-cli/pull/6680) Use global npm with version check [@Turbo87](https://github.com/Turbo87) +- [#6681](https://github.com/ember-cli/ember-cli/pull/6681) Run "ember-cli-eslint" blueprint on "ember init" [@Turbo87](https://github.com/Turbo87) +- [#6678](https://github.com/ember-cli/ember-cli/pull/6678) Avoid error upon registering a heimdall monitor twice. [@rwjblue](https://github.com/rwjblue) +- [#6682](https://github.com/ember-cli/ember-cli/pull/6682) Update the minimum version of ember-try [@kategengler](https://github.com/kategengler) +- [#6671](https://github.com/ember-cli/ember-cli/pull/6671) add description to build environment option [@kellyselden](https://github.com/kellyselden) +- [#6664](https://github.com/ember-cli/ember-cli/pull/6664) Update github to the latest version 🚀 [@stefanpenner](https://github.com/ember-cli) +- [#6731](https://github.com/ember-cli/ember-cli/pull/6731) Revert changes removing `bower.json` from default blueprints. [@rwjblue](https://github.com/rwjblue) +- [#6704](https://github.com/ember-cli/ember-cli/pull/6704) Update lockfile to use latest allowed versions. [@stefanpenner](https://github.com/ember-cli) +- [#6688](https://github.com/ember-cli/ember-cli/pull/6688) Replace custom Promise class with RSVP [@Turbo87](https://github.com/Turbo87) +- [#6696](https://github.com/ember-cli/ember-cli/pull/6696) Add --test-port 0 for random port [@morhook](https://github.com/morhook) +- [#6698](https://github.com/ember-cli/ember-cli/pull/6698) Remove "bower.json" and only create if necessary [@Turbo87](https://github.com/Turbo87) +- [#6692](https://github.com/ember-cli/ember-cli/pull/6692) tests/acceptance/generate: Fix flaky tests [@Turbo87](https://github.com/Turbo87) +- [#6705](https://github.com/ember-cli/ember-cli/pull/6705) add description to serve and test environment option [@kellyselden](https://github.com/kellyselden) +- [#6710](https://github.com/ember-cli/ember-cli/pull/6710) Fix linting issue with beta branch. [@rwjblue](https://github.com/rwjblue) +- [#6770](https://github.com/ember-cli/ember-cli/pull/6770) models/addon: Add @since tag to this.import() [@stefanpenner](https://github.com/ember-cli) +- [#6808](https://github.com/ember-cli/ember-cli/pull/6808) Use `_shouldCompileJS` to guard precompilation of addon JS. [@rwjblue](https://github.com/rwjblue) +- [#6827](https://github.com/ember-cli/ember-cli/pull/6827) Use `amd` for transpiling modules with babel@5. [@stefanpenner](https://github.com/ember-cli) +- [#6830](https://github.com/ember-cli/ember-cli/pull/6830) Revert "Use `amd` for transpiling modules with babel@5." [@stefanpenner](https://github.com/ember-cli) +- [#6856](https://github.com/ember-cli/ember-cli/pull/6856) models/project: Fix dependencies() documentation [@Turbo87](https://github.com/Turbo87) +- [#6860](https://github.com/ember-cli/ember-cli/pull/6860) blueprints/app: Update "ember-source" and "ember-data" to v2.12.0 [@Turbo87](https://github.com/Turbo87) +Thank you to all who took the time to contribute! -### 0.1.14 -This release fixes a regression in 0.1.13. See [#3267](https://github.com/ember-cli/ember-cli/issues/3267) for details. +### 2.12.0-beta.2 -The following changes are required if you are upgrading from the previous version: +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/1f5c865c5979d35f1aac72d00f97bda86864667f) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.12.0-beta.1...v2.12.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/cec7a598854db05f9190ebb6ef68d570592b8e6e) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.12.0-beta.1...v2.12.0-beta.2) + No changes required - Core Contributors + No changes required -- [#3267](https://github.com/ember-cli/ember-cli/pull/3267) [HOTFIX] Ensure reexports work to not cause an error on rebuild. [@rwjblue](https://github.com/rwjblue) +#### Community Contributions + +- [#6681](https://github.com/ember-cli/ember-cli/pull/6681) Run "ember-cli-eslint" blueprint on "ember init" [@Turbo87](https://github.com/Turbo87) +- [#6698](https://github.com/ember-cli/ember-cli/pull/6698) Remove "bower.json" and only create if necessary [@Turbo87](https://github.com/Turbo87) +- [#6711](https://github.com/ember-cli/ember-cli/pull/6711) Update `ember-cli-htmlbars-inline-precompile` requirement [@SaladFork](https://github.com/SaladFork) +- [#6720](https://github.com/ember-cli/ember-cli/pull/6720) ignore license change on init [@kellyselden](https://github.com/kellyselden) +- [#6721](https://github.com/ember-cli/ember-cli/pull/6721) use ~ instead of ^ for ember-source [@kellyselden](https://github.com/kellyselden) +- [#6763](https://github.com/ember-cli/ember-cli/pull/6763) Change livereload PortFinder.basePort to 49153 [@Turbo87](https://github.com/Turbo87) +- [#6808](https://github.com/ember-cli/ember-cli/pull/6808) Use `_shouldCompileJS` to guard precompilation of addon JS. [@rwjblue](https://github.com/rwjblue) + +Thank you to all who took the time to contribute! -### 0.1.13 +### 2.12.0-beta.1 -The following changes are required if you are upgrading from the previous version: +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/15a28d18f13b68d32b635535b168d1aa7c3f6d4d) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + Update the following packages in your `package.json`: - * Updated `ember-cli-qunit` to 0.3.7. Install with `npm install --save-dev ember-cli-qunit@0.3.7`. - * Updated `ember-data` to 1.0.0-beta.14.1. Install with `npm install --save-dev ember-data@1.0.0-beta.14.1`. - * Updated `ember-export-application-global` to 1.0.2. Install with `npm install --save-dev ember-export-application-global@^1.0.2`. - + Update the following packages in your `bower.json`: - * Updated `ember-data` to 1.0.0-beta.14.1. Install with `bower install --save ember-data#1.0.0-beta.14.1`. - * Updated `ember-cli-test-loader` to 0.1.1. Install with `bower install --save ember-cli-test-loader#0.1.1`. - * Updated `ember-qunit` to 0.2.8. Install with `bower install --save ember-qunit#0.2.8`. Please review [Ember QUnit 0.2.x](http://reefpoints.dockyard.com/2015/02/06/ember-qunit-0-2.html) for background and impact. - * Updated `ember-qunit-notifications` to 0.0.7. Install with `bower install --save ember-qunit-notifications#0.0.7`. + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.11.0...v2.12.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/8c1a672e0ccf0fe3c8f709191ff130cd20abb03e) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.11.0...v2.12.0-beta.1) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#3218](https://github.com/ember-cli/ember-cli/pull/3218) [ENHANCEMENT] Add JS context {{content-for}} hooks. This allows addons to inject things into `vendor.js`/`my-app-name.js` without violating CSP or having to do crazy hacks. [@rwjblue](https://github.com/rwjblue) -- [#3156](https://github.com/ember-cli/ember-cli/pull/3156) [BUGFIX] Serve static files from `/test` if they exist. [@trek](https://github.com/trek) -- [#3155](https://github.com/ember-cli/ember-cli/pull/3155) [BUGFIX] Guard against rawArgs being `undefined` [@chadhietala](https://github.com/chadhietala) -- [#3183](https://github.com/ember-cli/ember-cli/pull/3183) [BUGFIX] Use recent Esperanto update to allow ES3 safe output. [@rwjblue](https://github.com/rwjblue) -- [#3170](https://github.com/ember-cli/ember-cli/pull/3170) / [#3184](https://github.com/ember-cli/ember-cli/pull/3184) [#3255](https://github.com/ember-cli/ember-cli/pull/3255) [ENHANCEMENT] Update ember-qunit to 0.2.8. [@rwjblue](https://github.com/rwjblue) / [@jbrown](https://github.com/jbrown) -- [#3165](https://github.com/ember-cli/ember-cli/pull/3165) [BUGFIX] Fix `npm install --save-dev` ordering of default `package.json`. [@kellyselden](https://github.com/kellyselden) -- [#3164](https://github.com/ember-cli/ember-cli/pull/3164) [ENHANCEMENT] Enable asynchronous `Addon.prototype.serverMiddleware` hooks by returning a promise from the hook. [@taras](https://github.com/taras) -- [#3182](https://github.com/ember-cli/ember-cli/pull/3182) [INTERNAL ENHANCEMENT] Update `ember-router-generator` to ensure routes are injected into `router.js` with single quotes. [@abuiles](https://github.com/abuiles) -- [#3232](https://github.com/ember-cli/ember-cli/pull/3232) / [#3212](https://github.com/ember-cli/ember-cli/pull/3212) / [#3243](https://github.com/ember-cli/ember-cli/pull/3243) [INTERNAL ENHANCEMENT] Update testem to 0.6.39. [@joostdevries](https://github.com/joostdevries) / [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3203](https://github.com/ember-cli/ember-cli/pull/3203) / [#3252](https://github.com/ember-cli/ember-cli/pull/3252) [INTERNAL ENHANCEMENT] Bump broccoli-es6modules to v0.5.0. [@rwjblue](https://github.com/rwjblue) -- [#3197](https://github.com/ember-cli/ember-cli/pull/3197) [ENHANCEMENT] Update test blueprints to use [QUnit 2.0 compatible](http://qunitjs.com/upgrade-guide-2.x/) output. [@rwjblue](https://github.com/rwjblue) -- [#3199](https://github.com/ember-cli/ember-cli/pull/3199) [ENHANCEMENT] Provide locals to `Blueprint.prototype.beforeInstall`/`Blueprint.prototype.beforeUninstall` hooks. [@mattmarcum](https://github.com/mattmarcum) -- [#3188](https://github.com/ember-cli/ember-cli/pull/3188) [ENHANCEMENT] Update Ember Data version to 1.0.0-beta.14.1. [@abuiles](https://github.com/abuiles) -- [#3245](https://github.com/ember-cli/ember-cli/pull/3245) [ENHANCEMENT] Update ember-cli-qunit to v0.3.7. [@rwjblue](https://github.com/rwjblue) -- [#3231](https://github.com/ember-cli/ember-cli/pull/3231) [INTERNAL ENHANCEMENT] Remove extra Addon build steps. [@rwjblue](https://github.com/rwjblue) -- [#3236](https://github.com/ember-cli/ember-cli/pull/3236) [INTERNAL ENHANCEMENT] Remove module transpilation from Addon model. [@rwjblue](https://github.com/rwjblue) -- [#3242](https://github.com/ember-cli/ember-cli/pull/3242) [DOCS] Add `isDevelopingAddon` to `ADDON_HOOKS.md`. [@matthiasleitner](https://github.com/matthiasleitner) -- [#3244](https://github.com/ember-cli/ember-cli/pull/3244) [BUGFIX] Ensure that Blueprints are returned in a consistent order when looking them up. [@nathanpalmer](https://github.com/nathanpalmer) -- [#3251](https://github.com/ember-cli/ember-cli/pull/3251) Update ember-export-application-global to v1.0.2. [@rwjblue](https://github.com/rwjblue) -- [#3167](https://github.com/ember-cli/ember-cli/pull/3167) [ENHANCEMENT]`usePodsByDefault` in app config deprecated in favor of `usePods` in .ember-cli [@trabus](https://github.com/trabus) -- [#3260](https://github.com/ember-cli/ember-cli/pull/3260) [BUGFIX] Ensure newly generated project has an `app/styles/app.css` file (prevents a 404 on a newly generated project). [@rwjblue](https://github.com/rwjblue) +- [#6645](https://github.com/ember-cli/ember-cli/pull/6645) Make project.config() public [@simonihmig](https://github.com/simonihmig) +- [#6540](https://github.com/ember-cli/ember-cli/pull/6540) removing jshint reference in blueprints [@kellyselden](https://github.com/kellyselden) +- [#5874](https://github.com/ember-cli/ember-cli/pull/5874) Don't process CSS imports by default [@wagenet](https://github.com/wagenet) +- [#6516](https://github.com/ember-cli/ember-cli/pull/6516) Properly call `preprocessTree` / `postprocessTree` for addons. [@rwjblue](https://github.com/rwjblue) +- [#6600](https://github.com/ember-cli/ember-cli/pull/6600) Apply clean-base-url to config.rootURL [@nathanhammond](https://github.com/nathanhammond) +- [#6622](https://github.com/ember-cli/ember-cli/pull/6622) Remove support for Node 0.12. [@rwjblue](https://github.com/rwjblue) +- [#6624](https://github.com/ember-cli/ember-cli/pull/6624) Update version of ember-cli-eslint used in new applications. [@rwjblue](https://github.com/rwjblue) +- [#6625](https://github.com/ember-cli/ember-cli/pull/6625) Update dependencies previous avoided due to Node 0.12 support. [@rwjblue](https://github.com/rwjblue) +- [#6633](https://github.com/ember-cli/ember-cli/pull/6633) Split serving assets into two different in-repo addons [@kratiahuja](https://github.com/kratiahuja) +- [#6634](https://github.com/ember-cli/ember-cli/pull/6634) Remove "ember-cli-app-version" from "addon" blueprint [@Turbo87](https://github.com/Turbo87) +- [#6630](https://github.com/ember-cli/ember-cli/pull/6630) [DOC] Update license year [@cjnething](https://github.com/cjnething) +- [#6631](https://github.com/ember-cli/ember-cli/pull/6631) 🏎 Lazily install "bower" if required [@Turbo87](https://github.com/Turbo87) +- [#6636](https://github.com/ember-cli/ember-cli/pull/6636) Use ES6 features [@Turbo87](https://github.com/Turbo87) +- [#6649](https://github.com/ember-cli/ember-cli/pull/6649) Make in-repo-addon blueprint 'use strict'. [@stefanpenner](https://github.com/ember-cli) +- [#6647](https://github.com/ember-cli/ember-cli/pull/6647) Convert CoreObject classes to ES6 classes extending CoreObject [@Turbo87](https://github.com/Turbo87) +- [#6644](https://github.com/ember-cli/ember-cli/pull/6644) Use ES6 classes for internal classes [@Turbo87](https://github.com/Turbo87) +- [#6654](https://github.com/ember-cli/ember-cli/pull/6654) blueprints/app: Update "ember-cli-qunit" dependency [@Turbo87](https://github.com/Turbo87) +- [#6688](https://github.com/ember-cli/ember-cli/pull/6688) Replace custom Promise class with RSVP [@Turbo87](https://github.com/Turbo87) +- [#6680](https://github.com/ember-cli/ember-cli/pull/6680) Use global npm with version check [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 0.1.12 -The following changes are required if you are upgrading from the previous version: +### 2.11.1 + +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/a9bbe9c3cebc9768bf3e239ae8b2e5b5387335bf) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + `package.json` changes: - + Update `ember-cli-qunit` to 0.3.1. - + Update `ember-cli-app-version` to 0.3.1. + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.11.0...v2.11.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/fb04f954b345a1f5a1d891b64d7a596b2f566a57) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.11.0...v2.11.1) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#3118](https://github.com/ember-cli/ember-cli/pull/3118) [BUGFIX] Fix conflicting aliases. The `serve` command `host` alias is now `H` [@taddeimania](https://github.com/taddeimania) -- [#3130](https://github.com/ember-cli/ember-cli/pull/3130) [ENHANCEMENT] Tomster looks fabulous without breaking `ember new`[@johnnyshields](https://github.com/johnnyshields) -- [#3132](https://github.com/ember-cli/ember-cli/pull/3132) [BUGFIX] Update ember-cli-qunit to v0.3.1. Fixes `tests/.jshintrc` being used instead of app `.jshintrc`. [@rwjblue](https://github.com/rwjblue) -- [#3133](https://github.com/ember-cli/ember-cli/pull/3133) [BUGFIX] Fix analytics being disabled by default. Users can opt out of anylytics with `--disable-analytics` [@stefanpenner](https://github.com/stefanpenner) -- [#3153](https://github.com/ember-cli/ember-cli/pull/3153) [ENHANCEMENT] Remove deafult css from `app/styles/app.css` [@mattjmorrison](https://github.com/mattjmorrison) -- [#3132](https://github.com/ember-cli/ember-cli/pull/3157) [BUGFIX] Ensure `ember test --environment=production` runs JSHint. [@rwjblue](https://github.com/rwjblue) +- [#6711](https://github.com/ember-cli/ember-cli/pull/6711) Update `ember-cli-htmlbars-inline-precompile` requirement [@SaladFork](https://github.com/SaladFork) +- [#6720](https://github.com/ember-cli/ember-cli/pull/6720) ignore license change on init [@kellyselden](https://github.com/kellyselden) +- [#6721](https://github.com/ember-cli/ember-cli/pull/6721) use ~ instead of ^ for ember-source [@kellyselden](https://github.com/kellyselden) +- [#6763](https://github.com/ember-cli/ember-cli/pull/6763) Change livereload PortFinder.basePort to 49153 [@Turbo87](https://github.com/Turbo87) Thank you to all who took the time to contribute! -### 0.1.11 +### 2.11.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.10.2...v2.11.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.10.2...v2.11.0) + + No changes required +- Core Contributors + + No changes required + +#### Notes: +- This version of Ember CLI _will not officially support Node.js v0.12_ per the [Ember Node.js LTS Support policy](https://emberjs.com/blog/2016/09/07/ember-node-lts-support.html). +- As part of this release we have made the default behavior inclusion of Ember from npm via the `ember-source` npm package. + +#### Community Contributions + +- [#6531](https://github.com/ember-cli/ember-cli/pull/6531) Update to latest capture-exit, revert work around. [@stefanpenner](https://github.com/rwjblue) +- [#6525](https://github.com/ember-cli/ember-cli/pull/6525) utilities/npm: Run npm commands via "execa" [@Turbo87](https://github.com/Turbo87) +- [#6533](https://github.com/ember-cli/ember-cli/pull/6533) blueprints/addon: Fix path to "ember" executable in ".travis.yml" [@Turbo87](https://github.com/Turbo87) +- [#6536](https://github.com/ember-cli/ember-cli/pull/6536) fix phantom use on travis [@kellyselden](https://github.com/kellyselden) +- [#6537](https://github.com/ember-cli/ember-cli/pull/6537) Prevent deprecation from `ember-cli-babel` config options. [@rwjblue](https://github.com/rwjblue) +- [#6707](https://github.com/ember-cli/ember-cli/pull/6707) Change usage of shims for ember-source@2.11.0 final. [@stefanpenner](https://github.com/rwjblue) +- [#6254](https://github.com/ember-cli/ember-cli/pull/6254) [BUGFIX] Do not rely on ember-resolver, detect bower package instead [@martndemus](https://github.com/martndemus) +- [#6319](https://github.com/ember-cli/ember-cli/pull/6319) Use --save-dev by default when installing addons [@binhums](https://github.com/binhums) +- [#6378](https://github.com/ember-cli/ember-cli/pull/6378) Prepares Ember CLI for new version of ember-welcome-page [@locks](https://github.com/locks) +- [#6460](https://github.com/ember-cli/ember-cli/pull/6460) Refactor processTemplate. [@nathanhammond](https://github.com/nathanhammond) +- [#6385](https://github.com/ember-cli/ember-cli/pull/6385) Respect testem exit code [@johanneswuerbach](https://github.com/johanneswuerbach) +- [#6387](https://github.com/ember-cli/ember-cli/pull/6387) Adds Node 7 to testing matrix [@twokul](https://github.com/twokul) +- [#6388](https://github.com/ember-cli/ember-cli/pull/6388) Adds Node 7 to `engines` in `package.json` [@twokul](https://github.com/twokul) +- [#6407](https://github.com/ember-cli/ember-cli/pull/6407) Improve silent.js Deprecation [@nathanhammond](https://github.com/nathanhammond) +- [#6443](https://github.com/ember-cli/ember-cli/pull/6443) Fix preProcessTree API docs. [@kratiahuja](https://github.com/kratiahuja) +- [#6425](https://github.com/ember-cli/ember-cli/pull/6425) Adding json out for 'ember asset-sizes' [@kiwiupover](https://github.com/kiwiupover) +- [#6423](https://github.com/ember-cli/ember-cli/pull/6423) [BUGFIX] integrate capture exit [@stefanpenner](https://github.com/stefanpenner) +- [#6427](https://github.com/ember-cli/ember-cli/pull/6427) Document outputFile option [@ro0gr](https://github.com/ro0gr) +- [#6436](https://github.com/ember-cli/ember-cli/pull/6436) [BUGFIX] Watch vendor by default. [@nathanhammond](https://github.com/nathanhammond) +- [#6484](https://github.com/ember-cli/ember-cli/pull/6484) [BUGFIX] Fix remaining ember-source issues. [@nathanhammond](https://github.com/nathanhammond) +- [#6453](https://github.com/ember-cli/ember-cli/pull/6453) Avoid creating extraneous merge-trees. [@rwjblue](https://github.com/rwjblue) +- [#6496](https://github.com/ember-cli/ember-cli/pull/6496) [BUGFIX release] Revert the reverted revert. Ember assign not available in all ember try scenarios [@webark](https://github.com/webark) +- [#6482](https://github.com/ember-cli/ember-cli/pull/6482) Cleanup unused deps [@ro0gr](https://github.com/ro0gr) +- [#6475](https://github.com/ember-cli/ember-cli/pull/6475) extract ui to console-ui [@stefanpenner](https://github.com/stefanpenner) +- [#6479](https://github.com/ember-cli/ember-cli/pull/6479) docs: Blueprint:renamedFiles [@les2](https://github.com/les2) + +Thank you to all who took the time to contribute! -* [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/1f0fe5089efd1a28be810f261d6cd17a342fce7b) -* [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/52c9eca14e3d498786fc93a17e08a92688cd43a5) -* [#3126](https://github.com/ember-cli/ember-cli/pull/3126) hot-fix tomster ` -> ., prevents breaking the initial git commit -### 0.1.10 +### 2.10.1 -The following changes are required if you are upgrading from the previous version: +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/bc9e076e0bb2c00f183e479bf025cdce84eeca1a) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + `package.json` changes: - + Add `ember-cli-app-version` at 0.3.0. - + Add `ember-cli-uglify` at 1.0.1. - + Update `ember-cli-qunit` to 0.3.0. - + Update `ember-cli-6to5` to 3.0.0. - + `bower.json` changes: - + Update `ember-cli-test-loader` to 0.1.0. + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.10.0...v2.10.1) + + Upgrade your project's ember-cli version - [docs](http://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/88b8bf1f22e47298cd79b91bf2ccc6e054d5354b) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.10.0...v2.10.1) + No changes required - Core Contributors + No changes required #### Community Contributions -- [#2970](https://github.com/ember-cli/ember-cli/pull/2970) [ENHANCEMENT] - Added ember-cli-app-version to app blueprint - Close 2524 [@taras](https://github.com/taras) -- [#3086](https://github.com/ember-cli/ember-cli/pull/3086) [BUGFIX] Ensure that addon test-support trees are not JSHinted in the app. [@rwjblue](https://github.com/rwjblue) -- [#3085](https://github.com/ember-cli/ember-cli/pull/3085) [ENHANCEMENT] Better ASCII art [@johnnyshields](https://github.com/johnnyshields) -- [#3084](https://github.com/ember-cli/ember-cli/pull/3084) [ENHANCEMENT] Add `ember b` as `ember build` command alias. [@cbrock](https://github.com/cbrock) -- [#3092](https://github.com/ember-cli/ember-cli/pull/3092) [BUGFIX] Fix issues with running ember-cli in iojs. [@stefanpenner](https://github.com/stefanpenner) -- [#3096](https://github.com/ember-cli/ember-cli/pull/3096) [BUGFIX] Ensure that `ember g resource` uses custom blueprints (i.e. ember-cli-coffeescript or ember-cli-mocha) properly. [@jluckyiv](https://github.com/jluckyiv) -- [#3106](https://github.com/ember-cli/ember-cli/pull/3106) [BUGFIX] Fixes file stat related crashes (i.e. using Sublime Text with atomic save enabled). [@raytiley](https://github.com/raytiley) -- [#3114](https://github.com/ember-cli/ember-cli/pull/3114) [BUGFIX] Update version of ES2015 module transpiler (Esperanto). Fixes many transpilation issues (including shadowed declarations and re-exports). [@rwjblue](https://github.com/rwjblue) -- [#3116](https://github.com/ember-cli/ember-cli/pull/3116) [Bugfix] Ensure that files starting with `app/styles*` and `app/templates*` are still available in the app tree. [@stefanpenner](https://github.com/stefanpenner) -- [#3119](https://github.com/ember-cli/ember-cli/pull/3110) [ENHANCEMENT] Update ember-cli-qunit to 0.2.0. [@rwjblue](https://github.com/rwjblue) -- [#3117](https://github.com/ember-cli/ember-cli/pull/3117) [ENHANCEMENT] Replace builtin minification with ember-cli-uglify [@jkarsrud](https://github.com/jkarsrud) -- [#3119](https://github.com/ember-cli/ember-cli/pull/3119) & [#3121](https://github.com/ember-cli/ember-cli/pull/3121) [ENHANCEMENT] Update ember-cli-qunit to v0.3.0. [@rwjblue](https://github.com/rwjblue) -- [#3122](https://github.com/ember-cli/ember-cli/pull/3122) [ENHANCEMENT] Make linting pluggable. [@ef4](https://github.com/ef4) -- [#3123](https://github.com/ember-cli/ember-cli/pull/3123) [ENHANCEMENT] Update ember-cli-6to5 to v3.0.0. [@stefanpenner](https://github.com/stefanpenner) +- [#6485](https://github.com/ember-cli/ember-cli/pull/6485) tests/runner: Fix "capture-exit" compatibility [@Turbo87](https://github.com/Turbo87) +- [#6496](https://github.com/ember-cli/ember-cli/pull/6496) Revert the reverted revert. Ember assign not available in all ember try scenarios [@webark](https://github.com/webark) +- [#6531](https://github.com/ember-cli/ember-cli/pull/6531) Update to latest capture-exit, revert work around. [@rwjblue](https://github.com/rwjblue) +- [#6533](https://github.com/ember-cli/ember-cli/pull/6533) blueprints/addon: Fix path to "ember" executable in ".travis.yml" [@Turbo87](https://github.com/Turbo87) +- [#6536](https://github.com/ember-cli/ember-cli/pull/6536) fix phantom use on travis [@kellyselden](https://github.com/kellyselden) +- [#6693](https://github.com/ember-cli/ember-cli/pull/6693) Backport subprocess invocation of npm to v2.10 [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +### 2.10.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.9.1...v2.10.0) + + Upgrade your project's ember-cli version - [docs](http://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.9.1...v2.10.0) + + No changes required +- Core Contributors + + No changes required +#### Notes: +- This version of Ember CLI _no longer supports Node.js v0.10_ per the [Ember Node.js LTS Support policy](https://emberjs.com/blog/2016/09/07/ember-node-lts-support.html). +- As part of this release we have experimental support for including Ember from npm via the `ember-source` npm package. We hope to discover the gaps during this release cycle and make that the default in a future release. + +#### Community Contributions + +- [#5994](https://github.com/ember-cli/ember-cli/pull/5994) No 'diff' option when binaries are involved (confirm-dialog). [@fpauser](https://github.com/fpauser) +- [#6241](https://github.com/ember-cli/ember-cli/pull/6241) destroy deletes empty folders [@kellyselden](https://github.com/kellyselden) +- [#6096](https://github.com/ember-cli/ember-cli/pull/6096) Fix and improve Watcher.detectWatcher [@stefanpenner](https://github.com/stefanpenner) +- [#6081](https://github.com/ember-cli/ember-cli/pull/6081) [BUGFIX] Header files import concat [@stefanpenner](https://github.com/stefanpenner) +- [#6296](https://github.com/ember-cli/ember-cli/pull/6296) Include relative path on ember asset-sizes [@josemarluedke](https://github.com/josemarluedke) +- [#6301](https://github.com/ember-cli/ember-cli/pull/6301) [Fixes #6300] consistent concat, regardless of system EOL [@stefanpenner](https://github.com/stefanpenner) +- [#6305](https://github.com/ember-cli/ember-cli/pull/6305) Use Ember.assign for start-app test helper. +- [#6307](https://github.com/ember-cli/ember-cli/pull/6307) Node.js LTS updates. [@nathanhammond](https://github.com/nathanhammond) +- [#6306](https://github.com/ember-cli/ember-cli/pull/6306) [ENHANCEMENT] Use npm 3 [@dfreeman](https://github.com/dfreeman) +- [#6337](https://github.com/ember-cli/ember-cli/pull/6337) DOC: #addBowerPackagesToProject `source` option [@olleolleolle](https://github.com/olleolleolle) +- [#6358](https://github.com/ember-cli/ember-cli/pull/6358) Use secure URLs in docs where possible [@xtian](https://github.com/xtian) +- [#6363](https://github.com/ember-cli/ember-cli/pull/6363) [ENHANCEMENT] Add 2.8-lts scenario to default ember-try config [@BrianSipple](https://github.com/BrianSipple) +- [#6369](https://github.com/ember-cli/ember-cli/pull/6369) Enable ember-source. [@nathanhammond](https://github.com/nathanhammond) Thank you to all who took the time to contribute! -### 0.1.9 -This release fixes a regression in 0.1.8. See [#3075](https://github.com/ember-cli/ember-cli/issues/3075) for details. +### 2.10.0-beta.2 -The following changes are required if you are upgrading from the previous version: +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/2275ca51593bae2f6fa91568869f36cd84c264c4) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.10.0-beta.1...v2.10.0-beta.2) + + Upgrade your project's ember-cli version - [docs](http://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/d41dcf806a55056a5591dd4e81d712988be1e22f) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.10.0-beta.1...v2.10.0-beta.2) + No changes required - Core Contributors + No changes required +#### Notes: +- This version of Ember CLI _no longer supports Node.js v0.10_ per the [Ember Node.js LTS Support policy](https://emberjs.com/blog/2016/09/07/ember-node-lts-support.html). +- As part of this release we have experimental support for including Ember from npm via the `ember-source` npm package. We hope to discover the gaps during this release cycle and make that the default in a future release. + #### Community Contributions -- [#3077](https://github.com/ember-cli/ember-cli/pull/3077) [BUGFIX] Fix error `Cannot call method 'bind' of undefined` [@stefanpenner](https://github.com/stefanpenner) +- [#6375](https://github.com/ember-cli/ember-cli/pull/6375) Bump Ember versions. [@nathanhammond](https://github.com/nathanhammond) Thank you to all who took the time to contribute! -### 0.1.8 -The following changes are required if you are upgrading from the previous version: +### 2.10.0-beta.1 + +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/ed4f5bcbff0641dba8eca8fe3a3ab96f1347a7cf) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.9.0...v2.10.0-beta.1) + + Upgrade your project's ember-cli version - [docs](http://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/237c74c0b5972c8ee1ef9b427809aaf93c53db6a) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.9.0...v2.10.0-beta.1) + No changes required - Core Contributors + No changes required +#### Notes: +- This version of Ember CLI _no longer supports Node.js v0.10_ per the [Ember Node.js LTS Support policy](https://emberjs.com/blog/2016/09/07/ember-node-lts-support.html). +- As part of this release we have experimental support for including Ember from npm via the `ember-source` npm package. We hope to discover the gaps during this release cycle and make that the default in a future release. + #### Community Contributions -- [#3072](https://github.com/ember-cli/ember-cli/pull/3072) [BUGFIX] Update to app blueprint to use QUnit 1.17.1 [@rwjblue](https://github.com/rwjblue) -- [#3069](https://github.com/ember-cli/ember-cli/pull/3069) [BUGFIX] Fix style preprocessors for included addons [@pzuraq](https://github.com/pzuraq) -- [#3068](https://github.com/ember-cli/ember-cli/pull/3068) [ENHANCEMENT] Hide passed tests by default [@rwjblue](https://github.com/rwjblue) -- [#3036](https://github.com/ember-cli/ember-cli/pull/3036) [BUGFIX] Fix platform dependent path separator [@KarimBaaba](https://github.com/KarimBaaba) -- [#2754](https://github.com/ember-cli/ember-cli/pull/2754) [FEATURE] Allow addon commands to be classes [@chadhietala](https://github.com/chadhietala) -- [#2923](https://github.com/ember-cli/ember-cli/pull/2923) [ENHANCEMENT] Add disable-analytics option to all commands [@twokul](https://github.com/twokul) -- [#2901](https://github.com/ember-cli/ember-cli/pull/2901) [ENHANCEMENT] Improve boot by 300–400ms [@stefanpenner](https://github.com/stefanpenner) -- [#3049](https://github.com/ember-cli/ember-cli/pull/3049) [FEATURE] Add Test helper blueprint [@stefanpenner](https://github.com/stefanpenner) -- [#2826](https://github.com/ember-cli/ember-cli/pull/2826) [BUGFIX] Remove path.join from http-mock bluerint urls [@knownasilya](https://github.com/knownasilya) -- [#2983](https://github.com/ember-cli/ember-cli/pull/2983) [ENHANCEMENT] Update QUnit version [@wagenet](https://github.com/wagenet) -- [#2814](https://github.com/ember-cli/ember-cli/pull/2814) [ENHANCEMENT] Add listing of available addons [@rondale-sc](https://github.com/rondale-sc) -- [#3007](https://github.com/ember-cli/ember-cli/pull/3007) [FEATURE] Add a watcher option to the build command [@rauhryan](https://github.com/rauhryan) -- [#3039](https://github.com/ember-cli/ember-cli/pull/3039) [BUGFIX] Move static file check earlier so it only affects the default value [@krisselden](https://github.com/krisselden) -- [#3028](https://github.com/ember-cli/ember-cli/pull/3028) [BUGFIX] Update Testem (fixes timeouts and reloads with Pretender) [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#3026](https://github.com/ember-cli/ember-cli/pull/3026) [BUGFIX] Correct comment in server blueprint [@ohcibi](https://github.com/ohcibi) -- [#3008](https://github.com/ember-cli/ember-cli/pull/3008) [BUGFIX] Clarify error message for ensuring hyphen presence in component name [@artfuldodger](https://github.com/artfuldodger) -- [#3009](https://github.com/ember-cli/ember-cli/pull/3009) [BUGFIX] Tweak error message for executing `ember unknownCommand` [@artfuldodger](https://github.com/artfuldodger) -- [#2996](https://github.com/ember-cli/ember-cli/pull/2996) [BUGFIX] Rename .npmignore in addon blueprint (fixes broken package) [@jgwhite](https://github.com/jgwhite) -- [#2995](https://github.com/ember-cli/ember-cli/pull/2995) [BUGFIX] Correct package.json ordering in app blueprint [@kellyselden](https://github.com/kellyselden) -- [#2984](https://github.com/ember-cli/ember-cli/pull/2984) [ENHANCEMENT] Add `"strict": false` to blueprint .jshintrc [@quaertym](https://github.com/quaertym) +- [#5994](https://github.com/ember-cli/ember-cli/pull/5994) No 'diff' option when binaries are involved (confirm-dialog). [@fpauser](https://github.com/fpauser) +- [#6241](https://github.com/ember-cli/ember-cli/pull/6241) destroy deletes empty folders [@kellyselden](https://github.com/kellyselden) +- [#6096](https://github.com/ember-cli/ember-cli/pull/6096) Fix and improve Watcher.detectWatcher [@stefanpenner](https://github.com/stefanpenner) +- [#6081](https://github.com/ember-cli/ember-cli/pull/6081) [BUGFIX] Header files import concat [@stefanpenner](https://github.com/stefanpenner) +- [#6296](https://github.com/ember-cli/ember-cli/pull/6296) Include relative path on ember asset-sizes [@josemarluedke](https://github.com/josemarluedke) +- [#6301](https://github.com/ember-cli/ember-cli/pull/6301) [Fixes #6300] consistent concat, regardless of system EOL [@stefanpenner](https://github.com/stefanpenner) +- [#6305](https://github.com/ember-cli/ember-cli/pull/6305) Use Ember.assign for start-app test helper. +- [#6307](https://github.com/ember-cli/ember-cli/pull/6307) Node.js LTS updates. [@nathanhammond](https://github.com/nathanhammond) +- [#6306](https://github.com/ember-cli/ember-cli/pull/6306) [ENHANCEMENT] Use npm 3 [@dfreeman](https://github.com/dfreeman) +- [#6337](https://github.com/ember-cli/ember-cli/pull/6337) DOC: #addBowerPackagesToProject `source` option [@olleolleolle](https://github.com/olleolleolle) +- [#6358](https://github.com/ember-cli/ember-cli/pull/6358) Use secure URLs in docs where possible [@xtian](https://github.com/xtian) +- [#6363](https://github.com/ember-cli/ember-cli/pull/6363) [ENHANCEMENT] Add 2.8-lts scenario to default ember-try config [@BrianSipple](https://github.com/BrianSipple) +- [#6369](https://github.com/ember-cli/ember-cli/pull/6369) Enable ember-source. [@nathanhammond](https://github.com/nathanhammond) Thank you to all who took the time to contribute! -### 0.1.7 + +### 2.9.1 + +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/22b868fb064631d9ed16e208db982ee808f05296) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) - + Uninstall ember-cli-esnext. It has been replaced by ember-cli-6to5 which will be added to your project from the upgrade steps. + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.9.0...v2.9.1) + + Upgrade your project's ember-cli version - [docs](http://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/eb5319eb7fd626cc53bf0b5aee639167a031a4c5) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.9.0...v2.9.1) + No changes required - Core Contributors + No changes required -#### Using 6to5. +#### Community Contributions + +- [#6371](https://github.com/ember-cli/ember-cli/pull/6371) blueprints/app: Update Ember and Ember Data to v2.9.0 [@Turbo87](https://github.com/Turbo87) + +Thank you to all who took the time to contribute! + + +### 2.9.0 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.8.0...v2.9.0) + + Upgrade your project's ember-cli version - [docs](http://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.8.0...v2.9.0) + + No changes required +- Core Contributors + + No changes required -The module transpile step is still es3 safe, if you are concern about using -6to5 you can remove it from package.json. +Notes: +- This update includes a version bump of QUnit to 2.0.0. Please pay close attention to your test suites. #### Community Contributions -- [#2841](https://github.com/ember-cli/ember-cli/pull/2841) Add insecure-proxy option to `serve` command [@matthewlehner](https://github.com/matthewlehner) -- [#2922](https://github.com/ember-cli/ember-cli/pull/2922) 0.1.6 changelog [@stefanpenner](https://github.com/stefanpenner) -- [#2927](https://github.com/ember-cli/ember-cli/pull/2927) Cleanup badges [@knownasilya](https://github.com/knownasilya) -- [#2937](https://github.com/ember-cli/ember-cli/pull/2937) [BUGFIX] only include dependencies(not devDependencies) of included addons [@jakecraige](https://github.com/jakecraige) -- [#2951](https://github.com/ember-cli/ember-cli/pull/2951) Fix for aliases for hyphenated options [@marcioj](https://github.com/marcioj) -- [#2952](https://github.com/ember-cli/ember-cli/pull/2952) Fixes validation of shorthand commandOptions [@trabus](https://github.com/trabus) -- [#2960](https://github.com/ember-cli/ember-cli/pull/2960) Move from esnext to 6to5 [@stefanpenner](https://github.com/stefanpenner) -- [#2964](https://github.com/ember-cli/ember-cli/pull/2964) [Bugfix] when developing an add-on, it’s jshint tests would be duplicate... [@stefanpenner](https://github.com/stefanpenner) -- [#2965](https://github.com/ember-cli/ember-cli/pull/2965) Remove Hardcoded test-loader [@chadhietala](https://github.com/chadhietala) -- [#2976](https://github.com/ember-cli/ember-cli/pull/2976) debug logging for add-ons + projects [@stefanpenner](https://github.com/stefanpenner) +- [#6232](https://github.com/ember-cli/ember-cli/pull/6232) suggest testing addons against LTS [@kellyselden](https://github.com/kellyselden) +- [#6235](https://github.com/ember-cli/ember-cli/pull/6235) remove `default` ember try scenario [@kellyselden](https://github.com/kellyselden) +- [#6249](https://github.com/ember-cli/ember-cli/pull/6249) Update to ember-cli-qunit@3.0.1. [@rwjblue](https://github.com/rwjblue) +- [#6276](https://github.com/ember-cli/ember-cli/pull/6276) Revert #6193 (Ember.assign) [@nathanhammond](https://github.com/nathanhammond) +- [#6176](https://github.com/ember-cli/ember-cli/pull/6176) fixed typo in the example code in the comments in the blueprint.js [@foxnewsnetwork](https://github.com/foxnewsnetwork) +- [#5395](https://github.com/ember-cli/ember-cli/pull/5395) Skip bower/npm install on blueprint install if manifests are missing [@stefanpenner](https://github.com/stefanpenner) +- [#5976](https://github.com/ember-cli/ember-cli/pull/5976) Anonymous AMD Support [@ef4](https://github.com/ef4) +- [#6086](https://github.com/ember-cli/ember-cli/pull/6086) Use heimdalljs for structured instrumentation & structured logging [@hjdivad](https://github.com/hjdivad) +- [#6103](https://github.com/ember-cli/ember-cli/pull/6103) store add-on initialization/lookup times [@stefanpenner](https://github.com/stefanpenner) +- [#6127](https://github.com/ember-cli/ember-cli/pull/6127) Remove invalid backticks in docs [@san650](https://github.com/san650) +- [#6132](https://github.com/ember-cli/ember-cli/pull/6132) [Bugfix] Destroy in-repo-addon [@andyklimczak](https://github.com/andyklimczak) +- [#6193](https://github.com/ember-cli/ember-cli/pull/6193) Changed the start-app test helper to use `Ember.assign`. [@workmanw](https://github.com/workmanw) +- [#6145](https://github.com/ember-cli/ember-cli/pull/6145) Update .gitignore for npm-debug.log [@hckhanh](https://github.com/hckhanh) +- [#6139](https://github.com/ember-cli/ember-cli/pull/6139) Updating app/addon blueprints to latest dependency versions [@elwayman02](https://github.com/elwayman02) +- [#6148](https://github.com/ember-cli/ember-cli/pull/6148) Update to _findHost to use do/while. [@nathanhammond](https://github.com/nathanhammond) +- [#6206](https://github.com/ember-cli/ember-cli/pull/6206) Remove debug from package.json [@marpo60](https://github.com/marpo60) +- [#6171](https://github.com/ember-cli/ember-cli/pull/6171) Adding a test to cover historySupportMiddleware with unknown location type [@jasonmit](https://github.com/jasonmit) +- [#6162](https://github.com/ember-cli/ember-cli/pull/6162) Upgraded ember-cli-app-version to 2.0.0 [@taras](https://github.com/taras) +- [#6198](https://github.com/ember-cli/ember-cli/pull/6198) display cleanup progress. [@stefanpenner](https://github.com/stefanpenner) +- [#6189](https://github.com/ember-cli/ember-cli/pull/6189) `testem.js` must be loaded from `/`. [@rwjblue](https://github.com/rwjblue) +- [#6188](https://github.com/ember-cli/ember-cli/pull/6188) [BUGFIX] - fix reference for `ui.prompt` [@tgandee79](https://github.com/tgandee79) +- [#6182](https://github.com/ember-cli/ember-cli/pull/6182) [BUGFIX beta] Allow empty string as rootURL [@kanongil](https://github.com/kanongil) +- [#6186](https://github.com/ember-cli/ember-cli/pull/6186) [ENHANCEMENT] Warn when empty rootURL is used with history addon [@kanongil](https://github.com/kanongil) +- [#6180](https://github.com/ember-cli/ember-cli/pull/6180) bump portfinder to v1.0.7 [@eriktrom](https://github.com/eriktrom) +- [#6194](https://github.com/ember-cli/ember-cli/pull/6194) [BUGFIX beta] Prevent Ember Data from overriding Date.parse. [@bmac](https://github.com/bmac) +- [#6208](https://github.com/ember-cli/ember-cli/pull/6208) Replace "ember-cli-broccoli" with "broccoli-{brocfile-loader, builder, middleware}" [@Turbo87](https://github.com/Turbo87) +- [#6211](https://github.com/ember-cli/ember-cli/pull/6211) Document `--port 0` in ember serve's command line usage [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [#6227](https://github.com/ember-cli/ember-cli/pull/6227) add tests for alphabetize-object-keys [@kellyselden](https://github.com/kellyselden) +- [#6228](https://github.com/ember-cli/ember-cli/pull/6228) in-repo-addon: sort additions to ember-addon/paths [@kellyselden](https://github.com/kellyselden) Thank you to all who took the time to contribute! -### 0.1.6 -The following changes are required if you are upgrading from the previous version: +### 2.9.0-beta.2 + +The following changes are required if you are upgrading from the previous +version: - Users - + [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/5c08ee64df8df8bd40c3e1fd9541e19c9db9b214) - + Upgrade your project's ember-cli version - [docs](http://www.ember-cli.com/user-guide/#upgrading) + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.9.0-beta.1...v2.9.0-beta.2) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) - Addon Developers - + [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/82652c474424a118268383d429f4b054567bb966) + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.9.0-beta.1...v2.9.0-beta.2) + No changes required - Core Contributors - + Use `expect` over `assert` in tests going forward + + No changes required + +Notes: +- This update includes a version bump of QUnit to 2.0.0. Please pay close attention to your test suites. +- This update is marked as unsupporting Node 0.10. Please prepare for removal of support per [Ember's Node.js LTS Support policy](https://emberjs.com/blog/2016/09/07/ember-node-lts-support.html). #### Community Contributions -- [#2885](https://github.com/ember-cli/ember-cli/pull/2885) [ENHANCEMENT] NPM should use save-exact flags [@chadhietala](https://github.com/chadhietala) -- [#2840](https://github.com/ember-cli/ember-cli/pull/2840) [INTERNAL ]using 'expect' vs. assert. [@Mawaheb](https://github.com/Mawaheb) -- [#2669](https://github.com/ember-cli/ember-cli/pull/2669) [ENHANCEMENT] add .npmignore to addon blueprint [@pogopaule](https://github.com/pogopaule) -- [#2909](https://github.com/ember-cli/ember-cli/pull/2909) [INTERNAL] Use lib/ext/promise instead of RSVP directly [@zeppelin](https://github.com/zeppelin) -- [#2857](https://github.com/ember-cli/ember-cli/pull/2857) [ENHANCEMENT] Add descriptions to more Broccoli trees. [@rwjblue](https://github.com/rwjblue) -- [#2842](https://github.com/ember-cli/ember-cli/pull/2842) [INTERNAL] Prefer `expect` over `assert` for testing [@stavarotti](https://github.com/stavarotti) -- [#2847](https://github.com/ember-cli/ember-cli/pull/2847) [BUGFIX] Bump ember-router-generator (fixes WARN on description not present). [@abuiles](https://github.com/abuiles) -- [#2843](https://github.com/ember-cli/ember-cli/pull/2843) [INTERNAL] Unify using chai.expect [@twokul](https://github.com/twokul) -- [#2900](https://github.com/ember-cli/ember-cli/pull/2900) [INTERNAL] update some CI stuff [@stefanpenner](https://github.com/stefanpenner) -- [#2876](https://github.com/ember-cli/ember-cli/pull/2876) [BUGFIX] make sure adapter cannot extend from itself [@jakecraige](https://github.com/jakecraige) -- [#2869](https://github.com/ember-cli/ember-cli/pull/2869) [BUGFIX] Tolerate before & after references to missing addons [@ef4](https://github.com/ef4) -- [#2864](https://github.com/ember-cli/ember-cli/pull/2864) [ENHANCEMENT] the .gitkeep in /public can now be removed [@kellyselden](https://github.com/kellyselden) -- [#2887](https://github.com/ember-cli/ember-cli/pull/2887) [INTERNAL] I don’t think we need this anymore. [@stefanpenner](https://github.com/stefanpenner) -- [#2910](https://github.com/ember-cli/ember-cli/pull/2910) [DOCS] Update org references to ember-cli [@zeppelin](https://github.com/zeppelin) -- [#2911](https://github.com/ember-cli/ember-cli/pull/2911) [DOCS] More org updates to reference ember-cli [@Dhaulagiri](https://github.com/Dhaulagiri) -- [#2916](https://github.com/ember-cli/ember-cli/pull/2916) [BUGFIX] findAddonByName returning incorrect matches [@jakecraige](https://github.com/jakecraige) -- [#2918](https://github.com/ember-cli/ember-cli/pull/2918) [ENHANCEMENT] Updated testem [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#2919](https://github.com/ember-cli/ember-cli/pull/2919) [ENHANCEMENT] implement Blueprint.prototype.addAddonToProject [@jakecraige](https://github.com/jakecraige) -- [#2920](https://github.com/ember-cli/ember-cli/pull/2920) [BUGFIX] explicitly bump broccoli-sourcemap-concat to fix #2890 [@krisselden](https://github.com/krisselden) -- [#2929](https://github.com/ember-cli/ember-cli/pull/2929) [BUGFIX] Bump ember-router-generator. [@abuiles](https://github.com/abuiles) -- [#2939](https://github.com/ember-cli/ember-cli/pull/2939) [ENHANCEMENT] Add a hook for postprocessing tests tree [@ef4](https://github.com/ef4) -- [#2941](https://github.com/ember-cli/ember-cli/pull/2941) [ENHANCEMENT] Bumped testem [@johanneswuerbach](https://github.com/johanneswuerbach) -- [#2944](https://github.com/ember-cli/ember-cli/pull/2944) [INTERNAL] update CONTRIBUTING.md [@jakecraige](https://github.com/jakecraige) +- [#6232](https://github.com/ember-cli/ember-cli/pull/6232) suggest testing addons against LTS [@kellyselden](https://github.com/kellyselden) +- [#6235](https://github.com/ember-cli/ember-cli/pull/6235) remove `default` ember try scenario [@kellyselden](https://github.com/kellyselden) +- [#6249](https://github.com/ember-cli/ember-cli/pull/6249) Update to ember-cli-qunit@3.0.1. [@rwjblue](https://github.com/rwjblue) +- [#6250](https://github.com/ember-cli/ember-cli/pull/6250) Update engine field in package.json [@nathanhammond](https://github.com/nathanhammond) +- [#6276](https://github.com/ember-cli/ember-cli/pull/6276) Revert #6193 (Ember.assign) [@nathanhammond](https://github.com/nathanhammond) Thank you to all who took the time to contribute! -### 0.1.5 -### Applications +### 2.9.0-beta.1 + +The following changes are required if you are upgrading from the previous +version: + +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.8.0...v2.9.0-beta.1) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.8.0...v2.9.0-beta.1) + + No changes required +- Core Contributors + + No changes required + +#### Community Contributions + +- [#6176](https://github.com/ember-cli/ember-cli/pull/6176) fixed typo in the example code in the comments in the blueprint.js [@foxnewsnetwork](https://github.com/foxnewsnetwork) +- [#5395](https://github.com/ember-cli/ember-cli/pull/5395) Skip bower/npm install on blueprint install if manifests are missing [@stefanpenner](https://github.com/stefanpenner) +- [#5976](https://github.com/ember-cli/ember-cli/pull/5976) Anonymous AMD Support [@ef4](https://github.com/ef4) +- [#6086](https://github.com/ember-cli/ember-cli/pull/6086) Use heimdalljs for structured instrumentation & structured logging [@hjdivad](https://github.com/hjdivad) +- [#6103](https://github.com/ember-cli/ember-cli/pull/6103) store add-on initialization/lookup times [@stefanpenner](https://github.com/stefanpenner) +- [#6127](https://github.com/ember-cli/ember-cli/pull/6127) Remove invalid backticks in docs [@san650](https://github.com/san650) +- [#6132](https://github.com/ember-cli/ember-cli/pull/6132) [Bugfix] Destroy in-repo-addon [@andyklimczak](https://github.com/andyklimczak) +- [#6193](https://github.com/ember-cli/ember-cli/pull/6193) Changed the start-app test helper to use `Ember.assign`. [@workmanw](https://github.com/workmanw) +- [#6145](https://github.com/ember-cli/ember-cli/pull/6145) Update .gitignore for npm-debug.log [@hckhanh](https://github.com/hckhanh) +- [#6139](https://github.com/ember-cli/ember-cli/pull/6139) Updating app/addon blueprints to latest dependency versions [@elwayman02](https://github.com/elwayman02) +- [#6148](https://github.com/ember-cli/ember-cli/pull/6148) Update to _findHost to use do/while. [@nathanhammond](https://github.com/nathanhammond) +- [#6206](https://github.com/ember-cli/ember-cli/pull/6206) Remove debug from package.json [@marpo60](https://github.com/marpo60) +- [#6171](https://github.com/ember-cli/ember-cli/pull/6171) Adding a test to cover historySupportMiddleware with unknown location type [@jasonmit](https://github.com/jasonmit) +- [#6162](https://github.com/ember-cli/ember-cli/pull/6162) Upgraded ember-cli-app-version to 2.0.0 [@taras](https://github.com/taras) +- [#6198](https://github.com/ember-cli/ember-cli/pull/6198) display cleanup progress. [@stefanpenner](https://github.com/stefanpenner) +- [#6189](https://github.com/ember-cli/ember-cli/pull/6189) `testem.js` must be loaded from `/`. [@rwjblue](https://github.com/rwjblue) +- [#6188](https://github.com/ember-cli/ember-cli/pull/6188) [BUGFIX] - fix reference for `ui.prompt` [@tgandee79](https://github.com/tgandee79) +- [#6182](https://github.com/ember-cli/ember-cli/pull/6182) [BUGFIX beta] Allow empty string as rootURL [@kanongil](https://github.com/kanongil) +- [#6186](https://github.com/ember-cli/ember-cli/pull/6186) [ENHANCEMENT] Warn when empty rootURL is used with history addon [@kanongil](https://github.com/kanongil) +- [#6180](https://github.com/ember-cli/ember-cli/pull/6180) bump portfinder to v1.0.7 [@eriktrom](https://github.com/eriktrom) +- [#6194](https://github.com/ember-cli/ember-cli/pull/6194) [BUGFIX beta] Prevent Ember Data from overriding Date.parse. [@bmac](https://github.com/bmac) +- [#6208](https://github.com/ember-cli/ember-cli/pull/6208) Replace "ember-cli-broccoli" with "broccoli-{brocfile-loader, builder, middleware}" [@Turbo87](https://github.com/Turbo87) +- [#6211](https://github.com/ember-cli/ember-cli/pull/6211) Document `--port 0` in ember serve's command line usage [@sivakumar-kailasam](https://github.com/sivakumar-kailasam) +- [#6227](https://github.com/ember-cli/ember-cli/pull/6227) add tests for alphabetize-object-keys [@kellyselden](https://github.com/kellyselden) +- [#6228](https://github.com/ember-cli/ember-cli/pull/6228) in-repo-addon: sort additions to ember-addon/paths [@kellyselden](https://github.com/kellyselden) -- [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/feb4dd773e2d68c8576b060c2973062ba83ed66a) +Thank you to all who took the time to contribute! -- [#2727](https://github.com/ember-cli/ember-cli/pull/2727) Added - sourcemap support to the JS concatenation and minification steps of - the build. This eliminates the need for the wrapInEval hack. Any - Javascript preprocessors that produce sourcemaps will also be - automatically incorporated into the final result. Sourcemaps are - enabled by default in dev, to enable them in production pass - `{sourcemaps: { enabled: true, extensions: ['js']}}` to your - EmberApp constructor. -- [#2777](https://github.com/ember-cli/ember-cli/pull/2777) allowed - the creation of components with slashes in their names since this is - supported in Handlebars 2.0. +### 2.8.0 -- [#2800](https://github.com/ember-cli/ember-cli/pull/2800) Added 3 new commands +The following changes are required if you are upgrading from the previous +version: - ``` - ember install - ember install:bower moment - ember install:npm ember-browserify - ``` +- Users + + [`ember new` diff](https://github.com/ember-cli/ember-new-output/compare/v2.7.0...v2.8.0) + + Upgrade your project's ember-cli version - [docs](https://ember-cli.com/user-guide/#upgrading) +- Addon Developers + + [`ember addon` diff](https://github.com/ember-cli/ember-addon-output/compare/v2.7.0...v2.8.0) + + No changes required +- Core Contributors + + No changes required - They behave exactly as you'd expect. Install runs npm and bower - install on the project. The last two simply pass in the package names - you give it to the underlying task to do it. +#### Community Contributions -- [#2805](https://github.com/ember-cli/ember-cli/pull/2805) Added the - `install:addon` command, which installs an addon with NPM and then - runs the included generator of the same name if it provides one. +- [#6050](https://github.com/ember-cli/ember-cli/pull/6050) Make app/addon readmes consistent [@elwayman02](https://github.com/elwayman02) +- [#6005](https://github.com/ember-cli/ember-cli/pull/6005) [BUGFIX] Fixes broccoli errors when `tests` dir is not present [@twokul](https://github.com/twokul) +- [#5986](https://github.com/ember-cli/ember-cli/pull/5986) added transparent-proxy option to ember serve command [@badazz91](https://github.com/badazz91) +- [#6012](https://github.com/ember-cli/ember-cli/pull/6012) switch to a rollup subset of lodash and shave off 20 - 30%+ boot time [@stefanpenner](https://github.com/stefanpenner) +- [#6017](https://github.com/ember-cli/ember-cli/pull/6017) Allow `ember install addon_name --save` in addons. [@xcambar](https://github.com/xcambar) +- [#6030](https://github.com/ember-cli/ember-cli/pull/6030) [ENHANCEMENT] Asset Sizes I moved the creation of asset sizes to an object. [@kiwiupover](https://github.com/kiwiupover) +- [#6052](https://github.com/ember-cli/ember-cli/pull/6052) Turn on strict mode for tests. [@nathanhammond](https://github.com/nathanhammond) +- [#6043](https://github.com/ember-cli/ember-cli/pull/6043) [BUGFIX beta] Test nested addon import [@xcambar](https://github.com/xcambar) +- [#6045](https://github.com/ember-cli/ember-cli/pull/6045) [Enhancement] Return raw asset-size as data instead of strings [@kiwiupover](https://github.com/kiwiupover) +- [#6072](https://github.com/ember-cli/ember-cli/pull/6072) Makes sure dependecies are loaded on demand [@twokul](https://github.com/twokul) +- [#6092](https://github.com/ember-cli/ember-cli/pull/6092) Remove ember-qunit-notifications [@trentmwillis](https://github.com/trentmwillis) +- [#6094](https://github.com/ember-cli/ember-cli/pull/6094) Remove jQuery usage to read meta config. [@rwjblue](https://github.com/rwjblue) +- [#6095](https://github.com/ember-cli/ember-cli/pull/6095) [INTERNAL] Remove unused 'es3Safe' option [@ursm](https://github.com/ursm) +- [#6102](https://github.com/ember-cli/ember-cli/pull/6102) Refactor/cleanup/reduce slow tests [@stefanpenner](https://github.com/stefanpenner) +- [#6112](https://github.com/ember-cli/ember-cli/pull/6112) More specific docs for included hook [@xcambar](https://github.com/xcambar) +- [#6098](https://github.com/ember-cli/ember-cli/pull/6098) [BUGFIX beta] ServerWatcher disregards --watcher=* [@stefanpenner](https://github.com/stefanpenner) +- [#6166](https://github.com/ember-cli/ember-cli/pull/6166) changed --insecure-proxy to --secure-proxy in ember serve command [@badazz91](https://github.com/badazz91) - If the blueprint for the installed addon requires arguments, then - you can pass them too, for example, the `ember-cli-cordova` addon - needs an extra argument which you can pass running the command as - follows: `ember install:addon ember-cli-cordova com.myapp.app`. - -- [#2565](https://github.com/ember-cli/ember-cli/pull/2565) added - support for command options aliases, as well as aliases for - predefined options, this means that some commands can use aliases - for their existing options, for example, instead of running `ember g - route foo --type route` we can now use the -route alias: `ember g - route foo -route`. - - You can see available aliases for each command running `ember help`, - they will show as `aliases: ` follow by the alias. - -- [#2668](https://github.com/ember-cli/ember-cli/pull/2668) added the - `prepend` flag to `app.import` in `Brocfile.js`, allowing to prepend - a file to the vendor bundle rather than appended which is the - default behaviour. - - ``` - // Brocfile.js - app.import('bower_components/es5-shim/es5-shim.js', { - type: 'vendor', - prepend: true - }); - ``` - -- [#2694](https://github.com/ember-cli/ember-cli/pull/2694) disabled - default lookup & active generation logging in - `config/environment.js`. - -- [#2748](https://github.com/ember-cli/ember-cli/pull/2748) improved - the router generator to support properly nested routes and - resources, previously if you had a route similar like: - - ``` - Router.map(function() { - this.route("foo"); - }); - ``` - - And you did `ember g route foo/bar` the generated routes would be - - ``` - Router.map(function() { - this.route("foo"); - this.route("foo/bar"); - }); - ``` - - Now it keeps manages nested routes properly so the result would be: - - ``` - Router.map(function() { - this.route("foo", function() { - this.route("bar"); - }); - }); - ``` - - Additionally the option `--path` was added so you can do things like - `ember g route friends/edit --path=:friend_id/id` creating a nested - route under `friends` like: `this.route('edit', {path: ':friend_id/edit'})` - -- [#2734](https://github.com/ember-cli/ember-cli/pull/2734) changed - the options for editorconfig so it won't remove trailing whitespace - on .diff files. - -- [#2788](https://github.com/ember-cli/ember-cli/pull/2788) added an - `on('error')` handler to the proxy blueprint, with this your `ember - server` won't be killed when receiving `socket hang up` from the - `http-proxy`. - -- [#2741](https://github.com/stefanpenner/ember-cli/pull/2741) updated `broccoli-asset-rev` to 2.0.0. - -- [#2779](https://github.com/ember-cli/ember-cli/pull/2779) fixed a - bug in your `.ember-cli` file, if you had a liveReloadPort of say - "4200" it would not actually end up as that port. This casts the - string to a number so that the port is set correctly. - -- [#2817](https://github.com/ember-cli/ember-cli/pull/2817) added a - new feature so [Leek](https://github.com/twokul/leek) can be - configured through your `.ember-cli` file. It means you will be able - to configure the URLs Leek sends requests to, with this you can plug - internal tools and track usage patterns. - -- [#2828](https://github.com/ember-cli/ember-cli/pull/2828) added the - option to consume `app.env` before app instance creation in your - Brocfile, this is useful if you want to pass environment-dependent - options to the EmberApp constructor in your Brocfile: - - ``` - new EmberApp({ - someOption: EmberApp.env() === 'production' ? 'foo' : 'bar'; - }); - ``` -- [#2829](https://github.com/ember-cli/ember-cli/pull/2829) fixed an - issue on the model-test blueprint which was causing the build to fail - when the options `needs` wasn't present. - -- [#2832](https://github.com/ember-cli/ember-cli/pull/2832) added a - buildError hook which will be called when an error occurs during the - `preBuild` or `postBuild` hooks for addons, or when `builder#build` - fails hook. - -- [#2836](https://github.com/ember-cli/ember-cli/pull/2836) added a - check when passing `--proxy` to `ember server`. If the URL doesn't - include `http` or `https` then the command will fail since it - requires the protocol in order to get the proxy working correctly. - -#### Addons - -- [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/1df573947070214b058e830e962a673fc9819925) - -- [#2693](https://github.com/ember-cli/ember-cli/pull/2693) fixed an - issue with blueprints ensuring that last loaded blueprint takes - precedence. - -- [#2805](https://github.com/ember-cli/ember-cli/pull/2805) Added the - `install:addon` command, which installs an addon with NPM and then - runs the included generator of the same name if it provides one, - additionally if you addon generator's name is different to the addon - name, you can pass the option `defaultBlueprint` in your - `package.json` and the command will run the generator after - installed. The following will run `ember g cordova-starter-kit` - after it has successfully installed `ember-cli-cordova` - - ``` - name: 'ember-cli-cordova', - 'ember-addon': { - defaultBlueprint: 'cordova-starter-kit' - } - ``` -- [#2775](https://github.com/ember-cli/ember-cli/pull/2775) added a - default `.jshintrc` for `in-repo-addons` so they are treated as - `Node` applications. - -### 0.1.4 - -#### Applications - -* [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/94439eb22e76e7e71be472c264bef40235583fa9) -* [BUGFIX] Use the container from the created Ember.Application for initializer tests. [#2582](https://github.com/stefanpenner/ember-cli/pull/2582) -* [ENHANCEMENT] Add extra contentFor hooks [#2588](https://github.com/stefanpenner/ember-cli/pull/2592) - * `{{content-for 'head-footer'}}` - * `{{content-for 'test-head-footer'}}` - * `{{content-for 'body-footer'}}` - * `{{content-for 'test-body-footer'}}` -* [BUGFIX] Create separate server blueprint to stop http-{mock,proxy} removing files [#2610](https://github.com/stefanpenner/ember-cli/pull/2610) -* [BUGFIX] Fixes `--proxy` so it proxies correctly to API's under subdomains [#2615](https://github.com/stefanpenner/ember-cli/pull/2615) -* [BUGFIX] Ensure `watchman` does not conflict with NPM's `watchman` package. [#2645](https://github.com/stefanpenner/ember-cli/pull/2645) -* [BUGFIX] Ensure that the generated meta tag is now self closing. [#2661](https://github.com/stefanpenner/ember-cli/pull/2661) - -#### Addons - -* [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/1ba17bff6003dd176a038113f412fc24a26b03d2) - -### 0.1.3 - -#### Applications - - * [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/25323aef6cd8a4e277dc451aa4fa0d80a1715acd) - * [#2586](https://github.com/stefanpenner/ember-cli/pull/2586) Set locationType to none in tests. - * [#2573](https://github.com/stefanpenner/ember-cli/pull/2574) Added --silent option for quieter UI - * [#2458](https://github.com/stefanpenner/ember-cli/pull/2458) Added additional file watching mechanism: [Watchman](https://facebook.github.io/watchman/) - This helps resolve the recent Node + Yosemite file watching issues, but also improves file watching (when available) for all `*nix` systems - - What is Watchman? - - Watchman runs as a standalone service, this allows it to manage file-watching for multiple consumers (in our case ember-cli apps) - - How do I used it? - homebrew: `brew install watchman` - other: https://facebook.github.io/watchman/docs/install.html - windows: not supported yet, but it [may happen](https://github.com/facebook/watchman/issues/19) - - What happens if its not installed? - - We fall back to the existing watcher NodeWatcher - - How do I force it to fallback to NodeWatch - - ```sh - ember --watcher=node - ``` - - Common problem: `invalid watchman found, version: [2.9.8] did not satisfy [^3.0.0]` this basically means you have an older version of watchman installed. Be sure to install `3.0.0` and run `watchman shutdown-server` before re-starting your ember server. - - * [#2265](https://github.com/stefanpenner/ember-cli/pull/2265) Added auto-restarting of server and triggering of LR on `server/*` file changes - * [#2535](https://github.com/stefanpenner/ember-cli/pull/2535) Updated broccoli-asset-rev to 1.0.0 - * [#2452](https://github.com/stefanpenner/ember-cli/pull/2452) Including [esnext](https://github.com/esnext/esnext) via `ember-cli-esnext` per default - * [#2518](https://github.com/stefanpenner/ember-cli/pull/2518) improved HTTP logging when using http-mocks and proxy by using [morgan](https://www.npmjs.org/package/morgan) - * [#2532](https://github.com/stefanpenner/ember-cli/pull/2532) Added support to run specific tests via `ember test --module` and `ember test --filter` - * [#2514](https://github.com/stefanpenner/ember-cli/pull/2514) Added config.usePodsByDefault for users who wish to have blueprints run in `pod` mode all the time - * Warn on invalid command options - * Allow array of paths to the preprocessCss phase - * Adding --pods support for adapters, serializers, and transforms - * As part of the Ember 2.0 push remove controller types. - * http-mock now follows ember-data conventions - * many of ember-cli internals now are instrumented with [debug] - usage: `DEBUG=ember-cli:* ember ` to see ember-cli specific verbose logging. - * Added ember-cli-dependency-checker to app's package.json - * Added option to disable auto-start of ember app. - * Added optional globbing to init with `ember init `, this allows you to re-blueprint a single file like: `ember init app/index.html` - * Added support to test the app when built with `--env production`. - * Update to Ember 1.8.1 - * Update to Ember Data v1.0.0-beta.11 - * [#2351](https://github.com/stefanpenner/ember-cli/pull/2351) Fix automatic generated model belongs-to and has-many relations to resolve test lookup. - * [#1888](https://github.com/stefanpenner/ember-cli/pull/1888) Allow multiple SASS/LESS files to be built by populating `outputPaths.app.css` option - * [#2523](https://github.com/stefanpenner/ember-cli/pull/2523) Added `outputPaths.app.html` option - * [#2472](https://github.com/stefanpenner/ember-cli/pull/2472) Added Pod support for test blueprints. - - Add much more: [view entire diff](https://github.com/stefanpenner/ember-cli/compare/v0.1.2...v0.1.3) - -#### Addons - - * [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/b75bc2c5e0d8d5f954d6c7adcb108f71fdef9ebf) - * [#2505](https://github.com/stefanpenner/ember-cli/pull/2505) Added ability to dynamic add/remove module whitelist entries so that the [ember-browserify](https://github.com/ef4/ember-browserify) addon can work - * [#2505](https://github.com/stefanpenner/ember-cli/pull/2505) Added an addon postprocess hook for all javascript - * [#2271](https://github.com/stefanpenner/ember-cli/pull/2271) Added Addon.prototype.isEnabled for an addon to exclude itself from the project at runtime. - * [#2451](https://github.com/stefanpenner/ember-cli/pull/2451) Ensure that in-repo addons are watched. - * [#2411](https://github.com/stefanpenner/ember-cli/pull/2411) Add preBuild hook for addons. - -### 0.1.2 - -#### Applications - -* [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/0e6a6834b46df72658225490073980c44413892f) -* [BREAKING ENHANCEMENT] Remove hard-coded support for `broccoli-less-single`, use `ember-cli-less` for `.less` support now. [#2210](https://github.com/stefanpenner/ember-cli/pull/2210) -* [ENHANCEMENT] Provide a helpful error if the configuration info cannot be read from the proper `` tag. [#2219](https://github.com/stefanpenner/ember-cli/pull/2219) -* [ENHANCEMENT] Allow test filtering from the command line. Running `ember test --filter "foo bar"` or `ember test --server --filter "foo bar"` will limit test runs - to tests that contain "foo bar" in their module name or test name. [#2223](https://github.com/stefanpenner/ember-cli/pull/2223) -* [ENHANCEMENT] Add a few more `content-for` hooks to `index.html` and `tests/index.html`. [#2236](https://github.com/stefanpenner/ember-cli/pull/2236) -* [ENHANCEMENT] Properly display the file causing build errors in `ember build --watch` and `ember serve` commands. [#2237](https://github.com/stefanpenner/ember-cli/pull/2237), [#2246](https://github.com/stefanpenner/ember-cli/pull/2246), and [#2297](https://github.com/stefanpenner/ember-cli/pull/2297) -* [ENHANCEMENT] Update `broccoli-asset-rev` to 0.3.1. [#2250](https://github.com/stefanpenner/ember-cli/pull/2250) -* [ENHANCEMENT] Add `ember-export-application-global` to allow easier debugging. [#2270](https://github.com/stefanpenner/ember-cli/pull/2270) -* [BUGFIX] Fix default `.gitignore` to properly match `bower_components`. [#2285](https://github.com/stefanpenner/ember-cli/pull/2285) -* [ENHANCEMENT] Display `baseURL` in `ember serve` startup messages. [#2291](https://github.com/stefanpenner/ember-cli/pull/2291) -* [BUGFIX] Fix issues resulting in files outside of `tmp/` being removed due to following of symlinks. [#2290](https://github.com/stefanpenner/ember-cli/pull/2290) and [#2301](https://github.com/stefanpenner/ember-cli/pull/2301) -* [ENHANCEMENT] Add --watcher=polling option to `ember test --server`. This provides a work around for folks having `EMFILE` errors in some scenarios. [#2296](https://github.com/stefanpenner/ember-cli/pull/2296) -* [ENHANCEMENT] Allow opting out of storing the applications configuration in the generated `index.html` via `storeConfigInMeta` option in the `Brocfile.js`. [#2298](https://github.com/stefanpenner/ember-cli/pull/2298) -* [BUGFIX] Update ember-cli-content-security-policy and ember-cli-inject-live-reload packages to latest version. Allows livereload to function properly regardless - of host (0.1.0 always assumed `localhost` for the livereload server). [#2306](https://github.com/stefanpenner/ember-cli/pull/2306) -* [ENHANCEMENT] Update internal dependencies to latest versions. [#2307](https://github.com/stefanpenner/ember-cli/pull/2307) -* [BUGFIX] Allow overriding of vendor files to not loose required ordering. [#2312](https://github.com/stefanpenner/ember-cli/pull/2312) -* [ENHANCEMENT] Add `bowerDirectory` to `Project` model (discovered on initialization). [#2287](https://github.com/stefanpenner/ember-cli/pull/2287) - -#### Addons - -* [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/333ec703fba364bf5d2dcb3cc1d04a7b65c246f0) -* [ENHANCEMENT] Allow addons to inject middleware into testem. [#2128](https://github.com/stefanpenner/ember-cli/pull/2128) -* [ENHANCEMENT] Add {{content-for 'body'}} to `app/index.html` and `tests/index.html`. [#2236](https://github.com/stefanpenner/ember-cli/pull/2236) -* [ENHANCEMENT] Add {{content-for 'test-head'}} to `tests/index.html`. [#2236](https://github.com/stefanpenner/ember-cli/pull/2236) -* [ENHANCEMENT] Add {{content-for 'test-body'}} to `tests/index.html`. [#2236](https://github.com/stefanpenner/ember-cli/pull/2236) -* [ENHANCEMENT] Allow adding multiple bower packages at once via `Blueprint.prototype.addBowerPackagesToProject`. [#2222](https://github.com/stefanpenner/ember-cli/pull/2222) -* [ENHANCEMENT] Allow adding multiple NPM packages at once via `Blueprint.prototype.addPackagesToProject`. [#2245](https://github.com/stefanpenner/ember-cli/pull/2245) -* [ENHANCEMENT] Ensure generated addons are in strict mode. [#2295](https://github.com/stefanpenner/ember-cli/pull/2295) -* [BUGFIX] Ensure that addon's with `addon/styles/app.css` are able to compile properly (copying contents of `addon/styles/app.css` into `vendor.css`). [#2301](https://github.com/stefanpenner/ember-cli/pull/2301) -* [ENHANCEMENT] Provide the `httpServer` instance to `serverMiddleware` (and `./server/index.js`). [#2302](https://github.com/stefanpenner/ember-cli/issues/2302) - -#### Blueprints - -* [ENHANCEMENT] Tweak helper blueprint to make it easier to test. [#2257](https://github.com/stefanpenner/ember-cli/pull/2257) -* [ENHANCEMENT] Streamline initializer and service blueprints. [#2260](https://github.com/stefanpenner/ember-cli/pull/2260) - -### 0.1.1 - -#### Applications - -* [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/c97633f87717074986403e3ad87149d3bd8d1ee3) -* [BUGFIX] Fix symlink regression in Windows (update broccoli-file-remover to 0.3.1). [#2204](https://github.com/stefanpenner/ember-cli/pull/2204) - -#### Addons - -* [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/decc6c6c87071d271ee4b86dc292b7a353ead0e1) - -### 0.1.0 - -#### Applications - -* [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/d35102254aeac097405167f2abeef57b92def518) -* [ENHANCEMENT] Add symlinking to speed up Broccoli builds. [#2125](https://github.com/stefanpenner/ember-cli/pull/2125) -* [BUGFIX] Fix issue with livereload in 0.0.47. [#2176](https://github.com/stefanpenner/ember-cli/pull/2176) -* [BUGFIX] Change content security policy addon to use report only mode by default. [#2190](https://github.com/stefanpenner/ember-cli/pull/2190) -* [ENHANCEMENT] Allow addons to customize their ES6 module prefix (for `addon` tree). [#2189](https://github.com/stefanpenner/ember-cli/pull/2189) -* [BUGFIX] Ensure all addon hooks are executed in addon test harness. [#2195](https://github.com/stefanpenner/ember-cli/pull/2195) - -#### Addons - -* [`ember addon` diff](https://github.com/kellyselden/ember-addon-output/commit/84fd1c523407e9fa7df7d2a664a14ddb543ea5e0) - -### 0.0.47 - -#### Applications - -* [`ember new` diff](https://github.com/kellyselden/ember-cli-output/commit/7c59cf59fa42a9bf585b52455e424e2553aeb2aa) -* [ENHANCEMENT] Add `--pod` option to blueprints for generate and destroy. Add `fileMapTokens` hook to blueprints, and optional - blueprint file tokens `__path__` and `__test__` for pod support. [#1994](https://github.com/stefanpenner/ember-cli/pull/1994) -* [ENHANCEMENT] Provide better error messages when uncaught errors occur during `ember build` and `ember serve`. [#2043](https://github.com/stefanpenner/ember-cli/pull/2043) -* [ENHANCEMENT] Do not use inline ` - - - {{content-for 'body-footer'}} - - diff --git a/blueprints/app/files/app/router.js b/blueprints/app/files/app/router.js deleted file mode 100644 index cef554b3d9..0000000000 --- a/blueprints/app/files/app/router.js +++ /dev/null @@ -1,11 +0,0 @@ -import Ember from 'ember'; -import config from './config/environment'; - -var Router = Ember.Router.extend({ - location: config.locationType -}); - -Router.map(function() { -}); - -export default Router; diff --git a/blueprints/app/files/bower.json b/blueprints/app/files/bower.json deleted file mode 100644 index 6cb00e4ba6..0000000000 --- a/blueprints/app/files/bower.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "<%= name %>", - "dependencies": { - "ember": "2.0.0", - "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.4", - "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", - "ember-data": "2.0.0", - "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.5", - "ember-qunit": "0.4.10", - "ember-qunit-notifications": "0.0.7", - "ember-resolver": "~0.1.20", - "jquery": "^1.11.3", - "loader.js": "ember-cli/loader.js#3.2.1", - "qunit": "~1.18.0" - }, - "resolutions": { - "ember": "2.0.0" - } -} diff --git a/blueprints/app/files/ember-cli-build.js b/blueprints/app/files/ember-cli-build.js deleted file mode 100644 index a0c0b8462c..0000000000 --- a/blueprints/app/files/ember-cli-build.js +++ /dev/null @@ -1,23 +0,0 @@ -/* global require, module */ -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function(defaults) { - var app = new EmberApp(defaults, { - // Add options here - }); - - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. - - return app.toTree(); -}; diff --git a/blueprints/app/files/gitignore b/blueprints/app/files/gitignore deleted file mode 100644 index 86fceae7af..0000000000 --- a/blueprints/app/files/gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist -/tmp - -# dependencies -/node_modules -/bower_components - -# misc -/.sass-cache -/connect.lock -/coverage/* -/libpeerconnection.log -npm-debug.log -testem.log diff --git a/blueprints/app/files/package.json b/blueprints/app/files/package.json deleted file mode 100644 index b53b08ff45..0000000000 --- a/blueprints/app/files/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "<%= name %>", - "version": "0.0.0", - "description": "Small description for <%= name %> goes here", - "private": true, - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build", - "start": "ember server", - "test": "ember test" - }, - "repository": "", - "engines": { - "node": ">= 0.10.0" - }, - "author": "", - "license": "MIT", - "devDependencies": { - "broccoli-asset-rev": "^2.1.2", - "ember-cli": "<%= emberCLIVersion %>", - "ember-cli-app-version": "0.5.1", - "ember-cli-babel": "^5.1.3", - "ember-cli-content-security-policy": "0.4.0", - "ember-cli-dependency-checker": "^1.0.1", - "ember-cli-htmlbars": "^1.0.0", - "ember-cli-htmlbars-inline-precompile": "^0.2.0", - "ember-cli-ic-ajax": "0.2.1", - "ember-cli-inject-live-reload": "^1.3.1", - "ember-cli-qunit": "^1.0.1", - "ember-cli-release": "0.2.3", - "ember-cli-sri": "^1.0.3", - "ember-cli-uglify": "^1.2.0", - "ember-data": "2.0.0", - "ember-disable-proxy-controllers": "^1.0.0", - "ember-export-application-global": "^1.0.4" - } -} diff --git a/blueprints/app/files/public/crossdomain.xml b/blueprints/app/files/public/crossdomain.xml deleted file mode 100644 index 0c16a7a07b..0000000000 --- a/blueprints/app/files/public/crossdomain.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/blueprints/app/files/testem.json b/blueprints/app/files/testem.json deleted file mode 100644 index 0f35392cf2..0000000000 --- a/blueprints/app/files/testem.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "framework": "qunit", - "test_page": "tests/index.html?hidepassed", - "disable_watching": true, - "launch_in_ci": [ - "PhantomJS" - ], - "launch_in_dev": [ - "PhantomJS", - "Chrome" - ] -} diff --git a/blueprints/app/files/tests/.jshintrc b/blueprints/app/files/tests/.jshintrc deleted file mode 100644 index 6ec0b7c154..0000000000 --- a/blueprints/app/files/tests/.jshintrc +++ /dev/null @@ -1,52 +0,0 @@ -{ - "predef": [ - "document", - "window", - "location", - "setTimeout", - "$", - "-Promise", - "define", - "console", - "visit", - "exists", - "fillIn", - "click", - "keyEvent", - "triggerEvent", - "find", - "findWithAssert", - "wait", - "DS", - "andThen", - "currentURL", - "currentPath", - "currentRouteName" - ], - "node": false, - "browser": false, - "boss": true, - "curly": true, - "debug": false, - "devel": false, - "eqeqeq": true, - "evil": true, - "forin": false, - "immed": false, - "laxbreak": false, - "newcap": true, - "noarg": true, - "noempty": false, - "nonew": false, - "nomen": false, - "onevar": false, - "plusplus": false, - "regexp": false, - "undef": true, - "sub": true, - "strict": false, - "white": false, - "eqnull": true, - "esnext": true, - "unused": true -} diff --git a/blueprints/app/files/tests/helpers/resolver.js b/blueprints/app/files/tests/helpers/resolver.js deleted file mode 100644 index 28f4ece46a..0000000000 --- a/blueprints/app/files/tests/helpers/resolver.js +++ /dev/null @@ -1,11 +0,0 @@ -import Resolver from 'ember/resolver'; -import config from '../../config/environment'; - -var resolver = Resolver.create(); - -resolver.namespace = { - modulePrefix: config.modulePrefix, - podModulePrefix: config.podModulePrefix -}; - -export default resolver; diff --git a/blueprints/app/files/tests/helpers/start-app.js b/blueprints/app/files/tests/helpers/start-app.js deleted file mode 100644 index 0f7aab1afb..0000000000 --- a/blueprints/app/files/tests/helpers/start-app.js +++ /dev/null @@ -1,18 +0,0 @@ -import Ember from 'ember'; -import Application from '../../app'; -import config from '../../config/environment'; - -export default function startApp(attrs) { - var application; - - var attributes = Ember.merge({}, config.APP); - attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; - - Ember.run(function() { - application = Application.create(attributes); - application.setupForTesting(); - application.injectTestHelpers(); - }); - - return application; -} diff --git a/blueprints/app/files/tests/index.html b/blueprints/app/files/tests/index.html deleted file mode 100644 index d7a9d8fe48..0000000000 --- a/blueprints/app/files/tests/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - <%= namespace %> Tests - - - - {{content-for 'head'}} - {{content-for 'test-head'}} - - - - - - {{content-for 'head-footer'}} - {{content-for 'test-head-footer'}} - - - - {{content-for 'body'}} - {{content-for 'test-body'}} - - - - - - - {{content-for 'body-footer'}} - {{content-for 'test-body-footer'}} - - diff --git a/blueprints/app/files/tests/test-helper.js b/blueprints/app/files/tests/test-helper.js deleted file mode 100644 index e6cfb70fe8..0000000000 --- a/blueprints/app/files/tests/test-helper.js +++ /dev/null @@ -1,6 +0,0 @@ -import resolver from './helpers/resolver'; -import { - setResolver -} from 'ember-qunit'; - -setResolver(resolver); diff --git a/blueprints/app/index.js b/blueprints/app/index.js deleted file mode 100644 index 5b260efef3..0000000000 --- a/blueprints/app/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/*jshint node:true*/ - -var stringUtil = require('ember-cli-string-utils'); - -module.exports = { - description: 'The default blueprint for ember-cli projects.', - - locals: function(options) { - var entity = options.entity; - var rawName = entity.name; - var name = stringUtil.dasherize(rawName); - var namespace = stringUtil.classify(rawName); - - return { - name: name, - modulePrefix: name, - namespace: namespace, - emberCLIVersion: require('../../package').version - }; - } -}; diff --git a/blueprints/blueprint/files/blueprints/.jshintrc b/blueprints/blueprint/files/blueprints/.jshintrc deleted file mode 100644 index 33f4f6f4bb..0000000000 --- a/blueprints/blueprint/files/blueprints/.jshintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "predef": [ - "console" - ], - "strict": false -} diff --git a/blueprints/blueprint/index.js b/blueprints/blueprint/index.js deleted file mode 100644 index e7b8e25676..0000000000 --- a/blueprints/blueprint/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a blueprint and definition.' -}; diff --git a/blueprints/component-addon/files/__root__/__path__/__name__.js b/blueprints/component-addon/files/__root__/__path__/__name__.js deleted file mode 100644 index 71a8b71c1c..0000000000 --- a/blueprints/component-addon/files/__root__/__path__/__name__.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '<%= modulePath %>'; \ No newline at end of file diff --git a/blueprints/component-addon/index.js b/blueprints/component-addon/index.js deleted file mode 100644 index 006ced24af..0000000000 --- a/blueprints/component-addon/index.js +++ /dev/null @@ -1,56 +0,0 @@ -/*jshint node:true*/ - -var stringUtil = require('ember-cli-string-utils'); -var validComponentName = require('../../lib/utilities/valid-component-name'); -var getPathOption = require('../../lib/utilities/get-component-path-option'); -var path = require('path'); -var normalizeEntityName = require('ember-cli-normalize-entity-name'); - -module.exports = { - description: 'Generates a component. Name must contain a hyphen.', - - fileMapTokens: function() { - return { - __path__: function(options) { - if (options.pod) { - return path.join(options.podPath, options.locals.path, options.dasherizedModuleName); - } - return 'components'; - }, - __name__: function(options) { - if (options.pod) { - return 'component'; - } - return options.dasherizedModuleName; - }, - __root__: function(options) { - if (options.inRepoAddon) { - return path.join('lib', options.inRepoAddon, 'app'); - } - return 'app'; - } - }; - }, - - normalizeEntityName: function(entityName) { - entityName = normalizeEntityName(entityName); - - return validComponentName(entityName); - }, - - locals: function(options) { - var addonRawName = options.inRepoAddon ? options.inRepoAddon : options.project.name(); - var addonName = stringUtil.dasherize(addonRawName); - var fileName = stringUtil.dasherize(options.entity.name); - var importPathName = [addonName, 'components', fileName].join('/'); - - if (options.pod) { - importPathName = [addonName, 'components', fileName,'component'].join('/'); - } - - return { - modulePath: importPathName, - path: getPathOption(options) - }; - } -}; diff --git a/blueprints/component-test/files/tests/__testType__/__path__/__test__.js b/blueprints/component-test/files/tests/__testType__/__path__/__test__.js deleted file mode 100644 index 1f9cedee79..0000000000 --- a/blueprints/component-test/files/tests/__testType__/__path__/__test__.js +++ /dev/null @@ -1,34 +0,0 @@ -import { moduleForComponent, test } from 'ember-qunit';<% if (testType === 'integration') { %> -import hbs from 'htmlbars-inline-precompile';<% } %> - -moduleForComponent('<%= componentPathName %>', '<%= friendlyTestDescription %>', { - <% if (testType === 'integration' ) { %>integration: true<% } else if(testType === 'unit') { %>// Specify the other units that are required for this test - // needs: ['component:foo', 'helper:bar'], - unit: true<% } %> -}); - -test('it renders', function(assert) { - <% if (testType === 'integration' ) { %>assert.expect(2); - - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... });" + EOL + EOL + - - this.render(hbs`{{<%= componentPathName %>}}`); - - assert.equal(this.$().text(), ''); - - // Template block usage:" + EOL + - this.render(hbs` - {{#<%= componentPathName %>}} - template block text - {{/<%= componentPathName %>}} - `); - - assert.equal(this.$().text().trim(), 'template block text');<% } else if(testType === 'unit') { %>assert.expect(1); - - // Creates the component instance - var component = this.subject(); - // Renders the component to the page - this.render(); - assert.equal(this.$().text(), '');<% } %> -}); diff --git a/blueprints/component-test/index.js b/blueprints/component-test/index.js deleted file mode 100644 index 620b0ed59e..0000000000 --- a/blueprints/component-test/index.js +++ /dev/null @@ -1,59 +0,0 @@ -/*jshint node:true*/ - -var path = require('path'); -var testInfo = require('ember-cli-test-info'); -var stringUtil = require('ember-cli-string-utils'); -var getPathOption = require('../../lib/utilities/get-component-path-option'); - -module.exports = { - description: 'Generates a component integration or unit test.', - - availableOptions: [ - { - name: 'test-type', - type: ['integration', 'unit'], - default: 'integration', - aliases:[ - { 'i': 'integration'}, - { 'u': 'unit'}, - { 'integration': 'integration' }, - { 'unit': 'unit' } - ] - } - ], - - fileMapTokens: function() { - return { - __testType__: function(options) { - return options.locals.testType || 'integration'; - }, - __path__: function(options) { - if (options.pod) { - return path.join(options.podPath, options.locals.path, options.dasherizedModuleName); - } - return 'components'; - } - }; - }, - locals: function(options) { - var dasherizedModuleName = stringUtil.dasherize(options.entity.name); - var componentPathName = dasherizedModuleName; - var testType = options.testType || "integration"; - var friendlyTestDescription = testInfo.description(options.entity.name, "Integration", "Component"); - - if (options.pod && options.path !== 'components' && options.path !== '') { - componentPathName = [options.path, dasherizedModuleName].join('/'); - } - - if (options.testType === 'unit') { - friendlyTestDescription = testInfo.description(options.entity.name, "Unit", "Component"); - } - - return { - path: getPathOption(options), - testType: testType, - componentPathName: componentPathName, - friendlyTestDescription: friendlyTestDescription - }; - } -}; diff --git a/blueprints/component/files/__root__/__path__/__name__.js b/blueprints/component/files/__root__/__path__/__name__.js deleted file mode 100644 index fb1798c77c..0000000000 --- a/blueprints/component/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,4 +0,0 @@ -import Ember from 'ember'; -<%= importTemplate %> -export default Ember.Component.extend({<%= contents %> -}); diff --git a/blueprints/component/files/__root__/__templatepath__/__templatename__.hbs b/blueprints/component/files/__root__/__templatepath__/__templatename__.hbs deleted file mode 100644 index 889d9eeadc..0000000000 --- a/blueprints/component/files/__root__/__templatepath__/__templatename__.hbs +++ /dev/null @@ -1 +0,0 @@ -{{yield}} diff --git a/blueprints/component/index.js b/blueprints/component/index.js deleted file mode 100644 index 4b1e092bc0..0000000000 --- a/blueprints/component/index.js +++ /dev/null @@ -1,76 +0,0 @@ -/*jshint node:true*/ - -var stringUtil = require('ember-cli-string-utils'); -var pathUtil = require('ember-cli-path-utils'); -var validComponentName = require('../../lib/utilities/valid-component-name'); -var getPathOption = require('../../lib/utilities/get-component-path-option'); -var path = require('path'); - -var normalizeEntityName = require('ember-cli-normalize-entity-name'); - -module.exports = { - description: 'Generates a component. Name must contain a hyphen.', - - availableOptions: [ - { - name: 'path', - type: String, - default: 'components', - aliases:[ - {'no-path': ''} - ] - } - ], - - fileMapTokens: function() { - return { - __path__: function(options) { - if (options.pod) { - return path.join(options.podPath, options.locals.path, options.dasherizedModuleName); - } - return 'components'; - }, - __templatepath__: function(options) { - if (options.pod) { - return path.join(options.podPath, options.locals.path, options.dasherizedModuleName); - } - return 'templates/components'; - }, - __templatename__: function(options) { - if (options.pod) { - return 'template'; - } - return options.dasherizedModuleName; - } - }; - }, - - normalizeEntityName: function(entityName) { - entityName = normalizeEntityName(entityName); - - return validComponentName(entityName); - }, - - locals: function(options) { - var templatePath = ''; - var importTemplate = ''; - var contents = ''; - // if we're in an addon, build import statement - if (options.project.isEmberCLIAddon() || options.inRepoAddon && !options.inDummy) { - if(options.pod) { - templatePath = './template'; - } else { - templatePath = pathUtil.getRelativeParentPath(options.entity.name) + - 'templates/components/' + stringUtil.dasherize(options.entity.name); - } - importTemplate = 'import layout from \'' + templatePath + '\';\n'; - contents = '\n layout: layout'; - } - - return { - importTemplate: importTemplate, - contents: contents, - path: getPathOption(options) - }; - } -}; diff --git a/blueprints/controller-test/files/tests/unit/__path__/__test__.js b/blueprints/controller-test/files/tests/unit/__path__/__test__.js deleted file mode 100644 index 0528f3de30..0000000000 --- a/blueprints/controller-test/files/tests/unit/__path__/__test__.js +++ /dev/null @@ -1,12 +0,0 @@ -import { moduleFor, test } from 'ember-qunit'; - -moduleFor('controller:<%= dasherizedModuleName %>', { - // Specify the other units that are required for this test. - // needs: ['controller:foo'] -}); - -// Replace this with your real tests. -test('it exists', function(assert) { - var controller = this.subject(); - assert.ok(controller); -}); diff --git a/blueprints/controller-test/index.js b/blueprints/controller-test/index.js deleted file mode 100644 index 3f25677790..0000000000 --- a/blueprints/controller-test/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a controller unit test.' -}; diff --git a/blueprints/controller/files/__root__/__path__/__name__.js b/blueprints/controller/files/__root__/__path__/__name__.js deleted file mode 100644 index 55ff9aa587..0000000000 --- a/blueprints/controller/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,4 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Controller.extend({ -}); diff --git a/blueprints/controller/index.js b/blueprints/controller/index.js deleted file mode 100644 index cc81d47373..0000000000 --- a/blueprints/controller/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a controller.' -}; diff --git a/blueprints/helper-addon/files/__root__/__path__/__name__.js b/blueprints/helper-addon/files/__root__/__path__/__name__.js deleted file mode 100644 index a34b84552a..0000000000 --- a/blueprints/helper-addon/files/__root__/__path__/__name__.js +++ /dev/null @@ -1 +0,0 @@ -export { default, <%= camelizedModuleName %> } from '<%= modulePath %>'; diff --git a/blueprints/helper-addon/index.js b/blueprints/helper-addon/index.js deleted file mode 100644 index a123a8176f..0000000000 --- a/blueprints/helper-addon/index.js +++ /dev/null @@ -1,3 +0,0 @@ -/*jshint node:true*/ - -module.exports = require('../addon-import'); diff --git a/blueprints/helper-test/files/tests/unit/helpers/__name__-test.js b/blueprints/helper-test/files/tests/unit/helpers/__name__-test.js deleted file mode 100644 index 5ef494e6e5..0000000000 --- a/blueprints/helper-test/files/tests/unit/helpers/__name__-test.js +++ /dev/null @@ -1,10 +0,0 @@ -import { <%= camelizedModuleName %> } from '<%= dependencyDepth %>/helpers/<%= dasherizedModuleName %>'; -import { module, test } from 'qunit'; - -module('<%= friendlyTestName %>'); - -// Replace this with your real tests. -test('it works', function(assert) { - var result = <%= camelizedModuleName %>(42); - assert.ok(result); -}); diff --git a/blueprints/helper-test/index.js b/blueprints/helper-test/index.js deleted file mode 100644 index b0d1a2a854..0000000000 --- a/blueprints/helper-test/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/*jshint node:true*/ - -var getDependencyDepth = require('ember-cli-get-dependency-depth'); -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a helper unit test.', - locals: function(options) { - return { - friendlyTestName: testInfo.name(options.entity.name, "Unit", "Helper"), - dependencyDepth: getDependencyDepth(options.entity.name) - }; - } -}; diff --git a/blueprints/helper/files/__root__/helpers/__name__.js b/blueprints/helper/files/__root__/helpers/__name__.js deleted file mode 100644 index eb31fd5447..0000000000 --- a/blueprints/helper/files/__root__/helpers/__name__.js +++ /dev/null @@ -1,7 +0,0 @@ -import Ember from 'ember'; - -export function <%= camelizedModuleName %>(params/*, hash*/) { - return params; -} - -export default Ember.Helper.helper(<%= camelizedModuleName %>); diff --git a/blueprints/helper/index.js b/blueprints/helper/index.js deleted file mode 100644 index ccf5b7c180..0000000000 --- a/blueprints/helper/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; -/*jshint node:true*/ -var normalizeEntityName = require('ember-cli-normalize-entity-name'); - -module.exports = { - description: 'Generates a helper function.', - normalizeEntityName: function(entityName) { - return normalizeEntityName(entityName); - } -}; diff --git a/blueprints/http-mock/files/server/mocks/__name__.js b/blueprints/http-mock/files/server/mocks/__name__.js index b815ee964c..169ecd4e05 100644 --- a/blueprints/http-mock/files/server/mocks/__name__.js +++ b/blueprints/http-mock/files/server/mocks/__name__.js @@ -1,6 +1,8 @@ +'use strict'; + module.exports = function(app) { - var express = require('express'); - var <%= camelizedModuleName %>Router = express.Router(); + const express = require('express'); + let <%= camelizedModuleName %>Router = express.Router(); <%= camelizedModuleName %>Router.get('/', function(req, res) { res.send({ @@ -32,5 +34,15 @@ module.exports = function(app) { res.status(204).end(); }); + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to `use` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/<%= decamelizedModuleName %>', require('body-parser').json()); app.use('/api/<%= decamelizedModuleName %>', <%= camelizedModuleName %>Router); }; diff --git a/blueprints/http-mock/index.js b/blueprints/http-mock/index.js index f8bed02e6c..f129fea961 100644 --- a/blueprints/http-mock/index.js +++ b/blueprints/http-mock/index.js @@ -1,37 +1,36 @@ -/*jshint node:true*/ -var Blueprint = require('../../lib/models/blueprint'); -var isPackageMissing = require('ember-cli-is-package-missing'); +'use strict'; + +const Blueprint = require('@ember-tooling/blueprint-model'); +const isPackageMissing = require('ember-cli-is-package-missing'); +const SilentError = require('silent-error'); module.exports = { - description: 'Generates a mock api endpoint in /api prefix.', + description: '[Classic Only] Generates a mock api endpoint in /api prefix.', - anonymousOptions: [ - 'endpoint-path' - ], + anonymousOptions: ['endpoint-path'], - locals: function(options) { + locals(options) { return { - path: '/' + options.entity.name.replace(/^\//, '') + path: `/${options.entity.name.replace(/^\//, '')}`, }; }, - beforeInstall: function(options) { - var serverBlueprint = Blueprint.lookup('server', { + beforeInstall(options) { + if (this.project.isViteProject()) { + throw new SilentError('The http-mock blueprint is not supported in Vite projects.'); + } + + let serverBlueprint = Blueprint.lookup('server', { ui: this.ui, - analytics: this.analytics, - project: this.project + project: this.project, }); return serverBlueprint.install(options); }, - afterInstall: function(options) { - + afterInstall(options) { if (!options.dryRun && isPackageMissing(this, 'express')) { - return this.addPackagesToProject([ - { name: 'express', target: '^4.8.5' } - ]); + return this.addPackagesToProject([{ name: 'express', target: '^4.8.5' }]); } - - } + }, }; diff --git a/blueprints/http-proxy/files/server/proxies/__name__.js b/blueprints/http-proxy/files/server/proxies/__name__.js index 6d439a1fab..5d6e246a82 100644 --- a/blueprints/http-proxy/files/server/proxies/__name__.js +++ b/blueprints/http-proxy/files/server/proxies/__name__.js @@ -1,9 +1,11 @@ -var proxyPath = '/<%=camelizedModuleName %>'; +'use strict'; + +const proxyPath = '/<%=camelizedModuleName %>'; module.exports = function(app) { // For options, see: // https://github.com/nodejitsu/node-http-proxy - var proxy = require('http-proxy').createProxyServer({}); + let proxy = require('http-proxy').createProxyServer({}); proxy.on('error', function(err, req) { console.error(err, req.url); diff --git a/blueprints/http-proxy/index.js b/blueprints/http-proxy/index.js index abe1afc9a3..cafc873060 100644 --- a/blueprints/http-proxy/index.js +++ b/blueprints/http-proxy/index.js @@ -1,36 +1,35 @@ -/*jshint node:true*/ +'use strict'; -var Blueprint = require('../../lib/models/blueprint'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const SilentError = require('silent-error'); module.exports = { - description: 'Generates a relative proxy to another server.', + description: '[Classic Only] Generates a relative proxy to another server.', - anonymousOptions: [ - 'local-path', - 'remote-url' - ], + anonymousOptions: ['local-path', 'remote-url'], - locals: function(options) { - var proxyUrl = options.args[2]; + locals(options) { + let proxyUrl = options.args[2]; return { - path: '/' + options.entity.name.replace(/^\//, ''), - proxyUrl: proxyUrl + path: `/${options.entity.name.replace(/^\//, '')}`, + proxyUrl, }; }, - beforeInstall: function(options) { - var serverBlueprint = Blueprint.lookup('server', { + beforeInstall(options) { + if (this.project.isViteProject()) { + throw new SilentError('The http-proxy blueprint is not supported in Vite projects.'); + } + + let serverBlueprint = Blueprint.lookup('server', { ui: this.ui, - analytics: this.analytics, - project: this.project + project: this.project, }); return serverBlueprint.install(options); }, - afterInstall: function() { - return this.addPackagesToProject([ - { name: 'http-proxy', target: '^1.1.6' } - ]); - } + afterInstall() { + return this.addPackagesToProject([{ name: 'http-proxy', target: '^1.1.6' }]); + }, }; diff --git a/blueprints/in-repo-addon/files/__root__/__name__/index.js b/blueprints/in-repo-addon/files/__root__/__name__/index.js new file mode 100755 index 0000000000..57cf2c24dc --- /dev/null +++ b/blueprints/in-repo-addon/files/__root__/__name__/index.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + name: require('./package').name, + + isDevelopingAddon() { + return true; + } +}; diff --git a/blueprints/in-repo-addon/files/lib/__name__/package.json b/blueprints/in-repo-addon/files/__root__/__name__/package.json similarity index 100% rename from blueprints/in-repo-addon/files/lib/__name__/package.json rename to blueprints/in-repo-addon/files/__root__/__name__/package.json diff --git a/blueprints/in-repo-addon/files/lib/__name__/index.js b/blueprints/in-repo-addon/files/lib/__name__/index.js deleted file mode 100755 index 633222eda9..0000000000 --- a/blueprints/in-repo-addon/files/lib/__name__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - name: '<%= dasherizedModuleName %>', - - isDevelopingAddon: function() { - return true; - } -}; diff --git a/blueprints/in-repo-addon/index.js b/blueprints/in-repo-addon/index.js index fb94d030d0..71fbc0d841 100755 --- a/blueprints/in-repo-addon/index.js +++ b/blueprints/in-repo-addon/index.js @@ -1,37 +1,91 @@ -/*jshint node:true*/ +'use strict'; -var fs = require('fs-extra'); -var path = require('path'); -var stringUtil = require('ember-cli-string-utils'); -var Blueprint = require('../../lib/models/blueprint'); +const fs = require('fs-extra'); +const path = require('path'); +const stringUtil = require('ember-cli-string-utils'); +const stringifyAndNormalize = require('../../lib/utilities/stringify-and-normalize'); module.exports = { description: 'The blueprint for addon in repo ember-cli addons.', - beforeInstall: function(options) { - var libBlueprint = Blueprint.lookup('lib', { - ui: this.ui, - analytics: this.analytics, - project: this.project - }); + beforeInstall(options) { + let { root } = this._processTokens(options.entity.name); - return libBlueprint.install(options); + fs.mkdirsSync(root); }, - afterInstall: function(options) { - var packagePath = path.join(this.project.root, 'package.json'); - var contents = JSON.parse(fs.readFileSync(packagePath, { encoding: 'utf8' })); - var name = stringUtil.dasherize(options.entity.name); - var newPath = ['lib', name].join('/'); - var paths; + _processTokens(name) { + let root = 'lib'; + + if (name.match(/[./]/)) { + root = path.dirname(name); + name = path.basename(name); + } + + name = stringUtil.dasherize(name); + + return { root, name }; + }, + + locals(options) { + let { name } = this._processTokens(options.entity.name); + + return { + dasherizedModuleName: name, + }; + }, + + fileMapTokens() { + return { + __root__: (options) => this._processTokens(options.dasherizedModuleName).root, + __name__: (options) => this._processTokens(options.dasherizedModuleName).name, + }; + }, + + afterInstall(options) { + this._generatePackageJson(options, true); + }, + + afterUninstall(options) { + this._generatePackageJson(options, false); + }, + + _generatePackageJson(options, isInstall) { + let packagePath = path.join(this.project.root, 'package.json'); + let contents = this._readJsonSync(packagePath); + let { root, name } = this._processTokens(options.entity.name); + let newPath = [root, name].join('/'); + let paths; contents['ember-addon'] = contents['ember-addon'] || {}; paths = contents['ember-addon']['paths'] = contents['ember-addon']['paths'] || []; - if (paths.indexOf(newPath) === -1) { - paths.push(newPath); + if (isInstall) { + if (paths.indexOf(newPath) === -1) { + paths.push(newPath); + contents['ember-addon']['paths'] = paths.sort(); + } + } else { + let newPathIndex = paths.indexOf(newPath); + if (newPathIndex > -1) { + paths.splice(newPathIndex, 1); + } + if (paths.length === 0) { + delete contents['ember-addon']['paths']; + } + if (Object.keys(contents['ember-addon']).length === 0) { + delete contents['ember-addon']; + } } - fs.writeFileSync(packagePath, JSON.stringify(contents, null, 2)); - } + this._writeFileSync(packagePath, stringifyAndNormalize(contents)); + }, + + _readJsonSync(path) { + return fs.readJsonSync(path); + }, + + _writeFileSync(path, content) { + fs.writeFileSync(path, content); + }, }; diff --git a/blueprints/initializer-addon/files/__root__/__path__/__name__.js b/blueprints/initializer-addon/files/__root__/__path__/__name__.js deleted file mode 100644 index 79e541af69..0000000000 --- a/blueprints/initializer-addon/files/__root__/__path__/__name__.js +++ /dev/null @@ -1 +0,0 @@ -export { default, initialize } from '<%= modulePath %>'; diff --git a/blueprints/initializer-addon/index.js b/blueprints/initializer-addon/index.js deleted file mode 100644 index a123a8176f..0000000000 --- a/blueprints/initializer-addon/index.js +++ /dev/null @@ -1,3 +0,0 @@ -/*jshint node:true*/ - -module.exports = require('../addon-import'); diff --git a/blueprints/initializer-test/files/tests/unit/initializers/__name__-test.js b/blueprints/initializer-test/files/tests/unit/initializers/__name__-test.js deleted file mode 100644 index eab245b9c8..0000000000 --- a/blueprints/initializer-test/files/tests/unit/initializers/__name__-test.js +++ /dev/null @@ -1,23 +0,0 @@ -import Ember from 'ember'; -import { initialize } from '<%= dependencyDepth %>/initializers/<%= dasherizedModuleName %>'; -import { module, test } from 'qunit'; - -var registry, application; - -module('<%= friendlyTestName %>', { - beforeEach: function() { - Ember.run(function() { - application = Ember.Application.create(); - registry = application.registry; - application.deferReadiness(); - }); - } -}); - -// Replace this with your real tests. -test('it works', function(assert) { - initialize(registry, application); - - // you would normally confirm the results of the initializer here - assert.ok(true); -}); diff --git a/blueprints/initializer-test/index.js b/blueprints/initializer-test/index.js deleted file mode 100644 index b3460fc47e..0000000000 --- a/blueprints/initializer-test/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/*jshint node:true*/ - -var getDependencyDepth = require('ember-cli-get-dependency-depth'); -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates an initializer unit test.', - locals: function(options) { - return { - friendlyTestName: testInfo.name(options.entity.name, "Unit", "Initializer"), - dependencyDepth: getDependencyDepth(options.entity.name) - }; - } -}; diff --git a/blueprints/initializer/files/__root__/initializers/__name__.js b/blueprints/initializer/files/__root__/initializers/__name__.js deleted file mode 100644 index 96de5a3534..0000000000 --- a/blueprints/initializer/files/__root__/initializers/__name__.js +++ /dev/null @@ -1,8 +0,0 @@ -export function initialize(/* container, application */) { - // application.inject('route', 'foo', 'service:foo'); -} - -export default { - name: '<%= dasherizedModuleName %>', - initialize: initialize -}; diff --git a/blueprints/initializer/index.js b/blueprints/initializer/index.js deleted file mode 100644 index 48760763b4..0000000000 --- a/blueprints/initializer/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates an initializer.' -}; diff --git a/blueprints/lib/files/lib/.jshintrc b/blueprints/lib/files/lib/.jshintrc deleted file mode 100755 index 839c191fa9..0000000000 --- a/blueprints/lib/files/lib/.jshintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "node": true, - "browser": false -} diff --git a/blueprints/lib/index.js b/blueprints/lib/index.js index 0c9247478e..2eeeb4e22e 100755 --- a/blueprints/lib/index.js +++ b/blueprints/lib/index.js @@ -1,7 +1,16 @@ -/*jshint node:true*/ +'use strict'; + +const fs = require('fs-extra'); module.exports = { description: 'Generates a lib directory for in-repo addons.', - normalizeEntityName: function(name) { return name; } + normalizeEntityName(name) { + return name; + }, + + beforeInstall() { + // make sure to create `lib` directory + fs.mkdirsSync('lib'); + }, }; diff --git a/blueprints/mixin-test/files/tests/unit/mixins/__name__-test.js b/blueprints/mixin-test/files/tests/unit/mixins/__name__-test.js deleted file mode 100644 index 5f6e124cc2..0000000000 --- a/blueprints/mixin-test/files/tests/unit/mixins/__name__-test.js +++ /dev/null @@ -1,12 +0,0 @@ -import Ember from 'ember'; -import <%= classifiedModuleName %>Mixin from '../../../mixins/<%= dasherizedModuleName %>'; -import { module, test } from 'qunit'; - -module('<%= friendlyTestName %>'); - -// Replace this with your real tests. -test('it works', function(assert) { - var <%= classifiedModuleName %>Object = Ember.Object.extend(<%= classifiedModuleName %>Mixin); - var subject = <%= classifiedModuleName %>Object.create(); - assert.ok(subject); -}); diff --git a/blueprints/mixin-test/index.js b/blueprints/mixin-test/index.js deleted file mode 100644 index 27ca2fd7a3..0000000000 --- a/blueprints/mixin-test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/*jshint node:true*/ - -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a mixin unit test.', - locals: function(options) { - return { - friendlyTestName: testInfo.name(options.entity.name, 'Unit', 'Mixin') - }; - } -}; diff --git a/blueprints/mixin/files/__root__/mixins/__name__.js b/blueprints/mixin/files/__root__/mixins/__name__.js deleted file mode 100644 index abf754d716..0000000000 --- a/blueprints/mixin/files/__root__/mixins/__name__.js +++ /dev/null @@ -1,4 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Mixin.create({ -}); diff --git a/blueprints/mixin/index.js b/blueprints/mixin/index.js deleted file mode 100644 index 891547dd1f..0000000000 --- a/blueprints/mixin/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a mixin.' -}; diff --git a/blueprints/model-test/files/tests/unit/__path__/__test__.js b/blueprints/model-test/files/tests/unit/__path__/__test__.js deleted file mode 100644 index 88003fc2be..0000000000 --- a/blueprints/model-test/files/tests/unit/__path__/__test__.js +++ /dev/null @@ -1,12 +0,0 @@ -import { moduleForModel, test } from 'ember-qunit'; - -moduleForModel('<%= dasherizedModuleName %>', '<%= friendlyDescription %>', { - // Specify the other units that are required for this test. -<%= typeof needs !== 'undefined' ? needs : '' %> -}); - -test('it exists', function(assert) { - var model = this.subject(); - // var store = this.store(); - assert.ok(!!model); -}); diff --git a/blueprints/model-test/index.js b/blueprints/model-test/index.js deleted file mode 100644 index d85bd57e38..0000000000 --- a/blueprints/model-test/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/*jshint node:true*/ - -var ModelBlueprint = require('../model'); -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a model unit test.', - - locals: function(options) { - var result = ModelBlueprint.locals.apply(this, arguments); - - result.friendlyDescription = testInfo.description(options.entity.name, "Unit", "Model"); - - return result; - } -}; diff --git a/blueprints/model/HELP.md b/blueprints/model/HELP.md deleted file mode 100644 index 78bd0af7d4..0000000000 --- a/blueprints/model/HELP.md +++ /dev/null @@ -1,25 +0,0 @@ -You may generate models with as many attrs as you would like to pass. The following attribute types are supported: - - :array - :boolean - :date - :object - :number - :string - :your-custom-transform - :belongs-to: - :has-many: - -For instance: \`ember generate model taco filling:belongs-to:protein toppings:has-many:toppings name:string price:number misc\` -would result in the following model: - -```js -import DS from 'ember-data'; -export default DS.Model.extend({ - filling: DS.belongsTo('protein'), - toppings: DS.hasMany('topping'), - name: DS.attr('string'), - price: DS.attr('number'), - misc: DS.attr() -}); -``` diff --git a/blueprints/model/files/__root__/__path__/__name__.js b/blueprints/model/files/__root__/__path__/__name__.js deleted file mode 100644 index c1b8b8fac4..0000000000 --- a/blueprints/model/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,5 +0,0 @@ -import DS from 'ember-data'; - -export default DS.Model.extend({ - <%= attrs %> -}); diff --git a/blueprints/model/index.js b/blueprints/model/index.js deleted file mode 100644 index d2dd0e3d99..0000000000 --- a/blueprints/model/index.js +++ /dev/null @@ -1,63 +0,0 @@ -/*jshint node:true*/ - -var inflection = require('inflection'); -var stringUtils = require('ember-cli-string-utils'); -var EOL = require('os').EOL; - -module.exports = { - description: 'Generates an ember-data model.', - - anonymousOptions: [ - 'name', - 'attr:type' - ], - - locals: function(options) { - var attrs = []; - var needs = []; - var entityOptions = options.entity.options; - - for (var name in entityOptions) { - var type = entityOptions[name] || ''; - var dasherizedName = stringUtils.dasherize(name); - var dasherizedNameSingular = inflection.singularize(dasherizedName); - var camelizedName = stringUtils.camelize(name); - var dasherizedType = stringUtils.dasherize(type); - - if (/has-many/.test(dasherizedType)) { - var camelizedNamePlural = inflection.pluralize(camelizedName); - attrs.push(camelizedNamePlural + ': ' + dsAttr(dasherizedName, dasherizedType)); - } else { - attrs.push(camelizedName + ': ' + dsAttr(dasherizedName, dasherizedType)); - } - - if (/has-many|belongs-to/.test(dasherizedType)) { - needs.push("'model:" + dasherizedNameSingular + "'"); - } - } - - attrs = attrs.join(',' + EOL + ' '); - needs = ' needs: [' + needs.join(', ') + ']'; - - return { - attrs: attrs, - needs: needs - }; - } -}; - -function dsAttr(name, type) { - switch (type) { - case 'belongs-to': - return 'DS.belongsTo(\'' + name + '\')'; - case 'has-many': - var singularizedName = inflection.singularize(name); - return 'DS.hasMany(\'' + singularizedName + '\')'; - case '': - //"If you don't specify the type of the attribute, it will be whatever was provided by the server" - //http://emberjs.com/guides/models/defining-models/ - return 'DS.attr()'; - default: - return 'DS.attr(\'' + type + '\')'; - } -} diff --git a/blueprints/resource/index.js b/blueprints/resource/index.js deleted file mode 100644 index c7b4264769..0000000000 --- a/blueprints/resource/index.js +++ /dev/null @@ -1,67 +0,0 @@ -/*jshint node:true*/ - -var Blueprint = require('../../lib/models/blueprint'); -var Promise = require('../../lib/ext/promise'); -var merge = require('lodash/object/merge'); -var inflection = require('inflection'); - -module.exports = { - description: 'Generates a model and route.', - - install: function(options) { - return this._process('install', options); - }, - - uninstall: function(options) { - return this._process('uninstall', options); - }, - - _processBlueprint: function(type, name, options) { - var mainBlueprint = Blueprint.lookup(name, { - ui: this.ui, - analytics: this.analytics, - project: this.project - }); - - return Promise.resolve() - .then(function() { - return mainBlueprint[type](options); - }) - .then(function() { - var testBlueprint = mainBlueprint.lookupBlueprint(name + '-test', { - ui: this.ui, - analytics: this.analytics, - project: this.project, - ignoreMissing: true - }); - - if (!testBlueprint) { return; } - - if (testBlueprint.locals === Blueprint.prototype.locals) { - testBlueprint.locals = function(options) { - return mainBlueprint.locals(options); - }; - } - - return testBlueprint[type](options); - }); - }, - - _process: function(type, options) { - var entityName = options.entity.name; - - var modelOptions = merge({}, options, { - entity: { - name: entityName ? inflection.singularize(entityName) : '' - } - }); - - var routeOptions = merge({}, options); - - var self = this; - return this._processBlueprint(type, 'model', modelOptions) - .then(function() { - return self._processBlueprint(type, 'route', routeOptions); - }); - } -}; diff --git a/blueprints/route-addon/files/__root__/__path__/__name__.js b/blueprints/route-addon/files/__root__/__path__/__name__.js deleted file mode 100644 index d921567455..0000000000 --- a/blueprints/route-addon/files/__root__/__path__/__name__.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '<%= routeModulePath %>'; diff --git a/blueprints/route-addon/files/__root__/__templatepath__/__templatename__.js b/blueprints/route-addon/files/__root__/__templatepath__/__templatename__.js deleted file mode 100644 index 2701b13545..0000000000 --- a/blueprints/route-addon/files/__root__/__templatepath__/__templatename__.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '<%= templateModulePath %>'; diff --git a/blueprints/route-addon/index.js b/blueprints/route-addon/index.js deleted file mode 100644 index cc2d44b93b..0000000000 --- a/blueprints/route-addon/index.js +++ /dev/null @@ -1,68 +0,0 @@ -/*jshint node:true*/ - -var stringUtil = require('ember-cli-string-utils'); -var path = require('path'); -var inflector = require('inflection'); - -module.exports = { - description: 'Generates import wrappers for a route and its template.', - - fileMapTokens: function() { - return { - __templatepath__: function(options) { - if (options.pod) { - return path.join(options.podPath, options.dasherizedModuleName); - } - return 'templates'; - }, - __templatename__: function(options) { - if (options.pod) { - return 'template'; - } - return options.dasherizedModuleName; - }, - __name__: function (options) { - if (options.pod) { - return 'route'; - } - - return options.dasherizedModuleName; - }, - __path__: function(options) { - var blueprintName = options.originBlueprintName; - - if (options.pod && options.hasPathToken) { - return path.join(options.podPath, options.dasherizedModuleName); - } - - return inflector.pluralize(blueprintName); - }, - __root__: function(options) { - if (options.inRepoAddon) { - return path.join('lib', options.inRepoAddon, 'app'); - } - - return 'app'; - } - }; - }, - - locals: function (options) { - var locals = {}; - var addonRawName = options.inRepoAddon ? options.inRepoAddon : options.project.name(); - var addonName = stringUtil.dasherize(addonRawName); - var fileName = stringUtil.dasherize(options.entity.name); - - ['route', 'template'].forEach(function (blueprint) { - var pathName = [addonName, inflector.pluralize(blueprint), fileName].join('/'); - - if (options.pod) { - pathName = [addonName, fileName, blueprint].join('/'); - } - - locals[blueprint + 'ModulePath'] = pathName; - }); - - return locals; - } -}; diff --git a/blueprints/route-test/files/tests/unit/__path__/__test__.js b/blueprints/route-test/files/tests/unit/__path__/__test__.js deleted file mode 100644 index 4099e6fa2a..0000000000 --- a/blueprints/route-test/files/tests/unit/__path__/__test__.js +++ /dev/null @@ -1,11 +0,0 @@ -import { moduleFor, test } from 'ember-qunit'; - -moduleFor('route:<%= dasherizedModuleName %>', '<%= friendlyTestDescription %>', { - // Specify the other units that are required for this test. - // needs: ['controller:foo'] -}); - -test('it exists', function(assert) { - var route = this.subject(); - assert.ok(route); -}); diff --git a/blueprints/route-test/index.js b/blueprints/route-test/index.js deleted file mode 100644 index 79258af5df..0000000000 --- a/blueprints/route-test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/*jshint node:true*/ - -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a route unit test.', - locals: function(options) { - return { - friendlyTestDescription: testInfo.description(options.entity.name, "Unit", "Route") - }; - }, -}; diff --git a/blueprints/route/files/__root__/__path__/__name__.js b/blueprints/route/files/__root__/__path__/__name__.js deleted file mode 100644 index 26d9f3124e..0000000000 --- a/blueprints/route/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,4 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Route.extend({ -}); diff --git a/blueprints/route/files/__root__/__templatepath__/__templatename__.hbs b/blueprints/route/files/__root__/__templatepath__/__templatename__.hbs deleted file mode 100644 index c24cd68950..0000000000 --- a/blueprints/route/files/__root__/__templatepath__/__templatename__.hbs +++ /dev/null @@ -1 +0,0 @@ -{{outlet}} diff --git a/blueprints/route/index.js b/blueprints/route/index.js deleted file mode 100644 index 9d89e73340..0000000000 --- a/blueprints/route/index.js +++ /dev/null @@ -1,118 +0,0 @@ -/*jshint node:true*/ - -var fs = require('fs-extra'); -var path = require('path'); -var chalk = require('chalk'); -var EmberRouterGenerator = require('ember-router-generator'); - -module.exports = { - description: 'Generates a route and registers it with the router.', - - availableOptions: [ - { - name: 'path', - type: String, - default: '' - }, - { - name: 'skip-router', - type: Boolean, - default: false - } - ], - - fileMapTokens: function() { - return { - __templatepath__: function(options) { - if (options.pod) { - return path.join(options.podPath, options.dasherizedModuleName); - } - return 'templates'; - }, - __templatename__: function(options) { - if (options.pod) { - return 'template'; - } - return options.dasherizedModuleName; - }, - __root__: function(options) { - if (options.inRepoAddon) { - return path.join('lib', options.inRepoAddon, 'addon'); - } - - if (options.inDummy) { - return path.join('tests','dummy','app'); - } - - if (options.inAddon) { - return 'addon'; - } - - return 'app'; - } - }; - }, - - shouldEntityTouchRouter: function(name) { - var isIndex = name === 'index'; - var isBasic = name === 'basic'; - var isApplication = name === 'application'; - - return !isBasic && !isIndex && !isApplication; - }, - - shouldTouchRouter: function(name, options) { - var entityTouchesRouter = this.shouldEntityTouchRouter(name); - var isDummy = !!options.dummy; - var isAddon = !!options.project.isEmberCLIAddon(); - var isAddonDummyOrApp = (isDummy === isAddon); - - return (entityTouchesRouter && isAddonDummyOrApp && !options.dryRun && !options.inRepoAddon && !options.skipRouter); - }, - - afterInstall: function(options) { - updateRouter.call(this, 'add', options); - }, - - afterUninstall: function(options) { - updateRouter.call(this, 'remove', options); - } -}; - -function updateRouter(action, options) { - var entity = options.entity; - var actionColorMap = { - add: 'green', - remove: 'red' - }; - var color = actionColorMap[action] || 'gray'; - - if (this.shouldTouchRouter(entity.name, options)) { - writeRoute(action, entity.name, options); - - this.ui.writeLine('updating router'); - this._writeStatusToUI(chalk[color], action + ' route', entity.name); - } -} - -function findRouter(options) { - var routerPathParts = [options.project.root]; - - if (options.dummy && options.project.isEmberCLIAddon()) { - routerPathParts = routerPathParts.concat(['tests', 'dummy', 'app', 'router.js']); - } else { - routerPathParts = routerPathParts.concat(['app', 'router.js']); - } - - return routerPathParts; -} - -function writeRoute(action, name, options) { - var routerPath = path.join.apply(null, findRouter(options)); - var source = fs.readFileSync(routerPath, 'utf-8'); - - var routes = new EmberRouterGenerator(source); - var newRoutes = routes[action](name, options); - - fs.writeFileSync(routerPath, newRoutes.code()); -} diff --git a/blueprints/serializer-test/files/tests/unit/__path__/__test__.js b/blueprints/serializer-test/files/tests/unit/__path__/__test__.js deleted file mode 100644 index 55e0bd031b..0000000000 --- a/blueprints/serializer-test/files/tests/unit/__path__/__test__.js +++ /dev/null @@ -1,15 +0,0 @@ -import { moduleForModel, test } from 'ember-qunit'; - -moduleForModel('<%= dasherizedModuleName %>', '<%= friendlyTestDescription %>', { - // Specify the other units that are required for this test. - needs: ['serializer:<%= dasherizedModuleName %>'] -}); - -// Replace this with your real tests. -test('it serializes records', function(assert) { - var record = this.subject(); - - var serializedRecord = record.serialize(); - - assert.ok(serializedRecord); -}); diff --git a/blueprints/serializer-test/index.js b/blueprints/serializer-test/index.js deleted file mode 100644 index 4da25f30a6..0000000000 --- a/blueprints/serializer-test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/*jshint node:true*/ - -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a serializer unit test.', - locals: function(options) { - return { - friendlyTestDescription: testInfo.description(options.entity.name, "Unit", "Serializer") - }; - }, -}; diff --git a/blueprints/serializer/files/__root__/__path__/__name__.js b/blueprints/serializer/files/__root__/__path__/__name__.js deleted file mode 100644 index d698ab7a75..0000000000 --- a/blueprints/serializer/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,4 +0,0 @@ -import DS from 'ember-data'; - -export default DS.RESTSerializer.extend({ -}); diff --git a/blueprints/serializer/index.js b/blueprints/serializer/index.js deleted file mode 100644 index bd3f41c12c..0000000000 --- a/blueprints/serializer/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates an ember-data serializer.' -}; diff --git a/blueprints/server/files/server/.jshintrc b/blueprints/server/files/server/.jshintrc deleted file mode 100644 index c1f2978bcf..0000000000 --- a/blueprints/server/files/server/.jshintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "node": true -} diff --git a/blueprints/server/files/server/index.js b/blueprints/server/files/server/index.js index d1b91be002..24e9a440ff 100644 --- a/blueprints/server/files/server/index.js +++ b/blueprints/server/files/server/index.js @@ -1,3 +1,5 @@ +'use strict'; + // To use it create some files under `mocks/` // e.g. `server/mocks/ember-hamsters.js` // @@ -8,15 +10,14 @@ // }; module.exports = function(app) { - var globSync = require('glob').sync; - var mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); - var proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); + const globSync = require('glob').sync; + const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); + const proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); // Log proxy requests - var morgan = require('morgan'); + const morgan = require('morgan'); app.use(morgan('dev')); - mocks.forEach(function(route) { route(app); }); - proxies.forEach(function(route) { route(app); }); - + mocks.forEach(route => route(app)); + proxies.forEach(route => route(app)); }; diff --git a/blueprints/server/index.js b/blueprints/server/index.js index 3ff653c350..ce45025499 100644 --- a/blueprints/server/index.js +++ b/blueprints/server/index.js @@ -1,18 +1,25 @@ -/*jshint node:true*/ -var isPackageMissing = require('ember-cli-is-package-missing'); +'use strict'; + +const isPackageMissing = require('ember-cli-is-package-missing'); +const SilentError = require('silent-error'); module.exports = { - description: 'Generates a server directory for mocks and proxies.', + description: '[Classic Only] Generates a server directory for mocks and proxies.', - normalizeEntityName: function() {}, + normalizeEntityName() {}, - afterInstall: function(options) { + beforeInstall() { + if (this.project.isViteProject()) { + throw new SilentError('The server blueprint is not supported in Vite projects.'); + } + }, - var isMorganMissing = isPackageMissing(this, 'morgan'); - var isGlobMissing = isPackageMissing(this, 'glob'); + afterInstall(options) { + let isMorganMissing = isPackageMissing(this, 'morgan'); + let isGlobMissing = isPackageMissing(this, 'glob'); - var areDependenciesMissing = isMorganMissing || isGlobMissing; - var libsToInstall = []; + let areDependenciesMissing = isMorganMissing || isGlobMissing; + let libsToInstall = []; if (isMorganMissing) { libsToInstall.push({ name: 'morgan', target: '^1.3.2' }); @@ -25,6 +32,5 @@ module.exports = { if (!options.dryRun && areDependenciesMissing) { return this.addPackagesToProject(libsToInstall); } - - } + }, }; diff --git a/blueprints/service-test/files/tests/unit/__path__/__test__.js b/blueprints/service-test/files/tests/unit/__path__/__test__.js deleted file mode 100644 index ad0b0c97bf..0000000000 --- a/blueprints/service-test/files/tests/unit/__path__/__test__.js +++ /dev/null @@ -1,12 +0,0 @@ -import { moduleFor, test } from 'ember-qunit'; - -moduleFor('service:<%= dasherizedModuleName %>', '<%= friendlyTestDescription %>', { - // Specify the other units that are required for this test. - // needs: ['service:foo'] -}); - -// Replace this with your real tests. -test('it exists', function(assert) { - var service = this.subject(); - assert.ok(service); -}); diff --git a/blueprints/service-test/index.js b/blueprints/service-test/index.js deleted file mode 100644 index 0e9b734920..0000000000 --- a/blueprints/service-test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/*jshint node:true*/ - -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a service unit test.', - locals: function(options) { - return { - friendlyTestDescription: testInfo.description(options.entity.name, "Unit", "Service") - }; - }, -}; diff --git a/blueprints/service/files/__root__/__path__/__name__.js b/blueprints/service/files/__root__/__path__/__name__.js deleted file mode 100644 index b9261c61d8..0000000000 --- a/blueprints/service/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,4 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Service.extend({ -}); diff --git a/blueprints/service/index.js b/blueprints/service/index.js deleted file mode 100644 index 66f5cbbd57..0000000000 --- a/blueprints/service/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a service.' -}; diff --git a/blueprints/template/index.js b/blueprints/template/index.js deleted file mode 100644 index 571f9cc9d8..0000000000 --- a/blueprints/template/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a template.' -}; diff --git a/blueprints/test-helper/files/tests/helpers/__name__.js b/blueprints/test-helper/files/tests/helpers/__name__.js deleted file mode 100644 index b5387d2d3f..0000000000 --- a/blueprints/test-helper/files/tests/helpers/__name__.js +++ /dev/null @@ -1,5 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Test.registerAsyncHelper('<%= camelizedModuleName %>', function(app) { - -}); diff --git a/blueprints/test-helper/index.js b/blueprints/test-helper/index.js deleted file mode 100644 index 54fc263360..0000000000 --- a/blueprints/test-helper/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a test helper.' -}; diff --git a/blueprints/transform-test/files/tests/unit/__path__/__test__.js b/blueprints/transform-test/files/tests/unit/__path__/__test__.js deleted file mode 100644 index 5b62864849..0000000000 --- a/blueprints/transform-test/files/tests/unit/__path__/__test__.js +++ /dev/null @@ -1,12 +0,0 @@ -import { moduleFor, test } from 'ember-qunit'; - -moduleFor('transform:<%= dasherizedModuleName %>', '<%= friendlyTestDescription %>', { - // Specify the other units that are required for this test. - // needs: ['serializer:foo'] -}); - -// Replace this with your real tests. -test('it exists', function(assert) { - var transform = this.subject(); - assert.ok(transform); -}); diff --git a/blueprints/transform-test/index.js b/blueprints/transform-test/index.js deleted file mode 100644 index 59b7e9e873..0000000000 --- a/blueprints/transform-test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/*jshint node:true*/ - -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a transform unit test.', - locals: function(options) { - return { - friendlyTestDescription: testInfo.description(options.entity.name, "Unit", "Transform") - }; - }, -}; diff --git a/blueprints/transform/files/__root__/__path__/__name__.js b/blueprints/transform/files/__root__/__path__/__name__.js deleted file mode 100644 index 3fb7bbf57e..0000000000 --- a/blueprints/transform/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,11 +0,0 @@ -import DS from 'ember-data'; - -export default DS.Transform.extend({ - deserialize: function(serialized) { - return serialized; - }, - - serialize: function(deserialized) { - return deserialized; - } -}); diff --git a/blueprints/transform/index.js b/blueprints/transform/index.js deleted file mode 100644 index a258ff364c..0000000000 --- a/blueprints/transform/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates an ember-data value transform.' -}; diff --git a/blueprints/util-test/files/tests/unit/utils/__name__-test.js b/blueprints/util-test/files/tests/unit/utils/__name__-test.js deleted file mode 100644 index 0df8f6bf35..0000000000 --- a/blueprints/util-test/files/tests/unit/utils/__name__-test.js +++ /dev/null @@ -1,10 +0,0 @@ -import <%= camelizedModuleName %> from '../../../utils/<%= dasherizedModuleName %>'; -import { module, test } from 'qunit'; - -module('<%= friendlyTestName %>'); - -// Replace this with your real tests. -test('it works', function(assert) { - var result = <%= camelizedModuleName %>(); - assert.ok(result); -}); diff --git a/blueprints/util-test/index.js b/blueprints/util-test/index.js deleted file mode 100644 index d023d6673c..0000000000 --- a/blueprints/util-test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/*jshint node:true*/ - -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a util unit test.', - locals: function(options) { - return { - friendlyTestName: testInfo.name(options.entity.name, "Unit", "Utility") - }; - } -}; diff --git a/blueprints/util/files/__root__/utils/__name__.js b/blueprints/util/files/__root__/utils/__name__.js deleted file mode 100644 index ab636c1792..0000000000 --- a/blueprints/util/files/__root__/utils/__name__.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function <%= camelizedModuleName %>() { - return true; -} diff --git a/blueprints/util/index.js b/blueprints/util/index.js deleted file mode 100644 index eec280cac0..0000000000 --- a/blueprints/util/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a simple utility module/function.' -}; diff --git a/blueprints/view-test/files/tests/unit/__path__/__test__.js b/blueprints/view-test/files/tests/unit/__path__/__test__.js deleted file mode 100644 index c409e87a87..0000000000 --- a/blueprints/view-test/files/tests/unit/__path__/__test__.js +++ /dev/null @@ -1,9 +0,0 @@ -import { moduleFor, test } from 'ember-qunit'; - -moduleFor('view:<%= dasherizedModuleName %>', '<%= friendlyTestDescription %>'); - -// Replace this with your real tests. -test('it exists', function(assert) { - var view = this.subject(); - assert.ok(view); -}); diff --git a/blueprints/view-test/index.js b/blueprints/view-test/index.js deleted file mode 100644 index 4383990dae..0000000000 --- a/blueprints/view-test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/*jshint node:true*/ - -var testInfo = require('ember-cli-test-info'); - -module.exports = { - description: 'Generates a view unit test.', - locals: function(options) { - return { - friendlyTestDescription: testInfo.description(options.entity.name, "Unit", "View") - }; - }, -}; diff --git a/blueprints/view/files/__root__/__path__/__name__.js b/blueprints/view/files/__root__/__path__/__name__.js deleted file mode 100644 index 3885ee049a..0000000000 --- a/blueprints/view/files/__root__/__path__/__name__.js +++ /dev/null @@ -1,4 +0,0 @@ -import Ember from 'ember'; - -export default Ember.View.extend({ -}); diff --git a/blueprints/view/index.js b/blueprints/view/index.js deleted file mode 100644 index f4ccbc18f7..0000000000 --- a/blueprints/view/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/*jshint node:true*/ - -module.exports = { - description: 'Generates a view subclass.' -}; diff --git a/dev/linux/Dockerfile b/dev/linux/Dockerfile index b9fdecfa65..f86aa39cfa 100644 --- a/dev/linux/Dockerfile +++ b/dev/linux/Dockerfile @@ -1,11 +1,11 @@ FROM node:latest RUN npm i -g npm -RUN npm i -g phantomjs +RUN npm i -g phantomjs-prebuilt RUN apt-get update && apt-get install -y git RUN git clone https://github.com/ember-cli/ember-cli.git ~/ember-cli RUN cd ~/ember-cli && npm i -ENTRYPOINT ["/bin/bash"] \ No newline at end of file +ENTRYPOINT ["/bin/bash"] diff --git a/dev/linux/README.md b/dev/linux/README.md index 8f8b9af76f..7b34a05a2c 100644 --- a/dev/linux/README.md +++ b/dev/linux/README.md @@ -1,4 +1,5 @@ ##### Instructions + 1. Clone `ember-cli` into a folder under `C:/Users` in Windows or `/Users` in Mac 1. Install Docker on [Windows](https://github.com/boot2docker/windows-installer/releases/latest) or [Mac](https://github.com/boot2docker/osx-installer/releases/latest) 1. Start Boot2Docker @@ -11,4 +12,5 @@ 1. When you're done, run `exit` ##### Cleanup -1. Run `docker rmi ember-cli` \ No newline at end of file + +1. Run `docker rmi ember-cli` diff --git a/dev/online-editors/README.md b/dev/online-editors/README.md new file mode 100644 index 0000000000..fadf08348b --- /dev/null +++ b/dev/online-editors/README.md @@ -0,0 +1,23 @@ +# Online Editor output: + +repo: https://github.com/ember-cli/editor-output + +For each supported online editor, there should be a folder +with the online-editor-specific files for that online editor. + +For example: + +``` +stackblitz/ + .stackblitzrc +other-editor/ + .other-editor.js +``` + +This would result in the following branches on the +editor-output repo: + +- stackblitz-app-output +- stackblitz-addon-output +- other-editor-app-output +- other-editor-addon-output diff --git a/dev/online-editors/stackblitz/.stackblitzrc b/dev/online-editors/stackblitz/.stackblitzrc new file mode 100644 index 0000000000..aad9530995 --- /dev/null +++ b/dev/online-editors/stackblitz/.stackblitzrc @@ -0,0 +1,6 @@ +{ + "startCommand": "npm start", + "env": { + "NODE_OPTIONS": "--trace-warnings --unhandled-rejections=strict" + } +} diff --git a/dev/output-repo-helpers.js b/dev/output-repo-helpers.js new file mode 100644 index 0000000000..abd621ca5d --- /dev/null +++ b/dev/output-repo-helpers.js @@ -0,0 +1,82 @@ +'use strict'; + +const tmp = require('tmp'); +const path = require('path'); +const { execa, execaCommand } = require('execa'); + +async function clearRepo(repoPath) { + console.log(`clearing repo content in ${repoPath}`); + await execa(`git`, [`rm`, `-rf`, `.`], { + cwd: repoPath, + }); +} + +async function cloneBranch(containingPath, { repo, branch }) { + let outputName = 'output-repo'; + let outputRepoPath = path.join(containingPath, outputName); + + console.log(`cloning ${repo} in to ${containingPath}`); + + try { + await execaCommand(`git clone ${repo} --branch=${branch} ${outputName}`, { cwd: containingPath }); + } catch (e) { + console.log(`Branch does not exist -- creating fresh (local) repo.`); + + await execaCommand(`git clone ${repo} ${outputName}`, { cwd: containingPath }); + await execaCommand(`git switch -C ${branch}`, { cwd: outputRepoPath }); + } + + return outputRepoPath; +} + +let cliOutputCache = {}; +/** + * We can re-use generated projects + */ +async function generateOutputFiles({ name, variant, isTypeScript, version, command }) { + console.log(Object.keys(cliOutputCache)); + let cacheKey = `${command}-${variant}`; + + if (cliOutputCache[cacheKey]) { + return cliOutputCache[cacheKey]; + } + + let updatedOutputTmpDir = tmp.dirSync(); + console.log(`Running npx ember-cli@${version} ${command} ${name}`); + + await execa( + 'npx', + [`ember-cli@${version}`, command, name, `--skip-npm`, `--skip-git`, ...(isTypeScript ? ['--typescript'] : [])], + { + cwd: updatedOutputTmpDir.name, + env: { + /** + * using --typescript triggers npm's peer resolution features, + * and since we don't know if the npm package has been released yet, + * (and therefor) generate the project using the local ember-cli, + * the ember-cli version may not exist yet. + * + * We need to tell npm to ignore peers and just "let things be". + * Especially since we don't actually care about npm running, + * and just want the typescript files to generate. + * + * See this related issue: https://github.com/ember-cli/ember-cli/issues/10045 + */ + // eslint-disable-next-line camelcase + npm_config_legacy_peer_deps: 'true', + }, + } + ); + + // node_modules is .gitignored, but since we already need to remove package-lock.json due to #10045, + // we may as well remove node_modules as while we're at it, just in case. + await execa('rm', ['-rf', 'node_modules', 'package-lock.json'], { cwd: updatedOutputTmpDir.name }); + + let generatedOutputPath = path.join(updatedOutputTmpDir.name, name); + + cliOutputCache[cacheKey] = generatedOutputPath; + + return generatedOutputPath; +} + +module.exports = { cloneBranch, clearRepo, generateOutputFiles }; diff --git a/dev/update-blueprint-dependencies.js b/dev/update-blueprint-dependencies.js new file mode 100644 index 0000000000..be39ae46bd --- /dev/null +++ b/dev/update-blueprint-dependencies.js @@ -0,0 +1,208 @@ +'use strict'; + +function usage() { + console.log(`This script updates the dependencies / devDependencies in the main addon and app blueprints along with their corresponding test fixtures. + +Options: + + - '--ember-source' (required) - The dist-tag to use for ember-source + - '--ember-data' (required) - The dist-tag to use for ember-data + - '--filter' (optional) - A RegExp to filter the packages to update by + - '--latest' (optional) - Always use the latest version available for a package (includes major bumps, 'false' by default) + +Example: + +node dev/update-blueprint-dependencies.js --ember-source=beta --ember-data=beta + +node dev/update-blueprint-dependencies.js --filter /eslint/ + +node dev/update-blueprint-dependencies.js --filter some-package@beta +`); +} + +const fs = require('fs'); +const path = require('path'); +const util = require('util'); +const nopt = require('nopt'); +const npmPackageArg = require('npm-package-arg'); +const { default: _latestVersion } = require('latest-version'); + +nopt.typeDefs['regexp'] = { + type: RegExp, + validate(data, key, value) { + let regexp = new RegExp(value); + + data[key] = regexp; + }, +}; + +const OPTIONS = nopt({ + 'ember-source': String, + 'ember-data': String, + filter: String, + latest: Boolean, +}); + +const PACKAGE_FILES = [ + '../packages/app-blueprint/files/package.json', + '../packages/addon-blueprint/additional-package.json', + '../tests/fixtures/app/defaults/package.json', + '../tests/fixtures/app/npm/package.json', + '../tests/fixtures/app/yarn/package.json', + '../tests/fixtures/app/pnpm/package.json', + '../tests/fixtures/app/no-ember-data/package.json', + '../tests/fixtures/app/embroider/package.json', + '../tests/fixtures/app/embroider-yarn/package.json', + '../tests/fixtures/app/embroider-pnpm/package.json', + '../tests/fixtures/app/embroider-no-welcome/package.json', + '../tests/fixtures/app/embroider-no-ember-data/package.json', + '../tests/fixtures/app/typescript/package.json', + '../tests/fixtures/app/typescript-no-ember-data/package.json', + '../tests/fixtures/app/typescript-embroider/package.json', + '../tests/fixtures/app/typescript-embroider-no-ember-data/package.json', + '../tests/fixtures/addon/defaults/package.json', + '../tests/fixtures/addon/yarn/package.json', + '../tests/fixtures/addon/pnpm/package.json', + '../tests/fixtures/addon/typescript/package.json', +]; + +let filter = { + nameRegexp: null, + name: null, +}; + +if (OPTIONS.filter) { + if (OPTIONS.filter.startsWith('/')) { + filter.nameRegexp = new RegExp(OPTIONS.filter.substring(1, OPTIONS.filter.length - 1)); + // can only use latest when using a regexp style + filter.fetchSpec = 'latest'; + } else { + let packageArgResult = npmPackageArg(OPTIONS.filter); + filter.name = packageArgResult.name; + OPTIONS[packageArgResult.name] = filter.fetchSpec = packageArgResult.fetchSpec; + } +} + +function shouldCheckDependency(dependency) { + if (filter.nameRegexp) { + return filter.nameRegexp.test(dependency); + } + + if (filter.name) { + return dependency === filter.name; + } + + return true; +} + +const LATEST = new Map(); +async function latestVersion(packageName, semverRange) { + let result = LATEST.get(packageName); + + if (result === undefined) { + let options = { + version: semverRange, + }; + + if (OPTIONS[packageName]) { + options.version = OPTIONS[packageName]; + } + + /** + * We need any @ember-data/* or @warp-drive/* packages to match the same version as ember-data in the + * package.json files. + */ + if (packageName.startsWith('@ember-data/') || packageName.startsWith('@warp-drive/')) { + options.version = OPTIONS['ember-data']; + } + + result = _latestVersion(packageName, options); + LATEST.set(packageName, result); + } + + return result; +} + +async function updateDependencies(dependencies) { + for (let dependencyKey in dependencies) { + let dependencyName = removeTemplateExpression(dependencyKey); + + if (!shouldCheckDependency(dependencyName)) { + continue; + } + + let previousValue = dependencies[dependencyKey]; + + // grab the first char (~ or ^) + let prefix = previousValue[0]; + let isValidPrefix = prefix === '~' || prefix === '^'; + + // handle things from blueprints/app/files/package.json like `^2.4.0<% if (welcome) { %>` + let templateSuffix = previousValue.includes('<') ? previousValue.slice(previousValue.indexOf('<')) : ''; + + // check if we are dealing with `~<%= emberCLIVersion %>` + let hasVersion = previousValue[1] !== '<'; + + if (hasVersion && isValidPrefix) { + const semverRange = OPTIONS.latest ? 'latest' : removeTemplateExpression(previousValue); + try { + const newVersion = await latestVersion(dependencyName, semverRange); + + dependencies[dependencyKey] = `${prefix}${newVersion}${templateSuffix}`; + } catch (err) { + console.warn(`Error checking new version of ${dependencyName}: ${err.message}`); + } + } + } +} + +function removeTemplateExpression(dependency) { + if (dependency.includes('<') === false) { + return dependency; + } + + let semverRange = dependency.replace( + dependency.substring(dependency.indexOf('<'), dependency.lastIndexOf('>') + 1), + '' + ); + + return semverRange; +} + +async function main() { + for (let packageFile of PACKAGE_FILES) { + let filePath = path.join(__dirname, packageFile); + let pkg = JSON.parse(fs.readFileSync(filePath, { encoding: 'utf8' })); + + await updateDependencies(pkg.dependencies); + await updateDependencies(pkg.devDependencies); + + let output = `${JSON.stringify(pkg, null, 2)}\n`; + + fs.writeFileSync(filePath, output, { encoding: 'utf8' }); + } +} + +if (module === require.main) { + // ensure promise rejection is a failure + process.on('unhandledRejection', (error) => { + if (!(error instanceof Error)) { + error = new Error(`Promise rejected with value: ${util.inspect(error)}`); + } + + console.error(error.stack); + + // eslint-disable-next-line n/no-process-exit + process.exit(1); + }); + + if (OPTIONS.filter || (OPTIONS['ember-source'] && OPTIONS['ember-data'])) { + main(); + } else { + usage(); + process.exitCode = 1; + return; + } +} + +module.exports = { PACKAGE_FILES, updateDependencies }; diff --git a/dev/update-editor-output-repos.js b/dev/update-editor-output-repos.js new file mode 100644 index 0000000000..d91ba34537 --- /dev/null +++ b/dev/update-editor-output-repos.js @@ -0,0 +1,149 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs-extra'); +const path = require('path'); +const { execa } = require('execa'); +const tmp = require('tmp'); +const { default: latestVersion } = require('latest-version'); +const { cloneBranch, clearRepo, generateOutputFiles } = require('./output-repo-helpers'); + +tmp.setGracefulCleanup(); + +const ONLINE_EDITOR_FILES = path.join(__dirname, 'online-editors'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const VARIANT = process.env.VARIANT; +const VALID_VARIANT = ['javascript', 'typescript']; +const EDITORS = ['stackblitz']; +const REPO = 'ember-cli/editor-output'; +const [, , tag] = process.argv; + +assert(GITHUB_TOKEN, 'GITHUB_TOKEN must be set'); +assert( + VALID_VARIANT.includes(VARIANT), + `Invalid VARIANT env var specified: ${VARIANT}. Must be one of ${VALID_VARIANT}` +); + +assert(tag, 'a tag must be provided as the first argument to this script.'); + +/** + * The editor output repos differ from the output repos in that + * the editor output repos use branches for their convention of differentiating between + * editors and tags/versions/etc. + * + * The convention is (for the branch names): + * - {onlineEditor}-{projectType}-output{-VARIANT?}{-tag?} + * + * Examples: + * stackblitz-addon-output-typescript + * stackblitz-app-output-typescript-v4.10.0 + * codesandbox-app-output-v4.10.0 + * + * For every tag, we generate + * - 2 variants (js and ts) + * * 2 project types (app and addon) + * * # of supported editors with custom configurations + * (4 at the time of writing) + */ + +/** + * Returns an array of objects containing config for operations to attempt. + * This allows for reduced nesting / conditionals when working with the file system and git + * + * This also allows for easier debugging, reproducibility, testing (if we ever add that), etc + */ +async function determineOutputs(tag) { + let version = tag.replace(/^v/, '').replace(/-ember-cli$/, ''); + let latestEC = await latestVersion('ember-cli'); + let isLatest = version === latestEC; + let repo = `https://github-actions:${GITHUB_TOKEN}@github.com/${REPO}.git`; + + let result = []; + + for (let command of ['new', 'addon']) { + let isTypeScript = VARIANT === 'typescript'; + let branchSuffix = isTypeScript ? '-typescript' : ''; + + /** + * If we're working with the latest tag, we want to update the default + * branch for an editor as well as the tagged version. + */ + let getBranches = (onlineEditor, projectType) => { + let editorBranch = `${onlineEditor}-${projectType}-output${branchSuffix}`; + + if (isLatest) { + return [editorBranch, `${editorBranch}-v${version}`]; + } + + return [`${editorBranch}-v${version}`]; + }; + + let name = command === 'new' ? 'my-app' : 'my-addon'; + let projectType = command === 'new' ? 'app' : 'addon'; + + for (let onlineEditor of EDITORS) { + let branches = getBranches(onlineEditor, projectType); + + for (let editorBranch of branches) { + result.push({ + variant: VARIANT, + isLatest, + isTypeScript, + tag, + version, + command, + name, + projectType, + repo, + onlineEditor, + branch: editorBranch, + }); + } + } + } + + return result; +} + +async function push(repoPath, { branch }) { + console.log('pushing commit'); + + try { + await execa('git', ['push', '--force', 'origin', branch], { cwd: repoPath }); + } catch (e) { + // branch may not exist yet + await execa('git', ['push', '-u', 'origin', branch], { cwd: repoPath }); + } +} + +async function updateOnlineEditorRepos(tag) { + let infos = await determineOutputs(tag); + + console.log(`Updating online editor repo :: ${infos.length} branches`); + + for (let info of infos) { + let generatedOutputPath = await generateOutputFiles(info); + + let tmpdir = tmp.dirSync(); + await fs.mkdirp(tmpdir.name); + + let outputRepoPath = await cloneBranch(tmpdir.name, info); + + await clearRepo(outputRepoPath); + + console.log('copying generated contents to output repo'); + await fs.copy(generatedOutputPath, outputRepoPath); + + console.log('copying online editor files'); + await fs.copy(path.join(ONLINE_EDITOR_FILES, info.onlineEditor), outputRepoPath); + + console.log('commiting updates'); + await execa('git', ['add', '--all'], { cwd: outputRepoPath }); + await execa('git', ['commit', '-m', info.tag], { cwd: outputRepoPath }); + + await push(outputRepoPath, info); + } +} + +updateOnlineEditorRepos(tag); diff --git a/dev/update-output-repos.js b/dev/update-output-repos.js new file mode 100644 index 0000000000..7c53920f5e --- /dev/null +++ b/dev/update-output-repos.js @@ -0,0 +1,76 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs-extra'); +const { execa } = require('execa'); +const tmp = require('tmp'); +const { default: latestVersion } = require('latest-version'); +const { cloneBranch, clearRepo, generateOutputFiles } = require('./output-repo-helpers'); +tmp.setGracefulCleanup(); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const BLUEPRINT = process.env.BLUEPRINT; +const APP_REPO = 'ember-cli/ember-new-output'; +const ADDON_REPO = 'ember-cli/ember-addon-output'; +const [, , tag] = process.argv; + +assert(GITHUB_TOKEN, 'GITHUB_TOKEN must be set'); +assert(tag, 'a tag must be provided as the first argument to this script.'); +assert(BLUEPRINT === 'app' || BLUEPRINT === 'addon', 'BLUEPRINT must be set to either `app` or `addon`'); + +async function updateRepo(tag) { + let repoName = APP_REPO; + let command = 'new'; + let name = 'my-app'; + + if (BLUEPRINT === 'addon') { + repoName = ADDON_REPO; + command = 'addon'; + name = 'my-addon'; + } + + let version = tag.replace(/^v/, '').replace(/-ember-cli$/, ''); + let latestEC = await latestVersion('ember-cli'); + let latestECBeta = await latestVersion('ember-cli', { version: 'beta' }); + + let isLatest = version === latestEC; + let isLatestBeta = version === latestECBeta; + + let isStable = !tag.includes('-beta'); + + let outputRepoBranch = isStable ? 'stable' : 'master'; + let shouldUpdateMasterFromStable = tag.endsWith('-beta.1'); + let branchToClone = shouldUpdateMasterFromStable ? 'stable' : outputRepoBranch; + let tmpdir = tmp.dirSync(); + + let outputRepoPath = await cloneBranch(tmpdir.name, { + repo: `https://github-actions:${GITHUB_TOKEN}@github.com/${repoName}.git`, + branch: branchToClone, + }); + + await clearRepo(outputRepoPath); + + let generatedOutputPath = await generateOutputFiles({ version, name, command, variant: 'javascript' }); + + console.log('copying generated contents to output repo'); + await fs.copy(generatedOutputPath, outputRepoPath); + + if (shouldUpdateMasterFromStable) { + await execa('git', ['checkout', '-B', 'master'], { cwd: outputRepoPath }); + } + + console.log('commiting updates'); + await execa('git', ['add', '--all'], { cwd: outputRepoPath }); + await execa('git', ['commit', '-m', tag], { cwd: outputRepoPath }); + await execa('git', ['tag', `-f`, `v${version}`], { cwd: outputRepoPath }); + + console.log('pushing commit & tag'); + await execa('git', ['push', 'origin', `v${version}`, '--force'], { cwd: outputRepoPath }); + + // Only push this branch if we are using an up-to-date tag + if ((isStable && isLatest) || (!isStable && isLatestBeta)) { + await execa('git', ['push', '--force', 'origin', outputRepoBranch], { cwd: outputRepoPath }); + } +} + +updateRepo(tag); diff --git a/dev/windows/README.md b/dev/windows/README.md index 0afbba5b81..54251c6aac 100644 --- a/dev/windows/README.md +++ b/dev/windows/README.md @@ -1,4 +1,5 @@ ##### Instructions + 1. Install VirtualBox 1. Install Vagrant 1. Run `vagrant up` in this directory @@ -9,4 +10,5 @@ 1. When you're done, close the VM ##### Cleanup -1. Run `vagrant destroy -f` \ No newline at end of file + +1. Run `vagrant destroy -f` diff --git a/docs/analytics.md b/docs/analytics.md new file mode 100644 index 0000000000..6a86bffce8 --- /dev/null +++ b/docs/analytics.md @@ -0,0 +1,3 @@ +# Analytics + +Ember CLI no longer includes any telemetry. diff --git a/ARCHITECTURE.md b/docs/architecture.md similarity index 80% rename from ARCHITECTURE.md rename to docs/architecture.md index eaa469e4c1..1a84c773eb 100644 --- a/ARCHITECTURE.md +++ b/docs/architecture.md @@ -1,19 +1,23 @@ ## Architecture -![embercli architecture](./assets/architecture/Ember-CLI architecture.png) +![embercli architecture](./assets/architecture/Ember-CLI%20architecture.png) ## Overview + - **cli** parses args and calls the respective **command** - **command** calls a sequence of **tasks** - **tasks** do the actual work ## The different components of ember-cli + ### cli() + cli is a small function that gets everything going. Usage: -``` JavaScript -var cli = require('./cli'); + +```JavaScript +let cli = require('./cli'); cli({ cliArgs: argv, // Required @@ -24,7 +28,8 @@ cli({ ``` ## UI -In ember-cli we pass an `UI` instance around. Instead of calling + +In ember-cli we pass a `UI` instance around. Instead of calling `console.log` or writing things directly to `process.stdout` we access those through this wrapper. This makes our code testing friendly because it lets us simulate user input and it lets us verify if the output @@ -43,20 +48,21 @@ text input. stream. Also nice for testing, e.g. simulating input. ### Commands + Located in `lib/commands/`. They get picked up by `requireAsHash()` automatically. The CLI constructs command instances with dependencies including ui, -analytics, commands, project, etc. The plan is for these to eventually +commands, project, etc. The plan is for these to eventually be constructed and wired up via a dependency injection container. The following file structure is expected (Demonstrated on the imaginary command `develop-ember-cli`): -``` JavaScript +```JavaScript // e.g. commands/develop-ember-cli.js -var Command = require('../models/command'); +let Command = require('../models/command'); module.exports = Command.extend({ name: 'develop-ember-cli', // Optional, default is the filename @@ -85,13 +91,13 @@ module.exports = Command.extend({ ... ], - run: function(options) { // Required + run(options) { // Required // options === { packageName, ... } // Run tasks and return a promise }, - printDetailedHelp: function() { // Optional + printDetailedHelp() { // Optional this.ui.write('Detailed help...'); } }); @@ -108,12 +114,12 @@ The promise returned by `run()` should either - resolve to `undefined` - reject with an `Error` instance if the error is unhandled - or reject with `undefined` if it was handled. In this case the command -should log something via the `ui` first. + should log something via the `ui` first. `requireAsHash()` assembles from the files in `commands/` a hash that looks like this: -``` JavaScript +```JavaScript { DevelopEmberCLI: require('commands/develop-ember-cli'), ... @@ -121,6 +127,7 @@ like this: ``` #### Usage instructions formatting + ``` ember serve --port (Default: 4200) Description 1 @@ -128,6 +135,7 @@ ember serve ``` ##### Formatting colors + - white: `ember serve` - yellow: `` - cyan: `--port`, `--important-option` @@ -136,44 +144,49 @@ ember serve - cyan: `(Required)` ### Tasks + Located in `lib/tasks`. They get picked up by `requireAsHash()` automatically. -Tasks do the real work. They should also do only one thing: For example there -are separate `bower-install` and `npm-install` tasks, not just one unified -`install` task. And they should not call other tasks: For example -`install-blueprint` shouldn't call `npm-install` directly. That's because the -task sequence is determined by the command and thus should be declared there. +Tasks do the real work. They should also do only one thing and they should not +call other tasks: For example `install-blueprint` shouldn't call `npm-install` +directly. That's because the task sequence is determined by the command and thus +should be declared there. The command constructs task instances with dependencies including ui, -analytics, project, etc. The plan is for these to eventually +project, etc. The plan is for these to eventually be constructed and wired up via a dependency injection container. A task's `run` method has to return a promise which resolves or rejects depending on whether it ran through successfully or not. The promise of a task should either + - fulfill to `undefined` - reject with an `Error` instance if the error is unhandled - or reject with `undefined` if it was handled. In this case the task should -log something via the `ui` first. + log something via the `ui` first. So, tasks don't have a return value per design. The file format of a task looks like this: -``` JavaScript -// tasks/npm-install.js -var Task = require('../task'); +```JavaScript +// ./lib/tasks/npm-install.js + +'use strict'; + +const Task = require('../models/task'); -module.exports = Task.extend({ - run: function(options) { +module.exports = class NpmInstallTask extends Task { + run(/* options */) { // return promise } -}); +}; ``` `requireAsHash()` assembles from the files in `tasks/` a hash that looks like this: -``` JavaScript + +```JavaScript { NpmInstall: require('tasks/npm-install'), ... @@ -181,7 +194,7 @@ module.exports = Task.extend({ ``` ## Style guide -- Everything Promise based ( use: lib/ext/promise) + - Everything async (except require) - Short files - Tests, tests, tests @@ -202,22 +215,18 @@ module.exports = Task.extend({ - Okay: `url`, `id`, `rootURL` (property) or `URL`, `URLParser` (class) - Wrong: `Url`,`rootUrl` - We stick with how it's done in ember -> `rootURL` -- No comma separated var statements (`var cool = 123, supercool = 456;`) +- No comma separated assignment statements (`let cool = 123, supercool = 456;`) - Line break at the end of every file - Make constructors take an options object to avoid order-dependence -This list only contains style decisions not already covered by JSHint (e.g. +This list only contains style decisions not already covered by ESLint (e.g. mandatory semicolons and other rules are omitted). ### Indentation -#### Aligned require statements -``` JavaScript -var RSVP = require('rsvp'); -var Promise = RSVP.Promise; -``` #### Multi-line return statement -``` JavaScript + +```JavaScript // Correct return someFunction( someArgument, @@ -231,7 +240,7 @@ return someFunction( ); ``` -``` JavaScript +```JavaScript // Correct return returnsAPromise() .then(...) @@ -243,9 +252,10 @@ return retursAPromise().then(...) ``` ### Custom errors + Custom error classes should end with the suffix "Error". -``` JavaScript +```JavaScript function CustomError() { this.stack = (new Error()).stack; } @@ -256,12 +266,12 @@ CustomError.prototype.name = 'CustomError'; Also a `message` property should be set: Either in the constructor or as a property on `CustomError.prototype`. - ### Dependencies + When requiring modules, we should be aware of their effect on startup time. If they introduce a noticeable penalty, and are not needed except for some task/command we should require them lazily. Obviously a few -small modules wont make a difference, but eagerly requiring npm + bower +small modules won't make a difference, but eagerly requiring npm and all of lodash will add a second to startup time. The following example eagerly requires npm, but only truly requires it @@ -269,16 +279,21 @@ when that task is invoked, not for `ember help` `ember version` or even `ember server`. This introduces a 200ms-300ms startup penalty. ```js -var npm = require('npm'); +// ./lib/tasks/npm-install.js -module.exports = Task.extend({ - run: function() { - npm.install() // or something +"use strict"; + +const npm = require("npm"); +const Task = require("../models/task"); + +module.exports = class NpmInstallTask extends Task { + run() { + npm.install(); // or something } -}); +}; ``` -If a dependency (like bower or npm) turns out to have high startup cost, +If a dependency (like npm) turns out to have high startup cost, we should require them lazily. This also allows us to inject alternative dependencies at construction time. Some future DI refactoring can likely automate this process. @@ -286,18 +301,26 @@ refactoring can likely automate this process. example: ```js -module.exports = Task.extend({ - init: function() { - this.npm = this.npm || require('npm'); - }, - run: function() { - this.npm.install() // or something +// ./lib/tasks/npm-install.js + +"use strict"; + +const Task = require("../models/task"); + +module.exports = class NpmInstallTask extends Task { + init() { + this.npm = this.npm || require("npm"); } -}); + + run() { + this.npm.install(); // or something + } +}; ``` ### Sync vs async -Since [JavaScript uses an event loop](http://nodejs.org/about/), the use of + +Since [JavaScript uses an event loop](https://nodejs.org/about/), the use of blocking and compute intensive operations is discouraged. The general recommendation is to use asynchronous operations. diff --git a/assets/architecture/Ember-CLI architecture.png b/docs/assets/architecture/Ember-CLI architecture.png similarity index 100% rename from assets/architecture/Ember-CLI architecture.png rename to docs/assets/architecture/Ember-CLI architecture.png diff --git a/assets/architecture/Ember-CLI architecture.xml b/docs/assets/architecture/Ember-CLI architecture.xml similarity index 98% rename from assets/architecture/Ember-CLI architecture.xml rename to docs/assets/architecture/Ember-CLI architecture.xml index 5b80ab29b2..8d61c630aa 100644 --- a/assets/architecture/Ember-CLI architecture.xml +++ b/docs/assets/architecture/Ember-CLI architecture.xml @@ -1 +1 @@ -7Vzdj6JIEP9rTO4eZsP3OI8z7t7eyyWXjMndPaK2ShbBA5xx9q/fbqhCqAZEbIE164NK0fbHr35dXVXdODFnu+PXyN1v/wpXzJ8Y2uo4MT9PDOPJtvi7EHxkAnsKgk3krTKRfhK8et8ZCDWQHrwVi0sFkzD0E29fFi7DIGDLpCRbh365ib27wepPgtel68vSf7xVss2kU8M5yf9k3maLzejOU3YnTj6wjhVbuwc/eUhF/J64vXOxrnRU5hcOWBSGvBrxbXecMV+AhoA8Mostprxybb3W3IX2kGHxR9vi+TAiFkBXr60SlPbm+gcYJ9stWMRFE8Nxd/uJ+eLzpl6W4W7nBqtcuBFCCaOEHYV8m+x8LtD514jF3nd3kRbQ+LV7SEIuEZfitut7m4B/99la/PCNRYnH1fYM4iQUbcV7d+kFm7m4+Mx7jA2L0gz4WGYLNIh6OQNZ+lvA6ysLdyyJPvhvoGbz0c7qAKJPgTTvJyLp2My2QCKUucDnTV7zST/8C6iopbqg7YK6ZjDyghbet17CXjlo4vqdz96ySmrBuwKkRxgsYGQ6wKsCSFYFRihTitFUxsgcA0amWSbSoCDpMJ4iStCdYVGytRFRSQc7X0QJRMOiZIwKJZheRZSCUVglHO4oUJKX2k/8NdgaGidR+C13h0RF6zBIwFHDxaufJdahlvFR1pPZ1xKrQ18KepqPYo1FBNDhNtqBhDK1IMmOyHwUhlHH+GIUKD3KKI3CFaHLx7AoyQ7bfBSuiDkqlCAiLaIElmpglJ5GhFK+bhVQAlM1LErWdEwoyc7/HEzVwM4/iSOHRUl2/ufjcGtHhZJslySMlofojYkKBCIsWD1HUfjOL5e+G8fesowYO3rJv8LT/WTD1X/pnbRStpISfedg5D0JD1GqntqMigx1AUq7AkqURcx3E++t3KcqfKGFv0OP9/bkrZAAxcIkKlaRdR1+VczrkYpyV5hGOlhR4kYblkgVcU24YrhYbC8KxA0dxoFjh4F6df3SHdIvSBvXjoPGtVapPP+S9fjE1Vxn3ehrypNcom9Xxgqu854VbolLcQ8GqZ7NGEhl6m5KkLX2oHXkZMsp0Z79Fm2JkrYt+03KMq0d+5UQSE5+3D2BsuRhPYOIMTJQHbdnECYOLmWQTe0nZmv6YJCcGLqcQbU86X1BbcegdNbUM4jkOnpkUFcbZJMu98ogOWXlh+G3w36W7dr99rtEqGgb7hYH3rWXIUJGp5zv03EKFPfUMA9X1C8GUUo9WFPOZd2/BU+H2H7+IZnVzz+jTAUuUTT/6FJwy/kn5/nu3oLXxE01gU2uVuUEsonvx0OGjgSim0U0FrslgeQU6K8gui0DSDbEelLlA94qhiaMtYCxtTExTRKciaHNC8tbxHJa4GzVlicJX7OcA7hBjP6Tp5hq4u1+ZodB2GbT2HhsKSaTeORWOQUk94uePTpXnjoK5emnnr44He/EvwRvudm9TD2IoSj/QBihd6V8vp2Ec6c/j8CSt2guJk1nBtTvz5SddetmYQFZYnBJutxo1R0H6EOFKvI645n3eBaueeI3hwUk7sfdA/X80ck+Qs7Ua1c9m4YXtySQnNb5mQkEMU4zf9JIun7nk/CnIm+kiD/UCemalpBObaFV6IM/95XYwiPLzQRq3tyaEgKhNtQTiNiNnKoXE4hEc3afToiKvNbPZYCyxmr580T4g8pQzh+TbrR39oBIRXbLsE0Jf1qkte7OAKXJivrEqEYYVHEkXZEFqkkDXc4gmkHoMbNutUj93BuDCs8SVp/PIAyqOCyvKLVOE4NdnWiLdLlPJxqHfS8MguMmzQw6s7tH9tzyU2fqGURtUFc3mm4T9ulG2wpSMbfgSSMFsj28oTJwlgE0vTYDR9cevU+9y0cDfz2wBXkcEoi3PYF8kwe27KoTeE761LoAqKQs5/+DeDo/vfGQqeSZF9C1/fF0k39LH28Xj1bPZlzpvIR4oJGrUhSGQzV8CyNrZMGfls9+gBKuO1Fcbtb3AvaAgKQNfxJ9Je2Kx804DJp4okp8ph2YY+Ucn6z+rHDaL3wqP1jE4uMKURE3GFTa0bkbf8tHzLtQKtEsXnlvVHSdXip7qQ6CIUTpaJ/jj2ApCOb6fkesubgE92UPK3S1MFc9+kkMfIUlqfx7hTzDq9aWKMh4VGCmZA+mn6XbwcNM+dFLUkXbpdtxzlTUeenml6c/WcmKn/6ixvzyAw== \ No newline at end of file +7Vzdj6JIEP9rTO4eZsP3OI8z7t7eyyWXjMndPaK2ShbBA5xx9q/fbqhCqAZEbIE164NK0fbHr35dXVXdODFnu+PXyN1v/wpXzJ8Y2uo4MT9PDOPJtvi7EHxkAnsKgk3krTKRfhK8et8ZCDWQHrwVi0sFkzD0E29fFi7DIGDLpCRbh365ib27wepPgtel68vSf7xVss2kU8M5yf9k3maLzejOU3YnTj6wjhVbuwc/eUhF/J64vXOxrnRU5hcOWBSGvBrxbXecMV+AhoA8Mostprxybb3W3IX2kGHxR9vi+TAiFkBXr60SlPbm+gcYJ9stWMRFE8Nxd/uJ+eLzpl6W4W7nBqtcuBFCCaOEHYV8m+x8LtD514jF3nd3kRbQ+LV7SEIuEZfitut7m4B/99la/PCNRYnH1fYM4iQUbcV7d+kFm7m4+Mx7jA2L0gz4WGYLNIh6OQNZ+lvA6ysLdyyJPvhvoGbz0c7qAKJPgTTvJyLp2My2QCKUucDnTV7zST/8C6iopbqg7YK6ZjDyghbet17CXjlo4vqdz96ySmrBuwKkRxgsYGQ6wKsCSFYFRihTitFUxsgcA0amWSbSoCDpMJ4iStCdYVGytRFRSQc7X0QJRMOiZIwKJZheRZSCUVglHO4oUJKX2k/8NdgaGidR+C13h0RF6zBIwFHDxaufJdahlvFR1pPZ1xKrQ18KepqPYo1FBNDhNtqBhDK1IMmOyHwUhlHH+GIUKD3KKI3CFaHLx7AoyQ7bfBSuiDkqlCAiLaIElmpglJ5GhFK+bhVQAlM1LErWdEwoyc7/HEzVwM4/iSOHRUl2/ufjcGtHhZJslySMlofojYkKBCIsWD1HUfjOL5e+G8fesowYO3rJv8LT/WTD1X/pnbRStpISfedg5D0JD1GqntqMigx1AUq7AkqURcx3E++t3KcqfKGFv0OP9/bkrZAAxcIkKlaRdR1+VczrkYpyV5hGOlhR4kYblkgVcU24YrhYbC8KxA0dxoFjh4F6df3SHdIvSBvXjoPGtVapPP+S9fjE1Vxn3ehrypNcom9Xxgqu854VbolLcQ8GqZ7NGEhl6m5KkLX2oHXkZMsp0Z79Fm2JkrYt+03KMq0d+5UQSE5+3D2BsuRhPYOIMTJQHbdnECYOLmWQTe0nZmv6YJCcGLqcQbU86X1BbcegdNbUM4jkOnpkUFcbZJMu98ogOWXlh+G3w36W7dr99rtEqGgb7hYH3rWXIUJGp5zv03EKFPfUMA9X1C8GUUo9WFPOZd2/BU+H2H7+IZnVzz+jTAUuUTT/6FJwy/kn5/nu3oLXxE01gU2uVuUEsonvx0OGjgSim0U0FrslgeQU6K8gui0DSDbEelLlA94qhiaMtYCxtTExTRKciaHNC8tbxHJa4GzVlicJX7OcA7hBjP6Tp5hq4u1+ZodB2GbT2HhsKSaTeORWOQUk94uePTpXnjoK5emnnr44He/EvwRvudm9TD2IoSj/QBihd6V8vp2Ec6c/j8CSt2guJk1nBtTvz5SddetmYQFZYnBJutxo1R0H6EOFKvI645n3eBaueeI3hwUk7sfdA/X80ck+Qs7Ua1c9m4YXtySQnNb5mQkEMU4zf9JIun7nk/CnIm+kiD/UCemalpBObaFV6IM/95XYwiPLzQRq3tyaEgKhNtQTiNiNnKoXE4hEc3afToiKvNbPZYCyxmr580T4g8pQzh+TbrR39oBIRXbLsE0Jf1qkte7OAKXJivrEqEYYVHEkXZEFqkkDXc4gmkHoMbNutUj93BuDCs8SVp/PIAyqOCyvKLVOE4NdnWiLdLlPJxqHfS8MguMmzQw6s7tH9tzyU2fqGURtUFc3mm4T9ulG2wpSMbfgSSMFsj28oTJwlgE0vTYDR9cevU+9y0cDfz2wBXkcEoi3PYF8kwe27KoTeE761LoAqKQs5/+DeDo/vfGQqeSZF9C1/fF0k39LH28Xj1bPZlzpvIR4oJGrUhSGQzV8CyNrZMGfls9+gBKuO1Fcbtb3AvaAgKQNfxJ9Je2Kx804DJp4okp8ph2YY+Ucn6z+rHDaL3wqP1jE4uMKURE3GFTa0bkbf8tHzLtQKtEsXnlvVHSdXip7qQ6CIUTpaJ/jj2ApCOb6fkesubgE92UPK3S1MFc9+kkMfIUlqfx7hTzDq9aWKMh4VGCmZA+mn6XbwcNM+dFLUkXbpdtxzlTUeenml6c/WcmKn/6ixvzyAw== diff --git a/assets/architecture/README.md b/docs/assets/architecture/README.md similarity index 73% rename from assets/architecture/README.md rename to docs/assets/architecture/README.md index 8086b6a2b4..f36652ead9 100644 --- a/assets/architecture/README.md +++ b/docs/assets/architecture/README.md @@ -1,5 +1,6 @@ # Steps to update architecture diagram + 1. Open xml file in https://www.draw.io 2. Make necessary changes 3. Commit the updated diagram in xml and png format. -4. Point the ARCHITECTURE.md file to the updated diagram +4. Point the architecture.md file to the updated diagram diff --git a/TRANSITION.md b/docs/brocfile-transition.md similarity index 78% rename from TRANSITION.md rename to docs/brocfile-transition.md index 9690a8d3ca..55f9e0e67d 100644 --- a/TRANSITION.md +++ b/docs/brocfile-transition.md @@ -6,13 +6,13 @@ Broccoli historically assumed its brocfile was like a main file. Originally, we This change occurred in an effort to unify both sides, rather then duplicating state, we can now pass that state in as an argument. Since the behavior of this file is a superset of a typical Brocfile, we opted to also change the name. -With this change, we are in a good position to fix more bugs, remove some much needed tech-tech, and continue to improve the internals without causing uneeded pain in userland. +With this change, we are in a good position to fix more bugs, remove some much needed tech-debt, and continue to improve the internals without causing uneeded pain in userland. Happy Hacking! ## How -Transitioning your Brocfile is fairly straight forward. Simply take the contents of your Brocfile and place it in the body of the function in the new `ember-cli-build.js` file. Instead of using `module.exports` to return the tree simply have the function return the tree. Ensure you pass the `defaults` to the EmberApp constructor along with any options you were passing to `EmberApp` in the Brocfile. Internally these two objects will be merged from right to left. +Transitioning your Brocfile is fairly straight forward. Simply take the contents of your Brocfile and place it in the body of the function in the new `ember-cli-build.js` file. Instead of using `module.exports` to return the tree simply have the function return the tree. Ensure you pass the `defaults` to the EmberApp constructor along with any options you were passing to `EmberApp` in the Brocfile. Internally these two objects will be merged from right to left. Two steps: @@ -30,10 +30,11 @@ module.exports = app.toTree(); ``` ## After (ember-cli-build.js) + ``` var EmberApp = require('ember-cli/lib/broccoli/ember-app'); -module.exports = function(defaults) { +module.exports = function (defaults) { var app = new EmberApp(defaults, { // Any other options }); diff --git a/docs/build-concurrency.md b/docs/build-concurrency.md new file mode 100644 index 0000000000..749c6f2cdc --- /dev/null +++ b/docs/build-concurrency.md @@ -0,0 +1,15 @@ +# Build Concurrency + +In order to speed up your project's build time, Ember CLI has added a bit of +concurrency throughout the build system. The exact number of parallel +transpilation jobs that will be used can be customized via the `JOBS` process +environment variable. + +The default value for `process.env.JOBS` is (max concurrency) - 1 (via +`require('os').cpus().length - 1`), however there may be times when you need to +customize this value to avoid issues. + +The most common case for this is in CI systems like GitHub Actions +and CircleCI where the total number of CPU's available on the system is very +large (> 32) but the individual CI jobs are limited to only 1.5 or 2 concurrent +processes. diff --git a/docs/build-pipeline-debugging.md b/docs/build-pipeline-debugging.md new file mode 100644 index 0000000000..a818975c34 --- /dev/null +++ b/docs/build-pipeline-debugging.md @@ -0,0 +1,33 @@ +# Build Pipeline Debugging + +**Note**: This is _only_ for debugging purposes and should not be relied upon. + +To gain more visibility into what broccoli trees Ember CLI operates on, you can run the following command: + +```sh +BROCCOLI_DEBUG="bundler:*" ember build +``` + +Fish equivalent is: + +```sh +env BROCCOLI_DEBUG="bundler:*" ember build +``` + +For a brand new ember application (`ember new your-app-name`), `DEBUG/` folder is going to be created with the following contents: + +```sh +DEBUG/ +└── bundler:application-and-dependencies + ├── addon-tree-output + ├── tree-shake-test + └── vendor +``` + +`addon-tree-output` is the folder that contains all trees from Ember CLI add-ons; +`your-app-name` is an application tree and `vendor` contains imported external assets. + +Ember CLI uses [`broccoli-debug`](https://github.com/broccolijs/broccoli-debug/) to generate debug output mentioned above. + +More tips can be found in our [PERF_GUIDE](./perf-guide.md) +under [DEBUG logging section](./perf-guide.md#debug-logging). diff --git a/docs/code-coverage.md b/docs/code-coverage.md new file mode 100644 index 0000000000..c2b9d391ff --- /dev/null +++ b/docs/code-coverage.md @@ -0,0 +1,14 @@ +# Ember CLI Code Coverage & Analysis + +We are using [Coveralls](https://coveralls.io/) to track our code coverage +and [Code Climate](https://codeclimate.com/) to analyze the complexity our code base. + +Code coverage information is generated using [istanbuljs](https://github.com/istanbuljs/nyc) +and then later uploaded to both +[Coveralls](https://coveralls.io/github/ember-cli/ember-cli) and +[Code Climate](https://codeclimate.com/github/ember-cli/ember-cli) via +[`.github/workflows/coverage.yml`](../.github/workflows/coverage.yml) after each Pull Request. + +`CC_TEST_REPORTER_ID` is set via [GitHub encrypted +secrets](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets) +and not exposed to the public as they are private tokens. diff --git a/docs/error-propagation.md b/docs/error-propagation.md new file mode 100644 index 0000000000..a0a2f623b0 --- /dev/null +++ b/docs/error-propagation.md @@ -0,0 +1,138 @@ +# Error Propagation in Broccoli & Ember CLI + +## Quick Summary + +This is a short summary of how errors during build time are surfaced to the browser. Let's look at a particular example. + +Let's say you have the following template: + +```handlebars +

Sup you guise

+``` + +There's an unclosd `p` tag. Seems bad. + +Here's what is going to happen: + +- `ember-template-compiler` throws an exception + - `location` + - `message` + - `stack` + +Error contains information of where in the file error occurred and what kind of error it was. + +- `ember-cli-htmlbars` will catch the error from `ember-template-compiler`. + +Because `ember-cli-htmlbars` is based on `broccoli-persistent-filter`, technically `broccoli-persistent-filter` +will catch the exception first. For example, we attach `file` and `treeDir` information to errors [here](https://github.com/stefanpenner/broccoli-persistent-filter/blob/v1.3.1/index.js#L267-L272). + +- `broccoli` catches the error + +Ember CLI uses `broccoli` to build its trees so if `broccoli` throws an error, +Ember CLI is aware and can act accordingly. + +At this level, we can attach more information to the error, like `broccoli` node/annotation. + +- `broccoli-middleware` catches the error + +This is where we return an error page for the browser. Given all the information, we get from `ember-cli-htmlbars` +and `broccoli`, we show an error page. + +## Error object + +As long as add-ons respect the following error contract, Ember CLI should be able to show useful error messages. +Note, that is just an interface, not an actual class. + +```typescript +/** + * Represents a location of the error. + * @param {string} line - The line number in the file. + * @param {string} column - The column number in the file. + */ +interface Position { + line: number; + column: number; +} + +/** + * Represents an error that might occur during Ember CLI + * build process. + * @param {string} stack - The stack frame of the error. + * @param {string} message - The error message. + * @param {string} codeFrame - The nicely formatted code block with line and column number highlighter (babel style). + * @param {string} type - The class of the error (`Parse Error`, `Compiler Error`, `Syntax Error`). + * @param {string} location - The position of the error in the file. + */ +interface BuildError { + stack: string; + message: string; + codeFrame: string; + type: string; + location: Position; +} +``` + +On the add-on level, error handling might look something like: + +```javascript +"use strict"; + +const Filter = require("broccoli-persistent-filter"); +const stripBom = require("strip-bom"); +const nyanCompiler = require("nyan-compiler"); + +/* + * Compile semicolons to nyan cats because why not. + */ +class NyanCompiler extends Filter { + processString(string, relativePath) { + try { + return nyanCompiler(stripBom(string), relativePath); + } catch (e) { + e.type = "Nyan Compilation Error"; + // assuming error location is nested + e.location = e.location.start; + + throw e; + } + } +} +``` + +To give a real world example, let's modify `broccoli-sass-source-map`: + +```javascript +function rethrowBuildError(e) { + if (typeof e === "string") { + throw new Error("[string exception] " + e); + } else { + e.type = "Sass Syntax Error"; + e.message = e.formatted; + e.location = { + line: e.line, + column: e.column, + }; + + throw e; + } +} + +SassCompiler.prototype.build = function () { + // ... code + return this.renderSass(sassOptions) + .then( + function (result) { + var files = [writeFile(destFile, result.css)]; + + if (this.sassOptions.sourceMap && !this.sassOptions.sourceMapEmbed) { + files.push(writeFile(sourceMapFile, result.map)); + } + + return Promise.all(files); + }.bind(this) + ) + .catch(function (e) { + rethrowBuildError(e); + }); +}; +``` diff --git a/docs/experiments.md b/docs/experiments.md new file mode 100644 index 0000000000..3032e016a3 --- /dev/null +++ b/docs/experiments.md @@ -0,0 +1,50 @@ +# Experiments + +"Experiments" are what Ember CLI calls [Feature Toggle](https://en.wikipedia.org/wiki/Feature_toggle). + +They are defined in `packages/blueprint-model/utilities/experiments.js`. For example: + +```javascript +const availableExperiments = ["CONFIG_CACHING"]; +``` + +When a new feature is added, all supporting code and tests **must** be guarded +to ensure all tests pass when the feature is enabled _or_ disabled: + +```javascript +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +// ...snip... +if (isExperimentEnabled('SOME_EXPERIMENT')) { + ... +} else { + ... +} +``` + +An experiment can be summarized into three different states. + +## Development + +During active development of a feature, it can be enabled by setting the experiments +related environment variable (`'EMBER_CLI_' + EXPERIMENT_NAME`). + +For example, to enable the `CONFIG_CACHING` experiment mentioned in the example +above while running tests you would run the following command: + +``` +EMBER_CLI_CONFIG_CACHING=true pnpm test +``` + +## Unsupported + +The Ember CLI core team will evaluate each experiment before betas get released. +If the experiment is not ready, the entry for the experiment is deleted from +`packages/blueprint-model/utilities/experiments.js` (and therefore disabled). + +## Supported + +Once an experiment has gone through the different stages of beta, and we can +confidently say a specific feature from an experiment will be supported, we +delete the entry in `packages/blueprint-model/utilities/experiments.js` and +remove the experiment guards (e.g. if (experiments.FOO_BAR) {) from the codebase. diff --git a/docs/node-support.md b/docs/node-support.md new file mode 100644 index 0000000000..9e4dcbaa49 --- /dev/null +++ b/docs/node-support.md @@ -0,0 +1,49 @@ +# Node Support + +## Supported Version Matrix + +| Node LTS Version | Supported Ember CLI Versions | +| ---------------- | ---------------------------- | +| 0.10.x | 0.0.0 - 2.8.x | +| 0.12.x | 0.0.0 - 2.11.x | +| 4.x | 1.13.9 - 3.1.x | +| 5.x | 1.13.9 - 2.6.3 | +| 6.x | 2.9.0 - 3.9.x | +| 7.x | 2.10.0 - 2.16.x | +| 8.x | 2.13.3 - 3.16.x | +| 9.x | 2.16.2 - 3.2.x | +| 10.x | 3.1.3 - 3.28.0 | +| 11.x | 3.9.0 - 3.13.0 | +| 12.x | 3.10.0 - 4.6.0 | +| 13.x | 3.15.0 - 3.20.0 | +| 14.x | 3.19.0 - 5.0.0 | +| 16.x | 3.28.0 - 5.3.0 | +| 18.x | 4.6.0 - 6.5.0 | +| 20.x | 5.4.0 - Current | +| 22.x | 6.2.0 - Current | +| 24.x | 6.7.0 - Current | + +## Design + +Commits to the `HEAD` of the master branch will provide support for any Active +Node.js LTS and the current stable Node.js version(s). +This will be enforced via CI, preventing us from landing code which won't work +with versions which we support. This means that our schedule for support is +tied to the [LTS release schedule for +Node.js](https://github.com/nodejs/LTS#lts_schedule). + +## Current support: + +- v18: Released as stable version. + - Supported by ember-cli/ember-cli#master until: 2025-04-30. +- v20: Released as stable version. + - Supported by ember-cli/ember-cli#master until: 2026-04-30. +- v22: Released as stable version. + - Supported by ember-cli/ember-cli#master until: 2027-04-30. + +## Release Process and Support Policy + +Ember and Ember CLI have committed to supporting the [Node.js LTS schedule](https://github.com/nodejs/LTS#lts-schedule) +for the `HEAD` of our `master` branch(es). This means that we will drop support +per the [Node.js Release Working Group](https://github.com/nodejs/Release)'s schedule without a major version +bump/change of ember-cli itself. diff --git a/docs/perf-guide.md b/docs/perf-guide.md new file mode 100644 index 0000000000..88be569cdc --- /dev/null +++ b/docs/perf-guide.md @@ -0,0 +1,246 @@ +# Build performance + +When we talk about build performance, it is important to understand that there +are several build phases: + +- cold build (booting your app up for the first time) +- warm build (booting your app up when cache was populated) +- rebuild (subsequent rebuilds that happen on file change) + +_Cold build_ is the slowest because the cache is not yet populated and the +application is booting for the first time. Build time varies and depends on +the number of dependencies that application has but ballpark should be around 5 +seconds for small to middle size applications and around 15 seconds for large +size applications. + +_Warm build_ is faster then cold one because the cache was populated already +and it takes less time re-compute dependencies. Build time varies but ballpark +should be around 2 seconds for small to middle size applications and around 10 +seconds for large size applications. + +_Rebuild_ aims to be the fastest because it happens the most often. App/ +JavaScript Rebuild time varies but ballpark should be around 200-300ms for +small to middle size applications and up to 1 second for large size +applications (200kloc js + 3000 modules). + +- rebuild of JS in app/ <--- Largely our focus, as this is likely the most common. +- rebuild of CSS/Sass/Less in app/: largely depends on 3 factors + - which preprocessor is being used (libsass vs ruby-sass vs less vs ...) + - the amount of css +- rebuild of vendor/ <--- somewhat costly still, due to + how slow available sourceMap Libraries are. We have WIP with a more v8/JIT + friendly sourceMap lib, already showing some very nice improvements. + +_note_ these times are based on a posix box with and SSD, win32 unfortunately +tends to be slower. As we continue to improve performance, both posix and win32 +improve, hopefully future work will bring these platforms build times closer +together. + +If you see that application timings escape, there might be a problem. + +Addons known to cause a slow down (but have not yet been addressed): + +- any old non-patch based broccoli plugin +- ember-cli-component-css +- ember-browserify +- ... ? + +Make sure to mention which type of the build seems to be the problem so we can +help identify and fix issues faster. + +## FAQ/Common Issues & Solutions: + +##### Q + +My rebuilds are slow. (And I have anti-virus installed) + +##### A + +Our build-system assumes a relative fast/performant file system (although, we +continue to reduce IO related work). It is quite common for a Anti-virus to +slow down IO. + +Common issues: + +- anti-virus scanning of /tmp/, oftentimes this can be avoided + altogether. +- anti-virus on-file-access re-scanning files, oftentimes this can be disabled + for the app directory. or ember can be whitelisted. + +##### Q + +My rebuilds are slow. (And I am using an encrypted thumb-drive to host my project) + +##### A + +These sorts of drives are notoriously slow. Although we continue to reduce our +IO overhead, you will be running at a disadvantage. Oftentimes, a much +better alternative is hardware supported full-disk encryption, like on most OSX +corporate laptops (mine included) use. This setup is both reasonably secure, +and has negligible impact on performance. + +##### Q + +My JavaScript rebuilds are still slow. + +##### A + +please run + +``` +npm ls broccoli-funnel broccoli-merge-trees broccoli-filter broccoli-persistent-filter broccoli-concat broccoli-caching-writer +``` + +and ideally the following should be true (otherwise some upgrades may be required) + +- `broccoli-funnel` should be at `^1.0.1` +- `broccoli-merge-trees` should be at `^1.1.0` +- `broccoli-persistent-filter` should be at `^1.1.6` +- `broccoli-filter` often needs to be replaced with + `broccoli-persistent-filter` (we hope to re-merge the two eventually) +- `broccoli-sourcemap-concat` should be at `^2.0.2` but will soon be replace by + `broccoli-concat` (we have just re-merged the two) +- `broccoli-caching-writer` should be at `^2.2.1` +- `broccoli-concat` should be at `^2.0.3` +- `broccoli-stew` should be at `^1.2.0` +- likely more... + +Up next we should check for old and deprecate plugins + +- `npm ls broccoli-static-compiler` this should no longer be used, rather + `broccoli-funnel` at `v1.0.1` should be used +- .. + +##### Q + +npm v3 made my build slow + +##### A + +Well what happened is npm v3 changed the module topology, this coupled with a +misbehaving plugin may result in extra files (maybe all of node_modules) being +pulled into the build. This is going to be slow.., the solution is to find the +offending plugin, and upgrade (or report the issue if it is not yet fixed). + +One such plugin is `ember-cli-ic-ajax`, which has been fixed. So please be sure +to upgrade. + +Finding such plugins, we can use a series of DEBUG flags, to gain more insight + +`DEBUG=broccoli-funnel:Funnel*Addon* ember s` should reveal if extra files are +being pulled into the build + +##### Q + +My builds are slow, and the above Q/A hasn't helped + +##### A + +Please be sure to read this full document (including the tips and tricks bellow). +If the issue persists, please report an issue. + +Be sure to include: + +- `npm version` +- `npm ls` (as a gist) +- ideally a reproduction + - we are aware some are unable to share apps (even privately), this may prove + more difficult to debug. Although in some cases, consulting and proper IP + related paperwork to allow sharing could enable improved debugging + +##### Q + +My builds are slow for a reason not mentioned here + +##### A + +We would love a PR improving this guide. + +# Various tips and tricks + +# How to explore/debug and hopefully address performance issues + +### DEBUG logging + +We use [heimdalljs-logger](https://github.com/heimdalljs/heimdalljs-logger) for +logging, which supports the same usage as the de facto standard +[debug](https://github.com/visionmedia/debug). Quite often this can be used to +quickly discover obviously wrong things. + +Usage: + +- `DEBUG= ember s` +- `DEBUG=* ember s` for all logging (this will be very verbose) +- `DEBUG=ember-cli* ember s` for all ember-cli logging +- `DEBUG=broccoli* ember s` for all broccoli logging +- `DEBUG=broccoli*,ember-cli* ember s` for both broccoli and ember-cli logging + +The above patterns will be very verbose. But to make them even more verbose you +can set the log level via `DEBUG_LEVEL` + +- `DEBUG=* DEBUG_LEVEL=debug ember s` + +To make them a bit less verbose, a curated set of performance related logging +flags are: + +- `DEBUG=broccoli-caching-writer:* ember s` +- `DEBUG=broccoli-funnel:* ember s` +- `DEBUG=broccoli-funnel:Funnel*Addon* ember s` +- `DEBUG=broccoli-merge-trees ember s` +- `DEBUG=broccoli-merge-trees:TreeMerger* ember s` +- `DEBUG=broccoli-merge-trees:Addon* ember s` +- `DEBUG=broccoli-merge-trees:styles ember s` +- `DEBUG=broccoli-merge-trees:compileTemplates* ember s` +- `DEBUG=broccoli-merge-trees:compileTemplates* ember s` + +Because many plugins are used repeatedly it may be difficult to see the context +for log entries. By default, 3 nodes of context are shown. + +``` +DEBUG_LEVEL=debug DEBUG=broccoli-merge-trees: ember build +broccoli-merge-trees: [TreeMerger (testFiles)#777 -> ConcatWithMaps#782 -> BroccoliMergeTrees#783] deriving patches +``` + +To show more (or fewer) lines of context, specify the environment variable +`DEBUG_DEPTH`. + +``` +DEBUG_DEPTH=5 DEBUG_LEVEL=debug DEBUG=broccoli-merge-trees: ember build +# => broccoli-merge-trees: [TreeMerger (allTrees)#1 -> BroccoliMergeTrees#668 -> TreeMerger (testFiles)#777 -> ConcatWithMaps#782 -> BroccoliMergeTrees#783] +``` + +`[... ConcatWithMaps#782 -> BroccoliMergeTrees#783]` means that the log entry +occurred in broccoli merge-trees node with id 783, whose parent was a concat +with maps node with id 782. These ids are shown in the visualization graph. +See [Visualization](#visualization) for details. + +... more on what to look for ... + +### `broccoli-viz` + +#### Visualization + +To visualize build tree, we use [graphviz](http://www.graphviz.org/). To +install it run `brew install graphviz` or download it directly from +[here](http://www.graphviz.org/Download.php). + +You will also need to install +[broccoli-viz](https://github.com/stefanpenner/broccoli-viz) version `4.0.0` or +higher. `npm install -g broccoli-viz@^4.0.0`. + +To generate visualization: + +- `BROCCOLI_VIZ=1 ember build` +- `broccoli-viz instrumentation.build.0.json > instrumentation.build.0.dot` +- `dot -Tpng instrumentation.build.0.dot > instrumentation.build.0.png` + +Each build will generate an additional graph, `instrumentation.build..json` + +Alternatively, you can upload the JSON file to https://heimdalljs.github.io/heimdalljs-visualizer/ + +#### in-depth look + +in-depth tooling, aimed to provide much deeper insight into the given build + +- `dot`: is the input to graphviz, allowing tree visualization +- `json`: more detailed counts and timings related to the corresponding build diff --git a/docs/project_version_preprocessor.js b/docs/project_version_preprocessor.js new file mode 100644 index 0000000000..2a50955da4 --- /dev/null +++ b/docs/project_version_preprocessor.js @@ -0,0 +1,8 @@ +'use strict'; + +let versionUtils = require('../lib/utilities/version-utils'); +let emberCLIVersion = versionUtils.emberCLIVersion; + +module.exports = function (data, options) { + options.project.version = emberCLIVersion(); +}; diff --git a/docs/sourcemaps.md b/docs/sourcemaps.md new file mode 100644 index 0000000000..cb9490cb1a --- /dev/null +++ b/docs/sourcemaps.md @@ -0,0 +1,59 @@ +# Sourcemaps + +Today, we expect sourcemaps to work in ember-cli. A default ember-cli +application should have functioning sourcemaps, with the exception of in-module +ES6 -> ES5 mappings. This is for two reasons: + +1. dramatically increases build costs +2. babel (at least at the time) does not correctly mangle names, which results in + a very poor debugging experience when some of your variables are debuggable + and others are not. We hope this is eventually rectified. + +ember-cli, via ef4/fast-sourcemap-concat, attempts to detect invalid sourcemaps, +and discards them while still producing a valid, correct but incomplete +sourcemap. This means inputs with invalid sourcemaps can still be viewed +separately in the chrome debugger (as separate "files"), but will have no +mappings (eg for function names). + +If your experience differs from the above, please read the rest of +this document before logging an issue. + +## Debugging Sourcemaps + +Sourcemaps can be a bit tricky to read. This is due to them being VLQ encoded, +and humans not being very good at decoding VLQ on the fly. + +There are broadly two ways a source file will have sourcemaps attached. + +1. Via an external map file +2. Inline, in Base64. + +External sourcemap files can be read with the [helper tools at +fast-source-map](https://github.com/krisselden/fast-source-map/tree/master/bin). + +Inline sourcemaps will look something like + +``` +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImlubmVyL2ZpcnN0LmpzIiwiaW5uZXIvc2Vjb25kLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG1lYW5pbmdPZkxpZmUoKSB7XG4gIHRocm93IG5ldyBFcnJvcig0Mik7XG59XG5cbmZ1bmN0aW9uIGJvb20oKSB7XG4gIHRocm93IG5ldyBFcnJvcignYm9vbScpO1xufVxuIiwiZnVuY3Rpb24gc29tZXRoaW5nRWxzZSgpIHtcbiAgdGhyb3cgbmV3IEVycm9yKFwic29tZXRoaWduIGVsc2VcIik7XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDTkE7QUFDQTtBQUNBOyIsImZpbGUiOiJhbGwtaW5uZXIuanMifQ== +``` + +The easiest way to read them is to first convert them to an external map file. +To do so: + +1. take the part after `base64,` and in node `new Buffer(string, +'base64').toString();` +2. write this to a file, and one can now use the above described fast sourcemap + helpers + +### Handling Invalid Sourcemaps + +Add `DEBUG='fast-sourcemap-concat:*'` to see error output when invalid +sourcemaps are detected. + +## Useful tools + +- source map visualizer - https://sokra.github.io/source-map-visualization/ \*\* + Very useful, but appears to have issues with both large sourcemaps, and + incorrect sourcemaps (such as ones with unexpected negative offsets). +- encode/decode - https://github.com/krisselden/fast-source-map/tree/master/bin +- srcmap r3 - `https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit` diff --git a/docs/yuidoc.json b/docs/yuidoc.json index 1f5174ade9..b36cfd9cc6 100644 --- a/docs/yuidoc.json +++ b/docs/yuidoc.json @@ -1,13 +1,11 @@ { - "name": "The Ember CLI API", - "description": "The Ember CLI API: The command line interface for ambitious web applications.", - "version": "1.0 pre", - "url": "http://www.ember-cli.com/", + "logo": "https://ember-cli.com/assets/images/ember-cli-logo-small-dark.png", "options": { - "paths": [ - "../lib" - ], - "exclude": "vendor", - "outdir": "./build" + "paths": ["lib"], + "themedir": "node_modules/yuidoc-ember-cli-theme", + "helpers": ["node_modules/yuidoc-ember-cli-theme/helpers.js"], + "outdir": "docs/build", + "preprocessor": "docs/project_version_preprocessor.js", + "linkNatives": true } -} \ No newline at end of file +} diff --git a/lib/broccoli/app-boot.js b/lib/broccoli/app-boot.js index 3db31222c3..52de5ce563 100644 --- a/lib/broccoli/app-boot.js +++ b/lib/broccoli/app-boot.js @@ -1,9 +1 @@ -/* jshint ignore:start */ - -define('{{MODULE_PREFIX}}/config/environment', ['ember'], function(Ember) { - {{content-for 'config-module'}} -}); - {{content-for 'app-boot'}} - -/* jshint ignore:end */ diff --git a/lib/broccoli/app-config-from-meta.js b/lib/broccoli/app-config-from-meta.js index 51ed3f4db3..08c4bd639f 100644 --- a/lib/broccoli/app-config-from-meta.js +++ b/lib/broccoli/app-config-from-meta.js @@ -1,14 +1,14 @@ -/* jshint ignore:start */ - try { var metaName = prefix + '/config/environment'; - var rawConfig = Ember['default'].$('meta[name="' + metaName + '"]').attr('content'); - var config = JSON.parse(unescape(rawConfig)); + var rawConfig = document.querySelector('meta[name="' + metaName + '"]').getAttribute('content'); + var config = JSON.parse(decodeURIComponent(rawConfig)); + + var exports = { 'default': config }; - return { 'default': config }; + Object.defineProperty(exports, '__esModule', { value: true }); + + return exports; } catch(err) { throw new Error('Could not read config from meta tag with name "' + metaName + '".'); } - -/* jshint ignore:end */ diff --git a/lib/broccoli/app-config.js b/lib/broccoli/app-config.js new file mode 100644 index 0000000000..b47ecf2eb6 --- /dev/null +++ b/lib/broccoli/app-config.js @@ -0,0 +1,3 @@ +define('{{MODULE_PREFIX}}/config/environment', [], function() { + {{content-for 'config-module'}} +}); diff --git a/lib/broccoli/app-prefix.js b/lib/broccoli/app-prefix.js index cd1e75d52f..ef82ca8c8b 100644 --- a/lib/broccoli/app-prefix.js +++ b/lib/broccoli/app-prefix.js @@ -1,4 +1,3 @@ -"use strict"; -/* jshint ignore:start */ +'use strict'; + {{content-for 'app-prefix'}} -/* jshint ignore:end */ diff --git a/lib/broccoli/app-suffix.js b/lib/broccoli/app-suffix.js index 2ebf57760b..a51f77ab54 100644 --- a/lib/broccoli/app-suffix.js +++ b/lib/broccoli/app-suffix.js @@ -1,3 +1 @@ -/* jshint ignore:start */ {{content-for 'app-suffix'}} -/* jshint ignore:end */ diff --git a/lib/broccoli/broccoli-config-loader.js b/lib/broccoli/broccoli-config-loader.js deleted file mode 100644 index ada2884f59..0000000000 --- a/lib/broccoli/broccoli-config-loader.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var path = require('path'); -var Writer = require('broccoli-caching-writer'); - -function ConfigLoader (inputTree, options) { - if (!(this instanceof ConfigLoader)) { - return new ConfigLoader(inputTree, options); - } - - Writer.apply(this, arguments); - - this.options = options || {}; -} - -ConfigLoader.prototype = Object.create(Writer.prototype); -ConfigLoader.prototype.constructor = ConfigLoader; - -/* - * @private - * - * On windows, when residing on a UNC share (lib or app/addon code), exact - * match here is not possible. Although we could be more precise, there is - * little pain in evicting all fuzzy matches - * - * @method fuzzyPurgeRequireEntry - */ -function fuzzyPurgeRequireEntry(entry) { - var matches = Object.keys(require.cache).filter(function(path) { - return path.indexOf(entry) > -1; - }); - - matches.forEach(function(entry) { - delete require.cache[entry]; - }); -} - -ConfigLoader.prototype.clearConfigGeneratorCache = function() { - fuzzyPurgeRequireEntry(this.options.project.configPath() + '.js'); -}; - -ConfigLoader.prototype.updateCache = function(srcDir, destDir) { - var self = this; - - this.clearConfigGeneratorCache(); - - var outputDir = path.join(destDir, 'environments'); - fs.mkdirSync(outputDir); - - var environments = [this.options.env]; - if (this.options.tests) { - environments.push('test'); - } - - environments.forEach(function(env) { - var config = self.options.project.config(env); - var jsonString = JSON.stringify(config); - var outputPath = path.join(outputDir, env); - - fs.writeFileSync(outputPath + '.json', jsonString, { encoding: 'utf8' }); - }, this); -}; - -module.exports = ConfigLoader; diff --git a/lib/broccoli/broccoli-config-replace.js b/lib/broccoli/broccoli-config-replace.js deleted file mode 100644 index ca7b2b6af6..0000000000 --- a/lib/broccoli/broccoli-config-replace.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -var fs = require('fs-extra'); -var existsSync = require('exists-sync'); -var path = require('path'); -var Writer = require('broccoli-writer'); - -function CustomReplace (inputTree, configTree, options) { - if (!(this instanceof CustomReplace)) { - return new CustomReplace(inputTree, configTree, options); - } - - Writer.call(this, inputTree, options); // this._super(); - - this.inputTree = inputTree; - this.configTree = configTree; - this.options = options; -} -CustomReplace.prototype = Object.create(Writer.prototype); -CustomReplace.prototype.constructor = CustomReplace; - -CustomReplace.prototype.write = function (readTree, destDir) { - var self = this; - var inputDir, configDir; - - return readTree(this.inputTree) - .then(function(path) { - inputDir = path; - - return readTree(self.configTree); - }) - .then(function(path) { - configDir = path; - - return { - configDir: configDir, - inputDir: inputDir, - destDir: destDir - }; - }) - .then(this.process.bind(this)); -}; - -CustomReplace.prototype.process = function(results) { - var files = this.options.files; - var config = this.getConfig(results.configDir); - - for (var i = 0, l = files.length; i < l; i++) { - var file = files[i]; - var filePath = path.join(results.inputDir, file); - var destPath = path.join(results.destDir, file); - - this.processFile(config, filePath, destPath); - } -}; - -CustomReplace.prototype.processFile = function(config, filePath, destPath) { - var contents = fs.readFileSync(filePath, { encoding: 'utf8' }); - - for (var i = 0, l = this.options.patterns.length; i < l; i++) { - // jshint loopfunc:true - var pattern = this.options.patterns[i]; - var replacement = pattern.replacement; - - if (typeof pattern.replacement === 'function') { - replacement = function() { - var args = Array.prototype.slice.call(arguments); - - return pattern.replacement.apply(null, [config].concat(args)); - }; - } - - contents = contents.replace(pattern.match, replacement); - } - - if (!existsSync(path.dirname(destPath))) { - fs.mkdirsSync(path.dirname(destPath)); - } - fs.writeFileSync(destPath, contents, { encoding: 'utf8' }); -}; - -CustomReplace.prototype.getConfig = function (srcDir) { - var configPath = path.join(srcDir, this.options.configPath); - - return JSON.parse(fs.readFileSync(configPath, { encoding: 'utf8' })); -}; - -module.exports = CustomReplace; diff --git a/lib/broccoli/default-packager.js b/lib/broccoli/default-packager.js new file mode 100644 index 0000000000..fd159b5733 --- /dev/null +++ b/lib/broccoli/default-packager.js @@ -0,0 +1,1265 @@ +'use strict'; + +const p = require('ember-cli-preprocess-registry/preprocessors'); +const path = require('path'); +const concat = require('broccoli-concat'); +const Funnel = require('broccoli-funnel'); +const BroccoliDebug = require('broccoli-debug'); +const mergeTrees = require('./merge-trees'); +const ConfigLoader = require('broccoli-config-loader'); +const UnwatchedDir = require('broccoli-source').UnwatchedDir; +const ConfigReplace = require('broccoli-config-replace'); +const emberAppUtils = require('../utilities/ember-app-utils'); +const funnelReducer = require('broccoli-funnel-reducer'); +const addonProcessTree = require('../utilities/addon-process-tree'); + +const preprocessCss = p.preprocessCss; +const preprocessJs = p.preprocessJs; +const preprocessTemplates = p.preprocessTemplates; +const preprocessMinifyCss = p.preprocessMinifyCss; + +const DEFAULT_VENDOR_PATH = 'vendor'; +const EMBER_CLI_INTERNAL_FILES_PATH = '/vendor/ember-cli/'; +const EMBER_CLI_FILES = [ + 'app-boot.js', + 'app-config.js', + 'app-prefix.js', + 'app-suffix.js', + 'test-support-prefix.js', + 'test-support-suffix.js', + 'tests-prefix.js', + 'tests-suffix.js', + 'vendor-prefix.js', + 'vendor-suffix.js', +]; + +const configReplacePatterns = emberAppUtils.configReplacePatterns; + +function callAddonsPreprocessTreeHook(project, type, tree) { + return addonProcessTree(project, 'preprocessTree', type, tree); +} + +function callAddonsPostprocessTreeHook(project, type, tree) { + return addonProcessTree(project, 'postprocessTree', type, tree); +} + +/* + Creates an object with lists of files to be concatenated into `vendor.js` file. + + Given a map that looks like: + + ``` + { + 'assets/vendor.js': [ + 'vendor/ember-cli-shims/app-shims.js', + 'vendor/loader/loader.js', + 'vendor/ember-resolver/legacy-shims.js', + ... + ] + } + ``` + + Produces an object that looks like: + + ``` + { + headerFiles: [ + 'vendor/ember-cli/vendor-prefix.js', + 'vendor/loader/loader.js', + 'vendor/ember/jquery/jquery.js', + 'vendor/ember/ember.debug.js', + 'vendor/ember-cli-shims/app-shims.js', + 'vendor/ember-resolver/legacy-shims.js' + ], + inputFiles: [ + 'addon-tree-output/**\/*.js' + ], + footerFiles: [ + 'vendor/ember-cli/vendor-suffix.js' + ], + annotation: 'Vendor JS' + } + ``` + + @private + @method getVendorFiles + @param {Object} files A list of files to include into `.js` + @param {Boolean} isMainVendorFile Boolean flag to indicate if we are dealing with `vendor.js` file + @return {Object} An object with lists of files to be concatenated into `vendor.js` file. + */ +function getVendorFiles(files, isMainVendorFile) { + return { + headerFiles: files, + inputFiles: isMainVendorFile ? ['addon-tree-output/**/*.js'] : [], + footerFiles: isMainVendorFile ? ['vendor/ember-cli/vendor-suffix.js'] : [], + }; +} + +/** + * Responsible for packaging Ember.js application. + * + * @class DefaultPackager + * @constructor + */ +module.exports = class DefaultPackager { + constructor(options) { + this._cachedTests = null; + this._cachedVendor = null; + this._cachedPublic = null; + this._cachedConfig = null; + this._cachedJavascript = null; + this._cachedProcessedIndex = null; + this._cachedTransformedTree = null; + this._cachedProcessedStyles = null; + this._cachedProcessedTemplates = null; + this._cachedProcessedJavascript = null; + this._cachedEmberCliInternalTree = null; + this._cachedProcessedAdditionalAssets = null; + this._cachedProcessedAppAndDependencies = null; + + this.options = options || {}; + + this._debugTree = BroccoliDebug.buildDebugCallback('default-packager'); + + this.env = this.options.env; + this.name = this.options.name; + this.autoRun = this.options.autoRun; + this.project = this.options.project; + this.registry = this.options.registry; + this.sourcemaps = this.options.sourcemaps; + this.minifyCSS = this.options.minifyCSS; + this.distPaths = this.options.distPaths; + this.areTestsEnabled = this.options.areTestsEnabled; + this.styleOutputFiles = this.options.styleOutputFiles; + this.scriptOutputFiles = this.options.scriptOutputFiles; + this.storeConfigInMeta = this.options.storeConfigInMeta; + this.customTransformsMap = this.options.customTransformsMap; + this.additionalAssetPaths = this.options.additionalAssetPaths; + this.vendorTestStaticStyles = this.options.vendorTestStaticStyles; + this.legacyTestFilesToAppend = this.options.legacyTestFilesToAppend; + } + + /* + * Replaces variables in `index.html` file with values from + * `config/environment.js` and returns a single tree that contains + * `index.html` file with populated values. + * + * Input tree: + * + * ``` + * / + * ├── addon-tree-output/ + * ├── the-best-app-ever/ + * └── vendor/ + * ``` + * + * Returns: + * + * ``` + * / + * └── index.html + * ``` + * + * @private + * @method processIndex + * @param {BroccoliTree} + * @return {BroccoliTree} + */ + processIndex(tree) { + if (this._cachedProcessedIndex === null) { + let indexFilePath = this.distPaths.appHtmlFile; + + let index = new Funnel(tree, { + allowEmtpy: true, + include: [`${this.name}/index.html`], + getDestinationPath: () => indexFilePath, + annotation: 'Classic: index.html', + }); + + let patterns = configReplacePatterns({ + addons: this.project.addons, + autoRun: this.autoRun, + storeConfigInMeta: this.storeConfigInMeta, + }); + + this._cachedProcessedIndex = new ConfigReplace(index, this.packageConfig(), { + configPath: path.join(this.name, 'config', 'environments', `${this.env}.json`), + files: [indexFilePath], + patterns, + }); + } + + return this._cachedProcessedIndex; + } + + /* + * Combines compiled javascript, external files (node modules), + * vendor files and processed configuration (based on the + * environment) into a single tree. + * + * Input tree: + * + * ``` + * / + * ├── addon-tree-output/ + * ├── the-best-app-ever/ + * └── vendor/ + * ``` + * + * Changes are made "inline" so the output tree has the same structure. + * + * @private + * @method processAppAndDependencies + * @param {BroccoliTree} + * @return {BroccoliTree} + */ + processAppAndDependencies(allTrees) { + if (this._cachedProcessedAppAndDependencies === null) { + let config = this.packageConfig(); + let internal = this.packageEmberCliInternalFiles(); + let appContentsWithCompiledTemplates = this._debugTree( + this.processTemplates(allTrees), + 'app-and-deps:post-templates' + ); + + let trees = [allTrees, appContentsWithCompiledTemplates].filter(Boolean); + + let mergedTree = this._debugTree( + mergeTrees(trees, { + annotation: 'TreeMerger (preprocessedApp & templates)', + overwrite: true, + }), + 'app-and-deps:merged' + ); + + let external = this.applyCustomTransforms(allTrees); + let postprocessedApp = this.processJavascript(mergedTree); + + let sourceTrees = [external, postprocessedApp, config, internal]; + + this._cachedProcessedAppAndDependencies = this._debugTree( + mergeTrees(sourceTrees, { + overwrite: true, + annotation: 'Processed Application and Dependencies', + }), + 'app-and-deps:final' + ); + } + + return this._cachedProcessedAppAndDependencies; + } + + /* + * Adds additional assets to the results tree, given the following list: + * + * ``` + * [{ + * src: 'vendor/font-awesome/fonts', + * file: 'FontAwesome.otf', + * dest: 'fonts' + * }] + * ``` + * + * where `src` is a source path, `file` is a file name, and `dest` is a new + * destination. + * + * @private + * @method importAdditionalAssets + * @param {BroccoliTree} + * @return {BroccoliTree} + */ + importAdditionalAssets(tree) { + if (this._cachedProcessedAdditionalAssets === null) { + let otherAssetTrees = funnelReducer(this.additionalAssetPaths).map((options) => { + let files = options.include.join(','); + options.annotation = `${options.srcDir}/{${files}} => ${options.destDir}/{${files}}`; + + return new Funnel(tree, options); + }); + + this._cachedProcessedAdditionalAssets = mergeTrees(otherAssetTrees, { + annotation: 'Processed Additional Assets', + }); + } + + return this._cachedProcessedAdditionalAssets; + } + + /* + * Runs all registered transformations on the passed in tree and returns the + * result. + * + * Passed-in tree: + * + * ``` + * / + * ├── addon-tree-output/ + * └── vendor/ + * ``` + * + * `customTransformsMap` has information about files that needed to be + * transformed and the actual transformation functions that are executed. + * + * @private + * @method applyCustomTransforms + * @param {BroccoliTree} External (vendor) tree + * @return {BroccoliTree} + */ + applyCustomTransforms(externalTree) { + if (this._cachedTransformedTree === null) { + this._cachedTransformedTree = externalTree; + + for (let customTransformEntry of this.customTransformsMap) { + let transformName = customTransformEntry[0]; + let transformConfig = customTransformEntry[1]; + + let transformTree = new Funnel(this._cachedTransformedTree, { + files: transformConfig.files, + annotation: `Funnel (custom transform: ${transformName})`, + }); + + this._cachedTransformedTree = mergeTrees( + [this._cachedTransformedTree, transformConfig.callback(transformTree, transformConfig.options)], + { + annotation: `TreeMerger (custom transform: ${transformName})`, + overwrite: true, + } + ); + } + } + + return this._cachedTransformedTree; + } + + /* + * Returns a single tree with `ember-cli` internal files with the following + * structure: + * + * ``` + * vendor/ + * └── ember-cli + * ├── app-boot.js + * ├── app-config.js + * ├── app-prefix.js + * ├── app-suffix.js + * ├── test-support-suffix.js + * ├── test-support-prefix.js + * ├── tests-prefix.js + * ├── tests-suffix.js + * ├── vendor-prefix.js + * └── vendor-suffix.js + * ``` + * + * Note, that the contents of these files is being matched against several + * internal `ember-cli` variables, such as: + * + * + `{{MODULE_PREFIX}}` + * + different types of `{{content-for}}` (`{{content-for 'app-boot'}}`) + * + * @private + * @method packageEmberCliInternalFiles + * @return {BroccoliTree} + */ + packageEmberCliInternalFiles() { + if (this._cachedEmberCliInternalTree === null) { + let patterns = configReplacePatterns({ + addons: this.project.addons, + autoRun: this.autoRun, + storeConfigInMeta: this.storeConfigInMeta, + }); + + let configTree = this.packageConfig(); + let configPath = path.join(this.name, 'config', 'environments', `${this.env}.json`); + + let emberCLITree = new ConfigReplace(new UnwatchedDir(__dirname), configTree, { + configPath, + files: EMBER_CLI_FILES, + patterns, + }); + + this._cachedEmberCliInternalTree = new Funnel(emberCLITree, { + files: EMBER_CLI_FILES, + destDir: EMBER_CLI_INTERNAL_FILES_PATH, + annotation: 'Packaged Ember CLI Internal Files', + }); + } + + return this._cachedEmberCliInternalTree; + } + + /* + * Runs pre/post-processors hooks on the template files and returns a single + * tree with the processed templates. + * + * Given a tree: + * + * ``` + * / + * ├── addon-tree-output/ + * ├── the-best-app-ever/ + * └── vendor/ + * ``` + * + * Returns: + * + * ``` + * the-best-app-ever/ + * └── templates + * ├── application.js + * ├── error.js + * ├── index.js + * └── loading.js + * ``` + * + * @private + * @method processTemplates + * @param {BroccoliTree} tree + * @return {BroccoliTree} + */ + processTemplates(inputTree) { + if (this._cachedProcessedTemplates === null) { + let appFiles = new Funnel(inputTree, { + srcDir: `${this.name}/`, + destDir: `${this.name}/`, + annotation: 'processTemplates: app files', + }); + + let mergedTemplates = [appFiles]; + + mergedTemplates = mergeTrees(mergedTemplates, { + overwrite: true, + annotation: 'Templates', + }); + let preprocessedTemplatesFromAddons = callAddonsPreprocessTreeHook(this.project, 'template', mergedTemplates); + + this._cachedProcessedTemplates = callAddonsPostprocessTreeHook( + this.project, + 'template', + preprocessTemplates(preprocessedTemplatesFromAddons, { + registry: this.registry, + treeType: 'templates', + }) + ); + } + + return this._cachedProcessedTemplates; + } + + /* + * Runs pre/post-processors hooks on the javascript files and returns a single + * tree with the processed javascript. + * + * Given a tree: + * + * ``` + * / + * ├── addon-tree-output/ + * ├── the-best-app-ever/ + * └── vendor/ + * ``` + * + * Returns: + * + * ``` + * the-best-app-ever/ + * ├── adapters + * │ └── application.js + * ├── app.js + * ├── components + * ├── controllers + * ├── helpers + * │ ├── and.js + * │ ├── app-version.js + * │ ├── await.js + * │ ├── camelize.js + * │ ├── cancel-all.js + * │ ├── dasherize.js + * │ ├── dec.js + * │ ├── drop.js + * │ └── eq.js + * ... + * ``` + * + * @private + * @method processJavascript + * @param {BroccoliTree} tree + * @return {BroccoliTree} + */ + processJavascript(tree) { + if (this._cachedProcessedJavascript === null) { + let javascript = new Funnel(tree, { + srcDir: this.name, + destDir: this.name, + annotation: '', + }); + let app = callAddonsPreprocessTreeHook(this.project, 'js', javascript); + + let preprocessedApp = preprocessJs(app, '/', this.name, { + registry: this.registry, + treeType: 'app', + }); + + this._cachedProcessedJavascript = callAddonsPostprocessTreeHook(this.project, 'js', preprocessedApp); + } + + return this._cachedProcessedJavascript; + } + + /* + * Compiles application css files, runs pre/post-processors hooks on the them, + * concatenates them into one application and vendor files and returns a + * single tree. + * + * Given an input tree that looks like: + * + * ``` + * addon-tree-output/ + * ... + * the-best-app-ever/ + * styles/ + * ... + * vendor/ + * font-awesome/ + * ... + * ``` + * + * Returns: + * + * ``` + * assets/ + * the-best-app-ever.css + * vendor.css + * ``` + * + * @private + * @method packageStyles + * @return {BroccoliTree} + */ + packageStyles(tree) { + if (this._cachedProcessedStyles === null) { + let cssMinificationEnabled = this.minifyCSS.enabled; + let options = { + outputPaths: this.distPaths.appCssFile, + registry: this.registry, + minifyCSS: this.minifyCSS.options, + treeType: 'styles', + }; + + let stylesAndVendor = callAddonsPreprocessTreeHook(this.project, 'css', tree); + let preprocessedStyles = preprocessCss(stylesAndVendor, '/app/styles', '/assets', options); + + let vendorStyles = []; + for (let outputFile in this.styleOutputFiles) { + let isMainVendorFile = outputFile === this.distPaths.vendorCssFile; + let headerFiles = this.styleOutputFiles[outputFile]; + let inputFiles = isMainVendorFile ? ['addon-tree-output/**/__COMPILED_STYLES__/**/*.css'] : []; + + vendorStyles.push( + concat(stylesAndVendor, { + headerFiles, + inputFiles, + outputFile, + allowNone: true, + annotation: `Concat: Vendor Styles${outputFile}`, + }) + ); + } + + vendorStyles = mergeTrees(vendorStyles, { + annotation: 'TreeMerger (vendorStyles)', + overwrite: true, + }); + + if (cssMinificationEnabled === true) { + options.minifyCSS.registry = options.registry; + preprocessedStyles = preprocessMinifyCss(preprocessedStyles, options.minifyCSS); + vendorStyles = preprocessMinifyCss(vendorStyles, options.minifyCSS); + } + + this._cachedProcessedStyles = callAddonsPostprocessTreeHook( + this.project, + 'css', + mergeTrees([preprocessedStyles, vendorStyles], { + annotation: 'Packaged Styles', + }) + ); + } + + return this._cachedProcessedStyles; + } + + /* + * Given an input tree, returns a properly assembled Broccoli tree with vendor + * files. + * + * Given a tree: + * + * ``` + * ├── babel-polyfill/ + * ├── ember-cli-shims/ + * ├── ember-load-initializers/ + * ├── ember-qunit/ + * ├── ember-resolver/ + * ├── sinon/ + * └── tether/ + * ``` + * + * Returns: + * + * ``` + * vendor/ + * ├── babel-polyfill/ + * ├── ember-cli-shims/ + * ├── ember-load-initializers/ + * ├── ember-qunit/ + * ├── ember-resolver/ + * ├── sinon/ + * └── tether/ + * ``` + * + * @private + * @method packageVendor + * @param {BroccoliTree} tree + */ + packageVendor(tree) { + if (this._cachedVendor === null) { + this._cachedVendor = new Funnel(tree, { + destDir: DEFAULT_VENDOR_PATH, + annotation: 'Packaged Vendor', + }); + } + + return this._cachedVendor; + } + + /* + * Given an input tree, returns a properly assembled Broccoli tree with tests + * files. + * + * Given a tree: + * + * ``` + * addon-tree-output/ + * the-best-app-ever/ + * tests/ + * ├── acceptance/ + * ├── helpers/ + * ├── index.html + * ├── integration/ + * ├── test-helper.js + * └── unit/ + * ``` + * + * Returns: + * + * ``` + * [name]/ + * └── tests + * ├── acceptance/ + * ├── helpers/ + * ├── index.html + * ├── integration/ + * ├── test-helper.js + * └── unit/ + * ``` + * + * @private + * @method processTests + * @param {BroccoliTree} tree + */ + processTests(tree) { + if (this._cachedTests === null) { + let addonTestSupportTree = new Funnel(tree, { + srcDir: 'tests/addon-test-support', + destDir: 'addon-test-support', + }); + + let testTree = new Funnel(tree, { + srcDir: 'tests', + exclude: ['addon-test-support/**/*'], + }); + + let treeToCompile = new Funnel(testTree, { + destDir: `${this.name}/tests`, + annotation: 'Tests To Process', + }); + + treeToCompile = callAddonsPreprocessTreeHook(this.project, 'test', treeToCompile); + + const inputPath = '/tests'; + let preprocessedTests = preprocessJs(treeToCompile, inputPath, this.name, { + registry: this.registry, + treeType: 'test', + }); + + let mergedTestTrees = mergeTrees([addonTestSupportTree, preprocessedTests], { + overwrite: true, + annotation: 'Packaged Tests', + }); + + this._cachedTests = callAddonsPostprocessTreeHook(this.project, 'test', mergedTestTrees); + } + + return this._cachedTests; + } + + /* + * Concatenates all test files into one, as follows: + * + * Given an input tree that looks like: + * + * ``` + * addon-tree-output/ + * the-best-app-ever/ + * tests/ + * ├── acceptance/ + * ├── helpers/ + * ├── index.html + * ├── integration/ + * ├── test-helper.js + * └── unit/ + * vendor/ + * ``` + * + * Returns: + * + * ``` + * assets/ + * tests.js + * test.map (if sourcemaps are enabled) + * test-support.js + * test-support.map (if sourcemaps are enabled) + * test-support.css + * ``` + * + * @private + * @method packageTests + * @param {BroccoliTree} + * @return {BroccoliTree} + */ + packageTests(tree) { + let coreTestTree = this.processTests(tree); + + let testIndex = this.processTestIndex(tree); + let appTestTree = this.packageApplicationTests(coreTestTree); + let testFilesTree = this.packageTestFiles(tree, coreTestTree); + + return mergeTrees([testIndex, appTestTree, testFilesTree], { + annotation: 'Packaged Tests', + }); + } + + /* + * Replaces variables in `tests/index.html` file with values from + * `config/environment.js` and returns a single tree that contains + * `index.html` file with populated values. + * + * Input tree: + * + * ``` + * / + * ├── addon-tree-output/ + * ├── the-best-app-ever/ + * ├── tests/ + * └── vendor/ + * ``` + * + * Returns: + * + * ``` + * └── tests + * └── index.html/ + * ``` + * + * @private + * @method processTestIndex + * @param {BroccoliTree} + * @return {BroccoliTree} + */ + processTestIndex(tree) { + let index = new Funnel(tree, { + srcDir: '/tests', + files: ['index.html'], + destDir: '/tests', + annotation: 'Funnel (test index)', + }); + + let patterns = configReplacePatterns({ + addons: this.project.addons, + autoRun: this.autoRun, + storeConfigInMeta: this.storeConfigInMeta, + }); + + let configPath = path.join(this.name, 'config', 'environments', 'test.json'); + + return new ConfigReplace(index, this.packageConfig(), { + configPath, + files: ['tests/index.html'], + env: 'test', + patterns, + }); + } + + /* + * Wraps application configuration into AMD module: + * + * ```javascript + * define('the-best-app-ever/config/environment', [], function() { + * // read the meta tag that contains escaped configuration from + * // `index.html` and return as an object + * }); + * ``` + * + * Given a tree: + * + * ``` + * environments/ + * ├── development.json + * └── test.json + * ``` + * + * Returns: + * + * ``` + * └── vendor + * └── ember-cli + * └── app-config.js + * ``` + * @private + * @method packageTestApplicationConfig + */ + packageTestApplicationConfig() { + let files = ['app-config.js']; + let patterns = configReplacePatterns({ + addons: this.project.addons, + autoRun: this.autoRun, + storeConfigInMeta: this.storeConfigInMeta, + }); + + let configPath = path.join(this.name, 'config', 'environments', `test.json`); + let emberCLITree = new ConfigReplace(new UnwatchedDir(__dirname), this.packageConfig(), { + configPath, + files, + patterns, + }); + + return new Funnel(emberCLITree, { + files, + srcDir: '/', + destDir: '/vendor/ember-cli/', + annotation: 'Funnel (test-app-config-tree)', + }); + } + + /* + * Concatenates all application test files into one, as follows: + * + * Given an input tree that looks like: + * + * ``` + * addon-tree-output/ + * the-best-app-ever/ + * tests/ + * ├── acceptance/ + * ├── helpers/ + * ├── index.html + * ├── integration/ + * ├── test-helper.js + * └── unit/ + * vendor/ + * ``` + * + * Returns: + * + * ``` + * assets/ + * tests.js + * test.map (if sourcemaps are enabled) + * ``` + * + * @private + * @method packageApplicationTests + * @param {BroccoliTree} + * @return {BroccoliTree} + */ + packageApplicationTests(tree) { + let appTestTrees = [] + .concat(this.packageEmberCliInternalFiles(), this.packageTestApplicationConfig(), tree) + .filter(Boolean); + + appTestTrees = mergeTrees(appTestTrees, { + overwrite: true, + annotation: 'TreeMerger (appTestTrees)', + }); + + return concat(appTestTrees, { + inputFiles: ['**/tests/**/*.js'], + headerFiles: ['vendor/ember-cli/tests-prefix.js'], + footerFiles: ['vendor/ember-cli/app-config.js', 'vendor/ember-cli/tests-suffix.js'], + outputFile: this.distPaths.testJsFile, + annotation: 'Concat: App Tests', + sourceMapConfig: this.sourcemaps, + }); + } + + /* + * Concatenates all test support files into one, as follows: + * + * Given an input tree that looks like: + * + * ``` + * addon-tree-output/ + * the-best-app-ever/ + * tests/ + * ├── acceptance/ + * ├── helpers/ + * ├── index.html + * ├── integration/ + * ├── test-helper.js + * └── unit/ + * vendor/ + * ``` + * + * Returns: + * + * ``` + * assets/ + * test-support.js + * test-support.map (if sourcemaps are enabled) + * test-support.css + * ``` + * + * @private + * @method packageTestFiles + * @return {BroccoliTree} + */ + packageTestFiles(tree, coreTestTree) { + let testSupportPath = this.distPaths.testSupportJsFile; + + testSupportPath = testSupportPath.testSupport || testSupportPath; + + let emberCLITree = this.packageEmberCliInternalFiles(); + + let headerFiles = [].concat('vendor/ember-cli/test-support-prefix.js', this.legacyTestFilesToAppend); + + let inputFiles = ['addon-test-support/**/*.js']; + + let footerFiles = ['vendor/ember-cli/test-support-suffix.js']; + + let external = this.applyCustomTransforms(tree); + + let baseMergedTree = mergeTrees([emberCLITree, tree, external, coreTestTree], { + overwrite: true, + }); + let testJs = concat(baseMergedTree, { + headerFiles, + inputFiles, + footerFiles, + outputFile: testSupportPath, + annotation: 'Concat: Test Support JS', + allowNone: true, + sourceMapConfig: this.sourcemaps, + }); + + let testemPath = path.join(__dirname, 'testem'); + testemPath = path.dirname(testemPath); + + let testemTree = new Funnel(new UnwatchedDir(testemPath), { + files: ['testem.js'], + annotation: 'Funnel (testem)', + }); + + let sourceTrees = [testemTree, testJs]; + + if (this.vendorTestStaticStyles.length > 0) { + sourceTrees.push( + concat(tree, { + headerFiles: this.vendorTestStaticStyles, + outputFile: this.distPaths.testSupportCssFile, + annotation: 'Concat: Test Support CSS', + sourceMapConfig: this.sourcemaps, + }) + ); + } + + return mergeTrees(sourceTrees, { + overwrite: true, + annotation: 'TreeMerger (testFiles)', + }); + } + + /* + * Returns a flattened input tree. + * + * Given a tree: + * + * ``` + * public + * ├── crossdomain.xml + * ├── ember-fetch + * ├── favicon.ico + * ├── images + * └── robots.txt + * ``` + * + * Returns: + * + * ``` + * ├── crossdomain.xml + * ├── ember-fetch + * ├── favicon.ico + * ├── images + * └── robots.txt + * ``` + * + * @private + * @method packagePublic + * @param {BroccoliTree} tree + * @return {BroccoliTree} + */ + packagePublic(tree) { + if (this._cachedPublic === null) { + this._cachedPublic = new Funnel(tree, { + srcDir: 'public', + destDir: '.', + }); + } + + return this._cachedPublic; + } + + /* + * Given an input tree, returns a properly assembled Broccoli tree with + * configuration files. + * + * Given a tree: + * + * ``` + * environments/ + * ├── development.json + * └── test.json + * ``` + * + * Returns: + * + * ``` + * └── [name] + * └── config + * └── environments + * ├── development.json + * └── test.json + * ``` + * @private + * @method packageConfig + */ + packageConfig() { + let env = this.env; + let name = this.name; + let project = this.project; + let configPath = this.project.configPath(); + + if (this._cachedConfig === null) { + let configTree = new ConfigLoader(path.dirname(configPath), { + env, + tests: this.areTestsEnabled || false, + project, + }); + + this._cachedConfig = new Funnel(configTree, { + destDir: `${name}/config`, + annotation: 'Packaged Config', + }); + } + + return this._cachedConfig; + } + + /* + * Concatenates all javascript Broccoli trees into one, as follows: + * + * Given an input tree that looks like: + * + * ``` + * addon-tree-output/ + * ember-ajax/ + * ember-data/ + * ember-engines/ + * ember-resolver/ + * ... + * the-best-app-ever/ + * components/ + * config/ + * helpers/ + * routes/ + * ... + * vendor/ + * ... + * babel-core/ + * ... + * broccoli-concat/ + * ... + * ember-cli-template-lint/ + * ... + * ``` + * + * Returns: + * + * ``` + * assets/ + * the-best-app-ever.js + * the-best-app-ever.map (if sourcemaps are enabled) + * vendor.js + * vendor.map (if sourcemaps are enabled) + * ``` + * + * @private + * @method packageJavascript + * @return {BroccoliTree} + */ + packageJavascript(tree) { + if (this._cachedJavascript === null) { + let applicationJs = this.processAppAndDependencies(tree); + + let vendorFilePath = this.distPaths.vendorJsFile; + this.scriptOutputFiles[vendorFilePath].unshift('vendor/ember-cli/vendor-prefix.js'); + + let appJs = this.packageApplicationJs(applicationJs); + let vendorJs = this.packageVendorJs(applicationJs); + + this._cachedJavascript = mergeTrees([appJs, vendorJs], { + overwrite: true, + annotation: 'Packaged Javascript', + }); + } + + return this._cachedJavascript; + } + + /* + * Concatenates all application's javascript Broccoli trees into one, as follows: + * + * Given an input tree that looks like: + * + * ``` + * addon-tree-output/ + * ember-ajax/ + * ember-data/ + * ember-engines/ + * ember-resolver/ + * ... + * the-best-app-ever/ + * components/ + * config/ + * helpers/ + * routes/ + * ... + * vendor/ + * ... + * babel-core/ + * ... + * broccoli-concat/ + * ... + * ember-cli-template-lint/ + * ... + * ``` + * + * Returns: + * + * ``` + * assets/ + * the-best-app-ever.js + * the-best-app-ever.map (if sourcemaps are enabled) + * ``` + * + * @private + * @method packageApplicationJs + * @return {BroccoliTree} + */ + packageApplicationJs(tree) { + let inputFiles = [`${this.name}/**/*.js`]; + let headerFiles = ['vendor/ember-cli/app-prefix.js']; + let footerFiles = [ + 'vendor/ember-cli/app-suffix.js', + 'vendor/ember-cli/app-config.js', + 'vendor/ember-cli/app-boot.js', + ]; + + return concat(tree, { + inputFiles, + headerFiles, + footerFiles, + outputFile: this.distPaths.appJsFile, + annotation: 'Packaged Application Javascript', + separator: '\n;', + sourceMapConfig: this.sourcemaps, + }); + } + + /* + * Concatenates all application's vendor javascript Broccoli trees into one, as follows: + * + * Given an input tree that looks like: + * ``` + * addon-tree-output/ + * ember-ajax/ + * ember-data/ + * ember-engines/ + * ember-resolver/ + * ... + * the-best-app-ever/ + * components/ + * config/ + * helpers/ + * routes/ + * ... + * vendor/ + * ... + * babel-core/ + * ... + * broccoli-concat/ + * ... + * ember-cli-template-lint/ + * ... + * ``` + * + * Returns: + * + * ``` + * assets/ + * vendor.js + * vendor.map (if sourcemaps are enabled) + * ``` + * + * @method packageVendorJs + * @param {BroccoliTree} tree + * @return {BroccoliTree} + */ + packageVendorJs(tree) { + let importPaths = Object.keys(this.scriptOutputFiles); + + // iterate over the keys and concat files + // to support scenarios like + // app.import('vendor/foobar.js', { outputFile: 'assets/baz.js' }); + let vendorTrees = importPaths.map((importPath) => { + let files = this.scriptOutputFiles[importPath]; + let isMainVendorFile = importPath === this.distPaths.vendorJsFile; + + const vendorObject = getVendorFiles(files, isMainVendorFile); + + return concat(tree, { + inputFiles: vendorObject.inputFiles, + headerFiles: vendorObject.headerFiles, + footerFiles: vendorObject.footerFiles, + outputFile: importPath, + annotation: `Package ${importPath}`, + separator: '\n;', + sourceMapConfig: this.sourcemaps, + }); + }); + + return mergeTrees(vendorTrees, { + overwrite: true, + annotation: 'Packaged Vendor Javascript', + }); + } +}; diff --git a/lib/broccoli/ember-addon.js b/lib/broccoli/ember-addon.js index 8f9e63b235..277555dde4 100644 --- a/lib/broccoli/ember-addon.js +++ b/lib/broccoli/ember-addon.js @@ -1,60 +1,61 @@ -/* global require, module */ 'use strict'; /** @module ember-cli */ -var defaults = require('merge-defaults'); -var merge = require('lodash/object/merge'); -var Funnel = require('broccoli-funnel'); -var EmberApp = require('./ember-app'); - -module.exports = EmberAddon; - -/** - EmberAddon is used during addon development. - - @class EmberAddon - @extends EmberApp - @constructor - @param options -*/ -function EmberAddon() { - var args = []; - var options = {}; - - for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); - } - - if (args.length === 1) { - options = args[0]; - } else if (args.length > 1) { - args.reverse(); - options = defaults.apply(null, args); +const defaultsDeep = require('lodash/defaultsDeep'); +const Funnel = require('broccoli-funnel'); +const fs = require('fs'); + +const EmberApp = require('./ember-app'); + +class EmberAddon extends EmberApp { + /** + EmberAddon is used during addon development. + + @class EmberAddon + @extends EmberApp + @constructor + @param {Object} [defaults] + @param {Object} [options={}] Configuration options + */ + constructor(defaults, options) { + if (arguments.length === 0) { + options = {}; + } else if (arguments.length === 1) { + options = defaults; + } else { + defaultsDeep(options, defaults); + } + + process.env.EMBER_ADDON_ENV = process.env.EMBER_ADDON_ENV || 'development'; + let overrides = { + name: 'dummy', + configPath: './tests/dummy/config/environment', + trees: { + app: 'tests/dummy/app', + public: 'tests/dummy/public', + styles: 'tests/dummy/app/styles', + templates: 'tests/dummy/app/templates', + tests: new Funnel('tests', { + exclude: [/^dummy/], + }), + vendor: null, + }, + }; + + if (!fs.existsSync('tests/dummy/app')) { + overrides.trees.app = null; + overrides.trees.styles = null; + overrides.trees.templates = null; + } + + if (fs.existsSync('tests/dummy/vendor')) { + overrides.trees.vendor = 'tests/dummy/vendor'; + } + + super(defaultsDeep(options, overrides)); } - - process.env.EMBER_ADDON_ENV = process.env.EMBER_ADDON_ENV || 'development'; - - this.appConstructor(merge(options, { - name: 'dummy', - configPath: './tests/dummy/config/environment', - trees: { - app: 'tests/dummy/app', - styles: 'tests/dummy/app/styles', - templates: 'tests/dummy/app/templates', - public: 'tests/dummy/public', - tests: new Funnel('tests', { - exclude: [ /^dummy/ ] - }) - }, - jshintrc: { - tests: './tests', - app: './tests/dummy' - }, - }, defaults)); } -EmberAddon.prototype = Object.create(EmberApp.prototype); -EmberAddon.prototype.constructor = EmberAddon; -EmberAddon.prototype.appConstructor = EmberApp.prototype.constructor; +module.exports = EmberAddon; diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js index c07cfb7285..0de6c2e0d9 100644 --- a/lib/broccoli/ember-app.js +++ b/lib/broccoli/ember-app.js @@ -1,1473 +1,1407 @@ -/* global require, module, escape */ 'use strict'; /** @module ember-cli */ -var fs = require('fs'); -var existsSync = require('exists-sync'); -var path = require('path'); -var p = require('ember-cli-preprocess-registry/preprocessors'); -var chalk = require('chalk'); -var escapeRegExp = require('escape-string-regexp'); -var EOL = require('os').EOL; - -var Project = require('../models/project'); -var cleanBaseURL = require('clean-base-url'); -var SilentError = require('silent-error'); +const fs = require('fs'); +const path = require('path'); +const p = require('ember-cli-preprocess-registry/preprocessors'); +const { default: chalk } = require('chalk'); +const resolve = require('resolve'); + +const { assert } = require('../debug'); +const Project = require('../models/project'); + +const concat = require('broccoli-concat'); +const BroccoliDebug = require('broccoli-debug'); +const mergeTrees = require('./merge-trees'); +const broccoliMergeTrees = require('broccoli-merge-trees'); +const WatchedDir = require('broccoli-source').WatchedDir; +const UnwatchedDir = require('broccoli-source').UnwatchedDir; + +const merge = require('lodash/merge'); +const defaultsDeep = require('lodash/defaultsDeep'); +const omitBy = require('lodash/omitBy'); +const isNull = require('lodash/isNull'); +const Funnel = require('broccoli-funnel'); +const logger = require('heimdalljs-logger')('ember-cli:ember-app'); +const addonProcessTree = require('../utilities/addon-process-tree'); +const lintAddonsByType = require('../utilities/lint-addons-by-type'); +const DefaultPackager = require('./default-packager'); + +let DEFAULT_CONFIG = { + storeConfigInMeta: true, + autoRun: true, + minifyCSS: { + options: { relativeTo: 'assets' }, + }, + sourcemaps: {}, + trees: {}, + addons: {}, +}; -var preprocessJs = p.preprocessJs; -var preprocessCss = p.preprocessCss; -var isType = p.isType; +class EmberApp { + /** + EmberApp is the main class Ember CLI uses to manage the Broccoli trees + for your application. It is very tightly integrated with Broccoli and has + a `toTree()` method you can use to get the entire tree for your application. + + Available init options: + - storeConfigInMeta, defaults to `true` + - autoRun, defaults to `true` + - minifyCSS, defaults to `{enabled: !!isProduction,options: { relativeTo: 'assets' }} + - sourcemaps, defaults to `{}` + - trees, defaults to `{}` + - vendorFiles, defaults to `{}` + - addons, defaults to `{ exclude: [], include: [] }` + + @class EmberApp + @constructor + @param {Object} [defaults] + @param {Object} [options={}] Configuration options + */ + constructor(defaults, options) { + if (arguments.length === 0) { + options = {}; + } else if (arguments.length === 1) { + options = defaults; + } else { + defaultsDeep(options, defaults); + } -var preprocessTemplates = p.preprocessTemplates; + this._initProject(options); + this.name = options.name || this.project.name(); + + this.env = EmberApp.env(); + this.isProduction = this.env === 'production'; + + this.registry = options.registry || p.defaultRegistry(this); + + this._initTestsAndHinting(options); + this._initOptions(options); + this._initVendorFiles(); + + this._styleOutputFiles = {}; + + // ensure addon.css always gets concated + this._styleOutputFiles[this.options.outputPaths.vendor.css] = []; + + this._scriptOutputFiles = {}; + this._customTransformsMap = new Map(); + + this.otherAssetPaths = []; + this.legacyTestFilesToAppend = []; + this.vendorTestStaticStyles = []; + this._nodeModules = new Map(); + + this.trees = this.options.trees; + + this.populateLegacyFiles(); + this.initializeAddons(); + this.project.addons.forEach((addon) => (addon.app = this)); + p.setupRegistry(this); + this._importAddonTransforms(); + this._notifyAddonIncluded(); + + this._debugTree = BroccoliDebug.buildDebugCallback('ember-app'); + + this._defaultPackager = new DefaultPackager({ + env: this.env, + name: this.name, + autoRun: this.options.autoRun, + project: this.project, + registry: this.registry, + sourcemaps: this.options.sourcemaps, + minifyCSS: this.options.minifyCSS, + areTestsEnabled: this.tests, + styleOutputFiles: this._styleOutputFiles, + scriptOutputFiles: this._scriptOutputFiles, + storeConfigInMeta: this.options.storeConfigInMeta, + customTransformsMap: this._customTransformsMap, + additionalAssetPaths: this.otherAssetPaths, + vendorTestStaticStyles: this.vendorTestStaticStyles, + legacyTestFilesToAppend: this.legacyTestFilesToAppend, + distPaths: { + appJsFile: this.options.outputPaths.app.js, + appCssFile: this.options.outputPaths.app.css, + testJsFile: this.options.outputPaths.tests.js, + appHtmlFile: this.options.outputPaths.app.html, + vendorJsFile: this.options.outputPaths.vendor.js, + vendorCssFile: this.options.outputPaths.vendor.css, + testSupportJsFile: this.options.outputPaths.testSupport.js, + testSupportCssFile: this.options.outputPaths.testSupport.css, + }, + }); -var preprocessMinifyCss = p.preprocessMinifyCss; + this._cachedAddonBundles = {}; + + if (this.project.perBundleAddonCache && this.project.perBundleAddonCache.numProxies > 0) { + if (this.options.addons.include && this.options.addons.include.length) { + throw new Error( + [ + `[ember-cli] addon bundle caching is disabled for apps that specify an addon "include"`, + '', + 'All addons using bundle caching:', + ...this.project.perBundleAddonCache.getPathsToAddonsOptedIn(), + ].join('\n') + ); + } -var ES6Modules = require('broccoli-es6modules'); -var concatFilesWithSourcemaps = require('broccoli-sourcemap-concat'); + if (this.options.addons.exclude && this.options.addons.exclude.length) { + throw new Error( + [ + `[ember-cli] addon bundle caching is disabled for apps that specify an addon "exclude"`, + '', + 'All addons using bundle caching:', + ...this.project.perBundleAddonCache.getPathsToAddonsOptedIn(), + ].join('\n') + ); + } + } + } -var configLoader = require('./broccoli-config-loader'); -var configReplace = require('./broccoli-config-replace'); -var mergeTrees = require('./merge-trees'); -var WatchedDir = require('broccoli-source').WatchedDir; -var UnwatchedDir = require('broccoli-source').UnwatchedDir; + /** + Initializes the `tests` and `hinting` properties. -var defaults = require('merge-defaults'); -var merge = require('lodash/object/merge'); -var omit = require('lodash/object/omit'); -var ES3SafeFilter = require('broccoli-es3-safe-recast'); -var Funnel = require('broccoli-funnel'); + Defaults to `false` unless `ember test` was used or this is *not* a production build. -module.exports = EmberApp; + @private + @method _initTestsAndHinting + @param {Object} options + */ + _initTestsAndHinting(options) { + let testsEnabledDefault = process.env.EMBER_CLI_TEST_COMMAND === 'true' || !this.isProduction; -/** - EmberApp is the main class Ember CLI uses to manage the Brocolli trees - for your application. It is very tightly integrated with Brocolli and has - an `toTree()` method you can use to get the entire tree for your application. - - Available init options: - - es3Safe, defaults to `true`, - - storeConfigInMeta, defaults to `true`, - - autoRun, defaults to `true`, - - outputPaths, defaults to `{}`, - - minifyCSS, defaults to `{enabled: !!isProduction,options: { relativeTo: 'app/styles' }}, - - minifyJS, defaults to `{enabled: !!isProduction}, - - loader, defaults to this.bowerDirectory + '/loader.js/loader.js', - - sourcemaps, defaults to `{}`, - - trees, defaults to `{},` - - jshintrc, defaults to `{},` - - vendorFiles, defaults to `{}` - - @class EmberApp - @constructor - @param {Object} options Configuration options -*/ -function EmberApp() { - var args = []; - var options = {}; - - for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); - } - - if (args.length === 1) { - options = args[0]; - } else if (args.length > 1) { - args.reverse(); - options = defaults.apply(null, args); + this.tests = 'tests' in options ? options.tests : testsEnabledDefault; + this.hinting = 'hinting' in options ? options.hinting : testsEnabledDefault; } - this._initProject(options); + /** + Initializes the `project` property from `options.project` or the + closest Ember CLI project from the current working directory. - this.env = EmberApp.env(); - this.name = options.name || this.project.name(); + @private + @method _initProject + @param {Object} options + */ + _initProject(options) { + let app = this; - this.registry = options.registry || p.defaultRegistry(this); + this.project = options.project || Project.closestSync(process.cwd()); - this.bowerDirectory = this.project.bowerDirectory; + if (options.configPath) { + this.project.configPath = function () { + return app._resolveLocal(options.configPath); + }; + this.project.configCache.clear(); + } + } - var isProduction = this.env === 'production'; + /** + Initializes the `options` property from the `options` parameter and + a set of default values from Ember CLI. + + @private + @method _initOptions + @param {Object} options + */ + _initOptions(options) { + let resolvePathFor = (defaultPath, specified) => { + let path = defaultPath; + if (specified && typeof specified === 'string') { + path = specified; + } + let resolvedPath = this._resolveLocal(path); - this._initTestsAndHinting(options, isProduction); - this._initOptions(options, isProduction); - this._initVendorFiles(); + return resolvedPath; + }; - this.legacyFilesToAppend = []; - this.vendorStaticStyles = []; - this.otherAssetPaths = []; - this.legacyTestFilesToAppend = []; - this.vendorTestStaticStyles = []; + let buildTreeFor = (defaultPath, specified, shouldWatch) => { + if (specified !== null && specified !== undefined && typeof specified !== 'string') { + return specified; + } - this.trees = this.options.trees; + let tree = null; + let resolvedPath = resolvePathFor(defaultPath, specified); + if (fs.existsSync(resolvedPath)) { + if (shouldWatch !== false) { + tree = new WatchedDir(resolvedPath); + } else { + tree = new UnwatchedDir(resolvedPath); + } + } - this.populateLegacyFiles(); - p.setupRegistry(this); - this._notifyAddonIncluded(); -} + return tree; + }; + let trees = (options && options.trees) || {}; -/** - @private - @method _initTestsAndHinting - @param {Object} options - @param {Boolean} isProduction -*/ -EmberApp.prototype._initTestsAndHinting = function(options, isProduction) { - var testsEnabledDefault = process.env.EMBER_CLI_TEST_COMMAND || !isProduction; + let appTree = buildTreeFor('app', trees.app); + let testsTree = buildTreeFor('tests', trees.tests, options.tests); - this.tests = options.hasOwnProperty('tests') ? options.tests : testsEnabledDefault; - this.hinting = options.hasOwnProperty('hinting') ? options.hinting : testsEnabledDefault; -}; + // these are contained within app/ no need to watch again + // (we should probably have the builder or the watcher dedup though) + this._stylesPath = resolvePathFor('app/styles', trees.styles); -/** - @private - @method _initProject - @param {Object} options -*/ -EmberApp.prototype._initProject = function(options) { - this.project = options.project || Project.closestSync(process.cwd()); + let stylesTree = null; + if (fs.existsSync(this._stylesPath)) { + stylesTree = new UnwatchedDir(this._stylesPath); + } - if (options.configPath) { - this.project.configPath = function() { return options.configPath; }; - } -}; + let templatesTree = buildTreeFor('app/templates', trees.templates, false); + let vendorTree = buildTreeFor('vendor', trees.vendor); + let publicTree = buildTreeFor('public', trees.public); -/** - @private - @method _initOptions - @param {Object} options - @param {Boolean} isProduction -*/ -EmberApp.prototype._initOptions = function(options, isProduction) { - this.options = merge(options, { - es3Safe: true, - storeConfigInMeta: true, - autoRun: true, - outputPaths: {}, - minifyCSS: { - enabled: !!isProduction, - options: { relativeTo: 'app/styles' } - }, - minifyJS: { - enabled: !!isProduction, - }, - loader: this.bowerDirectory + '/loader.js/loader.js', - sourcemaps: {}, - trees: {}, - jshintrc: {}, - 'ember-cli-qunit': { - disableContainerStyles: false - } - }, defaults); - - // needs a deeper merge than is provided above - this.options.outputPaths = merge(this.options.outputPaths, { - app: { - html: 'index.html', - css: { - 'app': '/assets/' + this.name + '.css' + let detectedDefaultOptions = { + babel: {}, + minifyCSS: { + enabled: this.isProduction, + options: { processImport: false }, }, - js: '/assets/' + this.name + '.js' - }, - vendor: { - css: '/assets/vendor.css', - js: '/assets/vendor.js' - }, - testSupport: { - css: '/assets/test-support.css', - js: { - testSupport: '/assets/test-support.js', - testLoader: '/assets/test-loader.js' - } - } - }, defaults); - - this.options.sourcemaps = merge(this.options.sourcemaps, { - enabled: !isProduction, - extensions: ['js'] - }, defaults); + sourcemaps: { + enabled: !this.isProduction, + extensions: ['js'], + }, + trees: { + app: appTree, + tests: testsTree, + styles: stylesTree, + templates: templatesTree, + vendor: vendorTree, + public: publicTree, + }, + }; - this.options.trees = merge(this.options.trees, { - app: new WatchedDir('app'), - tests: new WatchedDir('tests'), + let emberCLIBabelInstance = this.project.findAddonByName('ember-cli-babel'); + if (emberCLIBabelInstance) { + detectedDefaultOptions['ember-cli-babel'] = detectedDefaultOptions['ember-cli-babel'] || {}; + detectedDefaultOptions['ember-cli-babel'].compileModules = true; + } - // these are contained within app/ no need to watch again - // (we should probably have the builder or the watcher dedup though) - styles: new UnwatchedDir('app/styles'), - templates: existsSync('app/templates') ? new UnwatchedDir('app/templates') : null, + this.options = defaultsDeep(options, detectedDefaultOptions, DEFAULT_CONFIG); + + // Keep `outputPaths` on `this.options` for now, because ember-auto-import reads it here: + // https://github.com/embroider-build/ember-auto-import/blob/ce42c052151ca39e74955212a963fcf3091d7c90/packages/ember-auto-import/ts/auto-import.ts#L84 + this.options.outputPaths = { + app: { + css: { + app: `/assets/${this.name}.css`, + }, + html: 'index.html', + js: `/assets/${this.name}.js`, + }, + tests: { + js: '/assets/tests.js', + }, + vendor: { + css: '/assets/vendor.css', + js: '/assets/vendor.js', + }, + testSupport: { + css: '/assets/test-support.css', + js: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + }, + }; - // do not watch vendor/ or bower's default directory by default - bower: this.project._watchmanInfo.enabled ? this.bowerDirectory : new UnwatchedDir(this.bowerDirectory), - vendor: existsSync('vendor') ? new UnwatchedDir('vendor') : null, + // For now we must disable Babel sourcemaps due to unforeseen + // performance regressions. + if (!('sourceMaps' in this.options.babel)) { + this.options.babel.sourceMaps = false; + } - public: existsSync('public') ? new WatchedDir('public') : null - }, defaults); + // Add testem.js to excludes for broccoli-asset-rev. + // This will allow tests to run against the production builds. + this.options.fingerprint = this.options.fingerprint || {}; + this.options.fingerprint.exclude = this.options.fingerprint.exclude || []; + this.options.fingerprint.exclude.push('testem'); + } - this.options.jshintrc = merge(this.options.jshintrc, { - app: this.project.root, - tests: path.join(this.project.root, 'tests'), - }, defaults); -}; + /** + Resolves a path relative to the project's root -/** - @private - @method _initVendorFiles -*/ -EmberApp.prototype._initVendorFiles = function() { - // in Ember 1.10 and higher `ember.js` is deprecated in favor of - // the more aptly named `ember.debug.js`. - var defaultDevelopmentEmber = this.bowerDirectory + '/ember/ember.debug.js'; - if (!existsSync(path.join(this.project.root, defaultDevelopmentEmber))) { - defaultDevelopmentEmber = this.bowerDirectory + '/ember/ember.js'; + @private + @method _resolveLocal + */ + _resolveLocal(to) { + return path.join(this.project.root, to); } - var handlebarsVendorFiles; - if ('handlebars' in this.project.bowerDependencies()) { - handlebarsVendorFiles = { - development: this.bowerDirectory + '/handlebars/handlebars.js', - production: this.bowerDirectory + '/handlebars/handlebars.runtime.js' - }; - } else { - handlebarsVendorFiles = null; + /** + @private + @method _initVendorFiles + */ + _initVendorFiles() { + let emberSource = this.project.findAddonByName('ember-source'); + + assert( + 'Could not find `ember-source`. Please install `ember-source` by running `ember install ember-source`.', + emberSource + ); + + this.vendorFiles = omitBy( + merge( + emberSource.paths + ? { + 'ember.js': { + development: emberSource.paths.debug, + production: emberSource.paths.prod, + }, + 'ember-testing.js': [emberSource.paths.testing, { type: 'test' }], + } + : {}, + this.options.vendorFiles + ), + isNull + ); } - this.vendorFiles = omit(merge({ - 'loader.js': this.options.loader, - 'jquery.js': this.bowerDirectory + '/jquery/dist/jquery.js', - 'handlebars.js': handlebarsVendorFiles, - 'ember.js': { - development: defaultDevelopmentEmber, - production: this.bowerDirectory + '/ember/ember.prod.js' - }, - 'ember-testing.js': [ - this.bowerDirectory + '/ember/ember-testing.js', - { type: 'test' } - ], - 'app-shims.js': [ - this.bowerDirectory + '/ember-cli-shims/app-shims.js', { - exports: { - ember: ['default'] - } - } - ], - 'ember-resolver.js': [ - this.bowerDirectory + '/ember-resolver/dist/modules/ember-resolver.js', { - exports: { - 'ember/resolver': ['default'] - } - } - ], - 'ember-load-initializers.js': [ - this.bowerDirectory + '/ember-load-initializers/ember-load-initializers.js', { - exports: { - 'ember/load-initializers': ['default'] - } - } - ] - }, this.options.vendorFiles), function(value) { - return value === null; - }); - - // this is needed to support versions of Ember older than - // 1.8.0 (when ember-testing.js was added to the deployment) - if (!existsSync(this.vendorFiles['ember-testing.js'][0])) { - delete this.vendorFiles['ember-testing.js']; + /** + Returns the environment name + + @public + @static + @method env + @return {String} Environment name + */ + static env() { + return process.env.EMBER_ENV || 'development'; } -}; -/** - Returns the environment name - - @public - @static - @method env - @return {String} Environment name - */ -EmberApp.env = function(){ - return process.env.EMBER_ENV || 'development'; -}; + /** + Delegates to `broccoli-concat` with the `sourceMapConfig` option set to `options.sourcemaps`. -/** - Provides a broccoli files concatenation filter that's configured - properly for this application. + @private + @method _concatFiles + @param tree + @param options + @return + */ + _concatFiles(tree, options) { + options.sourceMapConfig = this.options.sourcemaps; - @method concatFiles - @param tree - @param options - @return -*/ -EmberApp.prototype.concatFiles = function(tree, options) { - options.sourceMapConfig = this.options.sourcemaps; + return concat(tree, options); + } - return concatFilesWithSourcemaps(tree, options); -}; + /** + Checks the result of `addon.isEnabled()` if it exists, defaults to `true` otherwise. + @private + @method _addonEnabled + @param {Addon} addon + @return {Boolean} + */ + _addonEnabled(addon) { + return !addon.isEnabled || addon.isEnabled(); + } -/** - @private - @method _notifyAddonIncluded -*/ -EmberApp.prototype._notifyAddonIncluded = function() { - this.initializeAddons(); - this.project.addons = this.project.addons.filter(function(addon) { - addon.app = this; + /** + @private + @method _addonDisabledByExclude + @param {Addon} addon + @return {Boolean} + */ + _addonDisabledByExclude(addon) { + let exclude = this.options.addons.exclude; + return !!exclude && exclude.indexOf(addon.name) !== -1; + } - if (!addon.isEnabled || addon.isEnabled()) { - if (addon.included) { - addon.included(this); - } + /** + @private + @method _addonDisabledByInclude + @param {Addon} addon + @return {Boolean} + */ + _addonDisabledByInclude(addon) { + let include = this.options.addons.include; + return !!include && include.indexOf(addon.name) === -1; + } - return addon; + /** + Returns whether an addon should be added to the project + + @private + @method shouldIncludeAddon + @param {Addon} addon + @return {Boolean} + */ + shouldIncludeAddon(addon) { + if (!this._addonEnabled(addon)) { + return false; } - }, this); -}; -/** - Loads and initializes addons for this project. - Calls initializeAddons on the Project. + return !this._addonDisabledByExclude(addon) && !this._addonDisabledByInclude(addon); + } - @private - @method initializeAddons -*/ -EmberApp.prototype.initializeAddons = function() { - this.project.initializeAddons(); -}; + /** + Calls the included hook on addons. -/** - Returns a list of trees for a given type, returned by all addons. - - @private - @method addonTreesFor - @param {String} type Type of tree - @return {Array} List of trees - */ -EmberApp.prototype.addonTreesFor = function(type) { - return this.project.addons.map(function(addon) { - if (addon.treeFor) { - return addon.treeFor(type); - } - }).filter(Boolean); -}; - -/** - Runs addon postprocessing on a given tree and returns the processed tree. + @private + @method _notifyAddonIncluded + */ + _notifyAddonIncluded() { + let addonNames = this.project.addons.map((addon) => addon.name); - This enables addons to do process immediately **after** the preprocessor for a - given type is run, but before concatenation occurs. If an addon wishes to - apply a transform before the preprocessors run, they can instead implement the - preprocessTree hook. + if (this.options.addons.exclude) { + this.options.addons.exclude.forEach((addonName) => { + if (addonNames.indexOf(addonName) === -1) { + throw new Error(`Addon "${addonName}" defined in "exclude" is not found`); + } + }); + } - To utilize this addons implement `postprocessTree` hook. + if (this.options.addons.include) { + this.options.addons.include.forEach((addonName) => { + if (addonNames.indexOf(addonName) === -1) { + throw new Error(`Addon "${addonName}" defined in "include" is not found`); + } + }); + } - An example, would be to apply some broccoli transform on all JS files, but - only after the existing pre-processors have fun. + // the addons must be filtered before the `included` hook is called + // in case an addon caches the project.addons list + this.project.addons = this.project.addons.filter((addon) => this.shouldIncludeAddon(addon)); - ```js - module.exports = { - name: 'my-cool-addon', - postprocessTree: function(type, tree) { - if (type === 'js') { - return someBroccoliTransform(tree); + this.project.addons.forEach((addon) => { + if (addon.included) { + addon.included(this); } + }); + } - return tree; - } + /** + Calls the importTransforms hook on addons. + + @private + @method _importAddonTransforms + */ + _importAddonTransforms() { + this.project.addons.forEach((addon) => { + if (this.shouldIncludeAddon(addon)) { + if (addon.importTransforms) { + let transforms = addon.importTransforms(); + + if (!transforms) { + throw new Error(`Addon "${addon.name}" did not return a transform map from importTransforms function`); + } + + Object.keys(transforms).forEach((transformName) => { + let transformConfig = { + files: [], + options: {}, + }; + + // store the transform info + if (typeof transforms[transformName] === 'object') { + transformConfig['callback'] = transforms[transformName].transform; + transformConfig['processOptions'] = transforms[transformName].processOptions; + } else if (typeof transforms[transformName] === 'function') { + transformConfig['callback'] = transforms[transformName]; + transformConfig['processOptions'] = (assetPath, entry, options) => options; + } else { + throw new Error( + `Addon "${addon.name}" did not return a callback function correctly for transform "${transformName}".` + ); + } + + if (this._customTransformsMap.has(transformName)) { + // there is already a transform with a same name, therefore we warn the user + this.project.ui.writeWarnLine( + `Addon "${addon.name}" is defining a transform name: ${transformName} that is already being defined. Using transform from addon: "${addon.name}".` + ); + } + + this._customTransformsMap.set(transformName, transformConfig); + }); + } + } + }); } - ``` + /** + Loads and initializes addons for this project. + Calls initializeAddons on the Project. - @private - @method addonPostprocessTree - @param {String} type Type of tree - @param {Tree} tree Tree to process - @return {Tree} Processed tree - */ -EmberApp.prototype.addonPostprocessTree = function(type, tree) { - var workingTree = tree; + @private + @method initializeAddons + */ + initializeAddons() { + this.project.initializeAddons(); + } - this.project.addons.forEach(function(addon) { - if (addon.postprocessTree) { - workingTree = addon.postprocessTree(type, workingTree); - } - }); + _addonTreesFor(type) { + return this.project.addons.reduce((sum, addon) => { + if (addon.treeFor) { + let tree = addon.treeFor(type); + if (tree && !mergeTrees.isEmptyTree(tree)) { + sum.push({ + name: addon.name, + tree, + root: addon.root, + }); + } + } + return sum; + }, []); + } - return workingTree; -}; + /** + Returns a list of trees for a given type, returned by all addons. + @private + @method addonTreesFor + @param {String} type Type of tree + @return {Array} List of trees + */ + addonTreesFor(type) { + return this._addonTreesFor(type).map((addonBundle) => addonBundle.tree); + } -/** - Runs addon postprocessing on a given tree and returns the processed tree. + /** + Runs addon post-processing on a given tree and returns the processed tree. - This enables addons to do process immediately **before** the preprocessor for a - given type is run, but before concatenation occurs. If an addon wishes to - apply a transform after the preprocessors run, they can instead implement the - postprocessTree hoo. + This enables addons to do process immediately **after** the preprocessor for a + given type is run, but before concatenation occurs. If an addon wishes to + apply a transform before the preprocessors run, they can instead implement the + preprocessTree hook. - To utilize this addons implement `postprocessTree` hook. + To utilize this addons implement `postprocessTree` hook. - An example, would be to remove some set of files before the preprocessors run. + An example, would be to apply some broccoli transform on all JS files, but + only after the existing pre-processors have run. - ```js - var stew = require('broccoli-stew'); + ```js + module.exports = { + name: 'my-cool-addon', + postprocessTree(type, tree) { + if (type === 'js') { + return someBroccoliTransform(tree); + } - module.exports = { - name: 'my-cool-addon', - preprocessTree: function(type, tree) { - if (type === 'js' && type === 'template') { - return stew.rm(tree, someGlobPattern); + return tree; } - - return tree; - } - } - ``` - - @private - @method addonPreprocessTree - @param {String} type Type of tree - @param {Tree} tree Tree to process - @return {Tree} Processed tree - */ -EmberApp.prototype.addonPreprocessTree = function(type, tree) { - var workingTree = tree; - - this.project.addons.forEach(function(addon) { - if (addon.preprocessTree) { - workingTree = addon.preprocessTree(type, workingTree); - } - }); - - return workingTree; -}; - -/** - Runs addon lintTree hooks and returns a single tree containing all - their output. - - @private - @method addonLintTree - @param {String} type Type of tree - @param {Tree} tree Tree to process - @return {Tree} Processed tree - */ -EmberApp.prototype.addonLintTree = function(type, tree) { - var output = this.project.addons.map(function(addon) { - if (addon.lintTree) { - return addon.lintTree(type, tree); } - }).filter(Boolean); - return mergeTrees(output, { - overwrite: true, - annotation: 'TreeMerger (lint)' - }); -}; -/** - Imports legacy imports in this.vendorFiles - - @private - @method populateLegacyFiles -*/ -EmberApp.prototype.populateLegacyFiles = function () { - var name; - for (name in this.vendorFiles) { - var args = this.vendorFiles[name]; + ``` - if (args === null) { continue; } - - this.import.apply(this, [].concat(args)); + @private + @method addonPostprocessTree + @param {String} type Type of tree + @param {Tree} tree Tree to process + @return {Tree} Processed tree + */ + addonPostprocessTree(type, tree) { + return addonProcessTree(this.project, 'postprocessTree', type, tree); } -}; -/** - Returns the tree for app/index.html + /** + Runs addon pre-processing on a given tree and returns the processed tree. - @private - @method index - @return {Tree} Tree for app/index.html -*/ -EmberApp.prototype.index = function() { - var htmlName = this.options.outputPaths.app.html; - var files = [ - 'index.html' - ]; - - var index = new Funnel(this.trees.app, { - files: files, - getDestinationPath: function(relativePath) { - if (relativePath === 'index.html') { - relativePath = htmlName; - } - return relativePath; - }, - description: 'Funnel: index.html' - }); - - return configReplace(index, this._configTree(), { - configPath: path.join(this.name, 'config', 'environments', this.env + '.json'), - files: [ htmlName ], - patterns: this._configReplacePatterns() - }); -}; - -/** - @private - @method _filterAppTree - @return tree -*/ -EmberApp.prototype._filterAppTree = function() { - if (this._cachedFilterAppTree) { - return this._cachedFilterAppTree; - } + This enables addons to do process immediately **before** the preprocessor for a + given type is run. If an addon wishes to apply a transform after the + preprocessors run, they can instead implement the postprocessTree hook. - var podPatterns = this._podTemplatePatterns(); - var excludePatterns = podPatterns.concat([ - // note: do not use path.sep here Funnel uses - // walk-sync which always joins with `/` (not path.sep) - 'styles/**/*', - 'templates/**/*', - ]); - - return this._cachedFilterAppTree = new Funnel(this.trees.app, { - exclude: excludePatterns, - description: 'Funnel: Filtered App' - }); -}; + To utilize this addons implement `preprocessTree` hook. -/** - @private - @method _configReplacePatterns - @return -*/ -EmberApp.prototype._configReplacePatterns = function() { - return [{ - match: /\{\{EMBER_ENV\}\}/g, - replacement: calculateEmberENV - }, { - match: /\{\{content-for ['"](.+)["']\}\}/g, - replacement: this.contentFor.bind(this) - }, { - match: /\{\{MODULE_PREFIX\}\}/g, - replacement: calculateModulePrefix - }]; -}; - -/** - Returns the tree for /tests/index.html - - @private - @method testIndex - @return {Tree} Tree for /tests/index.html - */ -EmberApp.prototype.testIndex = function() { - var index = new Funnel(this.trees.tests, { - srcDir: '/', - files: ['index.html'], - destDir: '/tests' - }); - - return configReplace(index, this._configTree(), { - configPath: path.join(this.name, 'config', 'environments', 'test.json'), - files: [ 'tests/index.html' ], - env: 'test', - patterns: this._configReplacePatterns() - }); -}; + An example, would be to remove some set of files before the preprocessors run. -/** - Returns the tree for /public + ```js + var stew = require('broccoli-stew'); - @private - @method publicTree - @return {Tree} Tree for /public - */ -EmberApp.prototype.publicTree = function() { - var trees = this.addonTreesFor('public'); + module.exports = { + name: 'my-cool-addon', + preprocessTree(type, tree) { + if (type === 'js' && type === 'template') { + return stew.rm(tree, someGlobPattern); + } - if (this.trees.public) { - trees.push(this.trees.public); + return tree; + } + } + ``` + + @private + @method addonPreprocessTree + @param {String} type Type of tree + @param {Tree} tree Tree to process + @return {Tree} Processed tree + */ + addonPreprocessTree(type, tree) { + return addonProcessTree(this.project, 'preprocessTree', type, tree); } - return mergeTrees(trees, { - overwrite: true, - annotation: 'TreeMerge (public)' - }); -}; + /** + Runs addon lintTree hooks and returns a single tree containing all + their output. + @private + @method addonLintTree + @param {String} type Type of tree + @param {Tree} tree Tree to process + @return {Tree} Processed tree + */ + addonLintTree(type, tree) { + let output = lintAddonsByType(this.project.addons, type, tree); -/** - @private - @method _processedAppTree - @return -*/ -EmberApp.prototype._processedAppTree = function() { - var addonTrees = this.addonTreesFor('app'); - var mergedApp = mergeTrees(addonTrees.concat(this._filterAppTree()), { - overwrite: true, - annotation: 'TreeMerger (app)' - }); - - return new Funnel(mergedApp, { - srcDir: '/', - destDir: this.name, - annotation: 'ProcessedAppTree' - }); -}; + return mergeTrees(output, { + overwrite: true, + annotation: `TreeMerger (lint ${type})`, + }); + } -/** - @private - @method _processedTemplatesTree - @return -*/ -EmberApp.prototype._processedTemplatesTree = function() { - var addonTrees = this.addonTreesFor('templates'); - var mergedTrees = this.trees.templates ? addonTrees.concat(this.trees.templates) : addonTrees; - var mergedTemplates = mergeTrees(mergedTrees, { - overwrite: true, - annotation: 'TreeMerger (templates)' - }); - - var standardTemplates = new Funnel(mergedTemplates, { - srcDir: '/', - destDir: this.name + '/templates', - annotation: 'ProcessedTemplateTree' - }); - - var podTemplates = new Funnel(this.trees.app, { - include: this._podTemplatePatterns(), - exclude: [ 'templates/**/*' ], - destDir: this.name + '/', - annotation: 'Funnel: Pod Templates' - }); - - var templates = this.addonPreprocessTree('template', mergeTrees([ - standardTemplates, - podTemplates - ], { annotation: 'addonPreprocessTree(template)' })); - - return this.addonPostprocessTree('template', preprocessTemplates(templates, { - registry: this.registry, - description: 'TreeMerger (pod & standard templates)' - })); -}; + /** + Imports legacy imports in this.vendorFiles -/** - @private - @method _podTemplatePatterns - @returns Array An array of regular expressions. -*/ -EmberApp.prototype._podTemplatePatterns = function() { - return this.registry.extensionsForType('template').map(function(extension) { - return '**/*/template.' + extension; - }); -}; + @private + @method populateLegacyFiles + */ + populateLegacyFiles() { + let name; + for (name in this.vendorFiles) { + let args = this.vendorFiles[name]; -/** - @private - @method _processedTestsTree - @return -*/ -EmberApp.prototype._processedTestsTree = function() { - var addonTrees = this.addonTreesFor('test-support'); - var mergedTests = mergeTrees(addonTrees.concat(this.trees.tests), { - overwrite: true, - annotation: 'TreeMerger (tests)' - }); - - return new Funnel(mergedTests, { - srcDir: '/', - destDir: this.name + '/tests', - annotation: 'ProcessedTestTree' - }); -}; + if (args === null) { + continue; + } -/** - @private - @method _processedBowerTree - @return -*/ -EmberApp.prototype._processedBowerTree = function() { - if(this._cachedBowerTree) { - return this._cachedBowerTree; + this.import.apply(this, [].concat(args)); + } } - // do not attempt to merge bower and vendor together - // if they are the same tree - if (this.bowerDirectory === 'vendor') { - return; + podTemplates() { + return new Funnel(this.trees.app, { + include: this._podTemplatePatterns(), + exclude: ['templates/**/*'], + destDir: this.name, + annotation: 'Funnel: Pod Templates', + }); } - this._cachedBowerTree = new Funnel(this.trees.bower, { - srcDir: '/', - destDir: this.bowerDirectory + '/' - }); + _templatesTree() { + if (!this._cachedTemplateTree) { + let trees = []; + if (this.trees.templates) { + let standardTemplates = new Funnel(this.trees.templates, { + srcDir: '/', + destDir: `${this.name}/templates`, + annotation: 'Funnel: Templates', + }); + + trees.push(standardTemplates); + } - return this._cachedBowerTree; -}; + if (this.trees.app) { + trees.push(this.podTemplates()); + } -/** + this._cachedTemplateTree = mergeTrees(trees, { + annotation: 'TreeMerge (templates)', + }); + } -*/ -EmberApp.prototype._addonTree = function _addonTree() { - if (this._cachedAddonTree) { - return this._cachedAddonTree; + return this._cachedTemplateTree; } - var addonTrees = mergeTrees(this.addonTreesFor('addon'), { - overwrite: true, - annotation: 'TreeMerger (addons)' - }); - - var addonES6 = new Funnel(addonTrees, { - srcDir: 'modules', - allowEmpty: true, - description: 'Funnel: Addon JS' - }); - - // it is not currently possible to make Esperanto processing - // pre-existing AMD a no-op, so we have to remove the reexports - // to then merge them later :( - var addonReexports = new Funnel(addonTrees, { - srcDir: 'reexports', - allowEmpty: true, - description: 'Funnel: Addon Re-exports' - }); - - var transpiledAddonTree = new ES6Modules(addonES6, { - description: 'ES6: Addon Trees', - esperantoOptions: { - absolutePaths: true, - strict: true, - _evilES3SafeReExports: this.options.es3Safe - } - }); - - var reexportsAndTranspiledAddonTree = mergeTrees([ - transpiledAddonTree, - addonReexports - ], { - annotation: 'TreeMerger: (re-exports)' - }); - - return this._cachedAddonTree = [ - this.concatFiles(addonTrees, { - inputFiles: ['**/*.css'], - outputFile: '/addons.css', - allowNone: true, - description: 'Concat: Addon CSS' - }), - - this.concatFiles(reexportsAndTranspiledAddonTree, { - inputFiles: ['**/*.js'], - outputFile: '/addons.js', - allowNone: true, - description: 'Concat: Addon JS' - }) - ]; -}; + /* + * Gather application and add-ons javascript files and return them in a single + * tree. + * + * Resulting tree: + * + * ``` + * the-best-app-ever/ + * ├── adapters + * │ └── application.js + * ├── app.js + * ├── components + * ├── controllers + * ├── helpers + * │ ├── and.js + * │ ├── app-version.js + * │ ├── await.js + * │ ├── camelize.js + * │ ├── cancel-all.js + * │ ├── dasherize.js + * │ ├── dec.js + * │ ├── drop.js + * │ └── eq.js + * ... + * ``` + * + * Note, files in the example are "made up" and will differ from the real + * application. + * + * @private + * @method getAppJavascript + * @return {BroccoliTree} + */ + getAppJavascript() { + let appTrees = [].concat(this.addonTreesFor('app'), this.trees.app).filter(Boolean); + + let mergedApp = mergeTrees(appTrees, { + overwrite: true, + annotation: 'TreeMerger (app)', + }); -/** - @private - @method _processedVendorTree - @return -*/ -EmberApp.prototype._processedVendorTree = function() { - if(this._cachedVendorTree) { - return this._cachedVendorTree; + let appTree = new Funnel(mergedApp, { + srcDir: '/', + destDir: this.name, + annotation: 'ProcessedAppTree', + }); + + return appTree; } - var trees = this._addonTree(); - trees = trees.concat(this.addonTreesFor('vendor')); + /* + * Gather add-ons style (css/sass/less) files and return them in a single + * tree. + * + * Resulting tree: + * + * ``` + * the-best-app-ever/ + * └── app + * └── styles + * ├── ember-basic-dropdown.scss + * └── ember-power-select.scss + * ``` + * + * @private + * @method getStyles + * @return {BroccoliTree} + */ + getStyles() { + let styles; + if (this.trees.styles) { + styles = new Funnel(this.trees.styles, { + srcDir: '/', + destDir: '/app/styles', + annotation: 'Funnel (styles)', + }); + } + let addons = this.addonTreesFor('styles'); + + styles = mergeTrees(addons.concat(styles), { + overwrite: true, + annotation: 'Styles', + }); - if (this.trees.vendor) { - trees.push(this.trees.vendor); + return styles; } - var mergedVendor = mergeTrees(trees, { - overwrite: true, - annotation: 'TreeMerger (vendor)' - }); + /* + * Gather add-ons template files and return them in a single tree. + * + * Resulting tree: + * + * ``` + * the-best-app-ever/ + * └── templates + * ├── application.hbs + * ├── error.hbs + * ├── index.hbs + * └── loading.hbs + * ``` + * + * Note, files in the example are "made up" and will differ from the real + * application. + * + * @private + * @method getAddonTemplates + * @return {BroccoliTree} + */ + getAddonTemplates() { + let addonTrees = this.addonTreesFor('templates'); + let mergedTemplates = mergeTrees(addonTrees, { + overwrite: true, + annotation: 'TreeMerger (templates)', + }); - this._cachedVendorTree = new Funnel(mergedVendor, { - srcDir: '/', - destDir: 'vendor/' - }); + let addonTemplates = new Funnel(mergedTemplates, { + srcDir: '/', + destDir: `${this.name}/templates`, + annotation: 'ProcessedTemplateTree', + }); - return this._cachedVendorTree; -}; + return addonTemplates; + } -/** - @private - @method _processedExternalTree - @return -*/ -EmberApp.prototype._processedExternalTree = function() { - if (this._cachedExternalTree) { - return this._cachedExternalTree; + /** + @private + @method _podTemplatePatterns + @return {Array} An array of regular expressions. + */ + _podTemplatePatterns() { + return this.registry.extensionsForType('template').map((extension) => `**/*/template.${extension}`); } - var vendor = this._processedVendorTree(); - var bower = this._processedBowerTree(); + _nodeModuleTrees() { + if (!this._cachedNodeModuleTrees) { + this._cachedNodeModuleTrees = Array.from( + this._nodeModules.values(), + (module) => + new Funnel(module.path, { + srcDir: '/', + destDir: `node_modules/${module.name}/`, + annotation: `Funnel (node_modules/${module.name})`, + }) + ); + } - var trees = [vendor]; - if (bower) { - trees.unshift(bower); + return this._cachedNodeModuleTrees; } - return this._cachedExternalTree = mergeTrees(trees, { - annotation: 'TreeMerger (ExternalTree)' - }); -}; + _addonBundles(type) { + if (!this._cachedAddonBundles[type]) { + let addonBundles = this._addonTreesFor(type); -/** - @private - @method _configTree - @return -*/ -EmberApp.prototype._configTree = function() { - if (this._cachedConfigTree) { - return this._cachedConfigTree; + this._cachedAddonBundles[type] = addonBundles; + } + + return this._cachedAddonBundles[type]; } - var configPath = this.project.configPath(); - var configTree = configLoader(path.dirname(configPath), { - env: this.env, - tests: this.tests, - project: this.project - }); + /* + * @private + * @method @createAddonTree + */ + createAddonTree(type, outputDir, options) { + let addonBundles = this._addonBundles(type, options); + + let tree = mergeTrees( + addonBundles.map(({ tree }) => tree), + { + overwrite: true, + annotation: `TreeMerger (${type})`, + } + ); - this._cachedConfigTree = new Funnel(configTree, { - srcDir: '/', - destDir: this.name + '/config' - }); + return new Funnel(tree, { + destDir: outputDir, + annotation: `Funnel: ${outputDir} ${type}`, + }); + } - return this._cachedConfigTree; -}; + addonTree() { + if (!this._cachedAddonTree) { + this._cachedAddonTree = this.createAddonTree('addon', 'addon-tree-output'); + } -/** - @private - @method _processedEmberCLITree - @return -*/ -EmberApp.prototype._processedEmberCLITree = function() { - if (this._cachedEmberCLITree) { - return this._cachedEmberCLITree; + return this._cachedAddonTree; } - var files = [ - 'vendor-prefix.js', - 'vendor-suffix.js', - 'app-prefix.js', - 'app-suffix.js', - 'app-boot.js', - 'test-support-prefix.js', - 'test-support-suffix.js' - ]; - var emberCLITree = configReplace(new UnwatchedDir(__dirname), this._configTree(), { - configPath: path.join(this.name, 'config', 'environments', this.env + '.json'), - files: files, - - patterns: this._configReplacePatterns() - }); - - return this._cachedEmberCLITree = new Funnel(emberCLITree, { - files: files, - srcDir: '/', - destDir: '/vendor/ember-cli/' - }); -}; + addonTestSupportTree() { + if (!this._cachedAddonTestSupportTree) { + this._cachedAddonTestSupportTree = this.createAddonTree('addon-test-support', 'addon-test-support'); + } -/** - Returns the tree for the app and its dependencies + return this._cachedAddonTestSupportTree; + } - @private - @method appAndDependencies - @return {Tree} Merged tree -*/ -EmberApp.prototype.appAndDependencies = function() { - var app = this.addonPreprocessTree('js', this._processedAppTree()); - var templates = this._processedTemplatesTree(); - var config = this._configTree(); + /* + * Gather all dependencies external to `ember-cli`, namely: + * + * + app `vendor` files + * + add-ons' `vendor` files + * + node modules + * + * Resulting tree: + * + * ``` + * / + * ├── addon-tree-output/ + * └── vendor/ + * ``` + * + * @private + * @method getExternalTree + * @return {BroccoliTree} + */ + getExternalTree() { + if (!this._cachedExternalTree) { + let vendorTrees = this.addonTreesFor('vendor'); + + vendorTrees.push(this.trees.vendor); + + let vendor = this._defaultPackager.packageVendor( + mergeTrees(vendorTrees, { + overwrite: true, + annotation: 'TreeMerger (vendor)', + }) + ); + + let addons = this.addonTree(); + let trees = [vendor].concat(addons); + + trees = this._nodeModuleTrees().concat(trees); + + this._cachedExternalTree = mergeTrees(trees, { + annotation: 'TreeMerger (ExternalTree)', + overwrite: true, + }); + } - if (!this.registry.availablePlugins['ember-cli-babel'] && this.options.es3Safe) { - app = new ES3SafeFilter(app); + return this._cachedExternalTree; } - var external = this._processedExternalTree(); - var preprocessedApp = preprocessJs(app, '/', this.name, { - registry: this.registry - }); + /* + * Gather all tests under `tests` folder. + * + * Resulting tree: + * + * ``` + * / + * └── tests/ + * ├── acceptance/ + * ├── addon-test-support/ + * ├── helpers/ + * ├── integration/ + * ├── lint/ + * ├── unit/ + * ├── index.html + * └── test-helper.js + * ``` + * + * @private + * @method getTests + * @return {BroccoliTree} + */ + getTests() { + let addonTrees = this.addonTreesFor('test-support'); - var postprocessedApp = this.addonPostprocessTree('js', preprocessedApp); - var sourceTrees = [ - external, - postprocessedApp, - templates, - config - ]; - - this._addAppTests(sourceTrees); + if (this.hinting) { + addonTrees.push(this.getLintTests()); + } - var emberCLITree = this._processedEmberCLITree(); + let addonTestSupportFiles = this.addonTestSupportTree(); + let allTests = mergeTrees(addonTrees.concat(this.trees.tests, addonTestSupportFiles), { + overwrite: true, + annotation: 'TreeMerger (tests)', + }); - sourceTrees.push(emberCLITree); + return new Funnel(allTests, { + destDir: 'tests', + }); + } - return mergeTrees(sourceTrees, { - overwrite: true, - description: 'TreeMerger (appAndDependencies)' - }); -}; + /* + * Merges both application and add-ons public files and returns them in a + * single tree. + * + * Given a tree: + * + * ``` + * ├── 500.html + * ├── images + * ├── maintenance.html + * └── robots.txt + * ``` + * + * And add-on tree: + * + * ``` + * ember-fetch/ + * └── fastboot-fetch.js + * ``` + * + * Returns: + * + * ``` + * ├── 500.html + * ├── ember-fetch + * │ └── fastboot-fetch.js + * ├── images + * ├── maintenance.html + * └── robots.txt + * ``` + * + * @private + * @method getPublic + * @return {BroccoliTree} + */ + getPublic() { + let addonPublicTrees = this.addonTreesFor('public'); + addonPublicTrees = addonPublicTrees.concat(this.trees.public); + + let mergedPublicTrees = mergeTrees(addonPublicTrees, { + annotation: 'Public', + overwrite: true, + }); -/** - @private - @method _addAppTests - @param {Array} sourceTrees -*/ -EmberApp.prototype._addAppTests = function(sourceTrees) { - if (this.tests) { - var tests = this.addonPreprocessTree('test', this._processedTestsTree()); - var preprocessedTests = preprocessJs(tests, '/tests', this.name, { - registry: this.registry + return new Funnel(mergedPublicTrees, { + destDir: 'public', }); + } - sourceTrees.push(this.addonPostprocessTree('test', preprocessedTests)); + /** + Runs the `app`, `tests` and `templates` trees through the chain of addons that produces lint trees. - if (this.hinting) { - var jshintedApp = this.addonLintTree('app', this._filterAppTree()); - var jshintedTests = this.addonLintTree('tests', this.trees.tests); + Those lint trees are afterwards funneled into the `tests` folder, babel-ified and returned as an array. - jshintedApp = new Funnel(jshintedApp, { - srcDir: '/', - destDir: this.name + '/tests/' - }); + @private + @method getLintTests + @return {Array} + */ + getLintTests() { + let lintTrees = []; - jshintedTests = new Funnel(jshintedTests, { - srcDir: '/', - destDir: this.name + '/tests/' + if (this.trees.app) { + let lintedApp = this.addonLintTree('app', this.trees.app); + lintedApp = new Funnel(lintedApp, { + destDir: 'lint', + annotation: 'Funnel (lint app)', }); - sourceTrees.push(jshintedApp); - sourceTrees.push(jshintedTests); + lintTrees.push(lintedApp); } - } -}; -/** - Returns the tree for javascript files + let lintedTests = this.addonLintTree('tests', this.trees.tests); + let lintedTemplates = this.addonLintTree('templates', this._templatesTree()); - @private - @method javascript - @return {Tree} Merged tree -*/ -EmberApp.prototype.javascript = function() { - var applicationJs = this.appAndDependencies(); - var legacyFilesToAppend = this.legacyFilesToAppend; - var appOutputPath = this.options.outputPaths.app.js; - - var appJs = new ES6Modules( - new Funnel(applicationJs, { - include: [escapeRegExp(this.name + '/') + '**/*.js'], - description: 'Funnel: App JS Files' - }), - - { - description: 'ES6: App Tree', - esperantoOptions: { - absolutePaths: true, - strict: true, - _evilES3SafeReExports: this.options.es3Safe - } - } - ); - - appJs = mergeTrees([ - appJs, - this._processedEmberCLITree() - ], { - annotation: 'TreeMerger (appJS & processedEmberCLITree)' - }); - - appJs = this.concatFiles(appJs, { - inputFiles: [this.name + '/**/*.js'], - headerFiles: [ - 'vendor/ember-cli/app-prefix.js' - ], - footerFiles: [ - 'vendor/ember-cli/app-suffix.js', - 'vendor/ember-cli/app-boot.js' - ], - outputFile: appOutputPath, - description: 'Concat: App' - }); - - var inputFiles = ['vendor/ember-cli/vendor-prefix.js'] - .concat(legacyFilesToAppend) - .concat('vendor/addons.js') - .concat('vendor/ember-cli/vendor-suffix.js'); - - var vendor = this.concatFiles(applicationJs, { - inputFiles: inputFiles, - outputFile: this.options.outputPaths.vendor.js, - separator: EOL + ';', - description: 'Concat: Vendor' - }); - - return mergeTrees([ - vendor, - appJs - ], { - annotation: 'TreeMerger (vendor & appJS)' + lintedTests = new Funnel(lintedTests, { + destDir: 'lint', + annotation: 'Funnel (lint tests)', }); -}; - - -/** - Returns the tree for styles - - @private - @method styles - @return {Tree} Merged tree for styles -*/ -EmberApp.prototype.styles = function() { - if (this._processedStylesTree) { - return this._processedStylesTree; - } - if (existsSync('app/styles/' + this.name + '.css')) { - throw new SilentError('Style file cannot have the name of the application - ' + this.name); - } - - var addonTrees = this.addonTreesFor('styles'); - var external = this._processedExternalTree(); - var styles = new Funnel(this.trees.styles, { - srcDir: '/', - destDir: '/app/styles' - }); - - var trees = [external].concat(addonTrees); - trees.push(styles); - - var stylesAndVendor = this.addonPreprocessTree('css', mergeTrees(trees, { - annotation: 'TreeMerger (stylesAndVendor)', - overwrite: true - })); - - var options = { outputPaths: this.options.outputPaths.app.css }; - options.registry = this.registry; - var preprocessedStyles = preprocessCss(stylesAndVendor, '/app/styles', '/assets', options); - - - var vendorStyles = this.addonPreprocessTree('css', this.concatFiles(stylesAndVendor, { - inputFiles: this.vendorStaticStyles.concat(['vendor/addons.css']), - outputFile: this.options.outputPaths.vendor.css, - description: 'Concat: Vendor Styles' - })); - - if (this.options.minifyCSS.enabled === true) { - options = this.options.minifyCSS.options || {}; - options.registry = this.registry; - preprocessedStyles = preprocessMinifyCss(preprocessedStyles, options); - vendorStyles = preprocessMinifyCss(vendorStyles, options); - } - - var mergedTrees = mergeTrees([ - preprocessedStyles, - vendorStyles - ], { - annotation: 'styles' + lintedTemplates = new Funnel(lintedTemplates, { + destDir: 'lint', + annotation: 'Funnel (lint templates)', }); - return this._processedStylesTree = this.addonPostprocessTree('css', mergedTrees); -}; - -/** - Returns the tree for test files - - @private - @method testFiles - @return {Tree} Merged tree for test files - */ -EmberApp.prototype.testFiles = function() { - var testSupportPath = this.options.outputPaths.testSupport.js; - var testLoaderPath = this.options.outputPaths.testSupport.js.testLoader; - - testSupportPath = testSupportPath.testSupport || testSupportPath; - - var external = this._processedExternalTree(); - - var emberCLITree = this._processedEmberCLITree(); - - var testJs = this.concatFiles(external, { - inputFiles: this.legacyTestFilesToAppend, - outputFile: testSupportPath, - description: 'Concat: Test Support JS' - }); - - testJs = this.concatFiles(mergeTrees([testJs, emberCLITree]), { - inputFiles: [ - 'vendor/ember-cli/test-support-prefix.js', - testSupportPath.slice(1), - 'vendor/ember-cli/test-support-suffix.js' - ], - outputFile: testSupportPath, - annotation: 'Concat: Test Support Suffix' - }); - - var testCss = this.concatFiles(external, { - inputFiles: this.vendorTestStaticStyles, - outputFile: this.options.outputPaths.testSupport.css, - description: 'Concat: Test Support CSS' - }); - - var testemPath = path.join(__dirname, 'testem'); - testemPath = path.dirname(testemPath); - - var testemTree = new Funnel(new UnwatchedDir(testemPath), { - files: ['testem.js'], - srcDir: '/', - destDir: '/' + return mergeTrees([lintedTests, lintedTemplates].concat(lintTrees), { + overwrite: true, }); - - if (this.options.fingerprint && this.options.fingerprint.exclude) { - this.options.fingerprint.exclude.push('testem'); } - var testLoader = new Funnel(external, { - files: ['test-loader.js'], - srcDir: '/' + this.bowerDirectory + '/ember-cli-test-loader', - destDir: path.dirname(testLoaderPath) - }); - - var sourceTrees = [ - testJs, - testCss, - testLoader, - testemTree - ]; + /** + @public + @method dependencies + @return {Object} Alias to the project's dependencies function + */ + dependencies(pkg) { + return this.project.dependencies(pkg); + } - return mergeTrees(sourceTrees, { - overwrite: true, - annotation: 'TreeMerger (testFiles)' - }); -}; + /** + Imports an asset into the application. + + @public + @method import + @param {Object|String} asset Either a path to the asset or an object with environment names and paths as key-value pairs. + @param {Object} [options] Options object + @param {String} [options.type='vendor'] Either 'vendor' or 'test' + @param {Boolean} [options.prepend=false] Whether or not this asset should be prepended + @param {String} [options.destDir] Destination directory, defaults to the name of the directory the asset is in + @param {String} [options.outputFile] Specifies the output file for given import. Defaults to assets/vendor.{js,css} + @param {Array} [options.using] Specifies the array of transformations to be done on the asset. Can do an amd shim and/or custom transformation + */ + import(asset, options) { + let assetPath = this._getAssetPath(asset); + + if (!assetPath) { + return; + } -/** - Returns the tree for the additional assets which are not in - one of the default trees. - - @private - @method otherAssets - @return {Tree} Merged tree for other assets - */ -EmberApp.prototype.otherAssets = function() { - var external = this._processedExternalTree(); - var otherAssetTrees = this.otherAssetPaths.map(function (path) { - return new Funnel(external, { - srcDir: path.src, - files: [path.file], - destDir: path.dest + options = defaultsDeep(options || {}, { + type: 'vendor', + prepend: false, }); - }); - return mergeTrees(otherAssetTrees, { - annotation: 'TreeMerger (otherAssetTrees)' - }); -}; - -/** - @public - @method dependencies - @return {Object} Alias to the project's dependencies function -*/ -EmberApp.prototype.dependencies = function(pkg) { - return this.project.dependencies(pkg); -}; -/** - Imports an asset into the application. - - Options: - - type - Either 'vendor' or 'test', defaults to 'vendor' - - prepend - Whether or not this asset should be prepended, defaults to false - - destDir - Destination directory, defaults to the name of the directory the asset is in - - @public - @method import - @param {(Object|String)} asset Either a path to the asset or an object with envirnoment names and paths as key-value pairs. - @param {Object=} options Options object - */ -EmberApp.prototype.import = function(asset, options) { - var assetPath = this._getAssetPath(asset); - - if (!assetPath) { - return; - } + let match = assetPath.match(/^node_modules\/((@[^/]+\/)?[^/]+)\//); + if (match !== null) { + let basedir = options.resolveFrom || this.project.root; + let name = match[1]; + let _path = path.dirname(resolve.sync(`${name}/package.json`, { basedir })); + this._nodeModules.set(_path, { name, path: _path }); + } - options = defaults(options || {}, { - type: 'vendor', - prepend: false - }); + let directory = path.dirname(assetPath); + let subdirectory = directory.replace(new RegExp(`^vendor/|node_modules/`), ''); + let extension = path.extname(assetPath); - var directory = path.dirname(assetPath); - var subdirectory = directory.replace(new RegExp('^vendor/|' + this.bowerDirectory), ''); - var extension = path.extname(assetPath); + if (!extension) { + throw new Error( + 'You must pass a file to `app.import`. For directories specify them to the constructor under the `trees` option.' + ); + } - if (!extension) { - throw new Error('You must pass a file to `app.import`. For directories specify them to the constructor under the `trees` option.'); + this._import(assetPath, options, directory, subdirectory, extension); } - this._import( - assetPath, - options, - directory, - subdirectory, - extension - ); -}; + /** + @private + @method _import + @param {String} assetPath + @param {Object} options + @param {String} directory + @param {String} subdirectory + @param {String} extension + */ + _import(assetPath, options, directory, subdirectory, extension) { + // TODO: refactor, this has gotten very messy. Relevant tests: tests/unit/broccoli/ember-app-test.js + let basename = path.basename(assetPath); + + if (p.isType(assetPath, 'js', { registry: this.registry })) { + if (options.using) { + if (!Array.isArray(options.using)) { + throw new Error('You must pass an array of transformations for `using` option'); + } + options.using.forEach((entry) => { + if (!entry.transformation) { + throw new Error( + `while importing ${assetPath}: each entry in the \`using\` list must have a \`transformation\` name` + ); + } + + let transformName = entry.transformation; + + if (!this._customTransformsMap.has(transformName)) { + let availableTransformNames = Array.from(this._customTransformsMap.keys()).join(','); + throw new Error( + `while import ${assetPath}: found an unknown transformation name ${transformName}. Available transformNames are: ${availableTransformNames}` + ); + } + + // process options for the transform and update the options + let customTransforms = this._customTransformsMap.get(transformName); + customTransforms.options = customTransforms.processOptions(assetPath, entry, customTransforms.options); + customTransforms.files.push(assetPath); + }); + } -/** - @private - @method _import - @param {String} assetPath - @param {Object} options - @param {String} directory - @param {String} subdirectory - @param {String} extension - */ -EmberApp.prototype._import = function(assetPath, options, directory, subdirectory, extension) { - var basename = path.basename(assetPath); - - if (isType(assetPath, 'js', {registry: this.registry})) { - if(options.type === 'vendor') { - if (options.prepend) { - this.legacyFilesToAppend.unshift(assetPath); + if (options.type === 'vendor') { + options.outputFile = options.outputFile || this.options.outputPaths.vendor.js; + addOutputFile('firstOneWins', this._scriptOutputFiles, assetPath, options); + } else if (options.type === 'test') { + if (!allowImport('firstOneWins', this.legacyTestFilesToAppend, assetPath, options)) { + return; + } + if (options.prepend) { + this.legacyTestFilesToAppend.unshift(assetPath); + } else { + this.legacyTestFilesToAppend.push(assetPath); + } } else { - this.legacyFilesToAppend.push(assetPath); + throw new Error( + `You must pass either \`vendor\` or \`test\` for options.type in your call to \`app.import\` for file: ${basename}` + ); + } + } else if (extension === '.css') { + if (options.type === 'vendor') { + options.outputFile = options.outputFile || this.options.outputPaths.vendor.css; + addOutputFile('lastOneWins', this._styleOutputFiles, assetPath, options); + } else { + if (!allowImport('lastOneWins', this.vendorTestStaticStyles, assetPath, options)) { + return; + } + if (options.prepend) { + this.vendorTestStaticStyles.unshift(assetPath); + } else { + this.vendorTestStaticStyles.push(assetPath); + } } - } else if (options.type === 'test' ) { - this.legacyTestFilesToAppend.push(assetPath); - } else { - throw new Error( 'You must pass either `vendor` or `test` for options.type in your call to `app.import` for file: '+basename ); - } - } else if (extension === '.css') { - if(options.type === 'vendor') { - this.vendorStaticStyles.push(assetPath); } else { - this.vendorTestStaticStyles.push(assetPath); - } - } else { - var destDir = options.destDir; - if (destDir === '') { - destDir = '/'; + let destDir = options.destDir; + if (destDir === '') { + destDir = '/'; + } + this.otherAssetPaths.push({ + src: directory, + file: basename, + dest: destDir || subdirectory, + }); } - this.otherAssetPaths.push({ - src: directory, - file: basename, - dest: destDir || subdirectory - }); } -}; -/** - @private - @method _getAssetPath - @param {(Object|String)} asset - @return {(String|undefined)} assetPath - */ -EmberApp.prototype._getAssetPath = function(asset) { - /** @type {String} */ - var assetPath; - - if (typeof asset === 'object') { - if (this.env in asset) { + /** + @private + @method _getAssetPath + @param {(Object|String)} asset + @return {(String|undefined)} assetPath + */ + _getAssetPath(asset) { + /* @type {String} */ + let assetPath; + + if (typeof asset !== 'object') { + assetPath = asset; + } else if (this.env in asset) { assetPath = asset[this.env]; } else { assetPath = asset.development; } - } else { - assetPath = asset; - } - if (!assetPath) { - return; - } + if (!assetPath) { + return; + } + + assetPath = assetPath.split('\\').join('/'); - assetPath = assetPath.replace(path.sep, '/'); + if (assetPath.split('/').length < 2) { + console.log( + chalk.red( + `Using \`app.import\` with a file in the root of \`vendor/\` causes a significant performance penalty. Please move \`${assetPath}\` into a subdirectory.` + ) + ); + } + + if (/[*,]/.test(assetPath)) { + throw new Error( + `You must pass a file path (without glob pattern) to \`app.import\`. path was: \`${assetPath}\`` + ); + } - if (assetPath.split('/').length < 2) { - console.log(chalk.red('Using `app.import` with a file in the root of `vendor/` causes a significant performance penalty. Please move `'+ assetPath + '` into a subdirectory.')); + return assetPath; } - if (/[\*\,]/.test(assetPath)) { - throw new Error('You must pass a file path (without glob pattern) to `app.import`. path was: `' + assetPath + '`'); + /** + Returns an array of trees for this application + + @private + @method toArray + @return {Array} An array of trees + */ + toArray() { + return [ + this.getAddonTemplates(), + this.getStyles(), + this.getTests(), + this.getExternalTree(), + this.getPublic(), + this.getAppJavascript(), + ].filter(Boolean); } - return assetPath; -}; + _legacyPackage(fullTree) { + let javascriptTree = this._defaultPackager.packageJavascript(fullTree); + let stylesTree = this._defaultPackager.packageStyles(fullTree); + let appIndex = this._defaultPackager.processIndex(fullTree); + let additionalAssets = this._defaultPackager.importAdditionalAssets(fullTree); + let publicTree = this._defaultPackager.packagePublic(fullTree); -/** - Returns an array of trees for this application - - @private - @method toArray - @return {Array} An array of trees - */ -EmberApp.prototype.toArray = function() { - var sourceTrees = [ - this.index(), - this.javascript(), - this.styles(), - this.otherAssets(), - this.publicTree() - ]; - - if (this.tests) { - sourceTrees = sourceTrees.concat(this.testIndex(), this.testFiles()); + let sourceTrees = [appIndex, javascriptTree, stylesTree, additionalAssets, publicTree].filter(Boolean); + + if (this.tests && this.trees.tests) { + sourceTrees.push(this._defaultPackager.packageTests(fullTree)); + } + + return mergeTrees(sourceTrees, { + overwrite: true, + annotation: 'Application Dist', + }); } - return sourceTrees; -}; + /** + Returns the merged tree for this application -/** - Returns the merged tree for this application - - @public - @method toTree - @param {Array} additionalTrees Array of additional trees to merge - @return {Tree} Merged tree for this application - */ -EmberApp.prototype.toTree = function(additionalTrees) { - var tree = mergeTrees(this.toArray().concat(additionalTrees || []), { - overwrite: true, - annotation: 'TreeMerger (allTrees)' - }); - - return this.addonPostprocessTree('all', tree); -}; + @public + @method toTree + @param {Array} [additionalTrees] Array of additional trees to merge + @return {Tree} Merged tree for this application + */ + toTree(additionalTrees) { + let packagedTree; -/** - Returns the content for a specific type (section) for index.html. - - Currently supported types: - - 'head' - - 'config-module' - - 'app' - - 'head-footer' - - 'test-header-footer' - - 'body-footer' - - 'test-body-footer' - - Addons can also implement this method and could also define additional - types (eg. 'some-addon-section'). - - @private - @method contentFor - @param {Object} config Application configuration - @param {RegExp} match Regular expression to match against - @param {String} type Type of content - @return {String} The content. - */ -EmberApp.prototype.contentFor = function(config, match, type) { - var content = []; - - switch (type) { - case 'head': this._contentForHead(content, config); break; - case 'config-module': this._contentForConfigModule(content, config); break; - case 'app-boot': this._contentForAppBoot(content, config); break; - } + let fullTree = mergeTrees(this.toArray(), { + overwrite: true, + annotation: 'Full Application', + }); - content = this.project.addons.reduce(function(content, addon) { - var addonContent = addon.contentFor ? addon.contentFor(type, config) : null; - if (addonContent) { - return content.concat(addonContent); + fullTree = this._debugTree(fullTree, 'prepackage'); + + if (!packagedTree) { + packagedTree = this._legacyPackage(fullTree); } - return content; - }, content); + let trees = [].concat(packagedTree, additionalTrees).filter(Boolean); + let combinedPackageTree = broccoliMergeTrees(trees); - return content.join('\n'); -}; + return this.addonPostprocessTree('all', combinedPackageTree); + } +} -/** - @private - @method _contentForHead - @param {Array} content - @param {Object} config -*/ -EmberApp.prototype._contentForHead = function(content, config) { - content.push(calculateBaseTag(config)); +module.exports = EmberApp; - if (this.options.storeConfigInMeta) { - content.push(''); - } -}; +function addOutputFile(strategy, container, assetPath, options) { + let outputFile = options.outputFile; -/** - @private - @method _contentForConfigModule - @param {Array} content - @param {Object} config -*/ -EmberApp.prototype._contentForConfigModule = function(content, config) { - if (this.options.storeConfigInMeta) { - content.push('var prefix = \'' + config.modulePrefix + '\';'); - content.push(fs.readFileSync(path.join(__dirname, 'app-config-from-meta.js'))); - } else { - content.push('return { \'default\': ' + JSON.stringify(config) + '};'); + if (!outputFile) { + throw new Error('outputFile is not specified'); } -}; -/** - @private - @method _contentForAppBoot - @param {Array} content - @param {Object} config -*/ -EmberApp.prototype._contentForAppBoot = function(content, config) { - content.push('if (runningTests) {'); - content.push(' require("' + - config.modulePrefix + - '/tests/test-helper");'); - if (this.options.autoRun) { - content.push('} else {'); - content.push(' require("' + - config.modulePrefix + - '/app")["default"].create(' + - calculateAppConfig(config) + - ');'); + if (!container[outputFile]) { + container[outputFile] = []; + } + if (!allowImport(strategy, container[outputFile], assetPath, options)) { + return; } - content.push('}'); -}; -/** - Returns the tag for index.html + if (options.prepend) { + container[outputFile].unshift(assetPath); + } else { + container[outputFile].push(assetPath); + } +} - @param {Object} config Application configuration - @return {String} Base tag or empty string - */ -function calculateBaseTag(config){ - var baseURL = cleanBaseURL(config.baseURL); - var locationType = config.locationType; +// In this strategy the last instance of the asset in the array is the one which will be used. +// This applies to CSS where the last asset always "wins" no matter what. +function _lastOneWins(fileList, assetPath, options) { + let assetIndex = fileList.indexOf(assetPath); - if (locationType === 'hash') { - return ''; + // Doesn't exist in the current fileList. Safe to remove. + if (assetIndex === -1) { + return true; } - if (baseURL) { - return ''; + logger.info(`Highlander Rule: duplicate \`app.import(${assetPath})\`. Only including the last by order.`); + + if (options.prepend) { + // The existing asset is _already after_ this inclusion and would win. + // Therefore this branch is a no-op. + return false; } else { - return ''; + // The existing asset is _before_ this inclusion and needs to be removed. + fileList.splice(fileList.indexOf(assetPath), 1); + return true; } } -function calculateEmberENV(config) { - return JSON.stringify(config.EmberENV || {}); -} +// In JS the asset which would be first will win. +// If it is something which includes globals we want those defined as early as +// possible. Any initialization would likely be repeated. Any mutation of global +// state that occurs on initialization is likely _fixed_. +// Any module definitions will be identical except in the scenario where they'red +// reified to reassignment. This is likely fine. +function _firstOneWins(fileList, assetPath, options) { + let assetIndex = fileList.indexOf(assetPath); + + // Doesn't exist in the current fileList. Safe to remove. + if (assetIndex === -1) { + return true; + } -function calculateAppConfig(config) { - return JSON.stringify(config.APP || {}); + logger.info(`Highlander Rule: duplicate \`app.import(${assetPath})\`. Only including the first by order.`); + + if (options.prepend) { + // The existing asset is _after_ this inclusion and needs to be removed. + fileList.splice(fileList.indexOf(assetPath), 1); + return true; + } else { + // The existing asset is _already before_ this inclusion and would win. + // Therefore this branch is a no-op. + return false; + } } -function calculateModulePrefix(config) { - return config.modulePrefix; +function allowImport(strategy, fileList, assetPath, options) { + if (strategy === 'firstOneWins') { + // We must find all occurrences and decide what to do with each. + return _firstOneWins.call(undefined, fileList, assetPath, options); + } else if (strategy === 'lastOneWins') { + // We can simply use the "last one wins" strategy. + return _lastOneWins.call(undefined, fileList, assetPath, options); + } else { + return true; + } } diff --git a/lib/broccoli/merge-trees.js b/lib/broccoli/merge-trees.js index e56ab17f83..3e5116a9aa 100644 --- a/lib/broccoli/merge-trees.js +++ b/lib/broccoli/merge-trees.js @@ -1,13 +1,107 @@ 'use strict'; +const upstreamMergeTrees = require('broccoli-merge-trees'); +const heimdall = require('heimdalljs'); +const heimdallLogger = require('heimdalljs-logger'); +const logger = heimdallLogger('ember-cli:merge-trees'); +let EMPTY_MERGE_TREE; -var upstreamMergeTrees = require('broccoli-merge-trees'); +function overrideEmptyTree(tree) { + EMPTY_MERGE_TREE = tree; +} -module.exports = function(inputTree, options) { +function getEmptyTree() { + if (EMPTY_MERGE_TREE) { + return EMPTY_MERGE_TREE; + } + + EMPTY_MERGE_TREE = module.exports._upstreamMergeTrees([], { + annotation: 'EMPTY_MERGE_TREE', + description: 'EMPTY_MERGE_TREE', + }); + + let originalCleanup = EMPTY_MERGE_TREE.cleanup; + EMPTY_MERGE_TREE.cleanup = function () { + // this tree is being cleaned up, we must + // ensure that our shared EMPTY_MERGE_TREE is + // reset (otherwise it will not have a valid + // `outputPath`) + EMPTY_MERGE_TREE = null; + return originalCleanup.apply(this, arguments); + }; + + return EMPTY_MERGE_TREE; +} + +module.exports = function mergeTrees(_inputTrees, options) { options = options || {}; - options.description = options.annotation; - var tree = upstreamMergeTrees(inputTree, options); - tree.description = options && options.description; + let node = heimdall.start( + { + name: `EmberCliMergeTrees(${options.annotation})`, + emberCliMergeTrees: true, + }, + function MergeTreesSchema() { + this.emptyTrees = 0; + this.duplicateTrees = 0; + this.returns = { + empty: 0, + identity: 0, + merge: 0, + }; + } + ); + + let inputTrees = _inputTrees + .filter(function _removeEmptyTrees(tree) { + if (tree && tree !== getEmptyTree()) { + return true; + } else { + node.stats.emptyTrees++; + logger.debug('Removing empty tree'); + return false; + } + }) + .reduce(function _dedupeTrees(result, tree) { + // Check for a previous duplicate and remove it so the last one "wins" + let indexOfTree = result.indexOf(tree); + if (indexOfTree !== -1) { + node.stats.duplicateTrees++; + logger.debug(`Removing duplicate tree: ${tree.annotation}`); + result.splice(indexOfTree, 1); + } + + result.push(tree); + return result; + }, []); - return tree; + switch (inputTrees.length) { + case 0: + node.stats.returns.empty++; + logger.info('Returning empty tree'); + node.stop(); + return getEmptyTree(); + case 1: + node.stats.returns.identity++; + logger.info('Returning single input tree'); + node.stop(); + return inputTrees[0]; + default: { + options.description = options.annotation; + let tree = module.exports._upstreamMergeTrees(inputTrees, options); + + tree.description = options && options.description; + + node.stats.returns.merge++; + logger.info('Returning upstream merged trees'); + node.stop(); + return tree; + } + } }; + +module.exports.isEmptyTree = function isEmptyTree(tree) { + return tree === EMPTY_MERGE_TREE; +}; + +module.exports._overrideEmptyTree = overrideEmptyTree; +module.exports._upstreamMergeTrees = upstreamMergeTrees; diff --git a/lib/broccoli/test-support-prefix.js b/lib/broccoli/test-support-prefix.js index b67d3af471..78072a5f5c 100644 --- a/lib/broccoli/test-support-prefix.js +++ b/lib/broccoli/test-support-prefix.js @@ -1,3 +1 @@ -/* jshint ignore:start */ {{content-for 'test-support-prefix'}} -/* jshint ignore:end */ diff --git a/lib/broccoli/test-support-suffix.js b/lib/broccoli/test-support-suffix.js index 81032c7d50..b284e2474e 100644 --- a/lib/broccoli/test-support-suffix.js +++ b/lib/broccoli/test-support-suffix.js @@ -1,7 +1,7 @@ -/* jshint ignore:start */ - runningTests = true; -{{content-for 'test-support-suffix'}} +if (typeof Testem !== 'undefined' && (typeof QUnit !== 'undefined' || typeof Mocha !== 'undefined')) { + window.Testem.hookIntoTestFramework(); +} -/* jshint ignore:end */ +{{content-for 'test-support-suffix'}} diff --git a/lib/broccoli/testem.js b/lib/broccoli/testem.js index 4dfeec8f2b..cb666df67c 100644 --- a/lib/broccoli/testem.js +++ b/lib/broccoli/testem.js @@ -1,4 +1,4 @@ -/** +/* * This is dummy file that exists for the sole purpose * of allowing tests to run directly in the browser as * well as by Testem. @@ -7,7 +7,7 @@ * the test build of index.html, which requires a * snippet to load the testem.js file: * - * This has to go after the qunit framework and app + * This has to go before the qunit framework and app * tests are loaded. * * Testem internally supplies this file. However, if you diff --git a/lib/broccoli/tests-prefix.js b/lib/broccoli/tests-prefix.js new file mode 100644 index 0000000000..ad9a93a7c1 --- /dev/null +++ b/lib/broccoli/tests-prefix.js @@ -0,0 +1 @@ +'use strict'; diff --git a/lib/broccoli/tests-suffix.js b/lib/broccoli/tests-suffix.js new file mode 100644 index 0000000000..c9222ee460 --- /dev/null +++ b/lib/broccoli/tests-suffix.js @@ -0,0 +1,2 @@ +require('{{MODULE_PREFIX}}/tests/test-helper'); +EmberENV.TESTS_FILE_LOADED = true; diff --git a/lib/broccoli/vendor-prefix.js b/lib/broccoli/vendor-prefix.js index ee35d658c3..9636005787 100644 --- a/lib/broccoli/vendor-prefix.js +++ b/lib/broccoli/vendor-prefix.js @@ -1,8 +1,18 @@ -/* jshint ignore:start */ +window.EmberENV = (function(EmberENV, extra) { + for (var key in extra) { + EmberENV[key] = extra[key]; + } -window.EmberENV = {{EMBER_ENV}}; + return EmberENV; +})(window.EmberENV || {}, {{EMBER_ENV}}); + +// used to determine if the application should be booted immediately when `app-name.js` is evaluated +// when `runningTests` the `app-name.js` file will **not** import the applications `app/app.js` and +// call `Application.create(...)` on it. Additionally, applications can opt-out of this behavior by +// setting `autoRun` to `false` in their `ember-cli-build.js` +// +// The default `test-support.js` file will set this to `true` when it runs (so that Application.create() +// is not ran when running tests). var runningTests = false; {{content-for 'vendor-prefix'}} - -/* jshint ignore:end */ diff --git a/lib/broccoli/vendor-suffix.js b/lib/broccoli/vendor-suffix.js index 2616ded2ee..cbe2d02d2b 100644 --- a/lib/broccoli/vendor-suffix.js +++ b/lib/broccoli/vendor-suffix.js @@ -1,5 +1 @@ -/* jshint ignore:start */ - {{content-for 'vendor-suffix'}} - -/* jshint ignore:end */ diff --git a/lib/cli/cli.js b/lib/cli/cli.js index ad4cc7ee9e..05e3ea744c 100644 --- a/lib/cli/cli.js +++ b/lib/cli/cli.js @@ -1,157 +1,272 @@ 'use strict'; -var lookupCommand = require('./lookup-command'); -var Promise = require('../ext/promise'); -var versionUtils = require('../utilities/version-utils'); -var UpdateChecker = require('../models/update-checker'); -var getOptionArgs = require('../utilities/get-option-args'); -var debug = require('debug')('ember-cli:cli'); - -var validPlatformVersion = require('../utilities/valid-platform-version'); -var emberCLIVersion = versionUtils.emberCLIVersion; -var InstallationChecker = require('../models/installation-checker'); - -function CLI(options) { - this.name = options.name; - this.ui = options.ui; - this.analytics = options.analytics; - this.testing = options.testing; - this.root = options.root; - this.npmPackage = options.npmPackage; - - debug('testing %o', !!this.testing); -} +const lookupCommand = require('./lookup-command'); +const getOptionArgs = require('../utilities/get-option-args'); +const hash = require('promise.hash.helper'); +const logger = require('heimdalljs-logger')('ember-cli:cli'); +const loggerTesting = require('heimdalljs-logger')('ember-cli:testing'); +const Instrumentation = require('../models/instrumentation'); +const PackageInfoCache = require('../models/package-info-cache'); +const heimdall = require('heimdalljs'); -module.exports = CLI; +const onProcessInterrupt = require('../utilities/will-interrupt-process'); -CLI.prototype.run = function(environment) { - return Promise.hash(environment).then(function(environment) { - var args = environment.cliArgs.slice(); - var commandName = args.shift(); - var commandArgs = args; - var helpOptions; - var update; +class CLI { + /** + * @private + * @class CLI + * @constructor + * @param options + */ + constructor(options) { + /** + * @private + * @property name + */ + this.name = options.name; - var CurrentCommand = lookupCommand(environment.commands, commandName, commandArgs, { - project: environment.project, - ui: this.ui - }); + /** + * @private + * @property ui + * @type UI + */ + this.ui = options.ui; + + /** + * @private + * @property testing + * @type Boolean + */ + this.testing = options.testing; + + /** + * @private + * @property disableDependencyChecker + * @type Boolean + */ + this.disableDependencyChecker = options.disableDependencyChecker; + + /** + * @private + * @property root + */ + this.root = options.root; + + /** + * @private + * @property npmPackage + */ + this.npmPackage = options.npmPackage; - var command = new CurrentCommand({ - ui: this.ui, - analytics: this.analytics, - commands: environment.commands, - tasks: environment.tasks, - project: environment.project, - settings: environment.settings, - testing: this.testing, - cli: this + /** + * @private + * @property instrumentation + */ + this.instrumentation = + options.instrumentation || + new Instrumentation({ + ui: options.ui, + initInstrumentation: options.initInstrumentation, + }); + + this.packageInfoCache = new PackageInfoCache(this.ui); + + logger.info('testing %o', !!this.testing); + } + + /** + * @private + * @method maybeMakeCommand + * @param commandName + * @param commandArgs + * @return {null|CurrentCommand} + */ + maybeMakeCommand(commandName, commandArgs) { + if (this._environment === undefined) { + throw new Error('Unable to make command without environment, you have to execute "run" method first.'); + } + let CurrentCommand = lookupCommand(this._environment.commands, commandName, commandArgs, { + project: this._environment.project, + ui: this.ui, }); - getOptionArgs('--verbose', commandArgs).forEach(function(arg){ - process.env['EMBER_VERBOSE_' + arg.toUpperCase()] = 'true'; + /* + * XXX Need to decide what to do here about showing errors. For + * a non-CLI project the cache is local and probably should. For + * a CLI project the cache is there, but not sure when we'll know + * about all the errors, because there may be multiple projects. + * if (this.packageInfoCache.hasErrors()) { + * this.packageInfoCache.showErrors(); + * } + */ + let command = new CurrentCommand({ + ui: this.ui, + commands: this._environment.commands, + tasks: this._environment.tasks, + project: this._environment.project, + settings: this._environment.settings, + testing: this.testing, + cli: this, }); - if (!validPlatformVersion(process.version) && !this.testing) { - var chalk = require('chalk'); + return command; + } - this.ui.writeLine(chalk.red('Future versions of Ember CLI will not support ' + process.version + '. Please update to Node 0.12 or io.js.')); + /** + * @private + * @method run + * @param {Promise} environmentPromiseHash + * @return {Promise} + */ + async run(environmentPromiseHash) { + if (environmentPromiseHash === undefined) { + return Promise.reject(new Error('Unable to execute "run" command without environment argument')); } + let shutdownOnExit = null; - this.ui.writeLine('version: ' + emberCLIVersion()); - debug('command: %s', commandName); + let environment = (this._environment = await hash(environmentPromiseHash)); - if (commandName !== 'update' && !this.testing) { - var a = new UpdateChecker(this.ui, environment.settings); - update = a.checkForUpdates(); - } + try { + let args = environment.cliArgs.slice(); + let commandName = args.shift(); + let commandArgs = args; + let helpOptions; - if(!this.testing) { - process.chdir(environment.project.root); - var skipInstallationCheck = commandArgs.indexOf('--skip-installation-check') !== -1; - if (environment.project.isEmberCLIProject() && !skipInstallationCheck) { - new InstallationChecker({ project: environment.project }).checkInstallations(); - } - } + let commandLookupCreationToken = heimdall.start('lookup-command'); - command.beforeRun(commandArgs); + let command = this.maybeMakeCommand(commandName, commandArgs); - return Promise.resolve(update).then(function() { - return command.validateAndRun(commandArgs); - }).then(function(result) { - // if the help option was passed, call the help command - if (result === 'callHelp') { - helpOptions = { - environment: environment, - commandName: commandName, - commandArgs: commandArgs - }; + commandLookupCreationToken.stop(); - return this.callHelp(helpOptions); - } + getOptionArgs('--verbose', commandArgs).forEach((arg) => { + process.env[`EMBER_VERBOSE_${arg.toUpperCase()}`] = 'true'; + }); - return result; - }.bind(this)).then(function(exitCode) { - // TODO: fix this - // Possibly this issue: https://github.com/joyent/node/issues/8329 - // Wait to resolve promise when running on windows. - // This ensures that stdout is flushed so acceptance tests get full output - var result = { - exitCode: exitCode, - ui: this.ui - }; - return new Promise(function(resolve) { - if (process.platform === 'win32') { - setTimeout(resolve, 250, result); - } else { - resolve(result); + logger.info('command: %s', commandName); + + let instrumentation = this.instrumentation; + let onCommandInterrupt; + + let runPromise = Promise.resolve().then(async () => { + let resultOrExitCode; + + try { + instrumentation.stopAndReport('init'); + + try { + instrumentation.start('command'); + + loggerTesting.info('cli: command.beforeRun'); + onProcessInterrupt.addHandler(onCommandInterrupt); + + await command.beforeRun(commandArgs); + + loggerTesting.info('cli: command.validateAndRun'); + + resultOrExitCode = await command.validateAndRun(commandArgs); + } finally { + instrumentation.stopAndReport('command', commandName, commandArgs); + + onProcessInterrupt.removeHandler(onCommandInterrupt); + } + } finally { + instrumentation.start('shutdown'); + shutdownOnExit = function () { + instrumentation.stopAndReport('shutdown'); + }; + } + + // if the help option was passed, call the help command + if (resultOrExitCode === 'callHelp') { + helpOptions = { + environment, + commandName, + commandArgs, + }; + + resultOrExitCode = await this.callHelp(helpOptions); } + + loggerTesting.info(`cli: command run complete. exitCode: ${resultOrExitCode}`); + + return resultOrExitCode; }); - }.bind(this)); - - }.bind(this)).catch(this.logError.bind(this)); -}; - -CLI.prototype.callHelp = function(options) { - var environment = options.environment; - var commandName = options.commandName; - var commandArgs = options.commandArgs; - var helpIndex = commandArgs.indexOf('--help'); - var hIndex = commandArgs.indexOf('-h'); - - var HelpCommand = lookupCommand(environment.commands, 'help', commandArgs, { - project: environment.project, - ui: this.ui - }); - - var help = new HelpCommand({ - ui: this.ui, - analytics: this.analytics, - commands: environment.commands, - tasks: environment.tasks, - project: environment.project, - settings: environment.settings, - testing: this.testing - }); - - if (helpIndex > -1) { - commandArgs.splice(helpIndex,1); - } - if (hIndex > -1) { - commandArgs.splice(hIndex,1); + onCommandInterrupt = async () => { + await command.onInterrupt(); + + return await runPromise; + }; + + return await runPromise; + } catch (error) { + this.logError(error); + return 1; + } finally { + if (shutdownOnExit) { + shutdownOnExit(); + } + } } - commandArgs.unshift(commandName); + /** + * @private + * @method callHelp + * @param options + * @return {Promise} + */ + callHelp(options) { + let environment = options.environment; + let commandName = options.commandName; + let commandArgs = options.commandArgs; + let helpIndex = commandArgs.indexOf('--help'); + let hIndex = commandArgs.indexOf('-h'); - return help.validateAndRun(commandArgs); -}; + let HelpCommand = lookupCommand(environment.commands, 'help', commandArgs, { + project: environment.project, + ui: this.ui, + }); -CLI.prototype.logError = function(error) { - if (this.testing && error){ - console.error(error.message); - console.error(error.stack); + let help = new HelpCommand({ + ui: this.ui, + commands: environment.commands, + tasks: environment.tasks, + project: environment.project, + settings: environment.settings, + testing: this.testing, + }); + + if (helpIndex > -1) { + commandArgs.splice(helpIndex, 1); + } + + if (hIndex > -1) { + commandArgs.splice(hIndex, 1); + } + + commandArgs.unshift(commandName); + + return help.validateAndRun(commandArgs); } - this.ui.writeError(error); - return 1; -}; + + /** + * @private + * @method logError + * @param error + * @return {number} + */ + logError(error) { + if (this.testing && error) { + console.error(error.message); + if (error.stack) { + console.error(error.stack); + } + throw error; + } + this.ui.errorLog.push(error); + this.ui.writeError(error); + return 1; + } +} + +module.exports = CLI; diff --git a/lib/cli/index.js b/lib/cli/index.js index 91ebe1f5f7..aa6d3f8bc1 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -1,102 +1,100 @@ 'use strict'; -// Main entry point -var Project = require('../models/project'); -var requireAsHash = require('../utilities/require-as-hash'); -var Command = require('../models/command'); -var commands = requireAsHash('../commands/*.js', Command); -var Task = require('../models/task'); -var tasks = requireAsHash('../tasks/*.js', Task); -var CLI = require('./cli'); -var packageConfig = require('../../package.json'); -var debug = require('debug')('ember-cli:cli/index'); -var merge = require('lodash/object/merge'); -var path = require('path'); +const willInterruptProcess = require('../utilities/will-interrupt-process'); +const instrumentation = require('../utilities/instrumentation'); +const getConfig = require('../utilities/get-config'); +const ciInfo = require('ci-info'); + +let initInstrumentation; +if (instrumentation.instrumentationEnabled()) { + const heimdall = require('heimdalljs'); + let initInstrumentationToken = heimdall.start('init'); + initInstrumentation = { + token: initInstrumentationToken, + node: heimdall.current, + }; +} -var version = packageConfig.version; -var name = packageConfig.name; -var trackingCode = packageConfig.trackingCode; +// Main entry point +const requireAsHash = require('../utilities/require-as-hash'); +const merge = require('lodash/merge'); +const path = require('path'); +const heimdall = require('heimdalljs'); + +function loadCommands() { + let token = heimdall.start('load-commands'); + const Command = require('../models/command'); + let hash = requireAsHash('../commands/*.js', Command); + token.stop(); + return hash; +} -function clientId() { - var ConfigStore = require('configstore'); - var configStore = new ConfigStore('ember-cli'); - var id = configStore.get('client-id'); +function loadTasks() { + let token = heimdall.start('load-tasks'); + const Task = require('../models/task'); + let hash = requireAsHash('../tasks/*.js', Task); + token.stop(); + return hash; +} - if (id) { - return id; - } else { - id = require('node-uuid').v4().toString(); - configStore.set('client-id', id); - return id; +function configureLogger(env) { + let depth = Number(env['DEBUG_DEPTH']); + if (depth) { + let logConfig = require('heimdalljs').configFor('logging'); + logConfig.depth = depth; } } -// Options: Array cliArgs, Stream inputStream, Stream outputStream -module.exports = function(options) { - var UI = options.UI || require('../ui'); - var Leek = options.Leek || require('leek'); - var Yam = options.Yam || require('yam'); - - // TODO: one UI (lib/models/project.js also has one for now...) - var ui = new UI({ - inputStream: options.inputStream, - outputStream: options.outputStream, - ci: process.env.CI || /^(dumb|emacs)$/.test(process.env.TERM), - writeLevel: ~process.argv.indexOf('--silent') ? 'ERROR' : undefined - }); - - var config = new Yam('ember-cli', { - primary: Project.getProjectRoot() - }); - - var leekOptions; - - var disableAnalytics = options.cliArgs && - (options.cliArgs.indexOf('--disable-analytics') > -1 || - options.cliArgs.indexOf('-v') > -1 || - options.cliArgs.indexOf('--version') > -1) || - config.get('disableAnalytics'); - - var defaultLeekOptions = { - trackingCode: trackingCode, - globalName: name, - name: clientId(), - version: version, - silent: disableAnalytics - }; - - var defaultUpdateCheckerOptions = { - checkForUpdates: false - }; - - if (config.get('leekOptions')) { - leekOptions = merge(defaultLeekOptions, config.get('leekOptions')); - } else { - leekOptions = defaultLeekOptions; +// Options: Array cliArgs, Stream inputStream, Stream outputStream, EventEmitter process +module.exports = async function (options) { + // `process` should be captured before we require any libraries which + // may use `process.exit` work arounds for async cleanup. + willInterruptProcess.capture(options.process || process); + + try { + let UI = options.UI || require('console-ui'); + const CLI = require('./cli'); + const Project = require('../models/project'); + let config = getConfig(options.Yam); + + configureLogger(process.env); + + // TODO: one UI (lib/models/project.js also has one for now...) + let ui = new UI({ + inputStream: options.inputStream, + outputStream: options.outputStream, + errorStream: options.errorStream || process.stderr, + errorLog: options.errorLog || [], + ci: ciInfo.isCI || /^(dumb|emacs)$/.test(process.env.TERM), + writeLevel: process.argv.indexOf('--silent') !== -1 ? 'ERROR' : undefined, + }); + + let cli = new CLI({ + ui, + testing: options.testing, + name: options.cli ? options.cli.name : 'ember', + disableDependencyChecker: options.disableDependencyChecker, + root: options.cli ? options.cli.root : path.resolve(__dirname, '..', '..'), + npmPackage: options.cli ? options.cli.npmPackage : 'ember-cli', + initInstrumentation, + }); + + let project = Project.projectOrnullProject(ui, cli); + + let defaultUpdateCheckerOptions = { + checkForUpdates: false, + }; + + let environment = { + tasks: loadTasks(), + cliArgs: options.cliArgs, + commands: loadCommands(), + project, + settings: merge(defaultUpdateCheckerOptions, config.getAll()), + }; + + return await cli.run(environment); + } finally { + willInterruptProcess.release(); } - - debug('leek: %o', leekOptions); - - var leek = new Leek(leekOptions); - - var cli = new CLI({ - ui: ui, - analytics: leek, - testing: options.testing, - name: options.cli ? options.cli.name : 'ember', - root: options.cli ? options.cli.root : path.resolve(__dirname, '..', '..'), - npmPackage: options.cli ? options.cli.npmPackage : 'ember-cli' - }); - - var project = Project.projectOrnullProject(ui, cli); - - var environment = { - tasks: tasks, - cliArgs: options.cliArgs, - commands: commands, - project: project, - settings: merge(defaultUpdateCheckerOptions, config.getAll()) - }; - - return cli.run(environment); }; diff --git a/lib/cli/lookup-command.js b/lib/cli/lookup-command.js index 46aaeb16e5..f9a3c3ddd1 100644 --- a/lib/cli/lookup-command.js +++ b/lib/cli/lookup-command.js @@ -1,23 +1,22 @@ 'use strict'; -var chalk = require('chalk'); -var UnknownCommand = require('../commands/unknown'); +const UnknownCommand = require('../commands/unknown'); -module.exports = function(commands, commandName, commandArgs, optionHash) { - var options = optionHash || {}; - var project = options.project; - var ui = options.ui; +module.exports = function (commands, commandName, commandArgs, optionHash) { + let options = optionHash || {}; + let project = options.project; + let ui = options.ui; function aliasMatches(alias) { return alias === commandName; } function findCommand(commands, commandName) { - for (var key in commands) { - var command = commands[key]; + for (let key in commands) { + let command = commands[key]; - var name = command.prototype.name; - var aliases = command.prototype.aliases || []; + let name = command.prototype.name; + let aliases = command.prototype.aliases || []; if (name === commandName || aliases.some(aliasMatches)) { return command; @@ -26,40 +25,51 @@ module.exports = function(commands, commandName, commandArgs, optionHash) { } // Attempt to find command in ember-cli core commands - var command = findCommand(commands, commandName); + let command = findCommand(commands, commandName); - var addonCommand; + let addonWithCommand; + let addonCommand; // Attempt to find command within addons if (project && project.eachAddonCommand) { - project.eachAddonCommand(function(addonName, commands) { + project.eachAddonCommand((addonName, commands) => { addonCommand = findCommand(commands, commandName); + if (addonCommand) { + addonWithCommand = addonName; + } return !addonCommand; }); } if (command && addonCommand) { if (addonCommand.overrideCore) { - ui.writeLine(chalk.cyan('warning: An ember-addon has attempted to override the core command "' + - command.prototype.name + '". The addon command will be used as the overridding was explicit.')); + ui.writeWarnLine( + `An ember-addon (${addonWithCommand}) has attempted to override the core command "${command.prototype.name}". ` + + `The addon command will be used as the overridding was explicit.` + ); return addonCommand; } - ui.writeLine(chalk.cyan('warning: An ember-addon has attempted to override the core command "' + - command.prototype.name + '". The core command will be used.')); + ui.writeWarnLine( + `An ember-addon (${addonWithCommand}) has attempted to override the core command "${command.prototype.name}". ` + + `The core command will be used.` + ); return command; } - if(command) { + if (command) { return command; } - if(addonCommand) { + if (addonCommand) { return addonCommand; } // if we didn't find anything, return an "UnknownCommand" - return UnknownCommand.extend({ - commandName: commandName - }); + return class extends UnknownCommand { + constructor(options) { + super(options); + this.name = commandName; + } + }; }; diff --git a/lib/commands/addon.js b/lib/commands/addon.js index 7517d27a21..ad9167d017 100644 --- a/lib/commands/addon.js +++ b/lib/commands/addon.js @@ -1,6 +1,7 @@ 'use strict'; -var NewCommand = require('./new'); +const SilentError = require('silent-error'); +const NewCommand = require('./new'); module.exports = NewCommand.extend({ name: 'addon', @@ -10,12 +11,52 @@ module.exports = NewCommand.extend({ { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, { name: 'blueprint', type: String, default: 'addon', aliases: ['b'] }, - { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn'] }, - { name: 'skip-bower', type: Boolean, default: false, aliases: ['sb'] }, + { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn', 'skip-install', 'si'] }, { name: 'skip-git', type: Boolean, default: false, aliases: ['sg'] }, + { + name: 'package-manager', + type: ['npm', 'pnpm', 'yarn'], + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + }, + { name: 'directory', type: String, aliases: ['dir'] }, + { + name: 'lang', + type: String, + description: "Sets the base human language of the addon's own test application via index.html", + }, + { name: 'lint-fix', type: Boolean, default: true }, + { + name: 'ci-provider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + }, + { + name: 'typescript', + type: Boolean, + default: false, + description: 'Set up the addon to use TypeScript', + aliases: ['ts'], + }, + { + name: 'strict', + type: Boolean, + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + }, ], - anonymousOptions: [ - '' - ] + anonymousOptions: [''], + + run(commandOptions, commandArguments) { + let addonName = commandArguments[0]; + + if (addonName) { + return this._super(commandOptions, commandArguments); + } + + return Promise.reject( + new SilentError('The `ember addon` command requires a name to be specified. For more details, run `ember help`.') + ); + }, }); diff --git a/lib/commands/asset-sizes.js b/lib/commands/asset-sizes.js new file mode 100644 index 0000000000..73b8de1670 --- /dev/null +++ b/lib/commands/asset-sizes.js @@ -0,0 +1,25 @@ +'use strict'; + +const Command = require('../models/command'); + +module.exports = Command.extend({ + name: 'asset-sizes', + description: 'Shows the sizes of your asset files.', + skipHelp: true, + + availableOptions: [ + { name: 'output-path', type: 'Path', default: 'dist/', aliases: ['o'] }, + { name: 'json', type: Boolean, default: false }, + ], + + init() { + this._super.apply(this, arguments); + if (!this.isViteProject) { + this.skipHelp = false; + } + }, + + run(commandOptions) { + return this.runTask('ShowAssetSizes', commandOptions); + }, +}); diff --git a/lib/commands/build.js b/lib/commands/build.js index 99adf76abb..5c5f9e5d54 100644 --- a/lib/commands/build.js +++ b/lib/commands/build.js @@ -1,39 +1,73 @@ 'use strict'; -var path = require('path'); -var Command = require('../models/command'); -var win = require('../utilities/windows-admin'); +const Command = require('../models/command'); +const Win = require('../utilities/windows-admin'); +const SilentError = require('silent-error'); + +const ClassicOptions = [ + { name: 'watch', type: Boolean, default: false, aliases: ['w'] }, + { name: 'watcher', type: String }, +]; module.exports = Command.extend({ name: 'build', - description: 'Builds your app and places it into the output path (dist/ by default).', + description: 'Vestigial command in Vite-based projects. Use the `build` script from package.json instead.', aliases: ['b'], availableOptions: [ - { name: 'environment', type: String, default: 'development', aliases: ['e',{'dev' : 'development'}, {'prod' : 'production'}] }, - { name: 'output-path', type: path, default: 'dist/', aliases: ['o'] }, - { name: 'watch', type: Boolean, default: false, aliases: ['w'] }, - { name: 'watcher', type: String } + { + name: 'environment', + type: String, + default: 'development', + aliases: ['e', { dev: 'development' }, { prod: 'production' }], + description: 'Possible values are "development", "production", and "test".', + }, + { name: 'suppress-sizes', type: Boolean, default: false }, + { name: 'output-path', type: 'Path', default: 'dist/', aliases: ['o'] }, ], - run: function(commandOptions) { - var BuildTask = this.taskFor(commandOptions); - var buildTask = new BuildTask({ - ui: this.ui, - analytics: this.analytics, - project: this.project - }); - - return win.checkWindowsElevation(this.ui).then(function() { - return buildTask.run(commandOptions); - }); + init() { + this._super.apply(this, arguments); + if (!this.isViteProject) { + this.description = 'Builds your app and places it into the output path (dist/ by default).'; + this.availableOptions = this.availableOptions.concat(ClassicOptions); + } else if (process.env.EMBROIDER_PREBUILD) { + // having the --watch option available surpresses a warning that you get in Vite prebuild + this.availableOptions = this.availableOptions.concat(ClassicOptions[0]); + } }, - taskFor: function(options) { - if (options.watch) { - return this.tasks.BuildWatch; - } else { - return this.tasks.Build; + async run(commandOptions) { + if (this.isViteProject && !process.env.EMBROIDER_PREBUILD) { + // --watch is used during Embroider prebuild but should never be used directly so we only throw in this case if + // EMBROIDER_PREBUILD has been set + if (commandOptions.watch) { + return Promise.reject( + new SilentError( + 'The `--watch` option to `ember build` is not supported in Vite-based projects. Please use `vite dev` instead.' + ) + ); + } + + if (commandOptions.watcher) { + return Promise.reject( + new SilentError( + 'The `--watcher` option to `ember build` is not supported in Vite-based projects. Please use `vite dev` instead.' + ) + ); + } + } + + await Win.checkIfSymlinksNeedToBeEnabled(this.ui); + + let buildTaskName = commandOptions.watch ? 'BuildWatch' : 'Build'; + await this.runTask(buildTaskName, commandOptions); + + let isProduction = commandOptions.environment === 'production' || process.env.EMBER_ENV === 'production'; + if (!commandOptions.suppressSizes && isProduction) { + return this.runTask('ShowAssetSizes', { + outputPath: commandOptions.outputPath, + }); } - } + }, }); diff --git a/lib/commands/destroy.js b/lib/commands/destroy.js index a854e0830b..ff0fe75f82 100644 --- a/lib/commands/destroy.js +++ b/lib/commands/destroy.js @@ -1,11 +1,12 @@ 'use strict'; -var Command = require('../models/command'); -var Promise = require('../ext/promise'); -var Blueprint = require('../models/blueprint'); -var merge = require('lodash/object/merge'); +const path = require('path'); +const Command = require('../models/command'); +const mergeBlueprintOptions = require('../utilities/merge-blueprint-options'); +const merge = require('lodash/merge'); +const SilentError = require('silent-error'); -var SilentError = require('silent-error'); +const ClassicOptions = [{ name: 'dummy', type: Boolean, default: false, aliases: ['dum', 'id'] }]; module.exports = Command.extend({ name: 'destroy', @@ -16,80 +17,92 @@ module.exports = Command.extend({ availableOptions: [ { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, - { name: 'pod', type: Boolean, default: false, aliases: ['p'] }, + { name: 'pod', type: Boolean, default: false, aliases: ['p', 'pods'] }, { name: 'classic', type: Boolean, default: false, aliases: ['c'] }, - { name: 'dummy', type: Boolean, default: false, aliases: ['dum','id'] }, - { name: 'in-repo-addon', type: String, default: null, aliases: ['in-repo', 'ir'] } - + { name: 'in-repo-addon', type: String, default: null, aliases: ['in-repo', 'ir'] }, + { + name: 'in', + type: String, + default: null, + description: + 'Runs a blueprint against an in repo addon. ' + 'A path is expected, relative to the root of the project.', + }, + { + name: 'typescript', + type: Boolean, + aliases: ['ts'], + description: + 'Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output.', + }, ], - anonymousOptions: [ - '' - ], + anonymousOptions: [''], - beforeRun: function(rawArgs){ - if (!rawArgs.length) { - return; - } - try { - // merge in blueprint availableOptions - this.registerOptions(this.lookupBlueprint(rawArgs[0])); - } catch(e) { - SilentError.debugOrThrow('ember-cli/commands/destroy', e); + init() { + this._super.apply(this, arguments); + if (!this.isViteProject) { + this.availableOptions = this.availableOptions.concat(ClassicOptions); } }, - run: function(commandOptions, rawArgs) { - var blueprintName = rawArgs[0]; - var entityName = rawArgs[1]; + beforeRun: mergeBlueprintOptions, + + run(commandOptions, rawArgs) { + if (this.isViteProject && commandOptions.dummy) { + return Promise.reject( + new SilentError('The `--dummy` option to `ember destroy` is not supported in Vite-based projects.') + ); + } + + let blueprintName = rawArgs[0]; + let entityName = rawArgs[1]; if (!blueprintName) { - return Promise.reject(new SilentError('The `ember destroy` command requires a ' + - 'blueprint name to be specified. ' + - 'For more details, use `ember help`.')); + return Promise.reject( + new SilentError( + 'The `ember destroy` command requires a ' + + 'blueprint name to be specified. ' + + 'For more details, use `ember help`.' + ) + ); } if (!entityName) { - return Promise.reject(new SilentError('The `ember destroy` command requires an ' + - 'entity name to be specified. ' + - 'For more details, use `ember help`.')); + return Promise.reject( + new SilentError( + 'The `ember destroy` command requires an ' + + 'entity name to be specified. ' + + 'For more details, use `ember help`.' + ) + ); } - var Task = this.tasks.DestroyFromBlueprint; - var task = new Task({ - ui: this.ui, - analytics: this.analytics, - project: this.project, - settings: this.settings - }); - - var taskArgs = { - args: rawArgs + let taskArgs = { + args: rawArgs, }; - if(this.settings && this.settings.usePods && !commandOptions.classic) { - commandOptions.pod = !commandOptions.pod; + if (this.settings && this.settings.usePods && !commandOptions.classic) { + commandOptions.pod = true; + } + + if (commandOptions.in) { + let relativePath = path.relative(this.project.root, commandOptions.in); + commandOptions.in = path.resolve(relativePath); } - var taskOptions = merge(taskArgs, commandOptions || {}); + let taskOptions = merge(taskArgs, commandOptions || {}); if (this.project.initializeAddons) { this.project.initializeAddons(); } - return task.run(taskOptions); + return this.runTask('DestroyFromBlueprint', taskOptions); }, - lookupBlueprint: function(name) { - return Blueprint.lookup(name, { - paths: this.project.blueprintLookupPaths() - }); - }, - - printDetailedHelp: function() { - var ui = this.ui; + printDetailedHelp() { + let ui = this.ui; ui.writeLine(''); ui.writeLine(' Run `ember help generate` to view a list of available blueprints.'); - } + }, }); diff --git a/lib/commands/generate.js b/lib/commands/generate.js index e3f95999ec..e60a482f70 100644 --- a/lib/commands/generate.js +++ b/lib/commands/generate.js @@ -1,14 +1,21 @@ 'use strict'; -var chalk = require('chalk'); -var Command = require('../models/command'); -var Promise = require('../ext/promise'); -var Blueprint = require('../models/blueprint'); -var merge = require('lodash/object/merge'); -var reject = require('lodash/collection/reject'); -var EOL = require('os').EOL; - -var SilentError = require('silent-error'); +const path = require('path'); +const { default: chalk } = require('chalk'); +const Command = require('../models/command'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const mergeBlueprintOptions = require('../utilities/merge-blueprint-options'); +const merge = require('lodash/merge'); +const reject = require('lodash/reject'); +const EOL = require('os').EOL; +const SilentError = require('silent-error'); + +const UNKNOWN_BLUEPRINT_ERROR = + 'The `ember generate` command requires a ' + + 'blueprint name to be specified. ' + + 'For more details, use `ember help`'; + +const ClassicOptions = [{ name: 'dummy', type: Boolean, default: false, aliases: ['dum', 'id'] }]; module.exports = Command.extend({ name: 'generate', @@ -19,100 +26,100 @@ module.exports = Command.extend({ availableOptions: [ { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, - { name: 'pod', type: Boolean, default: false, aliases: ['p'] }, + { name: 'pod', type: Boolean, default: false, aliases: ['p', 'pods'] }, { name: 'classic', type: Boolean, default: false, aliases: ['c'] }, - { name: 'dummy', type: Boolean, default: false, aliases: ['dum','id'] }, - { name: 'in-repo-addon', type: String, default: null, aliases: ['in-repo', 'ir'] } + { name: 'in-repo-addon', type: String, default: null, aliases: ['in-repo', 'ir'] }, + { name: 'lint-fix', type: Boolean, default: true }, + { + name: 'in', + type: String, + default: null, + description: + 'Runs a blueprint against an in repo addon. ' + 'A path is expected, relative to the root of the project.', + }, + { + name: 'typescript', + type: Boolean, + aliases: ['ts'], + description: 'Generates a version of the blueprint written in TypeScript (if available).', + }, ], - anonymousOptions: [ - '' - ], + anonymousOptions: [''], - beforeRun: function(rawArgs){ - if (!rawArgs.length) { - return; - } - // merge in blueprint availableOptions - var blueprint; - try{ - blueprint = this.lookupBlueprint(rawArgs[0]); - this.registerOptions( blueprint ); - } - catch(e) { - SilentError.debugOrThrow('ember-cli/commands/generate', e); + init() { + this._super.apply(this, arguments); + if (!this.isViteProject) { + this.availableOptions = this.availableOptions.concat(ClassicOptions); } }, - run: function(commandOptions, rawArgs) { - var blueprintName = rawArgs[0]; + beforeRun: mergeBlueprintOptions, + + run(commandOptions, rawArgs) { + if (this.isViteProject && commandOptions.dummy) { + return Promise.reject( + new SilentError('The `--dummy` option to `ember generate` is not supported in Vite-based projects.') + ); + } + + let blueprintName = rawArgs[0]; if (!blueprintName) { - return Promise.reject(new SilentError('The `ember generate` command requires a ' + - 'blueprint name to be specified. ' + - 'For more details, use `ember help`.')); + return Promise.reject(new SilentError(UNKNOWN_BLUEPRINT_ERROR)); } - var Task = this.tasks.GenerateFromBlueprint; - var task = new Task({ - ui: this.ui, - analytics: this.analytics, - project: this.project, - testing: this.testing, - settings: this.settings - }); - - var taskArgs = { - args: rawArgs + + let taskArgs = { + args: rawArgs, }; if (this.settings && this.settings.usePods && !commandOptions.classic) { - commandOptions.pod = !commandOptions.pod; + commandOptions.pod = true; } - var taskOptions = merge(taskArgs, commandOptions || {}); + if (commandOptions.in) { + let relativePath = path.relative(this.project.root, commandOptions.in); + commandOptions.in = path.resolve(relativePath); + } + + let taskOptions = merge(taskArgs, commandOptions || {}); if (this.project.initializeAddons) { this.project.initializeAddons(); } - return task.run(taskOptions); - }, - - lookupBlueprint: function(name) { - return Blueprint.lookup(name, { - paths: this.project.blueprintLookupPaths() - }); + return this.runTask('GenerateFromBlueprint', taskOptions); }, - printDetailedHelp: function(options) { + printDetailedHelp(options) { this.ui.writeLine(this.getAllBlueprints(options)); }, - addAdditionalJsonForHelp: function(json, options) { + addAdditionalJsonForHelp(json, options) { json.availableBlueprints = this.getAllBlueprints(options); }, - getAllBlueprints: function(options) { - var lookupPaths = this.project.blueprintLookupPaths(); - var blueprintList = Blueprint.list({ paths: lookupPaths }); + getAllBlueprints(options) { + let lookupPaths = this.project.blueprintLookupPaths(); + let blueprintList = Blueprint.list({ paths: lookupPaths }); - var output = ''; + let output = ''; - var singleBlueprintName; + let singleBlueprintName; if (options.rawArgs) { singleBlueprintName = options.rawArgs[0]; } if (!singleBlueprintName && !options.json) { - output += EOL + ' Available blueprints:' + EOL; + output += `${EOL} Available blueprints:${EOL}`; } - var collectionsJson = []; + let collectionsJson = []; - blueprintList.forEach(function(collection) { - var result = this.getPackageBlueprints(collection, options, singleBlueprintName); + blueprintList.forEach(function (collection) { + let result = this.getPackageBlueprints(collection, options, singleBlueprintName); if (options.json) { - var collectionJson = {}; + let collectionJson = {}; collectionJson[collection.source] = result; collectionsJson.push(collectionJson); } else { @@ -121,8 +128,7 @@ module.exports = Command.extend({ }, this); if (singleBlueprintName && !output && !options.json) { - output = chalk.yellow('The \'' + singleBlueprintName + - '\' blueprint does not exist in this project.') + EOL; + output = chalk.yellow(`The '${singleBlueprintName}' blueprint does not exist in this project.`) + EOL; } if (options.json) { @@ -132,25 +138,24 @@ module.exports = Command.extend({ } }, - getPackageBlueprints: function(collection, options, singleBlueprintName) { - - var verbose = options.verbose; - var blueprints = collection.blueprints; + getPackageBlueprints(collection, options, singleBlueprintName) { + let verbose = options.verbose; + let blueprints = collection.blueprints; if (!verbose) { blueprints = reject(blueprints, 'overridden'); } - var output = ''; + let output = ''; if (blueprints.length && !singleBlueprintName && !options.json) { - output += ' ' + collection.source + ':' + EOL; + output += ` ${collection.source}:${EOL}`; } - var blueprintsJson = []; + let blueprintsJson = []; - blueprints.forEach(function(blueprint) { - var singleMatch = singleBlueprintName === blueprint.name; + blueprints.forEach(function (blueprint) { + let singleMatch = singleBlueprintName === blueprint.name; if (singleMatch) { verbose = true; } @@ -161,7 +166,7 @@ module.exports = Command.extend({ if (options.json) { blueprintsJson.push(blueprint.getJson(verbose)); } else { - output += this.getBlueprintInfoString(blueprint, verbose) + EOL; + output += blueprint.printBasicHelp(verbose) + EOL; } } }, this); @@ -172,70 +177,8 @@ module.exports = Command.extend({ return output; } }, - - getBlueprintInfoString: function(blueprint, verbose) { - var options; - var output = ' '; - if (blueprint.overridden) { - output += chalk.grey('(overridden)') + ' '; - output += chalk.grey(blueprint.name); - } else { - output += blueprint.name; - - blueprint.anonymousOptions.forEach(function(opt) { - output += ' ' + chalk.yellow('<' + opt + '>'); - }); - - options = blueprint.availableOptions || []; - - if (options.length > 0) { - output += ' ' + chalk.cyan(''); - } - - if (blueprint.description) { - output += EOL + ' ' + chalk.grey(blueprint.description); - } - - if (options.length > 0) { - options.forEach(function(opt) { - output += EOL + ' ' + chalk.cyan('--' + opt.name); - - if (opt.values) { - output += chalk.cyan('=' + opt.values.join('|')); - } - - if (opt.default !== undefined) { - output += ' ' + chalk.cyan('(Default: ' + opt.default + ')'); - } - - if (opt.required) { - output += ' ' + chalk.cyan('(Required)'); - } - - if (opt.aliases) { - output += EOL + ' ' + chalk.grey('aliases: ' + opt.aliases.map(function(a) { - var key; - if (typeof a === 'string') { - return '-' + a + (opt.type === Boolean ? '' : ' '); - } else { - key = Object.keys(a)[0]; - return '-' + key + ' (--' + opt.name + '=' + a[key] + ')'; - } - }).join(', ')); - } - - if (opt.description) { - output += ' ' + opt.description; - } - }); - } - - if (verbose && blueprint.printDetailedHelp) { - output += EOL + blueprint.printDetailedHelp(options); - } - - } - - return output; - } }); + +module.exports.ERRORS = { + UNKNOWN_BLUEPRINT_ERROR, +}; diff --git a/lib/commands/help.js b/lib/commands/help.js index 99dca1dd14..e1fd4e1878 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -1,154 +1,113 @@ 'use strict'; -var path = require('path'); -var Command = require('../models/command'); -var lookupCommand = require('../cli/lookup-command'); -var string = require('../utilities/string'); -var assign = require('lodash/object/assign'); - -var RootCommand = Command.extend({ - isRoot: true, - name: 'ember', - - availableOptions: [], - - anonymousOptions: [ - '' - ] -}); +const Command = require('../models/command'); +const lookupCommand = require('../cli/lookup-command'); +const stringUtils = require('ember-cli-string-utils'); +const GenerateCommand = require('./generate'); +const RootCommand = require('../utilities/root-command'); +const JsonGenerator = require('../utilities/json-generator'); module.exports = Command.extend({ name: 'help', - works: 'everywhere', description: 'Outputs the usage instructions for all commands or the provided command', aliases: [undefined, 'h', '--help', '-h'], + works: 'everywhere', availableOptions: [ { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, - { name: 'json', type: Boolean, default: false } + { name: 'json', type: Boolean, default: false }, ], - anonymousOptions: [ - '' - ], + anonymousOptions: [''], + + run(commandOptions, rawArgs) { + if (commandOptions.json) { + this._printJsonHelp(commandOptions); + } else { + this._printHelp(commandOptions, rawArgs); + } + }, - run: function(commandOptions, rawArgs) { - var multipleCommands = ['g','generate']; - var command; - var json; - var rootCommand = new RootCommand({ + _printHelp(commandOptions, rawArgs) { + let rootCommand = new RootCommand({ ui: this.ui, project: this.project, commands: this.commands, - tasks: this.tasks + tasks: this.tasks, }); - if (commandOptions.json) { - json = rootCommand.getJson(commandOptions); - json.commands = []; - json.addons = []; - } + if (rawArgs.length === 0) { - if (!commandOptions.json) { - rootCommand.printBasicHelp(commandOptions); - // Display usage for all commands. - this.ui.writeLine('Available commands in ember-cli:'); - this.ui.writeLine(''); - } + this.ui.writeLine(rootCommand.printBasicHelp(commandOptions)); + // Display usage for all commands. + this.ui.writeLine('Available commands in ember-cli:'); + this.ui.writeLine(''); - Object.keys(this.commands).forEach(function(commandName) { - if (commandOptions.json) { - this._addCommandHelpToJson(commandName, commandOptions, json); - } else { - this._printBasicHelpForCommand(commandName, commandOptions); - } + Object.keys(this.commands).forEach(function (commandName) { + this._printHelpForCommand(commandName, false, commandOptions); }, this); if (this.project.eachAddonCommand) { - this.project.eachAddonCommand(function(addonName, commands) { + this.project.eachAddonCommand((addonName, commands) => { this.commands = commands; - var addonJson; - if (commandOptions.json) { - addonJson = this.getJson(commandOptions); - addonJson.commands = []; - json.addons.push(addonJson); - } else { - this.ui.writeLine(''); - this.ui.writeLine('Available commands from ' + addonName + ':'); - } - Object.keys(this.commands).forEach(function(commandName) { - if (commandOptions.json) { - this._addCommandHelpToJson(commandName, commandOptions, addonJson); - } else { - this._printBasicHelpForCommand(commandName, commandOptions); - } + + this.ui.writeLine(''); + this.ui.writeLine(`Available commands from ${addonName}:`); + + Object.keys(this.commands).forEach(function (commandName) { + this._printHelpForCommand(commandName, false, commandOptions); }, this); - }.bind(this)); + }); } - } else { // If args were passed to the help command, // attempt to look up the command for each of them. - if (!commandOptions.json) { - this.ui.writeLine('Requested ember-cli commands:'); - this.ui.writeLine(''); - } + + this.ui.writeLine('Requested ember-cli commands:'); + this.ui.writeLine(''); if (this.project.eachAddonCommand) { - this.project.eachAddonCommand(function(addonName, commands) { - assign(this.commands, commands); - }.bind(this)); + this.project.eachAddonCommand((addonName, commands) => { + Object.assign(this.commands, commands); + }); } - if(multipleCommands.indexOf(rawArgs[0]) > -1) { - command = rawArgs.shift(); + let multipleCommands = [GenerateCommand.prototype.name].concat(GenerateCommand.prototype.aliases); + if (multipleCommands.indexOf(rawArgs[0]) > -1) { + let command = rawArgs.shift(); if (rawArgs.length > 0) { commandOptions.rawArgs = rawArgs; } - if (commandOptions.json) { - this._addCommandHelpToJson(command, commandOptions, json); - } else { - this._printDetailedHelpForCommand(command, commandOptions); - } - - } else { - - // Iterate through each arg beyond the initial 'help' command, - // and try to display usage instructions. - rawArgs.forEach(function(commandName) { - if (commandOptions.json) { - this._addCommandHelpToJson(commandName, commandOptions, json); - } else { - this._printDetailedHelpForCommand(commandName, commandOptions); - } - }, this); + rawArgs = [command]; } - } - if (commandOptions.json) { - this._printJsonHelp(json); + // Iterate through each arg beyond the initial 'help' command, + // and try to display usage instructions. + rawArgs.forEach(function (commandName) { + this._printHelpForCommand(commandName, true, commandOptions); + }, this); } }, - _addCommandHelpToJson: function(commandName, options, json) { - var command = this._lookupCommand(commandName); - if (!command.skipHelp) { - json.commands.push(command.getJson(options)); - } - }, + _printJsonHelp(commandOptions) { + let generator = new JsonGenerator({ + ui: this.ui, + project: this.project, + commands: this.commands, + tasks: this.tasks, + }); - _printBasicHelpForCommand: function(commandName, options) { - this._printHelpForCommand(commandName, false, options); - }, + let json = generator.generate(commandOptions); + + let outputJsonString = JSON.stringify(json, null, 2); - _printDetailedHelpForCommand: function(commandName, options) { - this._printHelpForCommand(commandName, true, options); + this.ui.writeLine(outputJsonString); }, - _printHelpForCommand: function(commandName, detailed, options) { - var command = this._lookupCommand(commandName); + _printHelpForCommand(commandName, detailed, options) { + let command = this._resolveCommand(commandName); - if (!command.skipHelp || (command.skipHelp && detailed)) { - command.printBasicHelp(options); + if (!command.skipHelp || detailed) { + this.ui.writeLine(command.printBasicHelp(options)); } if (detailed) { @@ -156,27 +115,18 @@ module.exports = Command.extend({ } }, - _printJsonHelp: function(json) { - var outputJsonString = JSON.stringify(json, function(key, value) { - // build command has a recursive property - if (value === path) { - return 'path'; - } - return value; - }, 2); - - this.ui.writeLine(outputJsonString); - }, - - _lookupCommand: function(commandName) { - var Command = this.commands[string.classify(commandName)] || - lookupCommand(this.commands, commandName); + _resolveCommand(commandName) { + let Command = this.commands[stringUtils.classify(commandName)] || this._lookupCommand(this.commands, commandName); return new Command({ ui: this.ui, project: this.project, commands: this.commands, - tasks: this.tasks + tasks: this.tasks, }); - } + }, + + _lookupCommand(commands, commandName) { + return lookupCommand(commands, commandName); + }, }); diff --git a/lib/commands/init.js b/lib/commands/init.js index 9c2368b9bf..a78c28a7e2 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -1,33 +1,81 @@ 'use strict'; -var Command = require('../models/command'); -var Promise = require('../ext/promise'); - -var SilentError = require('silent-error'); -var validProjectName = require('../utilities/valid-project-name'); -var normalizeBlueprint = require('../utilities/normalize-blueprint-option'); -var debug = require('debug')('ember-cli:command:init'); +const clone = require('lodash/clone'); +const merge = require('lodash/merge'); +const Command = require('../models/command'); +const SilentError = require('silent-error'); +const { default: chalk } = require('chalk'); +const isValidProjectName = require('../utilities/valid-project-name'); +const normalizeBlueprint = require('../utilities/normalize-blueprint-option'); +const mergeBlueprintOptions = require('../utilities/merge-blueprint-options'); +const { isPnpmProject, isYarnProject } = require('../utilities/package-managers'); +const getLangArg = require('../../lib/utilities/get-lang-arg'); +const { deprecate, DEPRECATIONS } = require('../debug'); module.exports = Command.extend({ name: 'init', - aliases: ['i'], - description: 'Creates a new ember-cli project in the current folder.', + description: 'Reinitializes a new ember-cli project in the current folder.', works: 'everywhere', availableOptions: [ { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, { name: 'blueprint', type: String, aliases: ['b'] }, - { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn'] }, - { name: 'skip-bower', type: Boolean, default: false, aliases: ['sb'] }, - { name: 'name', type: String, default: '', aliases: ['n'] } + { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn', 'skip-install', 'si'] }, + { name: 'lint-fix', type: Boolean, default: true }, + { + name: 'welcome', + type: Boolean, + default: true, + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + }, + { + name: 'package-manager', + type: ['npm', 'pnpm', 'yarn'], + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + }, + { name: 'name', type: String, default: '', aliases: ['n'] }, + { + name: 'lang', + type: String, + description: 'Sets the base human language of the application via index.html', + }, + { + name: 'embroider', + type: Boolean, + default: false, + description: 'Deprecated: Enables the build system to use Embroider', + }, + { + name: 'ci-provider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + }, + { + name: 'ember-data', + type: Boolean, + default: true, + description: 'Include ember-data dependencies and configuration', + }, + { + name: 'typescript', + type: Boolean, + default: false, + description: 'Set up the app to use TypeScript', + aliases: ['ts'], + }, + { + name: 'strict', + type: Boolean, + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + }, ], - anonymousOptions: [ - '' - ], + anonymousOptions: [''], - _defaultBlueprint: function() { + _defaultBlueprint() { if (this.project.isEmberCLIAddon()) { return 'addon'; } else { @@ -35,76 +83,97 @@ module.exports = Command.extend({ } }, - run: function(commandOptions, rawArgs) { + beforeRun: mergeBlueprintOptions, + + async run(commandOptions, rawArgs) { + deprecate( + "Don't use `--embroider` option. Use `-b @ember/app-blueprint` instead.", + !commandOptions.embroider, + DEPRECATIONS.EMBROIDER.options + ); + if (commandOptions.dryRun) { commandOptions.skipNpm = true; - commandOptions.skipBower = true; } - var installBlueprint = new this.tasks.InstallBlueprint({ - ui: this.ui, - analytics: this.analytics, - project: this.project - }); + let project = this.project; + let packageName = (commandOptions.name !== '.' && commandOptions.name) || project.name(); + let ciProvider = commandOptions.ciProvider; - if (!commandOptions.skipNpm) { - var npmInstall = new this.tasks.NpmInstall({ - ui: this.ui, - analytics: this.analytics, - project: this.project - }); - } + if (!packageName) { + let message = + `The \`ember ${this.name}\` command requires a ` + + `package.json in current folder with name attribute or a specified name via arguments. ` + + `For more details, use \`ember help\`.`; - if (!commandOptions.skipBower) { - var bowerInstall = new this.tasks.BowerInstall({ - ui: this.ui, - analytics: this.analytics, - project: this.project - }); + return Promise.reject(new SilentError(message)); } - var project = this.project; - var packageName = commandOptions.name !== '.' && commandOptions.name || project.name(); - - if (!packageName) { - var message = 'The `ember ' + this.name + '` command requires a ' + - 'package.json in current folder with name attribute or a specified name via arguments. ' + - 'For more details, use `ember help`.'; + if (commandOptions.lang) { + commandOptions.lang = getLangArg(commandOptions.lang, this.ui); + } - return Promise.reject(new SilentError(message)); + if (commandOptions.packageManager === undefined && project.isEmberCLIProject()) { + if (await isPnpmProject(project.root)) { + commandOptions.packageManager = 'pnpm'; + } else if (isYarnProject(project.root)) { + commandOptions.packageManager = 'yarn'; + } } - var blueprintOpts = { - dryRun: commandOptions.dryRun, - blueprint: commandOptions.blueprint || this._defaultBlueprint(), + let blueprintOpts = clone(commandOptions); + let blueprint = normalizeBlueprint(blueprintOpts.blueprint || this._defaultBlueprint()); + + merge(blueprintOpts, { rawName: packageName, - targetFiles: rawArgs || '', - rawArgs: rawArgs.toString() - }; + targetFiles: rawArgs || [], + rawArgs: rawArgs.toString(), + blueprint, + ciProvider, + }); - if (!validProjectName(packageName)) { - return Promise.reject(new SilentError('We currently do not support a name of `' + packageName + '`.')); + let { targetFiles } = blueprintOpts; + + deprecate( + `Do not pass file names or globs to \`init\`. Passed: "${targetFiles.join(' ')}"`, + targetFiles.length === 0, + DEPRECATIONS.INIT_TARGET_FILES.options + ); + + if (!isValidProjectName(packageName)) { + return Promise.reject(new SilentError(`We currently do not support a name of \`${packageName}\`.`)); } - blueprintOpts.blueprint = normalizeBlueprint(blueprintOpts.blueprint); - - debug('before:installblueprint'); - return installBlueprint.run(blueprintOpts) - .then(function() { - debug('after:installblueprint'); - if (!commandOptions.skipNpm) { - return npmInstall.run({ - verbose: commandOptions.verbose, - optional: false - }); - } - }) - .then(function() { - if (!commandOptions.skipBower) { - return bowerInstall.run({ - verbose: commandOptions.verbose - }); - } + await this.runTask('InstallBlueprint', blueprintOpts); + + if (!commandOptions.skipNpm) { + await this.runTask('NpmInstall', { + verbose: commandOptions.verbose, + packageManager: commandOptions.packageManager, }); - } + } + + if (commandOptions.skipGit === false) { + await this.runTask('GitInit', commandOptions, rawArgs); + } + + const projectName = this.project.name(); + const prependEmoji = require('@ember-tooling/blueprint-model/utilities/prepend-emoji'); + + this.ui.writeLine(''); + this.ui.writeLine(prependEmoji('🎉', `Successfully created project ${chalk.yellow(projectName)}.`)); + this.ui.writeLine(prependEmoji('👉', 'Get started by typing:')); + this.ui.writeLine(''); + const command = `cd ${projectName}`; + this.ui.writeLine(` ${chalk.gray('$')} ${chalk.cyan(command)}`); + this.ui.writeLine(` ${chalk.gray('$')} ${chalk.cyan(`${commandOptions.packageManager ?? 'npm'} start`)}`); + this.ui.writeLine(''); + this.ui.writeLine('Happy coding!'); + + deprecate( + "Don't use `--embroider` option. Use `-b @ember/app-blueprint` instead.", + !commandOptions.embroider, + DEPRECATIONS.EMBROIDER.options + ); + }, }); diff --git a/lib/commands/install-addon.js b/lib/commands/install-addon.js deleted file mode 100644 index a29e531d05..0000000000 --- a/lib/commands/install-addon.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -var InstallCommand = require('./install'); -var chalk = require('chalk'); - -module.exports = InstallCommand.extend({ - name: 'install:addon', - description: 'This command has been deprecated. Please use `ember install` instead.', - works: 'insideProject', - skipHelp: true, - - anonymousOptions: [ - '' - ], - - run: function() { - var warning = 'This command has been deprecated. Please use `ember '; - warning += 'install ` instead.'; - this.ui.writeLine(chalk.red(warning)); - return this._super.run.apply(this, arguments); - } -}); diff --git a/lib/commands/install-bower.js b/lib/commands/install-bower.js deleted file mode 100644 index 62210035e3..0000000000 --- a/lib/commands/install-bower.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -var Command = require('../models/command'); -var SilentError = require('silent-error'); -var Promise = require('../ext/promise'); - -module.exports = Command.extend({ - name: 'install:bower', - description: 'Bower package install are now managed by the user.', - works: 'insideProject', - skipHelp: true, - - anonymousOptions: [ - '' - ], - - run: function() { - var err = 'This command has been removed. Please use `bower install '; - err += ' --save-dev --save-exact` instead.'; - return Promise.reject(new SilentError(err)); - } -}); diff --git a/lib/commands/install-npm.js b/lib/commands/install-npm.js deleted file mode 100644 index 7473fa92e0..0000000000 --- a/lib/commands/install-npm.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -var Command = require('../models/command'); -var SilentError = require('silent-error'); -var Promise = require('../ext/promise'); - -module.exports = Command.extend({ - name: 'install:npm', - description: 'Npm package installs are now managed by the user.', - works: 'insideProject', - skipHelp: true, - - anonymousOptions: [ - '' - ], - - run: function() { - var err = 'This command has been removed. Please use `npm install '; - err += ' --save-dev --save-exact` instead.'; - return Promise.reject(new SilentError(err)); - } -}); diff --git a/lib/commands/install.js b/lib/commands/install.js index d6366ca154..c9524ce4a3 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -1,38 +1,53 @@ 'use strict'; -var Command = require('../models/command'); -var SilentError = require('silent-error'); -var Promise = require('../ext/promise'); +const Command = require('../models/command'); +const SilentError = require('silent-error'); +const { determineInstallCommand } = require('../utilities/package-managers'); module.exports = Command.extend({ name: 'install', description: 'Installs an ember-cli addon from npm.', + aliases: ['i'], works: 'insideProject', - anonymousOptions: [ - '' + availableOptions: [ + { name: 'save', type: Boolean, default: false, aliases: ['S'] }, + { name: 'save-dev', type: Boolean, default: true, aliases: ['D'] }, + { name: 'save-exact', type: Boolean, default: false, aliases: ['E', 'exact'] }, + { + name: 'package-manager', + type: ['npm', 'pnpm', 'yarn'], + description: + 'Use this option to force the usage of a specific package manager. ' + + 'By default, ember-cli will try to detect the right package manager ' + + 'from any lockfiles that exist in your project.', + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + }, ], - run: function(commandOptions, addonNames) { + anonymousOptions: [''], + + async run(commandOptions, addonNames) { if (!addonNames.length) { - var msg = 'The `install` command must take an argument with the name'; - msg += ' of an ember-cli addon. For installing all npm and bower '; - msg += 'dependencies you can run `npm install && bower install`.'; - return Promise.reject(new SilentError(msg)); + let installCommand = await determineInstallCommand(this.project.root); + + throw new SilentError( + `An addon's name is required when running the \`install\` command. If you want to install all node modules, please run \`${installCommand}\` instead.` + ); } - var AddonInstallTask = this.tasks.AddonInstall; - var addonInstall = new AddonInstallTask({ - ui: this.ui, - analytics: this.analytics, - project: this.project, - NpmInstallTask: this.tasks.NpmInstall, - BlueprintTask: this.tasks.GenerateFromBlueprint + return this.runTask('AddonInstall', { + packages: addonNames, + blueprintOptions: commandOptions, }); + }, - return addonInstall.run({ - 'packages': addonNames, - blueprintOptions: commandOptions - }); - } + _env() { + return { + ui: this.ui, + project: this.project, + NpmInstallTask: this.tasks.NpmInstall, + BlueprintTask: this.tasks.GenerateFromBlueprint, + }; + }, }); diff --git a/lib/commands/new.js b/lib/commands/new.js index af96bb7230..5ce6f9e776 100644 --- a/lib/commands/new.js +++ b/lib/commands/new.js @@ -1,91 +1,170 @@ 'use strict'; -var chalk = require('chalk'); -var Command = require('../models/command'); -var Promise = require('../ext/promise'); -var Project = require('../models/project'); -var SilentError = require('silent-error'); -var validProjectName = require('../utilities/valid-project-name'); -var normalizeBlueprint = require('../utilities/normalize-blueprint-option'); +const { default: chalk } = require('chalk'); +const Command = require('../models/command'); +const Project = require('../models/project'); +const SilentError = require('silent-error'); +const isValidProjectName = require('../utilities/valid-project-name'); +const normalizeBlueprint = require('../utilities/normalize-blueprint-option'); +const mergeBlueprintOptions = require('../utilities/merge-blueprint-options'); +const { deprecate, DEPRECATIONS } = require('../debug'); module.exports = Command.extend({ name: 'new', - description: 'Creates a new directory and runs ' + chalk.green('ember init') + ' in it.', - works: 'outsideProject', + description: `Creates a new directory and runs ${chalk.green('ember init')} in it.`, + works: 'everywhere', availableOptions: [ { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, { name: 'blueprint', type: String, default: 'app', aliases: ['b'] }, - { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn'] }, - { name: 'skip-bower', type: Boolean, default: false, aliases: ['sb'] }, + { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn', 'skip-install', 'si'] }, { name: 'skip-git', type: Boolean, default: false, aliases: ['sg'] }, - { name: 'directory', type: String , aliases: ['dir'] } + { + name: 'welcome', + type: Boolean, + default: true, + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + }, + { + name: 'package-manager', + type: ['npm', 'pnpm', 'yarn'], + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + }, + { name: 'directory', type: String, aliases: ['dir'] }, + { + name: 'lang', + type: String, + description: 'Sets the base human language of the application via index.html', + }, + { name: 'lint-fix', type: Boolean, default: true }, + { + name: 'embroider', + type: Boolean, + default: false, + description: 'Deprecated: Enables the build system to use Embroider', + }, + { + name: 'ci-provider', + type: ['github', 'none'], + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + }, + { + name: 'ember-data', + type: Boolean, + default: true, + description: 'Include ember-data dependencies and configuration', + }, + { + name: 'interactive', + type: Boolean, + default: false, + aliases: ['i'], + description: 'Create a new Ember app/addon in an interactive way.', + }, + { + name: 'typescript', + type: Boolean, + default: false, + description: 'Set up the app to use TypeScript', + aliases: ['ts'], + }, + { + name: 'strict', + type: Boolean, + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + }, ], - anonymousOptions: [ - '' - ], + anonymousOptions: [''], + + beforeRun: mergeBlueprintOptions, + + async run(commandOptions, rawArgs) { + deprecate( + "Don't use `--embroider` option. Generating a new project will use the latest Vite-based embroider by default.", + !commandOptions.embroider, + DEPRECATIONS.EMBROIDER.options + ); - run: function(commandOptions, rawArgs) { - var packageName = rawArgs[0], - message; + let projectName = rawArgs[0], + message; commandOptions.name = rawArgs.shift(); - if (!packageName) { - message = chalk.yellow('The `ember ' + this.name + '` command requires a ' + - 'name to be specified. For more details, use `ember help`.'); + if (!projectName || commandOptions.interactive) { + let answers = await this.runTask('InteractiveNew', commandOptions); - return Promise.reject(new SilentError(message)); + commandOptions.blueprint = answers.blueprint; + + if (answers.name) { + projectName = answers.name; + commandOptions.name = answers.name; + } + + if (answers.emberData) { + commandOptions.emberData = answers.emberData; + } + + if (answers.lang) { + commandOptions.lang = answers.lang; + } + + if (answers.packageManager) { + commandOptions.packageManager = answers.packageManager; + } + + if (answers.ciProvider) { + commandOptions.ciProvider = answers.ciProvider; + } } - if (commandOptions.dryRun){ + if (!commandOptions.ciProvider) { + commandOptions.ciProvider = 'github'; + } + + if (commandOptions.dryRun) { commandOptions.skipGit = true; } - if (packageName === '.') { - message = 'Trying to generate an application structure in this directory? Use `ember init` instead.'; + if (projectName === '.') { + let blueprintName = commandOptions.blueprint === 'app' ? 'application' : commandOptions.blueprint; + message = `Trying to generate an ${blueprintName} structure in this directory? Use \`ember init\` instead.`; return Promise.reject(new SilentError(message)); } - if (!validProjectName(packageName)) { - message = 'We currently do not support a name of `' + packageName + '`.'; + if (!isValidProjectName(projectName)) { + message = `We currently do not support a name of \`${projectName}\`.`; return Promise.reject(new SilentError(message)); } commandOptions.blueprint = normalizeBlueprint(commandOptions.blueprint); - if (!commandOptions.directory) { - commandOptions.directory = packageName; - } + let InitCommand = this.commands.Init; - var createAndStepIntoDirectory = new this.tasks.CreateAndStepIntoDirectory({ + let initCommand = new InitCommand({ ui: this.ui, - analytics: this.analytics + tasks: this.tasks, + project: Project.nullProject(this.ui, this.cli), }); - var InitCommand = this.commands.Init; - var gitInit = new this.tasks.GitInit({ - ui: this.ui, - project: this.project + await this.runTask('CreateAndStepIntoDirectory', { + projectName, + directoryName: commandOptions.directory, + dryRun: commandOptions.dryRun, }); - var initCommand = new InitCommand({ - ui: this.ui, - analytics: this.analytics, - tasks: this.tasks, - project: Project.nullProject(this.ui, this.cli) - }); + initCommand.project.root = process.cwd(); + + deprecate( + "Don't use `--embroider` option. Generating a new project will use the latest Vite-based embroider by default.", + !commandOptions.embroider, + DEPRECATIONS.EMBROIDER.options + ); - return createAndStepIntoDirectory - .run({ - directoryName: commandOptions.directory, - dryRun: commandOptions.dryRun - }) - .then(initCommand.run.bind(initCommand, commandOptions, rawArgs)) - .then(gitInit.run.bind(gitInit, commandOptions, rawArgs)); - } + return await initCommand.run(commandOptions, rawArgs); + }, }); diff --git a/lib/commands/serve.js b/lib/commands/serve.js index aa23d82dd2..bfbc99da37 100644 --- a/lib/commands/serve.js +++ b/lib/commands/serve.js @@ -1,70 +1,146 @@ 'use strict'; -var assign = require('lodash/object/assign'); -var path = require('path'); -var Command = require('../models/command'); -var Promise = require('../ext/promise'); -var SilentError = require('silent-error'); -var PortFinder = require('portfinder'); -var win = require('../utilities/windows-admin'); +const Command = require('../models/command'); +const SilentError = require('silent-error'); +const PortFinder = require('portfinder'); +const Win = require('../utilities/windows-admin'); +const EOL = require('os').EOL; -PortFinder.basePort = 49152; - -var getPort = Promise.denodeify(PortFinder.getPort); +const DEFAULT_PORT = 4200; module.exports = Command.extend({ name: 'serve', - description: 'Builds and serves your app, rebuilding on file changes.', + description: 'Vestigial command in Vite-based projects. Use the `start` script from package.json instead.', + skipHelp: true, aliases: ['server', 's'], availableOptions: [ - { name: 'port', type: Number, default: process.env.PORT || 4200, aliases: ['p'] }, - { name: 'host', type: String, default: '0.0.0.0', aliases: ['H'] }, - { name: 'proxy', type: String, aliases: ['pr','pxy'] }, - { name: 'insecure-proxy', type: Boolean, default: false, description: 'Set false to proxy self-signed SSL certificates', aliases: ['inspr'] }, - { name: 'watcher', type: String, default: 'events', aliases: ['w'] }, - { name: 'live-reload', type: Boolean, default: true, aliases: ['lr'] }, - { name: 'live-reload-host', type: String, description: 'Defaults to host', aliases: ['lrh'] }, - { name: 'live-reload-port', type: Number, description: '(Defaults to port number within [49152...65535] )', aliases: ['lrp']}, - { name: 'environment', type: String, default: 'development', aliases: ['e', {'dev' : 'development'}, {'prod' : 'production'}] }, - { name: 'output-path', type: path, default: 'dist/', aliases: ['op', 'out'] }, - { name: 'ssl', type: Boolean, default: false }, - { name: 'ssl-key', type: String, default: 'ssl/server.key' }, - { name: 'ssl-cert', type: String, default: 'ssl/server.crt' } + { + name: 'port', + type: Number, + default: process.env.PORT || DEFAULT_PORT, + aliases: ['p'], + description: `Overrides $PORT (currently ${ + process.env.PORT || 'blank' + }). If the port 0 or the default port 4200 is passed, ember will use any available port starting from 4200.`, + }, + { name: 'host', type: String, aliases: ['H'], description: 'Listens on all interfaces by default' }, + { name: 'proxy', type: String, aliases: ['pr', 'pxy'] }, + { + name: 'proxy-in-timeout', + type: Number, + default: 120000, + aliases: ['pit'], + description: 'When using --proxy: timeout (in ms) for incoming requests', + }, + { + name: 'proxy-out-timeout', + type: Number, + default: 0, + aliases: ['pot'], + description: 'When using --proxy: timeout (in ms) for outgoing requests', + }, + { + name: 'secure-proxy', + type: Boolean, + default: true, + aliases: ['spr'], + description: 'Set to false to proxy self-signed SSL certificates', + }, + { + name: 'transparent-proxy', + type: Boolean, + default: true, + aliases: ['transp'], + description: 'Set to false to omit x-forwarded-* headers when proxying', + }, + { name: 'watcher', type: String, default: 'events', aliases: ['w'] }, + { name: 'live-reload', type: Boolean, default: true, aliases: ['lr'] }, + { name: 'live-reload-host', type: String, aliases: ['lrh'], description: 'Defaults to host' }, + { name: 'live-reload-base-url', type: String, aliases: ['lrbu'], description: 'Defaults to rootURL' }, + { name: 'live-reload-port', type: Number, aliases: ['lrp'], description: 'Defaults to same port as ember app' }, + { name: 'live-reload-prefix', type: String, default: '_lr', aliases: ['lrprefix'], description: 'Default to _lr' }, + { + name: 'environment', + type: String, + default: 'development', + aliases: ['e', { dev: 'development' }, { prod: 'production' }], + description: 'Possible values are "development", "production", and "test".', + }, + { name: 'output-path', type: 'Path', default: 'dist/', aliases: ['op', 'out'] }, + { + name: 'ssl', + type: Boolean, + default: false, + description: 'Set to true to configure Ember CLI to serve using SSL.', + }, + { + name: 'ssl-key', + type: String, + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + }, + { + name: 'ssl-cert', + type: String, + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + }, + { name: 'path', type: 'Path', description: 'Reuse an existing build at given path.' }, ], - run: function(commandOptions) { - var liveReloadHost = commandOptions.liveReloadHost || commandOptions.host; - var liveReloadPort = commandOptions.liveReloadPort ? Promise.resolve(commandOptions.liveReloadPort) : - this._getPort({ host: liveReloadHost }); + init() { + this._super.apply(this, arguments); + if (!this.isViteProject) { + this.skipHelp = false; + this.description = 'Builds and serves your app, rebuilding on file changes.'; + } + }, - return liveReloadPort.then(function(liveReloadPort) { - commandOptions = assign({}, commandOptions, { - liveReloadPort: liveReloadPort, - liveReloadHost: liveReloadHost, - baseURL: this.project.config(commandOptions.environment).baseURL || '/' - }); + async run(commandOptions) { + if (this.isViteProject) { + return Promise.reject( + new SilentError( + 'The `serve` command is not supported in Vite-based projects. Please use the `start` script from package.json.' + ) + ); + } - if (commandOptions.proxy) { - if (!commandOptions.proxy.match(/^(http:|https:)/)) { - var message = 'You need to include a protocol with the proxy URL.\nTry --proxy http://' + commandOptions.proxy; + commandOptions.liveReloadHost = commandOptions.liveReloadHost || commandOptions.host; - return Promise.reject(new SilentError(message)); - } - } + let wrappedCommandOptions = await this._checkOrGetPort(commandOptions); + if (wrappedCommandOptions.proxy) { + if (!/^(http:|https:)/.test(wrappedCommandOptions.proxy)) { + let message = `You need to include a protocol with the proxy URL.${EOL}Try --proxy http://${wrappedCommandOptions.proxy}`; - var ServeTask = this.tasks.Serve; - var serve = new ServeTask({ - ui: this.ui, - analytics: this.analytics, - project: this.project - }); + return Promise.reject(new SilentError(message)); + } + } - return win.checkWindowsElevation(this.ui).then(function() { - return serve.run(commandOptions); - }); - }.bind(this)); + await Win.checkIfSymlinksNeedToBeEnabled(this.ui); + await this.runTask('Serve', commandOptions); }, - _getPort: getPort + async _checkOrGetPort(commandOptions) { + let portOptions = { port: commandOptions.port || DEFAULT_PORT, host: commandOptions.host }; + if (commandOptions.port !== 0 && commandOptions.port !== DEFAULT_PORT) { + // if a port was set, only check for that port + portOptions.stopPort = commandOptions.port; + } + try { + commandOptions.port = await PortFinder.getPortPromise(portOptions); + commandOptions.liveReloadPort = commandOptions.liveReloadPort || commandOptions.port; + return commandOptions; + } catch (err) { + let message; + if (portOptions.port === DEFAULT_PORT) { + message = `No open port found above ${portOptions.port}`; + } else if (commandOptions.port < 1024) { + message = `Port ${commandOptions.port} is already in use or you do not have permission to use this port.`; + } else { + message = `Port ${commandOptions.port} is already in use.`; + } + throw new SilentError(message); + } + }, }); diff --git a/lib/commands/test.js b/lib/commands/test.js index 08cb77761d..5dda94ff7d 100644 --- a/lib/commands/test.js +++ b/lib/commands/test.js @@ -1,142 +1,241 @@ 'use strict'; -var Command = require('../models/command'); -var Watcher = require('../models/watcher'); -var Builder = require('../models/builder'); - -var fs = require('fs'); -var path = require('path'); -var win = require('../utilities/windows-admin'); - -var defaultPortNumber = 7357; +const Command = require('../models/command'); +const Watcher = require('../models/watcher'); +const Builder = require('../models/builder'); +const SilentError = require('silent-error'); +const os = require('os'); +const path = require('path'); +const Win = require('../utilities/windows-admin'); +const fs = require('fs'); +const util = require('util'); +const PortFinder = require('portfinder'); +let getPort = util.promisify(PortFinder.getPort); + +let defaultPort = 7357; + +const ClassicOptions = [ + { name: 'server', type: Boolean, default: false, aliases: ['s'] }, + { name: 'output-path', type: 'Path', aliases: ['o'] }, +]; module.exports = Command.extend({ name: 'test', + description: "Runs your app's test suite.", aliases: ['t'], - description: 'Runs your app\'s test suite.', availableOptions: [ - { name: 'environment', type: String, default: 'test', aliases: ['e'] }, - { name: 'config-file', type: String, default: './testem.json', aliases: ['c', 'cf'] }, - { name: 'server', type: Boolean, default: false, aliases: ['s'] }, - { name: 'host', type: String, aliases: ['H'] }, - { name: 'test-port', type: Number, default: defaultPortNumber, description: 'The test port to use when running with --server.', aliases: ['tp'] }, - { name: 'filter', type: String, description: 'A string to filter tests to run', aliases: ['f'] }, - { name: 'module', type: String, description: 'The name of a test module to run', aliases: ['m'] }, - { name: 'watcher', type: String, default: 'events', aliases: ['w'] }, - { name: 'launch', type: String, default: false, description: 'A comma separated list of browsers to launch for tests.' }, - { name: 'reporter', type: String, description: 'Test reporter to use [tap|dot|xunit]', aliases: ['r']}, - { name: 'test-page', type: String, description: 'Test page to invoke'} + { + name: 'environment', + type: String, + default: 'test', + aliases: ['e'], + description: 'Possible values are "development", "production", and "test".', + }, + { name: 'config-file', type: String, aliases: ['c', 'cf'] }, + { name: 'host', type: String, aliases: ['H'] }, + { + name: 'test-port', + type: Number, + default: defaultPort, + aliases: ['tp'], + description: 'The test port to use when running tests. Pass 0 to automatically pick an available port', + }, + { name: 'filter', type: String, aliases: ['f'], description: 'A string to filter tests to run' }, + { name: 'module', type: String, aliases: ['m'], description: 'The name of a test module to run' }, + { name: 'watcher', type: String, default: 'events', aliases: ['w'] }, + { + name: 'launch', + type: String, + default: false, + description: 'A comma separated list of browsers to launch for tests.', + }, + { + name: 'reporter', + type: String, + aliases: ['r'], + description: 'Test reporter to use [tap|dot|xunit] (default: tap)', + }, + { name: 'silent', type: Boolean, default: false, description: 'Suppress any output except for the test report' }, + { + name: 'ssl', + type: Boolean, + default: false, + description: 'Set to true to configure testem to run the test suite using SSL.', + }, + { + name: 'ssl-key', + type: String, + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + }, + { + name: 'ssl-cert', + type: String, + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + }, + { name: 'testem-debug', type: String, description: 'File to write a debug log from testem' }, + { name: 'test-page', type: String, description: 'Test page to invoke' }, + { name: 'path', type: 'Path', description: 'Reuse an existing build at given path.' }, + { name: 'query', type: String, description: 'A query string to append to the test page URL.' }, ], - init: function() { - this.assign = require('lodash/object/assign'); - this.quickTemp = require('quick-temp'); + init() { + this._super.apply(this, arguments); + + if (!this.isViteProject) { + this.availableOptions = this.availableOptions.concat(ClassicOptions); + } this.Builder = this.Builder || Builder; this.Watcher = this.Watcher || Watcher; - if (!this.testing) { process.env.EMBER_CLI_TEST_COMMAND = true; } }, - tmp: function() { - return this.quickTemp.makeOrRemake(this, '-testsDist'); + tmp() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'tests-dist-')); }, - rmTmp: function() { - this.quickTemp.remove(this, '-testsDist'); - this.quickTemp.remove(this, '-customConfigFile'); - }, + _generateCustomConfigs(options) { + let config = {}; + if (!options.filter && !options.module && !options.launch && !options.query && !options['test-page']) { + return config; + } - _generateCustomConfigFile: function(options) { - if (!options.filter && !options.module && !options.launch && !options['test-page']) { return options.configFile; } + let testPage = options['test-page']; + let queryString = this.buildTestPageQueryString(options); + if (testPage) { + let containsQueryString = testPage.indexOf('?') > -1; + let testPageJoinChar = containsQueryString ? '&' : '?'; + config.testPage = testPage + testPageJoinChar + queryString; + } + if (queryString) { + config.queryString = queryString; + } - var tmpPath = this.quickTemp.makeOrRemake(this, '-customConfigFile'); - var customPath = path.join(tmpPath, 'testem.json'); - var originalContents = JSON.parse(fs.readFileSync(options.configFile, { encoding: 'utf8' })); + if (options.launch) { + config.launch = options.launch; + } - var testPage = options['test-page']?options['test-page']:originalContents['test_page']; + return config; + }, - var containsQueryString = testPage.indexOf('?') > -1; - var testPageJoinChar = containsQueryString ? '&' : '?'; + async _generateTestPortNumber(options, ui) { + if (options.testPort === defaultPort && !options.port) { + let foundPort = await getPort({ port: options.testPort }); - originalContents['test_page'] = testPage + testPageJoinChar + this.buildTestPageQueryString(options); - if (options.launch) { - originalContents['launch'] = options.launch; + if (options.testPort !== foundPort) { + ui.writeInfoLine(`Default port ${options.testPort} is already in use. Using alternate port ${foundPort}`); + } + + return foundPort; } - fs.writeFileSync(customPath, JSON.stringify(originalContents)); - return customPath; - }, + let port = parseInt(options.testPort, 10); + if ((options.port && options.testPort !== defaultPort) || (!isNaN(parseInt(options.testPort)) && !options.port)) { + port = parseInt(options.testPort, 10); + } else if (options.port) { + port = parseInt(options.port, 10) + 1; + } - _generateTestPortNumber: function(options) { - if (options.port && options.testPort !== defaultPortNumber || options.testPort && !options.port) { return options.testPort; } - if (options.port) { return parseInt(options.port, 10) + 1; } + return port; }, - buildTestPageQueryString: function(options) { - var params = []; + buildTestPageQueryString(options) { + let params = []; if (options.module) { - params.push('module=' + options.module); + params.push(`module=${options.module}`); } if (options.filter) { - params.push('filter=' + options.filter.toLowerCase()); + params.push(`filter=${options.filter.toLowerCase()}`); + } + + if (options.query) { + params.push(options.query); } return params.join('&'); }, - run: function(commandOptions) { - var outputPath = this.tmp(); - process.env['EMBER_CLI_TEST_OUTPUT'] = outputPath; - var testOptions = this.assign({}, commandOptions, { - outputPath: outputPath, - project: this.project, - configFile: this._generateCustomConfigFile(commandOptions), - port: this._generateTestPortNumber(commandOptions) - }); - - var options = { - ui: this.ui, - analytics: this.analytics, - project: this.project - }; + async run(commandOptions) { + if (this.isViteProject) { + if (commandOptions.outputPath) { + return Promise.reject( + new SilentError('The `--output-path` option to `ember test` is not supported in Vite-based projects.') + ); + } + + if (commandOptions.server) { + return Promise.reject( + new SilentError( + 'The `--server` option to `ember test` is not supported in Vite-based projects. Please use the `start` script from package.json and visit `/tests` in the browser.' + ) + ); + } + } - if (commandOptions.server) { - options.builder = new this.Builder(testOptions); + let hasBuild = !!commandOptions.path; + let outputPath; - var TestServerTask = this.tasks.TestServer; - var testServer = new TestServerTask(options); + if (hasBuild) { + outputPath = path.resolve(commandOptions.path); - testOptions.watcher = new this.Watcher(this.assign(options, { - verbose: false, - options: commandOptions - })); + if (!fs.existsSync(outputPath)) { + throw new SilentError( + `The path ${commandOptions.path} does not exist. Please specify a valid build directory to test.` + ); + } + } else { + outputPath = commandOptions.outputPath || this.tmp(); + } + + process.env['EMBER_CLI_TEST_OUTPUT'] = outputPath; - return testServer.run(testOptions) - .finally(this.rmTmp.bind(this)); + let testOptions = Object.assign( + {}, + commandOptions, + { + ui: this.ui, + outputPath, + project: this.project, + port: await this._generateTestPortNumber(commandOptions, this.ui), + }, + this._generateCustomConfigs(commandOptions) + ); + + await Win.checkIfSymlinksNeedToBeEnabled(this.ui); + let session; + if (commandOptions.server) { + if (hasBuild) { + session = this.runTask('TestServer', testOptions); + } else { + let builder = new this.Builder(testOptions); + testOptions.watcher = ( + await this.Watcher.build( + Object.assign(this._env(), { + builder, + verbose: false, + options: commandOptions, + }) + ) + ).watcher; + session = this.runTask('TestServer', testOptions).finally(() => builder.cleanup()); + } + } else if (hasBuild) { + session = this.runTask('Test', testOptions); } else { - var TestTask = this.tasks.Test; - var BuildTask = this.tasks.Build; - - var test = new TestTask(options); - var build = new BuildTask(options); - - return win.checkWindowsElevation(this.ui).then(function() { - return build.run({ - environment: commandOptions.environment, - outputPath: outputPath - }) - .then(function() { - return test.run(testOptions); - }) - .finally(this.rmTmp.bind(this)); - }.bind(this)); + await this.runTask('Build', { + environment: commandOptions.environment, + outputPath, + }); + session = await this.runTask('Test', testOptions); } - } + return session; + }, }); diff --git a/lib/commands/uninstall-npm.js b/lib/commands/uninstall-npm.js deleted file mode 100644 index c613b5ac39..0000000000 --- a/lib/commands/uninstall-npm.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -var Command = require('../models/command'); -var SilentError = require('silent-error'); -var Promise = require('../ext/promise'); - -module.exports = Command.extend({ - name: 'uninstall:npm', - description: 'Npm package uninstall are now managed by the user.', - works: 'insideProject', - skipHelp: true, - - anonymousOptions: [ - '' - ], - - run: function() { - var err = 'This command has been removed Please use `npm uninstall '; - err += ' --save-dev` instead.'; - return Promise.reject(new SilentError(err)); - } -}); diff --git a/lib/commands/unknown.js b/lib/commands/unknown.js index e11426bb9a..c1c4fda059 100644 --- a/lib/commands/unknown.js +++ b/lib/commands/unknown.js @@ -1,19 +1,18 @@ 'use strict'; -var Command = require('../models/command'); -var SilentError = require('silent-error'); -var chalk = require('chalk'); +const Command = require('../models/command'); +const SilentError = require('silent-error'); +const { default: chalk } = require('chalk'); module.exports = Command.extend({ skipHelp: true, + unknown: true, - printBasicHelp: function() { - this.ui.writeLine(chalk.red('No help entry for \'' + this.commandName + '\'')); + printBasicHelp() { + return chalk.red(`No help entry for '${this.name}'`); }, - validateAndRun: function() { - throw new SilentError('The specified command ' + this.commandName + - ' is invalid. For available options, see' + - ' `ember help`.'); - } + validateAndRun() { + throw new SilentError(`The specified command ${this.name} is invalid. For available options, see \`ember help\`.`); + }, }); diff --git a/lib/commands/version.js b/lib/commands/version.js index ba65bb4ecd..4b20bb50a2 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -1,31 +1,32 @@ 'use strict'; -var Command = require('../models/command'); +const Command = require('../models/command'); +const emberCLIVersion = require('../utilities/version-utils').emberCLIVersion; module.exports = Command.extend({ name: 'version', description: 'outputs ember-cli version', + aliases: ['v', '--version', '-v'], works: 'everywhere', - availableOptions: [ - { name: 'verbose', type: Boolean, default: false } - ], + availableOptions: [{ name: 'verbose', type: Boolean, default: false }], - aliases: ['v', '--version', '-v'], - run: function(options) { - var versions = process.versions; - versions['npm'] = require('npm').version; - versions['os'] = process.platform + ' ' + process.arch; + run(options) { + this.printVersion('ember-cli', emberCLIVersion()); + + let versions = process.versions; + versions['os'] = `${process.platform} ${process.arch}`; - var alwaysPrint = ['node','npm', 'os']; + let alwaysPrint = ['node', 'os']; - for(var module in versions) { - if(options.verbose || alwaysPrint.indexOf(module) > -1) { + for (let module in versions) { + if (options.verbose || alwaysPrint.indexOf(module) > -1) { this.printVersion(module, versions[module]); } } }, - printVersion: function(module, version) { - this.ui.writeLine(module + ': ' + version); - } + + printVersion(module, version) { + this.ui.writeLine(`${module}: ${version}`); + }, }); diff --git a/lib/debug/assert.js b/lib/debug/assert.js new file mode 100644 index 0000000000..6e7f883f45 --- /dev/null +++ b/lib/debug/assert.js @@ -0,0 +1,37 @@ +'use strict'; + +/** + * Verify that a certain condition is met, or throw an error if otherwise. + * + * This is useful for communicating expectations in the code to other human + * readers as well as catching bugs that accidentally violate these expectations. + * + * ```js + * const { assert } = require('ember-cli/lib/debug'); + * + * // Test for truthiness: + * assert('Must pass a string.', typeof str === 'string'); + * + * // Fail unconditionally: + * assert('This code path should never run.'); + * ``` + * + * @method assert + * @param {String} description Describes the condition. + * This will become the message of the error thrown if the assertion fails. + * @param {Any} condition Must be truthy for the assertion to pass. + * If falsy, an error will be thrown. + */ +function assert(description, condition) { + if (!description) { + throw new Error('When calling `assert`, you must provide a description as the first argument.'); + } + + if (condition) { + return; + } + + throw new Error(`ASSERTION FAILED: ${description}`); +} + +module.exports = assert; diff --git a/lib/debug/deprecate.js b/lib/debug/deprecate.js new file mode 100644 index 0000000000..4cdd6f55b0 --- /dev/null +++ b/lib/debug/deprecate.js @@ -0,0 +1,25 @@ +'use strict'; + +const emberCLIVersionPackageVersion = require('../../package').version; +const { makeDeprecate, isDeprecationRemoved } = require('semver-deprecate'); + +const emberCLIVersion = process.env.OVERRIDE_DEPRECATION_VERSION ?? emberCLIVersionPackageVersion; + +const deprecate = makeDeprecate('ember-cli', emberCLIVersion); + +/** + * This function deferrs to the upstream {@link isDeprecationRemoved} function from semver-deprecate + * but closes around the current emberCLIVersion for convenience since there is some logic around + * what is considered the current version + * + * @private + * @method + * @param {string} until - a Semver formatted version when the deprecation will be removed + * @return {boolean} + */ +function _isDeprecationRemoved(until) { + return isDeprecationRemoved(until, emberCLIVersion); +} + +module.exports = deprecate; +module.exports._isDeprecationRemoved = _isDeprecationRemoved; diff --git a/lib/debug/deprecations.js b/lib/debug/deprecations.js new file mode 100644 index 0000000000..342a1913e0 --- /dev/null +++ b/lib/debug/deprecations.js @@ -0,0 +1,47 @@ +'use strict'; + +const { _isDeprecationRemoved } = require('./deprecate'); + +// eslint-disable-next-line no-unused-vars +function deprecation(options) { + return { + options, + isRemoved: _isDeprecationRemoved(options.until), + }; +} + +const DEPRECATIONS = { + V1_ADDON_CONTENT_FOR_TYPES: deprecation({ + for: 'ember-cli', + id: 'ember-cli.v1-addon-content-for-types', + since: { + available: '6.3.0', + }, + until: '7.0.0', + url: 'https://deprecations.emberjs.com/id/v1-addon-content-for-types', + meta: { + types: ['app-prefix', 'app-suffix', 'tests-prefix', 'tests-suffix', 'vendor-prefix', 'vendor-suffix'], + }, + }), + EMBROIDER: deprecation({ + for: 'ember-cli', + id: 'ember-cli.dont-use-embroider-option', + since: { + available: '6.8.0', + }, + until: '7.0.0', + url: 'https://deprecations.emberjs.com/id/dont-use-embroider-option', + }), + + INIT_TARGET_FILES: deprecation({ + for: 'ember-cli', + id: 'ember-cli.init-target-files', + since: { + available: '6.8.0', + }, + until: '7.0.0', + url: 'https://deprecations.emberjs.com/id/init-no-file-names', + }), +}; + +module.exports = DEPRECATIONS; diff --git a/lib/debug/index.js b/lib/debug/index.js new file mode 100644 index 0000000000..d8b2720115 --- /dev/null +++ b/lib/debug/index.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = { + assert: require('./assert'), + deprecate: require('./deprecate'), + DEPRECATIONS: require('./deprecations'), +}; diff --git a/lib/errors/silent.js b/lib/errors/silent.js deleted file mode 100644 index a4c06a2fee..0000000000 --- a/lib/errors/silent.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -var SilentError = require('silent-error'); -var deprecate = require('../utilities/deprecate'); - -Object.defineProperty(module, 'exports', { - get: function () { - deprecate('`ember-cli/lib/errors/silent.js` is deprecated,'+ - ' use `silent-error` instead.', true); - return SilentError; - } -}); diff --git a/lib/ext/promise.js b/lib/ext/promise.js deleted file mode 100644 index c83af6a0dc..0000000000 --- a/lib/ext/promise.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -var RSVP = require('rsvp'); -var Promise = RSVP.Promise; - -module.exports = PromiseExt; - -// Utility functions on the native CTOR need some massaging -module.exports.hash = function() { - return this.resolve(RSVP.hash.apply(null, arguments)); -}; - -module.exports.denodeify = function() { - var fn = RSVP.denodeify.apply(null, arguments); - var Constructor = this; - var newFn = function() { - return Constructor.resolve(fn.apply(null, arguments)); - }; - newFn.__proto__ = arguments[0]; - return newFn; -}; - -module.exports.filter = function() { - return this.resolve(RSVP.filter.apply(null, arguments)); -}; - -module.exports.map = function() { - return this.resolve(RSVP.map.apply(null, arguments)); -}; - -function PromiseExt(resolver, label) { - this._superConstructor(resolver, label); -} - -PromiseExt.prototype = Object.create(Promise.prototype); -PromiseExt.prototype.constructor = PromiseExt; -PromiseExt.prototype._superConstructor = Promise; -PromiseExt.__proto__ = Promise; - -PromiseExt.prototype.returns = function(value) { - return this.then(function() { - return value; - }); -}; - -PromiseExt.prototype.invoke = function(method) { - var args = Array.prototype.slice(arguments, 1); - - return this.then(function(value) { - return value[method].apply(value, args); - }, undefined, 'invoke: ' + method + ' with: ' + args); -}; - -function constructorMethod(promise, methodName, fn) { - var Constructor = promise.constructor; - - return promise.then(function(values) { - return Constructor[methodName](values, fn); - }); -} - -PromiseExt.prototype.map = function(mapFn) { - return constructorMethod(this, 'map', mapFn); -}; - -PromiseExt.prototype.filter = function(filterFn) { - return constructorMethod(this, 'filter', filterFn); -}; diff --git a/lib/models/addon-discovery.js b/lib/models/addon-discovery.js deleted file mode 100644 index 19a670a3f3..0000000000 --- a/lib/models/addon-discovery.js +++ /dev/null @@ -1,255 +0,0 @@ -'use strict'; - -/** -@module ember-cli -*/ - -var assign = require('lodash/object/assign'); -var debug = require('debug')('ember-cli:addon-discovery'); -var existsSync = require('exists-sync'); -var path = require('path'); -var CoreObject = require('core-object'); -var resolve = require('resolve'); -var findup = require('findup'); - -/** - AddonDiscovery is responsible for collecting information about all of the - addons that will be used with a project. - - @class AddonDiscovery - @extends CoreObject - @constructor -*/ - -function AddonDiscovery(ui) { - this.ui = ui; -} - -AddonDiscovery.__proto__ = CoreObject; -AddonDiscovery.prototype.constructor = AddonDiscovery; - -/** - This is one of the primary APIs for this class and is called by the project. - It returns a tree of plain objects that each contain information about a - discovered addon. Each node has `name`, `path`, `pkg` and - `childAddons` properties. The latter is an array containing any addons - discovered from applying the discovery process to that addon. - - @private - @method discoverProjectAddons - */ -AddonDiscovery.prototype.discoverProjectAddons = function(project) { - var projectAsAddon = this.discoverFromProjectItself(project); - var internalAddons = this.discoverFromInternalProjectAddons(project); - var cliAddons = this.discoverFromCli(project.cli); - var dependencyAddons; - - if (project.hasDependencies()) { - dependencyAddons = this.discoverFromDependencies(project.root, project.nodeModulesPath, project.pkg, false); - } else { - dependencyAddons = []; - } - - var inRepoAddons = this.discoverInRepoAddons(project.root, project.pkg); - var addons = projectAsAddon.concat(cliAddons, internalAddons, dependencyAddons, inRepoAddons); - - return addons; -}; - -/** - This is one of the primary APIs for this class and is called by addons. - It returns a tree of plain objects that each contain information about a - discovered addon. Each node has `name`, `path`, `pkg` and - `childAddons` properties. The latter is an array containing any addons - discovered from applying the discovery process to that addon. - - @private - @method discoverProjectAddons - */ -AddonDiscovery.prototype.discoverChildAddons = function(addon) { - debug('discoverChildAddons: %s(%s)', addon.name, addon.root); - var dependencyAddons = this.discoverFromDependencies(addon.root, addon.nodeModulesPath, addon.pkg, true); - var inRepoAddons = this.discoverInRepoAddons(addon.root, addon.pkg); - var addons = dependencyAddons.concat(inRepoAddons); - return addons; -}; - -/** - Returns an array containing zero or one nodes, depending on whether or not - the passed project is an addon. - - @private - @method discoverFromProjectItself - */ -AddonDiscovery.prototype.discoverFromProjectItself = function(project) { - if (project.isEmberCLIAddon()) { - var addonPkg = this.discoverAtPath(project.root); - if (addonPkg) { - return [addonPkg]; - } - } - return []; -}; - -/** - Returns a tree based on the addons referenced in the provided `pkg` through - the package.json `dependencies` and optionally `devDependencies` collections, - as well as those discovered addons' child addons. - - @private - @method discoverFromDependencies - */ -AddonDiscovery.prototype.discoverFromDependencies = function(root, nodeModulesPath, pkg, excludeDevDeps) { - var discovery = this; - var addons = Object.keys(this.dependencies(pkg, excludeDevDeps)).map(function(name) { - if (name !== 'ember-cli') { - var addonPath = this.resolvePackage(root, name); - - if (addonPath) { - return discovery.discoverAtPath(addonPath); - } - - // this supports packages that do not have a valid entry point - // script (aka `main` entry in `package.json` or `index.js`) - addonPath = path.join(nodeModulesPath, name); - var addon = discovery.discoverAtPath(addonPath); - if (addon) { - var chalk = require('chalk'); - - discovery.ui.writeLine(chalk.yellow('The package `' + name + '` is not a properly formatted package, we have used a fallback lookup to resolve it at `' + addonPath + '`. This is generally caused by an addon not having a `main` entry point (or `index.js`).'), 'WARNING'); - - return addon; - } - } - }, this).filter(Boolean); - return addons; -}; - -AddonDiscovery.prototype.resolvePackage = function(root, packageName) { - try { - - var entryModulePath = resolve.sync(packageName, { basedir: root }); - - return findup.sync(entryModulePath, 'package.json'); - } catch(e) { - var acceptableError = 'Cannot find module \'' + packageName + '\' from \'' + root + '\''; - // pending: https://github.com/substack/node-resolve/pull/80 - var workAroundError = 'Cannot read property \'isFile\' of undefined'; - - if (e.message === workAroundError || e.message === acceptableError) { - return; - } - throw e; - } -}; - -/** - Returns a tree based on the in-repo addons referenced in the provided `pkg` - through paths listed in the `ember-addon` entry, as well as those discovered - addons' child addons. - - @private - @method discoverInRepoAddons - */ -AddonDiscovery.prototype.discoverInRepoAddons = function(root, pkg) { - if (!pkg || !pkg['ember-addon'] || !pkg['ember-addon'].paths) { - return []; - } - var discovery = this; - var addons = pkg['ember-addon'].paths.map(function(addonPath) { - addonPath = path.join(root, addonPath); - return discovery.discoverAtPath(addonPath); - }, this).filter(Boolean); - return addons; -}; - -/** - Returns a tree based on the internal addons that may be defined within the project. - It does this by consulting the projects `supportedInternalAddonPaths()` method, which - is primarily used for middleware addons. - - @private - @method discoverFromInternalProjectAddons - */ -AddonDiscovery.prototype.discoverFromInternalProjectAddons = function(project) { - var discovery = this; - return project.supportedInternalAddonPaths().map(function(path){ - return discovery.discoverAtPath(path); - }).filter(Boolean); -}; - -AddonDiscovery.prototype.discoverFromCli = function (cli) { - if (!cli) { return []; } - - var cliPkg = require(path.resolve(cli.root, 'package.json')); - return this.discoverInRepoAddons(cli.root, cliPkg); -}; - -/** - Given a particular path, return undefined if the path is not an addon, or if it is, - a node with the info about the addon. - - @private - @method discoverAtPath - */ -AddonDiscovery.prototype.discoverAtPath = function(addonPath) { - var pkgPath = path.join(addonPath, 'package.json'); - debug('attemping to add: %s', addonPath); - - if (existsSync(pkgPath)) { - var addonPkg = require(pkgPath); - var keywords = addonPkg.keywords || []; - debug(' - module found: %s', addonPkg.name); - - addonPkg['ember-addon'] = addonPkg['ember-addon'] || {}; - - if (keywords.indexOf('ember-addon') > -1) { - debug(' - is addon, adding...'); - var addonInfo = { - name: addonPkg.name, - path: addonPath, - pkg: addonPkg, - }; - return addonInfo; - } else { - debug(' - no ember-addon keyword, not including.'); - } - } else { - debug(' - no package.json (looked at ' + pkgPath + ').'); - } - - return null; -}; - -/** - Returns the dependencies from a package.json - - @private - @method dependencies - @param {Object} pkg Package object. If false, the current package is used. - @param {Boolean} excludeDevDeps Whether or not development dependencies should be excluded, defaults to false. - @return {Object} Dependencies - */ -AddonDiscovery.prototype.dependencies = function(pkg, excludeDevDeps) { - pkg = pkg || {}; - - var devDependencies = pkg['devDependencies']; - if (excludeDevDeps) { - devDependencies = {}; - } - - return assign({}, devDependencies, pkg['dependencies']); -}; - -AddonDiscovery.prototype.addonPackages = function(addonsList) { - var addonPackages = {}; - - addonsList.forEach(function(addonPkg) { - addonPackages[addonPkg.name] = addonPkg; - }); - - return addonPackages; -}; - -// Export -module.exports = AddonDiscovery; diff --git a/lib/models/addon-info.js b/lib/models/addon-info.js new file mode 100644 index 0000000000..e5583a28ae --- /dev/null +++ b/lib/models/addon-info.js @@ -0,0 +1,18 @@ +'use strict'; + +/** + * A simple class to store metadata info about an addon. This replaces a plain JS object + * that used to be created with the same fields. + * + * @module ember-cli + */ + +class AddonInfo { + constructor(name, path, pkg) { + this.name = name; + this.path = path; + this.pkg = pkg; + } +} + +module.exports = AddonInfo; diff --git a/lib/models/addon.js b/lib/models/addon.js index 47afb9f698..58f3fb054b 100644 --- a/lib/models/addon.js +++ b/lib/models/addon.js @@ -4,44 +4,121 @@ @module ember-cli */ -var existsSync = require('exists-sync'); -var path = require('path'); -var deprecate = require('../utilities/deprecate'); -var assign = require('lodash/object/assign'); -var SilentError = require('silent-error'); -var reexport = require('../utilities/reexport'); -var debug = require('debug')('ember-cli:addon'); - -var nodeModulesPath = require('node-modules-path'); +const fs = require('fs'); +const path = require('path'); +const SilentError = require('silent-error'); +const heimdallLogger = require('heimdalljs-logger'); +const logger = heimdallLogger('ember-cli:addon'); +const treeCacheLogger = heimdallLogger('ember-cli:addon:tree-cache'); +const cacheKeyLogger = heimdallLogger('ember-cli:addon:cache-key-for-tree'); +const PackageInfoCache = require('../models/package-info-cache'); + +const p = require('ember-cli-preprocess-registry/preprocessors'); +const preprocessJs = p.preprocessJs; +const preprocessCss = p.preprocessCss; +const preprocessTemplates = p.preprocessTemplates; + +const instantiateAddons = require('../models/instantiate-addons'); + +const CoreObject = require('core-object'); +const Project = require('../models/project'); + +const mergeTrees = require('../broccoli/merge-trees'); +const Funnel = require('broccoli-funnel'); +const walkSync = require('walk-sync'); +const ensurePosixPath = require('ensure-posix-path'); +const defaultsDeep = require('lodash/defaultsDeep'); +const heimdall = require('heimdalljs'); +const calculateCacheKeyForTree = require('calculate-cache-key-for-tree'); +const addonProcessTree = require('../utilities/addon-process-tree'); +const registryHasPreprocessor = require('../utilities/registry-has-preprocessor'); + +if (!heimdall.hasMonitor('addon-tree-cache')) { + heimdall.registerMonitor('addon-tree-cache', function AddonTreeCacheSchema() { + this.hits = 0; + this.misses = 0; + this.adds = 0; + }); +} -var p = require('ember-cli-preprocess-registry/preprocessors'); -var preprocessJs = p.preprocessJs; -var preprocessCss = p.preprocessCss; -var preprocessTemplates = p.preprocessTemplates; +if (!heimdall.hasMonitor('cache-key-for-tree')) { + heimdall.registerMonitor('cache-key-for-tree', function CacheKeyForTreeSchema() { + this.modifiedMethods = 0; + this.treeForMethodsOverride = 0; + }); +} -var AddonDiscovery = require('../models/addon-discovery'); -var AddonsFactory = require('../models/addons-factory'); -var CoreObject = require('core-object'); -var Project = require('./project'); +let DEFAULT_TREE_FOR_METHODS = { + app: 'treeForApp', + addon: 'treeForAddon', + 'addon-styles': 'treeForAddonStyles', + 'addon-templates': 'treeForAddonTemplates', + 'addon-test-support': 'treeForAddonTestSupport', + public: 'treeForPublic', + styles: 'treeForStyles', + templates: 'treeForTemplates', + 'test-support': 'treeForTestSupport', + vendor: 'treeForVendor', +}; -var upstreamMergeTrees = require('broccoli-merge-trees'); -var Funnel = require('broccoli-funnel'); -var walkSync = require('walk-sync'); +let GLOBAL_TREE_FOR_METHOD_METHODS = ['treeFor', '_treeFor', 'treeGenerator']; +let DEFAULT_TREE_FOR_METHOD_METHODS = { + app: ['treeForApp'], + addon: [ + 'treeForAddon', + 'treeForAddonStyles', + 'treeForAddonTemplates', + 'compileAddon', + 'processedAddonJsFiles', + 'compileTemplates', + '_addonTemplateFiles', + 'compileStyles', + 'preprocessJs', + ], + 'addon-styles': ['treeForAddonStyles'], + 'addon-templates': ['treeForAddonTemplates'], + 'addon-test-support': ['treeForAddonTestSupport', 'preprocessJs'], + public: ['treeForPublic'], + styles: ['treeForStyles'], + templates: ['treeForTemplates'], + 'test-support': ['treeForTestSupport'], + vendor: ['treeForVendor'], +}; -var WatchedDir = require('broccoli-source').WatchedDir; -var UnwatchedDir = require('broccoli-source').UnwatchedDir; +let ADDON_TREE_CACHE = { + __cache: Object.create(null), -function mergeTrees(inputTree, options) { - options = options || {}; - options.description = options.annotation; - var tree = upstreamMergeTrees(inputTree, options); + getItem(key) { + let addonTreeCacheStats = heimdall.statsFor('addon-tree-cache'); + let cachedValue = this.__cache[key]; - tree.description = options && options.description; + if (cachedValue) { + addonTreeCacheStats.hits++; + treeCacheLogger.info(`Cache Hit: ${key}`); + return cachedValue; + } else { + addonTreeCacheStats.misses++; + treeCacheLogger.info(`Cache Miss: ${key}`); + return null; + } + }, + + setItem(key, value) { + let hasValue = !!value; + heimdall.statsFor('addon-tree-cache').adds++; + treeCacheLogger.info(`Cache Add: ${key} - ${hasValue}`); + this.__cache[key] = value; + }, + + clear() { + this.__cache = Object.create(null); + }, +}; - return tree; +function _resetTreeCache() { + ADDON_TREE_CACHE.clear(); } - /** Root class for an Addon. If your addon module exports an Object this will be extended from this base class. If you export a constructor (function), @@ -49,718 +126,1362 @@ function mergeTrees(inputTree, options) { Hooks: - - {{#crossLink "Addon/config:method"}}config{{/crossLink}} - - {{#crossLink "Addon/blueprintsPath:method"}}blueprintsPath{{/crossLink}} - - {{#crossLink "Addon/includedCommands:method"}}includedCommands{{/crossLink}} - - {{#crossLink "Addon/serverMiddleware:method"}}serverMiddleware{{/crossLink}} - - {{#crossLink "Addon/postBuild:method"}}postBuild{{/crossLink}} - - {{#crossLink "Addon/preBuild:method"}}preBuild{{/crossLink}} - - {{#crossLink "Addon/buildError:method"}}buildError{{/crossLink}} - - {{#crossLink "Addon/included:method"}}included{{/crossLink}} - - {{#crossLink "Addon/postprocessTree:method"}}postprocessTree{{/crossLink}} - - {{#crossLink "Addon/treeFor:method"}}treeFor{{/crossLink}} + - {{#crossLink "Addon/config:method"}}{{/crossLink}} + - {{#crossLink "Addon/blueprintsPath:method"}}{{/crossLink}} + - {{#crossLink "Addon/includedCommands:method"}}{{/crossLink}} + - {{#crossLink "Addon/importTransforms:method"}}{{/crossLink}} + - {{#crossLink "Addon/serverMiddleware:method"}}{{/crossLink}} + - {{#crossLink "Addon/testemMiddleware:method"}}{{/crossLink}} + - {{#crossLink "Addon/postBuild:method"}}{{/crossLink}} + - {{#crossLink "Addon/preBuild:method"}}{{/crossLink}} + - {{#crossLink "Addon/outputReady:method"}}{{/crossLink}} + - {{#crossLink "Addon/buildError:method"}}{{/crossLink}} + - {{#crossLink "Addon/included:method"}}{{/crossLink}} + - {{#crossLink "Addon/shouldIncludeChildAddon:method"}}{{/crossLink}} + - {{#crossLink "Addon/setupPreprocessorRegistry:method"}}{{/crossLink}} + - {{#crossLink "Addon/preprocessTree:method"}}{{/crossLink}} + - {{#crossLink "Addon/postprocessTree:method"}}{{/crossLink}} + - {{#crossLink "Addon/lintTree:method"}}{{/crossLink}} + - {{#crossLink "Addon/contentFor:method"}}{{/crossLink}} + - {{#crossLink "Addon/treeFor:method"}}{{/crossLink}} @class Addon @extends CoreObject @constructor - @param {(Project|Addon)} parent The project or addon that directly depends on this addon + @param {Project|Addon} parent The project or addon that directly depends on this addon @param {Project} project The current project (deprecated) */ -function Addon(parent, project) { - this.parent = parent; - this.project = project; - this.ui = project && project.ui; - this.addonPackages = {}; - this.addons = []; - this.addonDiscovery = new AddonDiscovery(this.ui); - this.addonsFactory = new AddonsFactory(this, this.project); - this.registry = p.defaultRegistry(this); - this._didRequiredBuildPackages = false; - - if (!this.root) { - throw new Error('Addon classes must be instantiated with the `root` property'); - } - this.nodeModulesPath = nodeModulesPath(this.root); - - this.treePaths = { - app: 'app', - styles: 'app/styles', - templates: 'app/templates', - addon: 'addon', - 'addon-styles': 'addon/styles', - 'addon-templates': 'addon/templates', - vendor: 'vendor', - 'test-support': 'test-support', - public: 'public' - }; - - this.treeForMethods = { - app: 'treeForApp', - styles: 'treeForStyles', - templates: 'treeForTemplates', - 'addon-templates': 'treeForAddonTemplates', - addon: 'treeForAddon', - vendor: 'treeForVendor', - 'test-support': 'treeForTestSupport', - public: 'treeForPublic' - }; - - p.setupRegistry(this); - - if (!this.name) { - throw new SilentError('An addon must define a `name` property.'); - } -} +let addonProto = { + /** + The name of this addon. + + @public + @final + @type String + @property name + */ + + /** + The absolute path of the root directory where this addon is located. + + @public + @final + @type String + @property root + */ + + /** + The host app instance. + + **Note**: this property will only be present on addons that are a direct dependency + of the application itself, not of other addons. It is also not available in `init()`, + but will be set before `setupPreprocessorRegistry()` and `included()` are invoked. + + @public + @final + @type EmberApp + @property app + */ + + /** + The root {{#crossLink "Project"}}project{{/crossLink}} to which this addon belongs. + + @public + @final + @type Project + @property project + */ + + /** + This addon's parent. + + If the addon is a direct dependency of an application, then `parent` will be the + corresponding {{#crossLink "Project"}}project{{/crossLink}} instance. If it's a + dependency of another addon, then `parent` will be a reference to that addon. + + @public + @final + @type Project|Addon + @property parent + */ + + /** + The set of addons that this addon itself depends on. + + This array is populated from the addon's listed `dependencies` and any items in + `ember-addon.paths` in its `package.json`. + + @public + @final + @type Addon[] + @property addons + */ + + /** + A [`console-ui`](https://github.com/ember-cli/console-ui) object that can be used + to log messages for the user and indicate progress on long-running operations. + + @public + @final + @type UI + @property ui + */ + + /** + The contents of the addon's `package.json`. + + @public + @final + @type Object + @property pkg + */ + + /** + Initializes the addon. If you override this method make sure and call `this._super.init && this._super.init.apply(this, arguments);` or your addon will not work. + + @public + @method init + @param {Project|Addon} parent The project or addon that directly depends on this addon + @param {Project} project The current project (deprecated) + + @example + ```js + init(parent, project) { + this._super.init && this._super.init.apply(this, arguments); + this._someCustomSetup(); + } + ``` + */ + init(parent, project) { + this._super(); + this.parent = parent; + this.project = project; + this.ui = project && project.ui; + this.addonPackages = Object.create(null); + this.addons = []; + this.registry = p.defaultRegistry(this); + + if (!this.root) { + throw new Error('Addon classes must be instantiated with the `root` property'); + } -Addon.__proto__ = CoreObject; -Addon.prototype.constructor = Addon; + if (!this.name) { + throw new SilentError(`An addon must define a \`name\` property (found at ${this.root}).`); + } -Addon.prototype.hintingEnabled = function() { - var isProduction = process.env.EMBER_ENV === 'production'; - var testsEnabledDefault = process.env.EMBER_CLI_TEST_COMMAND || !isProduction; + this._nodeModulesPath = null; + + this.treePaths = { + app: 'app', + styles: 'app/styles', + templates: 'app/templates', + addon: 'addon', + 'addon-styles': 'addon/styles', + 'addon-templates': 'addon/templates', + vendor: 'vendor', + 'test-support': 'test-support', + 'addon-test-support': 'addon-test-support', + public: 'public', + }; + + this.treeForMethods = defaultsDeep({}, DEFAULT_TREE_FOR_METHODS); + + if (this.parent) { + // The parent should already have a packageInfoCache. + // Because it does, this package should already be in the cache. + this.packageInfoCache = this.parent.packageInfoCache; + } - return testsEnabledDefault; -}; + if (!this.packageInfoCache) { + // this is most likely a 'root' addon, so create a new PackageInfoCache + // for it so we get the PackageInfo for it and its children. + // + // this may also be someone who failed to mock their addon correctly in testing. + this.packageInfoCache = new PackageInfoCache(this.ui); + } -/** - Loads all required modules for a build + this._packageInfo = this.packageInfoCache.loadAddon(this); - @private - @method _requireBuildPackages - */ + p.setupRegistry(this); -Addon.prototype._requireBuildPackages = function() { - if (this._didRequiredBuildPackages === true) { - return; - } else { - this._didRequiredBuildPackages = true; - } + this._initDefaultBabelOptions(); + }, - this.transpileModules = deprecatedAddonFilters(this, 'this.transpileModules', 'broccoli-es6modules', function(tree, options) { - return new (require('broccoli-es6modules'))(tree, options); - }); + _initDefaultBabelOptions() { + this.options = defaultsDeep(this.options, { + babel: {}, + }); - this.pickFiles = deprecatedAddonFilters(this, 'this.pickFiles', 'broccoli-funnel', function(tree, options) { - return new Funnel(tree, options); - }); + this.options['ember-cli-babel'] = defaultsDeep(this.options['ember-cli-babel'], { + compileModules: true, + }); + }, + + /** + * Find an addon of the current addon. + * + * Example: ember-data depends on ember-cli-babel and wishes to have + * additional control over transpilation this method helps. + * + * ```js + * // ember-data/index.js + * treeForAddon(tree) { + * let babel = this.findOwnAddonByName('ember-cli-babel'); + * + * return babel.transpileTree(tree, { + * // customize the babel step (see: ember-cli-addons readme for more details); + * }); + * } + * ``` + * + * @public + * @method findOwnAddonByName + */ + findOwnAddonByName(name) { + return this.addons.find((addon) => addon.name === name); + }, + + /** + * Check if the current addon intends to be hinted. Typically this is for + * hinting/linting libraries such as eslint or jshint + * + * @public + * @method hintingEnabled + */ + hintingEnabled() { + let isProduction = process.env.EMBER_ENV === 'production'; + let testsEnabledDefault = process.env.EMBER_CLI_TEST_COMMAND === 'true' || !isProduction; + let explicitlyDisabled = this.app && this.app.options && this.app.options.hinting === false; + + return testsEnabledDefault && !explicitlyDisabled; + }, + + /** + Shorthand method for [broccoli-concat](https://github.com/ember-cli/broccoli-concat) + + @private + @method concatFiles + @param {tree} tree Tree of files + @param {Object} options Options for broccoli-concat + @return {tree} Modified tree + */ + concatFiles(tree, options) { + options.sourceMapConfig = this.app.options.sourcemaps; + return require('broccoli-concat')(tree, options); + }, + + /** + Allows to mark the addon as developing, triggering live-reload in the project the addon is linked to. + + #### Uses: + + - Working on projects with internal addons + + @public + @method isDevelopingAddon + @return {Boolean} + */ + isDevelopingAddon() { + if (process.env.EMBER_ADDON_ENV === 'development' && this.parent instanceof Project) { + const parentName = this.parent.name(); + // If the name in package.json and index.js match, we're definitely developing + if (parentName === this.name) { + return true; + } + + // Addon names in index.js and package.json must be the same at all times whether they have scope or not. + if (this.root === this.parent.root) { + if (parentName !== this.name) { + let pathToDisplay = process.cwd() === this.root ? process.cwd() : path.relative(process.cwd(), this.root); + + throw new SilentError( + 'ember-cli: Your names in package.json and index.js must match. ' + + `The addon in ${pathToDisplay} currently has '${parentName}' in package.json and '${this.name}' in index.js.` + ); + } + + return true; + } + } + return false; + }, + + /** + * Discovers all child addons of this addon and an AddonInfo about + * each addon in this.addonPackages (keyed on addon name). + * + * Child addons include those from 'dependencies' (not devDependencies) + * and in-repo addons + * + * Any packageInfos that we find that are marked as not valid are excluded. + * + * @private + * @method discoverAddons + */ + discoverAddons() { + // prefer `packageRoot`, fallback to `root`; this is to maintain backwards compatibility for + // consumers who create a new instance of the base addon model class directly and don't set + // `packageRoot` + let pkgInfo = this.packageInfoCache.getEntry(this.packageRoot || this.root); + + if (pkgInfo) { + let addonPackageList = pkgInfo.discoverAddonAddons(); + this.addonPackages = pkgInfo.generateAddonPackages( + addonPackageList, + (addonInfo) => this.shouldIncludeChildAddon && !this.shouldIncludeChildAddon(addonInfo) + ); + + // in case any child addons are invalid, dump to the console about them. + pkgInfo.dumpInvalidAddonPackages(addonPackageList); + } else { + // There are cases where an addon can be created in memory and not actually + // have a root entry (or have one that is not actually pointing to a directory, + // like 'foo' in some of the tests. We don't want to crash, but want to let + // things continue even though they're empty. + this.addonPackages = Object.create(null); + } + }, + initializeAddons() { + if (this._addonsInitialized) { + return; + } + this._addonsInitialized = true; + + logger.info('initializeAddons for: %s', this.name); + + this.discoverAddons(); + + this.addons = instantiateAddons(this, this.project, this.addonPackages); + this.addons.forEach((addon) => logger.info('addon: %s', addon.name)); + }, + + /** + Invoke the specified method for each enabled addon. + + @private + @method eachAddonInvoke + @param {String} methodName the method to invoke on each addon + @param {Array} args the arguments to pass to the invoked method + */ + eachAddonInvoke(methodName, args) { + this.initializeAddons(); + + let invokeArguments = args || []; + + return this.addons.reduce((sum, addon) => { + let method = addon[methodName]; + if (method) { + let val = method.apply(addon, invokeArguments); + if (val) { + sum.push(val); + } + } + return sum; + }, []); + }, + + /** + Invoke the specified method for each of the project's addons. + + @private + @method _eachProjectAddonInvoke + @param {String} methodName the method to invoke on each addon + @param {Array} args the arguments to pass to the invoked method + */ + _eachProjectAddonInvoke(methodName, args) { + this.initializeAddons(); + + let invokeArguments = args || []; + + return this.project.addons.reduce((sum, addon) => { + let method = addon[methodName]; + if (method) { + let val = method.apply(addon, invokeArguments); + if (val) { + sum.push(val); + } + } + return sum; + }, []); + }, + + _addonPreprocessTree(type, tree) { + return addonProcessTree(this, 'preprocessTree', type, tree); + }, + + _addonPostprocessTree(type, tree) { + return addonProcessTree(this, 'postprocessTree', type, tree); + }, + + /** + Generates a tree for the specified path + + @private + @method treeGenerator + @return {tree} + */ + treeGenerator(dir) { + let tree; + + if (!this.project) { + throw new SilentError( + `Addon \`${this.name}\` is missing \`addon.project\`. This may be the result of an addon forgetting to invoke \`super\` in its init hook.` + ); + } + // TODO: fix law of demeter `_watchmanInfo.canNestRoots` is obviously a poor idea + if ((this.project && this.project._watchmanInfo.canNestRoots) || this.isDevelopingAddon()) { + const WatchedDir = require('broccoli-source').WatchedDir; + tree = new WatchedDir(dir); + } else { + const UnwatchedDir = require('broccoli-source').UnwatchedDir; + tree = new UnwatchedDir(dir); + } - this.Funnel = deprecatedAddonFilters(this, 'new this.Funnel(..)', 'broccoli-funnel', function(tree, options) { - return new Funnel(tree, options); - }); + return tree; + }, + + _treePathFor(treeName) { + let treePath = this.treePaths[treeName]; + let absoluteTreePath = path.join(this.root, treePath); + let normalizedAbsoluteTreePath = path.normalize(absoluteTreePath); + + return ensurePosixPath(normalizedAbsoluteTreePath); + }, + + /** + Returns a given type of tree (if present), merged with the + application tree. For each of the trees available using this + method, you can also use a direct method called `treeFor[Type]` (eg. `treeForApp`). + + Available tree names: + - {{#crossLink "Addon/treeForApp:method"}}app{{/crossLink}} + - {{#crossLink "Addon/treeForStyles:method"}}styles{{/crossLink}} + - {{#crossLink "Addon/treeForTemplates:method"}}templates{{/crossLink}} + - {{#crossLink "Addon/treeForAddonTemplates:method"}}addon-templates{{/crossLink}} + - {{#crossLink "Addon/treeForAddon:method"}}addon{{/crossLink}} + - {{#crossLink "Addon/treeForVendor:method"}}vendor{{/crossLink}} + - {{#crossLink "Addon/treeForTestSupport:method"}}test-support{{/crossLink}} + - {{#crossLink "Addon/treeForAddonTestSupport:method"}}addon-test-support{{/crossLink}} + - {{#crossLink "Addon/treeForPublic:method"}}public{{/crossLink}} + + #### Uses: + + - manipulating trees at build time + + @public + @method treeFor + @param {String} name + @return {Tree} + */ + treeFor(treeType) { + let node = heimdall.start({ + name: `treeFor(${this.name} - ${treeType})`, + addonName: this.name, + treeType, + treeFor: true, + }); - this.mergeTrees = deprecatedAddonFilters(this, 'this.mergeTrees', 'broccoli-merge-trees', mergeTrees); - this.walkSync = deprecatedAddonFilters(this, 'this.walkSync', 'node-walk-sync', walkSync); -}; + let cacheKeyForTreeType = this.cacheKeyForTree(treeType); + + let cachedTree = ADDON_TREE_CACHE.getItem(cacheKeyForTreeType); + if (cachedTree) { + node.stop(); + return cachedTree; + } -function deprecatedAddonFilters(addon, name, insteadUse, fn) { - return function(tree, options) { - var message = '[Deprecated] ' + name + ' is deprecated, please use ' + insteadUse + ' directly instead [addon: ' + addon.name + ']'; - this.ui && this.ui.writeLine(message); + let trees = this.eachAddonInvoke('treeFor', [treeType]); + let tree = this._treeFor(treeType); - if (!this.ui) { - console.warn(message); + if (tree) { + trees.push(tree); } - return fn(tree, options); - }; -} + if (this.isDevelopingAddon() && this.hintingEnabled() && treeType === 'app') { + trees.push(this.jshintAddonTree()); + } -/** - Shorthand method for [broccoli-sourcemap-concat](https://github.com/ef4/broccoli-sourcemap-concat) + let mergedTreesForType = mergeTrees(trees, { + overwrite: true, + annotation: `Addon#treeFor (${this.name} - ${treeType})`, + }); - @private - @method concatFiles - @param {tree} tree Tree of files - @param {Object} options Options for broccoli-sourcemap-concat - @return {tree} Modified tree -*/ -Addon.prototype.concatFiles = function(tree, options) { - options.sourceMapConfig = this.app.options.sourcemaps; - return require('broccoli-sourcemap-concat')(tree, options); -}; + if (cacheKeyForTreeType) { + ADDON_TREE_CACHE.setItem(cacheKeyForTreeType, mergedTreesForType); + } -/** - Returns whether or not this addon is running in development + node.stop(); - @private - @method isDevelopingAddon - @return {Boolean} -*/ -Addon.prototype.isDevelopingAddon = function() { - if (process.env.EMBER_ADDON_ENV === 'development' && (this.parent instanceof Project)) { - return this.parent.name() === this.name; - } - return false; -}; + return mergedTreesForType; + }, -/** - Discovers all child addons of this addon and stores their names and - package.json contents in this.addonPackages as key-value pairs + /** + @private + @param {String} name + @method _treeFor + @return {tree} + */ + _treeFor(name) { + let treePath = path.resolve(this.root, this.treePaths[name]); + let treeForMethod = this.treeForMethods[name]; + let tree; - @private - @method discoverAddons - */ -Addon.prototype.discoverAddons = function() { - var addonsList = this.addonDiscovery.discoverChildAddons(this); + if (fs.existsSync(treePath)) { + tree = this.treeGenerator(treePath); + } - this.addonPackages = this.addonDiscovery.addonPackages(addonsList); -}; + if (this[treeForMethod]) { + tree = this[treeForMethod](tree); + } -Addon.prototype.initializeAddons = function() { - if (this._addonsInitialized) { - return; - } - this._addonsInitialized = true; + return tree; + }, + + /** + Calculates a cacheKey for the given treeType. It is expected to return a + cache key allowing multiple builds of the same tree to simply return the + original tree (preventing duplicate work). If it returns null / undefined + the tree in question will opt out of this caching system. + + This method is invoked prior to calling treeFor with the same tree name. + + You should override this method if you implement custom treeFor or treeFor* + methods, which cause addons to opt-out of this caching. + + @public + @method cacheKeyForTree + @param {String} treeType + @return {String} cacheKey + */ + cacheKeyForTree(treeType) { + let methodsToValidate = methodsForTreeType(treeType); + let cacheKeyStats = heimdall.statsFor('cache-key-for-tree'); + + // determine if treeFor* (or other methods for tree type) overrides for the given tree + let modifiedMethods = methodsToValidate.filter((methodName) => this[methodName] !== addonProto[methodName]); + + if (modifiedMethods.length) { + cacheKeyStats.modifiedMethods++; + cacheKeyLogger.info(`Opting out due to: modified methods: ${modifiedMethods.join(', ')}`); + return null; // uncacheable + } - debug('initializeAddons for: %s', this.name); + // determine if treeForMethods overrides for given tree + if (this.treeForMethods[treeType] !== DEFAULT_TREE_FOR_METHODS[treeType]) { + cacheKeyStats.treeForMethodsOverride++; + cacheKeyLogger.info('Opting out due to: treeForMethods override'); + return null; // uncacheable + } - this.discoverAddons(); - this.addons = this.addonsFactory.initializeAddons(this.addonPackages); + // compute cache key + let cacheKey = calculateCacheKeyForTree(treeType, this); - this.addons.forEach(function(addon) { - debug('addon: %s', addon.name); - }); -}; + return cacheKey; // profit? + }, -/** - Invoke the specified method for each enabled addon. + /** + This method climbs up the hierarchy of addons + up to the host application. - @private - @method eachAddonInvoke - @param {String} methodName the method to invoke on each addon - @param {Array} args the arguments to pass to the invoked method -*/ -Addon.prototype.eachAddonInvoke = function eachAddonInvoke(methodName, args) { - this.initializeAddons(); + This prevents previous addons (prior to `this.import`, ca 2.7.0) + to break at importing assets when they are used nested in other addons. - var invokeArguments = args || []; + @private + @method _findHost + */ + _findHost() { + let current = this; + let app; - return this.addons.map(function(addon) { - if (addon[methodName]) { - return addon[methodName].apply(addon, invokeArguments); - } - }).filter(Boolean); -}; -/** - Generates a tree for the specified path + // Keep iterating upward until we don't have a grandparent. + // Has to do this grandparent check because at some point we hit the project. + do { + app = current.app || app; + } while (current.parent.parent && (current = current.parent)); - @private - @method treeGenerator - @return {tree} -*/ -Addon.prototype.treeGenerator = function(dir) { - var tree; - if (this.project._watchmanInfo.canNestRoots || this.isDevelopingAddon()) { - tree = new WatchedDir(dir); - } else { - tree = new UnwatchedDir(dir); - } + return app; + }, - return tree; -}; + /** + This method is called when the addon is included in a build. You + would typically use this hook to perform additional imports -/** - Returns a given type of tree (if present), merged with the - application tree. For each of the trees available using this - method, you can also use a direct method called treeFor[Type] (eg. `treeForApp`). - - Available tree names: - - app - - styles - - templates - - {{#crossLink "Addon/treeForAddon:method"}}addon{{/crossLink}} - - vendor - - test-support - - {{#crossLink "Addon/treeForPublic:method"}}public{{/crossLink}} + #### Uses: - @public - @method treeFor - @param {String} name - @return {tree} -*/ -Addon.prototype.treeFor = function treeFor(name) { - this._requireBuildPackages(); + - including vendor files + - setting configuration options - var tree; - var trees = this.eachAddonInvoke('treeFor', [name]); + *Note:* Any options set in the consuming application will override the addon. - if (tree = this._treeFor(name)) { - trees.push(tree); - } + @public + @method included + @param {EmberApp|EmberAddon} parent The parent object which included this addon - if (this.isDevelopingAddon() && this.hintingEnabled() && name === 'app') { - trees.push(this.jshintAddonTree()); - } + @example + ```js + included(parent) { + this._super.included.apply(this, arguments); + this.import(somePath); + } + ``` + */ + included(/* parent */) { + if (!this._addonsInitialized) { + // someone called `this._super.included` without `apply` (because of older + // core-object issues that prevent a "real" super call from working properly) + return; + } - return mergeTrees(trees.filter(Boolean), { - overwrite: true, - annotation: 'Addon#treeFor (' + this.name + ' - ' + name + ')' - }); -}; + this.eachAddonInvoke('included', [this]); + }, + + /** + Imports an asset into this addon. + + @public + @method import + @param {Object|String} asset Either a path to the asset or an object with environment names and paths as key-value pairs. + @param {Object} [options] Options object + @param {String} [options.type] Either 'vendor' or 'test', defaults to 'vendor' + @param {Boolean} [options.prepend] Whether or not this asset should be prepended, defaults to false + @param {String} [options.destDir] Destination directory, defaults to the name of the directory the asset is in + @since 2.7.0 + */ + import(asset, options) { + options = options || {}; + options.resolveFrom = options.resolveFrom || this.root; + + let app = this._findHost(); + app.import(asset, options); + }, + + /** + Returns the tree for all app files + + @public + @method treeForApp + @param {Tree} tree + @return {Tree} App file tree + */ + treeForApp(tree) { + return tree; + }, -/** - @private - @param {String} name - @method _treeFor - @return {tree} -*/ -Addon.prototype._treeFor = function _treeFor(name) { - var treePath = path.resolve(this.root, this.treePaths[name]); - var treeForMethod = this.treeForMethods[name]; - var tree; + /** + Returns the tree for all template files - if (existsSync(treePath)) { - tree = this.treeGenerator(treePath); - } + @public + @method treeForTemplates + @param {Tree} tree + @return {Tree} Template file tree + */ + treeForTemplates(tree) { + return tree; + }, - if (this[treeForMethod]) { - tree = this[treeForMethod](tree); - } + /** + Returns the tree for this addon's templates - return tree; -}; + @public + @method treeForAddonTemplates + @param {Tree} tree + @return {Tree} Addon Template file tree + */ + treeForAddonTemplates(tree) { + return tree; + }, -/** - This method is called when the addon is included in a build. You - would typically use this hook to perform additional imports + /** + Returns a tree for this addon - ```js - included: function(app) { - app.import(somePath); + @public + @method treeForAddon + @param {Tree} tree + @return {Tree} Addon file tree + + @example + ```js + treeForAddon() { + let emberVersion = new VersionChecker(this.project).for('ember-source'); + let shouldUsePolyfill = emberVersion.lt('4.5.0-alpha.4'); + + if (shouldUsePolyfill) { + return this._super.treeForAddon.apply(this, arguments); + } + } + ``` + */ + treeForAddon(tree) { + if (!tree) { + return tree; } - ``` - @public - @method included - @param {EmberApp} app The application object -*/ -Addon.prototype.included = function(/* app */) { - if (!this._addonsInitialized) { - // someone called `this._super.included` without `apply` (because of older - // core-object issues that prevent a "real" super call from working properly) - return; - } + let addonTree = this.compileAddon(tree); + let stylesTree = this.compileStyles(this._treeFor('addon-styles')); - this.eachAddonInvoke('included', [this]); -}; + return mergeTrees([addonTree, stylesTree], { annotation: `Addon#treeForAddon(${this.name})` }); + }, -/** - Returns the tree for all public files + /** + Returns the tree for all style files - @public - @method treeForPublic - @param {Tree} tree - @return {Tree} Public file tree -*/ -Addon.prototype.treeForPublic = function(tree) { - this._requireBuildPackages(); + @public + @method treeForStyles + @param {Tree} tree The tree to process, usually `app/styles/` in the addon. + @return {Tree} The return tree has the same contents as the input tree, but is moved so that the `app/styles/` path is preserved. + */ + treeForStyles(tree) { + if (!tree) { + return tree; + } - if (!tree) { - return tree; - } + return new Funnel(tree, { + destDir: 'app/styles', + annotation: `Addon#treeForStyles (${this.name})`, + }); + }, - return new Funnel(tree, { - srcDir: '/', - destDir: '/' + this.moduleName() - }); -}; + /** + Returns the tree for all vendor files -/** - Returns a tree for this addon + @public + @method treeForVendor + @param {Tree} tree + @return {Tree} Vendor file tree + */ + treeForVendor(tree) { + return tree; + }, - @public - @method treeForAddon - @param {Tree} tree - @return {Tree} Addon file tree -*/ -Addon.prototype.treeForAddon = function(tree) { - this._requireBuildPackages(); + /** + Returns the tree for all test support files - if (!tree) { + @public + @method treeForTestSupport + @param {Tree} tree + @return {Tree} Test Support file tree + */ + treeForTestSupport(tree) { return tree; - } + }, + + /** + Returns the tree for all public files + + @public + @method treeForPublic + @param {Tree} tree + @return {Tree} Public file tree + */ + treeForPublic(tree) { + if (!tree) { + return tree; + } - var addonTree = this.compileAddon(tree); - var stylesTree = this.compileStyles(this._treeFor('addon-styles')); + return new Funnel(tree, { + srcDir: '/', + destDir: `/${this.moduleName()}`, + annotation: `Addon#treeForPublic (${this.name})`, + }); + }, + + /** + Returns the tree for all test files namespaced to a given addon. + + @public + @method treeForAddonTestSupport + @param {Tree} tree + @return {Tree} + */ + treeForAddonTestSupport(tree) { + if (!tree) { + return tree; + } - return mergeTrees([addonTree, stylesTree].filter(Boolean), { - annotation: 'Addon#treeForAddon(' + this.name + ')' - }); -}; + let namespacedTree = new Funnel(tree, { + srcDir: '/', + destDir: `/${this.moduleName()}/test-support`, + annotation: `Addon#treeForTestSupport (${this.name})`, + }); -/** - Runs the styles tree through preprocessors. + if (registryHasPreprocessor(this.registry, 'js')) { + return this.preprocessJs(namespacedTree, '/', this.name, { + registry: this.registry, + treeType: 'addon-test-support', + }); + } - @private - @method compileStyles - @param {Tree} tree Styles file tree - @return {Tree} Compiled styles tree -*/ -Addon.prototype.compileStyles = function(tree) { - this._requireBuildPackages(); + throw new SilentError( + `Addon test-support files were detected in \`${this._treePathFor('addon-test-support')}\`, but no JavaScript ` + + `preprocessor was found for \`${this.name}\`. Please make sure to add a preprocessor ` + + `(most likely \`ember-cli-babel\`) to \`dependencies\` (NOT \`devDependencies\`) in ` + + `\`${this.name}\`'s \`package.json\`.` + ); + }, + + /** + Runs the styles tree through preprocessors. + + @private + @method compileStyles + @param {Tree} addonStylesTree Styles file tree + @return {Tree} Compiled styles tree + */ + compileStyles(addonStylesTree) { + if (addonStylesTree) { + let preprocessedStylesTree = this._addonPreprocessTree('css', addonStylesTree); + + let processedStylesTree = preprocessCss(preprocessedStylesTree, '/', '/', { + outputPaths: { addon: `${this.name}.css` }, + registry: this.registry, + treeType: 'addon-styles', + }); + processedStylesTree = new Funnel(processedStylesTree, { + destDir: `${this.name}/__COMPILED_STYLES__`, + }); + + return this._addonPostprocessTree('css', processedStylesTree); + } + }, + + /** + Looks in the addon/ and addon/templates trees to determine if template files + exist that need to be precompiled. + + This is executed once when building, but not on rebuilds. + + @private + @method shouldCompileTemplates + @return {Boolean} indicates if templates need to be compiled for this addon + */ + shouldCompileTemplates() { + return this._fileSystemInfo().hasTemplates; + }, + + /** + Looks in the addon/ and addon/templates trees to determine if template files + exist in the pods format that need to be precompiled. + + This is executed once when building, but not on rebuilds. + + @private + @method _shouldCompilePodTemplates + @return {Boolean} indicates if pod based templates need to be compiled for this addon + */ + _shouldCompilePodTemplates() { + return this._fileSystemInfo().hasPodTemplates; + }, + + _fileSystemInfo() { + if (this._cachedFileSystemInfo) { + return this._cachedFileSystemInfo; + } + + let jsExtensions = this.registry.extensionsForType('js'); + let templateExtensions = this.registry.extensionsForType('template'); + let addonTreePath = this._treePathFor('addon'); + let addonTemplatesTreePath = this._treePathFor('addon-templates'); + let addonTemplatesTreeInAddonTree = addonTemplatesTreePath.indexOf(addonTreePath) === 0; + + let files = this._getAddonTreeFiles(); - if (tree) { - return preprocessCss(tree, '/', '/', { - outputPaths: { 'addon': this.name + '.css' }, - registry: this.registry + let addonTemplatesRelativeToAddonPath = + addonTemplatesTreeInAddonTree && addonTemplatesTreePath.replace(`${addonTreePath}/`, ''); + let podTemplateMatcher = new RegExp(`template.(${templateExtensions.join('|')})$`); + let hasPodTemplates = files.some((file) => { + // short circuit if this is actually an `addon/templates` file + if (addonTemplatesTreeInAddonTree && file.indexOf(addonTemplatesRelativeToAddonPath) === 0) { + return false; + } + + return podTemplateMatcher.test(file); }); - } -}; -/** - Looks in the addon/ and addon/templates trees to determine if template files - exists that need to be precompiled. + let jsMatcher = new RegExp(`(${jsExtensions.join('|')})$`); + let hasJSFiles = files.some((file) => jsMatcher.test(file)); - This is executed once when building, but not on rebuilds. + if (!addonTemplatesTreeInAddonTree) { + files = files.concat(this._getAddonTemplatesTreeFiles()); + } - @private - @method shouldCompileTemplates - @returns Boolean indicates if templates need to be compiled for this addon -*/ -Addon.prototype.shouldCompileTemplates = function() { - var templateExtensions = this.registry.extensionsForType('template'); - var addonTreePath = path.join(this.root, this.treePaths['addon']); - var addonTemplatesTreePath = path.join(this.root, this.treePaths['addon-templates']); + let extensionMatcher = new RegExp(`(${templateExtensions.join('|')})$`); + let hasTemplates = files.some((file) => extensionMatcher.test(file)); - var files = []; + this._cachedFileSystemInfo = { + hasJSFiles, + hasTemplates, + hasPodTemplates, + }; - if (existsSync(addonTreePath)) { - files = files.concat(walkSync(addonTreePath)); - } + return this._cachedFileSystemInfo; + }, - if (existsSync(addonTemplatesTreePath)) { - files = files.concat(walkSync(addonTemplatesTreePath)); - } + _getAddonTreeFiles() { + let addonTreePath = this._treePathFor('addon'); - var extensionMatcher = new RegExp('(' + templateExtensions.join('|') + ')$'); + if (fs.existsSync(addonTreePath)) { + return walkSync(addonTreePath); + } - return files.some(function(file) { - return file.match(extensionMatcher); - }); -}; + return []; + }, -/** - Runs the templates tree through preprocessors. + _getAddonTemplatesTreeFiles() { + let addonTemplatesTreePath = this._treePathFor('addon-templates'); - @private - @method compileTemplates - @param {Tree} tree Templates file tree - @return {Tree} Compiled templates tree -*/ -Addon.prototype.compileTemplates = function(tree) { - this._requireBuildPackages(); + if (fs.existsSync(addonTemplatesTreePath)) { + return walkSync(addonTemplatesTreePath); + } - if (this.shouldCompileTemplates()) { - var plugins = this.registry.load('template'); + return []; + }, - if (plugins.length === 0) { - throw new SilentError('Addon templates were detected, but there ' + - 'are no template compilers registered for `' + this.name + '`. ' + - 'Please make sure your template precompiler (commonly `ember-cli-htmlbars`) ' + - 'is listed in `dependencies` (NOT `devDependencies`) in ' + - '`' + this.name + '`\'s `package.json`.'); + _addonTemplateFiles(addonTree) { + if (this._cachedAddonTemplateFiles) { + return this._cachedAddonTemplateFiles; } - var addonTemplates = this._treeFor('addon-templates'); - var standardTemplates; + let trees = []; + let addonTemplates = this._treeFor('addon-templates'); + let standardTemplates; if (addonTemplates) { standardTemplates = new Funnel(addonTemplates, { srcDir: '/', - destDir: 'modules/' + this.name + '/templates' + destDir: `${this.moduleName()}/templates`, + annotation: `Addon#_addonTemplateFiles (${this.name})`, }); + + trees.push(standardTemplates); } - var includePatterns = this.registry.extensionsForType('template').map(function(extension) { - return '**/*/template.' + extension; - }); + if (this._shouldCompilePodTemplates()) { + let includePatterns = this.registry + .extensionsForType('template') + .map((extension) => `**/*/template.${extension}`); - var podTemplates = new Funnel(tree, { - include: includePatterns, - destDir: 'modules/' + this.name + '/', - annotation: 'Funnel: Addon Pod Templates' - }); + let podTemplates = new Funnel(addonTree, { + include: includePatterns, + destDir: `${this.moduleName()}/`, + annotation: 'Funnel: Addon Pod Templates', + }); + + trees.push(podTemplates); + } - return preprocessTemplates(mergeTrees([standardTemplates, podTemplates].filter(Boolean), { - annotation: 'compileTemplates(' + this.name + ')' - }), { - registry: this.registry + this._cachedAddonTemplateFiles = mergeTrees(trees, { + annotation: `TreeMerge (${this.name} templates)`, }); - } -}; -/** - Runs the addon tree through preprocessors. + return this._cachedAddonTemplateFiles; + }, + + /** + Runs the templates tree through preprocessors. + + @private + @method compileTemplates + @param {Tree} tree Templates file tree + @return {Tree} Compiled templates tree + */ + compileTemplates(addonTree) { + if (this.shouldCompileTemplates()) { + if (!registryHasPreprocessor(this.registry, 'template')) { + throw new SilentError( + `Addon templates were detected, but there are no template compilers registered for \`${this.name}\`. ` + + `Please make sure your template precompiler (commonly \`ember-cli-htmlbars\`) is listed in \`dependencies\` ` + + `(NOT \`devDependencies\`) in \`${this.name}\`'s \`package.json\`.` + ); + } + + let preprocessedTemplateTree = this._addonPreprocessTree('template', addonTree); + + let processedTemplateTree = preprocessTemplates(preprocessedTemplateTree, { + annotation: `compileTemplates(${this.name})`, + registry: this.registry, + treeType: 'addon-templates', + }); - @private - @method compileAddon - @param {Tree} tree Addon file tree - @return {Tree} Compiled addon tree -*/ -Addon.prototype.compileAddon = function(tree) { - this._requireBuildPackages(); + let postprocessedTemplateTree = this._addonPostprocessTree('template', processedTemplateTree); - var addonJs = this.processedAddonJsFiles(tree); - var templatesTree = this.compileTemplates(tree); - var reexported = reexport(this.name, this.name + '.js'); - var trees = [addonJs, templatesTree, reexported].filter(Boolean); + return postprocessedTemplateTree; + } else { + return addonTree; + } + }, + + /** + Runs the addon tree through preprocessors. + + @private + @method compileAddon + @param {Tree} tree Addon file tree + @return {Tree} Compiled addon tree + */ + compileAddon(tree) { + if (!this.options) { + throw new SilentError( + `Ember CLI addons manage their own module transpilation during the \`treeForAddon\` processing. ` + + `\`${this.name}\` (found at \`${this.root}\`) has removed \`this.options\` ` + + `which conflicts with the addons ability to transpile its \`addon/\` files properly.` + ); + } - return mergeTrees(trees, { - annotation: 'Addon#compileAddon(' + this.name + ') ' - }); -}; + if (!this.options.babel) { + throw new SilentError( + `Ember CLI addons manage their own module transpilation during the \`treeForAddon\` processing. ` + + `\`${this.name}\` (found at \`${this.root}\`) has overridden the \`this.options.babel\` options ` + + `which conflicts with the addons ability to transpile its \`addon/\` files properly.` + ); + } -/** - Returns a tree with JSHhint output for all addon JS. + if (!this.options['ember-cli-babel']) { + throw new SilentError( + `Ember CLI addons manage their own module transpilation during the \`treeForAddon\` processing. ` + + `\`${this.name}\` (found at \`${this.root}\`) has overridden the \`this.options['ember-cli-babel']\` options ` + + `which conflicts with the addons ability to transpile its \`addon/\` files properly.` + ); + } - @private - @method jshintAddonTree - @return {Tree} Tree with JShint output (tests) -*/ -Addon.prototype.jshintAddonTree = function() { - this._requireBuildPackages(); + let scopedInput = new Funnel(tree, { + destDir: this.moduleName(), + annotation: `Funnel: ${this.name}/addon`, + }); - var addonPath = path.join(this.root, this.treePaths['addon']); + let treeWithCompiledTemplates = this.compileTemplates(scopedInput); + let final = this.processedAddonJsFiles(treeWithCompiledTemplates); - if (!existsSync(addonPath)) { - return; - } + return final; + }, - var addonJs = this.addonJsFiles(addonPath); - var jshintedAddon = this.app.addonLintTree('addon', addonJs); + /** + Returns a tree with JSHint output for all addon JS. - return new Funnel(jshintedAddon, { - srcDir: '/', - destDir: this.name + '/tests/' - }); -}; + @private + @method jshintAddonTree + @return {Tree} Tree with JShint output (tests) + */ + jshintAddonTree() { + let trees = []; -/** - Returns a tree containing the addon's js files + let addonPath = this._treePathFor('addon'); + if (fs.existsSync(addonPath)) { + let addonJs = new Funnel(addonPath, { + exclude: ['templates/**/*'], + destDir: 'addon', + allowEmpty: true, + }); + let lintAddonJsTrees = this._eachProjectAddonInvoke('lintTree', ['addon', addonJs]); - @private - @method addonJsFiles - @return {Tree} The filtered addon js files -*/ -Addon.prototype.addonJsFiles = function(tree) { - this._requireBuildPackages(); + let addonTemplates = new Funnel(this._addonTemplateFiles(addonPath), { + srcDir: `${this.moduleName()}/templates`, + destDir: 'addon/templates', + allowEmpty: true, + }); + let lintTemplateTrees = this._eachProjectAddonInvoke('lintTree', ['templates', addonTemplates]); - var includePatterns = this.registry.extensionsForType('js').map(function(extension) { - return new RegExp(extension + '$'); - }); + trees = trees.concat(lintAddonJsTrees, lintTemplateTrees); + } - return new Funnel(tree, { - include: includePatterns, - destDir: 'modules/' + this.moduleName(), - description: 'Funnel: Addon JS' - }); -}, + let addonTestSupportPath = this._treePathFor('addon-test-support'); + if (fs.existsSync(addonTestSupportPath)) { + let addonTestSupportTree = new Funnel(addonTestSupportPath, { destDir: 'addon-test-support' }); + let lintAddonTestSupportJsTrees = this._eachProjectAddonInvoke('lintTree', [ + 'addon-test-support', + addonTestSupportTree, + ]); + trees = trees.concat(lintAddonTestSupportJsTrees); + } + let appPath = this._treePathFor('app'); + if (fs.existsSync(appPath)) { + let appTree = new Funnel(appPath, { destDir: 'app' }); + let lintAppJsTrees = this._eachProjectAddonInvoke('lintTree', ['app', appTree]); + trees = trees.concat(lintAppJsTrees); + } -/** - Preprocesses a javascript tree. + let testSupportPath = this._treePathFor('test-support'); + if (fs.existsSync(testSupportPath)) { + let testSupportTree = new Funnel(testSupportPath, { destDir: 'test-support' }); + let lintTestSupportJsTrees = this._eachProjectAddonInvoke('lintTree', ['test-support', testSupportTree]); + trees = trees.concat(lintTestSupportJsTrees); + } - @private - @method preprocessJs - @return {Tree} Preprocessed javascript -*/ -Addon.prototype.preprocessJs = function() { - return preprocessJs.apply(preprocessJs, arguments); -}, + let lintTrees = trees.filter(Boolean); + let lintedAddon = mergeTrees(lintTrees, { + overwrite: true, + annotation: 'TreeMerger (addon-lint)', + }); -/** - Returns a tree with all javascript for this addon. + return new Funnel(lintedAddon, { + srcDir: '/', + destDir: `${this.name}/tests/`, + annotation: `Funnel: Addon#jshintAddonTree(${this.name})`, + }); + }, + + /** + Preprocesses a javascript tree. + + @private + @method preprocessJs + @return {Tree} Preprocessed javascript + */ + preprocessJs() { + return preprocessJs.apply(preprocessJs, arguments); + }, + + /** + Returns a tree with all javascript for this addon. + + @private + @method processedAddonJsFiles + @param {Tree} the tree to preprocess + @return {Tree} Processed javascript file tree + */ + processedAddonJsFiles(inputTree) { + let preprocessedAddonJS = this._addonPreprocessTree('js', inputTree); + + let processedAddonJS = this.preprocessJs(preprocessedAddonJS, '/', this.name, { + annotation: `processedAddonJsFiles(${this.name})`, + registry: this.registry, + treeType: 'addon', + }); - @private - @method processedAddonJsFiles - @param {Tree} the tree to preprocess - @return {Tree} Processed javascript file tree -*/ -Addon.prototype.processedAddonJsFiles = function(tree) { - return this.preprocessJs(this.addonJsFiles(tree), '/', this.name, { - registry: this.registry - }); -}; + if (registryHasPreprocessor(this.registry, 'js')) { + return this._addonPostprocessTree('js', processedAddonJS); + } -/** - Returns the module name for this addon. + throw new SilentError( + `Addon files were detected in \`${this._treePathFor('addon')}\`, but no JavaScript ` + + `preprocessor was found for \`${this.name}\`. Please make sure to add a preprocessor ` + + '(most likely `ember-cli-babel`) to `dependencies` (NOT `devDependencies`) in ' + + `\`${this.name}\`'s \`package.json\`.` + ); + }, + + /** + Returns the module name for this addon. + + @public + @method moduleName + @return {String} module name + */ + moduleName() { + if (!this.modulePrefix) { + this.modulePrefix = this.name.toLowerCase().replace(/\s/g, '-'); + } - @public - @method moduleName - @return {String} module name -*/ -Addon.prototype.moduleName = function() { - if (!this.modulePrefix) { - this.modulePrefix = (this.modulePrefix || this.name).toLowerCase().replace(/\s/g, '-'); - } + return this.modulePrefix; + }, - return this.modulePrefix; -}; + /** + Returns the path for addon blueprints. -/** - Returns the path for addon blueprints. + @public + @method blueprintsPath + @return {String} The path for blueprints - @private - @method blueprintsPath - @return {String} The path for blueprints -*/ -Addon.prototype.blueprintsPath = function() { - var blueprintPath = path.join(this.root, 'blueprints'); + @example + - [ember-cli-coffeescript](https://github.com/kimroen/ember-cli-coffeescript/blob/v1.13.2/index.js#L26) + */ + blueprintsPath() { + let blueprintPath = path.join(this.root, 'blueprints'); - if (existsSync(blueprintPath)) { - return blueprintPath; - } -}; + if (fs.existsSync(blueprintPath)) { + return blueprintPath; + } + }, -/** - Augments the applications configuration settings. - Object returned from this hook is merged with the application's configuration object. - Application's configuration always take precedence. + /** + Augments the application's configuration settings. + Object returned from this hook is merged with the application's configuration object. - ```js - config: function(environment, appConfig) { + Application's configuration always take precedence. + + #### Uses: + + - Modifying configuration options (see list of defaults [here](https://github.com/ember-cli/ember-cli/blob/v2.4.3/lib/broccoli/ember-app.js#L163)) + - For example + - `storeConfigInMeta` + - et, al + + @public + @method config + @param {String} env Name of current environment (ie "development") + @param {Object} baseConfig Initial application configuration + @return {Object} Configuration object to be merged with application configuration. + + @example + ```js + config(environment, appConfig) { return { someAddonDefault: "foo" }; } - ``` + ``` + */ + config(env, baseConfig) { + let configPath = path.join(this.root, 'config', 'environment.js'); - @public - @method config - @param {String} env Name of current environment (ie "developement") - @param {Object} baseConfig Initial application configuration - @return {Object} Configuration object to be merged with application configuration. -*/ -Addon.prototype.config = function (env, baseConfig) { - var configPath = path.join(this.root, 'config', 'environment.js'); - - if (existsSync(configPath)) { - var configGenerator = require(configPath); + if (fs.existsSync(configPath)) { + const configGenerator = require(configPath); - return configGenerator(env, baseConfig); - } + return configGenerator(env, baseConfig); + } + }, + + /** + @public + @method dependencies + @return {Object} The addon's dependencies based on the addon's package.json + */ + dependencies() { + let pkg = this.pkg; + return pkg ? Object.assign({}, pkg.devDependencies, pkg.dependencies) : {}; + }, + + /** + @public + @method isEnabled + @return {Boolean} Whether or not this addon is enabled + */ + isEnabled() { + return true; + }, + + /** + Can be used to exclude addons from being added as a child addon. + + #### Uses: + + - Abstract away multiple addons while only including one into the built assets + + @public + @method shouldIncludeChildAddon + @param {Addon} childAddon + @return {Boolean} Whether or not a child addon is supposed to be included + + @example + ```js + shouldIncludeChildAddon(childAddon) { + if(childAddon.name === 'ember-cli-some-legacy-select-component') { + return this.options.legacyMode; + } else if(childAddon.name === 'ember-cli-awesome-new-select-component') { + return !this.options.legacyMode; + } else { + return this._super.shouldIncludeChildAddon.apply(this, arguments); + } + } + ``` + */ + shouldIncludeChildAddon() { + return true; + }, }; -/** - @public - @method dependencies - @return {Object} The addon's dependencies based on the addon's package.json -*/ -Addon.prototype.dependencies = function() { - var pkg = this.pkg || {}; - return assign({}, pkg['devDependencies'], pkg['dependencies']); -}; +// Methods without default implementation /** - @public - @method isEnabled - @return {Boolean} Whether or not this addon is enabled -*/ -Addon.prototype.isEnabled = function() { - return true; -}; + Allows the specification of custom addon commands. + Expects you to return an object whose key is the name of the command and value is the command instance.. -/** - Returns the absolute path for a given addon + This function is not implemented by default - @private - @method resolvePath - @param {String} addon Addon name - @return {String} Absolute addon path -*/ -Addon.resolvePath = function(addon) { - var addonMain; + #### Uses: - deprecate(addon.pkg.name + ' is using the deprecated ember-addon-main definition. It should be updated to {\'ember-addon\': {\'main\': \'' + addon.pkg['ember-addon-main'] + '\'}}', addon.pkg['ember-addon-main']); + - Include custom commands into consuming application - addonMain = addon.pkg['ember-addon-main'] || (addon.pkg['ember-addon'] && addon.pkg['ember-addon'].main) || addon.pkg['main'] || 'index.js'; + @public + @method includedCommands + @return {Object} An object with included commands - // Resolve will fail unless it has an extension - if(!path.extname(addonMain)) { - addonMain += '.js'; + @example + ```js + includedCommands() { + return { + 'do-foo': require('./lib/commands/foo') + }; } - - return path.resolve(addon.path, addonMain); -}; + ``` +*/ /** - Returns the addon class for a given addon name. - If the Addon exports a function, that function is used - as constructor. If an Object is exported, a subclass of - `Addon` is returned with the exported hash merged into it. - - @private - @static - @method lookup - @param {String} addon Addon name - @return {Addon} Addon class -*/ -Addon.lookup = function(addon) { - var Constructor, addonModule, modulePath, moduleDir; + Allows addons to define a custom transform function that other addons and app can use when using `app.import`. - modulePath = Addon.resolvePath(addon); - moduleDir = path.dirname(modulePath); + This function is not implemented by default - if (existsSync(modulePath)) { - addonModule = require(modulePath); + #### Uses: - if (typeof addonModule === 'function') { - Constructor = addonModule; - Constructor.prototype.root = Constructor.prototype.root || moduleDir; - Constructor.prototype.pkg = Constructor.prototype.pkg || addon.pkg; - } else { - Constructor = Addon.extend(assign({ - root: moduleDir, - pkg: addon.pkg - }, addonModule)); - } - } + - An app or addons want to transform a dependency that is being imported using `app.import`. - if (!Constructor) { - throw new SilentError('The `' + addon.pkg.name + '` addon could not be found at `' + addon.path + '`.'); - } + @public + @method importTransforms + @return {Object} An object with custom transforms - return Constructor; -}; + @example + ```js + importTransforms() { + return { + 'my-custom-transform': function(tree, options) { + // transform the incoming tree and return the updated tree + } + }; + } + ``` + Alternatively, if you want to process `options` before being passed into the custom transform function, use: -// Methods without default implementation + @example + ```js + importTransforms() { + return { + 'my-custom-transform': { + transform: function(tree, options) { + // transform the incoming tree and return the updated tree + }, + processOptions: function(assetPath, entry, options) { + // process your options + + return options + } + }; + } + ``` +*/ /** - CLI commands included with this addon. This function should return - an object with command names and command instances/create options. + Pre-process a tree - This function is not implemented by default + #### Uses: - ```js - includedCommands: function() { - return { - 'do-foo': require('./lib/commands/foo') - }; - } - ``` + - removing / adding files from the build. @public - @method includedCommands - @return {Object} An object with included commands -*/ - + @method preprocessTree + @param {String} type What kind of tree (eg. 'js', 'css', 'template') + @param {Tree} tree Tree to process + @return {Tree} Processed tree + */ /** Post-process a tree @public - @method postProcessTree - @param {String} type What kind of tree (eg. 'javascript', 'styles') - @param {Tree} Tree to process + @method postprocessTree + @param {String} type What kind of tree (eg. 'js', 'css', 'template') + @param {Tree} tree Tree to process @return {Tree} Processed tree -*/ + + @example + - [broccoli-asset-rev](https://github.com/rickharrison/broccoli-asset-rev/blob/c82c3580855554a31f7d6600b866aecf69cdaa6d/index.js#L29) + */ /** This hook allows you to make changes to the express server run by ember-cli. - It's passed an `startOptions` object which contains: + + It's passed a `startOptions` object which contains: - `app` Express server instance - `options` A hash with: - `project` Current {{#crossLink "Project"}}project{{/crossLink}} @@ -769,20 +1490,47 @@ Addon.lookup = function(addon) { This function is not implemented by default - ```js - serverMiddleware: function(startOptions) { - var app = startOptions.app; + #### Uses: - app.use(function(req, res, next) { - // Some middleware - }); - } - ``` + - Tacking on headers to each request + - Modifying the request object + + *Note:* that this should only be used in development, and if you need the same behavior in production you'll + need to configure your server. @public @method serverMiddleware @param {Object} startOptions Express server start options -*/ + + @example + ```js + serverMiddleware(startOptions) { + var app = startOptions.app; + + app.use(function(req, res, next) { + // Some middleware + }); + } + ``` + + - [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/blob/v0.5.0/index.js#L84) + - [history-support-addon](https://github.com/ember-cli/ember-cli/blob/v2.4.3/lib/tasks/server/middleware/history-support/index.js#L25) + */ + +/** + This hook allows you to make changes to the express server run by testem. + + This function is not implemented by default + + #### Uses: + + - Adding custom test-specific endpoints + - Manipulating HTTP requests in tests + + @public + @method testemMiddleware + @param {Object} app the express app instance + */ /** This hook is called before a build takes place. @@ -795,21 +1543,138 @@ Addon.lookup = function(addon) { /** This hook is called after a build is complete. - It's passed an `result` object which contains: + It's passed a `result` object which contains: - `directory` Path to build output + #### Uses: + + - Slow tree listing + - May be used to manipulate your project after build has happened + @public @method postBuild @param {Object} result Build result object */ /** - This hook is called when an error occurs during the preBuild or postBuild hooks + This hook is called after the build has been processed and the build files have been copied to the output directory + + It's passed a `result` object which contains: + - `directory` Path to build output + + @public + @method outputReady + @param {Object} result Build result object + + @example + - Opportunity to symlink or copy files elsewhere. + - [ember-cli-rails-addon](https://github.com/rondale-sc/ember-cli-rails-addon/blob/v0.7.0/index.js#L45) + - In this case we are using this in tandem with a rails middleware to remove a lock file. + This allows our ruby gem to block incoming requests until after the build happens reliably. + */ + +/** + This hook is called when an error occurs during the preBuild, postBuild or outputReady hooks for addons, or when the build fails + #### Uses: + + - Custom error handling during build process + @public @method buildError @param {Error} error The error that was caught during the processes listed above + + @example + - [ember-cli-rails-addon](https://github.com/rondale-sc/ember-cli-rails-addon/blob/v0.7.0/index.js#L11) */ +/** + Used to add preprocessors to the preprocessor registry. This is often used by addons like [ember-cli-htmlbars](https://github.com/ember-cli/ember-cli-htmlbars) + and [ember-cli-coffeescript](https://github.com/kimroen/ember-cli-coffeescript) to add a `template` or `js` preprocessor to the registry. + + **Uses:** + + - Adding preprocessors to the registry. + + @public + @method setupPreprocessorRegistry + @param {String} type either `"self"` or `"parent"` + @param registry the registry to be set up + + @example + ```js + setupPreprocessorRegistry(type, registry) { + // ensure that broccoli-ember-hbs-template-compiler is not processing hbs files + registry.remove('template', 'broccoli-ember-hbs-template-compiler'); + + registry.add('template', { + name: 'ember-cli-htmlbars', + ext: 'hbs', + _addon: this, + toTree(tree) { + var htmlbarsOptions = this._addon.htmlbarsOptions(); + return htmlbarsCompile(tree, htmlbarsOptions); + }, + + precompile(string) { + var htmlbarsOptions = this._addon.htmlbarsOptions(); + var templateCompiler = htmlbarsOptions.templateCompiler; + return utils.template(templateCompiler, string); + } + }); + + if (type === 'parent') { + this.parentRegistry = registry; + } + } + ``` +*/ + +/** + Return value is merged into the **tests** tree. This lets you inject + linter output as test results. + + **Uses:** + + - JSHint + - any other form of automated test generation that turns code into tests + + @public + @method lintTree + @param {String} treeType `app`, `tests`, `templates`, or `addon` + @param {Tree} tree tree of files (JavaScript files for `app`, `tests`, and `addon` types) + + @example + - [ember-cli-qunit](https://github.com/ember-cli/ember-cli-qunit/blob/v1.4.1/index.js#L206) + - [ember-cli-mocha](https://github.com/ef4/ember-cli-mocha/blob/66803037fe203b24e96dea83a2bd91de48b842e1/index.js#L101) +*/ + +/** + Allow addons to implement contentFor method to add string output into the associated `{{content-for 'foo'}}` section in `index.html` + + **Uses:** + + - For instance, to inject analytics code into `index.html` + + @public + @method contentFor + @param type + @param config + @param content + + @example + - [ember-cli-google-analytics](https://github.com/pgrippi/ember-cli-google-analytics/blob/v1.5.0/index.js#L79) +*/ + +function methodsForTreeType(treeType) { + let treeMethods = DEFAULT_TREE_FOR_METHOD_METHODS[treeType]; + + return GLOBAL_TREE_FOR_METHOD_METHODS.concat(treeMethods); +} + +let Addon = CoreObject.extend(addonProto); + module.exports = Addon; +module.exports._resetTreeCache = _resetTreeCache; +module.exports._treeCache = ADDON_TREE_CACHE; diff --git a/lib/models/addons-factory.js b/lib/models/addons-factory.js deleted file mode 100644 index 3cdc60e41b..0000000000 --- a/lib/models/addons-factory.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -/** -@module ember-cli -*/ - -var CoreObject = require('core-object'); -var DAG = require('../utilities/DAG'); -var debug = require('debug')('ember-cli:addons-factory'); - -/** - AddonsFactory is responsible for instantiating a collection of addons, in the right order. - - @class AddonsFactory - @extends CoreObject - @constructor -*/ -function AddonsFactory(addonParent, project) { - this.addonParent = addonParent; - this.project = project; -} - -AddonsFactory.__proto__ = CoreObject; -AddonsFactory.prototype.constructor = AddonsFactory; - -AddonsFactory.prototype.initializeAddons = function(addonPackages){ - var addonParent = this.addonParent; - var project = this.project; - var graph = new DAG(); - var Addon = require('../models/addon'); - var addonInfo, emberAddonConfig; - - debug('initializeAddons for: ', typeof addonParent.name === 'function' ? addonParent.name() : addonParent.name); - debug(' addon names are:', Object.keys(addonPackages)); - - for (var name in addonPackages) { - addonInfo = addonPackages[name]; - emberAddonConfig = addonInfo.pkg['ember-addon']; - - graph.addEdges(name, addonInfo, emberAddonConfig.before, emberAddonConfig.after); - } - - var addons = []; - graph.topsort(function (vertex) { - var addonInfo = vertex.value; - if (addonInfo) { - var AddonConstructor = Addon.lookup(addonInfo); - var addon = new AddonConstructor(addonParent, project); - if (addon.initializeAddons) { - addon.initializeAddons(); - } else { - addon.addons = []; - } - addons.push(addon); - } - }); - - debug(' addons ordered as:', addons.map(function(addon) { - return addon.name; - })); - - return addons; -}; - -// Export -module.exports = AddonsFactory; diff --git a/lib/models/asset-size-printer.js b/lib/models/asset-size-printer.js new file mode 100644 index 0000000000..78b40e75fe --- /dev/null +++ b/lib/models/asset-size-printer.js @@ -0,0 +1,92 @@ +'use strict'; + +const { default: chalk } = require('chalk'); +const path = require('path'); +const walkSync = require('walk-sync'); +const { pool: workerPool } = require('workerpool'); + +module.exports = class AssetPrinterSize { + constructor(options) { + Object.assign(this, options); + } + + print() { + const { filesize } = require('filesize'); + let ui = this.ui; + + return this.makeAssetSizesObject().then((files) => { + if (files.length !== 0) { + ui.writeLine(chalk.green('File sizes:')); + return files.forEach((file) => { + let sizeOutput = filesize(file.size); + if (file.showGzipped) { + sizeOutput += ` (${filesize(file.gzipSize)} gzipped)`; + } + + ui.writeLine(chalk.blue(` - ${file.name}: `) + sizeOutput); + }); + } else { + ui.writeLine(chalk.red(`No asset files found in the path provided: ${this.outputPath}`)); + } + }); + } + + printJSON() { + let ui = this.ui; + return this.makeAssetSizesObject().then((files) => { + if (files.length !== 0) { + let entries = files.map((file) => ({ + name: file.name, + size: file.size, + gzipSize: file.gzipSize, + })); + ui.writeLine(JSON.stringify({ files: entries })); + } else { + ui.writeLine(chalk.red(`No asset files found in the path provided: ${this.outputPath}`)); + } + }); + } + + async makeAssetSizesObject() { + let files = this.findFiles(); + let testFileRegex = /(test-(loader|support))|(testem)/i; + + // create a dedicated worker + const pool = workerPool(`${__dirname}/worker.js`); + + try { + let assets = files + // Skip test files + .filter((file) => { + let filename = path.basename(file); + return !testFileRegex.test(filename); + }) + // Print human-readable file sizes (including gzipped) + .map((file) => { + return pool.exec('gzipStats', [file]); + }); + + return await Promise.all(assets); + } finally { + pool.terminate(); // terminate all workers when done + } + } + + findFiles() { + let outputPath = this.outputPath; + + try { + return walkSync(outputPath, { + directories: false, + }) + .filter((x) => x.endsWith('.css') || x.endsWith('.js')) + .map((x) => path.join(outputPath, x)); + } catch (e) { + if (e !== null && typeof e === 'object' && e.code === 'ENOENT') { + throw new Error(`No asset files found in the path provided: ${outputPath}`); + } else { + throw e; + } + } + } +}; diff --git a/lib/models/blueprint.js b/lib/models/blueprint.js deleted file mode 100644 index 8a1b87c97f..0000000000 --- a/lib/models/blueprint.js +++ /dev/null @@ -1,1548 +0,0 @@ -'use strict'; - -/** -@module ember-cli -*/ -var FileInfo = require('./file-info'); -var Promise = require('../ext/promise'); -var chalk = require('chalk'); -var MarkdownColor = require('../../lib/utilities/markdown-color'); -var fs = require('fs-extra'); -var existsSync = require('exists-sync'); -var inflector = require('inflection'); -var minimatch = require('minimatch'); -var path = require('path'); -var sequence = require('../utilities/sequence'); -var stat = Promise.denodeify(fs.stat); -var stringUtils = require('ember-cli-string-utils'); -var compact = require('lodash/array/compact'); -var intersect = require('lodash/array/intersection'); -var uniq = require('lodash/array/uniq'); -var zipObject = require('lodash/array/zipObject'); -var contains = require('lodash/collection/contains'); -var any = require('lodash/collection/some'); -var keys = require('lodash/object/keys'); -var merge = require('lodash/object/merge'); -var values = require('lodash/object/values'); -var walkSync = require('walk-sync'); -var writeFile = Promise.denodeify(fs.outputFile); -var removeFile = Promise.denodeify(fs.remove); -var readFileSync = fs.readFileSync; -var SilentError = require('silent-error'); -var CoreObject = require('core-object'); -var EOL = require('os').EOL; -var deprecateUI = require('../utilities/deprecate').deprecateUI; -var bowEpParser = require('bower-endpoint-parser'); -var debug = require('debug')('ember-cli:blueprint'); -var printableProperties = require('../utilities/get-printable-properties')(); -var normalizeEntityName = require('ember-cli-normalize-entity-name'); - -module.exports = Blueprint; - -printableProperties.push('overridden'); - -/** - A blueprint is a bundle of template files with optional install - logic. - - Blueprints follow a simple structure. Let's take the built-in - `controller` blueprint as an example: - - ``` - blueprints/controller - ├── files - │   ├── app - │   │   └── __path__ - │   │   └── __name__.js - └── index.js - - blueprints/controller-test - ├── files - │   └── tests - │   └── unit - │   └── controllers - │   └── __test__.js - └── index.js - ``` - - ## Files - - `files` contains templates for the all the files to be - installed into the target directory. - - The `__name__` token is subtituted with the dasherized - entity name at install time. For example, when the user - invokes `ember generate controller foo` then `__name__` becomes - `foo`. When the `--pod` flag is used, for example `ember - generate controller foo --pod` then `__name__` becomes - `controller`. - - The `__path__` token is substituted with the blueprint - name at install time. For example, when the user invokes - `ember generate controller foo` then `__path__` becomes - `controller`. When the `--pod` flag is used, for example - `ember generate controller foo --pod` then `__path__` - becomes `foo` (or `/foo` if the - podModulePrefix is defined). This token is primarily for - pod support, and is only necessary if the blueprint can be - used in pod structure. If the blueprint does not require pod - support, simply use the blueprint name instead of the - `__path__` token. - - The `__test__` token is substituted with the dasherized - entity name and appended with `-test` at install time. - This token is primarily for pod support and only necessary - if the blueprint requires support for a pod structure. If - the blueprint does not require pod support, simply use the - `__name__` token instead. - - ## Template Variables (AKA Locals) - - Variables can be inserted into templates with - `<%= someVariableName %>`. - - For example, the built-in `util` blueprint - `files/app/utils/__name__.js` looks like this: - - ```js - export default function <%= camelizedModuleName %>() { - return true; - } - ``` - - `<%= camelizedModuleName %>` is replaced with the real - value at install time. - - The following template variables are provided by default: - - - `dasherizedPackageName` - - `classifiedPackageName` - - `dasherizedModuleName` - - `classifiedModuleName` - - `camelizedModuleName` - - `packageName` is the project name as found in the project's - `package.json`. - - `moduleName` is the name of the entity being generated. - - The mechanism for providing custom template variables is - described below. - - ## Index.js - - Custom installation and uninstallation behaviour can be added - by overriding the hooks documented below. `index.js` should - export a plain object, which will extend the prototype of the - `Blueprint` class. If needed, the original `Blueprint` prototype - can be accessed through the `_super` property. - - ```js - module.exports = { - locals: function(options) { - // Return custom template variables here. - return {}; - }, - - normalizeEntityName: function(entityName) { - // Normalize and validate entity name here. - return entityName; - }, - - fileMapTokens: function(options) ( - // Return custom tokens to be replaced in your files - return { - __token__: function(options){ - // logic to determine value goes here - return 'value'; - } - } - }, - - beforeInstall: function(options) {}, - afterInstall: function(options) {}, - beforeUninstall: function(options) {}, - afterUninstall: function(options) {} - - }; - ``` - - ## Blueprint Hooks - - As shown above, the following hooks are available to - blueprint authors: - - - `locals` - - `normalizeEntityName` - - `fileMapTokens` - - `beforeInstall` - - `afterInstall` - - `beforeUninstall` - - `afterUninstall` - - ### locals - - Use `locals` to add custom tempate variables. The method - receives one argument: `options`. Options is an object - containing general and entity-specific options. - - When the following is called on the command line: - - ```sh - ember generate controller foo --type=array --dry-run - ``` - - The object passed to `locals` looks like this: - - ```js - { - entity: { - name: 'foo', - options: { - type: 'array' - } - }, - dryRun: true - } - ``` - - This hook must return an object. It will be merged with the - aforementioned default locals. - - ### normalizeEntityName - - Use the `normalizeEntityName` hook to add custom normalization and - validation of the provided entity name. The default hook does not - make any changes to the entity name, but makes sure an entity name - is present and that it doesn't have a trailing slash. - - This hook receives the entity name as its first argument. The string - returned by this hook will be used as the new entity name. - - ### fileMapTokens - - Use `fileMapTokens` to add custom fileMap tokens for use - in the `mapFile` method. The hook must return an object in the - following pattern: - - ```js - { - __token__: function(options){ - // logic to determine value goes here - return 'value'; - } - } - ``` - - It will be merged with the default `fileMapTokens`, and can be used - to override any of the default tokens. - - Tokens are used in the files folder (see `files`), and get replaced with - values when the `mapFile` method is called. - - ### beforeInstall & beforeUninstall - - Called before any of the template files are processed and receives - the the `options` and `locals` hashes as parameters. Typically used for - validating any additional command line options or for any asynchronous - setup that is needed. As an example, the `controller` blueprint validates - its `--type` option in this hook. If you need to run any asynchronous code, - wrap it in a promise and return that promise from these hooks. This will - ensure that your code is executed correctly. - - ### afterInstall & afterUninstall - - The `afterInstall` and `afterUninstall` hooks receives the same - arguments as `locals`. Use it to perform any custom work after the - files are processed. For example, the built-in `route` blueprint - uses these hooks to add and remove relevant route declarations in - `app/router.js`. - - ### Overriding Install - - If you don't want your blueprint to install the contents of - `files` you can override the `install` method. It receives the - same `options` object described above and must return a promise. - See the built-in `resource` blueprint for an example of this. - - @class Blueprint - @constructor - @extends CoreObject - @param {String} [blueprintPath] -*/ -function Blueprint(blueprintPath) { - this.path = blueprintPath; - this.name = path.basename(blueprintPath); -} - -Blueprint.__proto__ = CoreObject; -Blueprint.prototype.constructor = Blueprint; - -Blueprint.prototype.availableOptions = []; -Blueprint.prototype.anonymousOptions = ['name']; - -/** - Used to retrieve files for blueprint. The `file` param is an - optional string that is turned into a glob. - - @method files - @return {Array} Contents of the blueprint's files directory -*/ -Blueprint.prototype.files = function() { - if (this._files) { return this._files; } - - var filesPath = path.join(this.path, 'files'); - if (existsSync(filesPath)) { - this._files = walkSync(path.join(this.path, 'files')); - } else { - this._files = []; - } - - return this._files; -}; - -/** - @method srcPath - @param {String} file - @return {String} Resolved path to the file -*/ -Blueprint.prototype.srcPath = function(file) { - return path.resolve(this.path, 'files', file); -}; - -/** - Hook for normalizing entity name - @method normalizeEntityName - @param {String} entityName - @return {null} -*/ -Blueprint.prototype.normalizeEntityName = function(entityName) { - return normalizeEntityName(entityName); -}; - -/** - Write a status and message to the UI - @private - @method _writeStatusToUI - @param {Function} chalkColor - @param {String} keyword - @param {String} message -*/ -Blueprint.prototype._writeStatusToUI = function(chalkColor, keyword, message) { - if (this.ui) { - this.ui.writeLine(' ' + chalkColor(keyword) + ' ' + message); - } -}; - -/** - @private - @method _writeFile - @param {Object} info - @return {Promise} -*/ -Blueprint.prototype._writeFile = function(info) { - if (!this.dryRun) { - return writeFile(info.outputPath, info.render()); - } -}; - -/** - Actions lookup - @private -*/ - -Blueprint.prototype._actions = { - write: function(info) { - this._writeStatusToUI(chalk.green, 'create', info.displayPath); - return this._writeFile(info); - }, - skip: function(info) { - var label = 'skip'; - - if (info.resolution === 'identical') { - label = 'identical'; - } - - this._writeStatusToUI(chalk.yellow, label, info.displayPath); - }, - - overwrite: function(info) { - this._writeStatusToUI(chalk.yellow, 'overwrite', info.displayPath); - return this._writeFile(info); - }, - - edit: function(info) { - this._writeStatusToUI(chalk.green, 'edited', info.displayPath); - }, - - remove: function(info) { - this._writeStatusToUI(chalk.red, 'remove', info.displayPath); - if (!this.dryRun) { - return removeFile(info.outputPath); - } - } -}; - -/** - Calls an action. - @private - @method _commit - @param {Object} result - @return {Promise} - @throws {Error} Action doesn't exist. -*/ -Blueprint.prototype._commit = function(result) { - var action = this._actions[result.action]; - - if (action) { - return action.call(this, result); - } else { - throw new Error('Tried to call action \"' + result.action + '\" but it does not exist'); - } -}; - -/** - Prints warning for pod unsupported. - @private - @method _checkForPod -*/ -Blueprint.prototype._checkForPod = function(verbose) { - if (!this.hasPathToken && this.pod && verbose) { - this.ui.writeLine(chalk.yellow('You specified the pod flag, but this' + - ' blueprint does not support pod structure. It will be generated with' + - ' the default structure.')); - } -}; - -/** - @private - @method _normalizeEntityName - @param {Object} entity -*/ -Blueprint.prototype._normalizeEntityName = function(entity) { - if (entity) { - entity.name = this.normalizeEntityName(entity.name); - } -}; - -/** - @private - @method _checkInRepoAddonExists - @param {String} inRepoAddon -*/ -Blueprint.prototype._checkInRepoAddonExists = function(inRepoAddon) { - if (inRepoAddon) { - if (!inRepoAddonExists(inRepoAddon, this.project.root)) { - throw new SilentError('You specified the in-repo-addon flag, but the' + - ' in-repo-addon \'' + inRepoAddon + '\' does not exist. Please' + - ' check the name and try again.'); - } - } -}; - -/** - @private - @method _process - @param {Object} options - @param {Function} beforeHook - @param {Function} process - @param {Function} afterHook -*/ -Blueprint.prototype._process = function(options, beforeHook, process, afterHook) { - var intoDir = options.target; - var locals = this._locals(options); - - return Promise.resolve() - .then(beforeHook.bind(this, options, locals)) - .then(process.bind(this, intoDir, locals)).map(this._commit.bind(this)) - .then(afterHook.bind(this, options)); -}; - -/** - @method install - @param {Object} options - @return {Promise} -*/ -Blueprint.prototype.install = function(options) { - var ui = this.ui = options.ui; - var dryRun = this.dryRun = options.dryRun; - this.project = options.project; - this.pod = options.pod; - this.hasPathToken = hasPathToken(this.files()); - - podDeprecations(this.project.config(), ui); - - ui.writeLine('installing ' + this.name); - - if (dryRun) { - ui.writeLine(chalk.yellow('You specified the dry-run flag, so no' + - ' changes will be written.')); - } - - this._normalizeEntityName(options.entity); - this._checkForPod(options.verbose); - this._checkInRepoAddonExists(options.inRepoAddon); - - debug('START: processing blueprint: `%s`', this.name); - var start = new Date(); - return this._process( - options, - this.beforeInstall, - this.processFiles, - this.afterInstall).finally(function() { - debug('END: processing blueprint: `%s` in (%dms)', this.name, new Date() - start); - }.bind(this)); -}; - -/** - @method uninstall - @param {Object} options - @return {Promise} -*/ -Blueprint.prototype.uninstall = function(options) { - var ui = this.ui = options.ui; - var dryRun = this.dryRun = options.dryRun; - this.project = options.project; - this.pod = options.pod; - this.hasPathToken = hasPathToken(this.files()); - - podDeprecations(this.project.config(), ui); - - ui.writeLine('uninstalling ' + this.name); - - if (dryRun) { - ui.writeLine(chalk.yellow('You specified the dry-run flag, so no' + - ' files will be deleted.')); - } - - this._normalizeEntityName(options.entity); - this._checkForPod(options.verbose); - - return this._process( - options, - this.beforeUninstall, - this.processFilesForUninstall, - this.afterUninstall); -}; - -/** - Hook for running operations before install. - @method beforeInstall - @return {Promise|null} -*/ -Blueprint.prototype.beforeInstall = function() {}; - -/** - Hook for running operations after install. - @method afterInstall - @return {Promise|null} -*/ -Blueprint.prototype.afterInstall = function() {}; - -/** - Hook for running operations before uninstall. - @method beforeUninstall - @return {Promise|null} -*/ -Blueprint.prototype.beforeUninstall = function() {}; - -/** - Hook for running operations after uninstall. - @method afterUninstall - @return {Promise|null} -*/ -Blueprint.prototype.afterUninstall = function() {}; - -/** - Hook for adding additional locals - @method locals - @return {Object|null} -*/ -Blueprint.prototype.locals = function() {}; - -/** - Hook to add additional or override existing fileMapTokens. - @method fileMapTokens - @return {Object|null} -*/ -Blueprint.prototype.fileMapTokens = function() { -}; - -/** - @private - @method _fileMapTokens - @param {Object} options - @return {Object} -*/ -Blueprint.prototype._fileMapTokens = function(options) { - var standardTokens = { - __name__: function(options) { - if (options.pod && options.hasPathToken) { - return options.blueprintName; - } - return options.dasherizedModuleName; - }, - __path__: function(options) { - var blueprintName = options.blueprintName; - - if(blueprintName.match(/-test/)) { - blueprintName = options.blueprintName.slice(0, options.blueprintName.indexOf('-test')); - } - if (options.pod && options.hasPathToken) { - return path.join(options.podPath, options.dasherizedModuleName); - } - return inflector.pluralize(blueprintName); - }, - __root__: function(options) { - if (options.inRepoAddon) { - return path.join('lib',options.inRepoAddon, 'addon'); - } - if (options.inDummy) { - return path.join('tests','dummy','app'); - } - if (options.inAddon) { - return 'addon'; - } - return 'app'; - }, - __test__: function(options) { - if (options.pod && options.hasPathToken) { - return options.blueprintName; - } - return options.dasherizedModuleName + '-test'; - } - }; - - var customTokens = this.fileMapTokens(options) || options.fileMapTokens || {}; - return merge(standardTokens, customTokens); -}; - -/** - Used to generate fileMap tokens for mapFile. - - @method generateFileMap - @param {Object} fileMapVariables - @return {Object} -*/ -Blueprint.prototype.generateFileMap = function(fileMapVariables){ - var tokens = this._fileMapTokens(fileMapVariables); - var fileMapValues = values(tokens); - var tokenValues = fileMapValues.map(function(token) { return token(fileMapVariables); }); - var tokenKeys = keys(tokens); - return zipObject(tokenKeys,tokenValues); -}; - -/** - @method buildFileInfo - @param {Function} destPath - @param {Object} templateVariables - @param {String} file - @return {FileInfo} -*/ -Blueprint.prototype.buildFileInfo = function(destPath, templateVariables, file) { - var mappedPath = this.mapFile(file, templateVariables); - - return new FileInfo({ - action: 'write', - outputPath: destPath(mappedPath), - displayPath: path.normalize(mappedPath), - inputPath: this.srcPath(file), - templateVariables: templateVariables, - ui: this.ui - }); -}; - -/** - @method isUpdate - @return {Boolean} -*/ -Blueprint.prototype.isUpdate = function() { - if (this.project && this.project.isEmberCLIProject) { - return this.project.isEmberCLIProject(); - } -}; - -/** - @private - @method _getFileInfos - @param {Array} files - @param {String} intoDir - @param {Object} templateVariables - @return {Array} file infos -*/ -Blueprint.prototype._getFileInfos = function(files, intoDir, templateVariables) { - return files.map(this.buildFileInfo.bind(this, destPath.bind(null, intoDir), templateVariables)); -}; - -/** - Add update files to ignored files - @private - @method _ignoreUpdateFiles -*/ -Blueprint.prototype._ignoreUpdateFiles = function() { - if (this.isUpdate()) { - Blueprint.ignoredFiles = Blueprint.ignoredFiles.concat(Blueprint.ignoredUpdateFiles); - } -}; - -/** - @private - @method _getFilesForInstall - @param {Array} targetFiles - @return {Array} files -*/ -Blueprint.prototype._getFilesForInstall = function(targetFiles) { - var files = this.files(); - - // if we've defined targetFiles, get file info on ones that match - return targetFiles && targetFiles.length > 0 && intersect(files, targetFiles) || files; -}; - -/** - @private - @method _checkForNoMatch - @param {Array} fileInfos - @param {String} rawArgs -*/ -Blueprint.prototype._checkForNoMatch = function(fileInfos, rawArgs) { - if (fileInfos.filter(isFilePath).length < 1 && rawArgs) { - this.ui.writeLine(chalk.yellow('The globPattern \"' + rawArgs + - '\" did not match any files, so no file updates will be made.')); - } -}; - -function finishProcessingForInstall(infos) { - infos.forEach(markIdenticalToBeSkipped); - - var infosNeedingConfirmation = infos.reduce(gatherConfirmationMessages, []); - - return sequence(infosNeedingConfirmation).returns(infos); -} - -function finishProcessingForUninstall(infos) { - infos.forEach(markToBeRemoved); - return infos; -} - -/** - @method processFiles - @param {String} intoDir - @param {Object} templateVariables -*/ -Blueprint.prototype.processFiles = function(intoDir, templateVariables) { - var files = this._getFilesForInstall(templateVariables.targetFiles); - var fileInfos = this._getFileInfos(files, intoDir, templateVariables); - - this._checkForNoMatch(fileInfos, templateVariables.rawArgs); - - this._ignoreUpdateFiles(); - - return Promise.filter(fileInfos, isValidFile). - map(prepareConfirm). - then(finishProcessingForInstall); -}; - -/** - @method processFilesForUninstall - @param {String} intoDir - @param {Object} templateVariables -*/ -Blueprint.prototype.processFilesForUninstall = function(intoDir, templateVariables) { - var fileInfos = this._getFileInfos(this.files(), intoDir, templateVariables); - - this._ignoreUpdateFiles(); - - return Promise.filter(fileInfos, isValidFile). - then(finishProcessingForUninstall); -}; - - -/** - @method mapFile - @param {String} file - @return {String} -*/ -Blueprint.prototype.mapFile = function(file, locals) { - var pattern, i; - var fileMap = locals.fileMap || { __name__: locals.dasherizedModuleName }; - file = Blueprint.renamedFiles[file] || file; - for (i in fileMap) { - pattern = new RegExp(i, 'g'); - file = file.replace(pattern, fileMap[i]); - } - return file; -}; - -/** - Looks for a __root__ token in the files folder. Must be present for - the blueprint to support addon tokens. The `server`, `blueprints`, and `test` - - @private - @method supportsAddon - @return {Boolean} -*/ -Blueprint.prototype.supportsAddon = function() { - return this.files().join().match(/__root__/); -}; - -/** - @private - @method _generateFileMapVariables - @param {Object} options - @return {Object} -*/ -Blueprint.prototype._generateFileMapVariables = function(moduleName, locals, options) { - var originBlueprintName = options.originBlueprintName || this.name; - var podModulePrefix = this.project.config().podModulePrefix || ''; - var podPath = podModulePrefix.substr(podModulePrefix.lastIndexOf('/') + 1); - var inAddon = this.project.isEmberCLIAddon() || !!options.inRepoAddon; - var inDummy = this.project.isEmberCLIAddon() ? options.dummy : false; - - return { - pod: this.pod, - podPath: podPath, - hasPathToken: this.hasPathToken, - inAddon: inAddon, - inRepoAddon: options.inRepoAddon, - inDummy: inDummy, - blueprintName: this.name, - originBlueprintName: originBlueprintName, - dasherizedModuleName: stringUtils.dasherize(moduleName), - locals: locals - }; -}; - -/** - @private - @method _locals - @param {Object} options - @return {Object} -*/ -Blueprint.prototype._locals = function(options) { - var packageName = options.project.name(); - var moduleName = options.entity && options.entity.name || packageName; - var sanitizedModuleName = moduleName.replace(/\//g, '-'); - var customLocals = this.locals(options); - var fileMapVariables = this._generateFileMapVariables(moduleName, customLocals, options); - var fileMap = this.generateFileMap(fileMapVariables); - - var standardLocals = { - dasherizedPackageName: stringUtils.dasherize(packageName), - classifiedPackageName: stringUtils.classify(packageName), - dasherizedModuleName: stringUtils.dasherize(moduleName), - classifiedModuleName: stringUtils.classify(sanitizedModuleName), - camelizedModuleName: stringUtils.camelize(sanitizedModuleName), - decamelizedModuleName: stringUtils.decamelize(sanitizedModuleName), - fileMap: fileMap, - hasPathToken: this.hasPathToken, - targetFiles: options.targetFiles, - rawArgs: options.rawArgs - }; - - return merge({}, standardLocals, customLocals); -}; - -/** - Used to add a package to the projects `package.json`. - - Generally, this would be done from the `afterInstall` hook, to - ensure that a package that is required by a given blueprint is - available. - - @method addPackageToProject - @param {String} packageName - @param {String} target - @return {Promise} -*/ -Blueprint.prototype.addPackageToProject = function(packageName, target) { - var packageObject = {name: packageName}; - - if (target) { - packageObject.target = target; - } - - return this.addPackagesToProject([packageObject]); -}; - -/** - Used to add multiple packages to the projects `package.json`. - - Generally, this would be done from the `afterInstall` hook, to - ensure that a package that is required by a given blueprint is - available. - - Expects each array item to be an object with a `name`. Each object - may optionally have a `target` to specify a specific version. - - @method addPackagesToProject - @param {Array} packages - @return {Promise} -*/ -Blueprint.prototype.addPackagesToProject = function(packages) { - var task = this.taskFor('npm-install'); - var install = (packages.length > 1) ? 'install packages' : 'install package'; - var packageNames = []; - var packageArray = []; - - for (var i = 0; i < packages.length; i++) { - packageNames.push(packages[i].name); - - var packageNameAndVersion = packages[i].name; - - if (packages[i].target) { - packageNameAndVersion += '@' + packages[i].target; - } - - packageArray.push(packageNameAndVersion); - } - - this._writeStatusToUI(chalk.green, install, packageNames.join(', ')); - - return task.run({ - 'save-dev': true, - verbose: false, - packages: packageArray - }); -}; - -/** - Used to remove a package from the projects `package.json`. - - Generally, this would be done from the `afterInstall` hook, to - ensure that any package conflicts can be resolved before the - addon is used. - - @method removePackageFromProject - @param {String} packageName - @return {Promise} -*/ -Blueprint.prototype.removePackageFromProject = function(packageName) { - var packageObject = {name: packageName}; - - return this.removePackagesFromProject([packageObject]); -}; - -/** - Used to remove multiple packages from the projects `package.json`. - - Generally, this would be done from the `afterInstall` hook, to - ensure that any package conflicts can be resolved before the - addon is used. - - Expects each array item to be an object with a `name` property. - - @method removePackagesFromProject - @param {Array} packages - @return {Promise} -*/ -Blueprint.prototype.removePackagesFromProject = function(packages) { - var task = this.taskFor('npm-uninstall'); - var install = (packages.length > 1) ? 'uninstall packages' : 'uninstall package'; - var packageNames = []; - var packageArray = []; - - for (var i = 0; i < packages.length; i++) { - packageNames.push(packages[i].name); - - var packageNameAndVersion = packages[i].name; - - packageArray.push(packageNameAndVersion); - } - - this._writeStatusToUI(chalk.green, install, packageNames.join(', ')); - - return task.run({ - 'save-dev': true, - verbose: false, - packages: packageArray - }); -}; - -/** - Used to add a package to the projects `bower.json`. - - Generally, this would be done from the `afterInstall` hook, to - ensure that a package that is required by a given blueprint is - available. - - `localPackageName` and `target` may be thought of as equivalent - to the key-value pairs in the `dependency` or `devDepencency` - objects contained within a bower.json file. - - Examples: - - addBowerPackageToProject('jquery', '~1.11.1'); - addBowerPackageToProject('old_jquery', 'jquery#~1.9.1'); - addBowerPackageToProject('bootstrap-3', 'http://twitter.github.io/bootstrap/assets/bootstrap'); - - @method addBowerPackageToProject - @param {String} localPackageName - @param {String} target - @param {Object} installOptions - @return {Promise} -*/ -Blueprint.prototype.addBowerPackageToProject = function(localPackageName, target, installOptions) { - var lpn = localPackageName; - var tar = target; - if (localPackageName.indexOf('#') >= 0) { - if (arguments.length === 1) { - var parts = localPackageName.split('#'); - lpn = parts[0]; - tar = parts[1]; - deprecateUI(this.ui)('passing ' + localPackageName + - ' directly to `addBowerPackageToProject` will soon be unsupported. \n' + - 'You may want to replace this with ' + - '`addBowerPackageToProject(\'' + lpn +'\', \'' + tar + '\')`', true); - } else { - deprecateUI(this.ui)('passing ' + localPackageName + - ' directly to `addBowerPackageToProject` will soon be unsupported', true); - } - } - var packageObject = bowEpParser.json2decomposed(lpn, tar); - return this.addBowerPackagesToProject([packageObject], installOptions); -}; - -/** - Used to add an array of packages to the projects `bower.json`. - - Generally, this would be done from the `afterInstall` hook, to - ensure that a package that is required by a given blueprint is - available. - - Expects each array item to be an object with a `name`. Each object - may optionally have a `target` to specify a specific version. - - @method addBowerPackagesToProject - @param {Array} packages - @param {Object} installOptions - @return {Promise} -*/ -Blueprint.prototype.addBowerPackagesToProject = function(packages, installOptions) { - var task = this.taskFor('bower-install'); - var packageNamesAndVersions = packages - .map(function(pkg) { - pkg.source = pkg.source || pkg.name; - return pkg; - }) - .map(bowEpParser.compose); - - return task.run({ - verbose: true, - packages: packageNamesAndVersions, - installOptions: installOptions - }); -}; - -/** - Used to add an addon to the projects `package.json` and run it's - `defaultBlueprint` if it provides one. - - Generally, this would be done from the `afterInstall` hook, to - ensure that a package that is required by a given blueprint is - available. - - @method addAddonToProject - @param {Object} options - @return {Promise} -*/ -Blueprint.prototype.addAddonToProject = function(options) { - var taskOptions = {}; - - if (typeof options === 'string') { - taskOptions['packages'] = [options]; - } else { - if (!options.name) { - throw new SilentError('You must provide a `name` to addAddonToProject'); - } - - if (options.target) { - options.name += '@' + options.target; - } - - taskOptions['packages'] = [options.name]; - taskOptions.extraArgs = options.extraArgs || []; - taskOptions.blueprintOptions = options.blueprintOptions || {}; - } - - var task = this.taskFor('addon-install'); - - this._writeStatusToUI(chalk.green, 'install addon', taskOptions['packages']); - - return task.run(taskOptions); -}; - -/** - Used to retrieve a task with the given name. Passes the new task - the standard information available (like `ui`, `analytics`, `project`, etc). - - @method taskFor - @param dasherizedName - @public -*/ -Blueprint.prototype.taskFor = function(dasherizedName) { - var Task = require('../tasks/' + dasherizedName); - - return new Task({ - ui: this.ui, - project: this.project, - analytics: this.analytics - }); -}; - -/* - - Inserts the given content into a file. If the `contentsToInsert` string is already - present in the current contents, the file will not be changed unless `force` option - is passed. - - If `options.before` is specified, `contentsToInsert` will be inserted before - the first instance of that string. If `options.after` is specified, the - contents will be inserted after the first instance of that string. - If the string specified by options.before or options.after is not in the file, - no change will be made. - - If neither `options.before` nor `options.after` are present, `contentsToInsert` - will be inserted at the end of the file. - - Example: - ``` - // app/router.js - Router.map(function(){ - }); - - insertIntoFile('app/router.js', - ' this.route("admin");', - {after:'Router.map(function() {'+EOL}); - - // new app/router.js - Router.map(function(){ - this.route("admin"); - }); - ``` - - @method insertIntoFile - @param {String} pathRelativeToProjectRoot - @param {String} contentsToInsert - @param {Object} options - @return {Promise} -*/ -Blueprint.prototype.insertIntoFile = function(pathRelativeToProjectRoot, contentsToInsert, providedOptions) { - var fullPath = path.join(this.project.root, pathRelativeToProjectRoot); - var originalContents = ''; - - if (existsSync(fullPath)) { - originalContents = fs.readFileSync(fullPath, { encoding: 'utf8' }); - } - - var contentsToWrite = originalContents; - - var options = providedOptions || {}; - var alreadyPresent = originalContents.indexOf(contentsToInsert) > -1; - var insert = !alreadyPresent; - var insertBehavior = 'end'; - - if (options.before) { insertBehavior = 'before'; } - if (options.after) { insertBehavior = 'after'; } - - if (options.force) { insert = true; } - - if (insert) { - if (insertBehavior === 'end') { - contentsToWrite += contentsToInsert; - } else { - var contentMarker = options[insertBehavior]; - var contentMarkerIndex = contentsToWrite.indexOf(contentMarker); - - if (contentMarkerIndex !== -1) { - var insertIndex = contentMarkerIndex; - if (insertBehavior === 'after') { insertIndex += contentMarker.length; } - - contentsToWrite = contentsToWrite.slice(0, insertIndex) + - contentsToInsert + EOL + - contentsToWrite.slice(insertIndex); - } - } - } - - var returnValue = { - path: fullPath, - originalContents: originalContents, - contents: contentsToWrite, - inserted: false - }; - - if (contentsToWrite !== originalContents) { - returnValue.inserted = true; - - return writeFile(fullPath, contentsToWrite) - .then(function() { - return returnValue; - }); - } else { - return Promise.resolve(returnValue); - } -}; - -Blueprint.prototype._getDetailedHelpPath = function() { - if (!this.path) { - return null; - } - - return path.join(this.path, './HELP.md'); -}; - -Blueprint.prototype.printDetailedHelp = function() { - var markdownColor = new MarkdownColor(); - var filePath = this._getDetailedHelpPath(); - - if (existsSync(filePath)) { - return markdownColor.renderFile(filePath, {indent:' '}); - } - return ''; -}; - -Blueprint.prototype.getJson = function(verbose) { - var json = {}; - - printableProperties.forEach(function(key) { - if (this[key] !== undefined) { - json[key] = this[key]; - } - }, this); - - if (verbose) { - var filePath = this._getDetailedHelpPath(); - - if (existsSync(filePath)) { - json.detailedHelp = readFileSync(filePath, 'utf-8'); - } - } - - return json; -}; - -/** - Used to retrieve a blueprint with the given name. - - @method lookupBlueprint - @param dasherizedName - @public -*/ -Blueprint.prototype.lookupBlueprint = function(dasherizedName) { - var projectPaths = this.project ? this.project.blueprintLookupPaths() : []; - - return Blueprint.lookup(dasherizedName, { - paths: projectPaths - }); -}; - -/** - @static - @method lookup - @namespace Blueprint - @param {String} [name] - @param {Object} [options] - @param {Array} [options.paths] Extra paths to search for blueprints - @param {Object} [options.properties] Properties - @return {Blueprint} -*/ -Blueprint.lookup = function(name, options) { - options = options || {}; - - var lookupPaths = generateLookupPaths(options.paths); - - var lookupPath; - var blueprintPath; - - for (var i = 0; lookupPath = lookupPaths[i]; i++) { - blueprintPath = path.resolve(lookupPath, name); - - if (existsSync(blueprintPath)) { - return Blueprint.load(blueprintPath); - } - } - - if (!options.ignoreMissing) { - throw new SilentError('Unknown blueprint: ' + name); - } -}; - -/** - Loads a blueprint from given path. - @static - @method load - @namespace Blueprint - @param {String} blueprintPath - @return {Blueprint} blueprint instance -*/ -Blueprint.load = function(blueprintPath) { - var constructorPath = path.resolve(blueprintPath, 'index.js'); - var blueprintModule; - var Constructor = Blueprint; - - if (fs.lstatSync(blueprintPath).isDirectory()) { - - if (existsSync(constructorPath)) { - blueprintModule = require(constructorPath); - - if (typeof blueprintModule === 'function') { - Constructor = blueprintModule; - } else { - Constructor = Blueprint.extend(blueprintModule); - } - } - - return new Constructor(blueprintPath); - } - - return; -}; - -/** - @static - @method list - @namespace Blueprint - @param {Object} [options] - @param {Array} [options.paths] Extra paths to search for blueprints - @return {Blueprint} -*/ -Blueprint.list = function(options) { - options = options || {}; - - var lookupPaths = generateLookupPaths(options.paths); - var seen = []; - - return lookupPaths.map(function(lookupPath) { - var blueprints = dir(lookupPath); - var packagePath = path.join(lookupPath, '../package.json'); - var source; - - if (existsSync(packagePath)) { - source = require(packagePath).name; - } else { - source = path.basename(path.join(lookupPath, '..')); - } - - blueprints = blueprints.map(function(blueprintPath) { - var blueprint = Blueprint.load(blueprintPath); - var name; - - if (blueprint) { - name = blueprint.name; - blueprint.overridden = contains(seen, name); - seen.push(name); - - return blueprint; - } - - return; - }); - - return { - source: source, - blueprints: compact(blueprints) - }; - }); -}; - -/** - @static - @property renameFiles -*/ -Blueprint.renamedFiles = { - 'gitignore': '.gitignore' -}; - -/** - @static - @property ignoredFiles -*/ -Blueprint.ignoredFiles = [ - '.DS_Store' -]; - -/** - @static - @property ignoredUpdateFiles -*/ -Blueprint.ignoredUpdateFiles = [ - '.gitkeep', - 'app.css' -]; - -/** - @static - @property defaultLookupPaths -*/ -Blueprint.defaultLookupPaths = function() { - return [ - path.resolve(__dirname, '..', '..', 'blueprints') - ]; -}; - -/** - @private - @method prepareConfirm - @param {FileInfo} info - @return {Promise} -*/ -function prepareConfirm(info) { - return info.checkForConflict().then(function(resolution) { - info.resolution = resolution; - return info; - }); -} - -/** - @private - @method markIdenticalToBeSkipped - @param {FileInfo} info -*/ -function markIdenticalToBeSkipped(info) { - if (info.resolution === 'identical') { - info.action = 'skip'; - } -} - -/** - @private - @method markToBeRemoved - @param {FileInfo} info -*/ -function markToBeRemoved(info) { - info.action = 'remove'; -} - -/** - @private - @method gatherConfirmationMessages - @param {Array} collection - @param {FileInfo} info - @return {Array} -*/ -function gatherConfirmationMessages(collection, info) { - if (info.resolution === 'confirm') { - collection.push(info.confirmOverwriteTask()); - } - return collection; -} - -/** - @private - @method isFile - @param {FileInfo} info - @return {Boolean} -*/ -function isFile(info) { - return stat(info.inputPath).invoke('isFile'); -} - -/** - @private - @method isIgnored - @param {FileInfo} info - @return {Boolean} -*/ -function isIgnored(info) { - var fn = info.inputPath; - - return any(Blueprint.ignoredFiles, function(ignoredFile) { - return minimatch(fn, ignoredFile, { matchBase: true }); - }); -} - -/** - Combines provided lookup paths with defaults and removes - duplicates. - - @private - @method generateLookupPaths - @param {Array} lookupPaths - @return {Array} -*/ -function generateLookupPaths(lookupPaths) { - lookupPaths = lookupPaths || []; - lookupPaths = lookupPaths.concat(Blueprint.defaultLookupPaths()); - return uniq(lookupPaths); -} - -/** - Looks for a __path__ token in the files folder. Must be present for - the blueprint to support pod tokens. - - @private - @method hasPathToken - @param {files} files - @return {Boolean} -*/ -function hasPathToken(files) { - return files.join().match(/__path__/); -} - -function inRepoAddonExists(name, root) { - var addonPath = path.join(root, 'lib', name); - return existsSync(addonPath); -} - -function podDeprecations(config, ui){ - /* - var podModulePrefix = config.podModulePrefix || ''; - var podPath = podModulePrefix.substr(podModulePrefix.lastIndexOf('/') + 1); - // Disabled until we are ready to deprecate podModulePrefix - deprecateUI(ui)('`podModulePrefix` is deprecated and will be removed from future versions of ember-cli.'+ - ' Please move existing pods from \'app/' + podPath + '/\' to \'app/\'.', config.podModulePrefix); - */ - deprecateUI(ui)('`usePodsByDefault` is no longer supported in \'config/environment.js\','+ - ' use `usePods` in \'.ember-cli\' instead.', config.usePodsByDefault); -} - -/** - @private - @method destPath - @param {String} intoDir - @param {String} file - @return {String} new path -*/ -function destPath(intoDir, file) { - return path.join(intoDir, file); -} - -/** - @private - @method isValidFile - @param {Object} fileInfo - @return {Promise} -*/ -function isValidFile(fileInfo) { - if (isIgnored(fileInfo)) { - return Promise.resolve(false); - } else { - return isFile(fileInfo); - } -} - -/** - @private - @method isFilePath - @param {Object} fileInfo - @return {Promise} -*/ -function isFilePath(fileInfo) { - return fs.statSync(fileInfo.inputPath).isFile(); -} - -/** - @private - - @method dir - @returns {Array} list of files in the given directory or and empty array if no directory exists -*/ -function dir(fullPath) { - if (existsSync(fullPath)) { - return fs.readdirSync(fullPath).map(function(fileName) { - return path.join(fullPath, fileName); - }); - } else { - return []; - } -} diff --git a/lib/models/builder.js b/lib/models/builder.js index f8c37f1a5c..8b83e5773d 100644 --- a/lib/models/builder.js +++ b/lib/models/builder.js @@ -1,191 +1,274 @@ 'use strict'; +const onProcessInterrupt = require('../utilities/will-interrupt-process'); +const fs = require('fs-extra'); +const path = require('path'); +const CoreObject = require('core-object'); +const SilentError = require('silent-error'); +const { default: chalk } = require('chalk'); +const findBuildFile = require('../utilities/find-build-file'); +const _resetTreeCache = require('./addon')._resetTreeCache; +const Sync = require('tree-sync'); +const heimdall = require('heimdalljs'); +const progress = require('../utilities/heimdall-progress'); -var fs = require('fs-extra'); -var existsSync = require('exists-sync'); -var path = require('path'); -var Promise = require('../ext/promise'); -var remove = Promise.denodeify(fs.remove); -var Task = require('./task'); -var SilentError = require('silent-error'); -var chalk = require('chalk'); -var cpd = require('ember-cli-copy-dereference'); -var attemptNeverIndex = require('../utilities/attempt-never-index'); -var deprecate = require('../utilities/deprecate'); -var findBuildFile = require('../utilities/find-build-file'); -var viz = require('broccoli-viz'); - -var signalsTrapped = false; - -module.exports = Task.extend({ - setupBroccoliBuilder: function() { - this.environment = this.environment || 'development'; - process.env.EMBER_ENV = process.env.EMBER_ENV || this.environment; +/** + * Wrapper for the Broccoli [Builder](https://github.com/broccolijs/broccoli/blob/master/lib/builder.js) class. + * + * @private + * @module ember-cli + * @class Builder + * @constructor + * @extends Task + */ +class Builder extends CoreObject { + constructor(options) { + super(options); - var broccoli = require('broccoli'); - var hasBrocfile = existsSync(path.join('.', 'Brocfile.js')); - var buildFile = findBuildFile('ember-cli-build.js'); + this._instantiationStack = new Error().stack.replace(/[^\n]*\n/, ''); + this._cleanup = this.cleanup.bind(this); - deprecate('Brocfile.js has been deprecated in favor of ember-cli-build.js. Please see the transition guide: https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md#user-content-brocfile-transition.', hasBrocfile); + this._cleanupStarted = false; + this._onProcessInterrupt = options.onProcessInterrupt || onProcessInterrupt; + + this._onProcessInterrupt.addHandler(this._cleanup); + } - if (hasBrocfile) { - this.tree = broccoli.loadBrocfile(); - } else if (buildFile) { - this.tree = buildFile({ project: this.project }); - } else { - throw new Error('No ember-cli-build.js found. Please see the transition guide: https://github.com/ember-cli/ember-cli/blob/master/TRANSITION.md#user-content-brocfile-transition.'); + /** + * @private + * @method readBuildFile + * @param path The file path to read the build file from + */ + async readBuildFile(path) { + // Load the build file + let buildFile = await findBuildFile(path); + if (buildFile) { + return await buildFile({ project: this.project }); } - this.builder = new broccoli.Builder(this.tree); - }, - - trapSignals: function() { - if (!signalsTrapped) { - process.on('SIGINT', this.onSIGINT.bind(this)); - process.on('SIGTERM', this.onSIGTERM.bind(this)); - process.on('message', this.onMessage.bind(this)); - signalsTrapped = true; + throw new SilentError('No ember-cli-build.js found.'); + } + + async ensureBroccoliBuilder() { + if (this.builder === undefined) { + await this.setupBroccoliBuilder(); } - }, + } + + /** + * @private + * @method setupBroccoliBuilder + */ + async setupBroccoliBuilder() { + this.environment = this.environment || 'development'; + process.env.EMBER_ENV = process.env.EMBER_ENV || this.environment; + + this.tree = await this.readBuildFile(this.project.root); - init: function() { - this.setupBroccoliBuilder(); - this.trapSignals(); - }, + const broccoli = require('broccoli'); + + this.builder = new broccoli.Builder(this.tree); + } /** Determine whether the output path is safe to delete. If the outputPath appears anywhere in the parents of the project root, the build would delete the project directory. In this case return `false`, otherwise return `true`. + + @private + @method canDeleteOutputPath + @param {String} outputPath + @return {Boolean} */ - canDeleteOutputPath: function(outputPath) { - var rootPathParents = [this.project.root]; - var dir = path.dirname(this.project.root); + canDeleteOutputPath(outputPath) { + let rootPathParents = [this.project.root]; + let dir = path.dirname(this.project.root); rootPathParents.push(dir); while (dir !== path.dirname(dir)) { dir = path.dirname(dir); rootPathParents.push(dir); } return rootPathParents.indexOf(outputPath) === -1; - }, + } /** - This is used to ensure that the output path is emptied, but not deleted - itself. If we simply used `remove(this.outputPath)`, any symlinks would - now be broken. This iterates the direct children of the output path, - and calls `remove` on each (this preserving any symlinks). - */ - clearOutputPath: function() { - var outputPath = this.outputPath; - if (!existsSync(outputPath)) { return Promise.resolve();} + * @private + * @method copyToOutputPath + * @param {String} inputPath + */ + copyToOutputPath(inputPath) { + let outputPath = this.outputPath; + + fs.mkdirsSync(outputPath); - if(!this.canDeleteOutputPath(outputPath)) { - return Promise.reject(new SilentError('Using a build destination path of `' + outputPath + '` is not supported.')); + if (!this.canDeleteOutputPath(outputPath)) { + throw new SilentError(`Using a build destination path of \`${outputPath}\` is not supported.`); } - var promises = []; - var entries = fs.readdirSync(outputPath); + let sync = this._sync; + if (sync === undefined) { + this._sync = sync = new Sync(inputPath, path.resolve(this.outputPath)); + } + + let changes = sync.sync(); + + return changes.map((op) => op[1]); + } - for (var i = 0, l = entries.length; i < l; i++) { - promises.push(remove(path.join(outputPath, entries[i]))); + /** + * @private + * @method processAddonBuildSteps + * @param buildStep + * @param results + * @return {Promise} + */ + processAddonBuildSteps(buildStep, results) { + let addonPromises = []; + if (this.project && this.project.addons.length) { + addonPromises = this.project.addons.reduce((sum, addon) => { + let method = addon[buildStep]; + if (method) { + let val = method.call(addon, results); + if (val) { + sum.push(val); + } + } + return sum; + }, []); } - return Promise.all(promises); - }, + return Promise.all(addonPromises).then(() => results); + } + + /** + * @private + * @method build + * @return {Promise} + */ + async build(addWatchDirCallback, resultAnnotation) { + await this.ensureBroccoliBuilder(); + + let buildResults, uiProgressIntervalID; - copyToOutputPath: function(inputPath) { - var outputPath = this.outputPath; + try { + this.project._instrumentation.start('build'); - return new Promise(function(resolve) { - if (!existsSync(outputPath)) { - fs.mkdirsSync(outputPath); + if (addWatchDirCallback) { + for (let path of this.builder.watchedPaths) { + addWatchDirCallback(path); + } } - resolve(cpd.sync(inputPath, outputPath)); - }); - }, + this.ui.startProgress(progress.format(progress())); - processBuildResult: function(results) { - var self = this; + uiProgressIntervalID = setInterval(() => { + this.ui.spinner.text = progress.format(progress()); + }, this.ui.spinner.interval); - return this.clearOutputPath() - .then(function() { - return self.copyToOutputPath(results.directory); - }) - .then(function() { - return results; - }); - }, + await this.processAddonBuildSteps('preBuild'); - processAddonBuildSteps: function(buildStep, results) { - var addonPromises = []; - if (this.project && this.project.addons.length) { - addonPromises = this.project.addons.map(function(addon){ - if (addon[buildStep]) { - return addon[buildStep](results); - } - }).filter(Boolean); + try { + await this.builder.build(); + + // build legacy style results object (this is passed to various addon APIs) + buildResults = { + directory: this.builder.outputPath, + graph: this.builder.outputNodeWrapper, + }; + } catch (error) { + this.throwFormattedBroccoliError(error); + } + + await this.processAddonBuildSteps('postBuild', buildResults); + + let outputChanges = await this.copyToOutputPath(buildResults.directory); + + await this.processAddonBuildSteps( + 'outputReady', + Object.assign({}, buildResults, { outputChanges, directory: this.outputPath }) + ); + + return buildResults; + } catch (error) { + await this.processAddonBuildSteps('buildError', error); + + // Mark this as a builder error so the watcher knows it has been handled + // and won't re-throw it + error.isBuilderError = true; + + throw error; + } finally { + clearInterval(uiProgressIntervalID); + this.ui.stopProgress(); + this.project._instrumentation.stopAndReport('build', buildResults, resultAnnotation); + this.project.configCache.clear(); } + } + + /** + * Delegates to the `cleanup` method of the wrapped Broccoli builder. + * + * @private + * @method cleanup + * @return {Promise} + */ + async cleanup() { + if (!this._cleanupStarted) { + this._cleanupStarted = true; + let ui = this.project.ui; + ui.startProgress('cleaning up'); + ui.writeLine('cleaning up...'); - return Promise.all(addonPromises).then(function() { - return results; - }); - }, + // ensure any addon treeFor caches are reset + _resetTreeCache(); - build: function() { - var self = this; - var args = []; - for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); + this._onProcessInterrupt.removeHandler(this._cleanup); + + let node = heimdall.start({ name: 'Builder Cleanup' }); + try { + await this.builder.cleanup(); + } catch (error) { + ui.writeLine(chalk.red('Cleanup error.')); + ui.writeError(error); + } finally { + ui.stopProgress(); + node.stop(); + } } + } - attemptNeverIndex('tmp'); - - return this.processAddonBuildSteps('preBuild') - .then(function() { - return self.builder.build.apply(self.builder, args); - }) - .then(function(result) { - if (process.env.BROCCOLI_VIZ) { - var flattened = viz.flatten(result.graph); - fs.writeFileSync('graph.dot', viz.dot(flattened)); - fs.writeFileSync('graph.json', JSON.stringify(flattened)); - } - return result; - }) - .then(this.processAddonBuildSteps.bind(this, 'postBuild')) - .then(this.processBuildResult.bind(this)) - .catch(function(error) { - - this.processAddonBuildSteps('buildError', error); - throw error; - }.bind(this)); - }, - - cleanup: function() { - var ui = this.ui; - - return this.builder.cleanup().catch(function(err) { - ui.writeLine(chalk.red('Cleanup error.')); - ui.writeError(err); - }); - }, - - cleanupAndExit: function() { - this.cleanup().finally(function() { - process.exit(1); - }); - }, - - onSIGINT: function() { - this.cleanupAndExit(); - }, - onSIGTERM: function() { - this.cleanupAndExit(); - }, - onMessage: function(message) { - if (message.kill) { - this.cleanupAndExit(); + throwFormattedBroccoliError(err) { + // TODO fix ember-cli/console-ui to handle current broccoli broccoliPayload + let broccoliPayload = err && err.broccoliPayload; + if (broccoliPayload) { + if (!broccoliPayload.error) { + let originalError = broccoliPayload.originalError || {}; + let location = broccoliPayload.location || originalError.location; + broccoliPayload.error = { + message: originalError.message, + stack: originalError.stack, + errorType: originalError.type || 'Build Error', + codeFrame: originalError.codeFrame || originalError.message, + location: location || {}, + }; + } + if (!broccoliPayload.broccoliNode) { + broccoliPayload.broccoliNode = { + nodeName: broccoliPayload.nodeName, + nodeAnnotation: broccoliPayload.nodeAnnotation, + instantiationStack: broccoliPayload.instantiationStack || '', + }; + } + if (!broccoliPayload.versions) { + broccoliPayload.versions = { + broccoli: require('broccoli/package').version, + node: process.version, + }; + } } + + throw err; } -}); +} + +module.exports = Builder; diff --git a/lib/models/command.js b/lib/models/command.js index 0b95e95661..713e157304 100644 --- a/lib/models/command.js +++ b/lib/models/command.js @@ -1,526 +1,667 @@ 'use strict'; -var nopt = require('nopt'); -var chalk = require('chalk'); -var path = require('path'); -var isGitRepo = require('is-git-url'); -var camelize = require('../utilities/string').camelize; -var getCallerFile = require('../utilities/get-caller-file'); -var Promise = require('../ext/promise'); -var union = require('lodash/array/union'); -var uniq = require('lodash/array/uniq'); -var pluck = require('lodash/collection/pluck'); -var reject = require('lodash/collection/reject'); -var where = require('lodash/collection/where'); -var assign = require('lodash/object/assign'); -var defaults = require('lodash/object/defaults'); -var keys = require('lodash/object/keys'); -var EOL = require('os').EOL; -var CoreObject = require('core-object'); -var debug = require('debug')('ember-cli:command'); -var Watcher = require('../models/watcher'); -var SilentError = require('silent-error'); -var printableProperties = require('../utilities/get-printable-properties')(); - -var allowedWorkOptions = { +const nopt = require('nopt'); +const { default: chalk } = require('chalk'); +const path = require('path'); +const isGitRepo = require('is-git-url'); +const camelize = require('ember-cli-string-utils').camelize; +const getCallerFile = require('get-caller-file'); +const printCommand = require('@ember-tooling/blueprint-model/utilities/print-command'); +const union = require('lodash/union'); +const defaults = require('lodash/defaults'); +const uniq = require('lodash/uniq'); +const uniqBy = require('lodash/uniqBy'); +const reject = require('lodash/reject'); +const EOL = require('os').EOL; +const CoreObject = require('core-object'); +const logger = require('heimdalljs-logger')('ember-cli:command'); +const WatchDetector = require('watch-detector'); +const SilentError = require('silent-error'); +const execSync = require('child_process').execSync; +const fs = require('fs'); +const { determineInstallCommand } = require('../utilities/package-managers'); + +let cache = {}; + +let allowedWorkOptions = { insideProject: true, outsideProject: true, - everywhere: true + everywhere: true, }; path.name = 'Path'; // extend nopt to recognize 'gitUrl' as a type nopt.typeDefs.gitUrl = { type: 'gitUrl', - validate: function(data, k, val) { + validate(data, k, val) { if (isGitRepo(val)) { data[k] = val; return true; } else { return false; } - } + }, }; -module.exports = Command; - -function Command() { - CoreObject.apply(this, arguments); - - this.isWithinProject = this.project.isEmberCLIProject(); - this.name = this.name || path.basename(getCallerFile(), '.js'); - - debug('initialize: name: %s, name: %s', this.name); - this.aliases = this.aliases || []; - - // Works Property - if (!allowedWorkOptions[this.works]) { - throw new Error('The "' + this.name + '" command\'s works field has to ' + - 'be either "everywhere", "insideProject" or "outsideProject".'); - } +/** + * The base class for all CLI commands. + * + * @module ember-cli + * @class Command + * @constructor + * @extends CoreObject + */ +let Command = CoreObject.extend({ + /** + * The description of what this command does. + * + * @final + * @property description + * @type String + */ + description: null, + + /** + * Does this command work everywhere or just inside or outside of projects. + * + * Possible values: + * + * - `insideProject` + * - `outsideProject` + * - `everywhere` + * + * @final + * @property works + * @type String + * @default `insideProject` + */ + works: 'insideProject', + + _printableProperties: ['name', 'description', 'aliases', 'works', 'availableOptions', 'anonymousOptions'], + + init() { + this._super.apply(this, arguments); + + /** + * @final + * @property isWithinProject + * @type Boolean + */ + this.isWithinProject = this.project.isEmberCLIProject(); + + /** + * @final + * @property isViteProject + * @type Boolean + */ + this.isViteProject = this.isWithinProject && this.project.isViteProject?.(); + + /** + * The name of the command. + * + * @final + * @property name + * @type String + * @example `new` or `generate` + */ + this.name = this.name || path.basename(getCallerFile(), '.js'); + + logger.info('initialize: name: %s, name: %s', this.name); + + /** + * An array of aliases for the command + * + * @final + * @property aliases + * @type Array + * @example `['g']` for the `generate` command + */ + this.aliases = this.aliases || []; + + // Works Property + if (!allowedWorkOptions[this.works]) { + throw new Error( + `The "${this.name}" command's works field has to be either "everywhere", "insideProject" or "outsideProject".` + ); + } - // Options properties - this.availableOptions = this.availableOptions || []; - this.anonymousOptions = this.anonymousOptions || []; - this.registerOptions(); -} -/* - Registers options with command. This method provides the ability to extend or override command options. - Expects an object containing anonymousOptions or availableOptions, which it will then merge with - existing availableOptions before building the optionsAliases which are used to define shorthands. -*/ -Command.prototype.registerOptions = function(options) { - var extendedAvailableOptions = options && options.availableOptions || []; - var extendedAnonymousOptions = options && options.anonymousOptions || []; + /** + * An array of available options for the command + * + * @final + * @property availableOptions + * @type Array + * @example + * ```js + * availableOptions: [ + * { name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }, + * { name: 'verbose', type: Boolean, default: false, aliases: ['v'] }, + * { name: 'blueprint', type: String, default: 'app', aliases: ['b'] }, + * { name: 'skip-npm', type: Boolean, default: false, aliases: ['sn', 'skip-install', 'si'] }, + * { name: 'skip-git', type: Boolean, default: false, aliases: ['sg'] }, + * { name: 'directory', type: String , aliases: ['dir'] } + * ], + * ``` + */ + this.availableOptions = this.availableOptions || []; + + /** + * An array of anonymous options for the command + * + * @final + * @property anonymousOptions + * @type Array + * @example + * ```js + * anonymousOptions: [ + * '' + * ], + * ``` + */ + this.anonymousOptions = this.anonymousOptions || []; + }, + + /** + Registers options with command. This method provides the ability to extend or override command options. + Expects an object containing anonymousOptions or availableOptions, which it will then merge with + existing availableOptions before building the optionsAliases which are used to define shorthands. + + @method registerOptions + @param {Object} options + */ + registerOptions(options) { + let extendedAvailableOptions = (options && options.availableOptions) || []; + let extendedAnonymousOptions = (options && options.anonymousOptions) || []; + + this.anonymousOptions = union(this.anonymousOptions.slice(0), extendedAnonymousOptions); + + // merge any availableOptions + this.availableOptions = union(this.availableOptions.slice(0), extendedAvailableOptions); + + let optionKeys = uniq(this.availableOptions.map((option) => option.name)); + + optionKeys.map(this.mergeDuplicateOption.bind(this)); + + this.optionsAliases = this.optionsAliases || {}; + + this.availableOptions.map(this.validateOption.bind(this)); + }, + + /** + * Called when command is interrupted from outside, e.g. ctrl+C or process kill + * Can be used to cleanup artifacts produced by command and control process exit code + * + * @method onInterrupt + * @return {Promise|undefined} if rejected promise then result of promise will be used as an exit code + */ + onInterrupt() { + if (this._currentTask) { + return this._currentTask.onInterrupt(); + } else { + // interrupt all external commands which don't use `runTask()` with an error - this.anonymousOptions = union(this.anonymousOptions.slice(0), extendedAnonymousOptions); + // eslint-disable-next-line n/no-process-exit + process.exit(1); + } + }, + + _env(/* name */) { + return { + ui: this.ui, + project: this.project, + testing: this.testing, + settings: this.settings, + }; + }, + + /** + * Looks up for the task and runs + * It also keeps the reference for the current active task + * Keeping reference for the current task allows to cleanup task on interruption + * + * @private + * @method runTask + * @throws {Error} when no task found + * @throws {Error} on attempt to run concurrent task + * @param {string} name Task name from the tasks registry. Should be capitalized + * @param {object} options + * @return {Promise} Task run + */ + runTask(name, options) { + if (this._currentTask) { + throw new Error(`Concurrent tasks are not supported`); + } - // merge any availableOptions - this.availableOptions = union(this.availableOptions.slice(0), extendedAvailableOptions); + logger.info(`\`${this.name}\` command running \`${name}\` task`); - var optionKeys = uniq(pluck(this.availableOptions, 'name')); + let Task = this.tasks[name]; + if (!Task) { + throw new Error(`Unknown task "${name}"`); + } - optionKeys.map(this.mergeDuplicateOption.bind(this)); + let task = new Task(this._env(name)); - this.optionsAliases = this.optionsAliases || {}; + this._currentTask = task; - this.availableOptions.map(this.validateOption.bind(this)); -}; + return Promise.resolve() + .then(() => task.run(options)) + .catch((error) => { + logger.info(`An error occurred running \`${name}\` from the \`${this.name}\` command.`, error.stack); -Command.__proto__ = CoreObject; + throw error; + }) + .finally(() => { + delete this._currentTask; + }); + }, + + /** + Hook for extending a command before it is run in the cli.run command. + Most common use case would be to extend availableOptions. + @method beforeRun + @return {Promise|null} + */ + beforeRun() {}, + + /** + @method validateAndRun + @return {Promise} + */ + async validateAndRun(args) { + let commandOptions = this.parseArgs(args); + + // If the `help` option was passed, resolve with `callHelp` to call the `help` command: + if (commandOptions && (commandOptions.options.help || commandOptions.options.h)) { + logger.info(`${this.name} called with help option`); + + return 'callHelp'; + } -Command.prototype.description = null; -Command.prototype.works = 'insideProject'; -Command.prototype.constructor = Command; -/* - Hook for extending a command before it is run in the cli.run command. - Most common use case would be to extend availableOptions. - @method beforeRun - @return {Promise|null} -*/ -Command.prototype.beforeRun = function() { + if (commandOptions === null) { + throw new SilentError(); + } -}; + if (this.works === 'outsideProject' && this.isWithinProject) { + throw new SilentError(`You cannot use the ${chalk.green(this.name)} command inside an ember-cli project.`); + } -/* - @method validateAndRun - @return {Promise} -*/ -Command.prototype.validateAndRun = function(args) { - var commandOptions = this.parseArgs(args); - // if the help option was passed, resolve with 'callHelp' to call help command - if (commandOptions && (commandOptions.options.help || commandOptions.options.h)) { - debug(this.name + ' called with help option'); - return Promise.resolve('callHelp'); - } + if (this.works === 'insideProject') { + if (!this.project.hasDependencies()) { + if (!this.isWithinProject && !this.project.isEmberCLIAddon()) { + throw new SilentError( + `You have to be inside an ember-cli project to use the ${chalk.green(this.name)} command.` + ); + } - this.analytics.track({ - name: 'ember ', - message: this.name - }); + let installCommand = await determineInstallCommand(this.project.root); - if (commandOptions === null) { - return Promise.resolve(); - } + throw new SilentError( + `Required packages are missing, run \`${installCommand}\` from this directory to install them.` + ); + } + } - if (this.works === 'insideProject' && !this.isWithinProject) { - return Promise.reject(new SilentError( - 'You have to be inside an ember-cli project in order to use ' + - 'the ' + chalk.green(this.name) + ' command.' - )); - } + let detector = new WatchDetector({ + ui: this.ui, + childProcess: { execSync }, + fs, + watchmanSupportsPlatform: /^win/.test(process.platform), + cache, + root: this.project.root, + }); - if (this.works === 'outsideProject' && this.isWithinProject) { - return Promise.reject(new SilentError( - 'You cannot use the '+ chalk.green(this.name) + - ' command inside an ember-cli project.' - )); - } + let options = commandOptions.options; - if (this.works === 'insideProject') { - if (!this.project.hasDependencies()) { - throw new SilentError('node_modules appears empty, you may need to run `npm install`'); - } - } + if (this.hasOption('watcher') || process.env.EMBROIDER_PREBUILD) { + // Do stuff to try and provide a good experience when it comes to file watching: + let watchPreference = detector.findBestWatcherOption(options); - return Watcher.detectWatcher(this.ui, commandOptions.options).then(function(options) { - if (options._watchmanInfo) { - this.project._watchmanInfo = options._watchmanInfo; + this.project._watchmanInfo = watchPreference.watchmanInfo; + options.watcher = watchPreference.watcher; } return this.run(options, commandOptions.args); - }.bind(this)); -}; - -/* - Merges any options with duplicate keys in the availableOptions array. - Used primarily by registerOptions. - @method mergeDuplicateOption - @param {String} key - @return {Object} -*/ -Command.prototype.mergeDuplicateOption = function(key) { - var duplicateOptions, mergedOption, mergedAliases; - // get duplicates to merge - duplicateOptions = where(this.availableOptions, {'name': key}); - - if (duplicateOptions.length > 1) { - // TODO: warn on duplicates and overwriting - mergedAliases = []; - - pluck(duplicateOptions, 'aliases').map(function(alias) { - alias.map(function(a) { - mergedAliases.push(a); - }); - }); - - // merge duplicate options - mergedOption = assign.apply(null,duplicateOptions); - - // replace aliases with unique aliases - mergedOption.aliases = uniq(mergedAliases, function(alias) { - if(typeof alias === 'object') { - return alias[Object.keys(alias)[0]]; + }, + + /** + Reports if the given command has a command line option by a given name + + @method hasOption + @param {String} name + @return {Boolean} + */ + hasOption(name) { + for (let i = 0; i < this.availableOptions.length; i++) { + if (this.availableOptions[i].name === name) { + return true; } - return alias; - }); - - // remove duplicates from options - this.availableOptions = reject(this.availableOptions, {'name': key}); - this.availableOptions.push(mergedOption); - } - return this.availableOptions; -}; + } + return false; + }, + + /** + Merges any options with duplicate keys in the availableOptions array. + Used primarily by registerOptions. + @method mergeDuplicateOption + @param {String} key + @return {Object} + */ + mergeDuplicateOption(key) { + let duplicateOptions, mergedOption, mergedAliases; + // get duplicates to merge + duplicateOptions = this.availableOptions.filter((option) => option.name === key); + + if (duplicateOptions.length > 1) { + // TODO: warn on duplicates and overwriting + mergedAliases = duplicateOptions.flatMap((option) => option.aliases); + + // merge duplicate options + mergedOption = Object.assign.apply(null, duplicateOptions); + + // replace aliases with unique aliases + mergedOption.aliases = uniqBy(mergedAliases, (alias) => { + if (typeof alias === 'object') { + return alias[Object.keys(alias)[0]]; + } + return alias; + }); -/* - Normalizes option, filling in implicit values - @method normalizeOption - @param {Object} option - @return {Object} -*/ -Command.prototype.normalizeOption = function(option) { - option.key = camelize(option.name); - option.required = option.required || false; - return option; -}; + // remove duplicates from options + this.availableOptions = reject(this.availableOptions, { name: key }); + this.availableOptions.push(mergedOption); + } -/* - Assigns option - @method assignOption - @param {Object} option - @param {Object} parsedOptions - @param {Object} commandOptions - @return {Boolean} -*/ -Command.prototype.assignOption = function(option, parsedOptions, commandOptions) { - var isValid = isValidParsedOption(option, parsedOptions[option.name]); - if (isValid) { - if (parsedOptions[option.name] === undefined) { - if (option.default !== undefined) { - commandOptions[option.key] = option.default; - } + return this.availableOptions; + }, + + /** + Normalizes option, filling in implicit values + @method normalizeOption + @param {Object} option + @return {Object} + */ + normalizeOption(option) { + option.key = camelize(option.name); + option.required = option.required || false; + return option; + }, + + /** + Assigns option + @method assignOption + @param {Object} option + @param {Object} parsedOptions + @param {Object} commandOptions + @return {Boolean} + */ + assignOption(option, parsedOptions, commandOptions) { + let isValid = isValidParsedOption(option, parsedOptions[option.name]); + if (isValid) { + if (parsedOptions[option.name] === undefined) { + if (option.default !== undefined) { + commandOptions[option.key] = option.default; + } - if (this.settings[option.name] !== undefined) { - commandOptions[option.key] = this.settings[option.name]; - } else if (this.settings[option.key] !== undefined) { - commandOptions[option.key] = this.settings[option.key]; + if (this.settings[option.name] !== undefined) { + commandOptions[option.key] = this.settings[option.name]; + } else if (this.settings[option.key] !== undefined) { + commandOptions[option.key] = this.settings[option.key]; + } + } else { + commandOptions[option.key] = parsedOptions[option.name]; + delete parsedOptions[option.name]; } } else { - commandOptions[option.key] = parsedOptions[option.name]; - delete parsedOptions[option.name]; + this.ui.writeLine( + `The specified command ${chalk.green(this.name)} requires the option ${chalk.green(option.name)}.` + ); + } + return isValid; + }, + + /** + Validates option + @method validateOption + @param {Object} option + @return {Boolean} + */ + validateOption(option) { + let parsedAliases; + + if (!option.name || !option.type) { + throw new Error(`The command "${this.name}" has an option without the required type and name fields.`); } - } else { - this.ui.writeLine('The specified command ' + chalk.green(this.name) + - ' requires the option ' + chalk.green(option.name) + '.'); - } - return isValid; -}; - -/* - Validates option - @method validateOption - @param {Object} option - @return {Boolean} -*/ -Command.prototype.validateOption = function(option) { - var parsedAliases; - - if (!option.name || !option.type) { - throw new Error('The command "' + this.name + '" has an option ' + - 'without the required type and name fields.'); - } - - if (option.name !== option.name.toLowerCase()) { - throw new Error('The "' + option.name + '" option\'s name of the "' + - this.name + '" command contains a capital letter.'); - } - this.normalizeOption(option); + if (option.name !== option.name.toLowerCase()) { + throw new Error(`The "${option.name}" option's name of the "${this.name}" command contains a capital letter.`); + } - if (option.aliases) { - parsedAliases = option.aliases.map(this.parseAlias.bind(this, option)); - return parsedAliases.map(this.assignAlias.bind(this, option)).indexOf(false) === -1; - } - return false; -}; + this.normalizeOption(option); -/* - Parses alias for an option and adds it to optionsAliases - @method parseAlias - @param {Object} option - @param {Object|String} alias - @return {Object} -*/ -Command.prototype.parseAlias = function(option, alias) { - var aliasType = typeof alias; - var key, value, aliasValue; - - if (isValidAlias(alias, option.type)) { - if (aliasType === 'string') { - key = alias; - value = ['--' + option.name]; - } else if (aliasType === 'object') { - key = Object.keys(alias)[0]; - value = ['--' + option.name, alias[key]]; + if (option.aliases) { + parsedAliases = option.aliases.map(this.parseAlias.bind(this, option)); + return parsedAliases.map(this.assignAlias.bind(this, option)).indexOf(false) === -1; } - } else { - if (Array.isArray(alias)) { - aliasType = 'array'; - aliasValue = alias.join(','); - } else { - aliasValue = alias; - try { - aliasValue = JSON.parse(alias); + return false; + }, + + /** + Parses alias for an option and adds it to optionsAliases + @method parseAlias + @param {Object} option + @param {Object|String} alias + @return {Object} + */ + parseAlias(option, alias) { + let aliasType = typeof alias; + let key, value, aliasValue; + + if (isValidAlias(alias, option.type)) { + if (aliasType === 'string') { + key = alias; + value = [`--${option.name}`]; + } else if (aliasType === 'object') { + key = Object.keys(alias)[0]; + value = [`--${option.name}`, alias[key]]; } - catch(e) { - var debug = require('debug')('ember-cli/models/command'); - debug(e); + } else { + if (Array.isArray(alias)) { + aliasType = 'array'; + aliasValue = alias.join(','); + } else { + aliasValue = alias; + try { + aliasValue = JSON.parse(alias); + } catch (e) { + const logger = require('heimdalljs-logger')('ember-cli/models/command'); + logger.error(e); + } } + throw new Error( + `The "${aliasValue}" [type:${aliasType}] alias is not an acceptable value. ` + + `It must be a string or single key object with a string value (for example, "value" or { "key" : "value" }).` + ); } - throw new Error('The "' + aliasValue + '" [type:' + aliasType + - '] alias is not an acceptable value. It must be a string or single key' + - ' object with a string value (for example, "value" or { "key" : "value" }).'); - } - - return { - key: key, - value: value, - original: alias - }; - -}; -Command.prototype.assignAlias = function(option, alias) { - var isValid = this.validateAlias(option, alias); - if (isValid) { - this.optionsAliases[alias.key] = alias.value; - } - return isValid; -}; - -/* - Validates alias value - @method validateAlias - @params {Object} alias - @return {Boolean} -*/ -Command.prototype.validateAlias = function(option, alias) { - var key = alias.key; - var value = alias.value; - - if (!this.optionsAliases[key]) { - return true; - } else { - if (value[0] !== this.optionsAliases[key][0]) { - throw new SilentError('The "' + key + '" alias is already in use by the "' + this.optionsAliases[key][0] + - '" option and cannot be used by the "' + value[0] + '" option. Please use a different alias.'); + return { + key, + value, + original: alias, + }; + }, + + /** + * @method assignAlias + * @param option + * @param alias + * @return {Boolean} + */ + assignAlias(option, alias) { + let isValid = this.validateAlias(option, alias); + + if (isValid) { + this.optionsAliases[alias.key] = alias.value; + } + return isValid; + }, + + /** + Validates alias value + @method validateAlias + @param {Object} alias + @return {Boolean} + */ + validateAlias(option, alias) { + let key = alias.key; + let value = alias.value; + + if (!this.optionsAliases[key]) { + return true; } else { - if (value[1] !== this.optionsAliases[key][1]) { - this.ui.writeLine(chalk.yellow('The "' + key + '" alias cannot be overridden. Please use a different alias.')); + if (value[0] !== this.optionsAliases[key][0]) { + throw new SilentError( + `The "${key}" alias is already in use by the "${this.optionsAliases[key][0]}" option ` + + `and cannot be used by the "${value[0]}" option. Please use a different alias.` + ); + } else if (value[1] !== this.optionsAliases[key][1]) { + this.ui.writeLine(chalk.yellow(`The "${key}" alias cannot be overridden. Please use a different alias.`)); // delete offending alias from options - var index = this.availableOptions.indexOf(option); - var aliasIndex = this.availableOptions[index].aliases.indexOf(alias.original); + let index = this.availableOptions.indexOf(option); + let aliasIndex = this.availableOptions[index].aliases.indexOf(alias.original); if (this.availableOptions[index].aliases[aliasIndex]) { - delete this.availableOptions[index].aliases[aliasIndex]; + // first one wins + this.availableOptions[index].aliases = [this.availableOptions[index].aliases[0]]; } } + return false; } - return false; - } -}; - -/* - Parses command arguments and processes - @method parseArgs - @param {Object} commandArgs - @return {Object|null} -*/ -Command.prototype.parseArgs = function(commandArgs) { - var knownOpts = {}; // Parse options - var commandOptions = {}; - var parsedOptions; - - var assembleAndValidateOption = function(option) { - return this.assignOption(option, parsedOptions, commandOptions); - }; - - var validateParsed = function(key) { - // ignore 'argv', 'h', and 'help' - if (!commandOptions.hasOwnProperty(key) && key !== 'argv' && key !== 'h' && key !== 'help') { - this.ui.writeLine(chalk.yellow('The option \'--' + key + '\' is not registered with the ' + this.name + ' command. ' + - 'Run `ember ' + this.name + ' --help` for a list of supported options.')); - } - if (typeof parsedOptions[key] !== 'object') { - commandOptions[camelize(key)] = parsedOptions[key]; - } - }; - - this.availableOptions.forEach(function(option) { - knownOpts[option.name] = option.type; - }); - - parsedOptions = nopt(knownOpts, this.optionsAliases, commandArgs, 0); - - if (!this.availableOptions.every(assembleAndValidateOption.bind(this))) { - return null; - } - - keys(parsedOptions).map(validateParsed.bind(this)); - - return { - options: defaults(commandOptions, this.settings), - args: parsedOptions.argv.remain - }; -}; - -/* - -*/ -Command.prototype.run = function(commandArgs) { - throw new Error('command must implement run' + commandArgs.toString()); -}; - -/* - Prints basic help for the command. - - Basic help looks like this: - - ember generate - Generates new code from blueprints - aliases: g - --dry-run (Default: false) - --verbose (Default: false) - - The default implementation is designed to cover all bases - but may be overriden if necessary. - - @method printBasicHelp -*/ -Command.prototype.printBasicHelp = function() { - // ember command-name - var output; - if (this.isRoot) { - output = 'Usage: ' + this.name; - } else { - output = 'ember ' + this.name; - } - - // ... - if (this.anonymousOptions.length > 0) { - var anonymousOptions = this.anonymousOptions; - - if (anonymousOptions.join) { - anonymousOptions = anonymousOptions.join(' '); - } - - output += ' ' + chalk.yellow(anonymousOptions); - } - - // - if (this.availableOptions.length > 0) { - output += ' ' + chalk.cyan(''); - } - - output += EOL; - - // Description - if (this.description) { - output += ' ' + this.description + EOL; - } - - // aliases: a b c - if (this.aliases.length) { - output += ' ' + chalk.grey('aliases: ' + this.aliases.filter(function(a) { return a; }).join(', ')) + EOL; - } - - // --available-option (Required) (Default: value) - // ... - if (this.availableOptions.length > 0) { - this.availableOptions.forEach(function(option) { - output += ' ' + chalk.cyan('--' + option.name); - - if (option.type) { - output += ' ' + chalk.cyan('(' + (Array.isArray(option.type) ? option.type.map(function(a) { - return typeof a === 'string' ? a : a.name; - }).join(', ') : option.type.name) + ')'); + }, + + /** + Parses command arguments and processes + @method parseArgs + @param {Object} commandArgs + @return {Object|null} + */ + parseArgs(commandArgs) { + let knownOpts = {}; // Parse options + let commandOptions = {}; + let parsedOptions; + + this.registerOptions(); + + let assembleAndValidateOption = function (option) { + return this.assignOption(option, parsedOptions, commandOptions); + }; + + let validateParsed = function (key) { + // ignore 'argv', 'h', and 'help' + if (!(key in commandOptions) && key !== 'argv' && key !== 'h' && key !== 'help') { + this.ui.writeLine( + chalk.yellow( + `The option '--${key}' is not registered with the '${this.name}' command. ` + + `Run \`ember ${this.name} --help\` for a list of supported options.` + ) + ); } - - if (option.required) { - output += ' ' + chalk.cyan('(Required)'); + if (typeof parsedOptions[key] !== 'object') { + commandOptions[camelize(key)] = parsedOptions[key]; } + }; - if (option.default !== undefined) { - output += ' ' + chalk.cyan('(Default: ' + option.default + ')'); + this.availableOptions.forEach((option) => { + if (typeof option.type !== 'string') { + knownOpts[option.name] = option.type; + } else if (option.type === 'Path') { + knownOpts[option.name] = path; + } else { + knownOpts[option.name] = String; } + }); - if (option.description) { - output += ' ' + option.description; - } + parsedOptions = nopt(knownOpts, this.optionsAliases, commandArgs, 0); - if (option.aliases) { - output += EOL + ' ' + chalk.grey('aliases: ' + option.aliases.map(function(a) { - var key; - if (typeof a === 'string') { - return '-' + a + (option.type === Boolean ? '' : ' '); - } else { - key = Object.keys(a)[0]; - return '-' + key + ' (--' + option.name + '=' + a[key] + ')'; - } - }).join(', ')); - } + if (!this.availableOptions.every(assembleAndValidateOption.bind(this))) { + return null; + } - output += EOL; - }); - } + Object.keys(parsedOptions).map(validateParsed.bind(this)); + + return { + options: defaults(commandOptions, this.settings), + args: parsedOptions.argv.remain, + }; + }, + + /** + * @method run + * @param commandArgs + */ + run(commandArgs) { + throw new Error(`command must implement run${commandArgs.toString()}`); + }, + + _printCommand: printCommand, + + /** + Prints basic help for the command. + + Basic help looks like this: + + ember generate + Generates new code from blueprints + aliases: g + --dry-run (Default: false) + --verbose (Default: false) + + The default implementation is designed to cover all bases + but may be overridden if necessary. + + @method printBasicHelp + */ + printBasicHelp() { + // ember command-name + let output; + if (this.isRoot) { + output = `Usage: ${this.name}`; + } else { + output = `ember ${this.name}`; + } - this.ui.writeLine(output); -}; + output += this._printCommand(); + output += EOL; -/* - Prints detailed help for the command. + return output; + }, - The default implementation is no-op and should be overridden - for each command where further help text is required. + /** + Prints detailed help for the command. - @method printDetailedHelp -*/ -Command.prototype.printDetailedHelp = function() {}; + The default implementation is no-op and should be overridden + for each command where further help text is required. -Command.prototype.getJson = function(options) { - var json = {}; + @method printDetailedHelp + */ + printDetailedHelp() {}, - printableProperties.forEach(function(key) { - if (this[key] !== undefined) { - json[key] = this[key]; - } - }, this); + /** + * @method getJson + * @param {Object} options + * @return {Object} + */ + getJson(options) { + let json = {}; - if (this.addAdditionalJsonForHelp) { - this.addAdditionalJsonForHelp(json, options); - } + this.registerOptions(options); + this._printableProperties.forEach((key) => (json[key] = this[key])); - return json; -}; + if (this.addAdditionalJsonForHelp) { + this.addAdditionalJsonForHelp(json, options); + } + + return json; + }, +}); /* Validates options parsed by nopt @@ -543,12 +684,11 @@ function isValidParsedOption(option, parsedOption) { Validates alias. Must be a string or single key object */ function isValidAlias(alias, expectedType) { - var type = typeof alias; - var value, valueType; + let type = typeof alias; + let value, valueType; if (type === 'string') { return true; } else if (type === 'object') { - // no arrays, no multi-key objects if (!Array.isArray(alias) && Object.keys(alias).length === 1) { value = alias[Object.keys(alias)[0]]; @@ -557,13 +697,13 @@ function isValidAlias(alias, expectedType) { if (valueType === expectedType.name.toLowerCase()) { return true; } - } else { - if (expectedType.indexOf(value) > -1) { - return true; - } + } else if (expectedType.indexOf(value) > -1) { + return true; } } } return false; } + +module.exports = Command; diff --git a/lib/models/edit-file-diff.js b/lib/models/edit-file-diff.js deleted file mode 100644 index 2f82e1267d..0000000000 --- a/lib/models/edit-file-diff.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var Promise = require('../ext/promise'); -var readFile = Promise.denodeify(fs.readFile); -var writeFile = Promise.denodeify(fs.writeFile); -var childProcess = require('child_process'); -var jsdiff = require('diff'); -var quickTemp = require('quick-temp'); -var path = require('path'); -var SilentError = require('silent-error'); - -function EditFileDiff(options) { - this.info = options.info; - - quickTemp.makeOrRemake(this, 'tmpDifferenceDir'); -} - -EditFileDiff.prototype.edit = function(){ - return Promise.hash({ - input: this.info.render(), - output: readFile(this.info.outputPath) - }) - .then(invokeEditor.bind(this)) - .then(applyPatch.bind(this)) - .finally(cleanUp.bind(this)); -}; - -function cleanUp() { - quickTemp.remove(this, 'tmpDifferenceDir'); // jshint ignore:line -} - -function applyPatch(resultHash) { - /*jshint validthis:true */ - return Promise.hash({ - diffString: readFile(resultHash.diffPath), - currentString: readFile(resultHash.outputPath) - }).then(function(result){ - var appliedDiff = jsdiff.applyPatch(result.currentString.toString(), result.diffString.toString()); - - if(!appliedDiff) { - var message = 'Patch was not cleanly applied.'; - this.info.ui.writeLine(message + ' Please choose another action.'); - throw new SilentError(message); - } - - return writeFile(resultHash.outputPath, appliedDiff); - }.bind(this)); -} - -function invokeEditor(result) { - var info = this.info; // jshint ignore:line - var diff = jsdiff.createPatch(info.outputPath, result.output.toString(), result.input); - var diffPath = path.join(this.tmpDifferenceDir, 'currentDiff.diff'); // jshint ignore:line - - return new Promise(function(resolve, reject) { - writeFile(diffPath, diff).then(function() { - var editorArgs = process.env.EDITOR.split(' '); - var editor = editorArgs.shift(); - var editProcess = childProcess.spawn(editor, [diffPath].concat(editorArgs), {stdio: 'inherit'}); - var results = { outputPath: info.outputPath, diffPath: diffPath }; - - editProcess.on('close', function(code){ - if (code === 0) { - resolve(results); - } else { - reject(); - } - }); - }); - }); -} - -module.exports = EditFileDiff; diff --git a/lib/models/file-info.js b/lib/models/file-info.js deleted file mode 100644 index 069875b68a..0000000000 --- a/lib/models/file-info.js +++ /dev/null @@ -1,175 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var Promise = require('../ext/promise'); -var readFile = Promise.denodeify(fs.readFile); -var lstat = Promise.denodeify(fs.stat); -var chalk = require('chalk'); -var EditFileDiff = require('./edit-file-diff'); -var EOL = require('os').EOL; -var isBinaryFile = require('isbinaryfile'); - -function processTemplate(content, context) { - var options = { - evaluate: /<%([\s\S]+?)%>/g, - interpolate: /<%=([\s\S]+?)%>/g, - escape: /<%-([\s\S]+?)%>/g - }; - return require('lodash/string/template')(content, options)(context); -} - -function diffHighlight(line) { - if (line[0] === '+') { - return chalk.green(line); - } else if (line[0] === '-') { - return chalk.red(line); - } else if (line.match(/^@@/)) { - return chalk.cyan(line); - } else { - return line; - } -} - -FileInfo.prototype.confirmOverwrite = function(path) { - var promptOptions = { - type: 'expand', - name: 'answer', - default: false, - message: chalk.red('Overwrite') + ' ' + path + '?', - choices: [ - { key: 'y', name: 'Yes, overwrite', value: 'overwrite' }, - { key: 'n', name: 'No, skip', value: 'skip' }, - { key: 'd', name: 'Diff', value: 'diff' } - ] - }; - - if(canEdit()) { - promptOptions.choices.push({ key: 'e', name: 'Edit', value: 'edit' }); - } - - return this.ui.prompt(promptOptions) - .then(function(response) { - return response.answer; - }); -}; - -function canEdit() { - return process.env.EDITOR !== undefined; -} - -FileInfo.prototype.displayDiff = function() { - var info = this, - jsdiff = require('diff'); - return Promise.hash({ - input: this.render(), - output: readFile(info.outputPath) - }).then(function(result) { - var diff = jsdiff.createPatch( - info.outputPath, result.output.toString(), result.input - ); - var lines = diff.split('\n'); - - for (var i=0;i ({ + name: file, + size: contentsBuffer.length, + gzipSize: buffer.length, + showGzipped: contentsBuffer.length > 0, + })); +}; diff --git a/lib/models/hardware-info.js b/lib/models/hardware-info.js new file mode 100644 index 0000000000..83f04ac0d3 --- /dev/null +++ b/lib/models/hardware-info.js @@ -0,0 +1,388 @@ +'use strict'; + +const { execaSync } = require('execa'); +const fs = require('fs'); +const logger = require('heimdalljs-logger')('ember-cli:hardware-info'); +const os = require('os'); + +// For testing purposes: +const execa = { sync: execaSync }; + +function isUsingBatteryAcpi() { + try { + const { stdout } = execa.sync('acpi', ['--ac-adapter']); + const lines = stdout.split('\n').filter(Boolean); + + return lines.every((line) => /off-line/.test(line)); + } catch (ex) { + logger.warn(`Could not get battery status from acpi: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function isUsingBatteryApm() { + try { + const { stdout } = execa.sync('apm', ['-a']); + + return parseInt(stdout, 10) === 0; + } catch (ex) { + logger.warn(`Could not get battery status from apm: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function isUsingBatteryBsd() { + const apm = isUsingBatteryApm(); + + if (apm !== null) { + return apm; + } + + return isUsingBatteryUpower(); +} + +function isUsingBatteryDarwin() { + try { + const { stdout } = execa.sync('pmset', ['-g', 'batt']); + + return stdout.indexOf('Battery Power') !== -1; + } catch (ex) { + logger.warn(`Could not get battery status from pmset: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function isUsingBatteryLinux() { + const sysClassPowerSupply = isUsingBatterySysClassPowerSupply(); + + if (sysClassPowerSupply !== null) { + return sysClassPowerSupply; + } + + const acpi = isUsingBatteryAcpi(); + + if (acpi !== null) { + return acpi; + } + + return isUsingBatteryUpower(); +} + +function isUsingBatterySysClassPowerSupply() { + try { + const value = fs.readFileSync('/sys/class/power_supply/AC/online'); + + return parseInt(value, 10) === 0; + } catch (ex) { + logger.warn(`Could not get battery status from /sys/class/power_supply: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function isUsingBatteryUpower() { + try { + const { stdout } = execa.sync('upower', ['--enumerate']); + const devices = stdout.split('\n').filter(Boolean); + + return devices.some((device) => { + const { stdout } = execa.sync('upower', ['--show-info', device]); + + return /\bpower supply:\s+yes\b/.test(stdout) && /\bstate:\s+discharging\b/.test(stdout); + }); + } catch (ex) { + logger.warn(`Could not get battery status from upower: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function isUsingBatteryWindows() { + try { + const { stdout } = execa.sync('wmic', [ + '/namespace:', + '\\\\root\\WMI', + 'path', + 'BatteryStatus', + 'get', + 'PowerOnline', + '/format:list', + ]); + + return /\bPowerOnline=FALSE\b/.test(stdout); + } catch (ex) { + logger.warn(`Could not get battery status from wmic: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function memorySwapUsedDarwin() { + try { + const { stdout } = execa.sync('sysctl', ['vm.swapusage']); + const match = /\bused = (\d+\.\d+)M\b/.exec(stdout); + + if (!match) { + throw new Error('vm.swapusage not in output.'); + } + + // convert from fractional megabytes to bytes + return parseFloat(match[1]) * 1048576; + } catch (ex) { + logger.warn(`Could not get swap status from sysctl: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function memorySwapUsedBsd() { + try { + const { stdout } = execa.sync('pstat', ['-s']); + const devices = stdout.split('\n').filter(Boolean); + const header = devices.shift(); + const match = /^Device\s+(\d+)(K?)-blocks\s+Used\b/.exec(header); + + if (!match) { + throw new Error('Block size not found in output.'); + } + + const blockSize = parseInt(match[1], 10) * (match[2] === 'K' ? 1024 : 1); + + return devices.reduce((total, line) => { + const match = /^\S+\s+\d+\s+(\d+)/.exec(line); + + if (!match) { + throw new Error(`Unrecognized line in output: '${line}'`); + } + + return total + parseInt(match[1], 10) * blockSize; + }, 0); + } catch (ex) { + logger.warn(`Could not get swap status from pstat: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function memorySwapUsedLinux() { + try { + const { stdout } = execa.sync('free', ['-b']); + const lines = stdout.split('\n').filter(Boolean); + const header = lines.shift(); + const columns = header.split(/\s+/).filter(Boolean); + const columnUsed = columns.reduce((columnUsed, column, index) => { + if (columnUsed !== undefined) { + return columnUsed; + } + + if (/used/i.test(column)) { + // there is no heading on the first column, so indices are off by 1 + return index + 1; + } + }, undefined); + + if (columnUsed === undefined) { + throw new Error('Could not find "used" column.'); + } + + for (const line of lines) { + const columns = line.split(/\s+/).filter(Boolean); + + if (/swap/i.test(columns[0])) { + return parseInt(columns[columnUsed], 10); + } + } + + throw new Error('Could not find "swap" row.'); + } catch (ex) { + logger.warn(`Could not get swap status from free: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +function memorySwapUsedWindows() { + try { + const { stdout } = execa.sync('wmic', ['PAGEFILE', 'get', 'CurrentUsage', '/format:list']); + const match = /\bCurrentUsage=(\d+)/.exec(stdout); + + if (!match) { + throw new Error('Page file usage info not in output.'); + } + + return parseInt(match[1], 10) * 1048576; + } catch (ex) { + logger.warn(`Could not get swap status from wmic: ${ex}`); + logger.warn(ex.stack); + + return null; + } +} + +const hwinfo = { + /** + * Indicates whether the host is running on battery power. This can cause + * performance degredation. + * + * @private + * @method isUsingBattery + * @for HardwareInfo + * @param {String=process.platform} platform The current hardware platform. + * USED FOR TESTING ONLY. + * @return {null|Boolean} `true` iff the host is running on battery power or + * `false` if not. `null` if the battery status + * cannot be determined. + */ + isUsingBattery(platform = process.platform) { + switch (platform) { + case 'darwin': + return isUsingBatteryDarwin(); + + case 'freebsd': + case 'openbsd': + return isUsingBatteryBsd(); + + case 'linux': + return isUsingBatteryLinux(); + + case 'win32': + return isUsingBatteryWindows(); + } + + logger.warn(`Battery status is unsupported on the '${platform}' platform.`); + + return null; + }, + + /** + * Determines the amount of swap/virtual memory currently in use. + * + * @private + * @method memorySwapUsed + * @param {String=process.platform} platform The current hardware platform. + * USED FOR TESTING ONLY. + * @return {null|Number} The amount of used swap space, in bytes. `null` if + * the used swap space cannot be determined. + */ + memorySwapUsed(platform = process.platform) { + switch (platform) { + case 'darwin': + return memorySwapUsedDarwin(); + + case 'freebsd': + case 'openbsd': + return memorySwapUsedBsd(); + + case 'linux': + return memorySwapUsedLinux(); + + case 'win32': + return memorySwapUsedWindows(); + } + + logger.warn(`Swap status is unsupported on the '${platform}' platform.`); + + return null; + }, + + /** + * Determines the total amount of memory available to the host, as from + * `os.totalmem`. + * + * @private + * @method memoryTotal + * @return {Number} The total memory in bytes. + */ + memoryTotal() { + return os.totalmem(); + }, + + /** + * Determines the amount of memory currently being used by the current Node + * process, as from `process.memoryUsage`. + * + * @private + * @method memoryUsed + * @return {Object} The Resident Set Size, as reported by + * `process.memoryUsage`. + */ + memoryUsed() { + return process.memoryUsage().rss; + }, + + /** + * Determines the number of logical processors available to the host, as from + * `os.cpus`. + * + * @private + * @method processorCount + * @return {Number} The number of logical processors. + */ + processorCount() { + return os.cpus().length; + }, + + /** + * Determines the average processor load across the system. This is + * expressed as a fractional number between 0 and the number of logical + * processors. + * + * @private + * @method processorLoad + * @param {String=process.platform} platform The current hardware platform. + * USED FOR TESTING ONLY. + * @return {Array} The one-, five-, and fifteen-minute processor load + * averages. + */ + processorLoad(platform = process.platform) { + // The os.loadavg() call works on win32, but never returns correct + // data. Better to intercept and warn that it's unsupported. + if (platform === 'win32') { + logger.warn(`Processor load is unsupported on the '${platform}' platform.`); + + return null; + } + + return os.loadavg(); + }, + + /** + * Gets the speed of the host's processors. + * + * If more than one processor is found, the average of their speeds is taken. + * + * @private + * @method processorSpeed + * @return {Number} The average processor speed in MHz. + */ + processorSpeed() { + const cpus = os.cpus(); + + return cpus.reduce((sum, cpu) => sum + cpu.speed, 0) / cpus.length; + }, + + /** + * Determines the time since the host was started, as from `os.uptime`. + * + * @private + * @method uptime + * @return {Number} The number of seconds since the host was started. + */ + uptime() { + return os.uptime(); + }, +}; + +module.exports = { execa, hwinfo }; diff --git a/lib/models/host-info-cache.js b/lib/models/host-info-cache.js new file mode 100644 index 0000000000..2cd70456dd --- /dev/null +++ b/lib/models/host-info-cache.js @@ -0,0 +1,332 @@ +'use strict'; + +function allPkgInfosEqualAtIndex(paths, index) { + const itemToCheck = paths[0][index]; + return paths.every((pathToLazyEngine) => pathToLazyEngine[index] === itemToCheck); +} + +class HostInfoCache { + constructor(project) { + this.project = project; + this._bundledPackageInfoCache = new Map(); + this._hostAddonInfoCache = new Map(); + this._lcaHostCache = new Map(); + } + + /** + * Given a path (calculated as part of `getHostAddonInfo`), return the correct + * "bundle host". A bundle host is considered the project or lazy engine. + * + * For example, given the following package structure: + * + * --Project-- + * / \ + * / \ + * Lazy Engine A \ + * Addon A + * | + * | + * Lazy Engine B + * / \ + * / \ + * Lazy Engine A Lazy Engine C + * + * The provided paths for lazy engine A would look like: + * + * - [Project] + * - [Project, Addon A, Lazy Engine B] + * + * For this project structure, this function would return [Project, [Project]] + * + * Similarly, given the following project structure: + * + * --Project-- + * / \ + * / \ + * Lazy Engine A \ + * / Lazy Engine B + * / | + * / | + * Lazy Engine C Lazy Engine C + * + * The provided paths for lazy engine C would look like: + * + * - [Project, Lazy Engine A] + * - [Project, Lazy Engine B] + * + * In this case, the host is the project and would also return [Project, [Project]] + * + * @method _findNearestBundleHost + * @param {Array} paths The found paths to a given bundle host + * @return {[PackageInfo, PackageInfo[]]} + * @private + */ + _findNearestBundleHost(paths, pkgInfoForLazyEngine) { + // building an engine in isolation (it's considered the project, but it's + // also added as a dependency to the project by `ember-cli`) + if (this.project._packageInfo === pkgInfoForLazyEngine) { + return [this.project._packageInfo, [this.project._packageInfo]]; + } + + const shortestPath = paths.reduce( + (acc, pathToLazyEngine) => Math.min(acc, pathToLazyEngine.length), + Number.POSITIVE_INFINITY + ); + + const pathsEqualToShortest = paths.filter((pathToLazyEngine) => pathToLazyEngine.length === shortestPath); + const [firstPath] = pathsEqualToShortest; + + for (let i = firstPath.length - 1; i >= 0; i--) { + const pkgInfo = firstPath[i]; + + if (pkgInfo.isForBundleHost() && allPkgInfosEqualAtIndex(pathsEqualToShortest, i)) { + return [pkgInfo, firstPath.slice(0, i + 1)]; + } + } + + // this should _never_ be triggered + throw new Error( + `[ember-cli] Could not find a common host for: \`${pkgInfoForLazyEngine.name}\` (located at \`${pkgInfoForLazyEngine.realPath}\`)` + ); + } + + /** + * Returns a `Set` of package-info objects that a given bundle host is + * _directly_ responsible for bundling (i.e., it excludes other bundle + * hosts/lazy engines when it encounters these) + * + * @method _getBundledPackageInfos + * @param {PackageInfo} pkgInfoToStartAt + * @return {Set} + * @private + */ + _getBundledPackageInfos(pkgInfoToStartAt) { + let pkgInfos = this._bundledPackageInfoCache.get(pkgInfoToStartAt); + + if (pkgInfos) { + return pkgInfos; + } + + if (!pkgInfoToStartAt.isForBundleHost()) { + throw new Error( + `[ember-cli] \`${pkgInfoToStartAt.name}\` is not a bundle host; \`getBundledPackageInfos\` should only be used to find bundled package infos for a project or lazy engine` + ); + } + + pkgInfos = new Set(); + this._bundledPackageInfoCache.set(pkgInfoToStartAt, pkgInfos); + + let findAddons = (currentPkgInfo) => { + if (!currentPkgInfo.valid || !currentPkgInfo.addonMainPath) { + return; + } + + if (pkgInfos.has(currentPkgInfo)) { + return; + } + + if (currentPkgInfo.isForBundleHost()) { + return; + } + + pkgInfos.add(currentPkgInfo); + + let addonPackageList = currentPkgInfo.discoverAddonAddons(); + addonPackageList.forEach((pkgInfo) => findAddons(pkgInfo)); + }; + + let addonPackageList = pkgInfoToStartAt.project + ? pkgInfoToStartAt.discoverProjectAddons() + : pkgInfoToStartAt.discoverAddonAddons(); + + addonPackageList.forEach((pkgInfo) => findAddons(pkgInfo)); + + return pkgInfos; + } + + /** + * This function intends to return a common host for a bundle host (lazy engine). The root + * package info should be the starting point (i.e., the project's package info). We do this + * by performing a breadth-first traversal until we find the intended lazy engine (represented + * as a package-info & the 1st argument passed to this function). As part of the traversal, we keep + * track of all paths to said engine; then, once we find the intended engine we use this to determine + * the nearest common host amongst all shortest paths. + * + * Some context: + * + * For a given engine/bundle host, this finds the lowest common ancestor that is considered a + * host amongst _all_ engines by the same name in the project. + * + * For example, given the following package structure: + * + * --Project-- + * / \ + * / \ + * Lazy Engine A \ + * Addon A + * | + * | + * Lazy Engine B + * / \ + * / \ + * Lazy Engine A Lazy Engine C + * + * - The LCA host for Lazy Engine A is the project + * - The LCA host for Lazy Engine B is the project + * - The LCA host for Lazy Engine C is Lazy Engine B + * + * This also returns `hostAndAncestorBundledPackageInfos`, which are all bundled addons above a given host: + * + * - `hostAndAncestorBundledPackageInfos` for lazy engine A includes all non-lazy dependencies of its LCA host & above (in this case, just the project) + * - `hostAndAncestorBundledPackageInfos` for lazy engine B includes all non-lazy dependencies of its LCA host & above (in this case, just the project) + * - `hostAndAncestorBundledPackageInfos` for lazy engine C includes non-lazy deps of lazy engine B & non-lazy deps of the project (LCA host & above) + * + * This is intended to mimic the behavior of `ancestorHostAddons` in `ember-engines`: + * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/engine-addon.js#L333 + * + * Unfortunately, we can't easily repurpose the logic in `ember-engines` since the algorithm has to be different; + * in `ember-engines` we need access to the actual addon instance, however, this is intended to be used _during_ + * addon instantiation, so we only have access to package-info objects. In having said this, we _can_ repurpose + * the `hostPackageInfo` to determine the LCA host; see below `findLCAHost`. + * + * @method getHostAddonInfo + * @param {PackageInfo} packageInfoForLazyEngine + * @return {{ hostPackageInfo: PackageInfo, hostAndAncestorBundledPackageInfos: Set }} + */ + getHostAddonInfo(packageInfoForLazyEngine) { + const cacheKey = `${this.project._packageInfo.realPath}-${packageInfoForLazyEngine.realPath}`; + + let hostInfoCacheEntry = this._hostAddonInfoCache.get(cacheKey); + + if (hostInfoCacheEntry) { + return hostInfoCacheEntry; + } + + if (!packageInfoForLazyEngine.isForEngine()) { + throw new Error( + `[ember-cli] \`${packageInfoForLazyEngine.name}\` is not an engine; \`getHostAddonInfo\` should only be used to find host information about engines` + ); + } + + const queue = [{ pkgInfo: this.project._packageInfo, path: [] }]; + const visited = new Set(); + const foundPaths = []; + + while (queue.length) { + const { pkgInfo: currentPackageInfo, path } = queue.shift(); + + const { + addonMainPath, + inRepoAddons = [], + dependencyPackages = {}, + devDependencyPackages = {}, + } = currentPackageInfo; + + const isCurrentPackageInfoProject = this.project._packageInfo === currentPackageInfo; + + // don't process non-ember addons + if (!isCurrentPackageInfoProject && typeof addonMainPath !== 'string') { + continue; + } + + // store found paths + if (currentPackageInfo === packageInfoForLazyEngine) { + foundPaths.push([...path]); + } + + // don't process a given `PackageInfo` object more than once + if (!visited.has(currentPackageInfo)) { + visited.add(currentPackageInfo); + + // add current package info to current path + path.push(currentPackageInfo); + + queue.push( + ...[...inRepoAddons, ...Object.values(dependencyPackages), ...Object.values(devDependencyPackages)].map( + (pkgInfo) => ({ pkgInfo, path: [...path] }) + ) + ); + } + } + + const [hostPackageInfo, foundPath] = this._findNearestBundleHost(foundPaths, packageInfoForLazyEngine); + + const hostAndAncestorBundledPackageInfos = foundPath + .filter((pkgInfo) => pkgInfo.isForBundleHost()) + .reduce((acc, curr) => { + acc.push(...this._getBundledPackageInfos(curr)); + return acc; + }, []); + + hostInfoCacheEntry = { + hostPackageInfo, + hostAndAncestorBundledPackageInfos: new Set(hostAndAncestorBundledPackageInfos), + }; + + this._hostAddonInfoCache.set(cacheKey, hostInfoCacheEntry); + return hostInfoCacheEntry; + } + + /** + * This returns the LCA host for a given engine; we use the associated package info + * to compute this (see `getHostAddonInfo` above); this finds the lowest common ancestor + * that is considered a host amongst _all_ engines by the same name in the project. This + * function is intended to replace the original behavior in `ember-engines`. + * + * For more info, see the original implementation here: + * + * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/utils/find-lca-host.js + * + * @method findLCAHost + * @param {EngineAddon} engineInstance + * @return {EngineAddon|EmberApp} + */ + findLCAHost(engineInstance) { + // only compute once for a given engine + // we're using the engine name as the cache key here because regardless of its + // version, lazy engines will always get output to: `engines-dist/${engineName}` + let lcaHost = this._lcaHostCache.get(engineInstance.name); + + if (lcaHost) { + return lcaHost; + } + + if (!engineInstance._packageInfo.isForEngine()) { + throw new Error( + `[ember-cli] \`findLCAHost\` should only be used for engines; \`${engineInstance.name}\` is not an engine` + ); + } + + const { hostPackageInfo } = this.getHostAddonInfo(engineInstance._packageInfo); + + let curr = engineInstance; + + while (curr && curr.parent) { + if (curr.app) { + lcaHost = curr.app; + break; + } + + if (curr._packageInfo === hostPackageInfo) { + lcaHost = curr; + break; + } + + curr = curr.parent; + } + + if (lcaHost) { + this._lcaHostCache.set(engineInstance.name, lcaHost); + return lcaHost; + } + + // this should _never_ be triggered + throw new Error( + `[ember-cli] Could not find an LCA host for: \`${engineInstance.name}\` (located at \`${ + engineInstance.packageRoot || engineInstance.root + }\`)` + ); + } +} + +module.exports = HostInfoCache; diff --git a/lib/models/installation-checker.js b/lib/models/installation-checker.js deleted file mode 100644 index 3b42dd3d67..0000000000 --- a/lib/models/installation-checker.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -var debug = require('debug')('ember-cli:installation-checker'); -var fs = require('fs'); -var existsSync = require('exists-sync'); -var path = require('path'); -var SilentError = require('silent-error'); - -module.exports = InstallationChecker; - -function InstallationChecker(options) { - this.project = options.project; -} - -/** -* Check if npm and bower installation directories are present, -* and raise an error message with instructions on how to proceed. -* -* If some of these package managers aren't being used in the project -* we just ignore them. Their usage is considered by checking the -* presence of your manifest files: package.json for npm and bower.json for bower. -*/ -InstallationChecker.prototype.checkInstallations = function() { - var commands = []; - - if (this.usingNpm() && this.npmDependenciesNotPresent()) { - debug('npm dependencies not installed'); - commands.push('`npm install`'); - } - if (this.usingBower() && this.bowerDependenciesNotPresent()) { - debug('bower dependencies not installed'); - commands.push('`bower install`'); - } - if (commands.length) { - var commandText = commands.join(' and '); - throw new SilentError('No dependencies installed. Run ' + commandText + ' to install missing dependencies.'); - } -}; - -function hasDependencies(pkg) { - return (pkg.dependencies && pkg.dependencies.length) || - (pkg.devDependencies && pkg.devDependencies.length); -} - -function readJSON(path) { - try { - return JSON.parse(fs.readFileSync(path).toString()); - } catch(e) { - throw new SilentError('InstallationChecker: Unable to parse: ' + path); - } -} - -InstallationChecker.prototype.hasBowerDeps = function() { - return hasDependencies(readJSON(path.join(this.project.root, 'bower.json'))); -}; - -InstallationChecker.prototype.usingBower = function() { - return existsSync(path.join(this.project.root, 'bower.json')) && this.hasBowerDeps(); -}; - -InstallationChecker.prototype.bowerDependenciesNotPresent = function() { - return !existsSync(this.project.bowerDirectory); -}; - -InstallationChecker.prototype.hasNpmDeps = function() { - return hasDependencies(readJSON(path.join(this.project.root, 'package.json'))); -}; - -InstallationChecker.prototype.usingNpm = function() { - return existsSync(path.join(this.project.root, 'package.json')) && this.hasNpmDeps(); -}; - -InstallationChecker.prototype.npmDependenciesNotPresent = function() { - return !existsSync(this.project.nodeModulesPath); -}; diff --git a/lib/models/instantiate-addons.js b/lib/models/instantiate-addons.js new file mode 100644 index 0000000000..e7a820a4c7 --- /dev/null +++ b/lib/models/instantiate-addons.js @@ -0,0 +1,97 @@ +'use strict'; + +/** +@module ember-cli +*/ + +const DAGMap = require('dag-map').default; +const logger = require('heimdalljs-logger')('ember-cli:addons-factory'); +const heimdall = require('heimdalljs'); +const SilentError = require('silent-error'); + +/** + * Create instances of a set of "child" addons for a parent addon or project. + * + * @method instantiateAddons + * @param {Object} parent an Addon or Project that is the direct containing object of the list + * of children defined in addonPackages. + * @param {Project} project the project that contains the parent (so either the addon's project + * if parent is an addon, or the project itself if it is a project). It is possible when + * constructing custom addon instances that the project will actually be undefined--various + * addon tests do this, for example. + * @param {Object} a map of addon name (including scope) to an AddonInfo with the name, path and + * 'pkg' object for that addon's package.json). These are what is turned into addons. + */ +function instantiateAddons(parent, project, addonPackages) { + // depending on whether this is really a project or an addon, the 'name' property may be a getter. + let parentName = typeof parent.name === 'function' ? parent.name() : parent.name; + + logger.info('instantiateAddons for: ', parentName); + + let addonNames = Object.keys(addonPackages || {}); + + if (addonNames.length === 0) { + logger.info(' no addons'); + return []; + } else { + logger.info(' addon names are:', addonNames); + } + + let initializeAddonsToken = heimdall.start(`${parentName}: initializeAddons`); + let graph = new DAGMap(); + let addonInfo, emberAddonConfig; + + addonNames.forEach((name) => { + addonInfo = addonPackages[name]; + emberAddonConfig = addonInfo.pkg['ember-addon']; + + graph.add(name, addonInfo, emberAddonConfig.before, emberAddonConfig.after); + }); + + let addons = []; + let timings = new Map(); + + graph.each((key, value) => { + let addonInfo = value; + if (addonInfo) { + let initializeAddonToken = heimdall.start({ + name: `initialize ${addonInfo.name}`, + addonName: addonInfo.name, + addonInitializationNode: true, + }); + + let start = Date.now(); + + let pkgInfo = parent.packageInfoCache.getEntry(addonInfo.path); + + if (!pkgInfo || !pkgInfo.valid) { + throw new SilentError( + `The \`${addonInfo.pkg.name}\` addon could not be found at \`${addonInfo.path}\` or was otherwise invalid.` + ); + } + + // get an instance of the addon. If that fails it will throw. + let addon = pkgInfo.getAddonInstance(parent, project); + + timings.set(addon, Date.now() - start); + + initializeAddonToken.stop(); + + addons.push(addon); + } + }); + + logger.info( + ' addon info %o', + addons.map((addon) => ({ + name: addon.name, + initializeTotalMillis: timings.get(addon), + })) + ); + + initializeAddonsToken.stop(); + + return addons; +} + +module.exports = instantiateAddons; diff --git a/lib/models/instrumentation.js b/lib/models/instrumentation.js new file mode 100644 index 0000000000..b6bbbbdf2e --- /dev/null +++ b/lib/models/instrumentation.js @@ -0,0 +1,339 @@ +'use strict'; + +const fs = require('fs-extra'); +const { default: chalk } = require('chalk'); +const heimdallGraph = require('heimdalljs-graph'); +const getConfig = require('../utilities/get-config'); +const utilsInstrumentation = require('../utilities/instrumentation'); +const logger = require('heimdalljs-logger')('ember-cli:instrumentation'); +const { hwinfo } = require('./hardware-info'); + +let vizEnabled = utilsInstrumentation.vizEnabled; +let instrumentationEnabled = utilsInstrumentation.instrumentationEnabled; + +function _enableFSMonitorIfInstrumentationEnabled(config) { + let monitor; + if (instrumentationEnabled(config)) { + const FSMonitor = require('heimdalljs-fs-monitor'); + monitor = new FSMonitor(); + monitor.start(); + } + return monitor; +} + +_enableFSMonitorIfInstrumentationEnabled(); + +function _getHardwareInfo(platform) { + const startTime = process.hrtime(); + + Object.getOwnPropertyNames(hwinfo).forEach((metric) => (platform[metric] = hwinfo[metric]())); + + const collectionTime = process.hrtime(startTime); + + // Convert from integer [seconds, nanoseconds] to floating-point milliseconds. + platform.collectionTime = collectionTime[0] * 10e3 + collectionTime[1] / 10e6; +} + +class Instrumentation { + /** + An instance of this class is used for invoking the instrumentation + hooks on addons. + + The instrumentation types currently supported are: + + * init + * build + * command + * shutdown + + @class Instrumentation + @private + */ + constructor(options) { + this.isVizEnabled = vizEnabled; + this.isEnabled = instrumentationEnabled; + + this.ui = options.ui; + + // project constructor will set up bidirectional link + this.project = null; + + this.instrumentations = { + init: options.initInstrumentation, + build: { + token: null, + node: null, + count: 0, + }, + command: { + token: null, + node: null, + }, + shutdown: { + token: null, + node: null, + }, + }; + + this._heimdall = null; + + this.config = getConfig(); + + if (!options.initInstrumentation && this.isEnabled()) { + this.instrumentations.init = { + token: null, + node: null, + }; + this.start('init'); + } + } + + _buildSummary(tree, result, resultAnnotation) { + let buildSteps = 0; + let totalTime = 0; + + let node; + let statName; + let statValue; + let nodeItr; + let statsItr; + let nextNode; + let nextStat; + + for (nodeItr = tree.dfsIterator(); ; ) { + nextNode = nodeItr.next(); + if (nextNode.done) { + break; + } + + node = nextNode.value; + if (node.label.broccoliNode && !node.label.broccoliCachedNode) { + ++buildSteps; + } + + for (statsItr = node.statsIterator(); ; ) { + nextStat = statsItr.next(); + if (nextStat.done) { + break; + } + + statName = nextStat.value[0]; + statValue = nextStat.value[1]; + + if (statName === 'time.self') { + totalTime += statValue; + } + } + } + + let summary = { + build: { + type: resultAnnotation.type, + count: this.instrumentations.build.count, + outputChangedFiles: null, + }, + platform: { + name: process.platform, + }, + output: null, + totalTime, + buildSteps, + }; + + _getHardwareInfo(summary.platform); + + if (result) { + summary.build.outputChangedFiles = result.outputChanges; + summary.output = result.directory; + } + + if (resultAnnotation.type === 'rebuild') { + summary.build.primaryFile = resultAnnotation.primaryFile; + summary.build.changedFileCount = resultAnnotation.changedFiles.length; + summary.build.changedFiles = resultAnnotation.changedFiles.slice(0, 10); + } + + return summary; + } + + _initSummary(tree) { + const summary = { + totalTime: totalTime(tree), + platform: { + name: process.platform, + }, + }; + + _getHardwareInfo(summary.platform); + + return summary; + } + + _commandSummary(tree, commandName, commandArgs) { + const summary = { + name: commandName, + args: commandArgs, + totalTime: totalTime(tree), + platform: { + name: process.platform, + }, + }; + + _getHardwareInfo(summary.platform); + + return summary; + } + + _shutdownSummary(tree) { + const summary = { + totalTime: totalTime(tree), + platform: { + name: process.platform, + }, + }; + + _getHardwareInfo(summary.platform); + + return summary; + } + + _instrumentationFor(name) { + let instr = this.instrumentations[name]; + if (!instr) { + throw new Error(`No such instrumentation "${name}"`); + } + return instr; + } + + _instrumentationTreeFor(name) { + return heimdallGraph.loadFromNode(this.instrumentations[name].node); + } + + _invokeAddonHook(name, instrumentationInfo) { + if (this.project && this.project.addons.length) { + this.project.addons.forEach((addon) => { + if (typeof addon.instrumentation === 'function') { + addon.instrumentation(name, instrumentationInfo); + } + }); + } + } + + _writeInstrumentation(name, instrumentationInfo) { + if (!vizEnabled()) { + return; + } + + let filename = `instrumentation.${name}`; + if (name === 'build') { + filename += `.${this.instrumentations.build.count}`; + } + filename = `${filename}.json`; + fs.writeJsonSync(filename, { + summary: instrumentationInfo.summary, + // we want to change this to tree, to be consistent with the hook, but first + // we must update broccoli-viz + // see see https://github.com/ember-cli/broccoli-viz/issues/35 + nodes: instrumentationInfo.tree.toJSON().nodes, + }); + } + + start(name) { + if (!instrumentationEnabled(this.config)) { + return; + } + + let instr = this._instrumentationFor(name); + this._heimdall = this._heimdall || require('heimdalljs'); + + if (instr.node) { + // don't leak nodes during build. We have already reported on this in the + // previous stopAndReport so no data is lost + instr.node.remove(); + } + + let token = this._heimdall.start({ name, emberCLI: true }); + instr.token = token; + instr.node = this._heimdall.current; + } + + stopAndReport(name) { + if (!instrumentationEnabled(this.config)) { + return; + } + + let instr = this._instrumentationFor(name); + if (!instr.token) { + throw new Error(`Cannot stop instrumentation "${name}". It has not started.`); + } + try { + instr.token.stop(); + } catch (e) { + this.ui.writeLine(chalk.red(`Error reporting instrumentation '${name}'.`)); + logger.error(e.stack); + return; + } + + let instrSummaryName = `_${name}Summary`; + if (!this[instrSummaryName]) { + throw new Error(`No summary found for "${name}"`); + } + + let tree = this._instrumentationTreeFor(name); + let args = Array.prototype.slice.call(arguments, 1); + args.unshift(tree); + + let instrInfo = { + summary: this[instrSummaryName].apply(this, args), + tree, + }; + + this._invokeAddonHook(name, instrInfo); + this._writeInstrumentation(name, instrInfo); + + if (name === 'build') { + instr.count++; + } + } +} + +function totalTime(tree) { + let totalTime = 0; + let nodeItr; + let node; + let statName; + let statValue; + let statsItr; + let nextNode; + let nextStat; + + for (nodeItr = tree.dfsIterator(); ; ) { + nextNode = nodeItr.next(); + if (nextNode.done) { + break; + } + + node = nextNode.value; + + for (statsItr = node.statsIterator(); ; ) { + nextStat = statsItr.next(); + if (nextStat.done) { + break; + } + + statName = nextStat.value[0]; + statValue = nextStat.value[1]; + + if (statName === 'time.self') { + totalTime += statValue; + } + } + } + + return totalTime; +} + +// exported for testing +Instrumentation._enableFSMonitorIfInstrumentationEnabled = _enableFSMonitorIfInstrumentationEnabled; + +module.exports = Instrumentation; diff --git a/lib/models/package-info-cache/error-entry.js b/lib/models/package-info-cache/error-entry.js new file mode 100644 index 0000000000..2582c37bfb --- /dev/null +++ b/lib/models/package-info-cache/error-entry.js @@ -0,0 +1,17 @@ +'use strict'; + +/* + * Small utility class to contain data about a single error found + * during loading of a package into the PackageInfoCache. + * + * @protected + * @class ErrorEntry + */ +class ErrorEntry { + constructor(type, data) { + this.type = type; + this.data = data; + } +} + +module.exports = ErrorEntry; diff --git a/lib/models/package-info-cache/error-list.js b/lib/models/package-info-cache/error-list.js new file mode 100644 index 0000000000..bda707e1e5 --- /dev/null +++ b/lib/models/package-info-cache/error-list.js @@ -0,0 +1,38 @@ +'use strict'; + +const ErrorEntry = require('./error-entry'); + +/* + * Small utility class to store a list of errors during loading of + * a package into the PackageInfoCache. + * + * @protected + * @class ErrorList + */ +module.exports = class ErrorList { + constructor() { + this.errors = []; + } + + /* + * Add an error. The errorData object is optional, and can be anything. + * We do this so we don't really need to create a series of error + * classes. + * + * @public + * @param {String} errorType one of the Errors.ERROR_* constants. + * @param {Object} errorData any error data relevant to the type of error + * being created. See showErrors(). + */ + addError(errorType, errorData) { + this.errors.push(new ErrorEntry(errorType, errorData)); + } + + getErrors() { + return this.errors; + } + + hasErrors() { + return this.errors.length > 0; + } +}; diff --git a/lib/models/package-info-cache/errors.js b/lib/models/package-info-cache/errors.js new file mode 100644 index 0000000000..4a606b07da --- /dev/null +++ b/lib/models/package-info-cache/errors.js @@ -0,0 +1,12 @@ +'use strict'; + +const Errors = { + ERROR_PACKAGE_DIR_MISSING: 'packageDirectoryMissing', + ERROR_PACKAGE_JSON_MISSING: 'packageJsonMissing', + ERROR_PACKAGE_JSON_PARSE: 'packageJsonParse', + ERROR_EMBER_ADDON_MAIN_MISSING: 'emberAddonMainMissing', + ERROR_DEPENDENCIES_MISSING: 'dependenciesMissing', + ERROR_NODEMODULES_ENTRY_MISSING: 'modulesEntryMissing', +}; + +module.exports = Errors; diff --git a/lib/models/package-info-cache/index.js b/lib/models/package-info-cache/index.js new file mode 100644 index 0000000000..295c45bbe9 --- /dev/null +++ b/lib/models/package-info-cache/index.js @@ -0,0 +1,699 @@ +'use strict'; + +/* + * Performance cache for information about packages (projects/addons/"apps"/modules) + * under an initial root directory and resolving addon/dependency links to other packages. + */ + +const fs = require('fs-extra'); +const path = require('path'); +const ErrorList = require('./error-list'); +const Errors = require('./errors'); +const PackageInfo = require('./package-info'); +const NodeModulesList = require('./node-modules-list'); +const logger = require('heimdalljs-logger')('ember-cli:package-info-cache'); +const resolvePackagePath = require('resolve-package-path'); + +const getRealFilePath = resolvePackagePath.getRealFilePath; +const getRealDirectoryPath = resolvePackagePath.getRealDirectoryPath; +const _resetCache = resolvePackagePath._resetCache; + +const PACKAGE_JSON = 'package.json'; + +/** + * Class that stores entries that are either PackageInfo or NodeModulesList objects. + * The entries are stored in a map keyed by real directory path. + * + * @public + * @class PackageInfoCache + */ +class PackageInfoCache { + constructor(ui) { + this.ui = ui; // a console-ui instance + this._clear(); + } + + /** + * Clear the cache information. + * + * @private + * @method _clear + */ + _clear() { + this.entries = Object.create(null); + this.projects = []; + _resetCache(); + } + + /** + * Indicates if there is at least one error in any object in the cache. + * + * @public + * @method hasErrors + * @return true if there are any errors in the cache, for any entries, else false. + */ + hasErrors() { + let paths = Object.keys(this.entries); + + if (paths.find((entryPath) => this.getEntry(entryPath).hasErrors())) { + return true; + } + + return false; + } + + /** + * Gather all the errors in the PIC and any cached objects, then dump them + * out to the ui-console. + * + * @public + * @method showErrors + */ + showErrors() { + let paths = Object.keys(this.entries).sort(); + + paths.forEach((entryPath) => { + this._showObjErrors(this.getEntry(entryPath)); + }); + } + + /** + * Dump all the errors for a single object in the cache out to the ui-console. + * + * Special case: because package-info-cache also creates PackageInfo objects for entries + * that do not actually exist (to allow simplifying the code), if there's a case where + * an object has only the single error ERROR_PACKAGE_DIR_MISSING, do not print + * anything. The package will have been found as a reference from some other + * addon or the root project, and we'll print a reference error there. Having + * both is just confusing to users. + * + * @private + * @method _showObjErrors + */ + _showObjErrors(obj) { + let errorEntries = obj.hasErrors() ? obj.errors.getErrors() : null; + + if (!errorEntries || (errorEntries.length === 1 && errorEntries[0].type === Errors.ERROR_PACKAGE_DIR_MISSING)) { + return; + } + + logger.info(''); + let rootPath; + + if (obj instanceof PackageInfoCache) { + logger.info('Top level errors:'); + rootPath = this.realPath || ''; + } else { + let typeName = obj.project ? 'project' : 'addon'; + + logger.info(`The 'package.json' file for the ${typeName} at ${obj.realPath}`); + rootPath = obj.realPath; + } + + errorEntries.forEach((errorEntry) => { + switch (errorEntry.type) { + case Errors.ERROR_PACKAGE_JSON_MISSING: + logger.info(` does not exist`); + break; + case Errors.ERROR_PACKAGE_JSON_PARSE: + logger.info(` could not be parsed`); + break; + case Errors.ERROR_EMBER_ADDON_MAIN_MISSING: + logger.info( + ` specifies a missing ember-addon 'main' file at relative path '${path.relative( + rootPath, + errorEntry.data + )}'` + ); + break; + case Errors.ERROR_DEPENDENCIES_MISSING: + if (errorEntry.data.length === 1) { + logger.info(` specifies a missing dependency '${errorEntry.data[0]}'`); + } else { + logger.info(` specifies some missing dependencies:`); + errorEntry.data.forEach((dependencyName) => { + logger.info(` ${dependencyName}`); + }); + } + break; + case Errors.ERROR_NODEMODULES_ENTRY_MISSING: + logger.info(` specifies a missing 'node_modules/${errorEntry.data}' directory`); + break; + } + }); + } + + /** + * Process the root directory of a project, given a + * Project object (we need the object in order to find the internal addons). + * _readPackage takes care of the general processing of the root directory + * and common locations for addons, filling the cache with each. Once it + * returns, we take care of the locations for addons that are specific to + * projects, not other packages (e.g. internal addons, cli root). + * + * Once all the project processing is done, go back through all cache entries + * to create references between the packageInfo objects. + * + * @public + * @method loadProject + * @param projectInstance the instance of the Project object to load package data + * about into the cache. + * @return {PackageInfo} the PackageInfo object for the given Project object. + * Note that if the project path is already in the cache, that will be returned. + * No copy is made. + */ + loadProject(projectInstance) { + logger.info('Loading project at %o...', projectInstance.root); + + let pkgInfo = this._readPackage(projectInstance.root, projectInstance.pkg, true); + + // NOTE: the returned val may contain errors, or may contain + // other packages that have errors. We will try to process + // things anyway. + if (!pkgInfo.processed) { + this.projects.push(projectInstance); + + // projects are a bit different than standard addons, in that they have + // possibly a CLI addon and internal addons. Add those now. + pkgInfo.project = projectInstance; + + if (projectInstance.cli && projectInstance.cli.root) { + logger.info('Reading package for "ember-cli": %o', projectInstance.cli.root); + pkgInfo.cliInfo = this._readPackage(projectInstance.cli.root); + } + + // add any internal addons in the project. Since internal addons are + // optional (and only used some of the time anyway), we don't want to + // create a PackageInfo unless there is really a directory at the + // suggested location. The created addon may internally have errors, + // as with any other PackageInfo. + projectInstance.supportedInternalAddonPaths().forEach((internalAddonPath) => { + if (getRealDirectoryPath(internalAddonPath)) { + logger.info('Reading package for internal addon: %o', internalAddonPath); + pkgInfo.addInternalAddon(this._readPackage(internalAddonPath)); + } + }); + + this._resolveDependencies(); + } + + return pkgInfo; + } + + /** + * To support the project.reloadPkg method, we need the ability to flush + * the cache and reload from the updated package.json. + * There are some issues with doing this: + * - Because of the possible relationship between projects and their addons + * due to symlinks, it's not trivial to flush only the data related to a + * given project. + * - If an 'ember-build-cli.js' dynamically adds new projects to the cache, + * we will not necessarily get called again to redo the loading of those + * projects. + * The solution, implemented here: + * - Keep track of the Project objects whose packages are loaded into the cache. + * - If a project is reloaded, flush the cache, then do loadPackage again + * for all the known Projects. + * + * @public + * @method reloadProjects + * @return null + */ + reloadProjects() { + let projects = this.projects.slice(); + this._clear(); + projects.forEach((project) => this.loadProject(project)); + } + + /** + * Do the actual processing of the root directory of an addon, when the addon + * object already exists (i.e. the addon is acting as the root object of a + * tree, like project does). We need the object in order to find the internal addons. + * _readPackage takes care of the general processing of the root directory + * and common locations for addons, filling the cache with each. + * + * Once all the addon processing is done, go back through all cache entries + * to create references between the packageInfo objects. + * + * @public + * @method loadAddon + * @param addonInstance the instance of the Addon object to load package data + * about into the cache. + * @return {PackageInfo} the PackageInfo object for the given Addon object. + * Note that if the addon path is already in the cache, that will be returned. + * No copy is made. + */ + loadAddon(addonInstance) { + // to maintain backwards compatibility for consumers who create a new instance + // of the base addon model class directly and don't set `packageRoot` + let pkgInfo = this._readPackage(addonInstance.packageRoot || addonInstance.root, addonInstance.pkg); + + // NOTE: the returned pkgInfo may contain errors, or may contain + // other packages that have errors. We will try to process + // things anyway. + if (!pkgInfo.processed) { + pkgInfo.addon = addonInstance; + this._resolveDependencies(); + } + + return pkgInfo; + } + + /** + * Resolve the node_module dependencies across all packages after they have + * been loaded into the cache, because we don't know when a particular package + * will enter the cache. + * + * Since loadProject can be called multiple times for different projects, + * we don't want to reprocess any packages that happen to be common + * between them. We'll handle this by marking any packageInfo once it + * has been processed here, then ignore it in any later processing. + * + * @private + * @method _resolveDependencies + */ + _resolveDependencies() { + logger.info('Resolving dependencies...'); + + let packageInfos = this._getPackageInfos(); + packageInfos.forEach((pkgInfo) => { + if (!pkgInfo.processed) { + let pkgs = pkgInfo.addDependencies(pkgInfo.pkg.dependencies); + if (pkgs) { + pkgInfo.dependencyPackages = pkgs; + } + + // for Projects only, we also add the devDependencies + if (pkgInfo.project) { + pkgs = pkgInfo.addDependencies(pkgInfo.pkg.devDependencies); + if (pkgs) { + pkgInfo.devDependencyPackages = pkgs; + } + } + + pkgInfo.processed = true; + } + }); + } + + /** + * Add an entry to the cache. + * + * @private + * @method _addEntry + */ + _addEntry(path, entry) { + this.entries[path] = entry; + } + + /** + * Retrieve an entry from the cache. + * + * @public + * @method getEntry + * @param {String} path the real path whose PackageInfo or NodeModulesList is desired. + * @return {PackageInfo} or {NodeModulesList} the desired entry. + */ + getEntry(path) { + return this.entries[path]; + } + + /** + * Indicate if an entry for a given path exists in the cache. + * + * @public + * @method contains + * @param {String} path the real path to check for in the cache. + * @return true if the entry is present for the given path, false otherwise. + */ + contains(path) { + return this.entries[path] !== undefined; + } + + _getPackageInfos() { + let result = []; + + Object.keys(this.entries).forEach((path) => { + let entry = this.entries[path]; + if (entry instanceof PackageInfo) { + result.push(entry); + } + }); + + return result; + } + + /* + * Find a PackageInfo cache entry with the given path. If there is + * no entry in the startPath, do as done in resolve.sync() - travel up + * the directory hierarchy, attaching 'node_modules' to each directory and + * seeing if the directory exists and has the relevant entry. + * + * We'll do things a little differently, though, for speed. + * + * If there is no cache entry, we'll try to use _readNodeModulesList to create + * a new cache entry and its contents. If the directory does not exist, + * We'll create a NodeModulesList cache entry anyway, just so we don't have + * to check with the file system more than once for that directory (we + * waste a bit of space, but gain speed by not hitting the file system + * again for that path). + * Once we have a NodeModulesList, check for the package name, and continue + * up the path until we hit the root or the PackageInfo is found. + * + * @private + * @method _findPackage + * @param {String} packageName the name/path of the package to search for + * @param {String} the path of the directory to start searching from + */ + _findPackage(packageName, startPath) { + let parsedPath = path.parse(startPath); + let root = parsedPath.root; + + let currPath = startPath; + + while (currPath !== root) { + let endsWithNodeModules = path.basename(currPath) === 'node_modules'; + + let nodeModulesPath = endsWithNodeModules ? currPath : `${currPath}${path.sep}node_modules`; + + let nodeModulesList = this._readNodeModulesList(nodeModulesPath); + + // _readNodeModulesList only returns a NodeModulesList or null + if (nodeModulesList) { + let pkg = nodeModulesList.findPackage(packageName); + if (pkg) { + return pkg; + } + } + + currPath = path.dirname(currPath); + } + + return null; + } + + /** + * Given a directory that supposedly contains a package, create a PackageInfo + * object and try to fill it out, EVEN IF the package.json is not readable. + * Errors will then be stored in the PackageInfo for anything with the package + * that might be wrong. + * Because it's possible that the path given to the packageDir is not actually valid, + * we'll just use the path.resolve() version of that path to search for the + * path in the cache, before trying to get the 'real' path (which also then + * resolves links). The cache itself is keyed on either the realPath, if the + * packageDir is actually a real valid directory path, or the normalized path (before + * path.resolve()), if it is not. + * + * NOTE: the cache is also used to store the NULL_PROJECT project object, + * which actually has no package.json or other files, but does have an empty + * package object. Because of that, and to speed up processing, loadProject() + * will pass in both the package root directory path and the project's package + * object, if there is one. If the package object is present, we will use that + * in preference to trying to find a package.json file. + * + * If there is no package object, and there is no package.json or the package.json + * is bad or the package is an addon with + * no main, the only thing we can do is return an ErrorEntry to the caller. + * Once past all those problems, if any error occurs with any of the contents + * of the package, they'll be cached in the PackageInfo itself. + * + * In summary, only PackageInfo or ErrorEntry will be returned. + * + * @private + * @method _readPackage + * @param {String} pkgDir the path of the directory to read the package.json from and + * process the contents and create a new cache entry or entries. + * @param {Boolean} isRoot, for when this is to be considered the root + * package, whose dependencies we must all consider for discovery. + */ + _readPackage(packageDir, pkg, isRoot) { + let normalizedPackageDir = path.normalize(packageDir); + + // Most of the time, normalizedPackageDir is already a real path (i.e. fs.realpathSync + // will return the same value as normalizedPackageDir if the dir actually exists). + // Because of that, we'll assume we can test for normalizedPackageDir first and return + // if we find it. + let pkgInfo = this.getEntry(normalizedPackageDir); + if (pkgInfo) { + return pkgInfo; + } + + // collect errors we hit while trying to create the PackageInfo object. + // We'll load these into the object once it's created. + let setupErrors = new ErrorList(); + + // We don't already have an entry (bad or otherwise) at normalizedPackageDir. See if + // we can actually find a real path (including resolving links if needed). + let pathFailed = false; + + let realPath = getRealDirectoryPath(normalizedPackageDir); + + if (realPath) { + if (realPath !== normalizedPackageDir) { + // getRealDirectoryPath actually changed something in the path (e.g. + // by resolving a symlink), so see if we have this entry. + pkgInfo = this.getEntry(realPath); + if (pkgInfo) { + return pkgInfo; + } + } else { + // getRealDirectoryPath is same as normalizedPackageDir, and we know already we + // don't have an entry there, so we need to create one. + } + } else { + // no realPath, so either nothing is at the path or it's not a directory. + // We need to use normalizedPackageDir as the real path. + pathFailed = true; + setupErrors.addError(Errors.ERROR_PACKAGE_DIR_MISSING, normalizedPackageDir); + realPath = normalizedPackageDir; + } + + // at this point we have realPath set, we don't already have a PackageInfo + // for the path, and the path may or may not actually correspond to a + // valid directory (pathFailed tells us which). If we don't have a pkg + // object already, we need to be able to read one, unless we also don't + // have a path. + if (!pkg) { + if (!pathFailed) { + // we have a valid realPath + let packageJsonPath = path.join(realPath, PACKAGE_JSON); + let pkgfile = getRealFilePath(packageJsonPath); + if (pkgfile) { + try { + pkg = fs.readJsonSync(pkgfile); + } catch (e) { + setupErrors.addError(Errors.ERROR_PACKAGE_JSON_PARSE, pkgfile); + } + } else { + setupErrors.addError(Errors.ERROR_PACKAGE_JSON_MISSING, packageJsonPath); + } + } + + // Some error has occurred resulting in no pkg object, so just + // create an empty one so we have something to use below. + if (!pkg) { + pkg = Object.create(null); + } + } + + // For storage, force the pkg.root to the calculated path. This will + // save us from issues where we have a package for a non-existing + // path and other stuff. + pkg.root = realPath; + + // Create a new PackageInfo and load any errors as needed. + // Note that pkg may be an empty object here. + logger.info('Creating new PackageInfo instance for %o at %o', pkg.name, realPath); + pkgInfo = new PackageInfo(pkg, realPath, this, isRoot); + + if (setupErrors.hasErrors()) { + pkgInfo.errors = setupErrors; + pkgInfo.valid = false; + } + + // If we have an ember-addon, check that the main exists and points + // to a valid file. + if (pkgInfo.isForAddon()) { + logger.info('%s is an addon', pkg.name); + + // Note: when we have both 'main' and ember-addon:main, the latter takes precedence + let main = (pkg['ember-addon'] && pkg['ember-addon'].main) || pkg['main']; + + if (!main || main === '.' || main === './') { + main = 'index.js'; + } else if (!path.extname(main)) { + main = `${main}.js`; + } + + logger.info('Addon entry point is %o', main); + pkg.main = main; + + let mainPath = path.join(realPath, main); + let mainRealPath = getRealFilePath(mainPath); + + if (mainRealPath) { + pkgInfo.addonMainPath = mainRealPath; + } else { + pkgInfo.addError(Errors.ERROR_EMBER_ADDON_MAIN_MISSING, mainPath); + this.valid = false; + } + } + + // The packageInfo itself is now "complete", though we have not + // yet dealt with any of its "child" packages. Add it to the + // cache + this._addEntry(realPath, pkgInfo); + + let emberAddonInfo = pkg['ember-addon']; + + // Set up packageInfos for any in-repo addons + if (emberAddonInfo) { + let paths = emberAddonInfo.paths; + + if (paths) { + paths.forEach((p) => { + let addonPath = path.join(realPath, p); // real path, though may not exist. + logger.info('Adding in-repo-addon at %o', addonPath); + let addonPkgInfo = this._readPackage(addonPath); // may have errors in the addon package. + pkgInfo.addInRepoAddon(addonPkgInfo); + }); + } + } + + if (pkgInfo.mayHaveAddons) { + logger.info('Reading "node_modules" for %o', realPath); + + // read addon modules from node_modules. We read the whole directory + // because it's assumed that npm/yarn may have placed addons in the + // directory from lower down in the project tree, and we want to get + // the data into the cache ASAP. It may not necessarily be a 'real' error + // if we find an issue, if nobody below is actually invoking the addon. + let nodeModules = this._readNodeModulesList(path.join(realPath, 'node_modules')); + + if (nodeModules) { + pkgInfo.nodeModules = nodeModules; + } + } else { + // will not have addons, so even if there are node_modules here, we can + // simply pretend there are none. + pkgInfo.nodeModules = NodeModulesList.NULL; + } + + return pkgInfo; + } + + /** + * Process a directory of modules in a given package directory. + * + * We will allow cache entries for node_modules that actually + * have no contents, just so we don't have to hit the file system more + * often than necessary--it's much quicker to check an in-memory object. + * object. + * + * Note: only a NodeModulesList or null is returned. + * + * @private + * @method _readModulesList + * @param {String} nodeModulesDir the path of the node_modules directory + * to read the package.json from and process the contents and create a + * new cache entry or entries. + */ + _readNodeModulesList(nodeModulesDir) { + let normalizedNodeModulesDir = path.normalize(nodeModulesDir); + + // Much of the time, normalizedNodeModulesDir is already a real path (i.e. + // fs.realpathSync will return the same value as normalizedNodeModulesDir, if + // the directory actually exists). Because of that, we'll assume + // we can test for normalizedNodeModulesDir first and return if we find it. + let nodeModulesList = this.getEntry(normalizedNodeModulesDir); + if (nodeModulesList) { + return nodeModulesList; + } + + // NOTE: because we call this when searching for objects in node_modules + // directories that may not exist, we'll just return null here if the + // directory is not real. If it actually is an error in some case, + // the caller can create the error there. + let realPath = getRealDirectoryPath(normalizedNodeModulesDir); + + if (!realPath) { + return null; + } + + // realPath may be different than the original normalizedNodeModulesDir, so + // we need to check the cache again. + if (realPath !== normalizedNodeModulesDir) { + nodeModulesList = this.getEntry(realPath); + if (nodeModulesList) { + return nodeModulesList; + } + } else { + // getRealDirectoryPath is same as normalizedPackageDir, and we know already we + // don't have an entry there, so we need to create one. + } + + // At this point we know the directory node_modules exists and we can + // process it. Further errors will be recorded here, or in the objects + // that correspond to the node_modules entries. + logger.info('Creating new NodeModulesList instance for %o', realPath); + nodeModulesList = new NodeModulesList(realPath, this); + + const entries = fs.readdirSync(realPath).filter((fileName) => { + if (fileName.startsWith('.') || fileName.startsWith('_')) { + // we explicitly want to ignore these, according to the + // definition of a valid package name. + return false; + } else if (fileName.startsWith('@')) { + return true; + } else if (!fs.existsSync(`${realPath}/${fileName}/package.json`)) { + // a node_module is only valid if it contains a package.json + return false; + } else { + return true; + } + }); // should not fail because getRealDirectoryPath passed + + entries.forEach((entryName) => { + // entries should be either a package or a scoping directory. I think + // there can also be files, but we'll ignore those. + + let entryPath = path.join(realPath, entryName); + + if (getRealFilePath(entryPath)) { + // we explicitly want to ignore valid regular files in node_modules. + // This is a bit slower than just checking for directories, but we need to be sure. + return; + } + + // At this point we have an entry name that should correspond to + // a directory, which should turn into either a NodeModulesList or + // PackageInfo. If not, it's an error on this NodeModulesList. + let entryVal; + + if (entryName.startsWith('@')) { + // we should have a scoping directory. + entryVal = this._readNodeModulesList(entryPath); + + // readModulesDir only returns NodeModulesList or null + if (entryVal instanceof NodeModulesList) { + nodeModulesList.addEntry(entryName, entryVal); + } else { + // This (null return) really should not occur, unless somehow the + // dir disappears between the time of fs.readdirSync and now. + nodeModulesList.addError(Errors.ERROR_NODEMODULES_ENTRY_MISSING, entryName); + } + } else { + // we should have a package. We will always get a PackageInfo + // back, though it may contain errors. + entryVal = this._readPackage(entryPath); + nodeModulesList.addEntry(entryName, entryVal); + } + }); + + this._addEntry(realPath, nodeModulesList); + + return nodeModulesList; + } +} + +module.exports = PackageInfoCache; diff --git a/lib/models/package-info-cache/node-modules-list.js b/lib/models/package-info-cache/node-modules-list.js new file mode 100644 index 0000000000..74b963f399 --- /dev/null +++ b/lib/models/package-info-cache/node-modules-list.js @@ -0,0 +1,114 @@ +'use strict'; + +const ErrorList = require('./error-list'); + +let NULL; + +/** + * Class that stores information about a node_modules directory (i.e., the + * packages and subdirectories in the directory). It is one of the + * two types of entries in a PackageInfoCache. It is only created by the + * PackageInfoCache. + * + * @public + * @class NodeModulesList + */ +module.exports = class NodeModulesList { + constructor(realPath, cache) { + this.realPath = realPath; + this.hasEntries = false; // for speed + this.entries = Object.create(null); + this.errors = new ErrorList(); + this.cache = cache; + } + + // when we encounter a node_modules we will never traverse, we insert a NULL variant. + // https://en.wikipedia.org/wiki/Null_object_pattern + // returns a Frozen and Empty NodeModulesList + static get NULL() { + if (NULL) { + return NULL; + } + + NULL = new this('/dev/null'); // could be anywhere, why not /dev/null? + + Object.freeze(NULL.entries); + Object.freeze(NULL.errors.errors); + Object.freeze(NULL.errors); + Object.freeze(NULL); + + return NULL; + } + + /** + * Given error data, add an ErrorEntry to the ErrorList for this object. + * + * @protected + * @method addError + * @param {String} errorType one of the Errors.ERROR_* constants. + * @param {Object} errorData any error data relevant to the type of error + * being created. See showErrors(). + */ + addError(errorType, errorData) { + this.errors.addError(errorType, errorData); + } + + /** + * Indicate if there are any errors in the NodeModulesList itself (not + * including errors within the individual entries). + * + * @public + * @method hasErrors + */ + hasErrors() { + return this.errors.hasErrors(); + } + + /** + * Add an entry (PackageInfo or NodeModulesList instance) to the entries + * for this list. This is only called by PackageInfoCache. It is not intended + * to be called directly by anything else. + * + * @protected + * @method addEntry + * @param {String} entryName the name of the entry, i.e., the name of the + * file or subdirectory in the directory listing. + * @param {Object} entryVal the PackageInfo or NodeModulesList tha corresponds + * to the given entry name in the file system. + */ + addEntry(entryName, entryVal) { + this.hasEntries = true; + this.entries[entryName] = entryVal; + } + + /** + * Return a PackageInfo object for a given package name (which may include + * a scope) + * + * @public + * @method findPackage + * @param {String} packageName the name (possibly including a scope) of + * the PackageInfo the caller wants returned. + * @return the desired PackageInfo if one exists for the given name, else null. + */ + findPackage(packageName) { + if (!this.hasEntries) { + return null; + } // for speed + + let val; + + if (packageName.startsWith('@')) { + let parts = packageName.split('/'); + let entry = this.entries[parts[0]]; // scope + val = + entry instanceof NodeModulesList + ? entry.findPackage(parts[1]) // the real name + : null; + } else { + val = this.entries[packageName] || null; + } + + return val; + } +}; diff --git a/lib/models/package-info-cache/package-info.js b/lib/models/package-info-cache/package-info.js new file mode 100644 index 0000000000..42b0ae87c5 --- /dev/null +++ b/lib/models/package-info-cache/package-info.js @@ -0,0 +1,563 @@ +'use strict'; + +const path = require('path'); +const ErrorList = require('./error-list'); +const Errors = require('./errors'); +const AddonInfo = require('../addon-info'); +const isAddon = require('@ember-tooling/blueprint-model/utilities/is-addon'); +const isEngine = require('../../utilities/is-engine'); +const isLazyEngine = require('../../utilities/is-lazy-engine'); +const logger = require('heimdalljs-logger')('ember-cli:package-info-cache:package-info'); +const PerBundleAddonCache = require('../per-bundle-addon-cache'); + +function lexicographically(a, b) { + const aIsString = typeof a.name === 'string'; + const bIsString = typeof b.name === 'string'; + + if (aIsString && bIsString) { + return a.name.localeCompare(b.name); + } else if (aIsString) { + return -1; + } else if (bIsString) { + return 1; + } else { + return 0; + } +} + +function pushUnique(array, entry) { + const index = array.indexOf(entry); + + if (index > -1) { + // the entry already exists in the array, but since the presedence between + // addons is "last right wins". We first remove the duplicate entry, and + // append it to the end of the array. + array.splice(index, 1); + } + + // At this point, the entry is not in the array. So we must append it. + array.push(entry); + + // All this ensures: + // pushUnique([a1,a2,a3], a1) + // results in: + // + // [a2, a3, a1] + // + // which results in the most "least surprising" addon ordering. +} + +/** + * Class that stores information about a single package (directory tree with + * a package.json and other data, like and Addon or Project.) It is one of the + * two types of entries in a PackageInfoCache. It is only created by the + * PackageInfoCache. + * + * @public + * @class PackageInfo + */ +class PackageInfo { + constructor(pkgObj, realPath, cache, isRoot = false) { + this.pkg = pkgObj; + this.pkg['ember-addon'] = this.pkg['ember-addon'] || {}; + this.realPath = realPath; + this.cache = cache; + this.errors = new ErrorList(); + + // other fields that will be set as needed. For JIT we'll define + // them here. + this.addonMainPath = undefined; // addons only + this.inRepoAddons = undefined; // (list of PackageInfo - both) + this.internalAddons = undefined; // (list of PackageInfo - project only) + this.cliInfo = undefined; // (PackageInfo - project only) + this.dependencyPackages = undefined; // (obj keyed by dependency name: PackageInfo) + // NOTE: ALL dependencies, not just addons + this.devDependencyPackages = undefined; // (obj keyed by devDependency name: PackageInfo) + // NOTE: these are ALL dependencies, not just addons + this.nodeModules = undefined; // (NodeModulesList, set only if pkg contains node_modules) + + // flag indicating that the packageInfo is considered valid. This will + // be true as long as we have a valid directory and our package.json file + // is okay and, if we're an ember addon, that we have a valid 'main' file. + // Missing dependencies will not be considered an error, since they may + // not actually be used. + this.valid = true; + + this.mayHaveAddons = isRoot || this.isForAddon(); // mayHaveAddons used in index.js + + this._hasDumpedInvalidAddonPackages = false; + } + + // Make various fields of the pkg object available. + get name() { + return this.pkg.name; + } + + /** + * Given error data, add an ErrorEntry to the ErrorList for this object. + * This is used by the _readPackage and _readNodeModulesList methods. It + * should not be called otherwise. + * + * @protected + * @method addError + * @param {String} errorType one of the Errors.ERROR_* constants. + * @param {Object} errorData any error data relevant to the type of error + * being created. See showErrors(). + */ + addError(errorType, errorData) { + this.errors.addError(errorType, errorData); + } + + /** + * Indicate if there are any errors in the ErrorList for this package. Note that this does + * NOT indicate if there are any errors in the objects referred to by this package (e.g., + * internal addons or dependencies). + * + * @public + * @method hasErrors + * @param {boolean} true if there are errors in the ErrorList, else false. + */ + hasErrors() { + return this.errors.hasErrors(); + } + + /** + * Add a reference to an in-repo addon PackageInfo object. + * + * @protected + * @method addInRepoAddon + * @param {PackageInfo} inRepoAddonPkg the PackageInfo for the in-repo addon + * @return null + */ + addInRepoAddon(inRepoAddonPkg) { + if (!this.inRepoAddons) { + this.inRepoAddons = []; + } + this.inRepoAddons.push(inRepoAddonPkg); + } + + /** + * Add a reference to an internal addon PackageInfo object. + * "internal" addons (note: not in-repo addons) only exist in + * Projects, not other packages. Since the cache is loaded from + * 'loadProject', this can be done appropriately. + * + * @protected + * @method addInternalAddon + * @param {PackageInfo} internalAddonPkg the PackageInfo for the internal addon + * @return null + */ + addInternalAddon(internalAddonPkg) { + if (!this.internalAddons) { + this.internalAddons = []; + } + this.internalAddons.push(internalAddonPkg); + } + + /** + * For each dependency in the given list, find the corresponding + * PackageInfo object in the cache (going up the file tree if + * necessary, as in the node resolution algorithm). Return a map + * of the dependencyName to PackageInfo object. Caller can then + * store it wherever they like. + * + * Note: this is not to be called until all packages that can be have + * been added to the cache. + * + * Note: this is for ALL dependencies, not just addons. To get just + * addons, filter the result by calling pkgInfo.isForAddon(). + * + * Note: this is only intended for use from PackageInfoCache._resolveDependencies. + * It is not to be called directly by anything else. + * + * @protected + * @method addDependencies + * @param {PackageInfo} dependencies value of 'dependencies' or 'devDependencies' + * attributes of a package.json. + * @return {Object} a JavaScript object keyed on dependency name/path with + * values the corresponding PackageInfo object from the cache. + */ + addDependencies(dependencies) { + if (!dependencies) { + return null; + } + + let dependencyNames = Object.keys(dependencies); + + if (dependencyNames.length === 0) { + return null; + } + + let packages = Object.create(null); + + let missingDependencies = []; + + dependencyNames.forEach((dependencyName) => { + logger.info('%s: Trying to find dependency %o', this.pkg.name, dependencyName); + + let dependencyPackage; + + // much of the time the package will have dependencies in + // a node_modules inside it, so check there first because it's + // quicker since we have the reference. Only check externally + // if we don't find it there. + if (this.nodeModules) { + dependencyPackage = this.nodeModules.findPackage(dependencyName); + } + + if (!dependencyPackage) { + dependencyPackage = this.cache._findPackage(dependencyName, path.dirname(this.realPath)); + } + + if (dependencyPackage) { + packages[dependencyName] = dependencyPackage; + } else { + missingDependencies.push(dependencyName); + } + }); + + if (missingDependencies.length > 0) { + this.addError(Errors.ERROR_DEPENDENCIES_MISSING, missingDependencies); + } + + return packages; + } + + /** + * Indicate if this packageInfo is for a project. Should be called only after the project + * has been loaded (see {@link PackageInfoCache#loadProject} for details). + * + * @method isForProject + * @return {Boolean} true if this packageInfo is for a Project, false otherwise. + */ + isForProject() { + return !!this.project && this.project.isEmberCLIProject && this.project.isEmberCLIProject(); + } + + /** + * Indicate if this packageInfo is for an Addon. + * + * @method isForAddon + * @return {Boolean} true if this packageInfo is for an Addon, false otherwise. + */ + isForAddon() { + return isAddon(this.pkg.keywords); + } + + /** + * Indicate if this packageInfo represents an engine. + * + * @method isForEngine + * @return {Boolean} true if this pkgInfo is configured as an engine & false otherwise + */ + isForEngine() { + return isEngine(this.pkg.keywords); + } + + /** + * Indicate if this packageInfo represents a lazy engine. + * + * @method isForLazyEngine + * @return {Boolean} true if this pkgInfo is configured as an engine and the + * module this represents has lazyLoading enabled, false otherwise. + */ + isForLazyEngine() { + return this.isForEngine() && isLazyEngine(this._getAddonEntryPoint()); + } + + /** + * For use with the PerBundleAddonCache, is this packageInfo representing a + * bundle host (for now, a Project or a lazy engine). + * + * @method isForBundleHost + * @return {Boolean} true if this pkgInfo is for a bundle host, false otherwise. + */ + isForBundleHost() { + return this.isForProject() || this.isForLazyEngine(); + } + + /** + * Add to a list of child addon PackageInfos for this packageInfo. + * + * @method addPackages + * @param {Array} addonPackageList the list of child addon PackageInfos being constructed from various + * sources in this packageInfo. + * @param {Array | Object} packageInfoList a list or map of PackageInfos being considered + * (e.g., pkgInfo.dependencyPackages) for inclusion in the addonPackageList. + * @param {Function} excludeFn an optional function. If passed in, each child PackageInfo + * will be tested against the function and only included in the package map if the function + * returns a truthy value. + */ + addPackages(addonPackageList, packageInfoList, excludeFn) { + if (!packageInfoList) { + return; + } + + let result = []; + if (Array.isArray(packageInfoList)) { + if (excludeFn) { + packageInfoList = packageInfoList.filter((pkgInfo) => !excludeFn(pkgInfo)); + } + + packageInfoList.forEach((pkgInfo) => result.push(pkgInfo)); + } else { + // We're going to assume we have a map of name to packageInfo + Object.keys(packageInfoList).forEach((name) => { + let pkgInfo = packageInfoList[name]; + if (!excludeFn || !excludeFn(pkgInfo)) { + result.push(pkgInfo); + } + }); + } + + result.sort(lexicographically).forEach((pkgInfo) => pushUnique(addonPackageList, pkgInfo)); + + return addonPackageList; + } + + /** + * Discover the child addons for this packageInfo, which corresponds to an Addon. + * + * @method discoverAddonAddons + */ + discoverAddonAddons() { + let addonPackageList = []; + + this.addPackages( + addonPackageList, + this.dependencyPackages, + (pkgInfo) => !pkgInfo.isForAddon() || pkgInfo.name === 'ember-cli' + ); + this.addPackages(addonPackageList, this.inRepoAddons); + + return addonPackageList; + } + + /** + * Discover the child addons for this packageInfo, which corresponds to a Project. + * + * @method discoverProjectAddons + */ + discoverProjectAddons() { + let project = this.project; + + let addonPackageList = []; + + this.addPackages(addonPackageList, project.isEmberCLIAddon() ? [this] : null); + this.addPackages(addonPackageList, this.cliInfo ? this.cliInfo.inRepoAddons : null); + this.addPackages(addonPackageList, this.internalAddons); + this.addPackages(addonPackageList, this.devDependencyPackages, (pkgInfo) => !pkgInfo.isForAddon()); + this.addPackages(addonPackageList, this.dependencyPackages, (pkgInfo) => !pkgInfo.isForAddon()); + this.addPackages(addonPackageList, this.inRepoAddons); + + return addonPackageList; + } + + /** + * Given a list of PackageInfo objects, generate the 'addonPackages' object (keyed by + * name, value = AddonInfo instance, for all packages marked 'valid'). This is stored in + * both Addon and Project instances. + * + * @method generateAddonPackages + * @param {Array} addonPackageList the list of child addon PackageInfos to work from + * @param {Function} excludeFn an optional function. If passed in, each child PackageInfo + * will be tested against the function and only included in the package map if the function + * returns a truthy value. + */ + generateAddonPackages(addonPackageList, excludeFn) { + let validPackages = this.getValidPackages(addonPackageList); + + let packageMap = Object.create(null); + + validPackages.forEach((pkgInfo) => { + let addonInfo = new AddonInfo(pkgInfo.name, pkgInfo.realPath, pkgInfo.pkg); + if (!excludeFn || !excludeFn(addonInfo)) { + packageMap[pkgInfo.name] = addonInfo; + } + }); + + return packageMap; + } + + getValidPackages(addonPackageList) { + return addonPackageList.filter((pkgInfo) => pkgInfo.valid); + } + + getInvalidPackages(addonPackageList) { + return addonPackageList.filter((pkgInfo) => !pkgInfo.valid); + } + + dumpInvalidAddonPackages(addonPackageList) { + if (this._hasDumpedInvalidAddonPackages) { + return; + } + this._hasDumpedInvalidAddonPackages = true; + + let invalidPackages = this.getInvalidPackages(addonPackageList); + + if (invalidPackages.length > 0) { + let typeName = this.project ? 'project' : 'addon'; + + let msg = `The 'package.json' file for the ${typeName} at ${this.realPath}`; + + let relativePath; + if (invalidPackages.length === 1) { + relativePath = path.relative(this.realPath, invalidPackages[0].realPath); + msg = `${msg}\n specifies an invalid, malformed or missing addon at relative path '${relativePath}'`; + } else { + msg = `${msg}\n specifies invalid, malformed or missing addons at relative paths`; + invalidPackages.forEach((packageInfo) => { + let relativePath = path.relative(this.realPath, packageInfo.realPath); + msg = `${msg}\n '${relativePath}'`; + }); + } + + throw msg; + } + } + + /** + * This is only supposed to be called by the addon instantiation code. + * Also, the assumption here is that this PackageInfo really is for an + * Addon, so we don't need to check each time. + * + * @private + * @method getAddonConstructor + * @return {AddonConstructor} an instance of a constructor function for the Addon class + * whose package information is stored in this object. + */ + getAddonConstructor() { + if (this.addonConstructor) { + return this.addonConstructor; + } + + let module = this._getAddonEntryPoint(); + let mainDir = path.dirname(this.addonMainPath); + + let ctor; + + if (typeof module === 'function') { + ctor = module; + ctor.prototype.root = ctor.prototype.root || mainDir; + ctor.prototype.packageRoot = ctor.prototype.packageRoot || this.realPath; + ctor.prototype.pkg = ctor.prototype.pkg || this.pkg; + } else { + const Addon = require('../addon'); // done here because of circular dependency + + ctor = Addon.extend(Object.assign({ root: mainDir, packageRoot: this.realPath, pkg: this.pkg }, module)); + } + + ctor._meta_ = { + modulePath: this.addonMainPath, + }; + + return (this.addonConstructor = ctor); + } + + /** + * Construct an addon instance. + * + * NOTE: this does NOT call constructors for the child addons. That is left to + * the caller to do, so they can insert any other logic they want. + * + * @private + * @method constructAddonInstance + * @param {Project|Addon} parent the parent that directly contains this addon + * @param {Project} project the project that is/contains this addon + */ + constructAddonInstance(parent, project) { + let start = Date.now(); + + let AddonConstructor = this.getAddonConstructor(); + + let addonInstance; + + try { + addonInstance = new AddonConstructor(parent, project); + } catch (e) { + if (parent && parent.ui) { + parent.ui.writeError(e); + } + + const SilentError = require('silent-error'); + throw new SilentError(`An error occurred in the constructor for ${this.name} at ${this.realPath}`); + } + + AddonConstructor._meta_.initializeIn = Date.now() - start; + addonInstance.constructor = AddonConstructor; + + return addonInstance; + } + + /** + * Create an instance of the addon represented by this packageInfo or (if we + * are supporting per-bundle caching and this is an allow-caching-per-bundle addon) + * check if we should be creating a proxy instead. + * + * NOTE: we assume that the value of 'allowCachingPerBundle' does not change between + * calls to the constructor! A given addon is either allowing or not allowing caching + * for an entire run. + * + * @method getAddonInstance + * @param {} parent the addon/project that is to be the direct parent of the + * addon instance created here + * @param {*} project the project that is to contain this addon instance + * @return {Object} the constructed instance of the addon + */ + getAddonInstance(parent, project) { + let addonEntryPointModule = this._getAddonEntryPoint(); + let addonInstance; + + if ( + PerBundleAddonCache.isEnabled() && + project && + project.perBundleAddonCache.allowCachingPerBundle(addonEntryPointModule) + ) { + addonInstance = project.perBundleAddonCache.getAddonInstance(parent, this); + } else { + addonInstance = this.constructAddonInstance(parent, project); + this.initChildAddons(addonInstance); + } + + return addonInstance; + } + + /** + * Initialize the child addons array of a newly-created addon instance. Normally when + * an addon derives from Addon, child addons will be created during 'setupRegistry' and + * this code is essentially unnecessary. But if an addon is created with custom constructors + * that don't call 'setupRegistry', any child addons may not yet be initialized. + * + * @method initChildAddons + * @param {Addon} addonInstance + */ + initChildAddons(addonInstance) { + if (addonInstance.initializeAddons) { + addonInstance.initializeAddons(); + } else { + addonInstance.addons = []; + } + } + + /** + * Gets the addon entry point + * + * @method _getAddonEntryPoint + * @return {Object|Function} The exported addon entry point + * @private + */ + _getAddonEntryPoint() { + if (!this.addonMainPath) { + throw new Error(`${this.pkg.name} at ${this.realPath} is missing its addon main file`); + } + + // Load the addon. + // TODO: Future work - allow a time budget for loading each addon and warn + // or error for those that take too long. + return require(this.addonMainPath); + } +} + +module.exports = PackageInfo; +module.exports.lexicographically = lexicographically; +module.exports.pushUnique = pushUnique; diff --git a/lib/models/per-bundle-addon-cache/addon-proxy.js b/lib/models/per-bundle-addon-cache/addon-proxy.js new file mode 100644 index 0000000000..ce1ccd1cef --- /dev/null +++ b/lib/models/per-bundle-addon-cache/addon-proxy.js @@ -0,0 +1,158 @@ +'use strict'; + +const { TARGET_INSTANCE } = require('./target-instance'); + +const CACHE_KEY_FOR_TREE_TRACKER = Symbol('CACHE_KEY_FOR_TREE_TRACKER'); + +/** + * Validates that a new cache key for a given tree type matches the previous + * cache key for the same tree type. To opt-in to bundle addon caching for + * a given addon it's assumed that it returns stable cache keys; specifically + * this is because the interplay between bundle addon caching and `ember-engines` + * when transitive deduplication is enabled assumes stable cache keys, so we validate + * for this case here. + * + * @method validateCacheKey + * @param {Addon} realAddonInstance The real addon instance + * @param {string} treeType + * @param {string} newCacheKey + * @throws {Error} If the new cache key doesn't match the previous cache key + */ +function validateCacheKey(realAddonInstance, treeType, newCacheKey) { + let cacheKeyTracker = realAddonInstance[CACHE_KEY_FOR_TREE_TRACKER]; + + if (!cacheKeyTracker) { + cacheKeyTracker = {}; + realAddonInstance[CACHE_KEY_FOR_TREE_TRACKER] = cacheKeyTracker; + } + + cacheKeyTracker[treeType] = treeType in cacheKeyTracker ? cacheKeyTracker[treeType] : newCacheKey; + + if (cacheKeyTracker[treeType] !== newCacheKey) { + throw new Error( + `[ember-cli] addon bundle caching can only be used on addons that have stable cache keys (previously \`${ + cacheKeyTracker[treeType] + }\`, now \`${newCacheKey}\`; for addon \`${realAddonInstance.name}\` located at \`${ + realAddonInstance.packageRoot || realAddonInstance.root + }\`)` + ); + } +} + +/** + * Returns a proxy to a target with specific handling for the + * `parent` property, as well has to handle the `app` property; + * that is, the proxy should maintain correct local state in + * closure scope for the `app` property if it happens to be set + * by `ember-cli`. Other than `parent` & `app`, this function also + * proxies _almost_ everything to `target[TARGET_INSTANCE] with a few + * exceptions: we trap & return `[]` for `addons`, and we don't return + * the original `included` (it's already called on the "real" addon + * by `ember-cli`). + * + * Note: the target is NOT the per-bundle cacheable instance of the addon. Rather, + * it is a cache entry POJO from PerBundleAddonCache. + * + * @method getAddonProxy + * @param targetCacheEntry the PerBundleAddonCache cache entry we are to proxy. It + * has one interesting property, the real addon instance the proxy is forwarding + * calls to (that property is not globally exposed). + * @param parent the parent object of the proxy being created (the same as + * the 'parent' property of a normal addon instance) + * @return Proxy + */ +function getAddonProxy(targetCacheEntry, parent) { + let _app; + + // handle `preprocessJs` separately for Embroider + // + // some context here: + // + // Embroider patches `preprocessJs`, so we want to maintain local state within the + // proxy rather than allowing a patched `preprocessJs` set on the original addon + // instance itself + // + // for more info as to where this happens, see: + // https://github.com/embroider-build/embroider/blob/master/packages/compat/src/v1-addon.ts#L634 + let _preprocessJs; + + return new Proxy(targetCacheEntry, { + get(targetCacheEntry, property) { + if (property === 'parent') { + return parent; + } + + if (property === 'app') { + return _app; + } + + // only return `_preprocessJs` here if it was previously set to a patched version + if (property === 'preprocessJs' && _preprocessJs) { + return _preprocessJs; + } + + // keep proxies from even trying to set or initialize addons + if (property === 'initializeAddons') { + return undefined; + } + + // See the {@link index.js} file for a discussion of why the proxy 'addons' + // property returns an empty array. + if (property === 'addons') { + return []; + } + + // allow access to the property pointing to the real instance. + if (property === TARGET_INSTANCE) { + return targetCacheEntry[TARGET_INSTANCE]; + } + + // `included` will be called on the "real" addon, so there's no need for it to be + // called again; instead we return a no-op implementation here + if (property === 'included') { + return () => undefined; + } + + if (targetCacheEntry[TARGET_INSTANCE]) { + if (property !== 'constructor' && typeof targetCacheEntry[TARGET_INSTANCE][property] === 'function') { + // If we fall through to the Reflect.get just below, the 'this' context of the function when + // invoked is the proxy, not the original instance (so its local state is incorrect). + // Wrap the original methods to maintain the correct 'this' context. + return function _originalAddonPropMethodWrapper() { + let originalReturnValue = targetCacheEntry[TARGET_INSTANCE][property](...arguments); + + if (property === 'cacheKeyForTree') { + const treeType = arguments[0]; + validateCacheKey(targetCacheEntry[TARGET_INSTANCE], treeType, originalReturnValue); + } + + return originalReturnValue; + }; + } + + return Reflect.get(targetCacheEntry[TARGET_INSTANCE], property); + } + + return Reflect.get(targetCacheEntry, property); + }, + set(targetCacheEntry, property, value) { + if (property === 'app') { + _app = value; + return true; + } + + if (property === 'preprocessJs') { + _preprocessJs = value; + return true; + } + + if (targetCacheEntry[TARGET_INSTANCE]) { + return Reflect.set(targetCacheEntry[TARGET_INSTANCE], property, value); + } + + return Reflect.set(targetCacheEntry, property, value); + }, + }); +} + +module.exports = { getAddonProxy }; diff --git a/lib/models/per-bundle-addon-cache/index.js b/lib/models/per-bundle-addon-cache/index.js new file mode 100644 index 0000000000..832db55ee2 --- /dev/null +++ b/lib/models/per-bundle-addon-cache/index.js @@ -0,0 +1,390 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const isLazyEngine = require('../../utilities/is-lazy-engine'); +const { getAddonProxy } = require('./addon-proxy'); +const logger = require('heimdalljs-logger')('ember-cli:per-bundle-addon-cache'); +const { TARGET_INSTANCE } = require('./target-instance'); + +function defaultAllowCachingPerBundle({ addonEntryPointModule }) { + return ( + addonEntryPointModule.allowCachingPerBundle || + (addonEntryPointModule.prototype && addonEntryPointModule.prototype.allowCachingPerBundle) + ); +} + +/** + * Resolves the perBundleAddonCacheUtil; this prefers the custom provided version by + * the consuming application, and defaults to an internal implementation here. + * + * @method resolvePerBundleAddonCacheUtil + * @param {Project} project + * @return {{allowCachingPerBundle: Function}} + */ +function resolvePerBundleAddonCacheUtil(project) { + const relativePathToUtil = + project.pkg && project.pkg['ember-addon'] && project.pkg['ember-addon'].perBundleAddonCacheUtil; + + if (typeof relativePathToUtil === 'string') { + const absolutePathToUtil = path.resolve(project.root, relativePathToUtil); + + if (!fs.existsSync(absolutePathToUtil)) { + throw new Error( + `[ember-cli] the provided \`${relativePathToUtil}\` for \`ember-addon.perBundleAddonCacheUtil\` does not exist` + ); + } + + return require(absolutePathToUtil); + } + + return { + allowCachingPerBundle: defaultAllowCachingPerBundle, + }; +} + +/** + * For large applications with many addons (and many instances of each, resulting in + * potentially many millions of addon instances during a build), the build can become + * very, very slow (tens of minutes) partially due to the sheer number of addon instances. + * The PerBundleAddonCache deals with this slowness by doing 3 things: + * + * (1) Making only a single copy of each of certain addons and their dependent addons + * (2) Replacing any other instances of those addons with Proxy copies to the single instance + * (3) Having the Proxies return an empty array for their dependent addons, rather + * than proxying to the contents of the single addon instance. This gives up the + * ability of the Proxies to traverse downward into their child addons, + * something that many addons do not do anyway, for the huge reduction in duplications + * of those child addons. For applications that enable `ember-engines` dedupe logic, + * that logic is stateful, and having the Proxies allow access to the child addons array + * just breaks everything, because that logic will try multiple times to remove items + * it thinks are duplicated, messing up the single copy of the child addon array. + * See the explanation of the dedupe logic in + * {@link https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/utils/deeply-non-duplicated-addon.js} + * + * What follows are the more technical details of how the PerBundleAddonCache implements + * the above 3 behaviors. + * + * This class supports per-bundle-host (bundle host = project or lazy engine) + * caching of addon instances. During addon initialization we cannot add a + * cache to each bundle host object AFTER it is instantiated because running the + * addon constructor ultimately causes Addon class `setupRegistry` code to + * run which instantiates child addons, which need the cache to already be + * in place for the parent bundle host. + * We handle this by providing a global cache that exists independent of the + * bundle host objects. That is this object. + * + * There are a number of "behaviors" being implemented by this object and + * its contents. They are: + * (1) Any addon that is a lazy engine has only a single real instance per + * project - all other references to the lazy engine are to be proxies. These + * lazy engines are compared by name, not by packageInfo.realPath. + * (2) Any addon that is not a lazy engine, there is only a single real instance + * of the addon per "bundle host" (i.e. lazy engine or project). + * (3) An optimization - any addon that is in a lazy engine but that is also + * in bundled by its LCA host - the single instance is the one bundled by this + * host. All other instances (in any lazy engine) are proxies. + * + * NOTE: the optimization is only enabled if the environment variable that controls + * `ember-engines` transitive deduplication (process.env.EMBER_ENGINES_ADDON_DEDUPE) + * is set to a truthy value. For more info, see: + * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/engine-addon.js#L396 + * + * @public + * @class PerBundleAddonCache + */ +class PerBundleAddonCache { + constructor(project) { + this.project = project; + + // The cache of bundle-host package infos and their individual addon caches. + // The cache is keyed by package info (representing a bundle host (project or + // lazy engine)) and an addon instance cache to bundle with that bundle host. + this.bundleHostCache = new Map(); + + // Indicate if ember-engines transitive dedupe is enabled. + this.engineAddonTransitiveDedupeEnabled = !!process.env.EMBER_ENGINES_ADDON_DEDUPE; + this._perBundleAddonCacheUtil = resolvePerBundleAddonCacheUtil(this.project); + + // For stats purposes, counts on the # addons and proxies created. Addons we + // can compare against the bundleHostCache addon caches. Proxies, not so much, + // but we'll count them here. + this.numAddonInstances = 0; + this.numProxies = 0; + } + + /** + * The default implementation here is to indicate if the original addon entry point has + * the `allowCachingPerBundle` flag set either on itself or on its prototype. + * + * If a consuming application specifies a relative path to a custom utility via the + * `ember-addon.perBundleAddonCacheUtil` configuration, we prefer the custom implementation + * provided by the consumer. + * + * @method allowCachingPerBundle + * @param {Object|Function} addonEntryPointModule + * @return {Boolean} true if the given constructor function or class supports caching per bundle, false otherwise + */ + allowCachingPerBundle(addonEntryPointModule) { + return this._perBundleAddonCacheUtil.allowCachingPerBundle({ addonEntryPointModule }); + } + + /** + * Creates a cache entry for the bundleHostCache. Because we want to use the same sort of proxy + * for both bundle hosts and for 'regular' addon instances (though their cache entries have + * slightly different structures) we'll use the Symbol from getAddonProxy. + * + * @method createBundleHostCacheEntry + * @param {PackageInfo} bundleHostPkgInfo bundle host's pkgInfo.realPath + * @return {Object} an object in the form of a bundle-host cache entry + */ + createBundleHostCacheEntry(bundleHostPkgInfo) { + return { [TARGET_INSTANCE]: null, realPath: bundleHostPkgInfo.realPath, addonInstanceCache: new Map() }; + } + + /** + * Create a cache entry object for a given (non-bundle-host) addon to put into + * an addon cache. + * + * @method createAddonCacheEntry + * @param {Addon} addonInstance the addon instance to cache + * @param {String} addonRealPath the addon's pkgInfo.realPath + * @return {Object} an object in the form of an addon-cache entry + */ + createAddonCacheEntry(addonInstance, addonRealPath) { + return { [TARGET_INSTANCE]: addonInstance, realPath: addonRealPath }; + } + + /** + * Given a parent object of a potential addon (another addon or the project), + * go up the 'parent' chain to find the potential addon's bundle host object + * (i.e. lazy engine or project.) Because Projects are always bundle hosts, + * this should always pass, but we'll throw if somehow it doesn't work. + * + * @method findBundleHost + * @param {Project|Addon} addonParent the direct parent object of a (potential or real) addon. + * @param {PackageInfo} addonPkgInfo the PackageInfo for an addon being instantiated. This is only + * used for information if an error is going to be thrown. + * @return {Object} the object in the 'parent' chain that is a bundle host. + * @throws {Error} if there is not bundle host + */ + findBundleHost(addonParent, addonPkgInfo) { + let curr = addonParent; + + while (curr) { + if (curr === this.project) { + return curr; + } + + if (isLazyEngine(curr)) { + // if we're building a lazy engine in isolation, prefer that the bundle host is + // the project, not the lazy engine addon instance + if (curr.parent === this.project && curr._packageInfo === this.project._packageInfo) { + return this.project; + } + + return curr; + } + + curr = curr.parent; + } + + // the following should not be able to happen given that Projects are always + // bundle hosts, but just in case, throw an error if we didn't find one. + throw new Error(`Addon at path\n ${addonPkgInfo.realPath}\n has 'allowCachingPerBundle' but has no bundleHost`); + } + + /** + * An optimization we support from lazy engines is the following: + * + * If an addon instance is supposed to be bundled with a particular lazy engine, and + * same addon is also to be bundled by a common LCA host, prefer the one bundled by the + * host (since it's ultimately going to be deduped later by `ember-engines`). + * + * NOTE: this only applies if this.engineAddonTransitiveDedupeEnabled is truthy. If it is not, + * the bundle host always "owns" the addon instance. + * + * If deduping is enabled and the LCA host also depends on the same addon, + * the lazy-engine instances of the addon will all be proxies to the one in + * the LCA host. This function indicates whether the bundle host passed in + * (either the project or a lazy engine) is really the bundle host to "own" the + * new addon. + * + * @method bundleHostOwnsInstance + * @param (Object} bundleHost the project or lazy engine that is trying to "own" + * the new addon instance specified by addonPkgInfo + * @param {PackageInfo} addonPkgInfo the PackageInfo of the potential new addon instance + * @return {Boolean} true if the bundle host is to "own" the instance, false otherwise. + */ + bundleHostOwnsInstance(bundleHost, addonPkgInfo) { + if (isLazyEngine(bundleHost)) { + return ( + !this.engineAddonTransitiveDedupeEnabled || + !this.project.hostInfoCache + .getHostAddonInfo(bundleHost._packageInfo) + .hostAndAncestorBundledPackageInfos.has(addonPkgInfo) + ); + } + + return true; + } + + findBundleOwner(bundleHost, addonPkgInfo) { + if (bundleHost === this.project._packageInfo) { + return bundleHost; + } + + let { hostPackageInfo, hostAndAncestorBundledPackageInfos } = + this.project.hostInfoCache.getHostAddonInfo(bundleHost); + + if (!hostAndAncestorBundledPackageInfos.has(addonPkgInfo)) { + return bundleHost; + } + + return this.findBundleOwner(hostPackageInfo, addonPkgInfo); + } + + /** + * Called from PackageInfo.getAddonInstance(), return an instance of the requested + * addon or a Proxy, based on the type of addon and its bundle host. + * + * @method getAddonInstance + * @param {Addon|Project} parent the parent Addon or Project this addon instance is + * a child of. + * @param {*} addonPkgInfo the PackageInfo for the addon being created. + * @return {Addon|Proxy} An addon instance (for the first copy of the addon) or a Proxy. + * An addon that is a lazy engine will only ever have a single copy in the cache. + * An addon that is not will have 1 copy per bundle host (Project or lazy engine), + * except if it is an addon that's also owned by a given LCA host and transitive + * dedupe is enabled (`engineAddonTransitiveDedupeEnabled`), in which case it will + * only have a single copy in the project's addon cache. + */ + getAddonInstance(parent, addonPkgInfo) { + // If the new addon is itself a bundle host (i.e. lazy engine), there is only one + // instance of the bundle host, and it's in the entries of the bundleHostCache, outside + // of the 'regular' addon caches. Because 'setupBundleHostCache' ran during construction, + // we know that an entry is in the cache with this engine name. + if (addonPkgInfo.isForBundleHost()) { + let cacheEntry = this._getBundleHostCacheEntry(addonPkgInfo); + + if (cacheEntry[TARGET_INSTANCE]) { + logger.debug(`About to construct BR PROXY to cache entry for addon at: ${addonPkgInfo.realPath}`); + this.numProxies++; + return getAddonProxy(cacheEntry, parent); + } else { + // create an instance, put it in the pre-existing cache entry, then + // return it (as the first instance of the lazy engine.) + logger.debug(`About to fill in BR EXISTING cache entry for addon at: ${addonPkgInfo.realPath}`); + this.numAddonInstances++; + let addon = addonPkgInfo.constructAddonInstance(parent, this.project); + cacheEntry[TARGET_INSTANCE] = addon; // cache BEFORE initializing child addons + addonPkgInfo.initChildAddons(addon); + return addon; + } + } + + // We know now we're asking for a 'regular' (non-bundle-host) addon instance. + + let bundleHost = this.findBundleHost(parent, addonPkgInfo); + + // if the bundle host "owns" the new addon instance + // * Do we already have an instance of the addon cached? + // * If so, make a proxy for it. + // * If not, make a new instance of the addon and cache it in the + // bundle host's addon cache. + // If not, it means the bundle host is a lazy engine but the LCA host also uses + // the addon and deduping is enabled + // * If the LCA host already has a cached entry, return a proxy to that + // * If it does not, create a 'blank' cache entry and return a proxy to that. + // When the addon is encountered later when processing the LCA host's addons, + // fill in the instance. + if (this.bundleHostOwnsInstance(bundleHost, addonPkgInfo)) { + let bundleHostCacheEntry = this._getBundleHostCacheEntry(bundleHost._packageInfo); + let addonInstanceCache = bundleHostCacheEntry.addonInstanceCache; + let addonCacheEntry = addonInstanceCache.get(addonPkgInfo.realPath); + let addonInstance; + + if (addonCacheEntry) { + if (addonCacheEntry[TARGET_INSTANCE]) { + logger.debug(`About to construct REGULAR ADDON PROXY for addon at: ${addonPkgInfo.realPath}`); + this.numProxies++; + return getAddonProxy(addonCacheEntry, parent); + } else { + // the cache entry was created 'empty' by an earlier call, indicating + // an addon that is used in a lazy engine but also used by its LCA host, + // and we're now creating the instance for the LCA host. + // Fill in the entry and return the new instance. + logger.debug(`About to fill in REGULAR ADDON EXISTING cache entry for addon at: ${addonPkgInfo.realPath}`); + this.numAddonInstances++; + addonInstance = addonPkgInfo.constructAddonInstance(parent, this.project); + addonCacheEntry[TARGET_INSTANCE] = addonInstance; // cache BEFORE initializing child addons + addonPkgInfo.initChildAddons(addonInstance); + return addonInstance; + } + } + + // There is no entry for this addon in the bundleHost's addon cache. Create a new + // instance, cache it in the addon cache, and return it. + logger.debug(`About to construct REGULAR ADDON NEW cache entry for addon at: ${addonPkgInfo.realPath}`); + this.numAddonInstances++; + addonInstance = addonPkgInfo.constructAddonInstance(parent, this.project); + addonCacheEntry = this.createAddonCacheEntry(addonInstance, addonPkgInfo.realPath); + addonInstanceCache.set(addonPkgInfo.realPath, addonCacheEntry); // cache BEFORE initializing child addons + addonPkgInfo.initChildAddons(addonInstance); + return addonInstance; + } else { + // The bundleHost is not the project but the some ancestor bundles the addon and + // deduping is enabled, so the cache entry needs to go in the bundle owner's cache. + // Get/create an empty cache entry and return a proxy to it. The bundle owner will + // set the instance later (see above). + let bundleHostCacheEntry = this._getBundleHostCacheEntry( + this.findBundleOwner(bundleHost._packageInfo, addonPkgInfo) + ); + let addonCacheEntry = bundleHostCacheEntry.addonInstanceCache.get(addonPkgInfo.realPath); + + if (!addonCacheEntry) { + logger.debug(`About to construct REGULAR ADDON EMPTY cache entry for addon at: ${addonPkgInfo.realPath}`); + addonCacheEntry = this.createAddonCacheEntry(null, addonPkgInfo.realPath); + bundleHostCacheEntry.addonInstanceCache.set(addonPkgInfo.realPath, addonCacheEntry); + } + + logger.debug(`About to construct REGULAR ADDON PROXY for EMPTY addon at: ${addonPkgInfo.realPath}`); + this.numProxies++; + return getAddonProxy(addonCacheEntry, parent); + } + } + + getPathsToAddonsOptedIn() { + const addonSet = new Set(); + + for (const [, { addonInstanceCache }] of this.bundleHostCache) { + Array.from(addonInstanceCache.keys()).forEach((realPath) => { + addonSet.add(realPath); + }); + } + + return Array.from(addonSet); + } + + _getBundleHostCacheEntry(pkgInfo) { + let cacheEntry = this.bundleHostCache.get(pkgInfo); + + if (!cacheEntry) { + cacheEntry = this.createBundleHostCacheEntry(pkgInfo); + this.bundleHostCache.set(pkgInfo, cacheEntry); + } + + return cacheEntry; + } + + // Support for per-bundle addon caching is GLOBAL opt OUT (unless you explicitly set + // EMBER_CLI_ADDON_INSTANCE_CACHING to false, it will be enabled.) If you opt out, that + // overrides setting `allowCachingPerBundle` for any particular addon type to true. + // To help make testing easier, we'll expose the setting as a function so it can be + // called multiple times and evaluate each time. + static isEnabled() { + return process.env.EMBER_CLI_ADDON_INSTANCE_CACHING !== 'false'; + } +} + +module.exports = PerBundleAddonCache; diff --git a/lib/models/per-bundle-addon-cache/target-instance.js b/lib/models/per-bundle-addon-cache/target-instance.js new file mode 100644 index 0000000000..9903efd14e --- /dev/null +++ b/lib/models/per-bundle-addon-cache/target-instance.js @@ -0,0 +1,15 @@ +'use strict'; + +/** + * A Symbol constant for sharing between index.js and addon-proxy.js rather than + * putting the symbol into the Symbol global cache. The symbol is used in per-bundle + * cache entries to refer to the field that points at the real instance that a Proxy + * refers to. + * @class TARGET_INSTANCE + * @type Symbol + * @private + * @final + */ +const TARGET_INSTANCE = Symbol('_targetInstance_'); + +module.exports.TARGET_INSTANCE = TARGET_INSTANCE; diff --git a/lib/models/project.js b/lib/models/project.js index bf2d8f67ec..db5591e164 100644 --- a/lib/models/project.js +++ b/lib/models/project.js @@ -3,673 +3,794 @@ /** @module ember-cli */ -var Promise = require('../ext/promise'); -var path = require('path'); -var findup = Promise.denodeify(require('findup')); -var resolve = Promise.denodeify(require('resolve')); -var fs = require('fs'); -var existsSync = require('exists-sync'); -var find = require('lodash/collection/find'); -var assign = require('lodash/object/assign'); -var forOwn = require('lodash/object/forOwn'); -var merge = require('lodash/object/merge'); -var debug = require('debug')('ember-cli:project'); -var AddonDiscovery = require('../models/addon-discovery'); -var AddonsFactory = require('../models/addons-factory'); -var Command = require('../models/command'); -var UI = require('../ui'); -var nodeModulesPath = require('node-modules-path'); - -var getPackageBaseName = require('../utilities/get-package-base-name'); -var versionUtils = require('../utilities/version-utils'); -var emberCLIVersion = versionUtils.emberCLIVersion; +const util = require('util'); +const path = require('path'); +const { findUpSync } = require('find-up'); +const resolve = util.promisify(require('resolve')); +const fs = require('fs-extra'); +const cloneDeep = require('lodash/cloneDeep'); +const merge = require('lodash/merge'); +const forOwn = require('lodash/forOwn'); +const logger = require('heimdalljs-logger')('ember-cli:project'); +const versionUtils = require('../utilities/version-utils'); +const emberCLIVersion = versionUtils.emberCLIVersion; +const heimdall = require('heimdalljs'); +const PackageInfoCache = require('./package-info-cache'); +const PerBundleAddonCache = require('./per-bundle-addon-cache'); +const instantiateAddons = require('./instantiate-addons'); +const HostInfoCache = require('./host-info-cache'); + +let processCwd = process.cwd(); -/** - The Project model is tied to your package.json. It is instiantiated - by giving Project.closest the path to your project. - - @class Project - @constructor - @param {String} root Root directory for the project - @param {Object} pkg Contents of package.json -*/ -function Project(root, pkg, ui, cli) { - debug('init root: %s', root); - this.root = root; - this.pkg = pkg; - this.ui = ui; - this.cli = cli; - this.addonPackages = {}; - this.addons = []; - this.liveReloadFilterPatterns = []; - this.setupBowerDirectory(); - this.setupNodeModulesPath(); - this.addonDiscovery = new AddonDiscovery(this.ui); - this.addonsFactory = new AddonsFactory(this, this); - this._watchmanInfo = { - enabled: false, - version: null, - canNestRoots: false - }; -} - -/** - Set when the `Watcher.detectWatchman` helper method finishes running, - so that other areas of the system can be aware that watchman is being used. - - For example, this information is used in the broccoli build pipeline to know - if we can watch additional directories (like bower_components) "cheaply". +// ensure NULL_PROJECT is a singleton +let NULL_PROJECT; + +class Project { + /** + The Project model is tied to your package.json. It is instantiated + by giving {{#crossLink "Project/closestSync:method"}}{{/crossLink}} + the path to your project. + + @class Project + @constructor + @param {String} root Root directory for the project + @param {Object} pkg Contents of package.json + @param {UI} ui + @param {CLI} cli + */ + constructor(root, pkg, ui, cli) { + logger.info('init root: %s', root); + + this.root = root; + this.pkg = pkg; + this.ui = ui; + this.cli = cli; + this.addonPackages = Object.create(null); + this.addons = []; + this.liveReloadFilterPatterns = []; + this.configCache = new Map(); + this.hostInfoCache = new HostInfoCache(this); + + /** + Set when the `Watcher.detectWatchman` helper method finishes running, + so that other areas of the system can be aware that watchman is being used. + + For example, this information is used in the broccoli build pipeline to know + if we can watch additional directories "cheaply". + + Contains `enabled` and `version`. + + @private + @property _watchmanInfo + @return {Object} + @default false + */ + this._watchmanInfo = { + enabled: false, + version: null, + canNestRoots: false, + }; + + let instrumentation = (this._instrumentation = ensureInstrumentation(cli, ui)); + instrumentation.project = this; + + this.emberCLIVersion = emberCLIVersion; + + this._nodeModulesPath = null; + + if (this.cli && this.cli.packageInfoCache) { + this.packageInfoCache = this.cli.packageInfoCache; + } else { + this.packageInfoCache = new PackageInfoCache(this.ui); + } - Contains `enabled` and `version`. + // we're not dealing with NULL_PROJECT (note that it has not + // be set yet, so we can't compare to that var.) + this._packageInfo = this.packageInfoCache.loadProject(this); - @private - @property _watchmanInfo - @returns {Object} - @default false -*/ + // force us to use the real path as the root. + this.root = this._packageInfo.realPath; -/** - Sets the name of the bower directory for this project + // XXX Need to decide what to do here about showing errors. For + // a non-CLI project the cache is local and probably should. For + // a CLI project the cache is there, but not sure when we'll know + // about all the errors, because there may be multiple projects. + if (this.packageInfoCache.hasErrors()) { + this.packageInfoCache.showErrors(); + } - @private - @method setupBowerDirectory - */ -Project.prototype.setupBowerDirectory = function() { - var bowerrcPath = path.join(this.root, '.bowerrc'); + if (PerBundleAddonCache.isEnabled()) { + this.perBundleAddonCache = new PerBundleAddonCache(this); + } + } - debug('bowerrc path: %s', bowerrcPath); + // Checks whether the project's npm dependencies are + // present. Previously this just looked for a node_modules folder in + // a fixed place (which is not compatible with node's module + // resolution algorithm). Now we just sample to see if we can + // resolve the first dependency or devDependency we find. + hasDependencies() { + if (this.cli.disableDependencyChecker) { + // Blueprint tests set this flag. + return true; + } - if (existsSync(bowerrcPath)) { - var bowerrcContent = fs.readFileSync(bowerrcPath); - try { - this.bowerDirectory = JSON.parse(bowerrcContent).directory; - } catch (exception) { - debug('failed to parse bowerc: %s', exception); - this.bowerDirectory = null; + for (let depName in this.dependencies()) { + try { + this.resolveSync(`${depName}/package.json`); + return true; + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err; + } + return false; + } } + return false; } - this.bowerDirectory = this.bowerDirectory || 'bower_components'; - debug('bowerDirectory: %s', this.bowerDirectory); -}; + static nullProject(ui, cli) { + if (NULL_PROJECT) { + return NULL_PROJECT; + } -Project.prototype.hasDependencies = function() { - return !!this.nodeModulesPath; -}; -/** - Sets the path to the node_modules directory for this - project. - - @private - @method setupNodeModulesPath - */ -Project.prototype.setupNodeModulesPath = function(){ - this.nodeModulesPath = nodeModulesPath(this.root); - debug('nodeModulesPath: %s', this.nodeModulesPath); -}; - -var processCwd = process.cwd(); -// ensure NULL_PROJECT is a singleton -var NULL_PROJECT; + NULL_PROJECT = new Project(processCwd, {}, ui, cli); -Project.nullProject = function (ui, cli) { - if (NULL_PROJECT) { return NULL_PROJECT; } + NULL_PROJECT.isEmberCLIProject = function () { + return false; + }; - NULL_PROJECT = new Project(processCwd, {}, ui, cli); + NULL_PROJECT.isViteProject = function () { + return false; + }; - NULL_PROJECT.isEmberCLIProject = function() { - return false; - }; + NULL_PROJECT.isEmberCLIAddon = function () { + return false; + }; - NULL_PROJECT.isEmberCLIAddon = function() { - return false; - }; - - NULL_PROJECT.name = function() { - return path.basename(process.cwd()); - }; + NULL_PROJECT.name = function () { + return path.basename(process.cwd()); + }; - NULL_PROJECT.initializeAddons(); + NULL_PROJECT.initializeAddons(); - return NULL_PROJECT; -}; + return NULL_PROJECT; + } -/** - Returns the name from package.json. + /** + Returns the name from package.json. - @private - @method name - @return {String} Package name - */ -Project.prototype.name = function() { - return getPackageBaseName(this.pkg.name); -}; + @private + @method name + @return {String} Package name + */ + name() { + const getPackageBaseName = require('../utilities/get-package-base-name'); -/** - Returns whether or not this is an Ember CLI project. - This checks whether ember-cli is listed in devDependencies. + return getPackageBaseName(this.pkg.name); + } - @private - @method isEmberCLIProject - @return {Boolean} Whether this is an Ember CLI project - */ -Project.prototype.isEmberCLIProject = function() { - return (this.cli ? this.cli.npmPackage : 'ember-cli') in this.dependencies(); -}; + /** + Returns whether or not this is an Ember CLI project. + This checks whether ember-cli is listed in (dev)Dependencies. -/** - Returns whether or not this is an Ember CLI addon. + @private + @method isEmberCLIProject + @return {Boolean} Whether this is an Ember CLI project + */ + isEmberCLIProject() { + return (this.cli ? this.cli.npmPackage : 'ember-cli') in this.dependencies(); + } - @method isEmberCLIAddon - @return {Boolean} Whether or not this is an Ember CLI Addon. - */ -Project.prototype.isEmberCLIAddon = function() { - return !!this.pkg.keywords && this.pkg.keywords.indexOf('ember-addon') > -1; -}; + /** + Returns whether this is a Vite-based project. + This checks whether @embroider/vite is listed in (dev)Dependencies. -/** - Returns the path to the configuration. + @private + @method isViteProject + @return {Boolean} Whether this is a Vite project + */ + isViteProject() { + return '@embroider/vite' in this.dependencies(); // This would preferably be `isClassicProject` as Vite is the default now, but it is tough to come up with a metric + } - @private - @method configPath - @return {String} Configuration path - */ -Project.prototype.configPath = function() { - var configPath = 'config'; + /** + Returns whether or not this is an Ember CLI addon. - if (this.pkg['ember-addon'] && this.pkg['ember-addon']['configPath']) { - configPath = this.pkg['ember-addon']['configPath']; + @method isEmberCLIAddon + @return {Boolean} Whether or not this is an Ember CLI Addon. + */ + isEmberCLIAddon() { + return !!this.pkg && !!this.pkg.keywords && this.pkg.keywords.indexOf('ember-addon') > -1; } - return path.join(configPath, 'environment'); -}; - -/** - Loads the configuration for this project and its addons. + /** + Returns the path to the configuration. - @private - @method config - @param {String} env Environment name - @return {Object} Merged confiration object - */ -Project.prototype.config = function(env) { - var configPath = this.configPath(); + @private + @method configPath + @return {String} Configuration path + */ + configPath() { + let configPath = 'config'; - if (existsSync(path.join(this.root, configPath + '.js'))) { - var appConfig = this.require('./' + configPath)(env); - var addonsConfig = this.getAddonsConfig(env, appConfig); + if (this.pkg['ember-addon'] && this.pkg['ember-addon']['configPath']) { + configPath = this.pkg['ember-addon']['configPath']; + } - return merge(addonsConfig, appConfig); - } else { - return this.getAddonsConfig(env, {}); + return path.join(this.root, configPath, 'environment'); } -}; -/** - Returns the addons configuration. - - @private - @method getAddonsConfig - @param {String} env Environment name - @param {Object} appConfig Application configuration - @return {Object} Merged configuration of all addons - */ -Project.prototype.getAddonsConfig = function(env, appConfig) { - this.initializeAddons(); - - var initialConfig = merge({}, appConfig); - - return this.addons.reduce(function(config, addon) { - if (addon.config) { - merge(config, addon.config(env, config)); + /** + Loads the configuration for this project and its addons. + + @public + @method config + @param {String} env Environment name + @return {Object} Merged configuration object + */ + config(env) { + let _env = env === undefined ? process.env.EMBER_ENV : env; + let key = `${this.configPath()}|${_env}`; + let config = this.configCache.get(key); + if (config === undefined) { + config = this.configWithoutCache(_env); + this.configCache.set(key, config); } + return cloneDeep(config); + } - return config; - }, initialConfig); -}; + /** + * @private + * @method configWithoutCache + * @param {String} env Environment name + * @return {Object} Merged configuration object + */ + configWithoutCache(env) { + let configPath = this.configPath(); -/** - Returns whether or not the given file name is present in this project. + if (fs.existsSync(`${configPath}.js`)) { + let appConfig = this.require(configPath)(env); + let addonsConfig = this.getAddonsConfig(env, appConfig); - @private - @method has - @param {String} file File name - @return {Boolean} Whether or not the file is present - */ -Project.prototype.has = function(file) { - return existsSync(path.join(this.root, file)) || existsSync(path.join(this.root, file + '.js')); -}; + return merge(addonsConfig, appConfig); + } else { + return this.getAddonsConfig(env, {}); + } + } -/** - Resolves the absolute path to a file. - - @private - @method resolve - @param {String} file File to resolve - @return {String} Absolute path to file - */ -Project.prototype.resolve = function(file) { - return resolve(file, { - basedir: this.root - }); -}; + /** + Returns the targets of this project, or the default targets if not present. -/** - Calls `require` on a given module. + @public + @property targets + @return {Object} Targets object + */ + get targets() { + if (this._targets) { + return this._targets; + } + let configPath = 'config'; - @private - @method require - @param {String} file File path or module name - @return {Object} Imported module - */ -Project.prototype.require = function(file) { - if (/^\.\//.test(file)) { // Starts with ./ - return require(path.join(this.root, file)); - } else { - return require(path.join(this.nodeModulesPath, file)); + if (this.pkg['ember-addon'] && this.pkg['ember-addon']['configPath']) { + configPath = this.pkg['ember-addon']['configPath']; + } + + let targetsPath = path.join(this.root, configPath, 'targets'); + + if (fs.existsSync(`${targetsPath}.js`)) { + this._targets = this.require(targetsPath); + } else { + this._targets = { browsers: ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'] }; + } + return this._targets; } -}; + /** + Returns the addons configuration. -Project.prototype.emberCLIVersion = emberCLIVersion; + @private + @method getAddonsConfig + @param {String} env Environment name + @param {Object} appConfig Application configuration + @return {Object} Merged configuration of all addons + */ + getAddonsConfig(env, appConfig) { + this.initializeAddons(); -/** - Returns the dependencies from a package.json + let initialConfig = merge({}, appConfig); - @private - @method dependencies - @param {Object} pkg Package object. If false, the current package is used. - @param {Boolean} excludeDevDeps Whether or not development dependencies should be excluded, defaults to false. - @return {Object} Dependencies - */ -Project.prototype.dependencies = function(pkg, excludeDevDeps) { - pkg = pkg || this.pkg || {}; + return this.addons.reduce((config, addon) => { + if (addon.config) { + merge(config, addon.config(env, config)); + } - var devDependencies = pkg['devDependencies']; - if (excludeDevDeps) { - devDependencies = {}; + return config; + }, initialConfig); } - return assign({}, devDependencies, pkg['dependencies']); -}; + /** + Returns whether or not the given file name is present in this project. -/** - Returns the bower dependencies for this project. + @private + @method has + @param {String} file File name + @return {Boolean} Whether or not the file is present + */ + has(file) { + return fs.existsSync(path.join(this.root, file)) || fs.existsSync(path.join(this.root, `${file}.js`)); + } - @private - @method bowerDependencies - @param {String} bower Path to bower.json - @return {Object} Bower dependencies - */ -Project.prototype.bowerDependencies = function(bower) { - if (!bower) { - var bowerPath = path.join(this.root, 'bower.json'); - bower = (existsSync(bowerPath)) ? require(bowerPath) : {}; + /** + Resolves the absolute path to a file synchronously + + @private + @method resolveSync + @param {String} file File to resolve + @return {String} Absolute path to file + */ + resolveSync(file) { + return resolve.sync(file, { + basedir: process.env.EMBER_NODE_PATH || this.root, + }); } - return assign({}, bower['devDependencies'], bower['dependencies']); -}; -/** - Provides the list of paths to consult for addons that may be provided - internally to this project. Used for middleware addons with built-in support. + /** + Calls `require` on a given module from the context of the project. For + instance, an addon may want to require a class from the root project's + version of ember-cli. + + @public + @method require + @param {String} file File path or module name + @return {Object} Imported module + */ + require(file) { + let path = this.resolveSync(file); + return require(path); + } - @private - @method supportedInternalAddonPaths -*/ -Project.prototype.supportedInternalAddonPaths = function(){ - if (!this.root) { return []; } + /** + Returns the dependencies from a package.json + + @private + @method dependencies + @param {Object} [pkg=this.pkg] Package object + @param {Boolean} [excludeDevDeps=false] Whether or not development dependencies should be excluded + @return {Object} Dependencies + */ + dependencies(pkg, excludeDevDeps) { + pkg = pkg || this.pkg || {}; + + let devDependencies = pkg['devDependencies']; + if (excludeDevDeps) { + devDependencies = {}; + } - var internalMiddlewarePath = path.join(__dirname, '../tasks/server/middleware'); + return Object.assign({}, devDependencies, pkg['dependencies']); + } - return [ - path.join(internalMiddlewarePath, 'tests-server'), - path.join(internalMiddlewarePath, 'history-support'), - path.join(internalMiddlewarePath, 'serve-files'), - path.join(internalMiddlewarePath, 'proxy-server') - ]; -}; + /** + Provides the list of paths to consult for addons that may be provided + internally to this project. Used for middleware addons with built-in support. -/** - Discovers all addons for this project and stores their names and - package.json contents in this.addonPackages as key-value pairs + @private + @method supportedInternalAddonPaths + */ + supportedInternalAddonPaths() { + if (!this.root) { + return []; + } - @private - @method discoverAddons - */ -Project.prototype.discoverAddons = function() { - var addonsList = this.addonDiscovery.discoverProjectAddons(this); + let internalMiddlewarePath = path.join(__dirname, '../tasks/server/middleware'); + let internalTransformPath = path.join(__dirname, '../tasks/transforms'); + + return [ + path.join(internalMiddlewarePath, 'testem-url-rewriter'), + path.join(internalMiddlewarePath, 'tests-server'), + path.join(internalMiddlewarePath, 'history-support'), + path.join(internalMiddlewarePath, 'broccoli-watcher'), + path.join(internalMiddlewarePath, 'broccoli-serve-files'), + path.join(internalMiddlewarePath, 'proxy-server'), + path.join(internalTransformPath, 'amd'), + ]; + } - this.addonPackages = this.addonDiscovery.addonPackages(addonsList); -}; + /** + * Discovers all addons for this project and stores their names and + * package.json contents in this.addonPackages as key-value pairs. + * + * Any packageInfos that we find that are marked as not valid are excluded. + * + * @private + * @method discoverAddons + */ + discoverAddons() { + if (this._didDiscoverAddons) { + return; + } + this._didDiscoverAddons = true; -/** - Loads and initializes all addons for this project. + let addonPackageList = this._packageInfo.discoverProjectAddons(); + this.addonPackages = this._packageInfo.generateAddonPackages(addonPackageList); - @private - @method initializeAddons - */ -Project.prototype.initializeAddons = function() { - if (this._addonsInitialized) { - return; + // in case any child addons are invalid, dump to the console about them. + this._packageInfo.dumpInvalidAddonPackages(addonPackageList); } - this._addonsInitialized = true; - debug('initializeAddons for: %s', this.name()); + /** + Loads and initializes all addons for this project. + + @private + @method initializeAddons + */ + initializeAddons() { + if (this._addonsInitialized) { + return; + } + this._addonsInitialized = true; + this._didDiscoverAddons = false; - this.discoverAddons(); + logger.info('initializeAddons for: %s', this.name()); - this.addons = this.addonsFactory.initializeAddons(this.addonPackages); + this.discoverAddons(); - this.addons.forEach(function(addon) { - debug('addon: %s', addon.name); - }); -}; + this.addons = instantiateAddons(this, this, this.addonPackages); + this.addons.forEach((addon) => logger.info('addon: %s', addon.name)); + } -/** - Returns what commands are made available by addons by inspecting - `includedCommands` for every addon. - - @private - @method addonCommands - @return {Object} Addon names and command maps as key-value pairs - */ -Project.prototype.addonCommands = function() { - var commands = {}; - this.addons.forEach(function(addon){ - var includedCommands = (addon.includedCommands && addon.includedCommands()) || {}; - var addonCommands = {}; - - for (var key in includedCommands) { - if (typeof includedCommands[key] === 'function') { - addonCommands[key] = includedCommands[key]; - } else { - addonCommands[key] = Command.extend(includedCommands[key]); + /** + Returns what commands are made available by addons by inspecting + `includedCommands` for every addon. + + @private + @method addonCommands + @return {Object} Addon names and command maps as key-value pairs + */ + addonCommands() { + const Command = require('../models/command'); + let commands = Object.create(null); + this.addons.forEach((addon) => { + if (!addon.includedCommands) { + return; } - } - if(Object.keys(addonCommands).length) { - commands[addon.name] = addonCommands; - } - }); - return commands; -}; -/** - Execute a given callback for every addon command. - Example: - - ``` - project.eachAddonCommand(function(addonName, commands) { - console.log('Addon ' + addonName + ' exported the following commands:' + commands.keys().join(', ')); - }); - ``` - - @private - @method eachAddonCommand - @param {Function} callback [description] - */ -Project.prototype.eachAddonCommand = function(callback) { - if (this.initializeAddons && this.addonCommands) { - this.initializeAddons(); - var addonCommands = this.addonCommands(); + let token = heimdall.start({ + name: `lookup-commands: ${addon.name}`, + addonName: addon.name, + addonCommandInitialization: true, + }); + + let includedCommands = addon.includedCommands(); + let addonCommands = Object.create(null); + + for (let key in includedCommands) { + if (typeof includedCommands[key] === 'function') { + addonCommands[key] = includedCommands[key]; + } else { + addonCommands[key] = Command.extend(includedCommands[key]); + } + } + if (Object.keys(addonCommands).length) { + commands[addon.name] = addonCommands; + } - forOwn(addonCommands, function(commands, addonName) { - return callback(addonName, commands); + token.stop(); }); + return commands; } -}; -/** - Path to the blueprints for this project. - - @private - @method localBlueprintLookupPath - @return {String} Path to blueprints - */ -Project.prototype.localBlueprintLookupPath = function() { - return path.join(this.root, 'blueprints'); -}; + /** + Execute a given callback for every addon command. + Example: -/** - Returns a list of paths (including addon paths) where blueprints will be looked up. + ``` + project.eachAddonCommand(function(addonName, commands) { + console.log('Addon ' + addonName + ' exported the following commands:' + commands.keys().join(', ')); + }); + ``` + + @private + @method eachAddonCommand + @param {Function} callback [description] + */ + eachAddonCommand(callback) { + if (this.initializeAddons && this.addonCommands) { + this.initializeAddons(); + let addonCommands = this.addonCommands(); + + forOwn(addonCommands, (commands, addonName) => callback(addonName, commands)); + } + } - @private - @method blueprintLookupPaths - @return {Array} List of paths - */ -Project.prototype.blueprintLookupPaths = function() { - if (this.isEmberCLIProject()) { - var lookupPaths = [this.localBlueprintLookupPath()]; - var addonLookupPaths = this.addonBlueprintLookupPaths(); + /** + Path to the blueprints for this project. - return lookupPaths.concat(addonLookupPaths); - } else { - return this.addonBlueprintLookupPaths(); + @private + @method localBlueprintLookupPath + @return {String} Path to blueprints + */ + localBlueprintLookupPath() { + return path.join(this.root, 'blueprints'); } -}; -/** - Returns a list of addon paths where blueprints will be looked up. - - @private - @method addonBlueprintLookupPaths - @return {Array} List of paths - */ -Project.prototype.addonBlueprintLookupPaths = function() { - var addonPaths = this.addons.map(function(addon) { - if (addon.blueprintsPath) { - return addon.blueprintsPath(); + /** + Returns a list of paths (including addon paths) where blueprints will be looked up. + + @private + @method blueprintLookupPaths + @return {Array} List of paths + */ + blueprintLookupPaths() { + if (this.isEmberCLIProject()) { + let lookupPaths = [this.localBlueprintLookupPath()]; + let addonLookupPaths = this.addonBlueprintLookupPaths(); + + return lookupPaths.concat(addonLookupPaths); + } else { + return this.addonBlueprintLookupPaths(); } - }, this); + } - return addonPaths.filter(Boolean).reverse(); -}; + /** + Returns a list of addon paths where blueprints will be looked up. + + @private + @method addonBlueprintLookupPaths + @return {Array} List of paths + */ + addonBlueprintLookupPaths() { + let addonPaths = this.addons + .reduce((sum, addon) => { + if (addon.blueprintsPath) { + let val = addon.blueprintsPath(); + if (val) { + sum.push(val); + } + } + return sum; + }, []) + .reverse(); + + return addonPaths; + } -/** - Reloads package.json + /** + Reloads package.json of the project. Clears and reloads the packageInfo and + per-bundle addon cache, too. - @private - @method reloadPkg - @return {Object} Package content - */ -Project.prototype.reloadPkg = function() { - var pkgPath = path.join(this.root, 'package.json'); + @private + @method reloadPkg + @return {Object} Package content + */ + reloadPkg() { + let pkgPath = path.join(this.root, 'package.json'); - // We use readFileSync instead of require to avoid the require cache. - this.pkg = JSON.parse(fs.readFileSync(pkgPath, { encoding: 'utf-8' })); + // We use readFileSync instead of require to avoid the require cache. + this.pkg = fs.readJsonSync(pkgPath); - return this.pkg; -}; + this.packageInfoCache.reloadProjects(); -/** - Re-initializes addons. + // update `_packageInfo` after reloading projects from the `PackageInfoCache` instance + // if we don't do this we get into a state where `_packageInfo` is referencing the old + // pkginfo object that hasn't been updated/reloaded + this._packageInfo = this.packageInfoCache.loadProject(this); - @private - @method reloadAddons - */ -Project.prototype.reloadAddons = function() { - this.reloadPkg(); - this._addonsInitialized = false; - return this.initializeAddons(); -}; + if (PerBundleAddonCache.isEnabled()) { + this.perBundleAddonCache = new PerBundleAddonCache(this); + } -/** - Find an addon by its name + return this.pkg; + } + + /** + Re-initializes addons. + + @private + @method reloadAddons + */ + reloadAddons() { + this.reloadPkg(); + this._addonsInitialized = false; + this._didDiscoverAddons = false; + return this.initializeAddons(); + } - @private - @method findAddonByName - @param {String} name Addon name as specified in package.json - @return {Addon} Addon instance - */ -Project.prototype.findAddonByName = function(name) { - this.initializeAddons(); + /** + Find an addon by its name - var exactMatch = find(this.addons, function(addon) { - return name === addon.name || name === addon.pkg.name; - }); + @public + @method findAddonByName + @param {String} name Addon name as specified in package.json + @return {Addon} Addon instance + */ + findAddonByName(name) { + this.initializeAddons(); - if (exactMatch) { - return exactMatch; + return this.addons.find((addon) => addon.pkg?.name === name); } - return find(this.addons, function(addon) { - return name.indexOf(addon.name) > -1 || name.indexOf(addon.pkg.name) > -1; - }); -}; + /** + Generate test file contents. -/** - Returns a new project based on the first package.json that is found - in `pathName`. - - @private - @static - @method closest - @param {String} pathName Path to your project - @return {Promise} Promise which resolves to a {Project} - */ -Project.closest = function(pathName, _ui, _cli) { - var ui = ensureUI(_ui); - return closestPackageJSON(pathName) - .then(function(result) { - debug('closest %s -> %s', pathName, result); - if (result.pkg && result.pkg.name === 'ember-cli') { - return Project.nullProject(_ui, _cli); + This method is supposed to be overwritten by test framework addons + like `ember-qunit`. + + @public + @method generateTestFile + @param {String} moduleName Name of the test module (e.g. `JSHint`) + @param {Object[]} tests Array of tests with `name`, `passed` and `errorMessage` properties + @return {String} The test file content + */ + generateTestFile() { + let message = 'Please install an Ember.js test framework addon or update your dependencies.'; + + if (this.ui) { + this.ui.writeDeprecateLine(message); + } else { + console.warn(message); + } + + return ''; + } + + /** + Returns a new project based on the first `package.json` that is found + in `pathName`. + + If the above `package.json` specifies `ember-addon.projectRoot`, we load + the project based on the relative path between this directory and the + specified `projectRoot`. + + @private + @static + @method closestSync + @param {String} pathName Path to your project + @param {UI} _ui The UI instance to provide to the created Project. + @return {Project} Project instance + */ + static closestSync(pathName, _ui, _cli) { + logger.info('looking for package.json starting at %s', pathName); + + let ui = ensureUI(_ui); + + let directory = findupPath(pathName); + let pkg = fs.readJsonSync(path.join(directory, 'package.json')); + logger.info('found package.json at %s', directory); + + // allow `package.json` files to specify where the actual project lives + if (pkg && pkg['ember-addon'] && typeof pkg['ember-addon'].projectRoot === 'string') { + if (fs.existsSync(path.join(directory, 'ember-cli-build.js'))) { + throw new Error( + `Both \`ember-addon.projectRoot\` and \`ember-cli-build.js\` exist as part of \`${directory}\`` + ); } - return new Project(result.directory, result.pkg, ui, _cli); - }) - .catch(function(reason) { - handleFindupError(pathName, reason); - }); -}; + return Project.closestSync(path.join(directory, pkg['ember-addon'].projectRoot), _ui, _cli); + } -/** - Returns a new project based on the first package.json that is found - in `pathName`. - - @private - @static - @method closestSync - @param {String} pathName Path to your project - @param {UI} _ui The UI instance to provide to the created Project. - @return {Project} Project instance - */ -Project.closestSync = function(pathName, _ui, _cli) { - var ui = ensureUI(_ui); - - try { - var directory = findup.sync(pathName, 'package.json'); - var pkg = require(path.join(directory, 'package.json')); + let relative = path.relative(directory, pathName); + if (relative.indexOf('tmp') === 0) { + logger.info('ignoring parent project since we are in the tmp folder of the project'); + return Project.nullProject(_ui, _cli); + } - if (pkg && pkg.name === 'ember-cli') { + logger.info('project name: %s', pkg && pkg.name); + + if (!isEmberCliProject(pkg)) { + logger.info('ignoring parent project since it is not an ember-cli project'); return Project.nullProject(_ui, _cli); } - debug('closestSync %s -> %s', pathName, directory); return new Project(directory, pkg, ui, _cli); - } catch(reason) { - handleFindupError(pathName, reason); } -}; -/** - Returns a new project based on the first package.json that is found - in `pathName`, or the nullProject. - - The nullProject signifies no-project, but abides by the null object pattern - - @private - @static - @method projectOrnullProject - @param {UI} _ui The UI instance to provide to the created Project. - @return {Project} Project instance - */ -Project.projectOrnullProject = function(_ui, _cli) { - try { - return Project.closestSync(process.cwd(), _ui, _cli); - } catch(reason) { - if (reason instanceof Project.NotFoundError) { - return Project.nullProject(_ui, _cli); - } else { - throw reason; + /** + Returns a new project based on the first package.json that is found + in `pathName`, or the nullProject. + + The nullProject signifies no-project, but abides by the null object pattern + + @private + @static + @method projectOrnullProject + @param {UI} _ui The UI instance to provide to the created Project. + @return {Project} Project instance + */ + static projectOrnullProject(_ui, _cli) { + try { + return Project.closestSync(process.cwd(), _ui, _cli); + } catch (reason) { + if (reason instanceof Project.NotFoundError) { + return Project.nullProject(_ui, _cli); + } else { + throw reason; + } } } -}; -/** - Returns the project root based on the first package.json that is found + /** + Returns the project root based on the first package.json that is found + + @static + @method getProjectRoot + @return {String} The project root directory + */ + static getProjectRoot() { + let packagePath = findUpSync('package.json'); + if (!packagePath) { + logger.info('getProjectRoot: not found. Will use cwd: %s', process.cwd()); + return process.cwd(); + } - @return {String} The project root directory - */ -Project.getProjectRoot = function () { - try { - var directory = findup.sync(process.cwd(), 'package.json'); - var pkg = require(path.join(directory, 'package.json')); + let directory = path.dirname(packagePath); + const pkg = require(packagePath); if (pkg && pkg.name === 'ember-cli') { - debug('getProjectRoot: named \'ember-cli\'. Will use cwd: %s', process.cwd()); + logger.info("getProjectRoot: named 'ember-cli'. Will use cwd: %s", process.cwd()); return process.cwd(); } - debug('getProjectRoot %s -> %s', process.cwd(), directory); + logger.info('getProjectRoot %s -> %s', process.cwd(), directory); return directory; - } catch(reason) { - if (isFindupError(reason)) { - debug('getProjectRoot: not found. Will use cwd: %s', process.cwd()); - return process.cwd(); - } else{ - throw reason; - } } -}; - -function NotFoundError(message) { - this.name = 'NotFoundError'; - this.message = message; - this.stack = (new Error()).stack; } -NotFoundError.prototype = Object.create(Error.prototype); -NotFoundError.prototype.constructor = NotFoundError; +class NotFoundError extends Error { + constructor(message) { + super(message); + this.name = 'NotFoundError'; + this.stack = new Error().stack; + } +} Project.NotFoundError = NotFoundError; +function ensureInstrumentation(cli, ui) { + if (cli && cli.instrumentation) { + return cli.instrumentation; + } + + // Instrumentation `require` needs to occur inline due to circular dependencies between + // Instrumentation => getConfig => Project => Instrumentation. getConfig is used in Project to + // get the project root. + let Instrumentation = require('./instrumentation'); + // created without a `cli` object (possibly from deprecated `Brocfile.js`) + return new Instrumentation({ ui, initInstrumentation: null }); +} + function ensureUI(_ui) { - var ui = _ui; + let ui = _ui; if (!ui) { // TODO: one UI (lib/cli/index.js also has one for now...) + const UI = require('console-ui'); ui = new UI({ - inputStream: process.stdin, + inputStream: process.stdin, outputStream: process.stdout, - ci: process.env.CI || /^(dumb|emacs)$/.test(process.env.TERM), - writeLevel: ~process.argv.indexOf('--silent') ? 'ERROR' : undefined + ci: process.env.CI || /^(dumb|emacs)$/.test(process.env.TERM), + writeLevel: process.argv.indexOf('--silent') !== -1 ? 'ERROR' : undefined, }); } return ui; } -function closestPackageJSON(pathName) { - return findup(pathName, 'package.json') - .then(function(directory) { - return Promise.hash({ - directory: directory, - pkg: require(path.join(directory, 'package.json')) - }); - }); -} +function findupPath(pathName) { + let pkgPath = findUpSync('package.json', { cwd: pathName }); + if (!pkgPath) { + throw new NotFoundError(`No project found at or up from: \`${pathName}\``); + } -function isFindupError(reason) { - // Would be nice if findup threw error subclasses - return reason && /not found/i.test(reason.message); + return path.dirname(pkgPath); } -function handleFindupError(pathName, reason) { - if (isFindupError(reason)) { - throw new NotFoundError('No project found at or up from: `' + pathName + '`'); - } else { - throw reason; - } +function isEmberCliProject(pkg) { + return ( + pkg && + ((pkg.dependencies && Object.keys(pkg.dependencies).indexOf('ember-cli') !== -1) || + (pkg.devDependencies && Object.keys(pkg.devDependencies).indexOf('ember-cli') !== -1)) + ); } // Export diff --git a/lib/models/server-watcher.js b/lib/models/server-watcher.js index 129a0f1007..ad0fc0e135 100644 --- a/lib/models/server-watcher.js +++ b/lib/models/server-watcher.js @@ -1,61 +1,34 @@ 'use strict'; -var Task = require('./task'); +const Watcher = require('./watcher'); -module.exports = Task.extend({ - verbose: true, +module.exports = class ServerWatcher extends Watcher { + static async build(options, build) { + let { watcher: instance } = await super.build(options, build); - init: function() { - this.watcher = this.watcher || new (require('sane'))(this.watchedDir, { - verbose: this.verbose, - poll: this.polling() - }); + instance.watcher.on('add', instance.didAdd.bind(instance)); + instance.watcher.on('delete', instance.didDelete.bind(instance)); + return { watcher: instance }; + } + + constructBroccoliWatcher(options) { + return new (require('sane'))(this.watchedDir, options); + } + + setupBroccoliChangeEvent() { this.watcher.on('change', this.didChange.bind(this)); - this.watcher.on('add', this.didAdd.bind(this)); - this.watcher.on('delete', this.didDelete.bind(this)); - }, - - didChange: function (filepath) { - this.ui.writeLine('Server file changed: ' + filepath); - - this.analytics.track({ - name: 'server file change', - description: 'File changed: "' + filepath + '"' - }); - }, - - didAdd: function (filepath) { - this.ui.writeLine('Server file added: ' + filepath); - - this.analytics.track({ - name: 'server file addition', - description: 'File added: "' + filepath + '"' - }); - }, - - didDelete: function (filepath) { - this.ui.writeLine('Server file deleted: ' + filepath); - - this.analytics.track({ - name: 'server file deletion', - description: 'File deleted: "' + filepath + '"' - }); - }, - - then: function() { - return this.watcher.then.apply(this.watcher, arguments); - }, - - on: function() { - this.watcher.on.apply(this.watcher, arguments); - }, - - off: function() { - this.watcher.off.apply(this.watcher, arguments); - }, - - polling: function () { - return this.options && this.options.watcher === 'polling'; } -}); + + didChange(relativePath) { + this.ui.writeLine(`File changed: "${relativePath}"`); + } + + didAdd(relativePath) { + this.ui.writeLine(`File added: "${relativePath}"`); + } + + didDelete(relativePath) { + this.ui.writeLine(`File deleted: "${relativePath}"`); + } +}; diff --git a/lib/models/task.js b/lib/models/task.js index 09754c95e1..74dcba2339 100644 --- a/lib/models/task.js +++ b/lib/models/task.js @@ -1,15 +1,23 @@ 'use strict'; -var CoreObject = require('core-object'); +const CoreObject = require('core-object'); -function Task() { - CoreObject.apply(this, arguments); +class Task extends CoreObject { + run(/*options*/) { + throw new Error('Task needs to have run() defined.'); + } + + /** + * Interrupt comamd with an exit code + * Called when the process is interrupted from outside, e.g. CTRL+C or `process.kill()` + * + * @private + * @method onInterrupt + */ + onInterrupt() { + // eslint-disable-next-line n/no-process-exit + process.exit(1); + } } module.exports = Task; - -Task.__proto__ = CoreObject; - -Task.prototype.run = function(/*options*/) { - throw new Error('Task needs to have run() defined.'); -}; diff --git a/lib/models/update-checker.js b/lib/models/update-checker.js deleted file mode 100644 index 569c84284b..0000000000 --- a/lib/models/update-checker.js +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; - -var Promise = require('../ext/promise'); -var versionUtils = require('../utilities/version-utils'); -var chalk = require('chalk'); -var debug = require('debug')('ember-cli:update-checker'); - -module.exports = UpdateChecker; - -function UpdateChecker(ui, settings, localVersion) { - this.ui = ui; - this.settings = settings; - this.localVersion = localVersion || versionUtils.emberCLIVersion(); - this.versionConfig = null; - - debug('version: %s', this.localVersion); - debug('version: %o', this.settings); -} - -/** -* Checks local config or npm for most recent version of ember-cli -*/ -UpdateChecker.prototype.checkForUpdates = function() { - // if 'checkForUpdates' is true, check for an updated ember-cli version - debug('checkingcheckForUpdates: %o', this.settings.checkingcheckForUpdates); - if (this.settings.checkForUpdates) { - return this.doCheck().then(function(updateInfo) { - debug('updatedNeeded %o', updateInfo.updateNeeded); - if (updateInfo.updateNeeded) { - this.ui.writeLine(''); - this.ui.writeLine('A new version of ember-cli is available (' + - updateInfo.newestVersion + ').'); - } - return updateInfo; - }.bind(this)); - } else { - return Promise.resolve({ - updateNeeded: false - }); - } -}; - -UpdateChecker.prototype.doCheck = function() { - this.versionConfig = this.versionConfig || new (require('configstore'))('ember-cli-version'); - var lastVersionCheckAt = this.versionConfig.get('lastVersionCheckAt'); - var now = new Date().getTime(); - - return new Promise(function(resolve, reject) { - // if the last check was less than a day ago, don't remotely check version - if (lastVersionCheckAt && lastVersionCheckAt > (now - 86400000)) { - resolve(this.versionConfig.get('newestVersion')); - } - - reject(); - }.bind(this)).catch(function() { - return this.checkNPM(); - }.bind(this)).then(function(version) { - var isDevBuild = versionUtils.isDevelopment(this.localVersion); - var updateNeeded = false; - - if (!isDevBuild) { - updateNeeded = version && require('semver').lt(this.localVersion, version); - } - - return { - updateNeeded: updateNeeded, - newestVersion: version - }; - }.bind(this)); -}; - -UpdateChecker.prototype.checkNPM = function() { - // use npm to determine the currently availabe ember-cli version - var loadNPM = Promise.denodeify(require('npm').load); - var fetchEmberCliVersion = function(npm){ - return Promise.denodeify(npm.commands.view)(['ember-cli', 'version']); - }; - var extractVersion = function(data) { return Object.keys(data)[0]; }; - - return loadNPM({ - 'loglevel': 'silent', - 'fetch-retries': 1, - 'cache-min': 1, - 'cache-max': 0 - }) - .then(fetchEmberCliVersion) - .then(extractVersion) - .then(this.saveVersionInformation.bind(this)) - .catch(function(){ - this.ui.writeLine(chalk.red('An error occurred while checking for Ember CLI updates. ' + - 'Please verify your internet connectivity and npm configurations.')); - return false; - }.bind(this)); -}; - -UpdateChecker.prototype.saveVersionInformation = function(version) { - var versionConfig = this.versionConfig; - var now = new Date().getTime(); - - // save version so we don't have to check again for another day - versionConfig.set('newestVersion', version); - versionConfig.set('lastVersionCheckAt', now); -}; diff --git a/lib/models/watcher.js b/lib/models/watcher.js index 9c148a6309..516a5cc458 100644 --- a/lib/models/watcher.js +++ b/lib/models/watcher.js @@ -1,134 +1,174 @@ 'use strict'; -var chalk = require('chalk'); -var Task = require('./task'); -var debug = require('debug')('ember-cli:watcher'); -var Promise = require('../ext/promise'); -var exec = Promise.denodeify(require('child_process').exec); -var isWin = /^win/.test(process.platform); +const { default: chalk } = require('chalk'); +const logger = require('heimdalljs-logger')('ember-cli:watcher'); +const CoreObject = require('core-object'); +const serveURL = require('../utilities/get-serve-url'); +const printSlowTrees = require('broccoli-slow-trees'); + +const eventTypeNormalization = { + add: 'added', + delete: 'deleted', + change: 'changed', +}; + +const ConstructedFromBuilder = Symbol('Watcher.build'); + +module.exports = class Watcher extends CoreObject { + constructor(_options, build) { + if (build !== ConstructedFromBuilder) { + throw new Error('instantiate Watcher with (await Watcher.build()).watcher, not new Watcher()'); + } + + super(_options); + + this.verbose = true; + this.serving = _options.serving; + } + + static async build(_options) { + let watcher = new this(_options, ConstructedFromBuilder); + await watcher.setupBroccoliWatcher(_options); -var Watcher = Task.extend({ - verbose: true, + // This indirection is because Watcher instances are themselves spec + // noncompliant thennables (see the then() method) so returning watcher + // directly will interfere with `await Watcher.build()` + return { watcher }; + } - init: function() { - var options = this.buildOptions(); + async setupBroccoliWatcher() { + let options = this.buildOptions(); - debug('initialize %o', options); + logger.info('initialize %o', options); + this.watcher = this.watcher || (await this.constructBroccoliWatcher(options)); - this.watcher = this.watcher || new (require('broccoli-sane-watcher'))(this.builder, options); + this.setupBroccoliChangeEvent(); + this.watcher.on('buildStart', this._setupBroccoliWatcherBuild.bind(this)); + this.watcher.on('buildSuccess', this.didChange.bind(this)); + // build errors arriving via watcher + this.watcher.on('buildFailure', this.didError.bind(this)); + // watcher specific errors this.watcher.on('error', this.didError.bind(this)); - this.watcher.on('change', this.didChange.bind(this)); - }, - didError: function(error) { - debug('didError %o', error); + this.serveURL = serveURL; + } + + async constructBroccoliWatcher(options) { + const { Watcher } = require('broccoli'); + await this.builder.ensureBroccoliBuilder(); + const { watchedSourceNodeWrappers } = this.builder.builder; + + let watcher = new Watcher(this.builder, watchedSourceNodeWrappers, { saneOptions: options, ignored: this.ignored }); + + watcher.start(); + + return watcher; + } + + setupBroccoliChangeEvent() { + // This is to keep backwards compatibility with broccoli-sane-watcher. + // https://github.com/ember-cli/broccoli-sane-watcher/blob/48860/index.js#L158 + if (this.verbose) { + this.watcher.on('change', (event, filePath) => { + this.ui.writeLine(`file ${eventTypeNormalization[event]} ${filePath}`); + }); + } + } + + async _setupBroccoliWatcherBuild() { + try { + const hash = await this.watcher.currentBuild; + hash.graph.__heimdall__.remove(); + } catch (e) { + if (e !== null && typeof e === 'object' && e.isBuilderError === true) { + // e must be a builder error which we can safely ignore. + // + // Builder errors are expected failures which are handled and presented + // to the user else-where. To prevent double reporting faliures to the + // user, it is important that we ignore them here as this functions + // single responsibilty is to reset the himedall state after each build. + // + // exists to prune heimdall data after a build. Those errors are + // reported elsewhere. + // + } else { + // this error is unexpected, we must re-throw as we cannot be sure it + // has been handled else-where + throw e; + } + } + } + + _totalTime(hash) { + const sumNodes = (node, cb) => { + let total = 0; + node.visitPreOrder((node) => { + total += cb(node); + }); + return total; + }; + + return sumNodes(hash.graph.__heimdall__, (node) => node.stats.time.self); + } + + didError(error) { + logger.info('didError %o', error); this.ui.writeError(error); - this.analytics.trackError({ - description: error && error.message - }); - }, + } - then: function() { - return this.watcher.then.apply(this.watcher, arguments); - }, + didChange(results) { + logger.info('didChange %o', results); - didChange: function(results) { - debug('didChange %o', results); - var totalTime = results.totalTime / 1e6; + results.totalTime = this._totalTime(results); + let totalTime = results.totalTime / 1e6; + let message = chalk.green(`Build successful (${Math.round(totalTime)}ms)`); this.ui.writeLine(''); - this.ui.writeLine(chalk.green('Build successful - ' + Math.round(totalTime) + 'ms.')); - - this.analytics.track({ - name: 'ember rebuild', - message: 'broccoli rebuild time: ' + totalTime + 'ms' - }); - - this.analytics.trackTiming({ - category: 'rebuild', - variable: 'rebuild time', - label: 'broccoli rebuild time', - value: parseInt(totalTime, 10) - }); - }, - - on: function() { - this.watcher.on.apply(this.watcher, arguments); - }, - - off: function() { - this.watcher.off.apply(this.watcher, arguments); - }, - buildOptions: function() { - var watcher = this.options && this.options.watcher; + + if (this.serving) { + message += ` – Serving on ${this.serveURL(this.options, this.options.project)}`; + } + + this.ui.writeLine(message); + + if (this.verbose) { + printSlowTrees(results.graph.__heimdall__); + } + } + + buildOptions() { + let watcher = this.options && this.options.watcher; if (watcher && ['polling', 'watchman', 'node', 'events'].indexOf(watcher) === -1) { - throw new Error('Unknown watcher type --watcher=[polling|watchman|node] but was: ' + watcher); + throw new Error(`Unknown watcher type --watcher=[polling|watchman|node|events] but was: ${watcher}`); } return { - verbose: this.verbose, - poll: watcher === 'polling', + verbose: this.verbose, + poll: watcher === 'polling', watchman: watcher === 'watchman' || watcher === 'events', - node: watcher === 'node' + node: watcher === 'node', }; } -}); - -Watcher.detectWatcher = function(ui, _options) { - var options = _options || {}; - var watchmanInfo = 'Visit http://www.ember-cli.com/user-guide/#watchman for more info.'; - - if (options.watcher === 'polling') { - debug('skip detecting watchman, poll instead'); - return Promise.resolve(options); - } else if (options.watcher === 'node') { - debug('skip detecting watchman, node instead'); - return Promise.resolve(options); - } else if (isWin) { - debug('watchman isn\'t supported on windows, node instead'); - options.watcher = 'node'; - return Promise.resolve(options); - } else { - debug('detecting watchman'); - return exec('watchman version').then(function(output) { - var version; - try { - version = JSON.parse(output).version; - } catch(e) { - options.watcher = 'node'; - ui.writeLine('Looks like you have a different program called watchman, falling back to NodeWatcher.'); - ui.writeLine(watchmanInfo); - return options; - } - debug('detected watchman: %s', version); - - var semver = require('semver'); - if (semver.satisfies(version, '^3.0.0')) { - debug('watchman %s does satisfy: %s', version, '^3.0.0'); - options.watcher = 'watchman'; - options._watchmanInfo = { - enabled: true, - version: version, - canNestRoots: semver.satisfies(version, '^3.7.0') - }; - } else { - debug('watchman %s does NOT satisfy: %s', version, '^3.0.0'); - ui.writeLine('Invalid watchman found, version: [' + version + '] did not satisfy [^3.0.0], falling back to NodeWatcher.'); - ui.writeLine(watchmanInfo); - options.watcher = 'node'; - } - return options; - }, function(reason) { - debug('detecting watchman failed %o', reason); - ui.writeLine('Could not find watchman, falling back to NodeWatcher for file system events.'); - ui.writeLine(watchmanInfo); - options.watcher = 'node'; - return options; - }); + then() { + return this.watcher.currentBuild.then.apply(this.watcher.currentBuild, arguments); } -}; -module.exports = Watcher; + on() { + const args = arguments; + if (args[0] === 'change' && !this.watchedDir) { + args[0] = 'buildSuccess'; + } + this.watcher.on.apply(this.watcher, args); + } + + off() { + const args = arguments; + if (args[0] === 'change' && !this.watchedDir) { + args[0] = 'buildSuccess'; + } + this.watcher.off.apply(this.watcher, args); + } +}; diff --git a/lib/models/worker.js b/lib/models/worker.js new file mode 100644 index 0000000000..12c7430e90 --- /dev/null +++ b/lib/models/worker.js @@ -0,0 +1,9 @@ +'use strict'; + +const { worker } = require('workerpool'); +const gzipStats = require('./gzipStats'); + +// create worker and register public functions +worker({ + gzipStats, +}); diff --git a/lib/tasks/addon-install.js b/lib/tasks/addon-install.js index b844b6f98b..ca3f8a5676 100644 --- a/lib/tasks/addon-install.js +++ b/lib/tasks/addon-install.js @@ -1,81 +1,101 @@ 'use strict'; -var Task = require('../models/task'); -var SilentError = require('silent-error'); -var merge = require('lodash/object/merge'); -var getPackageBaseName = require('../utilities/get-package-base-name'); -var Promise = require('../ext/promise'); - -module.exports = Task.extend({ - init: function() { +const Task = require('../models/task'); +const SilentError = require('silent-error'); +const merge = require('lodash/merge'); +const getPackageBaseName = require('../utilities/get-package-base-name'); + +class AddonInstallTask extends Task { + constructor(options) { + super(options); + this.NpmInstallTask = this.NpmInstallTask || require('./npm-install'); this.BlueprintTask = this.BlueprintTask || require('./generate-from-blueprint'); - }, - - run: function(options) { - var chalk = require('chalk'); - var ui = this.ui; - var packageNames = options['packages']; - var blueprintOptions = options.blueprintOptions || {}; - - var npmInstall = new this.NpmInstallTask({ - ui: this.ui, - analytics: this.analytics, - project: this.project + } + + run(options) { + const { default: chalk } = require('chalk'); + let ui = this.ui; + let packageNames = options.packages || []; + let blueprintOptions = options.blueprintOptions || {}; + let commandOptions = blueprintOptions; + + let npmInstall = new this.NpmInstallTask({ + ui: this.ui, + project: this.project, }); - var blueprintInstall = new this.BlueprintTask({ - ui: this.ui, - analytics: this.analytics, - project: this.project, - testing: this.testing + let blueprintInstall = new this.BlueprintTask({ + ui: this.ui, + project: this.project, + testing: this.testing, }); ui.startProgress(chalk.green('Installing addon package'), chalk.green('.')); - return npmInstall.run({ - packages: packageNames, - 'save-dev': true, - 'save-exact': true - }).then(function() { - return this.project.reloadAddons(); - }.bind(this)).then(function() { - return this.installBlueprint(blueprintInstall, packageNames, blueprintOptions); - }.bind(this)) - .finally(function() { ui.stopProgress(); }) - .then(function() { - ui.writeLine(chalk.green('Installed addon package.')); - }); - }, - - installBlueprint: function(install, packageNames, blueprintOptions) { - var blueprintName, taskOptions, addonInstall = this; - - return packageNames.reduce(function(promise, packageName) { - return promise.then(function() { - blueprintName = addonInstall.findDefaultBlueprintName(packageName); - taskOptions = merge({ - args: [blueprintName], - ignoreMissingMain: true - }, blueprintOptions || {}); - return install.run(taskOptions); - }); - }, Promise.resolve()); - }, - - findDefaultBlueprintName: function(givenName) { - var addon = this.project.findAddonByName(givenName); + return npmInstall + .run({ + packages: packageNames, + save: commandOptions.save, + 'save-dev': !commandOptions.save, + 'save-exact': commandOptions.saveExact, + packageManager: commandOptions.packageManager, + }) + .then(() => this.project.reloadAddons()) + .then(() => this.installBlueprint(blueprintInstall, packageNames, blueprintOptions)) + .finally(() => ui.stopProgress()) + .then(() => ui.writeLine(chalk.green('Installed addon package.'))); + } + installBlueprint(install, packageNames, blueprintOptions) { + let blueprintName, + taskOptions, + addonInstall = this; + + return packageNames.reduce( + (promise, packageName) => + promise.then(() => { + blueprintName = addonInstall.findDefaultBlueprintName(packageName); + if (blueprintName) { + taskOptions = merge( + { + args: [blueprintName], + ignoreMissingMain: true, + }, + blueprintOptions || {} + ); + + return install.run(taskOptions); + } else { + addonInstall.ui.writeWarnLine( + `Could not figure out blueprint name from: "${packageName}". ` + + `Please install the addon blueprint via "ember generate " if necessary.` + ); + } + }), + Promise.resolve() + ); + } + + findDefaultBlueprintName(givenName) { + let packageName = getPackageBaseName(givenName); + if (!packageName) { + return null; + } + + let addon = this.project.findAddonByName(packageName); if (!addon) { - throw new SilentError('Install failed. Could not find addon with name: ' + givenName); + throw new SilentError(`Install failed. Could not find addon with name: ${givenName}`); } - var emberAddon = addon.pkg['ember-addon']; + let emberAddon = addon.pkg['ember-addon']; if (emberAddon && emberAddon.defaultBlueprint) { return emberAddon.defaultBlueprint; } - return getPackageBaseName(addon.pkg.name); + return packageName; } -}); +} + +module.exports = AddonInstallTask; diff --git a/lib/tasks/bower-install.js b/lib/tasks/bower-install.js deleted file mode 100644 index bdbab8f9cb..0000000000 --- a/lib/tasks/bower-install.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -// Runs `bower install` in cwd - -var Promise = require('../ext/promise'); -var Task = require('../models/task'); - -module.exports = Task.extend({ - init: function() { - this.bower = this.bower || require('bower'); - this.bowerConfig = this.bowerConfig || require('bower-config'); - }, - // Options: Boolean verbose - run: function(options) { - var chalk = require('chalk'); - var bower = this.bower; - var bowerConfig = this.bowerConfig; - var ui = this.ui; - var packages = options.packages || []; - var installOptions = options.installOptions || { save: true }; - - ui.startProgress(chalk.green('Installing browser packages via Bower'), chalk.green('.')); - - var config = bowerConfig.read(); - config.interactive = true; - - return new Promise(function(resolve, reject) { - bower.commands.install(packages, installOptions, config) // Packages, options, config - .on('log', logBowerMessage) - .on('prompt', ui.prompt.bind(ui)) - .on('error', reject) - .on('end', resolve); - }) - .finally(function() { ui.stopProgress(); }) - .then(function() { - ui.writeLine(chalk.green('Installed browser packages via Bower.')); - }); - - function logBowerMessage(message) { - if (message.level === 'conflict') { - // e.g. - // conflict Unable to find suitable version for ember-data - // 1) ember-data 1.0.0-beta.6 - // 2) ember-data ~1.0.0-beta.7 - ui.writeLine(' ' + chalk.red('conflict') + ' ' + message.message); - message.data.picks.forEach(function(pick, index) { - ui.writeLine(' ' + chalk.green((index + 1) + ')') + ' ' + - message.data.name + ' ' + pick.endpoint.target); - }); - } else if (message.level === 'info' && options.verbose) { - // e.g. - // cached git://example.com/some-package.git#1.0.0 - ui.writeLine(' ' + chalk.green(message.id) + ' ' + message.message); - } - } - } -}); diff --git a/lib/tasks/build-watch.js b/lib/tasks/build-watch.js index 92c7029fc6..45ece7b36f 100644 --- a/lib/tasks/build-watch.js +++ b/lib/tasks/build-watch.js @@ -1,29 +1,64 @@ 'use strict'; -var chalk = require('chalk'); -var Task = require('../models/task'); -var Watcher = require('../models/watcher'); -var Builder = require('../models/builder'); -var Promise = require('../ext/promise'); - -module.exports = Task.extend({ - run: function(options) { - this.ui.startProgress( - chalk.green('Building'), chalk.green('.') - ); - - return new Watcher({ - ui: this.ui, - builder: new Builder({ - ui: this.ui, +const { default: chalk } = require('chalk'); +const path = require('path'); +const Task = require('../models/task'); +const Watcher = require('../models/watcher'); +const Builder = require('../models/builder'); +const { default: pDefer } = require('p-defer'); + +class BuildWatchTask extends Task { + constructor(options) { + super(options); + + this._builder = null; + this._runDeferred = null; + } + + async run(options) { + let { ui } = this; + + ui.startProgress(chalk.green('Building'), chalk.green('.')); + + this._runDeferred = pDefer(); + + let builder = (this._builder = + options._builder || + new Builder({ + ui, outputPath: options.outputPath, environment: options.environment, - project: this.project - }), - analytics: this.analytics, - options: options - }).then(function() { - return new Promise(function () {}); // Run until failure or signal to exit - }); + project: this.project, + })); + + ui.writeLine(`Environment: ${options.environment}`); + + let watcher = + options._watcher || + ( + await Watcher.build({ + ui, + builder, + options, + ignored: [path.resolve(this.project.root, options.outputPath)], + }) + ).watcher; + + await watcher; + // Run until failure or signal to exit + return this._runDeferred.promise; } -}); + + /** + * Exit silently + * + * @private + * @method onInterrupt + */ + async onInterrupt() { + await this._builder.cleanup(); + this._runDeferred.resolve(); + } +} + +module.exports = BuildWatchTask; diff --git a/lib/tasks/build.js b/lib/tasks/build.js index ae03e57efd..01a3289cad 100644 --- a/lib/tasks/build.js +++ b/lib/tasks/build.js @@ -1,52 +1,39 @@ 'use strict'; -var chalk = require('chalk'); -var Task = require('../models/task'); -var Builder = require('../models/builder'); +const { default: chalk } = require('chalk'); +const Task = require('../models/task'); +const Builder = require('../models/builder'); -module.exports = Task.extend({ +module.exports = class BuildTask extends Task { // Options: String outputPath - run: function(options) { - var ui = this.ui; - var analytics = this.analytics; + async run(options) { + let ui = this.ui; - ui.startProgress(chalk.green('Building'), chalk.green('.')); - - var builder = new Builder({ - ui: ui, + let builder = new Builder({ + ui, outputPath: options.outputPath, environment: options.environment, - project: this.project + project: this.project, }); - return builder.build() - .then(function(results) { - var totalTime = results.totalTime / 1e6; - - analytics.track({ - name: 'ember build', - message: totalTime + 'ms' - }); - - analytics.trackTiming({ - category: 'rebuild', - variable: 'build time', - label: 'broccoli build time', - value: parseInt(totalTime, 10) - }); - }) - .finally(function() { - ui.stopProgress(); - return builder.cleanup(); - }) - .then(function() { - ui.writeLine(chalk.green('Built project successfully. Stored in "' + - options.outputPath + '".')); - }) - .catch(function(err) { - ui.writeLine(chalk.red('Build failed.')); - - throw err; - }); + try { + ui.startProgress(chalk.green('Building'), chalk.green('.')); + + ui.writeLine(`Environment: ${options.environment}`); + + let annotation = { + type: 'initial', + reason: 'build', + primaryFile: null, + changedFiles: [], + }; + + await builder.build(null, annotation); + } finally { + ui.stopProgress(); + await builder.cleanup(); + } + + ui.writeLine(chalk.green(`Built project successfully. Stored in "${options.outputPath}".`)); } -}); +}; diff --git a/lib/tasks/create-and-step-into-directory.js b/lib/tasks/create-and-step-into-directory.js index f6aac29a67..b0444d22e6 100644 --- a/lib/tasks/create-and-step-into-directory.js +++ b/lib/tasks/create-and-step-into-directory.js @@ -3,42 +3,47 @@ // Creates a directory with the name directoryName in cwd and then sets cwd to // this directory. -var Promise = require('../ext/promise'); -var fs = require('fs'); -var existsSync = require('exists-sync'); -var mkdir = Promise.denodeify(fs.mkdir); -var Task = require('../models/task'); -var SilentError = require('silent-error'); - -module.exports = Task.extend({ +const fs = require('fs-extra'); +const Task = require('../models/task'); +const SilentError = require('silent-error'); +const directoryForPackageName = require('@ember-tooling/blueprint-model/utilities/directory-for-package-name'); + +class CreateTask extends Task { // Options: String directoryName, Boolean: dryRun - warnDirectoryAlreadyExists: function warnDirectoryAlreadyExists(){ - var message = 'Directory \'' + this.directoryName + '\' already exists.'; + warnDirectoryAlreadyExists(directoryName) { + let message = `Directory '${directoryName}' already exists.`; return new SilentError(message); - }, - - run: function(options) { - var directoryName = this.directoryName = options.directoryName; - if (options.dryRun){ - return new Promise(function(resolve, reject){ - if (existsSync(directoryName)){ - return reject(this.warnDirectoryAlreadyExists()); - } - resolve(); - }.bind(this)); + } + + async run(options) { + let directoryName = options.directoryName ? options.directoryName : directoryForPackageName(options.projectName); + + if (options.dryRun) { + if (fs.existsSync(directoryName) && fs.readdirSync(directoryName).length) { + throw this.warnDirectoryAlreadyExists(directoryName); + } + + return; } - return mkdir(directoryName) - .catch(function(err) { - if (err.code === 'EEXIST') { - throw this.warnDirectoryAlreadyExists(); - } else { - throw err; + try { + await fs.mkdir(directoryName); + } catch (err) { + if (err.code === 'EEXIST') { + // Allow using directory if it is empty. + if (fs.readdirSync(directoryName).length) { + throw this.warnDirectoryAlreadyExists(directoryName); } - }.bind(this)) - .then(function() { - process.chdir(directoryName); - }); + } else { + throw err; + } + } + let cwd = process.cwd(); + process.chdir(directoryName); + + return { initialDirectory: cwd, projectDirectory: directoryName }; } -}); +} + +module.exports = CreateTask; diff --git a/lib/tasks/destroy-from-blueprint.js b/lib/tasks/destroy-from-blueprint.js index bc50961b25..f73a410907 100644 --- a/lib/tasks/destroy-from-blueprint.js +++ b/lib/tasks/destroy-from-blueprint.js @@ -1,9 +1,12 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Generate = require('./generate-from-blueprint'); +const GenerateTask = require('./generate-from-blueprint'); + +class DestroyTask extends GenerateTask { + constructor(options) { + super(options); + this.blueprintFunction = 'uninstall'; + } +} -module.exports = Generate.extend({ - blueprintFunction: 'uninstall' -}); +module.exports = DestroyTask; diff --git a/lib/tasks/generate-from-blueprint.js b/lib/tasks/generate-from-blueprint.js index 7fd68bbcdd..2784306d4a 100644 --- a/lib/tasks/generate-from-blueprint.js +++ b/lib/tasks/generate-from-blueprint.js @@ -1,96 +1,126 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../ext/promise'); -var Blueprint = require('../models/blueprint'); -var Task = require('../models/task'); -var parseOptions = require('../utilities/parse-options'); -var merge = require('lodash/object/merge'); - -module.exports = Task.extend({ - blueprintFunction: 'install', - - run: function(options) { - var self = this; - var name = options.args[0]; - var noAddonBlueprint = ['mixin']; +const Blueprint = require('@ember-tooling/blueprint-model'); +const Task = require('../models/task'); +const parseOptions = require('../utilities/parse-options'); +const logger = require('heimdalljs-logger')('ember-cli:generate-from-blueprint'); +const lintFix = require('../utilities/lint-fix'); - var mainBlueprint = this.lookupBlueprint(name, options.ignoreMissingMain); - var testBlueprint = this.lookupBlueprint(name + '-test', true); - // lookup custom addon blueprint - var addonBlueprint = this.lookupBlueprint(name + '-addon', true); - // otherwise, use default addon-import - - if (noAddonBlueprint.indexOf(name) < 0 && !addonBlueprint && (mainBlueprint && mainBlueprint.supportsAddon()) && options.args[1]) { - addonBlueprint = this.lookupBlueprint('addon-import', true); - } +class GenerateTask extends Task { + constructor(options) { + super(options); + this.blueprintFunction = 'install'; + } - if (options.ignoreMissingMain && !mainBlueprint) { - return Promise.resolve(); - } + async run(options) { + await this.createBlueprints(options); - if (options.dummy) { - // don't install test or addon reexport for dummy - if (this.project.isEmberCLIAddon()) { - testBlueprint = null; - addonBlueprint = null; + if (options.lintFix) { + try { + await lintFix.run(this.project); + } catch (error) { + logger.error('Lint fix failed: %o', error); } } + } + + async createBlueprints(options) { + let name = options.args[0]; + let noAddonBlueprint = ['mixin', 'blueprint-test']; - var entity = { + let entity = { name: options.args[1], - options: parseOptions(options.args.slice(2)) + options: parseOptions(options.args.slice(2)), }; - var blueprintOptions = { + let baseBlueprintOptions = { target: this.project.root, - entity: entity, + entity, ui: this.ui, - analytics: this.analytics, project: this.project, settings: this.settings, testing: this.testing, taskOptions: options, - originBlueprintName: name + originBlueprintName: name, }; - blueprintOptions = merge(blueprintOptions, options || {}); + let mainBlueprintOptions = { ...baseBlueprintOptions, ...options }; + let testBlueprintOptions = { ...mainBlueprintOptions, installingTest: true }; + let addonBlueprintOptions = { ...mainBlueprintOptions, installingAddon: true }; + + let mainBlueprint = this.lookupBlueprint(name, { + blueprintOptions: mainBlueprintOptions, + ignoreMissing: options.ignoreMissingMain, + }); + + let testBlueprint = this.lookupBlueprint(`${name}-test`, { + blueprintOptions: testBlueprintOptions, + ignoreMissing: true, + }); + + let addonBlueprint = this.lookupBlueprint(`${name}-addon`, { + blueprintOptions: addonBlueprintOptions, + ignoreMissing: true, + }); - return mainBlueprint[this.blueprintFunction](blueprintOptions) - .then(function() { - if (!testBlueprint) { return; } + // otherwise, use default addon-import + if (noAddonBlueprint.indexOf(name) < 0 && !addonBlueprint && options.args[1]) { + let mainBlueprintSupportsAddon = mainBlueprint && mainBlueprint.supportsAddon(); + + if (mainBlueprintSupportsAddon) { + addonBlueprint = this.lookupBlueprint('addon-import', { + blueprintOptions: addonBlueprintOptions, + ignoreMissing: true, + }); + } + } - if (testBlueprint.locals === Blueprint.prototype.locals) { - testBlueprint.locals = function(options) { - return mainBlueprint.locals(options); - }; - } + if (options.ignoreMissingMain && !mainBlueprint) { + return; + } - var testBlueprintOptions = merge({} , blueprintOptions, { installingTest: true }); + if (options.dummy) { + // don't install test or addon reexport for dummy + if (this.project.isEmberCLIAddon()) { + testBlueprint = null; + addonBlueprint = null; + } + } - return testBlueprint[self.blueprintFunction](testBlueprintOptions); - }) - .then(function() { - if (!addonBlueprint) { return; } - if (!this.project.isEmberCLIAddon() && blueprintOptions.inRepoAddon === null) { return; } + await mainBlueprint[this.blueprintFunction](mainBlueprintOptions); + if (testBlueprint) { + if (testBlueprint.locals === Blueprint.prototype.locals) { + testBlueprint.locals = function (options) { + return mainBlueprint.locals(options); + }; + } + + await testBlueprint[this.blueprintFunction](testBlueprintOptions); + } - if (addonBlueprint.locals === Blueprint.prototype.locals) { - addonBlueprint.locals = function(options) { - return mainBlueprint.locals(options); - }; - } + if (!addonBlueprint || name.match(/-addon/)) { + return; + } + if (!this.project.isEmberCLIAddon() && mainBlueprintOptions.inRepoAddon === null) { + return; + } - var addonBlueprintOptions = merge({}, blueprintOptions, { installingAddon: true }); + if (addonBlueprint.locals === Blueprint.prototype.locals) { + addonBlueprint.locals = function (options) { + return mainBlueprint.locals(options); + }; + } - return addonBlueprint[self.blueprintFunction](addonBlueprintOptions); - }.bind(this)); - }, + return addonBlueprint[this.blueprintFunction](addonBlueprintOptions); + } - lookupBlueprint: function(name, ignoreMissing) { + lookupBlueprint(name, { blueprintOptions, ignoreMissing }) { return Blueprint.lookup(name, { + blueprintOptions, + ignoreMissing, paths: this.project.blueprintLookupPaths(), - ignoreMissing: ignoreMissing }); } -}); +} + +module.exports = GenerateTask; diff --git a/lib/tasks/git-init.js b/lib/tasks/git-init.js index 68eca13faf..cce6773ab0 100644 --- a/lib/tasks/git-init.js +++ b/lib/tasks/git-init.js @@ -1,44 +1,80 @@ 'use strict'; -var Promise = require('../../lib/ext/promise'); -var Task = require('../models/task'); -var exec = Promise.denodeify(require('child_process').exec); -var path = require('path'); -var pkg = require('../../package.json'); -var fs = require('fs'); -var template = require('lodash/string/template'); - -var gitEnvironmentVariables = { - GIT_AUTHOR_NAME: 'Tomster', - GIT_AUTHOR_EMAIL: 'tomster@emberjs.com', - get GIT_COMMITTER_NAME(){ return this.GIT_AUTHOR_NAME; }, - get GIT_COMMITTER_EMAIL(){ return this.GIT_AUTHOR_EMAIL; } -}; +const Task = require('../models/task'); +const path = require('path'); +const pkg = require('../../package.json'); +const fs = require('fs'); +const { execa } = require('execa'); + +module.exports = class GitInitTask extends Task { + async run(_commandOptions) { + let commandOptions = _commandOptions || {}; + const { default: chalk } = require('chalk'); + let ui = this.ui; + + if (commandOptions.skipGit) { + return; + } + + let hasGit = true; + try { + await this._gitVersion(); + } catch (e) { + hasGit = false; + } + if (!hasGit) { + return; + } + const prependEmoji = require('@ember-tooling/blueprint-model/utilities/prepend-emoji'); + ui.writeLine(''); + ui.writeLine(prependEmoji('🎥', 'Initializing git repository.')); + await this._gitInit(); + await this._gitAdd(); + await this._gitCommit(); + ui.writeLine(chalk.green('Git: successfully initialized.')); + } + + _gitVersion() { + return execa('git', ['--version']); + } + + _gitInit() { + return execa('git', ['init']); + } -module.exports = Task.extend({ - run: function(commandOptions) { - var chalk = require('chalk'); - var ui = this.ui; - - if(commandOptions.skipGit) { return Promise.resolve(); } - - return exec('git --version') - .then(function() { - return exec('git init') - .then(function() { - return exec('git add .'); - }) - .then(function(){ - var commitTemplate = fs.readFileSync(path.join(__dirname, '../utilities/COMMIT_MESSAGE.txt')); - var commitMessage = template(commitTemplate)(pkg); - return exec('git commit -m "' + commitMessage + '"', {env: gitEnvironmentVariables}); - }) - .then(function(){ - ui.writeLine(chalk.green('Successfully initialized git.')); - }); - }).catch(function(/*error*/){ - // if git is not found or an error was thrown during the `git` - // init process just swallow any errors here + _gitAdd() { + return execa('git', ['add', '.']); + } + + async _gitCommit() { + const template = require('lodash/template'); + let commitTemplate = fs.readFileSync(path.join(__dirname, '../utilities/COMMIT_MESSAGE.txt')); + let commitMessage = template(commitTemplate)(pkg); + let env = this.buildGitEnvironment(); + + try { + return await execa('git', ['commit', '-m', commitMessage], { env }); + } catch (error) { + if (isError(error) && error.message.indexOf('git config --global user') > -1) { + env.GIT_COMMITTER_NAME = 'Tomster'; + env.GIT_COMMITTER_EMAIL = 'tomster@emberjs.com'; + return execa('git', ['commit', '-m', commitMessage], { env }); + } + + throw error; + } + } + + buildGitEnvironment() { + // Make sure we merge in the current environment so that git has access to + // important environment variables like $HOME. + return Object.assign({}, process.env, { + GIT_AUTHOR_NAME: 'Tomster', + GIT_AUTHOR_EMAIL: 'tomster@emberjs.com', }); } -}); +}; + +function isError(error) { + return typeof error === 'object' && error !== null && typeof error.message === 'string'; +} diff --git a/lib/tasks/install-blueprint.js b/lib/tasks/install-blueprint.js index a7197d13b9..b1ff29dd5f 100644 --- a/lib/tasks/install-blueprint.js +++ b/lib/tasks/install-blueprint.js @@ -1,56 +1,221 @@ 'use strict'; -var Blueprint = require('../models/blueprint'); -var Task = require('../models/task'); -var Promise = require('../ext/promise'); -var isGitRepo = require('is-git-url'); -var temp = require('temp'); -var childProcess = require('child_process'); -var path = require('path'); - -// Automatically track and cleanup temp files at exit -temp.track(); - -var mkdir = Promise.denodeify(temp.mkdir); -var exec = Promise.denodeify(childProcess.exec); - -module.exports = Task.extend({ - run: function(options) { - var cwd = process.cwd(); - var name = options.rawName; - var blueprintOption = options.blueprint; +const fs = require('fs-extra'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const Task = require('../models/task'); +const os = require('os'); +const path = require('path'); +const merge = require('lodash/merge'); +const { execa } = require('execa'); +const SilentError = require('silent-error'); +const npa = require('npm-package-arg'); +const lintFix = require('../utilities/lint-fix'); + +const logger = require('heimdalljs-logger')('ember-cli:tasks:install-blueprint'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +const NOT_FOUND_REGEXP = /npm ERR! 404 {2}'(\S+)' is not in the npm registry/; + +class InstallBlueprintTask extends Task { + async run(options) { + let cwd = process.cwd(); + let name = options.rawName; + let blueprintOption = options.blueprint; // If we're in a dry run, pretend we changed directories. // Pretending we cd'd avoids prompts in the actual current directory. - var fakeCwd = path.join(cwd, name); - var target = options.dryRun ? fakeCwd : cwd; + let fakeCwd = path.join(cwd, name); + let target = options.dryRun ? fakeCwd : cwd; - var installOptions = { - target: target, - entity: { name: name }, + let installOptions = { + target, + entity: { name }, ui: this.ui, - analytics: this.analytics, project: this.project, dryRun: options.dryRun, targetFiles: options.targetFiles, - rawArgs: options.rawArgs + rawArgs: options.rawArgs, + ciProvider: options.ciProvider, }; - if (isGitRepo(blueprintOption)) { - return mkdir('ember-cli').then(function(pathName){ - var execArgs = ['git', 'clone', blueprintOption, pathName].join(' '); - return exec(execArgs).then(function(){ - return exec('npm install', {cwd: pathName}).then(function(){ - var blueprint = Blueprint.load(pathName); - return blueprint.install(installOptions); - }); - }); - }); - } else { - var blueprintName = blueprintOption || 'app'; - var blueprint = Blueprint.lookup(blueprintName, { - paths: this.project.blueprintLookupPaths() + installOptions = merge(installOptions, options || {}); + + let blueprint = await this._resolveBlueprint(blueprintOption); + logger.info(`Installing blueprint into "${target}" ...`); + await blueprint.install(installOptions); + + if (options.lintFix) { + try { + await lintFix.run(this.project); + } catch (error) { + logger.error('Lint fix failed: %o', error); + } + } + } + + async _resolveBlueprint(name) { + name = name || 'app'; + logger.info(`Resolving blueprint "${name}" ...`); + + if (isExperimentEnabled('VITE') && name === 'app') { + return Blueprint.load(path.dirname(require.resolve('@ember/app-blueprint'))); + } + + let blueprint; + try { + blueprint = await this._lookupLocalBlueprint(name); + } catch (error) { + blueprint = await this._handleLocalLookupFailure(name, error); + } + + return blueprint; + } + + async _lookupLocalBlueprint(name) { + logger.info(`Looking up blueprint "${name}" locally...`); + return Blueprint.lookup(name, { + paths: this.project.blueprintLookupPaths(), + }); + } + + _handleLocalLookupFailure(name, error) { + logger.info(`Local blueprint lookup for "${name}" failed`); + + try { + npa(name); + } catch (err) { + logger.info(`"${name} is not a valid npm package specifier -> rethrowing original error`); + throw error; + } + + logger.info(`"${name} is a valid npm package specifier -> trying npm`); + return this._tryRemoteBlueprint(name); + } + + async _tryRemoteBlueprint(name) { + let tmpDir = await this._createTempFolder(); + await this._createEmptyPackageJSON(tmpDir, name); + + try { + await this._npmInstall(tmpDir, name); + } catch (error) { + this._handleNpmInstallModuleError(error); // re-throws + } + + let packageName = await this._readSingleDependencyFromPackageJSON(tmpDir); + let blueprintPath = path.resolve(tmpDir, 'node_modules', packageName); + this._validateNpmModule(blueprintPath, packageName); + + return this._loadBlueprintFromPath(blueprintPath); + } + + _createTempFolder() { + return fs.mkdtemp(path.join(os.tmpdir(), 'ember-cli-')); + } + + _resolvePackageJSON(directoryPath) { + return path.resolve(directoryPath, 'package.json'); + } + + async _createEmptyPackageJSON(tempProjectPath, blueprintName) { + return fs.writeFile( + this._resolvePackageJSON(tempProjectPath), + JSON.stringify({ + name: 'ember-cli-blueprint-install-wrapper', + version: '0.0.0', + description: `This is a transient package used to fetch this blueprint: ${blueprintName}`, + license: 'UNLICENSED', + private: true, + dependencies: {}, + }) + ); + } + + async _readSingleDependencyFromPackageJSON(tempProjectPath) { + let packageJSON = JSON.parse(await fs.readFile(this._resolvePackageJSON(tempProjectPath), 'utf8')); + + if (!packageJSON.dependencies || typeof packageJSON.dependencies !== 'object') { + throw new SilentError(`The transient package.json at '${tempProjectPath}' is missing 'dependencies'.`); + } + + let dependencyNames = Object.keys(packageJSON.dependencies); + + if (dependencyNames.length !== 1) { + throw new SilentError( + `The transient package.json at '${tempProjectPath}' has more than one or no entries in 'dependencies'.` + ); + } + + return dependencyNames[0]; + } + + async _createPackageJSON(tempProjectPath, descriptor) { + let packageName = descriptor.name || `ember-cli-blueprint-${Date.now()}`; + let specifier = descriptor.saveSpec || descriptor.fetchSpec; + + await fs.writeFile( + this._resolvePackageJSON(tempProjectPath), + JSON.stringify({ + dependencies: { + [packageName]: specifier, + }, + }) + ); + + return packageName; + } + + _npmInstall(cwd, name) { + logger.info(`Running "npm install" for "${name}" in "${cwd}" ...`); + + this._copyNpmrc(cwd); + + return execa('npm', ['install', '--no-package-lock', '--save', name], { cwd }); + } + + _handleNpmInstallModuleError(error) { + let match = error.stderr && error.stderr.match(NOT_FOUND_REGEXP); + if (match) { + let packageName = match[1]; + throw new SilentError(`The package '${packageName}' was not found in the npm registry.`); + } + + throw error; + } + + _validateNpmModule(modulePath, packageName) { + logger.info(`Checking for "ember-blueprint" keyword in "${packageName}" module ...`); + let pkg = require(this._resolvePackageJSON(modulePath)); + if (!pkg || !pkg.keywords || pkg.keywords.indexOf('ember-blueprint') === -1) { + throw new SilentError( + `The package '${packageName}' is not a valid Ember CLI blueprint. The package.json keywords list must include "ember-blueprint".` + ); + } + } + + async _loadBlueprintFromPath(path) { + logger.info(`Loading blueprint from "${path}" ...`); + return Blueprint.load(path); + } + + /* + * NPM has 4 places it uses for .npmrc. 3 of the 4 are supported as their locations + * are external to the project root but if there is an .npmrc file located at the project root it will + * not be respected. This is because we create a tmp directory (createTempFolder) where we run + * the npm install. This function simply copies over the .npmrc file to the tmp directory so that it + * will be used during the install of the blueprint. Useful for installing private blueprints + * such as `ember init -b @company/company-specific-blueprint` where the module is in a different + * registry. + */ + _copyNpmrc(tmpDir) { + let rcPath = path.join(this.project.root, '.npmrc'); + let tempLocation = path.join(tmpDir, '.npmrc'); + + if (fs.existsSync(rcPath)) { + fs.copySync(rcPath, tempLocation, { + dereference: true, }); - return blueprint.install(installOptions); } } -}); +} + +module.exports = InstallBlueprintTask; diff --git a/lib/tasks/interactive-new.js b/lib/tasks/interactive-new.js new file mode 100644 index 0000000000..fafa5b6bda --- /dev/null +++ b/lib/tasks/interactive-new.js @@ -0,0 +1,175 @@ +'use strict'; + +const Task = require('../models/task'); +const isValidProjectName = require('../utilities/valid-project-name'); + +const DEFAULT_LOCALE = 'en-US'; + +class InteractiveNewTask extends Task { + async run(newCommandOptions, _testAnswers) { + const inquirer = await import('inquirer'); + + let prompt = inquirer.createPromptModule(); + let questions = await this.getQuestions(newCommandOptions); + let answers = await prompt(questions, _testAnswers); + + answers.lang = answers.langSelection || answers.langDifferent; + + delete answers.langSelection; + delete answers.langDifferent; + + return answers; + } + + async getQuestions(newCommandOptions = {}) { + const { isLangCode } = require('is-language-code'); + + return [ + { + name: 'blueprint', + type: 'select', + message: 'Is this an app or an addon?', + choices: [ + { + name: 'App', + value: 'app', + }, + { + name: 'Addon', + value: 'addon', + }, + ], + }, + { + name: 'name', + type: 'input', + message: ({ blueprint }) => `Please provide the name of your ${blueprint}:`, + when: !newCommandOptions.name, + validate: (name) => { + if (name) { + if (isValidProjectName(name)) { + return true; + } + + return `We currently do not support \`${name}\` as a name.`; + } + + return 'Please provide a name.'; + }, + }, + { + name: 'langSelection', + type: 'select', + message: ({ blueprint }) => `Please provide the spoken/content language of your ${blueprint}:`, + when: !newCommandOptions.lang, + choices: await this.getLangChoices(), + }, + { + name: 'langDifferent', + type: 'input', + message: 'Please provide the different language:', + when: ({ langSelection } = {}) => !newCommandOptions.lang && !langSelection, + validate: (lang) => { + if (isLangCode(lang).res) { + return true; + } + + return 'Please provide a valid locale code.'; + }, + }, + { + name: 'packageManager', + type: 'select', + message: 'Pick the package manager to use when installing dependencies:', + when: !newCommandOptions.packageManager, + choices: [ + { + name: 'NPM', + value: 'npm', + }, + { + name: 'pnpm', + value: 'pnpm', + }, + { + name: 'Yarn', + value: 'yarn', + }, + { + name: 'Ignore/Skip', + value: null, + }, + ], + }, + { + name: 'ciProvider', + type: 'select', + message: 'Which CI provider do you want to use?', + when: !newCommandOptions.ciProvider, + choices: [ + { + name: 'GitHub Actions', + value: 'github', + }, + { + name: 'None', + value: 'none', + }, + { + name: 'Ignore/Skip', + value: null, + }, + ], + }, + { + name: 'emberData', + type: 'confirm', + message: 'Do you want to include ember-data?', + when: undefined === newCommandOptions.emberData, + choices: [ + { + name: 'Include ember-data', + value: true, + }, + { + name: 'Do not include ember-data', + value: false, + }, + ], + }, + ]; + } + + async getLangChoices() { + let userLocale = await this.getUserLocale(); + let langChoices = [ + { + name: DEFAULT_LOCALE, + value: DEFAULT_LOCALE, + }, + ]; + + if (userLocale !== DEFAULT_LOCALE) { + langChoices.push({ + name: userLocale, + value: userLocale, + }); + } + + langChoices.push({ + name: 'I want to manually provide a different language', + value: null, + }); + + return langChoices; + } + + getUserLocale() { + const { osLocale } = require('os-locale'); + + return osLocale(); + } +} + +module.exports = InteractiveNewTask; +module.exports.DEFAULT_LOCALE = DEFAULT_LOCALE; diff --git a/lib/tasks/npm-install.js b/lib/tasks/npm-install.js index 40fa58af9c..e2def6eb46 100644 --- a/lib/tasks/npm-install.js +++ b/lib/tasks/npm-install.js @@ -1,11 +1,35 @@ 'use strict'; -// Runs `npm install` in cwd +const path = require('path'); +const fs = require('fs'); +const NpmTask = require('./npm-task'); +const formatPackageList = require('../utilities/format-package-list'); -var NpmTask = require('./npm-task'); +class NpmInstallTask extends NpmTask { + constructor(options) { + super(options); + this.command = 'install'; + } -module.exports = NpmTask.extend({ - command: 'install', - startProgressMessage: 'Installing packages for tooling via npm', - completionMessage: 'Installed packages for tooling via npm.' -}); + run(options) { + let ui = this.ui; + let packageJson = path.join(this.project.root, 'package.json'); + + if (!fs.existsSync(packageJson)) { + ui.writeWarnLine('Skipping install: `package.json` not found.'); + return Promise.resolve(); + } else { + return super.run(options); + } + } + + formatStartMessage(packages) { + return `${this.packageManagerOutputName}: Installing ${formatPackageList(packages)} ...`; + } + + formatCompleteMessage(packages) { + return `${this.packageManagerOutputName}: Installed ${formatPackageList(packages)}`; + } +} + +module.exports = NpmInstallTask; diff --git a/lib/tasks/npm-task.js b/lib/tasks/npm-task.js index 93f88e4a56..1fe6fa4ace 100644 --- a/lib/tasks/npm-task.js +++ b/lib/tasks/npm-task.js @@ -1,62 +1,335 @@ 'use strict'; -// Runs `npm install` in cwd - -var chalk = require('chalk'); -var Task = require('../models/task'); -var npm = require('../utilities/npm'); - -module.exports = Task.extend({ - // The command to run: can be 'install' or 'uninstall' - command: '', - // Message to send to ui.startProgress - startProgressMessage: '', - // Message to send to ui.writeLine on completion - completionMessage: '', - - init: function() { - this.npm = this.npm || require('npm'); - }, - // Options: Boolean verbose - run: function(options) { - this.ui.startProgress(chalk.green(this.startProgressMessage), chalk.green('.')); - - var npmOptions = { - loglevel: options.verbose ? 'verbose' : 'error', - logstream: this.ui.outputStream, - color: 'always', - // by default, do install peoples optional deps - 'optional': 'optional' in options ? options.optional : true, - 'save-dev': !!options['save-dev'], - 'save-exact': !!options['save-exact'] - }; - - var packages = options.packages || []; - - // npm otherwise is otherwise noisy, already submitted PR for npm to fix - // misplaced console.log - this.disableLogger(); - - return npm(this.command, packages, npmOptions, this.npm). - finally(this.finally.bind(this)). - then(this.announceCompletion.bind(this)); - }, - - announceCompletion: function() { - this.ui.writeLine(chalk.green(this.completionMessage)); - }, - - finally: function() { - this.ui.stopProgress(); - this.restoreLogger(); - }, - - disableLogger: function() { - this.oldLog = console.log; - console.log = function() {}; - }, - - restoreLogger: function() { - console.log = this.oldLog; // Hack, see above - } -}); +const { default: chalk } = require('chalk'); +const { execa } = require('execa'); +const semver = require('semver'); +const SilentError = require('silent-error'); +const { isPnpmProject, isYarnProject } = require('../utilities/package-managers'); + +const logger = require('heimdalljs-logger')('ember-cli:npm-task'); + +const Task = require('../models/task'); + +class NpmTask extends Task { + /** + * @private + * @class NpmTask + * @constructor + * @param {Object} options + */ + constructor(options) { + super(options); + + // The command to run: can be 'install' or 'uninstall' + this.command = ''; + } + + get packageManagerOutputName() { + return this.packageManager.name; + } + + npm(args) { + logger.info('npm: %j', args); + return execa('npm', args, { preferLocal: false }); + } + + yarn(args) { + logger.info('yarn: %j', args); + return execa('yarn', args, { preferLocal: false }); + } + + pnpm(args) { + logger.info('pnpm: %j', args); + return execa('pnpm', args, { preferLocal: false }); + } + + hasYarnLock() { + return isYarnProject(this.project.root); + } + + hasPNPMLock() { + return isPnpmProject(this.project.root); + } + + async checkYarn() { + try { + let result = await this.yarn(['--version']); + let version = result.stdout; + + if (semver.gte(version, '2.0.0')) { + logger.warn('yarn --version: %s', version); + let yarnConfig = await this.yarn(['config', 'get', 'nodeLinker']); + let nodeLinker = yarnConfig.stdout.trim(); + if (nodeLinker !== 'node-modules') { + this.ui.writeWarnLine(`Yarn v2 is not fully supported. Proceeding with yarn: ${version}`); + } + } else { + logger.info('yarn --version: %s', version); + } + + return { name: 'yarn', version }; + } catch (error) { + logger.error('yarn --version failed: %s', error); + + if (error.code === 'ENOENT') { + throw new SilentError( + 'Ember CLI is now using yarn, but was not able to find it.\n' + + 'Please install yarn using the instructions at https://classic.yarnpkg.com/en/docs/install' + ); + } + + throw error; + } + } + + async checkPNPM() { + try { + let result = await this.pnpm(['--version']); + let version = result.stdout; + + logger.info('pnpm --version: %s', version); + + return { name: 'pnpm', version }; + } catch (error) { + logger.error('pnpm --version failed: %s', error); + + if (error.code === 'ENOENT') { + throw new SilentError( + 'Ember CLI is now using pnpm, but was not able to find it.\n' + + 'Please install pnpm using the instructions at https://pnpm.io/installation' + ); + } + + throw error; + } + } + + async checkNpmVersion() { + try { + let result = await this.npm(['--version']); + let version = result.stdout; + logger.info('npm --version: %s', version); + + return { name: 'npm', version }; + } catch (error) { + logger.error('npm --version failed: %s', error); + + if (error.code === 'ENOENT') { + throw new SilentError( + 'Ember CLI is now using the global npm, but was not able to find it.\n' + + 'Please install npm using the instructions at https://github.com/npm/npm' + ); + } + + throw error; + } + } + + /** + * This method will determine what package manager (npm or yarn) should be + * used to install the npm dependencies. + * + * Setting `this.useYarn` to `true` or `false` will force the use of yarn + * or npm respectively. + * + * If `this.useYarn` is not set we check if `yarn.lock` exists and if + * `yarn` is available and in that case set `useYarn` to `true`. + * + * @private + * @method findPackageManager + * @return {Promise} + */ + async findPackageManager(packageManager = null) { + if (packageManager === 'yarn') { + logger.info('yarn requested -> trying yarn'); + return this.checkYarn(); + } + + if (packageManager === 'npm') { + logger.info('npm requested -> using npm'); + return this.checkNpmVersion(); + } + + if (packageManager === 'pnpm') { + logger.info('pnpm requested -> using pnpm'); + return this.checkPNPM(); + } + + if (this.hasYarnLock()) { + logger.info('yarn.lock found -> trying yarn'); + try { + const yarnResult = await this.checkYarn(); + logger.info('yarn found -> using yarn'); + return yarnResult; + } catch (_err) { + logger.info('yarn not found'); + } + } else { + logger.info('yarn.lock not found'); + } + + if (await this.hasPNPMLock()) { + logger.info('pnpm-lock.yaml found -> trying pnpm'); + try { + let result = await this.checkPNPM(); + logger.info('pnpm found -> using pnpm'); + return result; + } catch (_err) { + logger.info('pnpm not found'); + } + } else { + logger.info('pnpm-lock.yaml not found'); + } + + logger.info('using npm'); + return this.checkNpmVersion(); + } + + async run(options) { + this.packageManager = await this.findPackageManager(options.packageManager); + + let ui = this.ui; + let startMessage = this.formatStartMessage(options.packages); + let completeMessage = this.formatCompleteMessage(options.packages); + + const prependEmoji = require('@ember-tooling/blueprint-model/utilities/prepend-emoji'); + + ui.writeLine(''); + ui.writeLine(prependEmoji('🚧', 'Installing packages... This might take a couple of minutes.')); + ui.startProgress(chalk.green(startMessage)); + + try { + if (this.packageManager.name === 'yarn') { + let args = this.toYarnArgs(this.command, options); + await this.yarn(args); + } else if (this.packageManager.name === 'pnpm') { + let args = this.toPNPMArgs(this.command, options); + await this.pnpm(args); + } else { + let args = this.toNpmArgs(this.command, options); + await this.npm(args); + } + } finally { + ui.stopProgress(); + } + + ui.writeLine(chalk.green(completeMessage)); + } + + toNpmArgs(command, options) { + let args = [command]; + + if (options.save) { + args.push('--save'); + } + + if (options['save-dev']) { + args.push('--save-dev'); + } + + if (options['save-exact']) { + args.push('--save-exact'); + } + + if ('optional' in options && !options.optional) { + args.push('--no-optional'); + } + + if (options.verbose) { + args.push('--loglevel', 'verbose'); + } else { + args.push('--loglevel', 'error'); + } + + if (options.packages) { + args = args.concat(options.packages); + } + + return args; + } + + toYarnArgs(command, options) { + let args = []; + + if (command === 'install') { + if (options.save) { + args.push('add'); + } else if (options['save-dev']) { + args.push('add', '--dev'); + } else if (options.packages) { + throw new Error(`npm command "${command} ${options.packages.join(' ')}" can not be translated to Yarn command`); + } else { + args.push('install'); + } + + if (options['save-exact']) { + args.push('--exact'); + } + + if ('optional' in options && !options.optional) { + args.push('--ignore-optional'); + } + } else if (command === 'uninstall') { + args.push('remove'); + } else { + throw new Error(`npm command "${command}" can not be translated to Yarn command`); + } + + if (options.verbose) { + args.push('--verbose'); + } + + if (options.packages) { + args = args.concat(options.packages); + } + + // Yarn v2 defaults to non-interactive + // with an optional -i flag + + if (semver.lt(this.packageManager.version, '2.0.0')) { + args.push('--non-interactive'); + } + + return args; + } + + toPNPMArgs(command, options) { + let args = []; + + if (command === 'install') { + if (options.save) { + args.push('add'); + } else if (options['save-dev']) { + args.push('add', '--save-dev'); + } else if (options.packages) { + throw new Error(`npm command "${command} ${options.packages.join(' ')}" can not be translated to pnpm command`); + } else { + args.push('install'); + } + + if (options['save-exact']) { + args.push('--save-exact'); + } + } else if (command === 'uninstall') { + args.push('remove'); + } else { + throw new Error(`npm command "${command}" can not be translated to pnpm command`); + } + + if (options.packages) { + args = args.concat(options.packages); + } + + return args; + } + + formatStartMessage(/* packages */) { + return ''; + } + + formatCompleteMessage(/* packages */) { + return ''; + } +} + +module.exports = NpmTask; diff --git a/lib/tasks/npm-uninstall.js b/lib/tasks/npm-uninstall.js index 79db8f8a0a..36348c8796 100644 --- a/lib/tasks/npm-uninstall.js +++ b/lib/tasks/npm-uninstall.js @@ -2,10 +2,22 @@ // Runs `npm uninstall` in cwd -var NpmTask = require('./npm-task'); +const NpmTask = require('./npm-task'); +const formatPackageList = require('../utilities/format-package-list'); -module.exports = NpmTask.extend({ - command: 'uninstall', - startProgressMessage: 'Uninstalling packages for tooling via npm', - completionMessage: 'Uninstalled packages for tooling via npm.' -}); +class NpmUninstallTask extends NpmTask { + constructor(options) { + super(options); + this.command = 'uninstall'; + } + + formatStartMessage(packages) { + return `${this.packageManagerOutputName}: Uninstalling ${formatPackageList(packages)} ...`; + } + + formatCompleteMessage(packages) { + return `${this.packageManagerOutputName}: Uninstalled ${formatPackageList(packages)}`; + } +} + +module.exports = NpmUninstallTask; diff --git a/lib/tasks/serve.js b/lib/tasks/serve.js index 3987b8ebad..18ef540abc 100644 --- a/lib/tasks/serve.js +++ b/lib/tasks/serve.js @@ -1,64 +1,114 @@ 'use strict'; -var existsSync = require('exists-sync'); -var path = require('path'); -var LiveReloadServer = require('./server/livereload-server'); -var ExpressServer = require('./server/express-server'); -var Promise = require('../ext/promise'); -var Task = require('../models/task'); -var Watcher = require('../models/watcher'); -var Builder = require('../models/builder'); -var ServerWatcher = require('../models/server-watcher'); - -module.exports = Task.extend({ - run: function(options) { - var builder = new Builder({ - ui: this.ui, - outputPath: options.outputPath, - project: this.project, - environment: options.environment - }); - - var watcher = new Watcher({ - ui: this.ui, - builder: builder, - analytics: this.analytics, - options: options - }); - - var serverRoot = './server'; - var serverWatcher = null; - if (existsSync(serverRoot)) { - serverWatcher = new ServerWatcher({ +const fs = require('fs'); +const path = require('path'); +const ExpressServer = require('./server/express-server'); +const Task = require('../models/task'); +const Watcher = require('../models/watcher'); +const ServerWatcher = require('../models/server-watcher'); +const Builder = require('../models/builder'); +const SilentError = require('silent-error'); +const serveURL = require('../utilities/get-serve-url'); +const { default: pDefer } = require('p-defer'); + +function mockWatcher(distDir) { + let watcher = Promise.resolve({ directory: distDir }); + watcher.on = () => {}; + return watcher; +} + +function mockBuilder() { + return { + cleanup: () => Promise.resolve(), + }; +} + +class ServeTask extends Task { + constructor(options) { + super(options); + + this._runDeferred = null; + this._builder = null; + } + + async run(options) { + let hasBuild = !!options.path; + + if (hasBuild) { + if (!fs.existsSync(options.path)) { + throw new SilentError( + `The path ${options.path} does not exist. Please specify a valid build directory to serve.` + ); + } + options._builder = mockBuilder(); + options._watcher = mockWatcher(options.path); + } + + let builder = (this._builder = + options._builder || + new Builder({ ui: this.ui, - analytics: this.analytics, - watchedDir: path.resolve(serverRoot) - }); + outputPath: options.outputPath, + project: this.project, + environment: options.environment, + })); + + let watcher = + options._watcher || + ( + await Watcher.build({ + ui: this.ui, + builder, + options, + serving: true, + ignored: [path.resolve(this.project.root, options.outputPath)], + }) + ).watcher; + + let serverRoot = './server'; + let serverWatcher = null; + if (fs.existsSync(serverRoot)) { + serverWatcher = ( + await ServerWatcher.build({ + ui: this.ui, + watchedDir: path.resolve(serverRoot), + options, + }) + ).watcher; } - var expressServer = new ExpressServer({ - ui: this.ui, - project: this.project, - watcher: watcher, - serverRoot: serverRoot, - serverWatcher: serverWatcher - }); - - var liveReloadServer = new LiveReloadServer({ - ui: this.ui, - analytics: this.analytics, - project: this.project, - watcher: watcher, - expressServer: expressServer - }); - - return Promise.all([ - liveReloadServer.start(options), - expressServer.start(options) - ]).then(function() { - return new Promise(function() { - // hang until the user exits. - }); + let expressServer = + options._expressServer || + new ExpressServer({ + ui: this.ui, + project: this.project, + watcher, + serverRoot, + serverWatcher, }); + + /* hang until the user exits */ + this._runDeferred = pDefer(); + + await expressServer.start(options); + + if (hasBuild) { + this.ui.writeLine(`– Serving on ${serveURL(options, this.project)}`); + } + + return this._runDeferred.promise; } -}); + + /** + * Exit silently + * + * @private + * @method onInterrupt + */ + async onInterrupt() { + await this._builder.cleanup(); + return this._runDeferred.resolve(); + } +} + +module.exports = ServeTask; diff --git a/lib/tasks/server/express-server.js b/lib/tasks/server/express-server.js index 122655ad38..44c6325554 100644 --- a/lib/tasks/server/express-server.js +++ b/lib/tasks/server/express-server.js @@ -1,44 +1,46 @@ 'use strict'; -var path = require('path'); -var EventEmitter = require('events').EventEmitter; -var chalk = require('chalk'); -var fs = require('fs'); -var existsSync = require('exists-sync'); -var debounce = require('lodash/function/debounce'); -var mapSeries = require('promise-map-series'); -var Promise = require('../../ext/promise'); -var Task = require('../../models/task'); -var SilentError = require('silent-error'); - -var cleanBaseURL = require('clean-base-url'); - -module.exports = Task.extend({ - init: function() { +const path = require('path'); +const EventEmitter = require('events').EventEmitter; +const { default: chalk } = require('chalk'); +const fs = require('fs'); +const debounce = require('lodash/debounce'); +const mapSeries = require('promise-map-series'); +const Task = require('../../models/task'); +const SilentError = require('silent-error'); +const LiveReloadServer = require('./livereload-server'); + +class ExpressServerTask extends Task { + constructor(options) { + super(options); this.emitter = new EventEmitter(); this.express = this.express || require('express'); - this.http = this.http || require('http'); + this.http = this.http || require('http'); this.https = this.https || require('https'); - var serverRestartDelayTime = this.serverRestartDelayTime || 100; - this.scheduleServerRestart = debounce(function(){ + let serverRestartDelayTime = this.serverRestartDelayTime || 100; + this.scheduleServerRestart = debounce(function () { this.restartHttpServer(); }, serverRestartDelayTime); - }, + } - on: function() { + on() { this.emitter.on.apply(this.emitter, arguments); - }, + } - off: function() { + off() { this.emitter.off.apply(this.emitter, arguments); - }, + } - displayHost: function(specifiedHost) { - return specifiedHost === '0.0.0.0' ? 'localhost' : specifiedHost; - }, + emit() { + this.emitter.emit.apply(this.emitter, arguments); + } + + displayHost(specifiedHost) { + return specifiedHost || 'localhost'; + } - setupHttpServer: function() { + setupHttpServer() { if (this.startOptions.ssl) { this.httpServer = this.createHttpsServer(); } else { @@ -49,70 +51,97 @@ module.exports = Task.extend({ // when we need to restart. this.sockets = {}; this.nextSocketId = 0; - this.httpServer.on('connection', function(socket) { - var socketId = this.nextSocketId++; + this.httpServer.on('connection', (socket) => { + let socketId = this.nextSocketId++; this.sockets[socketId] = socket; - socket.on('close', function() { + socket.on('close', () => { delete this.sockets[socketId]; - }.bind(this)); - }.bind(this)); - }, + }); + }); + } - createHttpsServer: function() { - if(!existsSync(this.startOptions.sslKey)) { - throw new TypeError('SSL key couldn\'t be found in "' + this.startOptions.sslKey + '", please provide a path to an existing ssl key file with --ssl-key'); + createHttpsServer() { + if (!fs.existsSync(this.startOptions.sslKey)) { + throw new TypeError( + `SSL key couldn't be found in "${this.startOptions.sslKey}", ` + + `please provide a path to an existing ssl key file with --ssl-key` + ); } - if(!existsSync(this.startOptions.sslCert)) { - throw new TypeError('SSL certificate couldn\'t be found in "' + this.startOptions.sslCert + '", please provide a path to an existing ssl certificate file with --ssl-cert'); + + if (!fs.existsSync(this.startOptions.sslCert)) { + throw new TypeError( + `SSL certificate couldn't be found in "${this.startOptions.sslCert}", ` + + `please provide a path to an existing ssl certificate file with --ssl-cert` + ); } - var options = { + + let options = { key: fs.readFileSync(this.startOptions.sslKey), - cert: fs.readFileSync(this.startOptions.sslCert) + cert: fs.readFileSync(this.startOptions.sslCert), }; + return this.https.createServer(options, this.app); - }, + } - listen: function(port, host) { - var server = this.httpServer; - return new Promise(function(resolve, reject) { + listen(port, host) { + let server = this.httpServer; + + return new Promise((resolve, reject) => { server.listen(port, host); - server.on('listening', resolve); + server.on('listening', () => { + resolve(); + this.emit('listening'); + }); server.on('error', reject); }); - }, + } - processAddonMiddlewares: function(options) { + processAddonMiddlewares(options) { this.project.initializeAddons(); - return mapSeries(this.project.addons, function(addon) { - if (addon.serverMiddleware) { - return addon.serverMiddleware({ - app: this.app, - options: options - }); - } - }, this); - }, - processAppMiddlewares: function(options) { + return mapSeries( + this.project.addons, + function (addon) { + if (addon.serverMiddleware) { + return addon.serverMiddleware({ + app: this.app, + options, + }); + } + }, + this + ); + } + + processAppMiddlewares(options) { if (this.project.has(this.serverRoot)) { - var server = this.project.require(this.serverRoot); - if (typeof server !== 'function') { - throw new TypeError('ember-cli expected ./server/index.js to be the entry for your mock or proxy server'); - } - if (server.length === 3) { - // express app is function of form req, res, next - return this.app.use(server); + try { + let server = this.project.require(this.serverRoot); + + if (typeof server !== 'function') { + throw new TypeError('ember-cli expected ./server/index.js to be the entry for your mock or proxy server'); + } + + if (server.length === 3) { + // express app is function of form req, res, next + return this.app.use(server); + } + + return server(this.app, options); + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } } - return server(this.app, options); } - }, + } - start: function(options) { - options.project = this.project; - options.watcher = this.watcher; + start(options) { + options.project = this.project; + options.watcher = this.watcher; options.serverWatcher = this.serverWatcher; - options.ui = this.ui; + options.ui = this.ui; this.startOptions = options; @@ -122,103 +151,131 @@ module.exports = Task.extend({ this.serverWatcher.on('delete', this.serverWatcherDidChange.bind(this)); } - return this.startHttpServer() - .then(function () { - var baseURL = cleanBaseURL(options.baseURL); - - options.ui.writeLine('Serving on http' + (options.ssl ? 's' : '') + '://' + this.displayHost(options.host) + ':' + options.port + baseURL); - }.bind(this)); - }, + return this.startHttpServer(); + } - serverWatcherDidChange: function() { + serverWatcherDidChange() { this.scheduleServerRestart(); - }, - - restartHttpServer: function() { - if (!this.serverRestartPromise) { - this.serverRestartPromise = - this.stopHttpServer() - .then(function () { - this.invalidateCache(this.serverRoot); - return this.startHttpServer(); - }.bind(this)) - .then(function () { - this.emitter.emit('restart'); - this.ui.writeLine(''); - this.ui.writeLine(chalk.green('Server restarted.')); - this.ui.writeLine(''); - }.bind(this)) - .catch(function (err) { - this.ui.writeError(err); - }.bind(this)) - .finally(function () { - this.serverRestartPromise = null; - }.bind(this)); - return this.serverRestartPromise; - } else { - return this.serverRestartPromise.then(function () { - return this.restartHttpServer(); - }.bind(this)); + } + + restartHttpServer() { + if (this.serverRestartPromise) { + return this.serverRestartPromise.then(() => this.restartHttpServer()); } - }, - stopHttpServer: function() { - return new Promise(function (resolve, reject) { + this.serverRestartPromise = (async () => { + try { + await this.stopHttpServer(); + this.invalidateCache(this.serverRoot); + await this.startHttpServer(); + + this.emit('restart'); + this.ui.writeLine(''); + this.ui.writeLine(chalk.green('Server restarted.')); + this.ui.writeLine(''); + } catch (err) { + this.ui.writeError(err); + } finally { + this.serverRestartPromise = null; + } + })(); + + return this.serverRestartPromise; + } + + stopHttpServer() { + return new Promise((resolve, reject) => { if (!this.httpServer) { return resolve(); } - this.httpServer.close(function (err) { + this.httpServer.close((err) => { if (err) { reject(err); return; } this.httpServer = null; resolve(); - }.bind(this)); + }); - // We have to force close all sockets in order to get an fast restart - var sockets = this.sockets; - for (var socketId in sockets) { + // We have to force close all sockets in order to get a fast restart + let sockets = this.sockets; + for (let socketId in sockets) { sockets[socketId].destroy(); } - }.bind(this)); - }, + }); + } - startHttpServer: function() { + async startHttpServer() { this.app = this.express(); - this.app.use(require('compression')()); + const compression = require('compression'); + this.app.use( + compression({ + filter(req, res) { + let type = res.getHeader('Content-Type'); + + if (res.getHeader('x-no-compression')) { + // don't compress responses with this response header + return false; + } else if (type && type.indexOf('text/event-stream') > -1) { + // don't attempt to compress server sent events + return false; + } else { + return compression.filter(req, res); + } + }, + }) + ); this.setupHttpServer(); - var options = this.startOptions; + let options = this.startOptions; options.httpServer = this.httpServer; + let liveReloadServer; + + if (options.path) { + liveReloadServer = { + setupMiddleware() {}, + }; + } else { + liveReloadServer = new LiveReloadServer({ + app: this.app, + ui: options.ui, + watcher: options.watcher, + project: options.project, + httpServer: options.httpServer, + }); + } + + const config = this.project.config(options.environment); + const middlewareOptions = Object.assign({}, options, { + rootURL: config.rootURL, + }); + + await liveReloadServer.setupMiddleware(this.startOptions); + await this.processAppMiddlewares(middlewareOptions); + await this.processAddonMiddlewares(middlewareOptions); + + return this.listen(options.port, options.host).catch(() => { + throw new SilentError( + `Could not serve on http://${this.displayHost(options.host)}:${options.port}. ` + + `It is either in use or you do not have permission.` + ); + }); + } - return Promise.resolve() - .then(function(){ - return this.processAppMiddlewares(options); - }.bind(this)) - .then(function(){ - return this.processAddonMiddlewares(options); - }.bind(this)) - .then(function(){ - return this.listen(options.port, options.host) - .catch(function() { - throw new SilentError('Could not serve on http://' + this.displayHost(options.host) + ':' + options.port + '. It is either in use or you do not have permission.'); - }.bind(this)); - }.bind(this)); - }, - - invalidateCache: function (serverRoot) { - var absoluteServerRoot = path.resolve(serverRoot); + invalidateCache(serverRoot) { + let absoluteServerRoot = path.resolve(serverRoot); if (absoluteServerRoot[absoluteServerRoot.length - 1] !== path.sep) { absoluteServerRoot += path.sep; } - var allKeys = Object.keys(require.cache); - for (var i = 0; i < allKeys.length; i++) { + let allKeys = Object.keys(require.cache); + for (let i = 0; i < allKeys.length; i++) { if (allKeys[i].indexOf(absoluteServerRoot) === 0) { delete require.cache[allKeys[i]]; } } } -}); +} + +module.exports = ExpressServerTask; diff --git a/lib/tasks/server/livereload-server.js b/lib/tasks/server/livereload-server.js index 8c34a928af..c59d5078cc 100644 --- a/lib/tasks/server/livereload-server.js +++ b/lib/tasks/server/livereload-server.js @@ -1,116 +1,236 @@ 'use strict'; -var Promise = require('../../ext/promise'); -var path = require('path'); -var fs = require('fs'); -var Task = require('../../models/task'); -var SilentError = require('silent-error'); - -function createServer(options) { - var instance; - - var Server = (require('tiny-lr')).Server; - Server.prototype.error = function() { - instance.error.apply(instance, arguments); - }; - instance = new Server(options); - return instance; +const SilentError = require('silent-error'); +const walkSync = require('walk-sync'); +const path = require('path'); +const FSTree = require('fs-tree-diff'); +const logger = require('heimdalljs-logger')('ember-cli:live-reload:'); +const fs = require('fs'); +const cleanBaseUrl = require('clean-base-url'); +const isLiveReloadRequest = require('../../utilities/is-live-reload-request'); + +function isNotRemoved(entryTuple) { + let operation = entryTuple[0]; + return operation !== 'unlink' && operation !== 'rmdir'; } -module.exports = Task.extend({ - liveReloadServer: function(options) { - if (this._liveReloadServer) { - return this._liveReloadServer; - } +function isNotDirectory(entryTuple) { + let entry = entryTuple[2]; + return entry && !entry.isDirectory(); +} - this._liveReloadServer = createServer(options); - return this._liveReloadServer; - }, +function relativePath(patch) { + return patch[1]; +} +function isNotSourceMapFile(file) { + return !/\.map$/.test(file); +} - listen: function(options) { - var server = this.liveReloadServer(options); +function readSSLandCert(sslKey, sslCert) { + if (!fs.existsSync(sslKey)) { + throw new TypeError( + `SSL key couldn't be found in "${sslKey}", ` + `please provide a path to an existing ssl key file with --ssl-key` + ); + } - return new Promise(function(resolve, reject) { - server.error = reject; - server.listen(options.port, options.host, resolve); - }); - }, + if (!fs.existsSync(sslCert)) { + throw new TypeError( + `SSL certificate couldn't be found in "${sslCert}", ` + + `please provide a path to an existing ssl certificate file with --ssl-cert` + ); + } - start: function(options) { - var tlroptions = {}; + let key = fs.readFileSync(sslKey); + let cert = fs.readFileSync(sslCert); + return { key, cert }; +} + +const DEFAULT_PREFIX = '/'; + +module.exports = class LiveReloadServer { + constructor({ app, watcher, ui, project, httpServer }) { + this.app = app; + this.watcher = watcher; + this.ui = ui; + this.project = project; + this.httpServer = httpServer; + this.liveReloadPrefix = DEFAULT_PREFIX; + } - tlroptions.ssl = options.ssl; - tlroptions.host = options.liveReloadHost || options.host; - tlroptions.port = options.liveReloadPort; + setupMiddleware(options) { + const tinylr = require('tiny-lr'); + const Server = tinylr.Server; - if (options.liveReload !== true) { - return Promise.resolve('Livereload server manually disabled.'); + if (options.liveReloadPrefix) { + this.liveReloadPrefix = cleanBaseUrl(options.liveReloadPrefix); } - if (options.ssl) { - tlroptions.key = fs.readFileSync(options.sslKey); - tlroptions.cert = fs.readFileSync(options.sslCert); + if (options.liveReload) { + if (options.liveReloadPort && options.port !== options.liveReloadPort) { + return this.createServerforCustomPort(options, Server).catch((error) => { + if (error !== null && typeof error === 'object' && error.code === 'EADDRINUSE') { + let url = `http${options.ssl ? 's' : ''}://${this.displayHost(options.liveReloadHost)}:${ + options.liveReloadPort + }`; + throw new SilentError(`${error.message}\nLivereload failed on '${url}', It may be in use.`); + } else { + throw error; + } + }); + } else { + this.liveReloadServer = this.createServer(options, Server); + } + this.start(); + } else { + this.ui.writeWarnLine('Livereload server manually disabled.'); } + return Promise.resolve(); + } + start() { + this.tree = FSTree.fromEntries([]); // Reload on file changes - this.watcher.on('change', this.didChange.bind(this)); + this.watcher.on( + 'change', + function () { + try { + this.didChange.apply(this, arguments); + } catch (e) { + this.ui.writeError(e); + } + }.bind(this) + ); + this.watcher.on('error', this.didChange.bind(this)); // Reload on express server restarts - this.expressServer.on('restart', this.didRestart.bind(this)); + this.app.on('restart', this.didRestart.bind(this)); + this.httpServer.on('upgrade', (req, socket, head) => { + if (isLiveReloadRequest(req.url, this.liveReloadPrefix)) { + this.liveReloadServer.websocketify(req, socket, head); + } + }); + this.httpServer.on('error', this.liveReloadServer.error.bind(this.liveReloadServer)); + this.httpServer.on('close', this.liveReloadServer.close.bind(this.liveReloadServer)); + this.app.use(this.liveReloadPrefix, this.liveReloadServer.handler.bind(this.liveReloadServer)); + } + + createServer(options, Server) { + let serverOptions = { + app: this.app, + dashboard: 'false', + prefix: DEFAULT_PREFIX, + port: options.port, + }; + if (options.ssl) { + let { key, cert } = readSSLandCert(options.sslKey, options.sslCert); + serverOptions.key = key; + serverOptions.cert = cert; + } + let lrServer = new Server(serverOptions); + + // this is required to prevent tiny-lr from triggering an error + // when checking this.server._handle during its close handler + // here: https://github.com/mklabs/tiny-lr/blob/d68d983eaf80b5bae78b2dba259a1ad5e3b03a63/lib/server.js#L209 + lrServer.server = this.httpServer; + + return lrServer; + } - var url = 'http' + (options.ssl ? 's' : '') + '://' + this.displayHost(tlroptions.host) + ':' + tlroptions.port; - // Start LiveReload server - return this.listen(tlroptions) - .then(this.writeBanner.bind(this, url)) - .catch(this.writeErrorBanner.bind(this, url)); - }, + createServerforCustomPort(options, Server) { + let instance; + Server.prototype.error = function () { + instance.error.apply(instance, arguments); + }; + let serverOptions = { + dashboard: 'false', + prefix: options.liveReloadPrefix, + port: options.liveReloadPort, + host: options.liveReloadHost, + }; - displayHost: function(specifiedHost) { - return specifiedHost === '0.0.0.0' ? 'localhost' : specifiedHost; - }, + if (options.ssl) { + let { key, cert } = readSSLandCert(options.sslKey, options.sslCert); + serverOptions.key = key; + serverOptions.cert = cert; + } - writeBanner: function(url) { - this.ui.writeLine('Livereload server on ' + url); - }, + instance = new Server(serverOptions); + this.liveReloadServer = instance; + this.start(); + return new Promise((resolve, reject) => { + this.liveReloadServer.error = reject; + this.liveReloadServer.listen(options.liveReloadPort, options.liveReloadHost, resolve); + }); + } - writeErrorBanner: function(url) { - throw new SilentError('Livereload failed on ' + url + '. It is either in use or you do not have permission.'); - }, + displayHost(specifiedHost) { + return specifiedHost || 'localhost'; + } - didChange: function(results) { - var filePath = path.relative(this.project.root, results.filePath || ''); + writeSkipBanner(filePath) { + this.ui.writeLine(`Skipping livereload for: ${filePath}`); + } - var canTrigger = this.project.liveReloadFilterPatterns.reduce(function(bool, pattern) { - bool = bool && !filePath.match(pattern); - return bool; - }, true); + getDirectoryEntries(directory) { + return walkSync.entries(directory); + } - if (canTrigger) { - this.liveReloadServer().changed({ - body: { - files: ['LiveReload files'] - } - }); + shouldTriggerReload(options) { + let result = true; - this.analytics.track({ - name: 'broccoli watcher', - message: 'live-reload' + if (this.project.liveReloadFilterPatterns.length > 0) { + let filePath = path.relative(this.project.root, options.filePath || ''); + + result = this.project.liveReloadFilterPatterns.every((pattern) => pattern.test(filePath) === false); + + if (result === false) { + this.writeSkipBanner(filePath); + } + } + + return result; + } + + didChange(results) { + let previousTree = this.tree; + let files; + + if (results.stack) { + this._hasCompileError = true; + files = ['LiveReload due to compile error']; + } else if (this._hasCompileError) { + this._hasCompileError = false; + files = ['LiveReload due to resolved compile error']; + } else if (results.directory) { + this.tree = FSTree.fromEntries(this.getDirectoryEntries(results.directory), { sortAndExpand: true }); + files = previousTree + .calculatePatch(this.tree) + .filter(isNotRemoved) + .filter(isNotDirectory) + .map(relativePath) + .filter(isNotSourceMapFile); + } else { + files = ['LiveReload files']; + } + + logger.info('files %o', files); + + if (this.shouldTriggerReload(results)) { + this.liveReloadServer.changed({ + body: { + files, + }, }); } - }, + } - didRestart: function() { - this.liveReloadServer().changed({ + didRestart() { + this.liveReloadServer.changed({ body: { - files: ['LiveReload files'] - } - }); - - this.analytics.track({ - name: 'express server', - message: 'live-reload' + files: ['LiveReload files'], + }, }); } -}); +}; diff --git a/lib/tasks/server/middleware/broccoli-serve-files/index.js b/lib/tasks/server/middleware/broccoli-serve-files/index.js new file mode 100644 index 0000000000..730687005f --- /dev/null +++ b/lib/tasks/server/middleware/broccoli-serve-files/index.js @@ -0,0 +1,33 @@ +'use strict'; + +const logger = require('heimdalljs-logger')('ember-cli:broccoli-serve-files'); + +class ServeFilesAddon { + /** + * This addon is used to serve the requested assets and set the required response + * headers. It runs after broccoli-watcher addon. + * + * @class ServeFilesAddon + * @constructor + */ + constructor(project) { + this.project = project; + this.name = 'broccoli-serve-files'; + } + + serverMiddleware(options) { + let app = options.app; + options = options.options; + + let serveAssetMiddleware = require('broccoli-middleware').serveAssetMiddleware; + + app.use((req, res, next) => { + logger.info('serving asset: %s', req.url); + + // serve the asset and close the response. + serveAssetMiddleware(req, res, next); + }); + } +} + +module.exports = ServeFilesAddon; diff --git a/lib/tasks/server/middleware/broccoli-serve-files/package.json b/lib/tasks/server/middleware/broccoli-serve-files/package.json new file mode 100644 index 0000000000..b9b02c8190 --- /dev/null +++ b/lib/tasks/server/middleware/broccoli-serve-files/package.json @@ -0,0 +1,14 @@ +{ + "name": "broccoli-serve-files", + "keywords": [ + "ember-addon" + ], + "ember-addon": { + "after": "broccoli-watcher", + "before": "proxy-server-middleware" + }, + "dependencies": { + "heimdalljs-logger": "*", + "broccoli-middleware": "*" + } +} diff --git a/lib/tasks/server/middleware/broccoli-watcher/index.js b/lib/tasks/server/middleware/broccoli-watcher/index.js new file mode 100644 index 0000000000..e495c91be2 --- /dev/null +++ b/lib/tasks/server/middleware/broccoli-watcher/index.js @@ -0,0 +1,64 @@ +'use strict'; + +const cleanBaseURL = require('clean-base-url'); +const logger = require('heimdalljs-logger')('ember-cli:broccoli-watcher'); + +class WatcherAddon { + /** + * This addon is used to set the default response headers for the assets that will be + * served by the next middleware. It waits for the watcher promise to resolve before + * setting the response headers. + * + * @class WatcherAddon + * @constructor + */ + constructor(project) { + this.project = project; + this.name = 'broccoli-watcher'; + } + + serverMiddleware(options) { + let app = options.app; + options = options.options; + + let broccoliMiddleware = options.middleware || require('broccoli-middleware').watcherMiddleware; + let middleware = broccoliMiddleware(options.watcher, { + liveReloadPath: '/ember-cli-live-reload.js', + autoIndex: false, // disable directory listings + }); + + let rootURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL); + + logger.info('serverMiddleware: rootURL: %s', rootURL); + + app.use((req, res, next) => { + let oldURL = req.url; + let url = req.serveUrl || req.url; + logger.info('serving: %s', url); + + let actualPrefix = req.url.slice(0, rootURL.length - 1); // Don't care + let expectedPrefix = rootURL.slice(0, rootURL.length - 1); // about last slash + + if (actualPrefix === expectedPrefix) { + let urlToBeServed = url.slice(actualPrefix.length); // Remove rootURL prefix + req.url = urlToBeServed; + logger.info('serving: (prefix stripped) %s, was: %s', urlToBeServed, url); + + // Find asset and set response headers, if no file has been found, reset url for proxy stuff + // that comes afterwards + middleware(req, res, (err) => { + req.url = oldURL; + if (err) { + logger.error('err', err); + } + next(err); + }); + } else { + logger.info("prefixes didn't match, passing control on: (actual:%s expected:%s)", actualPrefix, expectedPrefix); + next(); + } + }); + } +} + +module.exports = WatcherAddon; diff --git a/lib/tasks/server/middleware/broccoli-watcher/package.json b/lib/tasks/server/middleware/broccoli-watcher/package.json new file mode 100644 index 0000000000..f7f7c7299d --- /dev/null +++ b/lib/tasks/server/middleware/broccoli-watcher/package.json @@ -0,0 +1,12 @@ +{ + "name": "broccoli-watcher", + "keywords": [ + "ember-addon" + ], + "ember-addon": {}, + "dependencies": { + "clean-base-url": "*", + "heimdalljs-logger": "*", + "broccoli-middleware": "*" + } +} diff --git a/lib/tasks/server/middleware/history-support/index.js b/lib/tasks/server/middleware/history-support/index.js index d2d95ec673..b5950c7fec 100644 --- a/lib/tasks/server/middleware/history-support/index.js +++ b/lib/tasks/server/middleware/history-support/index.js @@ -1,54 +1,101 @@ 'use strict'; -var path = require('path'); -var fs = require('fs'); +const path = require('path'); +const fs = require('fs'); +const cleanBaseURL = require('clean-base-url'); -var cleanBaseURL = require('clean-base-url'); - -function HistorySupportAddon(project) { - this.project = project; - this.name = 'history-support-middleware'; -} +class HistorySupportAddon { + /** + * This addon is used to serve the `index.html` file at every requested + * URL that begins with `rootURL` and is expecting `text/html` output. + * + * @class HistorySupportAddon + * @constructor + */ + constructor(project) { + this.project = project; + this.name = 'history-support-middleware'; + } -HistorySupportAddon.prototype.shouldAddMiddleware = function(environment) { - var config = this.project.config(environment); - var locationType = config.locationType; - var historySupportMiddlewareEnabled = config.historySupportMiddleware; + shouldAddMiddleware(environment) { + let config = this.project.config(environment); + let locationType = config.locationType; + let historySupportMiddleware = config.historySupportMiddleware; - return ['auto', 'history'].indexOf(locationType) !== -1 || historySupportMiddlewareEnabled; -}; + if (typeof historySupportMiddleware === 'boolean') { + return historySupportMiddleware; + } -HistorySupportAddon.prototype.serverMiddleware = function(config) { - if (this.shouldAddMiddleware(config.options.environment)) { - this.addMiddleware(config); + return ['auto', 'history'].indexOf(locationType) !== -1; } -}; -HistorySupportAddon.prototype.addMiddleware = function(config) { - var app = config.app; - var options = config.options; - var watcher = options.watcher; + serverMiddleware(config) { + if (this.shouldAddMiddleware(config.options.environment)) { + this.project.ui.writeWarnLine( + 'Empty `rootURL` is not supported. Disable history support, or use an absolute `rootURL`', + config.options.rootURL !== '' + ); + + this.addMiddleware(config); + } + } - var baseURL = cleanBaseURL(options.baseURL); - var baseURLRegexp = new RegExp('^' + baseURL); + addMiddleware(config) { + let app = config.app; + let options = config.options; + let watcher = options.watcher; + let rootURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL); - app.use(function(req, res, next) { - watcher.then(function(results) { + app.use(async (req, _, next) => { + try { + let results; + try { + results = await watcher; + } catch (e) { + // This means there was a build error, so we won't actually be serving + // index.html, and we have nothing to do. We have to catch it here, + // though, or it will go uncaught and cause the process to exit. + return; + } - var acceptHeaders = req.headers.accept || []; - var hasHTMLHeader = acceptHeaders.indexOf('text/html') !== -1; - var isForBaseURL = baseURLRegexp.test(req.path); + if (this.shouldHandleRequest(req, options)) { + let assetPath = req.path.slice(rootURL.length); + let isFile = false; - if (hasHTMLHeader && isForBaseURL && req.method === 'GET') { - var assetPath = req.path.slice(baseURL.length); - var isFile = false; - try { isFile = fs.statSync(path.join(results.directory, assetPath)).isFile(); } catch (err) { } - if (!isFile) { - req.serveUrl = baseURL + 'index.html'; + try { + isFile = fs.statSync(path.join(results.directory, assetPath)).isFile(); + } catch (err) { + /* ignore */ + } + if (!isFile) { + req.serveUrl = `${rootURL}index.html`; + } } + } finally { + next(); } - }).finally(next); - }); -}; + }); + } + + shouldHandleRequest(req, options) { + let acceptHeaders = req.headers.accept || []; + let hasHTMLHeader = acceptHeaders.indexOf('text/html') !== -1; + if (req.method !== 'GET') { + return false; + } + if (!hasHTMLHeader) { + return false; + } + let rootURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL); + if (req.path.startsWith(rootURL)) { + return true; + } + // exactly match the rootURL without a trailing slash + if (req.path === rootURL.slice(0, -1)) { + return true; + } + return false; + } +} module.exports = HistorySupportAddon; diff --git a/lib/tasks/server/middleware/history-support/package.json b/lib/tasks/server/middleware/history-support/package.json index f99c5f6572..ad739c5053 100644 --- a/lib/tasks/server/middleware/history-support/package.json +++ b/lib/tasks/server/middleware/history-support/package.json @@ -4,6 +4,9 @@ "ember-addon" ], "ember-addon": { - "before": "serve-files-middleware" + "before": "broccoli-watcher" + }, + "dependencies": { + "clean-base-url": "*" } } diff --git a/lib/tasks/server/middleware/proxy-server/index.js b/lib/tasks/server/middleware/proxy-server/index.js index da89ce91b2..f84a892e66 100644 --- a/lib/tasks/server/middleware/proxy-server/index.js +++ b/lib/tasks/server/middleware/proxy-server/index.js @@ -1,41 +1,60 @@ 'use strict'; -function ProxyServerAddon(project) { - this.project = project; - this.name = 'proxy-server-middleware'; -} - -ProxyServerAddon.prototype.serverMiddleware = function(options) { - var app = options.app, server = options.options.httpServer; - options = options.options; - - if (options.proxy) { - var proxy = require('http-proxy').createProxyServer({ - target: options.proxy, - ws: true, - secure: !options.insecureProxy, - changeOrigin: true - }); - - proxy.on('error', function (e) { - options.ui.writeLine('Error proxying to ' + options.proxy); - options.ui.writeError(e); - }); +// eslint-disable-next-line +const isLiveReloadRequest = require('../../../../utilities/is-live-reload-request'); - var morgan = require('morgan'); +class ProxyServerAddon { + constructor(project) { + this.project = project; + this.name = 'proxy-server-middleware'; + } - options.ui.writeLine('Proxying to ' + options.proxy); + serverMiddleware(options) { + let app = options.app, + server = options.options.httpServer; + options = options.options; + + if (options.proxy) { + let proxy = require('http-proxy').createProxyServer({ + target: options.proxy, + ws: true, + secure: options.secureProxy, + changeOrigin: true, + xfwd: options.transparentProxy, + preserveHeaderKeyCase: true, + proxyTimeout: options.proxyOutTimeout, + timeout: options.proxyInTimeout, + }); + + proxy.on('error', (e, req, res) => { + options.ui.writeLine(`Error proxying to ${options.proxy}`); + options.ui.writeError(e); + if (typeof res?.status === 'function') { + res.status(502); + res.end(); + } + }); + + const morgan = require('morgan'); + + options.ui.writeLine(`Proxying to ${options.proxy}`); + + server.on('upgrade', (req, socket, head) => { + this.handleProxiedRequest({ req, socket, head, options, proxy }); + }); + + app.use(morgan('dev')); + app.use((req, res) => proxy.web(req, res)); + } + } - server.on('upgrade', function (req, socket, head) { - options.ui.writeLine('Proxying websocket to ' + req.url); + handleProxiedRequest({ req, socket, head, options, proxy }) { + if (!isLiveReloadRequest(req.url, options.liveReloadPrefix)) { + options.ui.writeLine(`Proxying websocket to ${req.url}`); + socket.on('error', (e) => proxy.emit('error', e)); proxy.ws(req, socket, head); - }); - - app.use(morgan('dev')); - app.use(function(req, res) { - return proxy.web(req, res); - }); + } } -}; +} module.exports = ProxyServerAddon; diff --git a/lib/tasks/server/middleware/proxy-server/package.json b/lib/tasks/server/middleware/proxy-server/package.json index f6eb2cbee0..040a92a75b 100644 --- a/lib/tasks/server/middleware/proxy-server/package.json +++ b/lib/tasks/server/middleware/proxy-server/package.json @@ -2,5 +2,9 @@ "name": "proxy-server-middleware", "keywords": [ "ember-addon" - ] + ], + "dependencies": { + "http-proxy": "*", + "morgan": "*" + } } diff --git a/lib/tasks/server/middleware/serve-files/index.js b/lib/tasks/server/middleware/serve-files/index.js deleted file mode 100644 index 65ef99017a..0000000000 --- a/lib/tasks/server/middleware/serve-files/index.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -var cleanBaseURL = require('clean-base-url'); -var debug = require('debug')('ember-cli:serve-files'); - -function ServeFilesAddon(project) { - this.project = project; - this.name = 'serve-files-middleware'; -} - -ServeFilesAddon.prototype.serverMiddleware = function(options) { - var app = options.app; - options = options.options; - debug('serverMiddleware: %s', options.outputPath); - - var broccoliMiddleware = options.middleware || require('broccoli/lib/middleware'); - var middleware = broccoliMiddleware(options.watcher, { - liveReloadPath: '/ember-cli-live-reload.js', - autoIndex: false // disable directory listings - }); - - var baseURL = cleanBaseURL(options.baseURL); - - debug('serverMiddleware: output: %s baseURL: %s', options.outputPath, baseURL); - - app.use(function(req, res, next) { - var oldURL = req.url; - var url = req.serveUrl || req.url; - debug('serving: %s', url); - - var actualPrefix = req.url.slice(0, baseURL.length - 1); // Don't care - var expectedPrefix = baseURL.slice(0, baseURL.length - 1); // about last slash - - if (actualPrefix === expectedPrefix) { - req.url = url.slice(actualPrefix.length); // Remove baseURL prefix - debug('serving: (prefix stripped) %s', req.url); - - // Serve file, if no file has been found, reset url for proxy stuff - // that comes afterwards - middleware(req, res, function(err) { - req.url = oldURL; - if (err) { - debug('err', err); - } - next(err); - }); - } else { - debug('prefixes didn\'t match, passing control on: (actual:%s expected:%s)', actualPrefix, expectedPrefix); - next(); - } - }); -}; - -module.exports = ServeFilesAddon; diff --git a/lib/tasks/server/middleware/serve-files/package.json b/lib/tasks/server/middleware/serve-files/package.json deleted file mode 100644 index ffd93f3405..0000000000 --- a/lib/tasks/server/middleware/serve-files/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "serve-files-middleware", - "keywords": [ - "ember-addon" - ], - "ember-addon": { - "before": "proxy-server-middleware" - } -} diff --git a/lib/tasks/server/middleware/testem-url-rewriter/index.js b/lib/tasks/server/middleware/testem-url-rewriter/index.js new file mode 100644 index 0000000000..4118a64dfd --- /dev/null +++ b/lib/tasks/server/middleware/testem-url-rewriter/index.js @@ -0,0 +1,36 @@ +'use strict'; + +const cleanBaseURL = require('clean-base-url'); +const logger = require('heimdalljs-logger')('ember-cli:testem-url-rewriter'); + +class TestemUrlRewriterAddon { + constructor(project) { + this.name = 'testem-url-rewriter'; + this.project = project; + } + + testemMiddleware(app) { + let env = process.env.EMBER_ENV; + logger.info('Reading config for environment "%s"', env); + + let config = this.project.config(env); + logger.info('config.rootURL = %s', config.rootURL); + + let rootURL = cleanBaseURL(config.rootURL) || '/'; + logger.info('rootURL = %s', rootURL); + + app.use((req, res, next) => { + let oldUrl = req.url; + if (rootURL !== '/' && oldUrl.indexOf(rootURL) === 0) { + req.url = `/${oldUrl.slice(rootURL.length)}`; + logger.info('Rewriting %s %s -> %s', req.method, oldUrl, req.url); + } else { + logger.info('Ignoring %s %s', req.method, req.url); + } + + next(); + }); + } +} + +module.exports = TestemUrlRewriterAddon; diff --git a/lib/tasks/server/middleware/testem-url-rewriter/package.json b/lib/tasks/server/middleware/testem-url-rewriter/package.json new file mode 100644 index 0000000000..1e9c320854 --- /dev/null +++ b/lib/tasks/server/middleware/testem-url-rewriter/package.json @@ -0,0 +1,10 @@ +{ + "name": "testem-url-rewriter-middleware", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "clean-base-url": "*", + "heimdalljs-logger": "*" + } +} diff --git a/lib/tasks/server/middleware/tests-server/index.js b/lib/tasks/server/middleware/tests-server/index.js index 1166873cb4..9a7e372e7b 100644 --- a/lib/tasks/server/middleware/tests-server/index.js +++ b/lib/tasks/server/middleware/tests-server/index.js @@ -1,46 +1,76 @@ 'use strict'; -var cleanBaseURL = require('clean-base-url'); -var existsSync = require('exists-sync'); -var path = require('path'); -var fs = require('fs'); - -function TestsServerAddon(project) { - this.project = project; - this.name = 'tests-server-middleware'; -} - -TestsServerAddon.prototype.serverMiddleware = function(config) { - var app = config.app; - var options = config.options; - var watcher = options.watcher; +const cleanBaseURL = require('clean-base-url'); +const path = require('path'); +const fs = require('fs'); +const logger = require('heimdalljs-logger')('ember-cli:test-server'); - var baseURL = cleanBaseURL(options.baseURL); - var testsRegexp = new RegExp('^' + baseURL + 'tests'); +module.exports = class TestsServerAddon { + /** + * This addon is used to serve the QUnit or Mocha test runner + * at `rootURL + '/tests'`. + * + * @class TestsServerAddon + * @constructor + */ + constructor(project) { + this.project = project; + this.name = 'tests-server-middleware'; + } - app.use(function(req, res, next) { - watcher.then(function(results) { - var acceptHeaders = req.headers.accept || []; - var hasHTMLHeader = acceptHeaders.indexOf('text/html') !== -1; - var hasWildcardHeader = acceptHeaders.indexOf('*/*') !== -1; + serverMiddleware(config) { + let app = config.app; + let options = config.options; + let watcher = options.watcher; + let rootURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL); + let testsPath = `${rootURL}tests`; - var isForTests = testsRegexp.test(req.path); + app.use(async (req, _, next) => { + let results; + let watcherSuccess; - if (isForTests && (hasHTMLHeader || hasWildcardHeader) && req.method === 'GET') { - var assetPath = req.path.slice(baseURL.length); - var filePath = path.join(results.directory, assetPath); + try { + results = await watcher; + watcherSuccess = true; + } catch (e) { + watcherSuccess = false; + // the build has failed, the middleware can safely be skipped. + // build error reporting is handled by: + // 1. watcher production stderr + // 2. watcher-middleware serving the error page + } - if(!existsSync(filePath) || !fs.lstatSync(filePath).isFile()) { - req.url = baseURL + '/tests/index.html'; - } + if (watcherSuccess) { + rewriteRequestUrlIfBasePageIsNeeded(req, testsPath, rootURL, results.directory); } - }).finally(next).finally(function() { + next(); + if (config.finally) { config.finally(); } }); - }); + } }; -module.exports = TestsServerAddon; +function rewriteRequestUrlIfBasePageIsNeeded(req, testsPath, rootURL, directory) { + let acceptHeaders = req.headers.accept || []; + let hasHTMLHeader = acceptHeaders.indexOf('text/html') !== -1; + let hasWildcardHeader = acceptHeaders.indexOf('*/*') !== -1; + + let isForTests = req.path.indexOf(testsPath) === 0; + + logger.info('isForTests: %o', isForTests); + + if (isForTests && (hasHTMLHeader || hasWildcardHeader) && req.method === 'GET') { + let assetPath = req.path.slice(rootURL.length); + let filePath = path.join(directory, assetPath); + + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + // N.B., `rootURL` will end with a slash as it went through `cleanBaseURL` + let newURL = `${rootURL}tests/index.html`; + logger.info('url: %s resolved to path: %s which is not a file. Assuming %s instead', req.path, filePath, newURL); + req.url = newURL; + } + } +} diff --git a/lib/tasks/server/middleware/tests-server/package.json b/lib/tasks/server/middleware/tests-server/package.json index e0f0c866a7..258449ef43 100644 --- a/lib/tasks/server/middleware/tests-server/package.json +++ b/lib/tasks/server/middleware/tests-server/package.json @@ -5,5 +5,9 @@ ], "ember-addon": { "before": "history-support-middleware" + }, + "dependencies": { + "clean-base-url": "*", + "heimdalljs-logger": "*" } } diff --git a/lib/tasks/show-asset-sizes.js b/lib/tasks/show-asset-sizes.js new file mode 100644 index 0000000000..1326714c90 --- /dev/null +++ b/lib/tasks/show-asset-sizes.js @@ -0,0 +1,21 @@ +'use strict'; + +const Task = require('../models/task'); +const AssetSizePrinter = require('../models/asset-size-printer'); + +class ShowAssetSizesTask extends Task { + run(options) { + let sizePrinter = new AssetSizePrinter({ + ui: this.ui, + outputPath: options.outputPath, + }); + + if (options.json) { + return sizePrinter.printJSON(); + } + + return sizePrinter.print(); + } +} + +module.exports = ShowAssetSizesTask; diff --git a/lib/tasks/test-server.js b/lib/tasks/test-server.js index 758759a0c6..59f97afc50 100644 --- a/lib/tasks/test-server.js +++ b/lib/tasks/test-server.js @@ -1,51 +1,73 @@ 'use strict'; -var TestTask = require('./test'); -var Promise = require('../ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var chalk = require('chalk'); - -module.exports = TestTask.extend({ - init: function() { - this.testem = this.testem || new (require('testem'))(); - }, - - invokeTestem: function(options) { - var task = this; - - return new Promise(function(resolve) { - task.testem.startDev(task.testemOptions(options), function(code) { - remove(options.outputPath) - .finally(function() { - resolve(code); - }); +const TestTask = require('./test'); +const { default: chalk } = require('chalk'); +const SilentError = require('silent-error'); + +class TestServerTask extends TestTask { + invokeTestem(options) { + let task = this; + + return new Promise((resolve, reject) => { + task.testem.setDefaultOptions(task.defaultOptions(options)); + task.testem.startDev(task.transformOptions(options), (exitCode, error) => { + if (error) { + reject(error); + } else if (exitCode !== 0) { + reject(new SilentError('Testem finished with non-zero exit code. Tests failed.')); + } else { + resolve(exitCode); + } }); }); - }, + } - run: function(options) { - var ui = this.ui; - var testem = this.testem; - var task = this; + run(options) { + let ui = this.ui; + let testem = this.testem; + let task = this; // The building has actually started already, but we want some output while we wait for the server - ui.startProgress(chalk.green('Building'), chalk.green('.')); - return new Promise(function(resolve) { - var watcher = options.watcher; - var started = false; + return new Promise((resolve, reject) => { + if (options.path) { + resolve(task.invokeTestem(options)); + return; + } + + ui.startProgress(chalk.green('Building'), chalk.green('.')); + + let watcher = options.watcher; + let started = false; // Wait for a build and then either start or restart testem - watcher.on('change', function() { - if (started) { - testem.restart(); - } else { - started = true; + watcher.on('change', () => { + try { + if (started) { + testem.restart(); + } else { + started = true; - ui.stopProgress(); - resolve(task.invokeTestem(options)); + ui.stopProgress(); + resolve(task.invokeTestem(options)); + } + } catch (e) { + reject(e); } }); }); } -}); + + /** + * Exit silently + * + * @private + * @method onInterrupt + */ + onInterrupt() { + // We don't have any hanging promise to resolve here because the SIGINT is + // captured and handled by testem + } +} + +module.exports = TestServerTask; diff --git a/lib/tasks/test.js b/lib/tasks/test.js index c3785b628f..95f623f62b 100644 --- a/lib/tasks/test.js +++ b/lib/tasks/test.js @@ -1,51 +1,80 @@ 'use strict'; -var Task = require('../models/task'); -var Promise = require('../ext/promise'); -var SilentError = require('silent-error'); +const Task = require('../models/task'); +const SilentError = require('silent-error'); -module.exports = Task.extend({ - init: function() { +class TestTask extends Task { + constructor(options) { + super(options); this.testem = this.testem || new (require('testem'))(); - }, - invokeTestem: function (options) { - var testem = this.testem; - - return new Promise(function(resolve, reject) { - testem.startCI(this.testemOptions(options), function(exitCode) { - if (!testem.app.reporter.total) { - reject(new SilentError('No tests were run, please check whether any errors occurred in the page (ember test --server) and ensure that you have a test launcher (e.g. PhantomJS) enabled.')); - } + } - resolve(exitCode); + invokeTestem(options) { + let testem = this.testem; + let task = this; + + return new Promise((resolve, reject) => { + testem.setDefaultOptions(task.defaultOptions(options)); + testem.startCI(task.transformOptions(options), (exitCode, error) => { + if (error) { + reject(error); + } else if (exitCode !== 0) { + reject(new SilentError('Testem finished with non-zero exit code. Tests failed.')); + } else { + resolve(exitCode); + } }); - }.bind(this)); - }, + }); + } - addonMiddlewares: function() { + addonMiddlewares(options) { this.project.initializeAddons(); - return this.project.addons.reduce(function(addons, addon) { + return this.project.addons.reduce((addons, addon) => { if (addon.testemMiddleware) { - addons.push(addon.testemMiddleware.bind(addon)); + addons.push(function () { + addon.testemMiddleware(...arguments, options); + }); } return addons; }, []); - }, + } - testemOptions: function(options) { - return { - file: options.configFile, + defaultOptions(options) { + let defaultOptions = this.transformOptions(options); + defaultOptions.cwd = options.outputPath; + /* eslint-disable camelcase */ + defaultOptions.config_dir = process.cwd(); + /* eslint-enable camelcase */ + return defaultOptions; + } + + transformOptions(options) { + let transformed = { host: options.host, port: options.port, - cwd: options.outputPath, + debug: options.testemDebug, reporter: options.reporter, - middleware: this.addonMiddlewares() + middleware: this.addonMiddlewares(options), + launch: options.launch, + file: options.configFile, + /* eslint-disable camelcase */ + test_page: options.testPage, + query_params: options.queryString, + /* eslint-enable camelcase */ }; - }, - run: function(options) { + if (options.ssl) { + transformed.key = options.sslKey; + transformed.cert = options.sslCert; + } + return transformed; + } + + run(options) { return this.invokeTestem(options); } -}); +} + +module.exports = TestTask; diff --git a/lib/tasks/transforms/amd/amd-shim.js b/lib/tasks/transforms/amd/amd-shim.js new file mode 100644 index 0000000000..8477e704cc --- /dev/null +++ b/lib/tasks/transforms/amd/amd-shim.js @@ -0,0 +1,20 @@ +'use strict'; +const stew = require('broccoli-stew'); + +module.exports = function shimAmd(tree, nameMapping) { + return stew.map(tree, (content, relativePath) => { + let name = nameMapping[relativePath]; + let sections = [ + '(function(define){\n', + content, + '\n})((function(){ function newDefine(){ var args = Array.prototype.slice.call(arguments);', + ]; + if (name) { + sections.push(' args.unshift("'); + sections.push(name); + sections.push('");'); + } + sections.push(' return define.apply(null, args); }; newDefine.amd = true; return newDefine; })());'); + return sections.join(''); + }); +}; diff --git a/lib/tasks/transforms/amd/index.js b/lib/tasks/transforms/amd/index.js new file mode 100644 index 0000000000..d716db42e0 --- /dev/null +++ b/lib/tasks/transforms/amd/index.js @@ -0,0 +1,49 @@ +'use strict'; + +class AmdTransformAddon { + /** + * This addon is used to register a custom AMD transform for app and addons to use. + * + * @class AmdTransformAddon + * @constructor + */ + constructor(project) { + this.project = project; + this.name = 'amd-transform'; + } + + importTransforms() { + const shimAmd = require('./amd-shim'); + + return { + amd: { + transform: (tree, options) => { + let nameMapping = {}; + for (let relativePath in options) { + nameMapping[relativePath] = options[relativePath].as; + } + + let amdTransform = shimAmd(tree, nameMapping); + + return amdTransform; + }, + processOptions: (assetPath, entry, options) => { + // If the import is specified to be a different name we must break because of the broccoli rewrite behavior. + if (Object.keys(options).indexOf(assetPath) !== -1 && options[assetPath].as !== entry.as) { + throw new Error( + `Highlander error while importing ${assetPath}. You may not import an AMD transformed asset at different module names.` + ); + } + + options[assetPath] = { + as: entry.as, + }; + + return options; + }, + }, + }; + } +} + +module.exports = AmdTransformAddon; diff --git a/lib/tasks/transforms/amd/package.json b/lib/tasks/transforms/amd/package.json new file mode 100644 index 0000000000..469fca9aa2 --- /dev/null +++ b/lib/tasks/transforms/amd/package.json @@ -0,0 +1,10 @@ +{ + "name": "amd-transform", + "keywords": [ + "ember-addon" + ], + "ember-addon": {}, + "dependencies": { + "broccoli-stew": "*" + } +} diff --git a/lib/tasks/update.js b/lib/tasks/update.js deleted file mode 100644 index 691e254044..0000000000 --- a/lib/tasks/update.js +++ /dev/null @@ -1,118 +0,0 @@ -'use strict'; - -var Promise = require('../ext/promise'); -var chalk = require('chalk'); -var Task = require('../models/task'); -var fs = require('fs'); -var path = require('path'); - -function npmInstall(npm) { - return Promise.denodeify(npm.commands.install)(['ember-cli']); -} - -module.exports = Task.extend({ - init: function() { - this.npm = this.npm || require('npm'); - }, - - run: function(options, updateInfo) { - var env = options.environment || 'development'; - - process.env.EMBER_ENV = process.env.EMBER_ENV || env; - - this.ui.writeLine(''); - this.ui.writeLine(chalk.yellow('A new version of ember-cli is available (' + - updateInfo.newestVersion + ').')); - - var updatePrompt = { - type: 'confirm', - name: 'answer', - message: 'Are you sure you want to update ember-cli?', - choices: [ - { key: 'y', name: 'Yes, update', value: 'yes' }, - { key: 'n', name: 'No, cancel', value: 'no' } - ] - }; - - return this.ui.prompt(updatePrompt).then(function(response) { - if (response.answer === true) { - return this.runNpmUpdate(updateInfo.newestVersion); - } - }.bind(this)); - }, - - runNpmUpdate: function(newestVersion) { - this.ui.startProgress(chalk.green('Updating ember-cli'), chalk.green('.')); - - // first, run `npm install -g ember-cli` - var npm = this.npm; - var loadNPM = Promise.denodeify(npm.load); - - var stopProgress = (function() { - this.ui.stopProgress(); - }.bind(this)); - - var reportFailure = (function(reason) { - this.ui.writeLine('There was an error – possibly a permissions issue. You ' + - 'may need to manually run ' + - chalk.green('npm install -g ember-cli') + '.'); - throw reason; - }.bind(this)); - - var updateDependencies = (function() { - var pkg = this.project.pkg; - var packagePath = path.join(this.project.root, 'package.json'); - - if (!pkg) { - this.ui.write('There was an error locating your package.json file.'); - return false; - } - - pkg.devDependencies['ember-cli'] = newestVersion; - fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2)); - this.ui.writeLine(''); - this.ui.writeLine(chalk.green('✓ ember-cli was successfully updated!')); - - return this.showEmberInitPrompt(); - }.bind(this)); - - return loadNPM({ - loglevel: 'silent', - global: true - }) - .then(npmInstall) - .then(updateDependencies) - .catch(reportFailure) - .finally(stopProgress); - }, - - showEmberInitPrompt: function() { - this.ui.writeLine('To complete the update, you need to run ' + - chalk.green('ember init') + ' in your project directory.'); - - var initPrompt = { - type: 'confirm', - name: 'answer', - message: 'Would you like to run ' + chalk.green('ember init') + ' now?', - choices: [ - { key: 'y', name: 'Yes', value: 'yes' }, - { key: 'n', name: 'No', value: 'no' } - ] - }; - - return this.ui.prompt(initPrompt).then(function(response) { - if (response.answer === true) { - var InitCommand = this.commands.Init; - - var initCommand = new InitCommand({ - ui: this.ui, - analytics: this.analytics, - tasks: this.tasks, - project: this.project - }); - - return initCommand.run({}, []); - } - }.bind(this)); - } -}); diff --git a/lib/ui/index.js b/lib/ui/index.js deleted file mode 100644 index 1ac64498d0..0000000000 --- a/lib/ui/index.js +++ /dev/null @@ -1,179 +0,0 @@ -'use strict'; - -var PleasantProgress = require('pleasant-progress'); -var Promise = require('../ext/promise'); -var EOL = require('os').EOL; -var writeError = require('./write-error'); - -// Note: You should use `ui.outputStream`, `ui.inputStream` and `ui.write()` -// instead of `process.stdout` and `console.log`. -// Thus the pleasant progress indicator automatically gets -// interrupted and doesn't mess up the output! -> Convenience :P - -module.exports = UI; - -/* - @constructor - - The UI provides the CLI with a unified mechanism for providing output and - requesting input from the user. This becomes useful when wanting to adjust - logLevels, or mock input/output for tests. - - new UI{ - inputStream: process.stdin, - outputStream: process.stdout, - writeLevel: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' - ci: true | false - }); - -**/ - -function UI(options) { - var pleasantProgress = this.pleasantProgress = new PleasantProgress(); - - this.through = require('through'); - this.readline = require('readline2'); - - // Output stream - this.actualOuputStream = options.outputStream; - this.outputStream = this.through(function(data) { - pleasantProgress.stop(true); - this.emit('data', data); - }); - - this.outputStream.setMaxListeners(0); - this.outputStream.pipe(this.actualOuputStream); - - // Input stream - this.inputStream = options.inputStream; - - this.writeLevel = options.writeLevel || 'INFO'; - this.ci = !!options.ci; -} - -/** - Unified mechanism to write a string to the console. - Optionally include a writeLevel, this is used to decide if the specific - logging mechanism should or should not be printed. - - @method write - @param {String} data - @param {Number} writeLevel -*/ -UI.prototype.write = function(data, writeLevel) { - if (this.writeLevelVisible(writeLevel)) { - this.outputStream.write(data); - } -}; - -/** - Unified mechanism to write a string and new line to the console. - Optionally include a writeLevel, this is used to decide if the specific - - logging mechanism should or should not be printed. - @method writeLine - @param {String} data - @param {Number} writeLevel -*/ -UI.prototype.writeLine = function(data, writeLevel) { - if (this.writeLevelVisible(writeLevel)) { - this.write(data + EOL); - } -}; - -/** - Unified mechanism to an Error to the console. - This will occure at a writeLevel of ERROR - - @method writeError - @param {Error} error -*/ -UI.prototype.writeError = function(error) { - writeError(this, error); -}; - -/** - Sets the write level for the UI. Valid write levels are 'DEBUG', 'INFO', - 'WARNING', and 'ERROR'. - - @method setWriteLevel - @param {String} level -*/ -UI.prototype.setWriteLevel = function(level) { - if (Object.keys(this.WRITE_LEVELS).indexOf(level) === -1) { - throw new Error('Unknown write level. Valid values are \'DEBUG\', \'INFO\', \'WARNING\', and \'ERROR\'.'); - } - - this.writeLevel = level; -}; - -UI.prototype.startProgress = function(message, stepString) { - if (this.writeLevelVisible('INFO')) { - if (this.ci) { - this.writeLine(message); - } else { - this.pleasantProgress.start(message, stepString); - } - } -}; - -UI.prototype.stopProgress = function(printWithFullStepString) { - if (this.writeLevelVisible('INFO') && !this.ci) { - this.pleasantProgress.stop(printWithFullStepString); - } -}; - -UI.prototype.prompt = function(questions, callback) { - // Pipe it to the output stream but don't forward end event - var promtOutputStream = this.through(null, function() {}); - promtOutputStream.pipe(this.outputStream); - - var Prompt = require('inquirer').ui.Prompt; - // Note: Cannot move this outside - // Need a new readline interface each time, 'cause it gets torn down - function PromptExt() { - Prompt.apply(this, arguments); - } - PromptExt.prototype = Object.create(Prompt.prototype); - PromptExt.prototype.constructor = PromptExt; - PromptExt.prototype.rl = this.readline.createInterface({ - input: this.inputStream, - output: promtOutputStream - }); - - // If no callback was provided, automatically return a promise - if (callback) { - return new PromptExt(questions, callback); - } else { - return new Promise(function(resolve) { - new PromptExt(questions, resolve); - }); - } -}; - -/** - @property WRITE_LEVELS - @private - @type Object -*/ -UI.prototype.WRITE_LEVELS = { - 'DEBUG': 1, - 'INFO': 2, - 'WARNING': 3, - 'ERROR': 4 -}; - -/** - Whether or not the specified write level should be printed by this UI. - - @method writeLevelVisible - @private - @param {String} writeLevel - @return {Boolean} -*/ -UI.prototype.writeLevelVisible = function(writeLevel) { - var levels = this.WRITE_LEVELS; - writeLevel = writeLevel || 'INFO'; - - return levels[writeLevel] >= levels[this.writeLevel]; -}; diff --git a/lib/ui/write-error.js b/lib/ui/write-error.js deleted file mode 100644 index 89d73e32f8..0000000000 --- a/lib/ui/write-error.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; -var chalk = require('chalk'); - -module.exports = function writeError(ui, error){ - if (!error) { return; } - - // Uglify errors have a filename instead - var fileName = error.file || error.filename; - if (fileName) { - if (error.line) { - fileName += error.col ? ' (' + error.line + ':' + error.col + ')' : ' (' + error.line + ')'; - } - ui.writeLine(chalk.red('File: ' + fileName), 'ERROR'); - } - - if (error.message) { - ui.writeLine(chalk.red(error.message), 'ERROR'); - } else { - ui.writeLine(chalk.red(error), 'ERROR'); - } - - if (error.stack) { - ui.writeLine(error.stack, 'ERROR'); - } -}; diff --git a/lib/utilities/DAG.js b/lib/utilities/DAG.js deleted file mode 100644 index ef0a0d9409..0000000000 --- a/lib/utilities/DAG.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -function visit(vertex, fn, visited, path) { - var name = vertex.name, - vertices = vertex.incoming, - names = vertex.incomingNames, - len = names.length, - i; - if (!visited) { - visited = {}; - } - if (!path) { - path = []; - } - if (visited.hasOwnProperty(name)) { - return; - } - path.push(name); - visited[name] = true; - for (i = 0; i < len; i++) { - visit(vertices[names[i]], fn, visited, path); - } - fn(vertex, path); - path.pop(); -} - -function DAG() { - this.names = []; - this.vertices = {}; -} - -DAG.prototype.add = function(name) { - if (!name) { return; } - if (this.vertices.hasOwnProperty(name)) { - return this.vertices[name]; - } - var vertex = { - name: name, - incoming: {}, - incomingNames: [], - hasOutgoing: false, - value: null - }; - - this.vertices[name] = vertex; - this.names.push(name); - return vertex; -}; - -DAG.prototype.map = function(name, value) { - this.add(name).value = value; -}; - -DAG.prototype.addEdge = function(fromName, toName) { - if (!fromName || !toName || fromName === toName) { - return; - } - var from = this.add(fromName), to = this.add(toName); - if (to.incoming.hasOwnProperty(fromName)) { - return; - } - function checkCycle(vertex, path) { - if (vertex.name === toName) { - throw new Error('cycle detected: ' + toName + ' <- ' + path.join(' <- ')); - } - } - visit(from, checkCycle); - from.hasOutgoing = true; - to.incoming[fromName] = from; - to.incomingNames.push(fromName); -}; - -DAG.prototype.topsort = function(fn) { - var visited = {}, - vertices = this.vertices, - names = this.names, - len = names.length, - i, vertex; - for (i = 0; i < len; i++) { - vertex = vertices[names[i]]; - if (!vertex.hasOutgoing) { - visit(vertex, fn, visited); - } - } -}; - -DAG.prototype.addEdges = function(name, value, before, after) { - var i; - this.map(name, value); - if (before) { - if (typeof before === 'string') { - this.addEdge(name, before); - } else { - for (i = 0; i < before.length; i++) { - this.addEdge(name, before[i]); - } - } - } - if (after) { - if (typeof after === 'string') { - this.addEdge(after, name); - } else { - for (i = 0; i < after.length; i++) { - this.addEdge(after[i], name); - } - } - } -}; - -module.exports = DAG; diff --git a/lib/utilities/addon-process-tree.js b/lib/utilities/addon-process-tree.js new file mode 100644 index 0000000000..73b13e3ebe --- /dev/null +++ b/lib/utilities/addon-process-tree.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function addonProcessTree(projectOrAddon, hook, processType, tree) { + return projectOrAddon.addons.reduce((workingTree, addon) => { + if (addon[hook]) { + return addon[hook](processType, workingTree); + } + + return workingTree; + }, tree); +}; diff --git a/lib/utilities/attempt-never-index.js b/lib/utilities/attempt-never-index.js deleted file mode 100644 index d2ec532549..0000000000 --- a/lib/utilities/attempt-never-index.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -var isDarwin = /darwin/i.test(require('os').type()); -var debug = require('debug')('ember-cli:utilities/attempt-metadata-index-file'); - -module.exports = function(dir) { - var path = dir + '/.metadata_never_index'; - - if (!isDarwin) { - debug('not darwin, skipping %s (which hints to spotlight to prevent indexing)', path); - return; - } - - debug('creating: %s (to prevent spotlight indexing)', path); - - var fs = require('fs-extra'); - - fs.mkdirsSync(dir); - fs.writeFileSync(path); -}; diff --git a/lib/utilities/deprecate.js b/lib/utilities/deprecate.js deleted file mode 100644 index b6afa7ae62..0000000000 --- a/lib/utilities/deprecate.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -var chalk = require('chalk'); - -module.exports = function(message, test) { - if(!test) { return; } - - console.log(chalk.yellow('DEPRECATION: ' + message)); -}; - -module.exports.deprecateUI = function(ui){ - return function(message, test) { - if(!test) { return; } - - ui.writeLine(chalk.yellow('DEPRECATION: ' + message)); - }; -}; diff --git a/lib/utilities/doc-generator.js b/lib/utilities/doc-generator.js deleted file mode 100644 index 52bf099355..0000000000 --- a/lib/utilities/doc-generator.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -var versionUtils = require('./version-utils'); -var calculateVersion = versionUtils.emberCLIVersion; -var fs = require('fs'); - -function DocGenerator(options) { - options = options || {}; - this.exec = options.exec || require('child_process').exec; -} - -DocGenerator.prototype.generate = function() { - var command = 'cd docs && ' + fs.realpathSync('./node_modules/.bin/yuidoc') + - ' -q --project-version ' + calculateVersion(); // add '-p' flag to produce only JSON and not HTML - - console.log('Executing command: ' + command); - this.exec(command, function(error){ // stdout, stderr - if (error !== null) { - console.log('Error: ' + error); - } - }); -}; - -module.exports = DocGenerator; diff --git a/lib/utilities/ember-app-utils.js b/lib/utilities/ember-app-utils.js new file mode 100644 index 0000000000..3013a6c94f --- /dev/null +++ b/lib/utilities/ember-app-utils.js @@ -0,0 +1,187 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const cleanBaseURL = require('clean-base-url'); +const { deprecate, DEPRECATIONS } = require('../debug'); + +/** + * Returns a normalized url given a string. + * Returns an empty string if `null`, `undefined` or an empty string are passed + * in. + * + * @method normalizeUrl + * @param {String} Raw url. + * @return {String} Normalized url. + */ +function normalizeUrl(rootURL) { + if (rootURL === undefined || rootURL === null || rootURL === '') { + return ''; + } + + return cleanBaseURL(rootURL); +} + +/** + * Converts Javascript Object to a string. + * Returns an empty object string representation if a "falsy" value is passed + * in. + * + * @method convertObjectToString + * @param {Object} Any Javascript Object. + * @return {String} A string representation of a Javascript Object. + */ +function convertObjectToString(env) { + return JSON.stringify(env || {}); +} + +/** + * Returns the content for a specific type (section) for index.html. + * + * ``` + * {{content-for "[type]"}} + * ``` + * + * Supported types: + * + * - 'head' + * - 'config-module' + * - 'head-footer' + * - 'test-header-footer' + * - 'body-footer' + * - 'test-body-footer' + * + * @method contentFor + * @param {Object} config Ember.js application configuration + * @param {RegExp} match Regular expression to match against + * @param {String} type Type of content + * @param {Object} options Settings that control the default content + * @param {Boolean} options.autoRun Controls whether to bootstrap the + application or not + * @param {Boolean} options.storeConfigInMeta Controls whether to include the + contents of config + * @return {String} The content. +*/ +function contentFor(config, match, type, options) { + let content = []; + + // This normalizes `rootURL` to the value which we use everywhere inside of Ember CLI. + // This makes sure that the user doesn't have to account for it in application code. + if ('rootURL' in config) { + config.rootURL = normalizeUrl(config.rootURL); + } + + switch (type) { + case 'head': + if (options.storeConfigInMeta) { + content.push( + `` + ); + } + + break; + case 'config-module': + if (options.storeConfigInMeta) { + content.push(`var prefix = '${config.modulePrefix}';`); + content.push(fs.readFileSync(path.join(__dirname, '../broccoli/app-config-from-meta.js'))); + } else { + content.push(` + var exports = { + 'default': ${JSON.stringify(config)} + }; + Object.defineProperty(exports, '__esModule', {value: true}); + return exports; + `); + } + + break; + case 'app-boot': + if (options.autoRun) { + let moduleToRequire = `${config.modulePrefix}/app`; + content.push(` + if (!runningTests) { + require("${moduleToRequire}")["default"].create(${convertObjectToString(config.APP)}); + } + `); + } + + break; + case 'test-body-footer': + content.push( + `` + ); + + break; + } + + content = options.addons.reduce((content, addon) => { + let addonContent = addon.contentFor ? addon.contentFor(type, config, content) : null; + if (addonContent) { + for (const deprecatedType of DEPRECATIONS.V1_ADDON_CONTENT_FOR_TYPES.options.meta.types) { + deprecate( + `Addon \`${addon.name}\` is using the deprecated \`${deprecatedType}\` type in its \`contentFor\` method. Please see the deprecation guide for the correct path forward.`, + type !== deprecatedType, + DEPRECATIONS.V1_ADDON_CONTENT_FOR_TYPES.options + ); + } + + return content.concat(addonContent); + } + + return content; + }, content); + + return content.join('\n'); +} + +/* + * Return a list of pairs: a pattern to match to a replacement function. + * + * Used to replace various tags in `index.html` and `tests/index.html`. + * + * @param {Object} options + * @param {Array} options.addons A list of project's add-ons + * @param {Boolean} options.autoRun Controls whether to bootstrap the + application or not + * @param {Boolean} options.storeConfigInMeta Controls whether to include the + contents of config + @return {Array} An array of patterns to match against and replace +*/ +function configReplacePatterns(options) { + return [ + { + match: /{{\s?rootURL\s?}}/g, + replacement(config) { + return normalizeUrl(config.rootURL); + }, + }, + { + match: /{{\s?EMBER_ENV\s?}}/g, + replacement(config) { + return convertObjectToString(config.EmberENV); + }, + }, + { + match: /{{content-for ['"](.+?)["']}}/g, + replacement(config, match, type) { + return contentFor(config, match, type, options); + }, + }, + { + match: /{{\s?MODULE_PREFIX\s?}}/g, + replacement(config) { + return config.modulePrefix; + }, + }, + ]; +} + +module.exports = { normalizeUrl, convertObjectToString, contentFor, configReplacePatterns }; diff --git a/lib/utilities/find-build-file.js b/lib/utilities/find-build-file.js index 8c359ad6d4..d1307a4ead 100644 --- a/lib/utilities/find-build-file.js +++ b/lib/utilities/find-build-file.js @@ -1,22 +1,31 @@ 'use strict'; +const { findUpSync } = require('find-up'); +const path = require('path'); +const url = require('url'); -var findUp = require('findup-sync'); -var path = require('path'); +module.exports = async function (dir) { + let buildFilePath = null; -module.exports = function(file) { - var buildFilePath = findUp(file); + for (let ext of ['js', 'mjs', 'cjs']) { + let candidate = findUpSync(`ember-cli-build.${ext}`, { cwd: dir }); + if (candidate) { + buildFilePath = candidate; + break; + } + } - // Note - // In the future this should throw if (buildFilePath === null) { return null; } - var baseDir = path.dirname(buildFilePath); - - process.chdir(baseDir); + process.chdir(path.dirname(buildFilePath)); - var buildFile = require(buildFilePath); - - return buildFile; -}; \ No newline at end of file + let buildFileUrl = url.pathToFileURL(buildFilePath); + try { + let fn = (await import(buildFileUrl)).default; + return fn; + } catch (err) { + err.message = `Could not \`import('${buildFileUrl}')\`: ${err.message}`; + throw err; + } +}; diff --git a/lib/utilities/format-package-list.js b/lib/utilities/format-package-list.js new file mode 100644 index 0000000000..f7ed123ea0 --- /dev/null +++ b/lib/utilities/format-package-list.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = function formatPackageList(packages) { + if (Array.isArray(packages)) { + if (packages.length === 1) { + return packages[0]; + } else if (packages.length === 2) { + return `${packages[0]} and ${packages[1]}`; + } else if (packages.length === 3) { + return `${packages[0]}, ${packages[1]} and ${packages[2]}`; + } else if (packages.length > 3) { + return `${packages[0]}, ${packages[1]} and ${packages.length - 2} other packages`; + } + } + + return 'dependencies'; +}; diff --git a/lib/utilities/get-caller-file.js b/lib/utilities/get-caller-file.js deleted file mode 100644 index aa384a63d4..0000000000 --- a/lib/utilities/get-caller-file.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -// Call this function in a another function to find out the file from -// which that function was called from. (Inspects the v8 stack trace) -// -// Inspired by http://stackoverflow.com/questions/13227489 - -module.exports = getCallerFile; -function getCallerFile() { - var oldPrepareStackTrace = Error.prepareStackTrace; - Error.prepareStackTrace = function(err, stack) { return stack; }; - var stack = new Error().stack; - Error.prepareStackTrace = oldPrepareStackTrace; - - // stack[0] holds this file - // stack[1] holds where this function was called - // stack[2] holds the file we're interested in - return stack[2] ? stack[2].getFileName() : undefined; -} diff --git a/lib/utilities/get-component-path-option.js b/lib/utilities/get-component-path-option.js deleted file mode 100644 index 4a2c171a0c..0000000000 --- a/lib/utilities/get-component-path-option.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports = function getPathOption(options) { - var outputPath = 'components'; - if (options.path) { - outputPath = options.path; - } else { - if (options.path === '') { - outputPath = ''; - } - } - return outputPath; -}; \ No newline at end of file diff --git a/lib/utilities/get-config.js b/lib/utilities/get-config.js new file mode 100644 index 0000000000..c1e134b18b --- /dev/null +++ b/lib/utilities/get-config.js @@ -0,0 +1,18 @@ +'use strict'; + +let Yam = require('yam'); +let Project = require('../models/project'); + +function generateConfig() { + return new Yam('ember-cli', { + primary: Project.getProjectRoot(), + }); +} + +module.exports = function getConfig(override) { + if (override) { + return override; + } + + return generateConfig(); +}; diff --git a/lib/utilities/get-dependency-depth.js b/lib/utilities/get-dependency-depth.js deleted file mode 100644 index da98ca186e..0000000000 --- a/lib/utilities/get-dependency-depth.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function getDependencyDepth(options) { - var name = options.entity.name; - var nameParts = name.split('/'); - var depth = '../..'; - - return nameParts.reduce(function(prev) { - return prev + '/..'; - }, depth); -}; diff --git a/lib/utilities/get-lang-arg.js b/lib/utilities/get-lang-arg.js new file mode 100644 index 0000000000..0828117130 --- /dev/null +++ b/lib/utilities/get-lang-arg.js @@ -0,0 +1,268 @@ +/* + +This utility processes the argument passed with the `lang` option +in ember-cli, i.e. `ember (new||init||addon) app-name --lang=langArg` + +Execution Context (usage, input, output, error handling, etc.): + - called directly by `init` IFF `--lang` flag is used in (new||init||addon) + - receives single input: the argument passed with `lang` (herein `langArg`) + - processes `langArg`: lang code validation + error detection / handling + - DOES emit Warning messages if necessary + - DOES NOT halt execution process / throw errors / disrupt the build + - returns single result as output (to `init`): + - `langArg` (if it is a valid language code) + - `undefined` (otherwise) + - `init` assigns the value of `commandOptions.lang` to the returned result + - downstream, the `lang` attribute is assigned via inline template control: + - file: `blueprints/app/files/app/index.html` + - logic: ` lang="<%= lang %>"<% } %>> + +Internal Mechanics -- the utility processes `langArg` to determine: + - the value to return to `init` (i.e. validated lang code or undefined) + - a descriptive category for the usage: `correct`, `incorrect`, `edge`, etc. + - what message text (if any: category-dependent) to emit before return + +Warning Messages (if necessary): + - An internal instance of `console-ui` is used to emit messages + - IFF there is a message, it will be emitted before returning the result + - Components of all emitted messages -- [Name] (writeLevel): 'example': + - [`HEAD`] (WARNING): 'A warning was generated while processing `--lang`:' + - [`BODY`] (WARNING): 'Invalid language code, `en-UK`' + - [`STATUS`] (WARNING): '`lang` will NOT be set to `en-UK` in `index.html`' + - [`HELP`] (INFO): 'Correct usage of `--lang`: ... ' + +*/ + +'use strict'; + +const { isLangCode } = require('is-language-code'); + +// Primary language code validation function (boolean) +function isValidLangCode(langArg) { + return isLangCode(langArg).res; +} + +// Generates the result to pass back to `init` +function getResult(langArg) { + return isValidLangCode(langArg) ? langArg : undefined; +} + +/* +Misuse case: attempt to set application programming language via `lang` +AND +Edge case: valid language code AND a common programming language abbreviation +------------------------------------------------------------------------------- +It is possible that a user might mis-interpret the type of `language` that is +specified by the `--lang` flag. One notable potential `misuse case` is one in +which the user thinks `--lang` specifies the application's programming +language. For example, the user might call `ember new my-app --lang=typescript` +expecting to achieve an effect similar to the one provided by the +`ember-cli-typescript` addon. + +This misuse case is handled by checking the input `langArg` against an Array +containing notable programming language-related values: language names +(e.g. `JavaScript`), abbreviations (e.g. `js`), file extensions (e.g. `.js`), +or versions (e.g. `ES6`), etc. Specifically, if `langArg` is found within this +reference list, a WARNING message that describes correct `--lang` usage will +be emitted. The `lang` attribute will not be assigned in `index.html`, and the +user will be notified with a corresponding STATUS message. + +There are several edge cases (marked) where `langArg` is both a commonly-used +abbreviation for a programming language AND a valid language code. The behavior +for these cases is to assume the user has used `--lang` correctly and set the +`lang` attribute to the valid code in `index.html`. To cover for potential +misuage, several helpful messages will also be emitted: +- `ts` is a valid language code AND a common programming language abbreviation +- the `lang` attribute will be set to `ts` in the application +- if this is not correct, it can be changed in `app/index.html` directly +- (general `help` information about correct `--lang` usage) +*/ +const PROG_LANGS = [ + 'javascript', + '.js', + 'js', + 'emcascript2015', + 'emcascript6', + 'es6', + 'emcascript2016', + 'emcascript7', + 'es7', + 'emcascript2017', + 'emcascript8', + 'es8', + 'emcascript2018', + 'emcascript9', + 'es9', + 'emcascript2019', + 'emcascript10', + 'es10', + 'typescript', + '.ts', + 'node.js', + 'node', + 'handlebars', + '.hbs', + 'hbs', + 'glimmer', + 'glimmer.js', + 'glimmer-vm', + 'markdown', + 'markup', + 'html5', + 'html4', + '.md', + '.html', + '.htm', + '.xhtml', + '.xml', + '.xht', + 'md', + 'html', + 'htm', + 'xhtml', + '.sass', + '.scss', + '.css', + 'sass', + 'scss', + // Edge Cases + 'ts', // Tsonga + 'TS', // Tsonga (case insensitivity check) + 'xml', // Malaysian Sign Language + 'xht', // Hattic + 'css', // Costanoan +]; + +function isProgLang(langArg) { + return langArg && PROG_LANGS.includes(langArg.toLowerCase().trim()); +} + +function isValidCodeAndProg(langArg) { + return isValidLangCode(langArg) && isProgLang(langArg); +} + +/* +Misuse case: `--lang` called without `langArg` (NB: parser bug workaround) +------------------------------------------------------------------------------- +This is a workaround for handling an existing bug in the ember-cli parser +where the `--lang` option is specified in the command without a corresponding +value for `langArg`. + +As examples, the parser behavior would likely affect the following usages: + 1. `ember new app-name --lang --skip-npm + 2. `ember new app-name --lang` + +In this context, the unintended parser behavior is that `langArg` will be +assigned to the String that immediately follows `--lang` in the command. If +`--lang` is the last explicitly defined part of the command (as in the second +example above), the first of any any `hidden` options pushed onto the command +after the initial parse (e.g. `--no-watcher`) will be +used when assigning `langArg`. + +In the above examples, `langArg` would likely be assigned as follows: + 1. `ember new app-name --lang --skip-npm => `langArg='--skip-npm'` + +The workaround implemented herein is to check whether or not the value of +`langArg` starts with a hyphen. The rationale for this approach is based on +the following underlying assumptions: + - ALL CLI options start with (at least one) hyphen + - NO valid language codes start with a hyphen + +If the leading hyphen is detected, the current behavior is to assume `--lang` +was declared without a corresponding specification. A WARNING message that +describes correct `--lang` usage will be emitted. The `lang` attribute will not +be assigned in `index.html`, and the user will be notified with a corresponding +STATUS message. Execution will not be halted. + +Other complications related to this parser behavior are considered out-of-scope +and not handled here. In the first example above, this workaround would ensure +that `lang` is not assigned to `--skip-npm`, but it would not guarantee that +`--skip-npm` is correctly processed as a command option. That is, `npm` may or +may not get skipped during execution. +*/ + +function startsWithHyphen(langArg) { + return langArg && langArg[0] === '-'; +} + +// MESSAGE GENERATION: +// 1. `HEAD` Message: template for all `--lang`-related warnings emitted +const MSG_HEAD = `An issue with the \`--lang\` flag returned the following message:`; + +// 2. `BODY` Messages: category-dependent context information + +// Message context from language code validation (valid: null, invalid: reason) +function getLangCodeMsg(langArg) { + return isLangCode(langArg).message; +} + +// Edge case: valid language code AND a common programming language abbreviation +function getValidAndProgMsg(langArg) { + return `The \`--lang\` flag has been used with argument \`${langArg}\`, + which is BOTH a valid language code AND an abbreviation for a programming language. + ${getProgLangMsg(langArg)}`; +} + +// Misuse case: attempt to set application programming language via `lang` +function getProgLangMsg(langArg) { + return `Trying to set the app programming language to \`${langArg}\`? + This is not the intended usage of the \`--lang\` flag.`; +} + +// Misuse case: `--lang` called without `langArg` (NB: parser bug workaround) +function getCliMsg() { + return `Detected a \`--lang\` specification starting with command flag \`-\`. + This issue is likely caused by using the \`--lang\` flag without a specification.`; +} + +// 3. `STATUS` message: report if `lang` will be set in `index.html` +function getStatusMsg(langArg, willSet) { + return `The human language of the application will ${willSet ? `be set to ${langArg}` : `NOT be set`} in + the \`\` element's \`lang\` attribute in \`index.html\`.`; +} + +// 4. `HELP` message: template for all `--lang`-related warnings emitted +const MSG_HELP = `If this was not your intention, you may edit the \`\` element's + \`lang\` attribute in \`index.html\` manually after the process is complete. +Information about using the \`--lang\` flag: + The \`--lang\` flag sets the base human language of an app or test app: + - \`app/index.html\` (app) + - \`tests/dummy/app/index.html\` (addon test app) + If used, the lang option must specfify a valid language code. + For default behavior, remove the flag. + See \`ember help\` for more information.`; + +function getBodyMsg(langArg) { + return isValidCodeAndProg(langArg) + ? getValidAndProgMsg(langArg) + : isProgLang(langArg) + ? getProgLangMsg(langArg) + : startsWithHyphen(langArg) + ? getCliMsg(langArg) + : getLangCodeMsg(langArg); +} + +function getFullMsg(langArg) { + return { + head: MSG_HEAD, + body: getBodyMsg(langArg), + status: getStatusMsg(langArg, isValidCodeAndProg(langArg)), + help: MSG_HELP, + }; +} + +function writeFullMsg(fullMsg, ui) { + ui.setWriteLevel('WARNING'); + ui.writeWarnLine(`${fullMsg.head}\n ${fullMsg.body}\``); + ui.writeWarnLine(fullMsg.status); + ui.setWriteLevel('INFO'); + ui.writeInfoLine(fullMsg.help); +} + +module.exports = function getLangArg(langArg, ui) { + let fullMsg = getFullMsg(langArg); + if (fullMsg.body) { + writeFullMsg(fullMsg, ui); + } + return getResult(langArg); +}; diff --git a/lib/utilities/get-option-args.js b/lib/utilities/get-option-args.js index 3ce3bd7838..34198cecef 100644 --- a/lib/utilities/get-option-args.js +++ b/lib/utilities/get-option-args.js @@ -1,13 +1,19 @@ 'use strict'; -module.exports = function(option, commandArgs) { - var results = [], value, i; - var optionIndex = commandArgs.indexOf(option); - if (optionIndex === -1) { return results; } +module.exports = function (option, commandArgs) { + let results = [], + value, + i; + let optionIndex = commandArgs.indexOf(option); + if (optionIndex === -1) { + return results; + } for (i = optionIndex + 1; i < commandArgs.length; i++) { value = commandArgs[i]; - if (/^\-+/.test(value)) { break; } + if (/^-+/.test(value)) { + break; + } results.push(value); } diff --git a/lib/utilities/get-package-base-name.js b/lib/utilities/get-package-base-name.js index 9690ce43bb..3ff984a76a 100644 --- a/lib/utilities/get-package-base-name.js +++ b/lib/utilities/get-package-base-name.js @@ -1,12 +1,11 @@ 'use strict'; -module.exports = function (name) { - var packageParts; +const npa = require('npm-package-arg'); +module.exports = function (name) { if (!name) { return null; } - packageParts = name.split('/'); - return packageParts[(packageParts.length - 1)]; + return npa(name).name; }; diff --git a/lib/utilities/get-printable-properties.js b/lib/utilities/get-printable-properties.js deleted file mode 100644 index 2660e53f3f..0000000000 --- a/lib/utilities/get-printable-properties.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -module.exports = function() { - return [ - 'name', - 'description', - 'aliases', - 'works', - 'availableOptions', - 'anonymousOptions' - ]; -}; diff --git a/lib/utilities/get-serve-url.js b/lib/utilities/get-serve-url.js new file mode 100644 index 0000000000..cd37aee609 --- /dev/null +++ b/lib/utilities/get-serve-url.js @@ -0,0 +1,10 @@ +'use strict'; + +const cleanBaseURL = require('clean-base-url'); + +module.exports = function (options, project) { + let config = project.config(options.environment); + let rootURL = config.rootURL === '' ? '/' : cleanBaseURL(config.rootURL || '/'); + + return `http${options.ssl ? 's' : ''}://${options.host || 'localhost'}:${options.port}${rootURL}`; +}; diff --git a/lib/utilities/heimdall-progress.js b/lib/utilities/heimdall-progress.js new file mode 100644 index 0000000000..931a21b07c --- /dev/null +++ b/lib/utilities/heimdall-progress.js @@ -0,0 +1,22 @@ +'use strict'; + +const { default: chalk } = require('chalk'); + +module.exports = function progress(heimdalljs = require('heimdalljs')) { + let current = heimdalljs.current; + const stack = [current.id.name]; + + while ((current = current.parent)) { + // eslint-disable-line + stack.push(current.id.name); + } + + return stack + .filter((name) => name !== 'heimdall') + .reverse() + .join(' > '); +}; + +module.exports.format = function (text) { + return chalk.green('building... ') + (text ? `[${text}]` : ''); +}; diff --git a/lib/utilities/instrumentation.js b/lib/utilities/instrumentation.js new file mode 100644 index 0000000000..4ef68ce65b --- /dev/null +++ b/lib/utilities/instrumentation.js @@ -0,0 +1,21 @@ +'use strict'; + +const getConfig = require('./get-config'); + +function vizEnabled() { + return process.env.BROCCOLI_VIZ === '1'; +} + +function isInstrumentationConfigEnabled(configOverride) { + let config = getConfig(configOverride); + return !!config.get('enableInstrumentation'); +} + +function instrumentationEnabled(config) { + return vizEnabled() || process.env.EMBER_CLI_INSTRUMENTATION === '1' || isInstrumentationConfigEnabled(config); +} + +module.exports = { + vizEnabled, + instrumentationEnabled, +}; diff --git a/lib/utilities/is-engine.js b/lib/utilities/is-engine.js new file mode 100644 index 0000000000..352f7ee5e1 --- /dev/null +++ b/lib/utilities/is-engine.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function isEngine(keywords) { + return !!(Array.isArray(keywords) && keywords.indexOf('ember-engine') >= 0); +}; diff --git a/lib/utilities/is-lazy-engine.js b/lib/utilities/is-lazy-engine.js new file mode 100644 index 0000000000..156861502f --- /dev/null +++ b/lib/utilities/is-lazy-engine.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Indicate if a given object is a constructor function or class or an instance of an Addon. + * + * @private + * @method + * @param {Object} addonCtorOrInstance the constructor function/class or an instance of an Addon. + * @return {Boolean} True if the addonCtorOrInstance is a lazy engine, False otherwise. + */ +module.exports = function isLazyEngine(addonCtorOrInstance) { + if (!addonCtorOrInstance) { + return false; + } + + if (addonCtorOrInstance.lazyLoading) { + return addonCtorOrInstance.lazyLoading.enabled === true; + } else if (addonCtorOrInstance.options) { + return !!(addonCtorOrInstance.options.lazyLoading && addonCtorOrInstance.options.lazyLoading.enabled === true); + } else if (addonCtorOrInstance.prototype) { + if (addonCtorOrInstance.prototype.lazyLoading) { + return addonCtorOrInstance.prototype.lazyLoading.enabled === true; + } else if (addonCtorOrInstance.prototype.options) { + return !!( + addonCtorOrInstance.prototype.options.lazyLoading && + addonCtorOrInstance.prototype.options.lazyLoading.enabled === true + ); + } + } + + return false; +}; diff --git a/lib/utilities/is-live-reload-request.js b/lib/utilities/is-live-reload-request.js new file mode 100644 index 0000000000..b8906d348e --- /dev/null +++ b/lib/utilities/is-live-reload-request.js @@ -0,0 +1,7 @@ +'use strict'; + +const cleanBaseUrl = require('clean-base-url'); + +module.exports = function isLiveReloadRequest(url, liveReloadPrefix) { + return url === `${cleanBaseUrl(liveReloadPrefix)}livereload`; +}; diff --git a/lib/utilities/json-generator.js b/lib/utilities/json-generator.js new file mode 100644 index 0000000000..6501685398 --- /dev/null +++ b/lib/utilities/json-generator.js @@ -0,0 +1,72 @@ +'use strict'; + +const lookupCommand = require('../cli/lookup-command'); +const stringUtils = require('ember-cli-string-utils'); +const RootCommand = require('./root-command'); +const versionUtils = require('./version-utils'); +let emberCLIVersion = versionUtils.emberCLIVersion; + +class JsonGenerator { + constructor(options) { + options = options || {}; + + this.ui = options.ui; + this.project = options.project; + this.commands = options.commands; + this.tasks = options.tasks; + } + + generate(commandOptions) { + let rootCommand = new RootCommand({ + ui: this.ui, + project: this.project, + commands: this.commands, + tasks: this.tasks, + }); + + let json = rootCommand.getJson(commandOptions); + json.version = emberCLIVersion(); + json.commands = []; + json.addons = []; + + Object.keys(this.commands).forEach(function (commandName) { + this._addCommandHelpToJson(commandName, commandOptions, json); + }, this); + + if (this.project.eachAddonCommand) { + this.project.eachAddonCommand((addonName, commands) => { + this.commands = commands; + + let addonJson = { name: addonName }; + addonJson.commands = []; + json.addons.push(addonJson); + + Object.keys(this.commands).forEach(function (commandName) { + this._addCommandHelpToJson(commandName, commandOptions, addonJson); + }, this); + }); + } + + return json; + } + + _addCommandHelpToJson(commandName, options, json) { + let command = this._lookupCommand(commandName); + if (!command.skipHelp && !command.unknown) { + json.commands.push(command.getJson(options)); + } + } + + _lookupCommand(commandName) { + let Command = this.commands[stringUtils.classify(commandName)] || lookupCommand(this.commands, commandName); + + return new Command({ + ui: this.ui, + project: this.project, + commands: this.commands, + tasks: this.tasks, + }); + } +} + +module.exports = JsonGenerator; diff --git a/lib/utilities/lint-addons-by-type.js b/lib/utilities/lint-addons-by-type.js new file mode 100644 index 0000000000..9a6ea814e9 --- /dev/null +++ b/lib/utilities/lint-addons-by-type.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = function lintAddonsByType(addons, type, tree) { + return addons.reduce((sum, addon) => { + if (addon.lintTree) { + let val = addon.lintTree(type, tree); + if (val) { + sum.push(val); + } + } + return sum; + }, []); +}; diff --git a/lib/utilities/lint-fix.js b/lib/utilities/lint-fix.js new file mode 100644 index 0000000000..30de288077 --- /dev/null +++ b/lib/utilities/lint-fix.js @@ -0,0 +1,35 @@ +'use strict'; + +const { execa } = require('execa'); +const { isPnpmProject, isYarnProject } = require('../utilities/package-managers'); +const prependEmoji = require('@ember-tooling/blueprint-model/utilities/prepend-emoji'); + +async function run(project) { + let lintFixScriptName = 'lint:fix'; + let hasLintFixScript = Boolean(project.pkg.scripts[lintFixScriptName]); + + if (!hasLintFixScript) { + project.ui.writeWarnLine( + `Lint fix skipped: "${lintFixScriptName}" script not found in "package.json". New files might not pass linting.` + ); + + return; + } + + project.ui.writeLine(''); + project.ui.writeLine(prependEmoji('✨', `Running "${lintFixScriptName}" script...`)); + + let cwd = process.cwd(); + + if (await isPnpmProject(project.root)) { + await execa('pnpm', [lintFixScriptName], { cwd }); + } else if (isYarnProject(project.root)) { + await execa('yarn', [lintFixScriptName], { cwd }); + } else { + await execa('npm', ['run', lintFixScriptName], { cwd }); + } +} + +module.exports = { + run, +}; diff --git a/lib/utilities/markdown-color.js b/lib/utilities/markdown-color.js deleted file mode 100644 index 332cd3f1d1..0000000000 --- a/lib/utilities/markdown-color.js +++ /dev/null @@ -1,213 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var existsSync = require('exists-sync'); - -var chalk = require('chalk'); -var SilentError = require('silent-error'); -var isArray = require('lodash/lang/isArray'); -var merge = require('lodash/object/merge'); - -var colors = ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow', 'black', 'white', 'grey', 'gray']; -var backgroundColors = ['bgRed', 'bgGreen', 'bgBlue', 'bgCyan', 'bgMagenta', 'bgYellow', 'bgWhite', 'bgBlack']; - -module.exports = MarkdownColor; - -/* - Parses markdown and applies coloring to output by matching defined tokens and - applying styling. Default outputs chalk.js coloring for terminal output. - Options: - tokens: pass additional tokens in the following format: - { - name:{ - token: '(Reserved)', // example of token - pattern: /(Reserved)/g, // regexp pattern - render: function(string) { return string + '!'} // function that takes and returns a string - } - } - - markdownOptions: object containing options to pass (or override) to markdown-it-terminal upon - instantiation. See https://github.com/trabus/markdown-it-terminal/blob/master/index.js#L8 - - renderStyles: pass an object to render color styles, default is chalk.js - - @class MarkdownColor - @constructor - @param {Object} [options] -*/ -function MarkdownColor(options) { - var optionTokens = options && options.tokens || {}; - var renderStyles = options && options.renderStyles || chalk; - var tokens = this.generateTokens(renderStyles); - var markdownOptions = options && options.markdownOptions || {}; - var markdown = require('markdown-it'); - var terminal = require('markdown-it-terminal'); - this.options = options || {}; - this.markdown = markdown().use(terminal, markdownOptions); - this.tokens = merge(tokens, optionTokens); - - return this; -} - -/* - Lookup markdown file and render contents - @method renderFile - @param {String} [filePath] - @param {Object} [options] -*/ -MarkdownColor.prototype.renderFile = function (filePath, options) { - var file; - if (existsSync(filePath)) { - file = fs.readFileSync(filePath, 'utf-8'); - } else { - throw new SilentError('The file \'' + filePath + '\' doesn\'t exist.' + - ' Please check your filePath'); - } - - return this.render(file, options); -}; - -/* - Parse markdown and output as string - @method render - @param {String} [string] - @param {Object} [options] - @return {String} -*/ -MarkdownColor.prototype.render = function (string, options) { - var indent = options && options.indent || ''; - var input = this.markdown.render(string); - var styles = Object.keys(this.tokens); - input = input.replace(/^/mg, indent); - styles.reverse().map(function(style) { - input = input.replace(this.tokens[style].pattern, this.tokens[style].render); - }.bind(this)); - input = input.replace(/\~\^(.*)\~\^/g, escapeToken); - return input; -}; - -/* - Generate default style tokens - @method generateTokens - @return {Object} -*/ -MarkdownColor.prototype.generateTokens = function (renderer) { - var defaultTokens = { - // ember-cli styles - option: { - name: 'option', - token: '--option', - pattern: /((--\w*\b)|())/g, - render: renderStylesFactory(renderer, 'cyan') - }, - default: { - name: 'default', - token: '(Default: default)', - pattern: /(\(Default:\s.*\))/g, - render: renderStylesFactory(renderer, 'cyan') - }, - required: { - name: 'required', - token: '(Required)', - pattern: /(\(Required\))/g, - render: renderStylesFactory(renderer, 'cyan') - } - }; - - var colorTokens = unshiftValue(colors.concat(backgroundColors).map(getToken), {}).reduce(setToken); - return merge(colorTokens, defaultTokens); -}; - - -/* - Looks up multiple styles to apply to the rendered output - @method renderStylesFactory - @param {Object} [renderer] should match chalk.js format - @param {String, Array} [styleNames] - @return {Function} Function that will wrap the input string with styling -*/ -MarkdownColor.prototype.renderStylesFactory = renderStylesFactory; -function renderStylesFactory(renderer, styleNames){ - var styles; - if (isArray(styleNames)) { - styles = styleNames.map(checkStyleName.bind(null, renderer)); - } else { - styles = [checkStyleName(renderer, styleNames)]; - } - return function(match, pattern) { - return styles.reverse().reduce(function (previous, current) { - return renderer[current](previous); - }, pattern); - }; -} - -/* - Check to see if style exists on the renderer - @param {Object} [renderer] should match chalk.js format - @param {String} [name] -*/ -function checkStyleName(renderer, name) { - if (Object.keys(renderer.styles).indexOf(name) > -1) { - return name; - } else { - throw new SilentError('The style \'' + name + '\' is not supported by the markdown renderer.'); - } -} - -/* - @param {String} [name] - @param {Object} [options] - @return {RegExp} Returns lookup pattern -*/ -function getColorTokenRegex(name, options) { - options = options || {}; - var start = options.start || '(?:<'; - var end = options.end || '>)'; - var close = options.close || '(?:<\/'; - var middle = options.middle || '(.*?)'; - var tag = start + name + end; - var closeTag = close + name + end; - var pattern = tag + middle + closeTag; - return new RegExp(pattern, 'g'); -} - -/* - @param {String} [name] - @param {Object} [options] - @return {Object} Returns token object -*/ -function getToken(name, options) { - var renderer = options && options.renderer || chalk; - return { - name: name, - token: '<' + name + '>', - pattern: getColorTokenRegex(name), - render: renderStylesFactory(renderer, name) - }; -} - -function setToken(result, color) { - result[color.name] = color; - return result; -} - -function escapeToken(match, pattern){ - var output = pattern.replace(/\~/g,''); - return '<' + output + '>'; -} - -function unshiftValue(array, value) { - array.unshift(value); - return array; -} - -/* -Formatting colors for ember-cli help - -white: ember serve -yellow: -cyan: --port, --important-option -cyan: (Default: something), (Default: 4200) -white: Description 1, Description 2 -cyan: (Required) -*/ diff --git a/lib/utilities/merge-blueprint-options.js b/lib/utilities/merge-blueprint-options.js new file mode 100644 index 0000000000..57bb86fa78 --- /dev/null +++ b/lib/utilities/merge-blueprint-options.js @@ -0,0 +1,31 @@ +'use strict'; + +const SilentError = require('silent-error'); +const Blueprint = require('@ember-tooling/blueprint-model'); + +/* + * Helper for commands that use a blueprint to merge the blueprint's options + * into the command's options so they can be passed in. Needs to be invoked + * with `this` pointing to the command object, e.g. + * + * var mergeBlueprintOptions = require('../utilities/merge-blueprint-options'); + * + * Command.extend({ + * beforeRun: mergeBlueprintOptions + * }) + */ +module.exports = function (rawArgs) { + if (rawArgs.length === 0) { + return; + } + // merge in blueprint availableOptions + let blueprint; + try { + blueprint = Blueprint.lookup(rawArgs[0], { + paths: this.project.blueprintLookupPaths(), + }); + this.registerOptions(blueprint); + } catch (e) { + SilentError.debugOrThrow(`ember-cli/commands/${this.name}`, e); + } +}; diff --git a/lib/utilities/normalize-blueprint-option.js b/lib/utilities/normalize-blueprint-option.js index 1bedf9543c..64db5258c5 100644 --- a/lib/utilities/normalize-blueprint-option.js +++ b/lib/utilities/normalize-blueprint-option.js @@ -1,6 +1,6 @@ 'use strict'; -var path = require('path'); +const path = require('path'); module.exports = function normalizeBlueprintOption(blueprint) { return blueprint[0] === '.' ? path.resolve(process.cwd(), blueprint) : blueprint; diff --git a/lib/utilities/npm.js b/lib/utilities/npm.js deleted file mode 100644 index 0415528927..0000000000 --- a/lib/utilities/npm.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var Promise = require('../ext/promise'); - -// - -/** - Runs the npm command `command` with the supplied args and load options. - - Please note that the loaded module appears to retain some state, so do not - expect multiple invocations within the same process to work without quirks. - This problem is likely fixable. - - @method npm - @param {String} command The npm command to run. - @param {Array} npmArgs The arguments passed to the npm command. - @param {Array} options The options passed when loading npm. - @param {Module} [npm] A reference to the npm module. -*/ -module.exports = function npm(command, npmArgs, options/*, npm*/) { - var lib; - if (arguments.length === 4) { - lib = arguments[3]; - } else { - lib = require('npm'); - } - - var load = Promise.denodeify(lib.load); - - return load(options) - .then(function() { - // if install is denodeified outside load.then(), - // it throws "Call npm.load(config, cb) before using this command." - var operation = Promise.denodeify(lib.commands[command]); - - return operation(npmArgs || []); - }); -}; diff --git a/lib/utilities/package-managers.js b/lib/utilities/package-managers.js new file mode 100644 index 0000000000..1ee012bec2 --- /dev/null +++ b/lib/utilities/package-managers.js @@ -0,0 +1,47 @@ +'use strict'; + +const fs = require('fs'); + +async function determineInstallCommand(projectRoot) { + if (await isPnpmProject(projectRoot)) { + return 'pnpm install'; + } else if (isYarnProject(projectRoot)) { + return 'yarn install'; + } else { + return 'npm install'; + } +} + +async function isPnpmProject(projectRoot) { + if (fs.existsSync(`${projectRoot}/pnpm-lock.yaml`)) { + return true; + } + + const { findWorkspaceDir } = await import('@pnpm/find-workspace-dir'); + + if (await findWorkspaceDir(projectRoot)) { + return true; + } + + return false; +} + +function isYarnProject(projectRoot) { + if (fs.existsSync(`${projectRoot}/yarn.lock`)) { + return true; + } + + const findWorkspaceRoot = require('find-yarn-workspace-root'); + + if (findWorkspaceRoot(projectRoot)) { + return true; + } + + return false; +} + +module.exports = { + determineInstallCommand, + isPnpmProject, + isYarnProject, +}; diff --git a/lib/utilities/parse-options.js b/lib/utilities/parse-options.js index 9ef1350bd6..bed78bf036 100644 --- a/lib/utilities/parse-options.js +++ b/lib/utilities/parse-options.js @@ -1,11 +1,9 @@ 'use strict'; -var reduce = require('lodash/collection/reduce'); - module.exports = function parseOptions(args) { - return reduce(args, function(result, arg) { - var pair = arg.split(':'); - result[pair[0]] = pair[1]; + return args.reduce((result, arg) => { + let parts = arg.split(':'); + result[parts[0]] = parts.slice(1).join(':'); return result; }, {}); }; diff --git a/lib/utilities/path.js b/lib/utilities/path.js deleted file mode 100644 index d35a7176e2..0000000000 --- a/lib/utilities/path.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -module.exports = { - /** - Returns a relative parent path string using the path provided - - @method getRelativeParentPath - @param {String} path The path to relatively get to. - @return {String} the relative path string. - */ - getRelativeParentPath: function getRelativeParentPath(path, offset, slash) { - var offsetValue = offset || 0; - var trailingSlash = typeof slash === 'undefined' ? true : slash; - var outputPath = new Array(path.split('/').length + 1 - offsetValue).join('../'); - - return trailingSlash ? outputPath : outputPath.substr(0, outputPath.length - 1); - }, - - /** - Returns a relative path string using the path provided - - @method getRelativePath - @param {String} path The path to relatively get to. - @return {String} the relative path string. - */ - getRelativePath: function getRelativePath(path, offset) { - var offsetValue = offset || 0; - var relativePath = new Array(path.split('/').length - offsetValue).join('../'); - return relativePath || './'; - } -}; diff --git a/lib/utilities/reexport-template.js b/lib/utilities/reexport-template.js deleted file mode 100644 index 97498134b6..0000000000 --- a/lib/utilities/reexport-template.js +++ /dev/null @@ -1,12 +0,0 @@ -/* global define */ -define('{{DEST}}', ['{{SRC}}', 'ember', 'exports'], function(__index__, __Ember__, __exports__) { - 'use strict'; - var keys = Object.keys || __Ember__['default'].keys; - var forEach = Array.prototype.forEach && function(array, cb) { - array.forEach(cb); - } || __Ember__['default'].EnumerableUtils.forEach; - - forEach(keys(__index__), (function(key) { - __exports__[key] = __index__[key]; - })); -}); diff --git a/lib/utilities/reexport.js b/lib/utilities/reexport.js deleted file mode 100644 index 0e22c4fc72..0000000000 --- a/lib/utilities/reexport.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -var fs = require('fs-extra'); -var path = require('path'); -var Promise = require('../ext/promise'); -var rimraf = Promise.denodeify(fs.remove); -var template = fs.readFileSync(path.join(__dirname, 'reexport-template.js'), 'utf-8'); -var quickTemp = require('quick-temp'); -var hashStrings = require('broccoli-kitchen-sink-helpers').hashStrings; - -function Reexporter(name, outputFile) { - this.name = name; - this.outputFile = outputFile; - this.lastHash = undefined; -} - -Reexporter.prototype.content = function() { - return template - .replace(/\s*\/\*.*\*\/\s*/, '') - .replace('{{DEST}}', this.name) - .replace('{{SRC}}', this.name + '/index'); -}; - -Reexporter.prototype.read = function() { - var dir = quickTemp.makeOrReuse(this, 'tmpCacheDir'); - - if (!this.subdirCreated) { - fs.mkdirSync(path.join(dir, 'reexports')); - this.subdirCreated = true; - } - - var outputPath = path.join(dir, 'reexports', this.outputFile); - - var content = this.content(); - var hash = hashStrings([content]); - - if (this.lastHash !== hash) { - this.lastHash = hash; - fs.writeFileSync(outputPath, content); - } - - return dir; -}; - -Reexporter.prototype.cleanup = function() { - return rimraf(this['tmpCacheDir']); -}; - -module.exports = function(name, outputFile) { - return new Reexporter(name, outputFile); -}; diff --git a/lib/utilities/registry-has-preprocessor.js b/lib/utilities/registry-has-preprocessor.js new file mode 100644 index 0000000000..a3a9233238 --- /dev/null +++ b/lib/utilities/registry-has-preprocessor.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function registryHasPreprocessor(registry, type) { + return registry.load(type).length > 0; +}; diff --git a/lib/utilities/require-as-hash.js b/lib/utilities/require-as-hash.js index 5d4d65e36d..24b01bce28 100644 --- a/lib/utilities/require-as-hash.js +++ b/lib/utilities/require-as-hash.js @@ -1,11 +1,11 @@ 'use strict'; -var stringUtils = require('ember-cli-string-utils'); +const stringUtils = require('ember-cli-string-utils'); // Gathers subclasses of a certain specified base class into a hash. // // e.g.: // Files: -// - ./hamster.js which exports an class of Hamster subclass of Rodent +// - ./hamster.js which exports a class of Hamster subclass of Rodent // - ./parrot.js which exports an instance of Parrot (not a Rodent!) // // requireAsHash('./*.js', Rodent): @@ -13,19 +13,18 @@ var stringUtils = require('ember-cli-string-utils'); // Hamster: Hamster // Same as require('./hamster.js') // } - -var globSync = require('glob').sync; -var path = require('path'); -var getCallerFile = require('./get-caller-file'); +const { globSync } = require('glob'); +const path = require('path'); +const getCallerFile = require('get-caller-file'); module.exports = requireAsHash; function requireAsHash(pattern, type) { - var callerFileDir = path.dirname(getCallerFile()); + let callerFileDir = path.dirname(getCallerFile()); return globSync(pattern, { cwd: callerFileDir }) - .reduce(function(hash, file) { - - var klass = require(callerFileDir + '/' + file); - if (!type || (klass.prototype instanceof type)) { + .sort() + .reduce((hash, file) => { + const klass = require(`${callerFileDir}/${file}`); + if (!type || klass.prototype instanceof type) { hash[stringUtils.classify(path.basename(file, '.js'))] = klass; } return hash; diff --git a/lib/utilities/require-local.js b/lib/utilities/require-local.js deleted file mode 100644 index 69ee177d40..0000000000 --- a/lib/utilities/require-local.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -var path = require('path'); -var nodeModulesPath = require('node-modules-path'); - -module.exports = function requireLocal(lib) { - return require(path.join(nodeModulesPath(process.cwd()), lib)); -}; diff --git a/lib/utilities/root-command.js b/lib/utilities/root-command.js new file mode 100644 index 0000000000..8427c9b0d5 --- /dev/null +++ b/lib/utilities/root-command.js @@ -0,0 +1,10 @@ +'use strict'; + +const Command = require('../models/command'); + +module.exports = Command.extend({ + isRoot: true, + name: 'ember', + + anonymousOptions: [''], +}); diff --git a/lib/utilities/string.js b/lib/utilities/string.js deleted file mode 100644 index 84cef5558a..0000000000 --- a/lib/utilities/string.js +++ /dev/null @@ -1,145 +0,0 @@ -'use strict'; - -// TODO: remove once Ember's version is on npm -var STRING_DASHERIZE_REGEXP = (/[ _]/g); -var STRING_DASHERIZE_CACHE = {}; -var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); -var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); -var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); -var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); - -module.exports = { - /** - Converts a camelized string into all lower case separated by underscores. - - ```javascript - 'innerHTML'.decamelize(); // 'inner_html' - 'action_name'.decamelize(); // 'action_name' - 'css-class-name'.decamelize(); // 'css-class-name' - 'my favorite items'.decamelize(); // 'my favorite items' - ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. - */ - decamelize: function(str) { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); - }, - - /** - Replaces underscores, spaces, or camelCase with dashes. - - ```javascript - 'innerHTML'.dasherize(); // 'inner-html' - 'action_name'.dasherize(); // 'action-name' - 'css-class-name'.dasherize(); // 'css-class-name' - 'my favorite items'.dasherize(); // 'my-favorite-items' - ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - hit = cache.hasOwnProperty(str), - ret; - - if (hit) { - return cache[str]; - } else { - ret = this.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; - } - - return ret; - }, - - /** - Returns the lowerCamelCase form of a string. - - ```javascript - 'innerHTML'.camelize(); // 'innerHTML' - 'action_name'.camelize(); // 'actionName' - 'css-class-name'.camelize(); // 'cssClassName' - 'my favorite items'.camelize(); // 'myFavoriteItems' - 'My Favorite Items'.camelize(); // 'myFavoriteItems' - ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. - */ - camelize: function(str) { - return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }).replace(/^([A-Z])/, function(match) { - return match.toLowerCase(); - }); - }, - - /** - Returns the UpperCamelCase form of a string. - - ```javascript - 'innerHTML'.classify(); // 'InnerHTML' - 'action_name'.classify(); // 'ActionName' - 'css-class-name'.classify(); // 'CssClassName' - 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string - */ - classify: function(str) { - var parts = str.split('.'), - out = []; - - for (var i=0, l=parts.length; i -1) { return false; } - if (name.indexOf('.') > -1) { return false; } - if (!isNaN(parseInt(name.charAt(0), 10))) { return false; } +/** + * Checks if the string starts with a number. + * + * @method startsWithNumber + * @return {Boolean} + */ +function startsWithNumber(name) { + return /^\d.*$/.test(name); +} + +function containsInvalidSlash(name) { + let indexOfFirstSlash = name.indexOf('/'); + let isScoped = name[0] === '@' && indexOfFirstSlash !== -1; + + let containsInvalidSlash = isScoped ? name.indexOf('/', indexOfFirstSlash + 1) > -1 : name.includes('/'); + + return containsInvalidSlash; +} + +/** + * Checks if project name is valid. + * + * Invalid names are some of the internal constants that Ember CLI uses, such as + * `app`, `ember`, `ember-cli`, `test`, and `vendor`. Names that start with + * numbers are considered invalid as well. + * + * @method validProjectName + * @param {String} name The name of Ember CLI project + * @return {Boolean} + */ +module.exports = function isValidProjectName(projectName) { + let lowerSanitizedName = projectName.toLowerCase(); + + if ( + INVALID_PROJECT_NAMES.includes(lowerSanitizedName) || + lowerSanitizedName.includes('.') || + containsInvalidSlash(lowerSanitizedName) || + startsWithNumber(lowerSanitizedName) + ) { + return false; + } return true; }; diff --git a/lib/utilities/version-utils.js b/lib/utilities/version-utils.js index 7315cd7ce7..fd01d12318 100644 --- a/lib/utilities/version-utils.js +++ b/lib/utilities/version-utils.js @@ -1,26 +1,28 @@ 'use strict'; -var existsSync = require('exists-sync'); -var path = require('path'); -var getRepoInfo = require('git-repo-info'); +const fs = require('fs'); +const path = require('path'); +const getRepoInfo = require('git-repo-info'); module.exports = { - emberCLIVersion: function emberCLIVersion() { - var gitPath = path.join(__dirname, '..','..','.git'); - var output = [require('../../package.json').version]; + emberCLIVersion() { + let gitPath = path.join(__dirname, '..', '..', '.git'); + let output = [require('../../package.json').version]; - if (existsSync(gitPath)) { - var repoInfo = getRepoInfo(gitPath); + if (fs.existsSync(gitPath)) { + let { branch, abbreviatedSha } = getRepoInfo(gitPath); - output.push(repoInfo.branch); - output.push(repoInfo.abbreviatedSha); + if (branch) { + output.push(branch.replace(/\//g, '-')); + } + output.push(abbreviatedSha); } return output.join('-'); }, - isDevelopment: function isDevelopment(version) { + isDevelopment(version) { // match postfix SHA in dev version - return !!version.match(/\b[0-9a-f]{5,40}\b/); - } + return /\b[0-9a-f]{5,40}\b/.test(version); + }, }; diff --git a/lib/utilities/will-interrupt-process.js b/lib/utilities/will-interrupt-process.js new file mode 100644 index 0000000000..b0f11f0afb --- /dev/null +++ b/lib/utilities/will-interrupt-process.js @@ -0,0 +1,207 @@ +'use strict'; + +// Allows to setup process interruption handlers. +// The process can be interrupted when ges SIGINT, SIGTERM signal, +// something called process.exit(exitCode) or CTRL+C was pressed. +// +// Node.js doesn't allow to perform async tasks when the process is exiting. +// Also there are some work arounds for exit in the node.js ecosystem. +// +// In order to supply reliable process exit phase, `will-interrupt-process` +// is tightly integrated with `capture-exit` which allows us to perform async cleanup +// on `process.exit()` and control the final exit code. + +const captureExit = require('capture-exit'); + +const handlers = []; + +let _process, _processCapturedLocation, windowsCtrlCTrap, originalIsRaw; + +module.exports = { + capture(outerProcess) { + if (_process) { + throw new Error(`process already captured at: \n\n${_processCapturedLocation.stack}`); + } + + if (isEventEmitterCompatible(outerProcess) === false) { + throw new Error('attempt to capture bad process instance'); + } + + _process = outerProcess; + _processCapturedLocation = new Error(); + + // ember-cli and user apps have many dependencies, many of which require + // process.addListener('exit', ....) for cleanup, by default this limit for + // such listeners is 10, recently users have been increasing this and not to + // their fault, rather they are including large and more diverse sets of + // node_modules. + // + // https://github.com/babel/ember-cli-babel/issues/76 + _process.setMaxListeners(1000); + + // work around misbehaving libraries, so we can correctly cleanup before actually exiting. + captureExit.captureExit(); + }, + + /** + * Drops all the interruption handlers and disables an ability to add new one + * + * Note: We don't call `captureExit.releaseExit() here. + * In some rare scenarios it can lead to the hard to debug issues. + * see: https://github.com/ember-cli/ember-cli/issues/6779#issuecomment-280940358 + * + * We can more or less feel comfortable with a captured exit because it behaves very + * similar to the original `exit` except of cases when we need to do cleanup before exit. + * + * @private + * @method release + */ + release() { + while (handlers.length > 0) { + this.removeHandler(handlers[0]); + } + + _process = null; + _processCapturedLocation = null; + }, + + /** + * Add process interruption handler + * + * When the first handler is added then automatically + * sets up process interruption signals listeners + * + * @private + * @method addHandler + * @param {function} cb Callback to be called when process interruption fired + */ + addHandler(cb) { + if (!_process) { + throw new Error('process is not captured'); + } + + let index = handlers.indexOf(cb); + if (index > -1) { + return; + } + + if (handlers.length === 0) { + setupSignalsTrap(); + } + + handlers.push(cb); + captureExit.onExit(cb); + }, + + /** + * Remove process interruption handler + * + * If there are no remaining handlers after removal + * then clean up all the process interruption signal listeners + * + * @private + * @method removeHandler + * @param {function} cb Callback to be removed + */ + removeHandler(cb) { + let index = handlers.indexOf(cb); + if (index < 0) { + return; + } + + handlers.splice(index, 1); + captureExit.offExit(cb); + + if (handlers.length === 0) { + teardownSignalsTrap(); + } + }, +}; + +/** + * Sets up listeners for interruption signals + * + * When one of these signals is caught than raise process.exit() + * which enforces `capture-exit` to run registered interruption handlers + * + * @method setupSignalsTrap + */ +function setupSignalsTrap() { + _process.on('SIGINT', exit); + _process.on('SIGTERM', exit); + _process.on('message', onMessage); + + if (isWindowsTTY(_process)) { + trapWindowsSignals(_process); + } +} + +/** + * Removes interruption signal listeners and tears down capture-exit + * + * @method teardownSignalsTrap + */ +function teardownSignalsTrap() { + _process.removeListener('SIGINT', exit); + _process.removeListener('SIGTERM', exit); + _process.removeListener('message', onMessage); + + if (isWindowsTTY(_process)) { + cleanupWindowsSignals(_process); + } +} + +/** + * Suppresses "Terminate batch job (Y/N)" confirmation on Windows + * + * @method trapWindowsSignals + */ +function trapWindowsSignals(_process) { + const stdin = _process.stdin; + + originalIsRaw = stdin.isRaw; + + // This is required to capture Ctrl + C on Windows + stdin.setRawMode(true); + + windowsCtrlCTrap = function (data) { + if (data.length === 1 && data[0] === 0x03) { + _process.emit('SIGINT'); + } + }; + stdin.on('data', windowsCtrlCTrap); +} + +function cleanupWindowsSignals(_process) { + const stdin = _process.stdin; + + stdin.setRawMode(originalIsRaw); + + stdin.removeListener('data', windowsCtrlCTrap); +} + +function isWindowsTTY(_process) { + return /^win/.test(_process.platform) && _process.stdin && _process.stdin.isTTY; +} + +function exit() { + _process.exit(); +} + +function onMessage(message) { + if (message.kill) { + exit(); + } +} + +function isEventEmitterCompatible(obj) { + return ( + Boolean(obj) && + typeof obj === 'object' && + typeof obj.on === 'function' && + typeof obj.emit === 'function' && + typeof obj.removeListener === 'function' && + typeof obj.setMaxListeners === 'function' && + typeof obj.exit === 'function' + ); +} diff --git a/lib/utilities/windows-admin.js b/lib/utilities/windows-admin.js index b465eb6e54..d446aaaf16 100644 --- a/lib/utilities/windows-admin.js +++ b/lib/utilities/windows-admin.js @@ -1,41 +1,136 @@ 'use strict'; -var Promise = require('../ext/promise'); -var chalk = require('chalk'); -var exec = require('child_process').exec; +const { default: chalk } = require('chalk'); -module.exports = { +class WindowsSymlinkChecker { /** + * + * On Windows, users will have a much better experience if symlinks are enabled + * and usable. When queried, this object informs Windows users regarding + * improving their build performance, and how. + * + * > Windows Vista: nothing we can really do, so we fall back to junctions for folders + copying of files + * <= Windows Vista: symlinks are available but using them is somewhat tricky + * * if the user is an admin, the process needs to have been started with elevated privileges + * * if the user is not an admin, a specific setting needs to be enabled + * <= Windows 10 + * * if developer mode is enabled, symlinks "just work" + * * https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10 + * + * ```js + * let checker = WindowsSymlinkChecker; + * let { + * windows, + * elevated + * } = await = checker.checkIfSymlinksNeedToBeEnabled(); // aslso emits helpful warnings + * ``` + * + * @public + * @class WindowsSymlinkChecker + */ + constructor(ui, isWindows, canSymlink, exec) { + this.ui = ui; + this.isWindows = isWindows; + this.canSymlink = canSymlink; + this.exec = exec; + } + + /** + * + * if not windows, will fulfill with: + * `{ windows: false, elevated: null)` + * + * if windows, and elevated will fulfill with: + * `{ windows: false, elevated: true)` + * + * if windows, and is NOT elevated will fulfill with: + * `{ windows: false, elevated: false)` + * + * will include heplful warning, so that users know (if possible) how to + * achieve better windows build performance + * + * @public + * @method checkIfSymlinksNeedToBeEnabled + * @return {Promise} Object describing whether we're on windows and if admin rights exist + */ + static checkIfSymlinksNeedToBeEnabled(ui) { + return this._setup(ui).checkIfSymlinksNeedToBeEnabled(); + } + + /** + * sets up a WindowsSymlinkChecker + * + * providing it with defaults for: + * + * * if we are on windows + * * if we can symlink + * * a reference to exec + * + * @private + * @method _setup + * @param UI {UI} + * @return {WindowsSymlinkChecker} + */ + static _setup(ui) { + const exec = require('child_process').exec; + const symlinkOrCopy = require('symlink-or-copy'); + + return new WindowsSymlinkChecker(ui, /^win/.test(process.platform), symlinkOrCopy.canSymlink, exec); + } + + /** + * @public + * @method checkIfSymlinksNeedToBeEnabled + * @return {Promise} Object describing whether we're on windows and if admin rights exist + */ + checkIfSymlinksNeedToBeEnabled() { + return new Promise((resolve) => { + if (!this.isWindows) { + resolve({ + windows: false, + elevated: null, + }); + } else if (this.canSymlink) { + resolve({ + windows: true, + elevated: null, + }); + } else { + resolve(this._checkForElevatedRights(this.ui)); + } + }); + } + + /** + * * Uses the eon-old command NET SESSION to determine whether or not the * current user has elevated rights (think sudo, but Windows). * - * @method checkWindowsElevation + * @private + * @method _checkForElevatedRights * @param {Object} ui - ui object used to call writeLine(); * @return {Object} Object describing whether we're on windows and if admin rights exist */ - checkWindowsElevation: function (ui) { - return new Promise(function (resolve) { - if (/^win/.test(process.platform)) { - exec('NET SESSION', function (error, stdout, stderr) { - var elevated = (!stderr || stderr.length === 0); - - if (!elevated && ui && ui.writeLine) { - ui.writeLine(chalk.yellow('\nRunning without elevated rights. ' + - 'Running Ember CLI "as Administrator" increases performance significantly.')); - ui.writeLine('See www.ember-cli.com/user-guide/#windows for details.\n'); - } - - resolve({ - windows: true, - elevated: elevated - }); - }); - } else { + _checkForElevatedRights() { + let ui = this.ui; + let exec = this.exec; + + return new Promise((resolve) => { + exec('NET SESSION', (error, stdout, stderr) => { + let elevated = !stderr || stderr.length === 0; + + if (!elevated) { + ui.writeLine(chalk.yellow('\nRunning without permission to symlink will degrade build performance.')); + ui.writeLine('See https://cli.emberjs.com/release/appendix/windows/ for details.\n'); + } + resolve({ - windows: false, - elevated: null + windows: true, + elevated, }); - } + }); }); } -}; +} + +module.exports = WindowsSymlinkChecker; diff --git a/package.json b/package.json index f83c5f052a..b198901c12 100644 --- a/package.json +++ b/package.json @@ -1,175 +1,190 @@ { "name": "ember-cli", - "version": "1.13.8", - "main": "lib/cli/index.js", + "version": "7.2.0-alpha.1", "description": "Command line tool for developing ambitious ember.js apps", - "trackingCode": "UA-49225444-1", - "bin": { - "ember": "./bin/ember" - }, - "scripts": { - "docs": "node bin/generate-docs.js", - "test": "node tests/runner", - "test:debug": "node debug tests/runner", - "test-all": "node tests/runner all", - "test-all:debug": "node debug tests/runner all", - "test-all:cover": "istanbul cover tests/runner.js all" - }, - "repository": { - "type": "git", - "url": "https://github.com/ember-cli/ember-cli.git" - }, - "engines": { - "node": ">= 0.10.0" - }, "keywords": [ - "ember.js", - "ember", - "cli", "app", - "kit", "app-kit", - "ember-app-kit" + "blockchain", + "cli", + "ember", + "ember-app-kit", + "ember.js", + "kit" ], - "author": "Stefan Penner, Robert Jackson and ember-cli contributors", - "license": "MIT", + "homepage": "https://cli.emberjs.com/release/", "bugs": { "url": "https://github.com/ember-cli/ember-cli/issues" }, - "bundledDependencies": [ - "bower", - "bower-config", - "broccoli", - "broccoli-clean-css", - "chalk", - "concat-stream", - "configstore", - "diff", - "exists-sync", - "express", - "findup", - "fs-extra", - "git-repo-info", - "glob", - "inflection", - "inquirer", - "leek", - "lodash", - "minimatch", - "morgan", - "node-uuid", - "nopt", - "npm", - "proxy-middleware", - "quick-temp", - "readline2", - "resolve", - "testem", - "through", - "tiny-lr" - ], + "repository": { + "type": "git", + "url": "https://github.com/ember-cli/ember-cli.git" + }, + "license": "MIT", + "author": "Stefan Penner, Robert Jackson and ember-cli contributors", + "main": "lib/cli/index.js", + "bin": { + "ember": "./bin/ember" + }, + "scripts": { + "docs": "yuidoc", + "format": "prettier . --cache --write", + "lint": "concurrently \"pnpm:lint:*(!fix)\" --group --names \"lint:\" --prefixColors auto --timings", + "lint:fix": "concurrently \"pnpm:lint:*:fix\" --group --names \"fix:\" --prefixColors auto --timings && pnpm format", + "lint:format": "prettier . --cache --check", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "mocha": "pnpm exec mocha --require ./tests/bootstrap.js tests/**/*-test.js", + "prepack": "pnpm run docs", + "test": "node --unhandled-rejections=strict tests/runner", + "test:all": "node --unhandled-rejections=strict tests/runner all", + "test:cover": "nyc --all --reporter=text --reporter=lcov node tests/runner all", + "test:debug": "node --unhandled-rejections=strict debug tests/runner", + "test:slow": "node --unhandled-rejections=strict tests/runner slow" + }, + "release-plan": { + "semverIncrementAs": { + "minor": "prerelease", + "patch": "prerelease" + }, + "semverIncrementTag": "alpha", + "publishTag": "alpha" + }, "dependencies": { - "abbrev": "^1.0.5", - "bower": "^1.3.12", - "bower-config": "0.6.1", - "bower-endpoint-parser": "0.2.2", - "broccoli": "0.16.5", - "broccoli-caching-writer": "^1.1.0", - "broccoli-clean-css": "0.2.0", - "broccoli-es3-safe-recast": "^2.0.0", - "broccoli-es6modules": "^1.0.1", - "broccoli-filter": "0.1.14", - "broccoli-funnel": "0.2.8", - "broccoli-kitchen-sink-helpers": "0.2.7", - "broccoli-merge-trees": "0.2.3", - "broccoli-sane-watcher": "^1.1.1", - "broccoli-source": "^1.1.0", - "broccoli-sourcemap-concat": "^1.0.0", - "broccoli-viz": "^1.0.0", - "broccoli-writer": "0.1.1", - "chalk": "1.1.0", + "@ember-tooling/blueprint-blueprint": "workspace:*", + "@ember-tooling/blueprint-model": "workspace:*", + "@ember-tooling/classic-build-addon-blueprint": "workspace:*", + "@ember-tooling/classic-build-app-blueprint": "workspace:*", + "@ember/app-blueprint": "~7.2.0-alpha.1", + "@pnpm/find-workspace-dir": "^1000.1.5", + "babel-remove-types": "^2.0.0", + "broccoli": "^4.0.0", + "broccoli-concat": "^4.2.7", + "broccoli-config-loader": "^1.0.1", + "broccoli-config-replace": "^1.1.3", + "broccoli-debug": "^0.6.5", + "broccoli-funnel": "^3.0.8", + "broccoli-funnel-reducer": "^1.0.0", + "broccoli-merge-trees": "^4.2.0", + "broccoli-middleware": "^2.1.2", + "broccoli-slow-trees": "^3.1.0", + "broccoli-source": "^3.0.1", + "broccoli-stew": "^3.0.0", + "calculate-cache-key-for-tree": "^2.0.0", + "capture-exit": "^2.0.0", + "chalk": "^5.6.2", + "ci-info": "^4.4.0", "clean-base-url": "^1.0.0", - "compression": "^1.4.4", - "concat-stream": "^1.4.7", - "configstore": "0.3.2", - "core-object": "0.0.2", - "cpr": "^0.4.1", - "debug": "^2.1.3", - "diff": "^1.3.1", - "ember-cli-copy-dereference": "^1.0.0", - "ember-cli-get-dependency-depth": "^1.0.0", + "compression": "^1.8.1", + "configstore": "^8.0.0", + "console-ui": "^3.1.2", + "content-tag": "^4.2.0", + "core-object": "^3.1.5", + "dag-map": "^2.0.2", + "diff": "^8.0.4", "ember-cli-is-package-missing": "^1.0.0", "ember-cli-normalize-entity-name": "^1.0.0", - "ember-cli-path-utils": "^1.0.0", - "ember-cli-preprocess-registry": "^1.0.3", - "ember-cli-string-utils": "^1.0.0", - "ember-cli-test-info": "^1.0.0", - "ember-router-generator": "^1.0.0", - "escape-string-regexp": "^1.0.3", - "exists-sync": "0.0.3", + "ember-cli-preprocess-registry": "^5.0.1", + "ember-cli-string-utils": "^1.1.0", + "ensure-posix-path": "^1.1.1", + "execa": "^9.6.1", "exit": "^0.1.2", - "express": "^4.12.3", - "findup": "0.1.5", - "findup-sync": "^0.2.1", - "fs-extra": "0.22.1", - "git-repo-info": "^1.0.4", - "github": "^0.2.3", - "glob": "5.0.13", - "http-proxy": "^1.9.0", - "inflection": "^1.7.0", - "inquirer": "0.5.1", - "is-git-url": "^0.2.0", - "isbinaryfile": "^2.0.3", - "js-string-escape": "^1.0.0", - "leek": "0.0.18", - "lodash": "^3.6.0", - "markdown-it": "4.3.0", - "markdown-it-terminal": "0.0.2", - "merge-defaults": "^0.2.1", - "minimatch": "^2.0.4", - "morgan": "^1.5.2", - "node-modules-path": "^1.0.0", - "node-uuid": "^1.4.3", - "nopt": "^3.0.1", - "npm": "^2.7.6", - "pleasant-progress": "^1.0.2", - "portfinder": "^0.4.0", - "process-relative-require": "^1.0.0", - "promise-map-series": "^0.2.1", - "proxy-middleware": "0.13.0", - "quick-temp": "0.1.2", - "readline2": "0.1.1", - "resolve": "^1.1.6", - "rimraf": "2.3.2", - "rsvp": "^3.0.17", - "sane": "^1.1.1", - "semver": "^4.3.3", - "silent-error": "^1.0.0", - "symlink-or-copy": "^1.0.1", - "temp": "0.8.1", - "testem": "0.9.1", - "through": "^2.3.6", - "tiny-lr": "0.1.5", - "walk-sync": "0.1.3", - "yam": "0.0.18" + "express": "^5.2.1", + "filesize": "^11.0.17", + "find-up": "^8.0.0", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^11.3.5", + "fs-tree-diff": "^2.0.1", + "get-caller-file": "^2.0.5", + "git-repo-info": "^2.1.1", + "glob": "^13.0.6", + "heimdalljs": "^0.2.6", + "heimdalljs-fs-monitor": "^1.1.2", + "heimdalljs-graph": "^1.0.0", + "heimdalljs-logger": "^0.1.10", + "http-proxy": "^1.18.1", + "inflection": "^3.0.2", + "inquirer": "^13.4.3", + "is-git-url": "^1.0.0", + "is-language-code": "^5.1.3", + "lodash": "^4.18.1", + "markdown-it": "^14.1.1", + "markdown-it-terminal": "^0.4.0", + "minimatch": "^10.2.5", + "morgan": "^1.10.1", + "nopt": "^3.0.6", + "npm-package-arg": "^13.0.2", + "os-locale": "^6.0.2", + "p-defer": "^4.0.1", + "portfinder": "^1.0.38", + "promise-map-series": "^0.3.0", + "promise.hash.helper": "^1.0.8", + "quick-temp": "^0.1.9", + "resolve": "^1.22.12", + "resolve-package-path": "^4.0.3", + "safe-stable-stringify": "^2.5.0", + "sane": "^5.0.1", + "semver": "^7.8.1", + "semver-deprecate": "^1.1.0", + "silent-error": "^1.1.1", + "sort-package-json": "^3.6.1", + "symlink-or-copy": "^1.3.1", + "testem": "^3.20.0", + "tiny-lr": "^2.0.0", + "tree-sync": "^2.1.0", + "walk-sync": "^4.0.2", + "watch-detector": "^1.0.2", + "workerpool": "^10.0.2", + "yam": "^1.0.0" }, "devDependencies": { - "chai": "^2.1.2", - "chai-as-promised": "^4.3.0", - "codeclimate-test-reporter": "0.0.4", - "coveralls": "^2.11.2", - "istanbul": "^0.3.13", - "mocha": "^2.2.1", - "mocha-jshint": "^2.0.0", - "mocha-only-detector": "0.0.2", - "multiline": "^1.0.2", - "nock": "^1.2.1", - "node-require-timings": "1.0.0", - "proxyquire": "^1.6.0", - "strip-ansi": "^2.0.0", - "supertest": "0.15.0", - "supports-color": "^3.0.0", - "tmp-sync": "^1.0.1", - "yuidocjs": "0.6.0" + "broccoli-plugin": "^4.0.7", + "broccoli-test-helper": "^2.0.0", + "chai": "^6.2.2", + "chai-as-promised": "^8.0.2", + "chai-files": "^1.4.0", + "chai-jest-snapshot": "^2.0.0", + "concurrently": "^9.2.1", + "ember-cli-blueprint-test-helpers": "^0.19.2", + "ember-cli-internal-test-helpers": "^0.9.1", + "eslint": "^8.57.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-chai-expect": "^3.1.0", + "eslint-plugin-mocha": "^10.5.0", + "eslint-plugin-n": "^17.24.0", + "fixturify": "^3.0.0", + "fixturify-project": "^2.1.1", + "jsdom": "^21.1.2", + "latest-version": "^9.0.0", + "mocha": "^11.7.6", + "nock": "^14.0.15", + "nyc": "^17.1.0", + "prettier": "3.7.4", + "release-plan": "^0.17.4", + "strip-ansi": "^7.2.0", + "supertest": "^7.2.2", + "testdouble": "^3.20.2", + "tmp": "^0.2.5", + "tmp-promise": "^3.0.3", + "websocket": "^1.0.35", + "which": "6.0.0", + "yuidoc-ember-cli-theme": "^1.0.4", + "yuidocjs": "0.10.2" + }, + "packageManager": "pnpm@10.12.1", + "pnpm": { + "patchedDependencies": { + "ember-cli-blueprint-test-helpers": "patches/ember-cli-blueprint-test-helpers.patch" + } + }, + "engines": { + "node": ">= 20.19.0" + }, + "volta": { + "node": "20.19.6", + "pnpm": "10.12.1" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org" } } diff --git a/packages/addon-blueprint/additional-package.json b/packages/addon-blueprint/additional-package.json new file mode 100644 index 0000000000..505e525787 --- /dev/null +++ b/packages/addon-blueprint/additional-package.json @@ -0,0 +1,17 @@ +{ + "keywords": ["ember-addon"], + "scripts": { + "test:ember-compatibility": "ember try:each" + }, + "devDependencies": { + "@embroider/test-setup": "^4.0.0", + "ember-source-channel-url": "^3.0.0", + "ember-try": "^4.0.0" + }, + "peerDependencies": { + "ember-source": ">= 4.0.0" + }, + "ember-addon": { + "configPath": "tests/dummy/config" + } +} diff --git a/packages/addon-blueprint/files/.github/workflows/ci.yml b/packages/addon-blueprint/files/.github/workflows/ci.yml new file mode 100644 index 0000000000..1bac0d4a42 --- /dev/null +++ b/packages/addon-blueprint/files/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: "Tests" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3<% if (pnpm) { %> + - uses: pnpm/action-setup@v4 + with: + version: 9<% } %> + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm' %> + - name: Install Dependencies + run: <%= pnpm ? 'pnpm install --frozen-lockfile' : yarn ? 'yarn install --frozen-lockfile' : 'npm ci' %> + - name: Lint + run: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm run' %> lint + - name: Run Tests + run: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm run' %> test:ember + + floating: + name: "Floating Dependencies" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3<% if (pnpm) { %> + - uses: pnpm/action-setup@v4 + with: + version: 9<% } %> + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm' %> + - name: Install Dependencies + run: <%= pnpm ? 'pnpm install --no-lockfile' : yarn ? 'yarn install --no-lockfile' : 'npm install --no-shrinkwrap' %> + - name: Run Tests + run: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm run' %> test:ember + + try-scenarios: + name: ${{ matrix.try-scenario }} + runs-on: ubuntu-latest + needs: "test" + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + try-scenario: + - ember-lts-5.8 + - ember-lts-5.12 + - ember-release + - ember-beta + - ember-canary + - embroider-safe + - embroider-optimized + + steps: + - uses: actions/checkout@v3<% if (pnpm) { %> + - uses: pnpm/action-setup@v4 + with: + version: 9<% } %> + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm' %> + - name: Install Dependencies + run: <%= pnpm ? 'pnpm install --frozen-lockfile' : yarn ? 'yarn install --frozen-lockfile' : 'npm ci' %> + - name: Run Tests + run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} diff --git a/packages/addon-blueprint/files/.npmrc b/packages/addon-blueprint/files/.npmrc new file mode 100644 index 0000000000..a6eb43de64 --- /dev/null +++ b/packages/addon-blueprint/files/.npmrc @@ -0,0 +1,8 @@ +# these settings allow us to be good stewards of the ecosystem +# end-consumers can flip these settings in their own projects, +# but library authors should not allow for less-than-strictest +# behavior. +# +# For more information: https://pnpm.io/npmrc +auto-install-peers=false +resolve-peers-from-workspace-root=false diff --git a/packages/addon-blueprint/files/CONTRIBUTING.md b/packages/addon-blueprint/files/CONTRIBUTING.md new file mode 100644 index 0000000000..2ff48f7435 --- /dev/null +++ b/packages/addon-blueprint/files/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# How To Contribute + +## Installation + +- `git clone ` +- `cd <%= addonDirectory %>` +- `<% if (pnpm) { %>pnpm<% } else if (yarn) { %>yarn<% } else { %>npm<% } %> install` + +## Linting + +- `<%= invokeScriptPrefix %> lint` +- `<%= invokeScriptPrefix %> lint:fix` + +## Running tests + +- `<%= invokeScriptPrefix %> test` – Runs the test suite on the current Ember version +- `<%= invokeScriptPrefix %> test:ember <% if (npm) { %>-- <% } %>--server` – Runs the test suite in "watch mode" +- `<%= invokeScriptPrefix %> test:ember-compatibility` – Runs the test suite against multiple Ember versions + +## Running the dummy application + +- `<%= invokeScriptPrefix %> start` +- Visit the dummy application at [http://localhost:4200](http://localhost:4200). + +For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). diff --git a/blueprints/addon/files/LICENSE.md b/packages/addon-blueprint/files/LICENSE.md similarity index 100% rename from blueprints/addon/files/LICENSE.md rename to packages/addon-blueprint/files/LICENSE.md diff --git a/packages/addon-blueprint/files/README.md b/packages/addon-blueprint/files/README.md new file mode 100644 index 0000000000..9968209d9d --- /dev/null +++ b/packages/addon-blueprint/files/README.md @@ -0,0 +1,37 @@ +# <%= addonName %> + +[Short description of the addon.] + +## Compatibility + +- Ember.js v5.8 or above +- Ember CLI v5.8 or above +- Node.js v20 or above + +## Installation + +``` +ember install <%= addonName %> +``` + +## Usage + +[Longer description of how to use the addon in apps.] + +> TODO: Document the package's public API. +> +> For each public api (including components, helpers, modifiers, and other apis) include: +> +> - The import path for a consumer (e.g. `import MyAddonsComponent from 'my-addon/components/my-addons-component'`) +> - What it does +> - Parameters/options +> - Return value +> - Example usage + +## Contributing + +See the [Contributing](CONTRIBUTING.md) guide for details. + +## License + +This project is licensed under the [MIT License](LICENSE.md). diff --git a/blueprints/addon/files/addon/.gitkeep b/packages/addon-blueprint/files/addon/.gitkeep similarity index 100% rename from blueprints/addon/files/addon/.gitkeep rename to packages/addon-blueprint/files/addon/.gitkeep diff --git a/blueprints/addon/files/app/.gitkeep b/packages/addon-blueprint/files/app/.gitkeep similarity index 100% rename from blueprints/addon/files/app/.gitkeep rename to packages/addon-blueprint/files/app/.gitkeep diff --git a/packages/addon-blueprint/files/config/ember-try.js b/packages/addon-blueprint/files/config/ember-try.js new file mode 100644 index 0000000000..8fa9378659 --- /dev/null +++ b/packages/addon-blueprint/files/config/ember-try.js @@ -0,0 +1,54 @@ +'use strict'; + +const getChannelURL = require('ember-source-channel-url'); +const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); + +module.exports = async function () { + return { + packageManager: '<%= packageManager %>', + scenarios: [ + { + name: 'ember-lts-5.8', + npm: { + devDependencies: { + 'ember-source': '~5.8.0', + }, + }, + }, + { + name: 'ember-lts-5.12', + npm: { + devDependencies: { + 'ember-source': '~5.12.0', + }, + }, + }, + { + name: 'ember-release', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('release'), + }, + }, + }, + { + name: 'ember-beta', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('beta'), + }, + }, + }, + { + name: 'ember-canary', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('canary'), + }, + }, + }, + embroiderSafe(), + embroiderOptimized(), + ], + }; +}; diff --git a/packages/addon-blueprint/files/config/optional-features.json b/packages/addon-blueprint/files/config/optional-features.json new file mode 100644 index 0000000000..f84cd7eed7 --- /dev/null +++ b/packages/addon-blueprint/files/config/optional-features.json @@ -0,0 +1,7 @@ +{ + "application-template-wrapper": false, + "default-async-observers": true, + "jquery-integration": false, + "template-only-glimmer-components": true, + "use-ember-modules": true +} diff --git a/packages/addon-blueprint/files/ember-cli-build.js b/packages/addon-blueprint/files/ember-cli-build.js new file mode 100644 index 0000000000..6ca7d27948 --- /dev/null +++ b/packages/addon-blueprint/files/ember-cli-build.js @@ -0,0 +1,27 @@ +'use strict'; + +const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); + +module.exports = function (defaults) { + const app = new EmberAddon(defaults, { + <% if (typescript) {%>'ember-cli-babel': { enableTypeScriptTransform: true }, + + <% } %>// Add options here + }); + + /* + This build file specifies the options for the dummy test app of this + addon, located in `/tests/dummy` + This build file does *not* influence how the addon or the app using it + behave. You most likely want to be modifying `./index.js` or app's build file + */ + + const { maybeEmbroider } = require('@embroider/test-setup'); + return maybeEmbroider(app, { + skipBabel: [ + { + package: 'qunit', + }, + ], + }); +}; diff --git a/packages/addon-blueprint/files/index.js b/packages/addon-blueprint/files/index.js new file mode 100644 index 0000000000..a64c45f744 --- /dev/null +++ b/packages/addon-blueprint/files/index.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + name: require('./package').name,<% if (typescript) {%> + + options: { + 'ember-cli-babel': { enableTypeScriptTransform: true }, + },<% } %> +}; diff --git a/packages/addon-blueprint/files/npmignore b/packages/addon-blueprint/files/npmignore new file mode 100644 index 0000000000..5f42c582c7 --- /dev/null +++ b/packages/addon-blueprint/files/npmignore @@ -0,0 +1,28 @@ +# compiled output +/dist/ +/tmp/ + +# misc +/.editorconfig +/.ember-cli +/.env* +/.eslintcache +/.git/ +/.github/ +/.gitignore +/.prettierignore +/.prettierrc.js +/.stylelintignore +/.stylelintrc.js +/.template-lintrc.js +/.watchmanconfig +/CONTRIBUTING.md +/ember-cli-build.js +/eslint.config.mjs +/testem.js +/tests/ +/tsconfig.declarations.json +/tsconfig.json +/yarn-error.log +/yarn.lock +.gitkeep diff --git a/packages/addon-blueprint/files/tsconfig.declarations.json b/packages/addon-blueprint/files/tsconfig.declarations.json new file mode 100644 index 0000000000..5a21df72e5 --- /dev/null +++ b/packages/addon-blueprint/files/tsconfig.declarations.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationDir": "declarations", + "emitDeclarationOnly": true, + "noEmit": false, + "rootDir": "." + }, + "include": ["addon", "addon-test-support"] +} diff --git a/packages/addon-blueprint/files/tsconfig.json b/packages/addon-blueprint/files/tsconfig.json new file mode 100644 index 0000000000..3261fbc2de --- /dev/null +++ b/packages/addon-blueprint/files/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@tsconfig/ember/tsconfig.json", + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "compilerOptions": { + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "dummy/tests/*": ["tests/*"], + "dummy/*": ["tests/dummy/app/*", "app/*"], + "<%= addonName %>": ["addon"], + "<%= addonName %>/*": ["addon/*"], + "<%= addonName %>/test-support": ["addon-test-support"], + "<%= addonName %>/test-support/*": ["addon-test-support/*"], + "*": ["types/*"] + }, + "types": ["ember-source/types"] + } +} diff --git a/packages/addon-blueprint/index.js b/packages/addon-blueprint/index.js new file mode 100644 index 0000000000..167f159192 --- /dev/null +++ b/packages/addon-blueprint/index.js @@ -0,0 +1,316 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const walkSync = require('walk-sync'); +const { default: chalk } = require('chalk'); +const stringUtil = require('ember-cli-string-utils'); +const merge = require('lodash/merge'); +const uniq = require('lodash/uniq'); +const SilentError = require('silent-error'); +const { sortPackageJson } = require('sort-package-json'); + +let date = new Date(); + +const normalizeEntityName = require('ember-cli-normalize-entity-name'); + +const directoryForPackageName = require('@ember-tooling/blueprint-model/utilities/directory-for-package-name'); +const FileInfo = require('@ember-tooling/blueprint-model/utilities/file-info'); + +const blueprintVersion = require('./package.json').version; + +function stringifyAndNormalize(contents) { + return `${JSON.stringify(contents, null, 2)}\n`; +} + +const replacers = { + 'package.json'(content) { + return this.updatePackageJson(content); + }, +}; + +const ADDITIONAL_PACKAGE = require('./additional-package.json'); + +const description = 'The default blueprint for ember-cli addons.'; +module.exports = { + description, + appBlueprintName: 'app', + + shouldTransformTypeScript: true, + + filesToRemove: [ + 'tests/dummy/app/styles/.gitkeep', + 'tests/dummy/app/templates/.gitkeep', + 'tests/dummy/app/views/.gitkeep', + 'tests/dummy/public/.gitkeep', + 'Brocfile.js', + 'testem.json', + ], + + updatePackageJson(content) { + let contents = JSON.parse(content); + + contents.name = stringUtil.dasherize(this.options.entity.name); + contents.description = this.description; + + delete contents.private; + + contents.dependencies = contents.dependencies || {}; + contents.devDependencies = contents.devDependencies || {}; + + // npm doesn't like it when we have something in both deps and devDeps + // and dummy app still uses it when in deps + contents.dependencies['ember-cli-babel'] = contents.devDependencies['ember-cli-babel']; + delete contents.devDependencies['ember-cli-babel']; + + // Addons must bring in their own version of `@babel/core` when using + // `ember-cli-babel` >= v8. More info: + // https://github.com/babel/ember-cli-babel/blob/master/UPGRADING.md#upgrade-path-for-addons + contents.dependencies['@babel/core'] = contents.devDependencies['@babel/core']; + delete contents.devDependencies['@babel/core']; + + // Move ember-cli-htmlbars into the dependencies of the addon blueprint by default + // to prevent error: + // `Addon templates were detected but there are no template compilers registered for (addon-name)` + contents.dependencies['ember-cli-htmlbars'] = contents.devDependencies['ember-cli-htmlbars']; + delete contents.devDependencies['ember-cli-htmlbars']; + + contents.dependencies['ember-template-imports'] = contents.devDependencies['ember-template-imports']; + delete contents.devDependencies['ember-template-imports']; + + // 95% of addons don't need ember-data or ember-fetch, make them opt-in instead + let deps = Object.keys(contents.devDependencies); + for (let depName of deps) { + if (depName.includes('ember-data') || depName.includes('warp-drive')) { + delete contents.devDependencies[depName]; + } + } + + // Per RFC #811, addons should not have this dependency. + // @see https://github.com/emberjs/rfcs/blob/master/text/0811-element-modifiers.md#detailed-design + delete contents.devDependencies['ember-modifier']; + + // 100% of addons don't need ember-cli-app-version, make it opt-in instead + delete contents.devDependencies['ember-cli-app-version']; + + // add scripts to build type declarations for TypeScript addons + if (this.options.typescript) { + contents.devDependencies.rimraf = '^5.0.10'; + + contents.scripts.prepack = 'tsc --project tsconfig.declarations.json'; + contents.scripts.postpack = 'rimraf declarations'; + + contents.typesVersions = { + '*': { + 'test-support': ['declarations/addon-test-support/index.d.ts'], + 'test-support/*': ['declarations/addon-test-support/*', 'declarations/addon-test-support/*/index.d.ts'], + '*': ['declarations/addon/*', 'declarations/addon/*/index.d.ts'], + }, + }; + } + + merge(contents, ADDITIONAL_PACKAGE); + + return stringifyAndNormalize(sortPackageJson(contents)); + }, + + /** + * @override + * + * This modification of buildFileInfo allows our differing + * input files to output to a single file, depending on the options. + * For example: + * + * for javascript, + * _ts_eslint.config.mjs is deleted + * _js_eslint.config.mjs is renamed to eslint.config.mjs + * + * for typescript, + * _js_eslint.config.mjs is deleted + * _ts_eslint.config.mjs is renamed to eslint.config.mjs + */ + buildFileInfo(intoDir, templateVariables, file, commandOptions) { + if (file.includes('_js_') || file.includes('_ts_')) { + let fileInfo = this._super.buildFileInfo.apply(this, arguments); + + if (file.includes('_js_')) { + if (commandOptions.typescript) { + return null; + } + + fileInfo.outputBasePath = fileInfo.outputPath.replace('_js_', ''); + fileInfo.outputPath = fileInfo.outputPath.replace('_js_', ''); + fileInfo.displayPath = fileInfo.outputPath.replace('_js_', ''); + return fileInfo; + } + + if (file.includes('_ts_')) { + if (!commandOptions.typescript) { + return null; + } + + fileInfo.outputBasePath = fileInfo.outputPath.replace('_ts_', ''); + fileInfo.outputPath = fileInfo.outputPath.replace('_ts_', ''); + fileInfo.displayPath = fileInfo.outputPath.replace('_ts_', ''); + return fileInfo; + } + + return fileInfo; + } + + let mappedPath = this.mapFile(file, templateVariables); + let options = { + action: 'write', + outputBasePath: path.normalize(intoDir), + outputPath: path.join(intoDir, mappedPath), + displayPath: path.normalize(mappedPath), + inputPath: this.srcPath(file), + templateVariables, + ui: this.ui, + }; + + if (file in replacers) { + options.replacer = replacers[file].bind(this); + } + + return new FileInfo(options); + }, + + beforeInstall() { + const prependEmoji = require('@ember-tooling/blueprint-model/utilities/prepend-emoji'); + + this.ui.writeLine(chalk.blue(`@ember-tooling/classic-build-addon-blueprint v${blueprintVersion}`)); + this.ui.writeLine(''); + this.ui.writeLine(prependEmoji('✨', `Creating a new Ember addon in ${chalk.yellow(process.cwd())}:`)); + }, + + locals(options) { + let entity = { name: 'dummy' }; + let rawName = entity.name; + let name = stringUtil.dasherize(rawName); + let namespace = stringUtil.classify(rawName); + + let addonEntity = options.entity; + let addonRawName = addonEntity.name; + let addonName = stringUtil.dasherize(addonRawName); + let addonNamespace = stringUtil.classify(addonRawName); + + let hasOptions = options.welcome || options.packageManager || options.ciProvider; + let blueprintOptions = ''; + if (hasOptions) { + let indent = `\n `; + let outdent = `\n `; + + blueprintOptions = + indent + + [ + options.welcome && '"--welcome"', + options.packageManager === 'yarn' && '"--yarn"', + options.packageManager === 'pnpm' && '"--pnpm"', + options.ciProvider && `"--ci-provider=${options.ciProvider}"`, + options.typescript && `"--typescript"`, + ] + .filter(Boolean) + .join(',\n ') + + outdent; + } + + let invokeScriptPrefix = 'npm run'; + + if (options.packageManager === 'yarn') { + invokeScriptPrefix = 'yarn'; + } + + if (options.packageManager === 'pnpm') { + invokeScriptPrefix = 'pnpm'; + } + + return { + addonDirectory: directoryForPackageName(addonName), + name, + modulePrefix: name, + namespace, + addonName, + addonNamespace, + blueprintVersion, + year: date.getFullYear(), + yarn: options.packageManager === 'yarn', + pnpm: options.packageManager === 'pnpm', + npm: options.packageManager !== 'yarn' && options.packageManager !== 'pnpm', + invokeScriptPrefix, + welcome: options.welcome, + blueprint: '@ember-tooling/classic-build-addon-blueprint', + blueprintOptions, + embroider: false, + lang: options.lang, + emberData: options.emberData, + warpDrive: options.warpDrive ?? options.emberData ?? false, + ciProvider: options.ciProvider, + typescript: options.typescript, + strict: options.strict, + packageManager: options.packageManager ?? 'npm', + }; + }, + + files(options) { + let appFiles = this.lookupBlueprint(this.appBlueprintName).files(options); + let addonFilesPath = this.filesPath(this.options); + let ignore = []; + + if (this.options.ciProvider !== 'github') { + ignore.push('.github'); + } + + let addonFiles = walkSync(addonFilesPath, { ignore }); + + if (options.packageManager !== 'pnpm') { + addonFiles = addonFiles.filter((file) => !file.endsWith('.npmrc')); + } + + if (!options.typescript) { + addonFiles = addonFiles.filter((file) => !file.startsWith('tsconfig.') && !file.endsWith('.d.ts')); + } + + return uniq(appFiles.concat(addonFiles)); + }, + + mapFile() { + let result = this._super.mapFile.apply(this, arguments); + return this.fileMapper(result); + }, + + fileMap: { + '^app/.gitkeep': 'app/.gitkeep', + '^app.*': 'tests/dummy/:path', + '^config.*': 'tests/dummy/:path', + '^public.*': 'tests/dummy/:path', + + '^npmignore': '.npmignore', + }, + + fileMapper(path) { + for (let pattern in this.fileMap) { + if (new RegExp(pattern).test(path)) { + return this.fileMap[pattern].replace(':path', path); + } + } + + return path; + }, + + normalizeEntityName(entityName) { + entityName = normalizeEntityName(entityName); + + if (this.project.isEmberCLIProject() && !this.project.isEmberCLIAddon()) { + throw new SilentError('Generating an addon in an existing ember-cli project is not supported.'); + } + + return entityName; + }, + + srcPath(file) { + let path = `${this.path}/files/${file}`; + let superPath = `${this.lookupBlueprint(this.appBlueprintName).path}/files/${file}`; + return fs.existsSync(path) ? path : superPath; + }, +}; diff --git a/packages/addon-blueprint/package.json b/packages/addon-blueprint/package.json new file mode 100644 index 0000000000..1da629cc2d --- /dev/null +++ b/packages/addon-blueprint/package.json @@ -0,0 +1,32 @@ +{ + "name": "@ember-tooling/classic-build-addon-blueprint", + "version": "7.2.0-alpha.1", + "repository": { + "type": "git", + "url": "https://github.com/ember-cli/ember-cli.git", + "directory": "packages/addon-blueprint" + }, + "license": "MIT", + "keywords": [ + "ember-blueprint" + ], + "dependencies": { + "@ember-tooling/blueprint-model": "workspace:*", + "chalk": "^5.6.2", + "ember-cli-normalize-entity-name": "^1.0.0", + "ember-cli-string-utils": "^1.1.0", + "fs-extra": "^11.3.5", + "lodash": "^4.18.1", + "silent-error": "^1.1.1", + "sort-package-json": "^2.15.1", + "walk-sync": "^3.0.0" + }, + "release-plan": { + "semverIncrementAs": { + "minor": "prerelease", + "patch": "prerelease" + }, + "semverIncrementTag": "alpha", + "publishTag": "alpha" + } +} diff --git a/blueprints/app/files/.editorconfig b/packages/app-blueprint/files/.editorconfig similarity index 67% rename from blueprints/app/files/.editorconfig rename to packages/app-blueprint/files/.editorconfig index 47c5438403..c35a002406 100644 --- a/blueprints/app/files/.editorconfig +++ b/packages/app-blueprint/files/.editorconfig @@ -4,7 +4,6 @@ root = true - [*] end_of_line = lf charset = utf-8 @@ -13,22 +12,8 @@ insert_final_newline = true indent_style = space indent_size = 2 -[*.js] -indent_style = space -indent_size = 2 - [*.hbs] insert_final_newline = false -indent_style = space -indent_size = 2 - -[*.css] -indent_style = space -indent_size = 2 - -[*.html] -indent_style = space -indent_size = 2 [*.{diff,md}] trim_trailing_whitespace = false diff --git a/packages/app-blueprint/files/.ember-cli b/packages/app-blueprint/files/.ember-cli new file mode 100644 index 0000000000..25e5832e97 --- /dev/null +++ b/packages/app-blueprint/files/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": <%= typescript ? 'true' : 'false' %>, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": <%= strict ? '"strict"' : '"loose"' %>, + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": <%= strict ? '"strict"' : '"loose"' %> +} diff --git a/packages/app-blueprint/files/.github/workflows/ci.yml b/packages/app-blueprint/files/.github/workflows/ci.yml new file mode 100644 index 0000000000..b28e6d69f5 --- /dev/null +++ b/packages/app-blueprint/files/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3<% if (pnpm) { %> + - uses: pnpm/action-setup@v4 + with: + version: 9<% } %> + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm' %> + - name: Install Dependencies + run: <%= pnpm ? 'pnpm install --frozen-lockfile' : yarn ? 'yarn install --frozen-lockfile' : 'npm ci' %> + - name: Lint + run: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm run' %> lint + + test: + name: "Test" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3<% if (pnpm) { %> + - uses: pnpm/action-setup@v4 + with: + version: 9<% } %> + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm' %> + - name: Install Dependencies + run: <%= pnpm ? 'pnpm install --frozen-lockfile' : yarn ? 'yarn install --frozen-lockfile' : 'npm ci' %> + - name: Run Tests + run: <%= pnpm ? 'pnpm' : yarn ? 'yarn' : 'npm' %> test diff --git a/packages/app-blueprint/files/.prettierignore b/packages/app-blueprint/files/.prettierignore new file mode 100644 index 0000000000..d7ab45945f --- /dev/null +++ b/packages/app-blueprint/files/.prettierignore @@ -0,0 +1,13 @@ +# unconventional js +/blueprints/*/files/ + +# compiled output +/dist/ + +# misc +/coverage/ +!.* +.*/ +/pnpm-lock.yaml +ember-cli-update.json +*.html diff --git a/packages/app-blueprint/files/.prettierrc.js b/packages/app-blueprint/files/.prettierrc.js new file mode 100644 index 0000000000..8e62a451ad --- /dev/null +++ b/packages/app-blueprint/files/.prettierrc.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + plugins: ['prettier-plugin-ember-template-tag'], + overrides: [ + { + files: '*.{js,gjs,ts,gts,mjs,mts,cjs,cts}', + options: { + singleQuote: true, + templateSingleQuote: false, + }, + }, + ], +}; diff --git a/packages/app-blueprint/files/.stylelintignore b/packages/app-blueprint/files/.stylelintignore new file mode 100644 index 0000000000..fc178a0b91 --- /dev/null +++ b/packages/app-blueprint/files/.stylelintignore @@ -0,0 +1,5 @@ +# unconventional files +/blueprints/*/files/ + +# compiled output +/dist/ diff --git a/packages/app-blueprint/files/.stylelintrc.js b/packages/app-blueprint/files/.stylelintrc.js new file mode 100644 index 0000000000..56a013c908 --- /dev/null +++ b/packages/app-blueprint/files/.stylelintrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + extends: ['stylelint-config-standard'], +}; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/index.js b/packages/app-blueprint/files/.template-lintrc.js similarity index 57% rename from tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/index.js rename to packages/app-blueprint/files/.template-lintrc.js index 733f4299c9..f35f61c7b3 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/index.js +++ b/packages/app-blueprint/files/.template-lintrc.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'ember-cool-addon' + extends: 'recommended', }; diff --git a/packages/app-blueprint/files/.watchmanconfig b/packages/app-blueprint/files/.watchmanconfig new file mode 100644 index 0000000000..f9c3d8f84f --- /dev/null +++ b/packages/app-blueprint/files/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["dist"] +} diff --git a/packages/app-blueprint/files/README.md b/packages/app-blueprint/files/README.md new file mode 100644 index 0000000000..5c8ddf0337 --- /dev/null +++ b/packages/app-blueprint/files/README.md @@ -0,0 +1,58 @@ +# <%= name %> + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/)<% if (pnpm) { %> +- [pnpm](https://pnpm.io/)<% } else if (yarn) { %> +- [Yarn](https://yarnpkg.com/)<% } else { %> (with npm)<% } %> +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd <%= appDirectory %>` +- `<% if (pnpm) { %>pnpm<% } else if (yarn) { %>yarn<% } else { %>npm<% } %> install` + +## Running / Development + +- `<%= invokeScriptPrefix %> start` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `<%= invokeScriptPrefix %> test` +- `<%= invokeScriptPrefix %> test:ember <% if (npm) { %>-- <% } %>--server` + +### Linting + +- `<%= invokeScriptPrefix %> lint` +- `<%= invokeScriptPrefix %> lint:fix` + +### Building + +- `<%= execBinPrefix %> ember build` (development) +- `<%= invokeScriptPrefix %> build` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/packages/app-blueprint/files/_js_eslint.config.mjs b/packages/app-blueprint/files/_js_eslint.config.mjs new file mode 100644 index 0000000000..e4c0229f9a --- /dev/null +++ b/packages/app-blueprint/files/_js_eslint.config.mjs @@ -0,0 +1,128 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import globals from 'globals'; +import js from '@eslint/js'; + +import ember from 'eslint-plugin-ember/recommended';<% if (warpDrive) { %> +import WarpDrive from 'eslint-plugin-warp-drive/recommended';<% } %> +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const esmParserOptions = { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], + ], + }, +}; + +export default [ + js.configs.recommended, + eslintConfigPrettier, + ember.configs.base, + ember.configs.gjs, + <% if (warpDrive) { %>...WarpDrive, + <% } %>/** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: esmParserOptions, + globals: { + ...globals.browser, + }, + }, + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: esmParserOptions, + globals: { + ...globals.node, + }, + }, + }, +]; diff --git a/packages/app-blueprint/files/_ts_eslint.config.mjs b/packages/app-blueprint/files/_ts_eslint.config.mjs new file mode 100644 index 0000000000..31dd559784 --- /dev/null +++ b/packages/app-blueprint/files/_ts_eslint.config.mjs @@ -0,0 +1,153 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import globals from 'globals'; +import js from '@eslint/js'; + +import ts from 'typescript-eslint'; + +import ember from 'eslint-plugin-ember/recommended';<% if (warpDrive) { %> +import WarpDrive from 'eslint-plugin-warp-drive/recommended';<% } %> + +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const parserOptions = { + esm: { + js: { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + [ + '@babel/plugin-proposal-decorators', + { decoratorsBeforeExport: true }, + ], + ], + }, + }, + ts: { + projectService: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, +}; + +export default ts.config( + js.configs.recommended, + ember.configs.base, + ember.configs.gjs, + ember.configs.gts, + <% if (warpDrive) { %>...WarpDrive, + <% } %>eslintConfigPrettier, + /** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: parserOptions.esm.js, + globals: { + ...globals.browser, + }, + }, + }, + { + files: ['**/*.{ts,gts}'], + languageOptions: { + parser: ember.parser, + parserOptions: parserOptions.esm.ts, + }, + extends: [...ts.configs.recommendedTypeChecked, ember.configs.gts], + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs,ts,gts}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: parserOptions.esm.js, + globals: { + ...globals.node, + }, + }, + }, +); diff --git a/packages/app-blueprint/files/app/app.ts b/packages/app-blueprint/files/app/app.ts new file mode 100644 index 0000000000..dea884f275 --- /dev/null +++ b/packages/app-blueprint/files/app/app.ts @@ -0,0 +1,18 @@ +<% if (warpDrive) { %>import '@warp-drive/ember/install'; +<% } %>import Application from '@ember/application'; +import Resolver from 'ember-resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from '<%= modulePrefix %>/config/environment'; +import { importSync, isDevelopingApp, macroCondition } from '@embroider/macros'; + +if (macroCondition(isDevelopingApp())) { + importSync('./deprecation-workflow'); +} + +export default class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = Resolver; +} + +loadInitializers(App, config.modulePrefix); diff --git a/blueprints/app/files/app/components/.gitkeep b/packages/app-blueprint/files/app/components/.gitkeep similarity index 100% rename from blueprints/app/files/app/components/.gitkeep rename to packages/app-blueprint/files/app/components/.gitkeep diff --git a/packages/app-blueprint/files/app/config/environment.d.ts b/packages/app-blueprint/files/app/config/environment.d.ts new file mode 100644 index 0000000000..8a75a83575 --- /dev/null +++ b/packages/app-blueprint/files/app/config/environment.d.ts @@ -0,0 +1,14 @@ +/** + * Type declarations for + * import config from '<%= name %>/config/environment' + */ +declare const config: { + environment: string; + modulePrefix: string; + podModulePrefix: string; + locationType: 'history' | 'hash' | 'none'; + rootURL: string; + APP: Record; +}; + +export default config; diff --git a/blueprints/app/files/app/controllers/.gitkeep b/packages/app-blueprint/files/app/controllers/.gitkeep similarity index 100% rename from blueprints/app/files/app/controllers/.gitkeep rename to packages/app-blueprint/files/app/controllers/.gitkeep diff --git a/packages/app-blueprint/files/app/deprecation-workflow.ts b/packages/app-blueprint/files/app/deprecation-workflow.ts new file mode 100644 index 0000000000..274a689db8 --- /dev/null +++ b/packages/app-blueprint/files/app/deprecation-workflow.ts @@ -0,0 +1,24 @@ +import setupDeprecationWorkflow from 'ember-cli-deprecation-workflow'; + +/** + * Docs: https://github.com/ember-cli/ember-cli-deprecation-workflow + */ +setupDeprecationWorkflow({ + /** + false by default, but if a developer / team wants to be more aggressive about being proactive with + handling their deprecations, this should be set to "true" + */ + throwOnUnhandled: false, + workflow: [ + /* ... handlers ... */ + /* to generate this list, run your app for a while (or run the test suite), + * and then run in the browser console: + * + * deprecationWorkflow.flushDeprecations() + * + * And copy the handlers here + */ + /* example: */ + /* { handler: 'silence', matchId: 'template-action' }, */ + ], +}); diff --git a/blueprints/app/files/app/helpers/.gitkeep b/packages/app-blueprint/files/app/helpers/.gitkeep similarity index 100% rename from blueprints/app/files/app/helpers/.gitkeep rename to packages/app-blueprint/files/app/helpers/.gitkeep diff --git a/packages/app-blueprint/files/app/index.html b/packages/app-blueprint/files/app/index.html new file mode 100644 index 0000000000..ad1d6573b5 --- /dev/null +++ b/packages/app-blueprint/files/app/index.html @@ -0,0 +1,24 @@ + + lang="<%= lang %>"<% } %>> + + + <%= namespace %> + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/blueprints/app/files/app/models/.gitkeep b/packages/app-blueprint/files/app/models/.gitkeep similarity index 100% rename from blueprints/app/files/app/models/.gitkeep rename to packages/app-blueprint/files/app/models/.gitkeep diff --git a/packages/app-blueprint/files/app/router.ts b/packages/app-blueprint/files/app/router.ts new file mode 100644 index 0000000000..521696ac99 --- /dev/null +++ b/packages/app-blueprint/files/app/router.ts @@ -0,0 +1,11 @@ +import EmberRouter from '@ember/routing/router'; +import config from '<%= modulePrefix %>/config/environment'; + +export default class Router extends EmberRouter { + location = config.locationType; + rootURL = config.rootURL; +} + +Router.map(function () {<% if (typescript) { %> + // Add route declarations here +<% } %>}); diff --git a/blueprints/app/files/app/routes/.gitkeep b/packages/app-blueprint/files/app/routes/.gitkeep similarity index 100% rename from blueprints/app/files/app/routes/.gitkeep rename to packages/app-blueprint/files/app/routes/.gitkeep diff --git a/packages/app-blueprint/files/app/services/store.ts b/packages/app-blueprint/files/app/services/store.ts new file mode 100644 index 0000000000..fae48c6060 --- /dev/null +++ b/packages/app-blueprint/files/app/services/store.ts @@ -0,0 +1,17 @@ +import { useLegacyStore } from '@warp-drive/legacy'; +import { JSONAPICache } from '@warp-drive/json-api'; + +const Store = useLegacyStore({ + linksMode: false, + cache: JSONAPICache, + handlers: [ + // -- your handlers here + ], + schemas: [ + // -- your schemas here + ], +}); + +type Store = InstanceType; + +export default Store; diff --git a/packages/app-blueprint/files/app/styles/app.css b/packages/app-blueprint/files/app/styles/app.css new file mode 100644 index 0000000000..2763afa4cf --- /dev/null +++ b/packages/app-blueprint/files/app/styles/app.css @@ -0,0 +1 @@ +/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ diff --git a/packages/app-blueprint/files/app/templates/_js_application.gjs b/packages/app-blueprint/files/app/templates/_js_application.gjs new file mode 100644 index 0000000000..5d9453c6f0 --- /dev/null +++ b/packages/app-blueprint/files/app/templates/_js_application.gjs @@ -0,0 +1,15 @@ +import pageTitle from 'ember-page-title/helpers/page-title';<% if (welcome) { %> +import WelcomePage from 'ember-welcome-page/components/welcome-page';<% } %> + + diff --git a/packages/app-blueprint/files/app/templates/_ts_application.gts b/packages/app-blueprint/files/app/templates/_ts_application.gts new file mode 100644 index 0000000000..5d9453c6f0 --- /dev/null +++ b/packages/app-blueprint/files/app/templates/_ts_application.gts @@ -0,0 +1,15 @@ +import pageTitle from 'ember-page-title/helpers/page-title';<% if (welcome) { %> +import WelcomePage from 'ember-welcome-page/components/welcome-page';<% } %> + + diff --git a/packages/app-blueprint/files/app/templates/application.hbs b/packages/app-blueprint/files/app/templates/application.hbs new file mode 100644 index 0000000000..cc9e0748f9 --- /dev/null +++ b/packages/app-blueprint/files/app/templates/application.hbs @@ -0,0 +1,10 @@ +{{page-title "<%= namespace %>"}} +<% if (welcome) { %> +{{outlet}} + +{{! The following component displays Ember's default welcome message. }} + +{{! Feel free to remove this! }}<% } else { %> +

Welcome to Ember

+ +{{outlet}}<% } %> \ No newline at end of file diff --git a/packages/app-blueprint/files/config/ember-cli-update.json b/packages/app-blueprint/files/config/ember-cli-update.json new file mode 100644 index 0000000000..fb618a7d1d --- /dev/null +++ b/packages/app-blueprint/files/config/ember-cli-update.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "<%= blueprint %>", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "<%= blueprint %>", + "isBaseBlueprint": true, + "options": [<%= blueprintOptions %>] + } + ] + } + ] +} diff --git a/blueprints/app/files/config/environment.js b/packages/app-blueprint/files/config/environment.js similarity index 72% rename from blueprints/app/files/config/environment.js rename to packages/app-blueprint/files/config/environment.js index 302ce71268..10a7d5bc7f 100644 --- a/blueprints/app/files/config/environment.js +++ b/packages/app-blueprint/files/config/environment.js @@ -1,22 +1,23 @@ -/* jshint node: true */ +'use strict'; -module.exports = function(environment) { - var ENV = { +module.exports = function (environment) { + const ENV = { modulePrefix: '<%= modulePrefix %>', - environment: environment, - baseURL: '/', - locationType: 'auto', + environment, + rootURL: '/', + locationType: 'history', EmberENV: { + EXTEND_PROTOTYPES: false, FEATURES: { // Here you can enable experimental features on an ember canary build - // e.g. 'with-controller': true - } + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, }, APP: { // Here you can pass flags/options to your application instance // when it is created - } + }, }; if (environment === 'development') { @@ -29,7 +30,6 @@ module.exports = function(environment) { if (environment === 'test') { // Testem prefers this... - ENV.baseURL = '/'; ENV.locationType = 'none'; // keep test console output quieter @@ -37,10 +37,11 @@ module.exports = function(environment) { ENV.APP.LOG_VIEW_LOOKUPS = false; ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; } if (environment === 'production') { - + // here you can enable a production-specific feature } return ENV; diff --git a/packages/app-blueprint/files/config/optional-features.json b/packages/app-blueprint/files/config/optional-features.json new file mode 100644 index 0000000000..756228612b --- /dev/null +++ b/packages/app-blueprint/files/config/optional-features.json @@ -0,0 +1,8 @@ +{ + "application-template-wrapper": false, + "default-async-observers": true, + "jquery-integration": false, + "template-only-glimmer-components": true, + "no-implicit-route-model": true, + "use-ember-modules": true +} diff --git a/packages/app-blueprint/files/config/targets.js b/packages/app-blueprint/files/config/targets.js new file mode 100644 index 0000000000..1e48e0599f --- /dev/null +++ b/packages/app-blueprint/files/config/targets.js @@ -0,0 +1,11 @@ +'use strict'; + +const browsers = [ + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions', +]; + +module.exports = { + browsers, +}; diff --git a/packages/app-blueprint/files/ember-cli-build.js b/packages/app-blueprint/files/ember-cli-build.js new file mode 100644 index 0000000000..b6afacfe80 --- /dev/null +++ b/packages/app-blueprint/files/ember-cli-build.js @@ -0,0 +1,36 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +<% if (warpDrive) {%>const { setConfig } = require('@warp-drive/core/build-config'); +<% } %> +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + <% if (typescript) {%>'ember-cli-babel': { enableTypeScriptTransform: true }, + + <% } %>// Add options here + }); +<% if (warpDrive) {%> + setConfig(app, __dirname, { + // this should be the most recent . version for + // which all deprecations have been fully resolved + // and should be updated when that changes + compatWith: '5.8', + deprecations: { + // ... list individual deprecations that have been resolved here + }, + }); +<% } %> + + <% if (embroider) { %>const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + staticAddonTestSupportTrees: true, + staticAddonTrees: true, + staticEmberSource: true, + staticInvokables: true, + skipBabel: [ + { + package: 'qunit', + }, + ], + });<% } else { %>return app.toTree();<% } %> +}; diff --git a/packages/app-blueprint/files/gitignore b/packages/app-blueprint/files/gitignore new file mode 100644 index 0000000000..f0dde6db94 --- /dev/null +++ b/packages/app-blueprint/files/gitignore @@ -0,0 +1,18 @@ +# compiled output +/dist/ +/declarations/ + +# dependencies +/node_modules/ + +# misc +/.env* +/.pnp* +/.eslintcache +/coverage/ +/npm-debug.log* +/testem.log +/yarn-error.log + +# broccoli-debug +/DEBUG/ diff --git a/packages/app-blueprint/files/package.json b/packages/app-blueprint/files/package.json new file mode 100644 index 0000000000..cea6479cf7 --- /dev/null +++ b/packages/app-blueprint/files/package.json @@ -0,0 +1,110 @@ +{ + "name": "<%= name %>", + "version": "0.0.0", + "private": true, + "description": "Small description for <%= name %> goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"<%= packageManager %>:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"<%= packageManager %>:lint:css -- --fix\"", + "lint:fix": "concurrently \"<%= packageManager %>:lint:*:fix\" --names \"fix:\" --prefixColors auto && <%= invokeScriptPrefix %> format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix<% if (typescript) { %>", + "lint:types": "tsc --noEmit<% } %>", + "start": "ember serve", + "test": "concurrently \"<%= packageManager %>:lint\" \"<%= packageManager %>:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0<% if (typescript && emberData) { %>", + "@ember-data/adapter": "~5.8.2", + "@ember-data/graph": "~5.8.2", + "@ember-data/json-api": "~5.8.2", + "@ember-data/legacy-compat": "~5.8.2", + "@ember-data/model": "~5.8.2", + "@ember-data/request": "~5.8.2", + "@ember-data/request-utils": "~5.8.2", + "@ember-data/serializer": "~5.8.2", + "@ember-data/store": "~5.8.2", + "@warp-drive/ember": "~5.8.2<% } %>", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2<% if (embroider) { %>", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10<% } %>", + "@embroider/macros": "^1.20.2<% if (embroider) { %>", + "@embroider/webpack": "^4.1.2<% } %>", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2<% if (typescript) { %>", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.7.7", + "@tsconfig/ember": "^3.0.12", + "@types/qunit": "^2.19.14", + "@types/rsvp": "^4.0.9<% } %><% if (warpDrive) { %>", + "@warp-drive/core": "~5.8.2", + "@warp-drive/json-api": "~5.8.2", + "@warp-drive/legacy": "~5.8.2", + "@warp-drive/utilities": "~5.8.2<% } %>", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~7.2.0-alpha.1", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0<% if (!embroider) { %>", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2<% } %><% if (emberData) { %>", + "ember-data": "~5.8.2<% } %>", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0<% if (welcome) { %>", + "ember-welcome-page": "^8.0.5<% } %>", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6<% if (warpDrive) { %>", + "eslint-plugin-warp-drive": "^5.8.2<% } %>", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1<% if (typescript) { %>", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4<% } %>", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/blueprints/app/files/public/robots.txt b/packages/app-blueprint/files/public/robots.txt similarity index 100% rename from blueprints/app/files/public/robots.txt rename to packages/app-blueprint/files/public/robots.txt diff --git a/packages/app-blueprint/files/testem.js b/packages/app-blueprint/files/testem.js new file mode 100644 index 0000000000..ed2f37124a --- /dev/null +++ b/packages/app-blueprint/files/testem.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = { + test_page: 'tests/index.html?hidepassed', + disable_watching: true, + launch_in_ci: ['Chrome'], + launch_in_dev: ['Chrome'], + browser_start_timeout: 120, + browser_args: { + Chrome: { + ci: [ + // --no-sandbox is needed when running Chrome inside a container + process.env.CI ? '--no-sandbox' : null, + '--headless', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--mute-audio', + '--remote-debugging-port=0', + '--window-size=1440,900', + ].filter(Boolean), + }, + }, +}; diff --git a/packages/app-blueprint/files/tests/helpers/index.ts b/packages/app-blueprint/files/tests/helpers/index.ts new file mode 100644 index 0000000000..e190f567ed --- /dev/null +++ b/packages/app-blueprint/files/tests/helpers/index.ts @@ -0,0 +1,43 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, + setupTest as upstreamSetupTest, + type SetupTestOptions, +} from 'ember-qunit'; + +// This file exists to provide wrappers around ember-qunit's +// test setup functions. This way, you can easily extend the setup that is +// needed per test type. + +function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupApplicationTest(hooks, options); + + // Additional setup for application tests can be done here. + // + // For example, if you need an authenticated session for each + // application test, you could do: + // + // hooks.beforeEach(async function () { + // await authenticateSession(); // ember-simple-auth + // }); + // + // This is also a good place to call test setup functions coming + // from other addons: + // + // setupIntl(hooks, 'en-us'); // ember-intl + // setupMirage(hooks); // ember-cli-mirage +} + +function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupRenderingTest(hooks, options); + + // Additional setup for rendering tests can be done here. +} + +function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupTest(hooks, options); + + // Additional setup for unit tests can be done here. +} + +export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/packages/app-blueprint/files/tests/index.html b/packages/app-blueprint/files/tests/index.html new file mode 100644 index 0000000000..8ba842cb85 --- /dev/null +++ b/packages/app-blueprint/files/tests/index.html @@ -0,0 +1,39 @@ + + + + + <%= namespace %> Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + +
+
+
+
+
+
+ + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/blueprints/app/files/app/templates/components/.gitkeep b/packages/app-blueprint/files/tests/integration/.gitkeep similarity index 100% rename from blueprints/app/files/app/templates/components/.gitkeep rename to packages/app-blueprint/files/tests/integration/.gitkeep diff --git a/packages/app-blueprint/files/tests/test-helper.ts b/packages/app-blueprint/files/tests/test-helper.ts new file mode 100644 index 0000000000..4bc7e42f57 --- /dev/null +++ b/packages/app-blueprint/files/tests/test-helper.ts @@ -0,0 +1,15 @@ +<% if (warpDrive) { %>import '@warp-drive/ember/install'; +<% } %>import Application from '<%= modulePrefix %>/app'; +import config from '<%= modulePrefix %>/config/environment'; +import * as QUnit from 'qunit'; +import { setApplication } from '@ember/test-helpers'; +import { setup } from 'qunit-dom'; +import { loadTests } from 'ember-qunit/test-loader'; +import { start, setupEmberOnerrorValidation } from 'ember-qunit'; + +setApplication(Application.create(config.APP)); + +setup(QUnit.assert); +setupEmberOnerrorValidation(); +loadTests(); +start(); diff --git a/blueprints/app/files/tests/unit/.gitkeep b/packages/app-blueprint/files/tests/unit/.gitkeep similarity index 100% rename from blueprints/app/files/tests/unit/.gitkeep rename to packages/app-blueprint/files/tests/unit/.gitkeep diff --git a/packages/app-blueprint/files/tsconfig.json b/packages/app-blueprint/files/tsconfig.json new file mode 100644 index 0000000000..679912c164 --- /dev/null +++ b/packages/app-blueprint/files/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@tsconfig/ember", + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "compilerOptions": { + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "<%= name %>/tests/*": ["tests/*"], + "<%= name %>/*": ["app/*"], + "*": ["types/*"] + }, + "types": ["ember-source/types"] + } +} diff --git a/packages/app-blueprint/files/types/global.d.ts b/packages/app-blueprint/files/types/global.d.ts new file mode 100644 index 0000000000..2c531e29af --- /dev/null +++ b/packages/app-blueprint/files/types/global.d.ts @@ -0,0 +1 @@ +import '@glint/environment-ember-loose'; diff --git a/packages/app-blueprint/index.js b/packages/app-blueprint/index.js new file mode 100644 index 0000000000..0cf9ff5371 --- /dev/null +++ b/packages/app-blueprint/index.js @@ -0,0 +1,177 @@ +'use strict'; + +const stringUtil = require('ember-cli-string-utils'); +const { default: chalk } = require('chalk'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); +const directoryForPackageName = require('@ember-tooling/blueprint-model/utilities/directory-for-package-name'); +const blueprintVersion = require('./package.json').version; + +module.exports = { + name: '@ember-tooling/classic-build-app-blueprint', + description: 'The default blueprint for ember-cli projects.', + + shouldTransformTypeScript: true, + + filesToRemove: [ + 'app/styles/.gitkeep', + 'app/templates/.gitkeep', + 'app/views/.gitkeep', + 'public/.gitkeep', + 'Brocfile.js', + 'testem.json', + ], + + locals(options) { + let entity = options.entity; + let rawName = entity.name; + let name = stringUtil.dasherize(rawName); + let namespace = stringUtil.classify(rawName); + let embroider = isExperimentEnabled('EMBROIDER') || options.embroider; + + let hasOptions = !options.welcome || options.packageManager || embroider || options.ciProvider; + let blueprintOptions = ''; + if (hasOptions) { + let indent = `\n `; + let outdent = `\n `; + + blueprintOptions = + indent + + [ + !options.welcome && '"--no-welcome"', + options.packageManager === 'yarn' && '"--yarn"', + options.packageManager === 'pnpm' && '"--pnpm"', + embroider && '"--embroider"', + options.ciProvider && `"--ci-provider=${options.ciProvider}"`, + options.typescript && `"--typescript"`, + !options.emberData && `"--no-ember-data"`, + ] + .filter(Boolean) + .join(',\n ') + + outdent; + } + + let invokeScriptPrefix = 'npm run'; + let execBinPrefix = 'npm exec'; + + if (options.packageManager === 'yarn') { + invokeScriptPrefix = 'yarn'; + execBinPrefix = 'yarn'; + } + + if (options.packageManager === 'pnpm') { + invokeScriptPrefix = 'pnpm'; + execBinPrefix = 'pnpm'; + } + + return { + appDirectory: directoryForPackageName(name), + name, + modulePrefix: name, + namespace, + blueprintVersion, + yarn: options.packageManager === 'yarn', + pnpm: options.packageManager === 'pnpm', + npm: options.packageManager !== 'yarn' && options.packageManager !== 'pnpm', + invokeScriptPrefix, + execBinPrefix, + welcome: options.welcome, + blueprint: '@ember-tooling/classic-build-app-blueprint', + blueprintOptions, + embroider, + lang: options.lang, + emberData: options.emberData, + warpDrive: options.warpDrive ?? options.emberData ?? false, + ciProvider: options.ciProvider, + typescript: options.typescript, + strict: options.strict, + packageManager: options.packageManager ?? 'npm', + }; + }, + + files(options) { + if (this._files) { + return this._files; + } + + let files = this._super(); + + if (options.ciProvider !== 'github') { + files = files.filter((file) => file.indexOf('.github') < 0); + } + + if (!options.typescript) { + files = files.filter( + (file) => !['tsconfig.json', 'app/config/', 'types/'].includes(file) && !file.endsWith('.d.ts') + ); + } + + if (!options.emberData) { + files = files.filter((file) => !file.includes('models/')); + files = files.filter((file) => !file.includes('ember-data/')); + } + + if (!options.warpDrive && !options.emberData) { + files = files.filter((file) => !file.includes('services/store.ts')); + } + + if (options.strict) { + files = files.filter((file) => !file.endsWith('.hbs')); + } else { + files = files.filter((file) => !file.endsWith('.gjs') && !file.endsWith('.gts')); + } + + this._files = files; + + return this._files; + }, + + beforeInstall() { + const prependEmoji = require('@ember-tooling/blueprint-model/utilities/prepend-emoji'); + this.ui.writeLine(chalk.blue(`@ember-tooling/classic-build-app-blueprint v${blueprintVersion}`)); + this.ui.writeLine(''); + this.ui.writeLine(prependEmoji('✨', `Creating a new Ember app in ${chalk.yellow(process.cwd())}:`)); + }, + + /** + * @override + * + * This modification of buildFileInfo allows our differing + * input files to output to a single file, depending on the options. + * For example: + * + * for javascript, + * _ts_eslint.config.mjs is deleted + * _js_eslint.config.mjs is renamed to eslint.config.mjs + * + * for typescript, + * _js_eslint.config.mjs is deleted + * _ts_eslint.config.mjs is renamed to eslint.config.mjs + */ + buildFileInfo(intoDir, templateVariables, file, options) { + let fileInfo = this._super.buildFileInfo.apply(this, arguments); + + if (file.includes('_js_')) { + if (options.typescript) { + return null; + } + + fileInfo.outputBasePath = fileInfo.outputPath.replace('_js_', ''); + fileInfo.outputPath = fileInfo.outputPath.replace('_js_', ''); + fileInfo.displayPath = fileInfo.outputPath.replace('_js_', ''); + return fileInfo; + } + + if (file.includes('_ts_')) { + if (!options.typescript) { + return null; + } + + fileInfo.outputBasePath = fileInfo.outputPath.replace('_ts_', ''); + fileInfo.outputPath = fileInfo.outputPath.replace('_ts_', ''); + fileInfo.displayPath = fileInfo.outputPath.replace('_ts_', ''); + return fileInfo; + } + + return fileInfo; + }, +}; diff --git a/packages/app-blueprint/package.json b/packages/app-blueprint/package.json new file mode 100644 index 0000000000..bfd896fff7 --- /dev/null +++ b/packages/app-blueprint/package.json @@ -0,0 +1,26 @@ +{ + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "7.2.0-alpha.1", + "repository": { + "type": "git", + "url": "https://github.com/ember-cli/ember-cli.git", + "directory": "packages/app-blueprint" + }, + "license": "MIT", + "keywords": [ + "ember-blueprint" + ], + "dependencies": { + "@ember-tooling/blueprint-model": "workspace:*", + "chalk": "^5.6.2", + "ember-cli-string-utils": "^1.1.0" + }, + "release-plan": { + "semverIncrementAs": { + "minor": "prerelease", + "patch": "prerelease" + }, + "semverIncrementTag": "alpha", + "publishTag": "alpha" + } +} diff --git a/blueprints/app/files/vendor/.gitkeep b/packages/blueprint-blueprint/files/blueprints/__name__/files/.gitkeep similarity index 100% rename from blueprints/app/files/vendor/.gitkeep rename to packages/blueprint-blueprint/files/blueprints/__name__/files/.gitkeep diff --git a/blueprints/blueprint/files/blueprints/__name__/index.js b/packages/blueprint-blueprint/files/blueprints/__name__/index.js similarity index 74% rename from blueprints/blueprint/files/blueprints/__name__/index.js rename to packages/blueprint-blueprint/files/blueprints/__name__/index.js index 41a516d29c..ed5e510def 100644 --- a/blueprints/blueprint/files/blueprints/__name__/index.js +++ b/packages/blueprint-blueprint/files/blueprints/__name__/index.js @@ -1,14 +1,16 @@ +'use strict'; + module.exports = { description: '' - // locals: function(options) { + // locals(options) { // // Return custom template variables here. // return { // foo: options.entity.options.foo // }; // } - // afterInstall: function(options) { + // afterInstall(options) { // // Perform extra work here. // } }; diff --git a/packages/blueprint-blueprint/index.js b/packages/blueprint-blueprint/index.js new file mode 100644 index 0000000000..064388d101 --- /dev/null +++ b/packages/blueprint-blueprint/index.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + description: 'Generates a blueprint and definition.', +}; diff --git a/packages/blueprint-blueprint/package.json b/packages/blueprint-blueprint/package.json new file mode 100644 index 0000000000..dfb9081d50 --- /dev/null +++ b/packages/blueprint-blueprint/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ember-tooling/blueprint-blueprint", + "version": "0.3.0", + "repository": { + "type": "git", + "url": "https://github.com/ember-cli/ember-cli.git", + "directory": "packages/blueprint-blueprint" + }, + "license": "MIT", + "keywords": [ + "ember-blueprint" + ] +} diff --git a/packages/blueprint-model/index.js b/packages/blueprint-model/index.js new file mode 100644 index 0000000000..26f09804e2 --- /dev/null +++ b/packages/blueprint-model/index.js @@ -0,0 +1,1807 @@ +'use strict'; + +/** +@module ember-cli +*/ +const FileInfo = require('@ember-tooling/blueprint-model/utilities/file-info'); +const { default: chalk } = require('chalk'); +const MarkdownColor = require('@ember-tooling/blueprint-model/utilities/markdown-color'); +const sequence = require('./utilities/sequence'); +const printCommand = require('@ember-tooling/blueprint-model/utilities/print-command'); +const insertIntoFile = require('@ember-tooling/blueprint-model/utilities/insert-into-file'); +const cleanRemove = require('@ember-tooling/blueprint-model/utilities/clean-remove'); +const fs = require('fs-extra'); +const { pluralize } = require('inflection'); +const { minimatch } = require('minimatch'); +const path = require('path'); +const stringUtils = require('ember-cli-string-utils'); +const merge = require('lodash/merge'); +const zipObject = require('lodash/zipObject'); +const intersection = require('lodash/intersection'); +const cloneDeep = require('lodash/cloneDeep'); +const compact = require('lodash/compact'); +const uniq = require('lodash/uniq'); +const sortBy = require('lodash/sortBy'); +const walkSync = require('walk-sync'); +const SilentError = require('silent-error'); +const CoreObject = require('core-object'); +const EOL = require('os').EOL; +const logger = require('heimdalljs-logger')('ember-cli:blueprint'); +const normalizeEntityName = require('ember-cli-normalize-entity-name'); +const isAddon = require('@ember-tooling/blueprint-model/utilities/is-addon'); + +const initialIgnoredFiles = ['.DS_Store']; + +/** + A blueprint is a bundle of template files with optional install + logic. + + Blueprints follow a simple structure. Let's take the built-in + `controller` blueprint as an example: + + ``` + blueprints/controller + ├── files + │ ├── app + │ │ └── __path__ + │ │ └── __name__.js + └── index.js + + blueprints/controller-test + ├── files + │ └── tests + │ └── unit + │ └── controllers + │ └── __test__.js + └── index.js + ``` + + ## Files + + `files` contains templates for the all the files to be + installed into the target directory. + + The `__name__` token is subtituted with the dasherized + entity name at install time. For example, when the user + invokes `ember generate controller foo` then `__name__` becomes + `foo`. When the `--pod` flag is used, for example `ember + generate controller foo --pod` then `__name__` becomes + `controller`. + + The `__path__` token is substituted with the blueprint + name at install time. For example, when the user invokes + `ember generate controller foo` then `__path__` becomes + `controller`. When the `--pod` flag is used, for example + `ember generate controller foo --pod` then `__path__` + becomes `foo` (or `/foo` if the + podModulePrefix is defined). This token is primarily for + pod support, and is only necessary if the blueprint can be + used in pod structure. If the blueprint does not require pod + support, simply use the blueprint name instead of the + `__path__` token. + + The `__test__` token is substituted with the dasherized + entity name and appended with `-test` at install time. + This token is primarily for pod support and only necessary + if the blueprint requires support for a pod structure. If + the blueprint does not require pod support, simply use the + `__name__` token instead. + + ## Template Variables (AKA Locals) + + Variables can be inserted into templates with + `<%= someVariableName %>`. + + For example, the built-in `util` blueprint + `files/app/utils/__name__.js` looks like this: + + ```js + export default function <%= camelizedModuleName %>() { + return true; + } + ``` + + `<%= camelizedModuleName %>` is replaced with the real + value at install time. + + The following template variables are provided by default: + + - `dasherizedPackageName` + - `classifiedPackageName` + - `dasherizedModuleName` + - `classifiedModuleName` + - `camelizedModuleName` + + `packageName` is the project name as found in the project's + `package.json`. + + `moduleName` is the name of the entity being generated. + + The mechanism for providing custom template variables is + described below. + + ## Index.js + + Custom installation and uninstallation behavior can be added + by overriding the hooks documented below. `index.js` should + export a plain object, which will extend the prototype of the + `Blueprint` class. If needed, the original `Blueprint` prototype + can be accessed through the `_super` property. + + ```js + module.exports = { + locals(options) { + // Return custom template variables here. + return {}; + }, + + normalizeEntityName(entityName) { + // Normalize and validate entity name here. + return entityName; + }, + + fileMapTokens(options) { + // Return custom tokens to be replaced in your files + return { + __token__(options){ + // logic to determine value goes here + return 'value'; + } + } + }, + + filesPath(options) { + return path.join(this.path, 'files'); + }, + + beforeInstall(options) {}, + afterInstall(options) {}, + beforeUninstall(options) {}, + afterUninstall(options) {} + + }; + ``` + + ## Blueprint Hooks + + ### beforeInstall & beforeUninstall + + Called before any of the template files are processed and receives + the `options` and `locals` hashes as parameters. Typically used for + validating any additional command line options or for any asynchronous + setup that is needed. As an example, the `controller` blueprint validates + its `--type` option in this hook. If you need to run any asynchronous code, + wrap it in a promise and return that promise from these hooks. This will + ensure that your code is executed correctly. + + ### afterInstall & afterUninstall + + The `afterInstall` and `afterUninstall` hooks receives the same + arguments as `locals`. Use it to perform any custom work after the + files are processed. For example, the built-in `route` blueprint + uses these hooks to add and remove relevant route declarations in + `app/router.js`. + + ### Overriding Install + + If you don't want your blueprint to install the contents of + `files` you can override the `install` method. It receives the + same `options` object described above and must return a promise. + See the built-in `resource` blueprint for an example of this. + + @class Blueprint + @constructor + @extends CoreObject + @param {String} [blueprintPath] + @param {Object} [blueprintOptions] +*/ +let Blueprint = CoreObject.extend({ + availableOptions: [], + anonymousOptions: ['name'], + + _printableProperties: ['name', 'description', 'availableOptions', 'anonymousOptions', 'overridden'], + + /** + Indicates whether or not a blueprint is a candidate for automatic transpilation from TS to JS. + This property could be false in the case that the blueprint is written in JS and is not intended + to work with TS at all, OR in the case that the blueprint is written in TS and the author does + not intend to support transpilation to JS. + + @public + @property shouldTransformTypeScript + @type Boolean + */ + shouldTransformTypeScript: false, + + init(blueprintPath, blueprintOptions) { + this._super(); + + this.path = blueprintPath; + this.name = blueprintOptions?.name ?? path.basename(blueprintPath); + + this._processOptions(blueprintOptions); + }, + + /** + Process the options object coming from either + the `init`, `install` or `uninstall` hook. + + @private + @method _processOptions + @param {Object} options + */ + _processOptions(options = {}) { + this.options = options; + this.dryRun = options.dryRun; + this.pod = options.pod; + this.project = options.project; + this.ui = options.ui; + }, + + /** + Hook to specify the path to the blueprint's files. By default this is + `path.join(this.path, 'files)`. + + This can be used to customize which set of files to install based on options + or environmental variables. It defaults to the `files` directory within the + blueprint's folder. + + @public + @method filesPath + @param {Object} options + @return {String} Path to the blueprints files directory. + */ + filesPath(/* options */) { + return path.join(this.path, 'files'); + }, + + /** + Used to retrieve files for blueprint. + + @public + @method files + @param {Object} options + @return {Array} Contents of the blueprint's files directory + */ + files(/* options */) { + if (this._files) { + return this._files; + } + + let filesPath = this.filesPath(this.options); + if (Blueprint._existsSync(filesPath)) { + this._files = walkSync(filesPath); + } else { + this._files = []; + } + + return this._files; + }, + + /** + @method srcPath + @param {String} file + @return {String} Resolved path to the file + */ + srcPath(file) { + return path.resolve(this.filesPath(this.options), file); + }, + + /** + Hook for normalizing entity name + + Use the `normalizeEntityName` hook to add custom normalization and + validation of the provided entity name. The default hook does not + make any changes to the entity name, but makes sure an entity name + is present and that it doesn't have a trailing slash. + + This hook receives the entity name as its first argument. The string + returned by this hook will be used as the new entity name. + + @public + @method normalizeEntityName + @param {String} entityName + @return {null} + */ + normalizeEntityName(entityName) { + return normalizeEntityName(entityName); + }, + + /** + Write a status and message to the UI + @private + @method _writeStatusToUI + @param {Function} chalkColor + @param {String} keyword + @param {String} message + */ + _writeStatusToUI(chalkColor, keyword, message) { + if (this.ui) { + this.ui.writeLine(` ${chalkColor(keyword)} ${message}`); + } + }, + + /** + @private + @method _writeFile + @param {Object} info + @return {Promise} + */ + async _writeFile(info) { + if (!this.dryRun) { + return fs.outputFile(info.outputPath, await info.render()); + } + }, + + /** + Actions lookup + @private + @property _actions + @type Object + */ + _actions: { + write(info) { + this._writeStatusToUI(chalk.green, 'create', info.displayPath); + return this._writeFile(info); + }, + skip(info) { + let label = 'skip'; + + if (info.resolution === 'identical') { + label = 'identical'; + } + + this._writeStatusToUI(chalk.yellow, label, info.displayPath); + }, + + overwrite(info) { + this._writeStatusToUI(chalk.yellow, 'overwrite', info.displayPath); + return this._writeFile(info); + }, + + edit(info) { + this._writeStatusToUI(chalk.green, 'edited', info.displayPath); + }, + + remove(info) { + this._writeStatusToUI(chalk.red, 'remove', info.displayPath); + if (!this.dryRun) { + return cleanRemove(info); + } + }, + }, + + /** + Calls an action. + @private + @method _commit + @param {Object} result + @return {Promise} + @throws {Error} Action doesn't exist. + */ + _commit(result) { + let action = this._actions[result.action]; + + if (action) { + return action.call(this, result); + } else { + throw new Error(`Tried to call action "${result.action}" but it does not exist`); + } + }, + + /** + Prints warning for pod unsupported. + @private + @method _checkForPod + */ + _checkForPod(verbose) { + if (!this.hasPathToken && this.pod && verbose) { + this.ui.writeLine( + chalk.yellow( + 'You specified the pod flag, but this' + + ' blueprint does not support pod structure. It will be generated with' + + ' the default structure.' + ) + ); + } + }, + + /** + @private + @method _normalizeEntityName + @param {Object} entity + */ + _normalizeEntityName(entity) { + if (entity) { + entity.name = this.normalizeEntityName(entity.name); + } + }, + + /** + @private + @method _checkInRepoAddonExists + @param {Object} options + */ + _checkInRepoAddonExists(options) { + let addon; + + if (options.inRepoAddon) { + addon = findAddonByName(this.project, options.inRepoAddon); + + if (!addon) { + throw new SilentError( + `You specified the 'in-repo-addon' flag, but the ` + + `in-repo-addon '${options.inRepoAddon}' does not exist. Please check the name and try again.` + ); + } + } + + if (options.in) { + if (!ensureTargetDirIsAddon(options.in)) { + throw new SilentError( + `You specified the 'in' flag, but the ` + + `in repo addon '${options.in}' does not exist. Please check the name and try again.` + ); + } + } + }, + + /** + @private + @method _process + @param {Object} options + @param {Function} beforeHook + @param {Function} process + @param {Function} afterHook + */ + async _process(options, beforeHook, process, afterHook) { + let intoDir = options.target; + + let locals = await this._locals(options); + + // run beforeInstall/beforeUninstall userland hooks + await beforeHook.call(this, options, locals); + + // gather fileInfos to be processed + let fileInfos = await process.call(this, intoDir, locals, options); + + // commit changes for each FileInfo (with prompting as needed) + await Promise.all(fileInfos.map((info) => this._commit(info))); + + // run afterInstall/afterUninstall userland hooks + await afterHook.call(this, options); + }, + + /** + @private + @method shouldConvertToJS + @param {Object} options + @param {FileInfo} fileInfo + @return {Boolean} + */ + shouldConvertToJS(options, fileInfo) { + // If this isn't turned on, it doesn't matter what else was passed, we're not touching it. + if (!this.shouldTransformTypeScript) { + return false; + } + + // If the blueprint isn't a TS file to begin with, there's nothing to convert. + if (!isTypeScriptFile(fileInfo.outputPath)) { + return false; + } + + // Indicates when the user explicitly passed either `--typescript` or `--no-typescript` as opposed + // to not passing a flag at all and allowing for default behavior + const userExplicitlySelectedTypeScriptStatus = options.typescript !== undefined; + + // Indicates when the user has asked for TypeScript either globally (by setting + // `isTypeScriptProject` to true) or locally (by passing the `--typescript` flag when they + // invoked the generator). Although ember-cli merges `.ember-cli` and all of the flag values into + // one object, we thought the DX would be improved by differentiating between what is intended + // to be global vs. local config. + const shouldUseTypeScript = userExplicitlySelectedTypeScriptStatus + ? options.typescript + : options.isTypeScriptProject; + + // if the user wants TS output and we have a TS file available, we do *not* want to downlevel to JS + if (shouldUseTypeScript) { + return false; + } + + return true; + }, + + /** + @private + @method convertToJS + @param {FileInfo} fileInfo + @return {Promise} + */ + async convertToJS(fileInfo) { + let rendered = await fileInfo.render(); + + fileInfo.rendered = await this.removeTypes(path.extname(fileInfo.displayPath), rendered); + fileInfo.displayPath = replaceTypeScriptExtension(fileInfo.displayPath); + fileInfo.outputPath = replaceTypeScriptExtension(fileInfo.outputPath); + + return fileInfo; + }, + + /** + @private + @method removeTypes + @param {string} extension + @param {string} code + @return {Promise} + */ + async removeTypes(extension, code) { + const { removeTypes } = require('babel-remove-types'); + + if (extension === '.gts') { + const { Preprocessor } = require('content-tag'); + const preprocessor = new Preprocessor(); + // Strip template tags + const replacementClassMember = (i) => `template = __TEMPLATE_TAG_${i}__;`; + const replacementExpression = (i) => `__TEMPLATE_TAG_${i}__`; + const templateTagMatches = preprocessor.parse(code); + let strippedCode = code; + for (let i = 0; i < templateTagMatches.length; i++) { + const match = templateTagMatches[i]; + const templateTag = substringBytes(code, match.range.startByte, match.range.endByte); + if (match.type === 'class-member') { + strippedCode = strippedCode.replace(templateTag, replacementClassMember(i)); + } else { + strippedCode = strippedCode.replace(templateTag, replacementExpression(i)); + } + } + + // Remove types + const transformed = await removeTypes(strippedCode); + + // Readd stripped template tags + let transformedWithTemplateTag = transformed; + for (let i = 0; i < templateTagMatches.length; i++) { + const match = templateTagMatches[i]; + const templateTag = substringBytes(code, match.range.startByte, match.range.endByte); + if (match.type === 'class-member') { + transformedWithTemplateTag = transformedWithTemplateTag.replace(replacementClassMember(i), templateTag); + } else { + // babel-remove-types uses prettier under the hood, and adds trailing `;` where allowed, + // so we need to take that into account when restoring the template tags: + transformedWithTemplateTag = transformedWithTemplateTag.replace(`${replacementExpression(i)};`, templateTag); + transformedWithTemplateTag = transformedWithTemplateTag.replace(replacementExpression(i), templateTag); + } + } + + return transformedWithTemplateTag; + } + + return await removeTypes(code); + }, + + /** + @method install + @param {Object} options + @return {Promise} + */ + install(options) { + this._processOptions(options); + + this.hasPathToken = hasPathToken(this.files(this.options)); + + this.ui.writeLine(`installing ${this.name}`); + + if (this.dryRun) { + this.ui.writeLine(chalk.yellow('You specified the `dry-run` flag, so no changes will be written.')); + } + + this._normalizeEntityName(options.entity); + this._checkForPod(options.verbose); + this._checkInRepoAddonExists(options); + + logger.info('START: processing blueprint: `%s`', this.name); + let start = new Date(); + return this._process(options, this.beforeInstall, this.processFiles, this.afterInstall).finally(() => + logger.info('END: processing blueprint: `%s` in (%dms)', this.name, new Date() - start) + ); + }, + + /** + @method uninstall + @param {Object} options + @return {Promise} + */ + uninstall(options) { + this._processOptions(options); + + this.hasPathToken = hasPathToken(this.files(this.options)); + + this.ui.writeLine(`uninstalling ${this.name}`); + + if (this.dryRun) { + this.ui.writeLine(chalk.yellow('You specified the `dry-run` flag, so no files will be deleted.')); + } + + this._normalizeEntityName(options.entity); + this._checkForPod(options.verbose); + + return this._process(options, this.beforeUninstall, this.processFilesForUninstall, this.afterUninstall); + }, + + /** + Hook for running operations before install. + @method beforeInstall + @return {Promise|null} + */ + beforeInstall() {}, + + /** + Hook for running operations after install. + @method afterInstall + @return {Promise|null} + */ + afterInstall() {}, + + /** + Hook for running operations before uninstall. + @method beforeUninstall + @return {Promise|null} + */ + beforeUninstall() {}, + + /** + Hook for running operations after uninstall. + @method afterUninstall + @return {Promise|null} + */ + afterUninstall() {}, + + filesToRemove: [], + + /** + Hook for adding custom template variables. + + When the following is called on the command line: + + ```sh + ember generate controller foo --type=array --dry-run isAdmin:true + ``` + + The object passed to `locals` looks like this: + + ```js + { + entity: { + name: 'foo', + options: { + isAdmin: true + } + }, + dryRun: true + type: "array" + // more keys + } + ``` + + This hook must return an object or a Promise which resolves to an object. + The resolved object will be merged with the aforementioned default locals. + + @public + @method locals + @param {Object} options General and entity-specific options + @return {Object|Promise|null} + */ + locals(/* options */) {}, + + /** + Hook to add additional or override existing fileMap tokens. + + Use `fileMapTokens` to add custom fileMap tokens for use + in the `mapFile` method. The hook must return an object in the + following pattern: + + ```js + { + __token__(options){ + // logic to determine value goes here + return 'value'; + } + } + ``` + + It will be merged with the default `fileMapTokens`, and can be used + to override any of the default tokens. + + Tokens are used in the files folder (see `files`), and get replaced with + values when the `mapFile` method is called. + + @public + @method fileMapTokens + @return {Object|null} + */ + fileMapTokens() {}, + + /** + @private + @method _fileMapTokens + @param {Object} options + @return {Object} + */ + _fileMapTokens(options) { + let { project } = this; + let standardTokens = { + __name__(options) { + if (options.pod && options.hasPathToken) { + return options.blueprintName; + } + return options.dasherizedModuleName; + }, + __path__(options) { + let blueprintName = options.blueprintName; + + if (/-test/.test(blueprintName)) { + blueprintName = options.blueprintName.slice(0, options.blueprintName.indexOf('-test')); + } + if (options.pod && options.hasPathToken) { + return path.join(options.podPath, options.dasherizedModuleName); + } + return pluralize(blueprintName); + }, + __root__(options) { + if (options.inRepoAddon) { + let addon = findAddonByName(project, options.inRepoAddon); + let relativeAddonPath = path.relative(project.root, addon.root); + return path.join(relativeAddonPath, 'addon'); + } + if (options.in) { + let relativeAddonPath = path.relative(project.root, options.in); + return path.join(relativeAddonPath, 'addon'); + } + if (options.inDummy) { + return path.join('tests', 'dummy', 'app'); + } + if (options.inAddon) { + return 'addon'; + } + return 'app'; + }, + __test__(options) { + if (options.pod && options.hasPathToken) { + return options.blueprintName; + } + return `${options.dasherizedModuleName}-test`; + }, + }; + + let customTokens = this.fileMapTokens(options) || options.fileMapTokens || {}; + return merge(standardTokens, customTokens); + }, + + /** + Used to generate fileMap tokens for mapFile. + + @method generateFileMap + @param {Object} fileMapVariables + @return {Object} + */ + generateFileMap(fileMapVariables) { + let tokens = this._fileMapTokens(fileMapVariables); + let fileMapValues = Object.values(tokens); + let tokenValues = fileMapValues.map((token) => token(fileMapVariables)); + let tokenKeys = Object.keys(tokens); + return zipObject(tokenKeys, tokenValues); + }, + + /** + @method buildFileInfo + @param {Function} destPath + @param {Object} templateVariables + @param {String} file + @param {Object} options may be used when buildFileInfo is customized in a blueprint + @return {FileInfo | null} + */ + buildFileInfo(intoDir, templateVariables, file /*, options */) { + let mappedPath = this.mapFile(file, templateVariables); + + return new FileInfo({ + action: 'write', + outputBasePath: path.normalize(intoDir), + outputPath: path.join(intoDir, mappedPath), + displayPath: path.normalize(mappedPath), + inputPath: this.srcPath(file), + templateVariables, + ui: this.ui, + }); + }, + + /** + @method isUpdate + @return {Boolean} + */ + isUpdate() { + if (this.project && this.project.isEmberCLIProject) { + return this.project.isEmberCLIProject(); + } + }, + + /** + @private + @method _getFileInfos + @param {Array} files + @param {String} intoDir + @param {Object} templateVariables + @param {Object} options + @return {Array} file infos + */ + _getFileInfos(files, intoDir, templateVariables, options) { + return files + .map((file) => this.buildFileInfo.call(this, intoDir, templateVariables, file, options)) + .filter(Boolean); + }, + + /** + Add update files to ignored files or reset them + @private + @method _ignoreUpdateFiles + */ + _ignoreUpdateFiles() { + if (this.isUpdate()) { + Blueprint.ignoredFiles = Blueprint.ignoredFiles.concat(Blueprint.ignoredUpdateFiles); + } else { + Blueprint.ignoredFiles = initialIgnoredFiles; + } + }, + + /** + @private + @method _getFilesForInstall + @param {Array} targetFiles + @return {Array} files + */ + _getFilesForInstall(targetFiles) { + let files = this.files(this.options); + + // if we've defined targetFiles, get file info on ones that match + return (targetFiles && targetFiles.length > 0 && intersection(files, targetFiles)) || files; + }, + + /** + @private + @method _checkForNoMatch + @param {Array} fileInfos + @param {String} rawArgs + */ + _checkForNoMatch(fileInfos, rawArgs) { + if (fileInfos.filter(isFilePath).length < 1 && rawArgs) { + this.ui.writeLine( + chalk.yellow(`The globPattern "${rawArgs}" ` + `did not match any files, so no file updates will be made.`) + ); + } + }, + + /** + @method processFiles + @param {String} intoDir + @param {Object} templateVariables + @param {Object} options + @return {Promise} + */ + processFiles(intoDir, templateVariables, options) { + let files = this._getFilesForInstall(templateVariables.targetFiles); + let fileInfos = this._getFileInfos(files, intoDir, templateVariables, options); + this._checkForNoMatch(fileInfos, templateVariables.rawArgs); + + this._ignoreUpdateFiles(); + + let fileInfosToRemove = this._getFileInfos(this.filesToRemove, intoDir, templateVariables); + + fileInfosToRemove = finishProcessingForUninstall(fileInfosToRemove); + + return Promise.all(fileInfos.filter(isValidFile).map(prepareConfirm)) + .then(finishProcessingForInstall) + .then((fileInfos) => { + return Promise.all( + fileInfos.map((info) => { + if (this.shouldConvertToJS(this.options, info)) { + return this.convertToJS(info); + } + + return info; + }) + ); + }) + .then((fileInfos) => fileInfos.concat(fileInfosToRemove)); + }, + + /** + @method processFilesForUninstall + @param {String} intoDir + @param {Object} templateVariables + */ + processFilesForUninstall(intoDir, templateVariables) { + let fileInfos = this._getFileInfos(this.files(this.options), intoDir, templateVariables); + + this._ignoreUpdateFiles(); + + fileInfos = fileInfos.filter(isValidFile).reduce((acc, info) => { + // if it's possible that this blueprint could have produced either typescript OR javascript, we have to do some + // work to figure out which files to delete. + if (this.shouldTransformTypeScript) { + if (this.options.typescript === true) { + // if the user explicitly passed `--typescript`, we only want to delete TS files, so we stick with the existing + // info object since it will contain a .ts outputPath (since we know this blueprint is authored in TS because + // of our check above) + acc.push(info); + return acc; + } + + const jsInfo = new FileInfo({ + ...info, + outputPath: replaceTypeScriptExtension(info.outputPath), + displayPath: replaceTypeScriptExtension(info.displayPath), + }); + + if (this.options.typescript === false) { + // if the user explicitly passed `--no-typescript`, we only want to delete JS file, so we return our newly + // created jsInfo object since it contains the javascript version of the output path. + acc.push(jsInfo); + return acc; + } + + if (this.options.typescript === undefined) { + // if the user didn't specify one way or the other, then both the JS and TS paths are possibilities, so we add + // both of them to the list. `finishProcessingForUninstall` will actually look to see which of them exists and + // delete whatever it finds. + acc.push(info, jsInfo); + return acc; + } + } + + acc.push(info); + return acc; + }, []); + + return finishProcessingForUninstall(fileInfos); + }, + + /** + @method mapFile + @param {String} file + @param locals + @return {String} + */ + mapFile(file, locals) { + let pattern, i; + let fileMap = locals.fileMap || { __name__: locals.dasherizedModuleName }; + file = Blueprint.renamedFiles[file] || file; + for (i in fileMap) { + pattern = new RegExp(i, 'g'); + file = file.replace(pattern, fileMap[i]); + } + return file; + }, + + /** + Looks for a __root__ token in the files folder. Must be present for + the blueprint to support addon tokens. The `server`, `blueprints`, and `test` + + @private + @method supportsAddon + @return {Boolean} + */ + supportsAddon() { + return /__root__/.test(this.files(this.options).join()); + }, + + /** + @private + @method _generateFileMapVariables + @param {String} moduleName + @param locals + @param {Object} options + @return {Object} + */ + _generateFileMapVariables(moduleName, locals, options) { + let originBlueprintName = options.originBlueprintName || this.name; + let podModulePrefix = this.project.config().podModulePrefix || ''; + let podPath = podModulePrefix.substr(podModulePrefix.lastIndexOf('/') + 1); + let inAddon = this.project.isEmberCLIAddon() || !!options.inRepoAddon; + let inDummy = this.project.isEmberCLIAddon() ? options.dummy : false; + + return { + pod: this.pod, + podPath, + hasPathToken: this.hasPathToken, + inAddon, + inRepoAddon: options.inRepoAddon, + in: options.in, + inDummy, + blueprintName: this.name, + originBlueprintName, + dasherizedModuleName: stringUtils.dasherize(moduleName), + locals, + }; + }, + + /** + @private + @method _locals + @param {Object} options + @return {Object} + */ + _locals(options) { + let packageName = options.project.name(); + let moduleName = (options.entity && options.entity.name) || packageName; + let sanitizedModuleName = moduleName.replace(/\//g, '-'); + + return new Promise((resolve) => { + resolve(this.locals(options)); + }).then((customLocals) => { + let fileMapVariables = this._generateFileMapVariables(moduleName, customLocals, options); + let fileMap = this.generateFileMap(fileMapVariables); + let standardLocals = { + dasherizedPackageName: stringUtils.dasherize(packageName), + classifiedPackageName: stringUtils.classify(packageName), + dasherizedModuleName: stringUtils.dasherize(moduleName), + classifiedModuleName: stringUtils.classify(sanitizedModuleName), + camelizedModuleName: stringUtils.camelize(sanitizedModuleName), + decamelizedModuleName: stringUtils.decamelize(sanitizedModuleName), + fileMap, + hasPathToken: this.hasPathToken, + targetFiles: options.targetFiles, + rawArgs: options.rawArgs, + }; + + return merge({}, standardLocals, customLocals); + }); + }, + + /** + Used to add a package to the project's `package.json`. + + Generally, this would be done from the `afterInstall` hook, to + ensure that a package that is required by a given blueprint is + available. + + @method addPackageToProject + @param {String} packageName + @param {String} target + @return {Promise} + */ + addPackageToProject(packageName, target) { + let packageObject = { name: packageName }; + + if (target) { + packageObject.target = target; + } + + return this.addPackagesToProject([packageObject]); + }, + + /** + Used to add multiple packages to the project's `package.json`. + + Generally, this would be done from the `afterInstall` hook, to + ensure that a package that is required by a given blueprint is + available. + + Expects each array item to be an object with a `name`. Each object + may optionally have a `target` to specify a specific version. + + @method addPackagesToProject + @param {Array} packages + @return {Promise} + + @example + ```js + this.addPackagesToProject([ + { name: 'lodash' }, + { name: 'moment', target: '^2.17.0' }, + ]); + ``` + */ + addPackagesToProject(packages) { + let task = this.taskFor('npm-install'); + let installText = packages.length > 1 ? 'install packages' : 'install package'; + let packageNames = []; + let packageArray = []; + + for (let i = 0; i < packages.length; i++) { + packageNames.push(packages[i].name); + + let packageNameAndVersion = packages[i].name; + + if (packages[i].target) { + packageNameAndVersion += `@${packages[i].target}`; + } + + packageArray.push(packageNameAndVersion); + } + + this._writeStatusToUI(chalk.green, installText, packageNames.join(', ')); + + return task.run({ + 'save-dev': true, + verbose: false, + packages: packageArray, + }); + }, + + /** + Used to remove a package from the project's `package.json`. + + Generally, this would be done from the `afterInstall` hook, to + ensure that any package conflicts can be resolved before the + addon is used. + + @method removePackageFromProject + @param {String} packageName + @return {Promise} + */ + removePackageFromProject(packageName) { + let packageObject = { name: packageName }; + + return this.removePackagesFromProject([packageObject]); + }, + + /** + Used to remove multiple packages from the project's `package.json`. + + Generally, this would be done from the `afterInstall` hook, to + ensure that any package conflicts can be resolved before the + addon is used. + + Expects each array item to be an object with a `name` property. + + @method removePackagesFromProject + @param {Array} packages + @return {Promise} + */ + removePackagesFromProject(packages) { + let task = this.taskFor('npm-uninstall'); + let installText = packages.length > 1 ? 'uninstall packages' : 'uninstall package'; + let packageNames = []; + + let projectDependencies = this.project.dependencies(); + + for (let i = 0; i < packages.length; i++) { + let packageName = packages[i].name; + if (packageName in projectDependencies) { + packageNames.push(packageName); + } + } + + if (packageNames.length === 0) { + this._writeStatusToUI(chalk.yellow, 'remove', 'Skipping uninstall because no matching package is installed.'); + return Promise.resolve(); + } + + this._writeStatusToUI(chalk.green, installText, packageNames.join(', ')); + + return task.run({ + 'save-dev': true, + verbose: false, + packages: packageNames, + }); + }, + + /** + Used to add an addon to the project's `package.json` and run it's + `defaultBlueprint` if it provides one. + + Generally, this would be done from the `afterInstall` hook, to + ensure that a package that is required by a given blueprint is + available. + + @method addAddonToProject + @param {Object} options + @return {Promise} + */ + addAddonToProject(options) { + return this.addAddonsToProject({ + packages: [options], + extraArgs: options.extraArgs || {}, + blueprintOptions: options.blueprintOptions || {}, + }); + }, + + /** + Used to add multiple addons to the project's `package.json` and run their + `defaultBlueprint` if they provide one. + + Generally, this would be done from the `afterInstall` hook, to + ensure that a package that is required by a given blueprint is + available. + + @method addAddonsToProject + @param {Object} options + @return {Promise} + */ + addAddonsToProject(options) { + let taskOptions = { + packages: [], + extraArgs: options.extraArgs || [], + blueprintOptions: options.blueprintOptions || {}, + }; + + let packages = options.packages; + if (packages && packages.length) { + taskOptions.packages = packages.map((pkg) => { + if (typeof pkg === 'string') { + return pkg; + } + + if (!pkg.name) { + throw new SilentError('You must provide a package `name` to addAddonsToProject'); + } + + if (pkg.target) { + pkg.name += `@${pkg.target}`; + } + + return pkg.name; + }); + } else { + throw new SilentError('You must provide package to addAddonsToProject'); + } + + let installText = packages.length > 1 ? 'install addons' : 'install addon'; + this._writeStatusToUI(chalk.green, installText, taskOptions['packages'].join(', ')); + + let previousCwd; + if (!this.project.isEmberCLIProject()) { + // The install task adds dependencies based on the current working directory. + // But in case we created the new project by calling the blueprint with a custom target directory (options.target), + // the current directory will *not* be the one the project is created in, so we must adjust this here. + previousCwd = process.cwd(); + process.chdir(options.blueprintOptions.target); + } + + let result = this.taskFor('addon-install').run(taskOptions); + if (previousCwd) { + return result.then(() => process.chdir(previousCwd)); + } + + return result; + }, + + /** + Used to retrieve a task with the given name. Passes the new task + the standard information available (like `ui`, `project`, etc). + + @method taskFor + @param dasherizedName + @public + */ + taskFor(dasherizedName) { + const Task = require(`../tasks/${dasherizedName}`); + + return new Task({ + ui: this.ui, + project: this.project, + }); + }, + + /** + Inserts the given content into a file. If the `contentsToInsert` string is already + present in the current contents, the file will not be changed unless `force` option + is passed. + + If `options.before` is specified, `contentsToInsert` will be inserted before + the first instance of that string. If `options.after` is specified, the + contents will be inserted after the first instance of that string. + If the string specified by options.before or options.after is not in the file, + no change will be made. + + If neither `options.before` nor `options.after` are present, `contentsToInsert` + will be inserted at the end of the file. + + Example: + ``` + // app/router.js + Router.map(function () { + }); + ``` + + ``` + insertIntoFile('app/router.js', ' this.route("admin");', { + after: 'Router.map(function () {' + EOL + }).then(function() { + // file has been inserted into! + }); + + + ``` + + ``` + // app/router.js + Router.map(function () { + this.route("admin"); + }); + ``` + + @method insertIntoFile + @param {String} pathRelativeToProjectRoot + @param {String} contentsToInsert + @param {Object} providedOptions + @return {Promise} + */ + insertIntoFile(pathRelativeToProjectRoot, contentsToInsert, providedOptions) { + let fullPath = path.join(this.project.root, pathRelativeToProjectRoot); + return insertIntoFile(fullPath, contentsToInsert, providedOptions); + }, + + _printCommand: printCommand, + + printBasicHelp(verbose) { + let initialMargin = ' '; + let output = initialMargin; + if (this.overridden) { + output += chalk.grey(`(overridden) ${this.name}`); + } else { + output += this.name; + + output += this._printCommand(initialMargin, true); + + if (verbose) { + output += EOL + this.printDetailedHelp(this.availableOptions); + } + } + + return output; + }, + + printDetailedHelp() { + let markdownColor = new MarkdownColor(); + let filePath = getDetailedHelpPath(this.path); + + if (Blueprint._existsSync(filePath)) { + return markdownColor.renderFile(filePath, { indent: ' ' }); + } + return ''; + }, + + getJson(verbose) { + let json = {}; + this._printableProperties.forEach((key) => { + let value = this[key]; + if (key === 'availableOptions') { + value = cloneDeep(value); + value.forEach((option) => { + if (typeof option.type === 'function') { + option.type = option.type.name; + } + }); + } + json[key] = value; + }); + + if (verbose) { + let detailedHelp = this.printDetailedHelp(this.availableOptions); + if (detailedHelp) { + json.detailedHelp = detailedHelp; + } + } + + return json; + }, + + /** + Used to retrieve a blueprint with the given name. + + @method lookupBlueprint + @param {String} dasherizedName + @return {Blueprint} + @public + */ + lookupBlueprint(dasherizedName) { + let projectPaths = this.project ? this.project.blueprintLookupPaths() : []; + + return Blueprint.lookup(dasherizedName, { + paths: projectPaths, + }); + }, +}); + +const builtInBlueprints = new Map([ + ['app', '@ember-tooling/classic-build-app-blueprint'], + ['addon', '@ember-tooling/classic-build-addon-blueprint'], + ['blueprint', '@ember-tooling/blueprint-blueprint'], +]); + +/** + @static + @method lookup + @namespace Blueprint + @param {String} name + @param {Object} [options] + @param {Array} [options.paths] Extra paths to search for blueprints + @param {Boolean} [options.ignoreMissing] Throw a `SilentError` if a + matching Blueprint could not be found + @param {Object} [options.blueprintOptions] Options object that will be passed + along to the Blueprint instance on creation. + @return {Blueprint} +*/ +Blueprint.lookup = function (name, options) { + options = options || {}; + + let lookupPaths = generateLookupPaths(options.paths); + + let lookupPath; + for (let i = 0; (lookupPath = lookupPaths[i]); i++) { + let blueprintPath = path.resolve(lookupPath, name); + + if (Blueprint._existsSync(blueprintPath)) { + return Blueprint.load(blueprintPath, options.blueprintOptions); + } + } + + // Check if `name` itself is a path to a blueprint: + if (name.includes(path.sep)) { + let blueprintPath = path.resolve(name); + + if (Blueprint._existsSync(blueprintPath)) { + return Blueprint.load(blueprintPath, options.blueprintOptions); + } + } + + // Check for the built in blueprints + if (builtInBlueprints.has(name)) { + let blueprintPath = require.resolve(builtInBlueprints.get(name)); + + if (Blueprint._existsSync(blueprintPath)) { + return Blueprint.load(path.dirname(blueprintPath), options.blueprintOptions); + } + } + + if (!options.ignoreMissing) { + throw new SilentError(`Unknown blueprint: ${name}`); + } +}; + +/** + Loads a blueprint from given path. + @static + @method load + @namespace Blueprint + @param {String} blueprintPath + @param {Object} [blueprintOptions] + @return {Blueprint} blueprint instance +*/ +Blueprint.load = function (blueprintPath, blueprintOptions) { + if (fs.lstatSync(blueprintPath).isDirectory()) { + let Constructor = Blueprint; + + let constructorPath = path.resolve(blueprintPath, 'index.js'); + if (Blueprint._existsSync(constructorPath)) { + let blueprintModule = require(constructorPath); + + if (blueprintModule.__esModule) { + blueprintModule = blueprintModule.default; + } + + if (typeof blueprintModule === 'function') { + Constructor = blueprintModule; + } else { + Constructor = Blueprint.extend(blueprintModule); + } + } + + return new Constructor(blueprintPath, blueprintOptions); + } +}; + +/** + @static + @method list + @namespace Blueprint + @param {Object} [options] + @param {Array} [options.paths] Extra paths to search for blueprints + @return {Array} +*/ +Blueprint.list = function (options) { + options = options || {}; + + let lookupPaths = generateLookupPaths(options.paths); + let seen = []; + + return lookupPaths.map((lookupPath) => { + let source; + let packagePath = path.join(lookupPath, '../package.json'); + if (Blueprint._existsSync(packagePath)) { + source = require(packagePath).name; + } else { + source = path.basename(path.join(lookupPath, '..')); + } + + let blueprints = dir(lookupPath).map((blueprintPath) => { + let blueprint = Blueprint.load(blueprintPath); + if (blueprint) { + let name = blueprint.name; + blueprint.overridden = seen.includes(name); + seen.push(name); + + return blueprint; + } + }); + + // if we're listing blueprints from ember-cli then we should add the built-in blueprints + if (source === 'ember-cli') { + for (let [blueprintName, blueprintPackage] of builtInBlueprints.entries()) { + let blueprint = Blueprint.load(path.dirname(require.resolve(blueprintPackage)), { + name: blueprintName, + }); + + let name = blueprintName; + blueprint.overridden = seen.includes(name); + seen.push(name); + + blueprints.push(blueprint); + } + } + + return { + source, + blueprints: sortBy(compact(blueprints), 'name'), + }; + }); +}; + +Blueprint._existsSync = function (path, parent) { + return fs.existsSync(path, parent); +}; + +Blueprint._readdirSync = function (path) { + return fs.readdirSync(path); +}; + +/** + Files that are renamed when installed into the target directory. + This allows including files in the blueprint that would have an effect + on another process, such as a file named `.gitignore`. + + The keys are the filenames used in the files folder. + The values are the filenames used in the target directory. + + @static + @property renamedFiles +*/ +Blueprint.renamedFiles = { + gitignore: '.gitignore', +}; + +/** + @static + @property ignoredFiles +*/ +Blueprint.ignoredFiles = initialIgnoredFiles; + +/** + @static + @property ignoredUpdateFiles +*/ +Blueprint.ignoredUpdateFiles = ['.gitkeep', 'app.css', 'LICENSE.md']; + +/** + @static + @property defaultLookupPaths +*/ +Blueprint.defaultLookupPaths = function () { + return [path.resolve(__dirname, '..', '..', 'blueprints')]; +}; + +/** + @private + @method prepareConfirm + @param {FileInfo} info + @return {Promise} +*/ +function prepareConfirm(info) { + return info.checkForConflict().then((resolution) => { + info.resolution = resolution; + return info; + }); +} + +/** + @private + @method markIdenticalToBeSkipped + @param {FileInfo} info +*/ +function markIdenticalToBeSkipped(info) { + if (info.resolution === 'identical') { + info.action = 'skip'; + } +} + +/** + @private + @method markToBeRemoved + @param {FileInfo} info +*/ +function markToBeRemoved(info) { + info.action = 'remove'; +} + +/** + @private + @method gatherConfirmationMessages + @param {Array} collection + @param {FileInfo} info + @return {Array} +*/ +function gatherConfirmationMessages(collection, info) { + if (info.resolution === 'confirm') { + collection.push(info.confirmOverwriteTask()); + } + return collection; +} + +/** + @private + @method isIgnored + @param {FileInfo} info + @return {Boolean} +*/ +function isIgnored(info) { + let fn = info.inputPath; + + return Blueprint.ignoredFiles.some((ignoredFile) => minimatch(fn, ignoredFile, { matchBase: true })); +} + +/** + Combines provided lookup paths with defaults and removes + duplicates. + + @private + @method generateLookupPaths + @param {Array} lookupPaths + @return {Array} +*/ +function generateLookupPaths(lookupPaths) { + lookupPaths = lookupPaths || []; + lookupPaths = lookupPaths.concat(Blueprint.defaultLookupPaths()); + return uniq(lookupPaths); +} + +/** + Looks for a __path__ token in the files folder. Must be present for + the blueprint to support pod tokens. + + @private + @method hasPathToken + @param {files} files + @return {Boolean} +*/ +function hasPathToken(files) { + return /__path__/.test(files.join()); +} + +function findAddonByName(addonOrProject, name) { + let addon = addonOrProject.addons.find((addon) => addon.name === name); + + if (addon) { + return addon; + } + + return addonOrProject.addons.find((addon) => findAddonByName(addon, name)); +} + +function ensureTargetDirIsAddon(addonPath) { + let projectInfo; + + try { + projectInfo = require(path.join(addonPath, 'package.json')); + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND') { + throw new Error(`The directory ${addonPath} does not appear to be a valid addon directory.`); + } else { + throw err; + } + } + + return isAddon(projectInfo.keywords); +} + +/** + @private + @method isValidFile + @param {Object} fileInfo + @return {Promise} +*/ +function isValidFile(fileInfo) { + if (isIgnored(fileInfo)) { + return false; + } else { + return isFilePath(fileInfo); + } +} + +/** + @private + @method isFilePath + @param {Object} fileInfo + @return {Promise} +*/ +function isFilePath(fileInfo) { + return fs.statSync(fileInfo.inputPath).isFile(); +} + +/** + @private + @method dir + @return {Array} list of files in the given directory or and empty array if no directory exists +*/ +function dir(fullPath) { + if (Blueprint._existsSync(fullPath)) { + return Blueprint._readdirSync(fullPath).map((fileName) => path.join(fullPath, fileName)); + } else { + return []; + } +} + +/** + @private + @method getDetailedHelpPath + @param {String} thisPath + @return {String} help path +*/ +function getDetailedHelpPath(thisPath) { + return path.join(thisPath, './HELP.md'); +} + +function finishProcessingForInstall(infos) { + infos.forEach(markIdenticalToBeSkipped); + + let infosNeedingConfirmation = infos.reduce(gatherConfirmationMessages, []); + + return sequence(infosNeedingConfirmation).then(() => infos); +} + +function finishProcessingForUninstall(infos) { + let validInfos = infos.filter((info) => fs.existsSync(info.outputPath)); + validInfos.forEach(markToBeRemoved); + + return validInfos; +} + +function replaceExtension(filePath, newExt) { + const { dir, name } = path.parse(filePath); + + return path.format({ + dir, + name, + ext: newExt, + }); +} + +function replaceTypeScriptExtension(filePath) { + const extensionMap = { + '.ts': '.js', + '.gts': '.gjs', + }; + const ext = path.extname(filePath); + const newExt = extensionMap[ext]; + + return replaceExtension(filePath, newExt); +} + +function isTypeScriptFile(filePath) { + return path.extname(filePath) === '.ts' || path.extname(filePath) === '.gts'; +} + +/** + * Takes a substring of a string based on byte offsets. + * @private + * @method substringBytes + * @param {string} value : The input string. + * @param {number} start : The byte index of the substring start. + * @param {number} end : The byte index of the substring end. + * @return {string} : The substring. + */ +function substringBytes(value, start, end) { + let buf = Buffer.from(value); + return buf.subarray(start, end).toString(); +} + +module.exports = Blueprint; diff --git a/packages/blueprint-model/package.json b/packages/blueprint-model/package.json new file mode 100644 index 0000000000..288173103a --- /dev/null +++ b/packages/blueprint-model/package.json @@ -0,0 +1,31 @@ +{ + "name": "@ember-tooling/blueprint-model", + "version": "0.7.0", + "repository": { + "type": "git", + "url": "https://github.com/ember-cli/ember-cli.git", + "directory": "packages/blueprint-model" + }, + "license": "MIT", + "dependencies": { + "babel-remove-types": "^2.0.0", + "chalk": "^5.6.2", + "content-tag": "^4.0.0", + "core-object": "^3.1.5", + "diff": "^8.0.4", + "ember-cli-normalize-entity-name": "^1.0.0", + "ember-cli-string-utils": "^1.1.0", + "fs-extra": "^11.3.4", + "heimdalljs-logger": "^0.1.10", + "inflection": "^3.0.2", + "isbinaryfile": "^5.0.7", + "lodash": "^4.17.23", + "markdown-it": "^14.1.0", + "markdown-it-terminal": "^0.4.0", + "minimatch": "^10.1.1", + "promise.hash.helper": "^1.0.8", + "quick-temp": "^0.1.9", + "silent-error": "^1.1.1", + "walk-sync": "^4.0.0" + } +} diff --git a/packages/blueprint-model/utilities/clean-remove.js b/packages/blueprint-model/utilities/clean-remove.js new file mode 100644 index 0000000000..275635762f --- /dev/null +++ b/packages/blueprint-model/utilities/clean-remove.js @@ -0,0 +1,31 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs-extra'); +const walkUp = require('./walk-up-path'); + +async function cleanRemove(fileInfo) { + try { + await fs.stat(fileInfo.outputPath); + await fs.remove(fileInfo.outputPath); + let paths = walkUp(fileInfo.displayPath).map((thePath) => path.join(fileInfo.outputBasePath, thePath)); + + for (let thePath of paths) { + let childPaths = await fs.readdir(thePath); + if (childPaths.length > 0) { + return; + } + + await fs.remove(thePath); + } + } catch (err) { + // you tried to destroy a blueprint without first generating it + // instead of trying to read dirs that don't exist + // swallow error and carry on + if (err.code !== 'ENOENT') { + throw err; + } + } +} + +module.exports = cleanRemove; diff --git a/packages/blueprint-model/utilities/directory-for-package-name.js b/packages/blueprint-model/utilities/directory-for-package-name.js new file mode 100644 index 0000000000..5b62b62fca --- /dev/null +++ b/packages/blueprint-model/utilities/directory-for-package-name.js @@ -0,0 +1,31 @@ +'use strict'; + +const path = require('path'); + +/** + * Derive a directory name from a package name. + * Takes scoped packages into account. + * + * @method directoryForPackageName + * @param {String} packageName + * @return {String} Derived directory name. + */ +module.exports = function directoryForPackageName(packageName) { + let isScoped = packageName[0] === '@' && packageName.includes('/'); + + if (isScoped) { + let slashIndex = packageName.indexOf('/'); + let scopeName = packageName.substring(1, slashIndex); + let packageNameWithoutScope = packageName.substring(slashIndex + 1); + let pathParts = process.cwd().split(path.sep); + let parentDirectoryContainsScopeName = pathParts.includes(scopeName); + + if (parentDirectoryContainsScopeName) { + return packageNameWithoutScope; + } else { + return `${scopeName}-${packageNameWithoutScope}`; + } + } else { + return packageName; + } +}; diff --git a/packages/blueprint-model/utilities/edit-file-diff.js b/packages/blueprint-model/utilities/edit-file-diff.js new file mode 100644 index 0000000000..96ccb4357b --- /dev/null +++ b/packages/blueprint-model/utilities/edit-file-diff.js @@ -0,0 +1,64 @@ +'use strict'; + +const fs = require('fs'); +const util = require('util'); +const { applyPatch, createPatch } = require('diff'); +const quickTemp = require('quick-temp'); +const path = require('path'); +const SilentError = require('silent-error'); +const openEditor = require('./open-editor'); +const hash = require('promise.hash.helper'); + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); + +class EditFileDiff { + constructor(options) { + this.info = options.info; + + quickTemp.makeOrRemake(this, 'tmpDifferenceDir'); + } + + edit() { + return hash({ + input: this.info.render(), + output: readFile(this.info.outputPath), + }) + .then(this.invokeEditor.bind(this)) + .then(this.applyPatch.bind(this)) + .finally(this.cleanUp.bind(this)); + } + + cleanUp() { + quickTemp.remove(this, 'tmpDifferenceDir'); + } + + applyPatch(resultHash) { + return hash({ + diffString: readFile(resultHash.diffPath), + currentString: readFile(resultHash.outputPath), + }).then((result) => { + let appliedDiff = applyPatch(result.currentString.toString(), result.diffString.toString()); + + if (!appliedDiff) { + let message = 'Patch was not cleanly applied.'; + this.info.ui.writeLine(`${message} Please choose another action.`); + throw new SilentError(message); + } + + return writeFile(resultHash.outputPath, appliedDiff); + }); + } + + invokeEditor(result) { + let info = this.info; + let diff = createPatch(info.outputPath, result.output.toString(), result.input); + let diffPath = path.join(this.tmpDifferenceDir, 'currentDiff.diff'); + + return writeFile(diffPath, diff) + .then(() => openEditor(diffPath)) + .then(() => ({ outputPath: info.outputPath, diffPath })); + } +} + +module.exports = EditFileDiff; diff --git a/packages/blueprint-model/utilities/experiments.js b/packages/blueprint-model/utilities/experiments.js new file mode 100644 index 0000000000..ab7ca9d777 --- /dev/null +++ b/packages/blueprint-model/utilities/experiments.js @@ -0,0 +1,63 @@ +'use strict'; + +const { default: chalk } = require('chalk'); + +/* + If you're here to remove the VITE experiment flag in favor of it being + permanently on, you can't do that until addressing + https://github.com/ember-cli/ember-cli/pull/10781#pullrequestreview-3230644293 + + A lot of test coverage would otherwise be lost, because valid tests are being + run only when the VITE experiment is off. +*/ +const availableExperiments = Object.freeze(['EMBROIDER', 'CLASSIC', 'VITE']); + +const deprecatedExperiments = Object.freeze([]); +const enabledExperiments = Object.freeze(['VITE']); +const deprecatedExperimentsDeprecationsIssued = []; + +function isExperimentEnabled(experimentName) { + if (!availableExperiments.includes(experimentName)) { + return false; + } + + if (process.env.EMBER_CLI_ENABLE_ALL_EXPERIMENTS && deprecatedExperiments.includes(experimentName)) { + return false; + } + + if (process.env.EMBER_CLI_ENABLE_ALL_EXPERIMENTS) { + return true; + } + + if (process.env.EMBER_CLI_CLASSIC && experimentName === 'EMBROIDER') { + return false; + } + + let experimentEnvironmentVariable = `EMBER_CLI_${experimentName}`; + let experimentValue = process.env[experimentEnvironmentVariable]; + + if (deprecatedExperiments.includes(experimentName)) { + let deprecationPreviouslyIssued = deprecatedExperimentsDeprecationsIssued.includes(experimentName); + let isSpecifiedByUser = experimentValue !== undefined; + + if (!deprecationPreviouslyIssued && isSpecifiedByUser) { + console.warn( + chalk.yellow(`The ${experimentName} experiment in ember-cli has been deprecated and will be removed.`) + ); + deprecatedExperimentsDeprecationsIssued.push(experimentName); + } + } + + if (enabledExperiments.includes(experimentName)) { + return experimentValue !== 'false'; + } else { + return experimentValue !== undefined && experimentValue !== 'false'; + } +} + +module.exports = { + isExperimentEnabled, + + // exported for testing purposes + _deprecatedExperimentsDeprecationsIssued: deprecatedExperimentsDeprecationsIssued, +}; diff --git a/packages/blueprint-model/utilities/file-info.js b/packages/blueprint-model/utilities/file-info.js new file mode 100644 index 0000000000..8a83239088 --- /dev/null +++ b/packages/blueprint-model/utilities/file-info.js @@ -0,0 +1,170 @@ +'use strict'; + +const fs = require('fs'); +const util = require('util'); +const { default: chalk } = require('chalk'); +const EditFileDiff = require('./edit-file-diff'); +const EOL = require('os').EOL; +const rxEOL = new RegExp(EOL, 'g'); +const isBinaryFile = require('isbinaryfile').isBinaryFileSync; +const hash = require('promise.hash.helper'); +const canEdit = require('./open-editor').canEdit; +const processTemplate = require('./process-template'); + +const readFile = util.promisify(fs.readFile); +const lstat = util.promisify(fs.stat); + +function diffHighlight(line) { + if (line[0] === '+') { + return chalk.green(line); + } else if (line[0] === '-') { + return chalk.red(line); + } else if (/^@@/.test(line)) { + return chalk.cyan(line); + } else { + return line; + } +} + +const NOOP = (_) => _; +class FileInfo { + constructor(options) { + this.action = options.action; + this.outputBasePath = options.outputBasePath; + this.outputPath = options.outputPath; + this.displayPath = options.displayPath; + this.inputPath = options.inputPath; + this.templateVariables = options.templateVariables; + this.replacer = options.replacer || NOOP; + this.ui = options.ui; + } + + confirmOverwrite(path) { + let promptOptions = { + type: 'expand', + name: 'answer', + default: false, + message: `${chalk.red('Overwrite')} ${path}?`, + choices: [ + { key: 'y', name: 'Yes, overwrite', value: 'overwrite' }, + { key: 'n', name: 'No, skip', value: 'skip' }, + ], + }; + + let outputPathIsFile = false; + try { + outputPathIsFile = fs.statSync(this.outputPath).isFile(); + } catch (err) { + /* ignore */ + } + + let canDiff = !isBinaryFile(this.inputPath) && (!outputPathIsFile || !isBinaryFile(this.outputPath)); + + if (canDiff) { + promptOptions.choices.push({ key: 'd', name: 'Diff', value: 'diff' }); + + if (canEdit()) { + promptOptions.choices.push({ key: 'e', name: 'Edit', value: 'edit' }); + } + } + + return this.ui.prompt(promptOptions).then((response) => response.answer); + } + + displayDiff() { + let info = this, + { createPatch } = require('diff'); + return hash({ + input: this.render(), + output: readFile(info.outputPath), + }).then((result) => { + let diff = createPatch( + info.outputPath, + result.output.toString().replace(rxEOL, '\n'), + result.input.replace(rxEOL, '\n') + ); + let lines = diff.split('\n'); + + for (let i = 0; i < lines.length; i++) { + info.ui.write(diffHighlight(lines[i] + EOL)); + } + }); + } + + async render() { + if (!this.rendered) { + let result = await this._render(); + this.rendered = this.replacer(result, this); + } + + return this.rendered; + } + + _render() { + let path = this.inputPath; + let context = this.templateVariables; + + return readFile(path).then((content) => + lstat(path).then((fileStat) => { + if (isBinaryFile(content, fileStat.size)) { + return content; + } else { + try { + return processTemplate(content.toString(), context); + } catch (err) { + err.message += ` (Error in blueprint template: ${path})`; + throw err; + } + } + }) + ); + } + + checkForConflict() { + return this.render().then((input) => { + input = input.toString().replace(rxEOL, '\n'); + + return readFile(this.outputPath) + .then((output) => { + output = output.toString().replace(rxEOL, '\n'); + + return input === output ? 'identical' : 'confirm'; + }) + .catch((e) => { + if (e.code === 'ENOENT') { + return 'none'; + } + + throw e; + }); + }); + } + + confirmOverwriteTask() { + let info = this; + + return function () { + function doConfirm() { + return info.confirmOverwrite(info.displayPath).then((action) => { + if (action === 'diff') { + return info.displayDiff().then(doConfirm); + } else if (action === 'edit') { + let editFileDiff = new EditFileDiff({ info }); + return editFileDiff + .edit() + .then(() => (info.action = action)) + .catch(() => doConfirm()) + .then(() => info); + } else { + info.action = action; + return info; + } + }); + } + + return doConfirm(); + }; + } +} + +module.exports = FileInfo; diff --git a/packages/blueprint-model/utilities/insert-into-file.js b/packages/blueprint-model/utilities/insert-into-file.js new file mode 100644 index 0000000000..dff87ddd1f --- /dev/null +++ b/packages/blueprint-model/utilities/insert-into-file.js @@ -0,0 +1,125 @@ +'use strict'; + +const fs = require('fs-extra'); +const EOL = require('os').EOL; + +/** + Inserts the given content into a file. If the `contentsToInsert` string is already + present in the current contents, the file will not be changed unless `force` option + is passed. + + If `options.before` is specified, `contentsToInsert` will be inserted before + the first instance of that string. If `options.after` is specified, the + contents will be inserted after the first instance of that string. + If the string specified by options.before or options.after is not in the file, + no change will be made. Both of these options support regular expressions. + + If neither `options.before` nor `options.after` are present, `contentsToInsert` + will be inserted at the end of the file. + + It will create a new file if one doesn't exist, unless you set the `options.create` + option to `false`. + + Example: + + ``` + // app/router.js + Router.map(function () { + }); + ``` + + ``` + insertIntoFile('app/router.js', ' this.route("admin");', { + after: 'Router.map(function () {' + EOL + }); + ``` + + ``` + // app/router.js + Router.map(function () { + this.route("admin"); + }); + ``` + + @method insertIntoFile + @param {String} pathRelativeToProjectRoot + @param {String} contentsToInsert + @param {Object} providedOptions + @return {Promise} +*/ +async function insertIntoFile(fullPath, contentsToInsert, providedOptions) { + let options = providedOptions || {}; + + let returnValue = { + path: fullPath, + originalContents: '', + contents: '', + inserted: false, + }; + + let exists = fs.existsSync(fullPath); + + if (exists || (!exists && options.create !== false)) { + let originalContents = ''; + + if (exists) { + originalContents = fs.readFileSync(fullPath, { encoding: 'utf8' }); + } + + let contentsToWrite = originalContents; + + let alreadyPresent = originalContents.indexOf(contentsToInsert) > -1; + let insert = !alreadyPresent; + let insertBehavior = 'end'; + + if (options.before) { + insertBehavior = 'before'; + } + if (options.after) { + insertBehavior = 'after'; + } + + if (options.force) { + insert = true; + } + + if (insert) { + if (insertBehavior === 'end') { + contentsToWrite += contentsToInsert; + } else { + let contentMarker = options[insertBehavior]; + if (contentMarker instanceof RegExp) { + let matches = contentsToWrite.match(contentMarker); + if (matches) { + contentMarker = matches[0]; + } + } + let contentMarkerIndex = contentsToWrite.indexOf(contentMarker); + + if (contentMarkerIndex !== -1) { + let insertIndex = contentMarkerIndex; + if (insertBehavior === 'after') { + insertIndex += contentMarker.length; + } + + contentsToWrite = + contentsToWrite.slice(0, insertIndex) + contentsToInsert + EOL + contentsToWrite.slice(insertIndex); + } + } + } + + returnValue.originalContents = originalContents; + returnValue.contents = contentsToWrite; + + if (contentsToWrite !== originalContents) { + returnValue.inserted = true; + + await fs.outputFile(fullPath, contentsToWrite); + return returnValue; + } + } + + return Promise.resolve(returnValue); +} + +module.exports = insertIntoFile; diff --git a/packages/blueprint-model/utilities/is-addon.js b/packages/blueprint-model/utilities/is-addon.js new file mode 100644 index 0000000000..1ce9da9a38 --- /dev/null +++ b/packages/blueprint-model/utilities/is-addon.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function isAddon(keywords) { + return Array.isArray(keywords) && keywords.indexOf('ember-addon') >= 0; +}; diff --git a/packages/blueprint-model/utilities/markdown-color.js b/packages/blueprint-model/utilities/markdown-color.js new file mode 100644 index 0000000000..b57e5c1697 --- /dev/null +++ b/packages/blueprint-model/utilities/markdown-color.js @@ -0,0 +1,208 @@ +'use strict'; + +const fs = require('fs'); + +const { default: chalk } = require('chalk'); +const SilentError = require('silent-error'); +const merge = require('lodash/merge'); + +let colors = ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow', 'black', 'white', 'grey', 'gray']; +let backgroundColors = ['bgRed', 'bgGreen', 'bgBlue', 'bgCyan', 'bgMagenta', 'bgYellow', 'bgWhite', 'bgBlack']; + +/* + Parses markdown and applies coloring to output by matching defined tokens and + applying styling. Default outputs chalk.js coloring for terminal output. + Options: + tokens: pass additional tokens in the following format: + { + name:{ + token: '(Reserved)', // example of token + pattern: /(Reserved)/g, // regexp pattern + render(string) { return string + '!'} // function that takes and returns a string + } + } + + markdownOptions: object containing options to pass (or override) to markdown-it-terminal upon + instantiation. See https://github.com/trabus/markdown-it-terminal/blob/master/index.js#L8 + + renderStyles: pass an object to render color styles, default is chalk.js + + @class MarkdownColor + @constructor + @param {Object} [options] +*/ +class MarkdownColor { + constructor(options) { + let optionTokens = (options && options.tokens) || {}; + let renderStyles = (options && options.renderStyles) || chalk; + let tokens = this.generateTokens(renderStyles); + let markdownOptions = (options && options.markdownOptions) || {}; + const markdown = require('markdown-it'); + const terminal = require('markdown-it-terminal'); + this.options = options || {}; + this.markdown = markdown().use(terminal, markdownOptions); + this.tokens = merge(tokens, optionTokens); + } + + /* + Lookup markdown file and render contents + @method renderFile + @param {String} [filePath] + @param {Object} [options] + */ + renderFile(filePath, options) { + let file; + try { + file = fs.readFileSync(filePath, 'utf-8'); + } catch (e) { + throw new SilentError(`The file '${filePath}' doesn't exist. Please check your filePath`); + } + + return this.render(file, options); + } + + /* + Parse markdown and output as string + @method render + @param {String} [string] + @param {Object} [options] + @return {String} + */ + render(string, options) { + let indent = (options && options.indent) || ''; + let input = this.markdown.render(string); + let styles = Object.keys(this.tokens); + + input = input.replace(/^/gm, indent); + styles.reverse().map((style) => { + input = input.replace(this.tokens[style].pattern, this.tokens[style].render); + }); + input = input.replace(/~\^(.*)~\^/g, escapeToken); + return input; + } + + /* + Generate default style tokens + @method generateTokens + @return {Object} + */ + generateTokens(renderer) { + let defaultTokens = { + // ember-cli styles + option: { + name: 'option', + token: '--option', + pattern: /((--\w*\b)|())/g, + render: this.renderStylesFactory(renderer, 'cyan'), + }, + default: { + name: 'default', + token: '(Default: default)', + pattern: /(\(Default:\s.*\))/g, + render: this.renderStylesFactory(renderer, 'cyan'), + }, + required: { + name: 'required', + token: '(Required)', + pattern: /(\(Required\))/g, + render: this.renderStylesFactory(renderer, 'cyan'), + }, + }; + let colorTokens = unshiftValue(colors.concat(backgroundColors).map(getToken.bind(this)), {}).reduce(setToken); + return merge(colorTokens, defaultTokens); + } + + /* + Looks up multiple styles to apply to the rendered output + @method renderStylesFactory + @param {Object} [renderer] should match chalk.js format + @param {String, Array} [styleNames] + @return {Function} Function that will wrap the input string with styling + */ + renderStylesFactory(renderer, styleNames) { + let styles; + + if (Array.isArray(styleNames)) { + styles = styleNames.map(checkStyleName.bind(null, renderer)); + } else { + styles = [checkStyleName(renderer, styleNames)]; + } + + return function (match, pattern) { + return styles.reverse().reduce((previous, current) => renderer[current](previous), pattern); + }; + } +} + +module.exports = MarkdownColor; + +/* + Check to see if style exists on the renderer + @param {Object} [renderer] should match chalk.js format + @param {String} [name] +*/ +function checkStyleName(renderer, name) { + if (typeof renderer[name] === 'function') { + return name; + } else { + throw new SilentError(`The style '${name}' is not supported by the markdown renderer.`); + } +} + +/* + @param {String} [name] + @param {Object} [options] + @return {RegExp} Returns lookup pattern +*/ +function getColorTokenRegex(name, options) { + options = options || {}; + let start = options.start || '(?:<'; + let end = options.end || '>)'; + let close = options.close || '(?:`, + pattern: getColorTokenRegex(name), + render: this.renderStylesFactory(renderer, name), + }; +} + +function setToken(result, color) { + result[color.name] = color; + return result; +} + +function escapeToken(match, pattern) { + let output = pattern.replace(/~/g, ''); + return `<${output}>`; +} + +function unshiftValue(array, value) { + array.unshift(value); + return array; +} + +/* +Formatting colors for ember-cli help + +white: ember serve +yellow: +cyan: --port, --important-option +cyan: (Default: something), (Default: 4200) +white: Description 1, Description 2 +cyan: (Required) +*/ diff --git a/packages/blueprint-model/utilities/open-editor.js b/packages/blueprint-model/utilities/open-editor.js new file mode 100644 index 0000000000..99260ff34c --- /dev/null +++ b/packages/blueprint-model/utilities/open-editor.js @@ -0,0 +1,44 @@ +'use strict'; + +const spawn = require('child_process').spawn; + +function openEditor(file) { + if (!openEditor.canEdit()) { + throw new Error('EDITOR environment variable is not set'); + } + + if (!file) { + throw new Error('No `file` option provided'); + } + + let editorArgs = openEditor._env().EDITOR.split(' '); + let editor = editorArgs.shift(); + const args = [file].concat(editorArgs); + let editProcess = openEditor._spawn(editor, args, { stdio: 'inherit' }); + + return new Promise((resolve, reject) => { + editProcess.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject( + new Error(`Spawn('${editor}', [${args.join(',')}]) exited with a non zero error status code: '${code}'`) + ); + } + }); + }); +} + +openEditor.canEdit = function () { + return openEditor._env().EDITOR !== undefined; +}; + +openEditor._env = function () { + return process.env; +}; + +openEditor._spawn = function () { + return spawn.apply(this, arguments); +}; + +module.exports = openEditor; diff --git a/packages/blueprint-model/utilities/prepend-emoji.js b/packages/blueprint-model/utilities/prepend-emoji.js new file mode 100644 index 0000000000..bb10999f4c --- /dev/null +++ b/packages/blueprint-model/utilities/prepend-emoji.js @@ -0,0 +1,12 @@ +'use strict'; + +function supportEmoji() { + const hasEmojiTurnedOff = process.argv.indexOf('--no-emoji') > -1; + return process.stdout.isTTY && process.platform !== 'win32' && !hasEmojiTurnedOff; +} + +const areEmojiSupported = supportEmoji(); + +module.exports = function prependEmoji(emoji, msg) { + return areEmojiSupported ? `${emoji} ${msg}` : msg; +}; diff --git a/packages/blueprint-model/utilities/print-command.js b/packages/blueprint-model/utilities/print-command.js new file mode 100644 index 0000000000..ad1885f8cf --- /dev/null +++ b/packages/blueprint-model/utilities/print-command.js @@ -0,0 +1,102 @@ +'use strict'; + +const { default: chalk } = require('chalk'); +const EOL = require('os').EOL; + +module.exports = function (initialMargin, shouldDescriptionBeGrey) { + initialMargin = initialMargin || ''; + + let output = ''; + + let options = this.anonymousOptions; + + // ... + if (options.length) { + output += ` ${chalk.yellow( + options + .map((option) => { + // blueprints we insert brackets, commands already have them + if (option.indexOf('<') === 0) { + return option; + } else { + return `<${option}>`; + } + }) + .join(' ') + )}`; + } + + options = this.availableOptions; + + // + if (options.length) { + output += ` ${chalk.cyan('')}`; + } + + // Description + let description = this.description; + if (description) { + if (shouldDescriptionBeGrey) { + description = chalk.grey(description); + } + output += `${EOL + initialMargin} ${description}`; + } + + // aliases: a b c + if (this.aliases && this.aliases.length) { + output += `${EOL + initialMargin} ${chalk.grey(`aliases: ${this.aliases.filter((a) => a).join(', ')}`)}`; + } + + // --available-option (Required) (Default: value) + // ... + options.forEach((option) => { + output += `${EOL + initialMargin} ${chalk.cyan(`--${option.name}`)}`; + + if (option.values) { + output += chalk.cyan(`=${option.values.join('|')}`); + } + + if (option.type) { + let types = Array.isArray(option.type) ? option.type.map(formatType).join(', ') : formatType(option.type); + + output += ` ${chalk.cyan(`(${types})`)}`; + } + + if (option.required) { + output += ` ${chalk.cyan('(Required)')}`; + } + + if (option.default !== undefined) { + output += ` ${chalk.cyan(`(Default: ${formatValue(option.default)})`)}`; + } + + if (option.description) { + output += ` ${option.description}`; + } + + if (option.aliases && option.aliases.length) { + output += `${EOL + initialMargin} ${chalk.grey( + `aliases: ${option.aliases + .map((a) => { + if (typeof a === 'string') { + return (a.length > 4 ? '--' : '-') + a + (option.type === Boolean ? '' : ' '); + } else { + let key = Object.keys(a)[0]; + return `${(key.length > 4 ? '--' : '-') + key} (--${option.name}=${formatValue(a[key])})`; + } + }) + .join(', ')}` + )}`; + } + }); + + return output; +}; + +function formatType(type) { + return typeof type === 'string' ? formatValue(type) : type.name; +} + +function formatValue(val) { + return val === '' ? '""' : val; +} diff --git a/packages/blueprint-model/utilities/process-template.js b/packages/blueprint-model/utilities/process-template.js new file mode 100644 index 0000000000..7f888ac9dd --- /dev/null +++ b/packages/blueprint-model/utilities/process-template.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = function processTemplate(content, context) { + let options = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g, + }; + return require('lodash/template')(content, options)(context); +}; diff --git a/lib/utilities/sequence.js b/packages/blueprint-model/utilities/sequence.js similarity index 66% rename from lib/utilities/sequence.js rename to packages/blueprint-model/utilities/sequence.js index ec9e6eb7e7..8b5df8b430 100644 --- a/lib/utilities/sequence.js +++ b/packages/blueprint-model/utilities/sequence.js @@ -1,6 +1,5 @@ 'use strict'; -var Promise = require('../ext/promise'); /* * * given an array of functions, that may or may not return promises sequence @@ -28,14 +27,16 @@ var Promise = require('../ext/promise'); * @return Promise * */ -module.exports = function sequence(tasks) { - var length = tasks.length; - var current = Promise.resolve(); - var results = new Array(length); +module.exports = async function sequence(tasks) { + let length = tasks.length; + let results = []; - for (var i = 0; i < length; ++i) { - current = results[i] = current.then(tasks[i]); + for (let i = 0; i < length; ++i) { + let task = tasks[i]; + let prevResult = results[i - 1]; + + results.push(typeof task === 'function' ? await task(prevResult) : await task); } - return Promise.all(results); + return results; }; diff --git a/packages/blueprint-model/utilities/walk-up-path.js b/packages/blueprint-model/utilities/walk-up-path.js new file mode 100644 index 0000000000..fd6c4f18af --- /dev/null +++ b/packages/blueprint-model/utilities/walk-up-path.js @@ -0,0 +1,23 @@ +'use strict'; + +const path = require('path'); + +let regex = /^[./]$/; + +function walkUp(thePath) { + let paths = []; + + let currentPath = thePath; + // eslint-disable-next-line no-constant-condition + while (true) { + currentPath = path.dirname(currentPath); + if (regex.test(currentPath)) { + break; + } + paths.push(currentPath); + } + + return paths; +} + +module.exports = walkUp; diff --git a/patches/ember-cli-blueprint-test-helpers.patch b/patches/ember-cli-blueprint-test-helpers.patch new file mode 100644 index 0000000000..6d18277a07 --- /dev/null +++ b/patches/ember-cli-blueprint-test-helpers.patch @@ -0,0 +1,15 @@ +diff --git a/lib/helpers/setup.js b/lib/helpers/setup.js +index c337ec0020f46e0e9ed3f76b325c7d174168a900..29dfd7fb0acd5ae895d470b80be15bf1c8ff9a25 100644 +--- a/lib/helpers/setup.js ++++ b/lib/helpers/setup.js +@@ -31,7 +31,9 @@ module.exports = function setupTestHooks(scope, options) { + } + + if (disabledTasks.length) { +- let Blueprint = requireFromCLI('lib/models/blueprint'); ++ let Blueprint = require('@ember-tooling/blueprint-model', { ++ paths: [options.cliPath] ++ }); + + before(() => { + MockBlueprintTaskFor.disableTasks(Blueprint, disabledTasks); diff --git a/patches/patch-engine-io-client.js b/patches/patch-engine-io-client.js deleted file mode 100755 index 7829f75739..0000000000 --- a/patches/patch-engine-io-client.js +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env node -/* - * patch engion.io-client to use any XMLHTTPRequest, but then bundle the one it - * wanted in ember-cli’s tarball - * - * pending: https://github.com/socketio/engine.io-client/issues/405 - */ - -var fs = require('fs'); -var packagePath = __dirname + '/../node_modules/testem/node_modules/socket.io/node_modules/socket.io-client/node_modules/engine.io-client/package.json'; -var pkg = JSON.parse(fs.readFileSync(packagePath, 'UTF-8')); - -if (pkg.dependencies.xmlhttprequest !== 'https://github.com/rase-/node-XMLHttpRequest/archive/a6b6f2.tar.gz') { - throw new Error('engine.io-clients deps changed, XMLHTTPRequest may be fixed'); -} -pkg.dependencies.xmlhttprequest === '*'; - -fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2)); diff --git a/patches/patch-ws.js b/patches/patch-ws.js deleted file mode 100755 index 1ae1ce7a4e..0000000000 --- a/patches/patch-ws.js +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node -/* - * patch engion.io-client to use any XMLHTTPRequest, but then bundle the one it - * wanted in ember-cli’s tarball - * - * pending: https://github.com/socketio/engine.io-client/issues/405 - */ - -var fs = require('fs'); -var packagePath = __dirname + '/../node_modules/testem/node_modules/socket.io/node_modules/engine.io/node_modules/ws/package.json'; -var pkg = JSON.parse(fs.readFileSync(packagePath, 'UTF-8')); - -delete pkg.optionalDependencies; -delete pkg.dependencies['bufferutil']; -delete pkg.dependencies['utf-8-validate']; -console.log(packagePath); -fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..02b1d050a2 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,11553 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +patchedDependencies: + ember-cli-blueprint-test-helpers: + hash: 67b4e6082643614a12fdca71c9ed35d2df9b46dfb0d24cb3ba3665b2048bac9a + path: patches/ember-cli-blueprint-test-helpers.patch + +importers: + + .: + dependencies: + '@ember-tooling/blueprint-blueprint': + specifier: workspace:* + version: link:packages/blueprint-blueprint + '@ember-tooling/blueprint-model': + specifier: workspace:* + version: link:packages/blueprint-model + '@ember-tooling/classic-build-addon-blueprint': + specifier: workspace:* + version: link:packages/addon-blueprint + '@ember-tooling/classic-build-app-blueprint': + specifier: workspace:* + version: link:packages/app-blueprint + '@ember/app-blueprint': + specifier: ~7.2.0-alpha.1 + version: 7.2.0-alpha.1 + '@pnpm/find-workspace-dir': + specifier: ^1000.1.5 + version: 1000.1.5 + babel-remove-types: + specifier: ^2.0.0 + version: 2.0.0 + broccoli: + specifier: ^4.0.0 + version: 4.0.0 + broccoli-concat: + specifier: ^4.2.7 + version: 4.2.7 + broccoli-config-loader: + specifier: ^1.0.1 + version: 1.0.1 + broccoli-config-replace: + specifier: ^1.1.3 + version: 1.1.3 + broccoli-debug: + specifier: ^0.6.5 + version: 0.6.5 + broccoli-funnel: + specifier: ^3.0.8 + version: 3.0.8 + broccoli-funnel-reducer: + specifier: ^1.0.0 + version: 1.0.0 + broccoli-merge-trees: + specifier: ^4.2.0 + version: 4.2.0 + broccoli-middleware: + specifier: ^2.1.2 + version: 2.1.2 + broccoli-slow-trees: + specifier: ^3.1.0 + version: 3.1.0 + broccoli-source: + specifier: ^3.0.1 + version: 3.0.1 + broccoli-stew: + specifier: ^3.0.0 + version: 3.0.0 + calculate-cache-key-for-tree: + specifier: ^2.0.0 + version: 2.0.0 + capture-exit: + specifier: ^2.0.0 + version: 2.0.0 + chalk: + specifier: ^5.6.2 + version: 5.6.2 + ci-info: + specifier: ^4.4.0 + version: 4.4.0 + clean-base-url: + specifier: ^1.0.0 + version: 1.0.0 + compression: + specifier: ^1.8.1 + version: 1.8.1 + configstore: + specifier: ^8.0.0 + version: 8.0.0 + console-ui: + specifier: ^3.1.2 + version: 3.1.2 + content-tag: + specifier: ^4.2.0 + version: 4.2.0 + core-object: + specifier: ^3.1.5 + version: 3.1.5 + dag-map: + specifier: ^2.0.2 + version: 2.0.2 + diff: + specifier: ^8.0.4 + version: 8.0.4 + ember-cli-is-package-missing: + specifier: ^1.0.0 + version: 1.0.0 + ember-cli-normalize-entity-name: + specifier: ^1.0.0 + version: 1.0.0 + ember-cli-preprocess-registry: + specifier: ^5.0.1 + version: 5.0.1 + ember-cli-string-utils: + specifier: ^1.1.0 + version: 1.1.0 + ensure-posix-path: + specifier: ^1.1.1 + version: 1.1.1 + execa: + specifier: ^9.6.1 + version: 9.6.1 + exit: + specifier: ^0.1.2 + version: 0.1.2 + express: + specifier: ^5.2.1 + version: 5.2.1 + filesize: + specifier: ^11.0.17 + version: 11.0.17 + find-up: + specifier: ^8.0.0 + version: 8.0.0 + find-yarn-workspace-root: + specifier: ^2.0.0 + version: 2.0.0 + fs-extra: + specifier: ^11.3.5 + version: 11.3.5 + fs-tree-diff: + specifier: ^2.0.1 + version: 2.0.1 + get-caller-file: + specifier: ^2.0.5 + version: 2.0.5 + git-repo-info: + specifier: ^2.1.1 + version: 2.1.1 + glob: + specifier: ^13.0.6 + version: 13.0.6 + heimdalljs: + specifier: ^0.2.6 + version: 0.2.6 + heimdalljs-fs-monitor: + specifier: ^1.1.2 + version: 1.1.2 + heimdalljs-graph: + specifier: ^1.0.0 + version: 1.0.0 + heimdalljs-logger: + specifier: ^0.1.10 + version: 0.1.10 + http-proxy: + specifier: ^1.18.1 + version: 1.18.1 + inflection: + specifier: ^3.0.2 + version: 3.0.2 + inquirer: + specifier: ^13.4.3 + version: 13.4.3(@types/node@22.10.2) + is-git-url: + specifier: ^1.0.0 + version: 1.0.0 + is-language-code: + specifier: ^5.1.3 + version: 5.1.3 + lodash: + specifier: ^4.18.1 + version: 4.18.1 + markdown-it: + specifier: ^14.1.1 + version: 14.1.1 + markdown-it-terminal: + specifier: ^0.4.0 + version: 0.4.0(markdown-it@14.1.1) + minimatch: + specifier: ^10.2.5 + version: 10.2.5 + morgan: + specifier: ^1.10.1 + version: 1.10.1 + nopt: + specifier: ^3.0.6 + version: 3.0.6 + npm-package-arg: + specifier: ^13.0.2 + version: 13.0.2 + os-locale: + specifier: ^6.0.2 + version: 6.0.2 + p-defer: + specifier: ^4.0.1 + version: 4.0.1 + portfinder: + specifier: ^1.0.38 + version: 1.0.38 + promise-map-series: + specifier: ^0.3.0 + version: 0.3.0 + promise.hash.helper: + specifier: ^1.0.8 + version: 1.0.8 + quick-temp: + specifier: ^0.1.9 + version: 0.1.9 + resolve: + specifier: ^1.22.12 + version: 1.22.12 + resolve-package-path: + specifier: ^4.0.3 + version: 4.0.3 + safe-stable-stringify: + specifier: ^2.5.0 + version: 2.5.0 + sane: + specifier: ^5.0.1 + version: 5.0.1 + semver: + specifier: ^7.8.1 + version: 7.8.1 + semver-deprecate: + specifier: ^1.1.0 + version: 1.1.0 + silent-error: + specifier: ^1.1.1 + version: 1.1.1 + sort-package-json: + specifier: ^3.6.1 + version: 3.6.1 + symlink-or-copy: + specifier: ^1.3.1 + version: 1.3.1 + testem: + specifier: ^3.20.0 + version: 3.20.0(@babel/core@7.29.0)(bufferutil@4.0.8)(ejs@3.1.10)(handlebars@4.7.8)(underscore@1.13.7)(utf-8-validate@5.0.10) + tiny-lr: + specifier: ^2.0.0 + version: 2.0.0 + tree-sync: + specifier: ^2.1.0 + version: 2.1.0 + walk-sync: + specifier: ^4.0.2 + version: 4.0.2 + watch-detector: + specifier: ^1.0.2 + version: 1.0.2 + workerpool: + specifier: ^10.0.2 + version: 10.0.2 + yam: + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + broccoli-plugin: + specifier: ^4.0.7 + version: 4.0.7 + broccoli-test-helper: + specifier: ^2.0.0 + version: 2.0.0 + chai: + specifier: ^6.2.2 + version: 6.2.2 + chai-as-promised: + specifier: ^8.0.2 + version: 8.0.2(chai@6.2.2) + chai-files: + specifier: ^1.4.0 + version: 1.4.0 + chai-jest-snapshot: + specifier: ^2.0.0 + version: 2.0.0(chai@6.2.2) + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + ember-cli-blueprint-test-helpers: + specifier: ^0.19.2 + version: 0.19.2(patch_hash=67b4e6082643614a12fdca71c9ed35d2df9b46dfb0d24cb3ba3665b2048bac9a) + ember-cli-internal-test-helpers: + specifier: ^0.9.1 + version: 0.9.1 + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@8.57.1) + eslint-plugin-chai-expect: + specifier: ^3.1.0 + version: 3.1.0(eslint@8.57.1) + eslint-plugin-mocha: + specifier: ^10.5.0 + version: 10.5.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^17.24.0 + version: 17.24.0(eslint@8.57.1)(typescript@5.9.3) + fixturify: + specifier: ^3.0.0 + version: 3.0.0 + fixturify-project: + specifier: ^2.1.1 + version: 2.1.1 + jsdom: + specifier: ^21.1.2 + version: 21.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + latest-version: + specifier: ^9.0.0 + version: 9.0.0 + mocha: + specifier: ^11.7.6 + version: 11.7.6 + nock: + specifier: ^14.0.15 + version: 14.0.15 + nyc: + specifier: ^17.1.0 + version: 17.1.0 + prettier: + specifier: 3.7.4 + version: 3.7.4 + release-plan: + specifier: ^0.17.4 + version: 0.17.4 + strip-ansi: + specifier: ^7.2.0 + version: 7.2.0 + supertest: + specifier: ^7.2.2 + version: 7.2.2 + testdouble: + specifier: ^3.20.2 + version: 3.20.2 + tmp: + specifier: ^0.2.5 + version: 0.2.5 + tmp-promise: + specifier: ^3.0.3 + version: 3.0.3 + websocket: + specifier: ^1.0.35 + version: 1.0.35 + which: + specifier: 6.0.0 + version: 6.0.0 + yuidoc-ember-cli-theme: + specifier: ^1.0.4 + version: 1.0.4 + yuidocjs: + specifier: 0.10.2 + version: 0.10.2 + + packages/addon-blueprint: + dependencies: + '@ember-tooling/blueprint-model': + specifier: workspace:* + version: link:../blueprint-model + chalk: + specifier: ^5.6.2 + version: 5.6.2 + ember-cli-normalize-entity-name: + specifier: ^1.0.0 + version: 1.0.0 + ember-cli-string-utils: + specifier: ^1.1.0 + version: 1.1.0 + fs-extra: + specifier: ^11.3.5 + version: 11.3.5 + lodash: + specifier: ^4.18.1 + version: 4.18.1 + silent-error: + specifier: ^1.1.1 + version: 1.1.1 + sort-package-json: + specifier: ^2.15.1 + version: 2.15.1 + walk-sync: + specifier: ^3.0.0 + version: 3.0.0 + + packages/app-blueprint: + dependencies: + '@ember-tooling/blueprint-model': + specifier: workspace:* + version: link:../blueprint-model + chalk: + specifier: ^5.6.2 + version: 5.6.2 + ember-cli-string-utils: + specifier: ^1.1.0 + version: 1.1.0 + + packages/blueprint-blueprint: {} + + packages/blueprint-model: + dependencies: + babel-remove-types: + specifier: ^2.0.0 + version: 2.0.0 + chalk: + specifier: ^5.6.2 + version: 5.6.2 + content-tag: + specifier: ^4.0.0 + version: 4.2.0 + core-object: + specifier: ^3.1.5 + version: 3.1.5 + diff: + specifier: ^8.0.4 + version: 8.0.4 + ember-cli-normalize-entity-name: + specifier: ^1.0.0 + version: 1.0.0 + ember-cli-string-utils: + specifier: ^1.1.0 + version: 1.1.0 + fs-extra: + specifier: ^11.3.4 + version: 11.3.5 + heimdalljs-logger: + specifier: ^0.1.10 + version: 0.1.10 + inflection: + specifier: ^3.0.2 + version: 3.0.2 + isbinaryfile: + specifier: ^5.0.7 + version: 5.0.7 + lodash: + specifier: ^4.17.23 + version: 4.18.1 + markdown-it: + specifier: ^14.1.0 + version: 14.1.1 + markdown-it-terminal: + specifier: ^0.4.0 + version: 0.4.0(markdown-it@14.1.1) + minimatch: + specifier: ^10.1.1 + version: 10.2.5 + promise.hash.helper: + specifier: ^1.0.8 + version: 1.0.8 + quick-temp: + specifier: ^0.1.9 + version: 0.1.9 + silent-error: + specifier: ^1.1.1 + version: 1.1.1 + walk-sync: + specifier: ^4.0.0 + version: 4.0.1 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.3': + resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.3': + resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.3': + resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.3': + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.4': + resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@cnakazawa/watch@1.0.4': + resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} + engines: {node: '>=0.1.95'} + hasBin: true + + '@ember/app-blueprint@7.2.0-alpha.1': + resolution: {integrity: sha512-nF/4LKTl5t6rmdyJ4hQKBSbZU2AMnlNUowYuSDay+fw19EUsAqUxdclrIZX6TjvhTXZICHULwornpdbpFCXN2A==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@inquirer/ansi@2.0.5': + resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/checkbox@5.1.5': + resolution: {integrity: sha512-Jmf9tgBHIEK5SAOB7swYfStqmtkZb00xOTpSQmkoGEpdxOTpJi9RS0A8bkfDPHTTItZRJrRdZrEMu25wyj0VfQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@6.0.13': + resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.1.10': + resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.1.2': + resolution: {integrity: sha512-Y3Nor7S/DhIPo+8Ym/dSY4efwKI4BsflKDwXh0jNeXJsSF3dteS/3Yf+z4wkibVZDvYMyCgknSTQlNahfunGHg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.0.14': + resolution: {integrity: sha512-qyY9zcIX2eKYwaAUiQo9zORd61Lc3sXeM72fVbeHkYnDkqfr8/armcRbmVAIrExeJhI2puk+uomeKtWrpUVUmQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@3.0.0': + resolution: {integrity: sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.5': + resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/input@5.0.13': + resolution: {integrity: sha512-0l0jCHlJnXIV8CTxwQC0C+5Ziq8WP22edWgmciW2xYvoeoSck4v5FvCS1ctKdqLLR0dUo93uAHgWHywgBSoRyw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.0.13': + resolution: {integrity: sha512-WHmkYnnJAou5gx7RgcvAfUggnHNM1zWfoh0dFPl3dxVssuqt+dK5rIbaOYQXNyOegvFnopbKupjnhw2O8gANNg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.0.13': + resolution: {integrity: sha512-XDGu64ROHZjOOXLAANvJN7iIxWKhOSCG5VakrZ5kaScVR+snVJCFglD/hL3/677awtWcu4pXoWa280CDIYcBeg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.4.3': + resolution: {integrity: sha512-ai5LseTw9HhegupIgmo4cn7RpnCGznjjXu4OI+7jMR8vu7T1ZCCNMzFFAovUCjL1fl0cceksIN1++yQE59SmZw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.2.9': + resolution: {integrity: sha512-a1ErXEfgjfPYpyQ89dp+7n2IISjH9oQg3ygvF5adz8B7aHn4n2PjEgu1wpVTp69K3bj3lVLxP0qJ2b1clk1Whw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.1.9': + resolution: {integrity: sha512-ZlbM28Q9lmLkFPNAIv+ZuY530n5Km8U1WW48oYEvDhe9yc2uL3m3t+JSdRUkQlk5fuIuskgiIVjcb7czFzQpuA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.1.5': + resolution: {integrity: sha512-6SRg6kHfK/sjLXOsuqNebuir+sjwrf/iWuRUnXgB2slzEewppI1WfzeS16XxDcOQmXBruMmmB9Cgrz7wsAxqMg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@4.0.5': + resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@manypkg/find-root@2.2.3': + resolution: {integrity: sha512-jtEZKczWTueJYHjGpxU3KJQ08Gsrf4r6Q2GjmPp/RGk5leeYAA1eyDADSAF+KVCsQ6EwZd/FMcOFCoMhtqdCtQ==} + engines: {node: '>=14.18.0'} + + '@manypkg/get-packages@2.2.2': + resolution: {integrity: sha512-3+Zd8kLZmsyJFmWTBtY0MAuCErI7yKB2cjMBlujvSVKZ2R/BMXi0kjCXu2dtRlSq/ML86t1FkumT0yreQ3n8OQ==} + engines: {node: '>=14.18.0'} + + '@manypkg/tools@1.1.2': + resolution: {integrity: sha512-3lBouSuF7CqlseLB+FKES0K4FQ02JrbEoRtJhxnsyB1s5v4AP03gsoohN8jp7DcOImhaR9scYdztq3/sLfk/qQ==} + engines: {node: '>=14.18.0'} + + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/git@6.0.3': + resolution: {integrity: sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@npmcli/package-json@6.1.1': + resolution: {integrity: sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/promise-spawn@8.0.2': + resolution: {integrity: sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.5': + resolution: {integrity: sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.2.2': + resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/openapi-types@25.0.0': + resolution: {integrity: sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==} + + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.2.3': + resolution: {integrity: sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==} + engines: {node: '>= 18'} + + '@octokit/rest@21.1.1': + resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@14.0.0': + resolution: {integrity: sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/constants@1001.3.1': + resolution: {integrity: sha512-2hf0s4pVrVEH8RvdJJ7YRKjQdiG8m0iAT26TTqXnCbK30kKwJW69VLmP5tED5zstmDRXcOeH5eRcrpkdwczQ9g==} + engines: {node: '>=18.12'} + + '@pnpm/error@1000.1.0': + resolution: {integrity: sha512-Dqc2IJJPjUatwc9Letw+vG29rnaMrDGi5g6WCx1HiZYm0obXbTmLygeRafMbgf+sLKXrWE1shOeiayQuczBdoA==} + engines: {node: '>=18.12'} + + '@pnpm/find-workspace-dir@1000.1.5': + resolution: {integrity: sha512-r1WzYXBD8cqlglOi4ilN9BphX74mJmH2hhiogzYbcNCHhtXnG7tw/9Iq54UGZ+cpDkgGHjL0xLwj9QLUoKJxmg==} + engines: {node: '>=18.12'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@types/cookie@0.4.1': + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/glob@8.1.0': + resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} + + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + + '@types/rimraf@2.0.5': + resolution: {integrity: sha512-YyP+VfeaqAyFmXoTh3HChxOQMyjByRMsHU7kc5KOJkSlXudhMhQIALbYV7rHh/l8d2lX3VUQzprrcAgWdRuU8g==} + + '@types/rimraf@3.0.2': + resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} + + '@types/symlink-or-copy@1.2.2': + resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==} + + '@types/tmp@0.0.33': + resolution: {integrity: sha512-gVC1InwyVrO326wbBZw+AO3u2vRXz/iRWq9jYhpG4W8LXyIgDv3ZmcLQ5Q4Gs+gFMyqx+viFoFT+l3p61QFCmQ==} + + '@ungap/structured-clone@1.2.1': + resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher + + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + + ansi-html@0.0.9: + resolution: {integrity: sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + ansicolors@0.2.1: + resolution: {integrity: sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@2.0.0: + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-transform@2.0.0: + resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==} + engines: {node: '>=8'} + + archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-equal@1.0.2: + resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1@0.1.11: + resolution: {integrity: sha512-Fh9zh3G2mZ8qM/kwsiKwL2U2FmXxVsboP4x1mXjnhKHv3SmzaBZoYvxEQJz/YS2gnCgd8xlAVWcZnQyC9qZBsA==} + engines: {node: '>=0.4.9'} + + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + + assert-plus@0.1.5: + resolution: {integrity: sha512-brU24g7ryhRwGCI2y+1dGQmQXiZF7TtIj583S96y0jjdajIe6wn8BuXyELYhvD22dtIxDQVFk04YTJwwdwOYJw==} + engines: {node: '>=0.8'} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + + async-disk-cache@1.3.5: + resolution: {integrity: sha512-VZpqfR0R7CEOJZ/0FOTgWq70lCrZyS1rkI8PXugDUkTKyyAUgZ2zQ09gLhMkEn+wN8LYeUTPxZdXtlX/kmbXKQ==} + + async-promise-queue@1.0.5: + resolution: {integrity: sha512-xi0aQ1rrjPWYmqbwr18rrSKbSaXIeIwSd1J4KAgVfkq8utNbdZoht7GfvfY6swFUAMJ9obkc4WPJmtGwl+B8dw==} + + async@0.9.2: + resolution: {integrity: sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + atomically@2.1.0: + resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-sign2@0.5.0: + resolution: {integrity: sha512-oqUX0DM5j7aPWPCnpWebiyNIj2wiNI87ZxnOMoGv0aE4TGlBy2N+5iWc6dQ/NOKZaBD2W6PVz8jtOGkWzSC5EA==} + + babel-remove-types@2.0.0: + resolution: {integrity: sha512-HHLQOs/c2h//gl7f5VDa/r1Nd2gIk0kgzC1v01BSCEK5IJu1Zc4/fUj5uYCzC8Q0NWiucCgWKr9a43qmHkY6oA==} + + backbone@1.6.1: + resolution: {integrity: sha512-YQzWxOrIgL6BoFnZjThVN99smKYhyEXXFyJJ2lsF1wJLyo4t+QjmkLrH8/fN22FZ4ykF70Xq7PgTugJVR4zS9Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + + binaryextensions@2.3.0: + resolution: {integrity: sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==} + engines: {node: '>=0.8'} + + blank-object@1.0.2: + resolution: {integrity: sha512-kXQ19Xhoghiyw66CUiGypnuRpWlbHAzY/+NyvqTEdTfhfQGH1/dbEMYiXju7fYKIFePpzp/y9dsu5Cu/PkmawQ==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + engines: {node: '>=18'} + + body@5.1.0: + resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} + + boom@0.4.2: + resolution: {integrity: sha512-OvfN8y1oAxxphzkl2SnCS+ztV/uVKTATtgLjWYg/7KwcNyf3rzpHxNQJZCKtsZd4+MteKczhWbSjtEX4bGgU9g==} + engines: {node: '>=0.8.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + broccoli-caching-writer@3.0.3: + resolution: {integrity: sha512-g644Kb5uBPsy+6e2DvO3sOc+/cXZQQNgQt64QQzjA9TSdP0dl5qvetpoNIx4sy/XIjrPYG1smEidq9Z9r61INw==} + + broccoli-concat@4.2.7: + resolution: {integrity: sha512-JePfBFwHtZ2FR33PBZQA99/hQ4idIbZ205rH84Jw6vgkuKDRVXWVzZP2gvR2WXugXaQ1fj3+yO04b0QsstNHzQ==} + engines: {node: 10.* || >= 12.*} + + broccoli-config-loader@1.0.1: + resolution: {integrity: sha512-MDKYQ50rxhn+g17DYdfzfEM9DjTuSGu42Db37A8TQHQe8geYEcUZ4SQqZRgzdAI3aRQNlA1yBHJfOeGmOjhLIg==} + + broccoli-config-replace@1.1.3: + resolution: {integrity: sha512-gWGS2h/2VyJnD9tI1/HzRsXLOptnt7tu+KLpfPuxd+DBcdswn/i0kyVrTxQpFy+C5eo2hBn672QAEZzf/7LlAA==} + + broccoli-debug@0.6.5: + resolution: {integrity: sha512-RIVjHvNar9EMCLDW/FggxFRXqpjhncM/3qq87bn/y+/zR9tqEkHvTqbyOc4QnB97NO2m6342w4wGkemkaeOuWg==} + + broccoli-funnel-reducer@1.0.0: + resolution: {integrity: sha512-SaOCEdh+wnt2jFUV2Qb32m7LXyElvFwW3NKNaEJyi5PGQNwxfqpkc0KI6AbQANKgdj/40U2UC0WuGThFwuEUaA==} + + broccoli-funnel@2.0.2: + resolution: {integrity: sha512-/vDTqtv7ipjEZQOVqO4vGDVAOZyuYzQ/EgGoyewfOgh1M7IQAToBKZI0oAQPgMBeFPPlIbfMuAngk+ohPBuaHQ==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + broccoli-funnel@3.0.8: + resolution: {integrity: sha512-ng4eIhPYiXqMw6SyGoxPHR3YAwEd2lr9FgBI1CyTbspl4txZovOsmzFkMkGAlu88xyvYXJqHiM2crfLa65T1BQ==} + engines: {node: 10.* || >= 12.*} + + broccoli-kitchen-sink-helpers@0.3.1: + resolution: {integrity: sha512-gqYnKSJxBSjj/uJqeuRAzYVbmjWhG0mOZ8jrp6+fnUIOgLN6MvI7XxBECDHkYMIFPJ8Smf4xaI066Q2FqQDnXg==} + + broccoli-merge-trees@3.0.2: + resolution: {integrity: sha512-ZyPAwrOdlCddduFbsMyyFzJUrvW6b04pMvDiAQZrCwghlvgowJDY+EfoXn+eR1RRA5nmGHJ+B68T63VnpRiT1A==} + engines: {node: '>=6.0.0'} + + broccoli-merge-trees@4.2.0: + resolution: {integrity: sha512-nTrQe5AQtCrW4enLRvbD/vTLHqyW2tz+vsLXQe4IEaUhepuMGVKJJr+I8n34Vu6fPjmPLwTjzNC8izMIDMtHPw==} + engines: {node: 10.* || >= 12.*} + + broccoli-middleware@2.1.2: + resolution: {integrity: sha512-hdJ5mPwvsQI/eDZbpztfaA0DNINqp/aHzEz4lPG8WCVOXUfbFdbiWO7nMu3v+mmwTcgRD2e8I4DVQ9J2AoYnPQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + broccoli-node-api@1.7.0: + resolution: {integrity: sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==} + + broccoli-node-info@1.1.0: + resolution: {integrity: sha512-DUohSZCdfXli/3iN6SmxPbck1OVG8xCkrLx47R25his06xVc1ZmmrOsrThiM8BsCWirwyocODiYJqNP5W2Hg1A==} + engines: {node: '>= 0.10.0'} + + broccoli-node-info@2.2.0: + resolution: {integrity: sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==} + engines: {node: 8.* || >= 10.*} + + broccoli-output-wrapper@3.2.5: + resolution: {integrity: sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==} + engines: {node: 10.* || >= 12.*} + + broccoli-persistent-filter@2.3.1: + resolution: {integrity: sha512-hVsmIgCDrl2NFM+3Gs4Cr2TA6UPaIZip99hN8mtkaUPgM8UeVnCbxelCvBjUBHo0oaaqP5jzqqnRVvb568Yu5g==} + engines: {node: 6.* || >= 8.*} + + broccoli-plugin@1.3.1: + resolution: {integrity: sha512-DW8XASZkmorp+q7J4EeDEZz+LoyKLAd2XZULXyD9l4m9/hAKV3vjHmB1kiUshcWAYMgTP1m2i4NnqCE/23h6AQ==} + + broccoli-plugin@2.1.0: + resolution: {integrity: sha512-ElE4caljW4slapyEhSD9jU9Uayc8SoSABWdmY9SqbV8DHNxU6xg1jJsPcMm+cXOvggR3+G+OXAYQeFjWVnznaw==} + engines: {node: 6.* || 8.* || >= 10.*} + + broccoli-plugin@4.0.7: + resolution: {integrity: sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==} + engines: {node: 10.* || >= 12.*} + + broccoli-slow-trees@3.1.0: + resolution: {integrity: sha512-FRI7mRTk2wjIDrdNJd6znS7Kmmne4VkAkl8Ix1R/VoePFMD0g0tEl671xswzFqaRjpT9Qu+CC4hdXDLDJBuzMw==} + + broccoli-source@1.1.0: + resolution: {integrity: sha512-ahvqmwF6Yvh6l+sTJJdey4o4ynwSH8swSSBSGmUXGSPPCqBWvquWB/4rWN65ZArKilBFq/29O0yQnZNIf//sTg==} + + broccoli-source@3.0.1: + resolution: {integrity: sha512-ZbGVQjivWi0k220fEeIUioN6Y68xjMy0xiLAc0LdieHI99gw+tafU8w0CggBDYVNsJMKUr006AZaM7gNEwCxEg==} + engines: {node: 8.* || 10.* || >= 12.*} + + broccoli-stew@3.0.0: + resolution: {integrity: sha512-NXfi+Vas24n3Ivo21GvENTI55qxKu7OwKRnCLWXld8MiLiQKQlWIq28eoARaFj0lTUFwUa4jKZeA7fW9PiWQeg==} + engines: {node: 8.* || >= 10.*} + + broccoli-test-helper@2.0.0: + resolution: {integrity: sha512-TKwh8dBT+RcxKEG+vAoaRRhZsCMwZIHPZbCzBNCA0nUi1aoFB/LVosqwMC6H9Ipe06FxY5hpQxDLFbnBMdUPsA==} + engines: {node: 6.* || 8.* || >= 10.*} + + broccoli@2.3.0: + resolution: {integrity: sha512-TeYMYlCGFK8EGk4Wce1G1uU3i52+YxRqP3WPOVDojC1zUk+Gi40wHBzUT2fncQZDl26dmCQMNugtHKjvUpcGQg==} + engines: {node: '>= 6'} + + broccoli@4.0.0: + resolution: {integrity: sha512-p5el5/ig0QeRGFPkLMPdm7KblkTm44eicEWfwnRTz6hncghVuRZ0+XDAtCi7ynxobeE/mey5Q7lAulFkgNzxVA==} + engines: {node: '>= 20.19.*'} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserslist@4.24.3: + resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + bufferutil@4.0.8: + resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} + engines: {node: '>=6.14.2'} + + bytes@1.0.0: + resolution: {integrity: sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + + caching-transform@4.0.0: + resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} + engines: {node: '>=8'} + + calculate-cache-key-for-tree@2.0.0: + resolution: {integrity: sha512-Quw8a6y8CPmRd6eU+mwypktYCwUcf8yVFIRbNZ6tPQEckX9yd+EBVEPC/GSZZrMWH9e7Vz4pT7XhpmyApRByLQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + can-symlink@1.0.0: + resolution: {integrity: sha512-RbsNrFyhwkx+6psk/0fK/Q9orOUr9VMxohGd8vTa4djf4TGLfblBgUfqZChrZuW0Q+mz2eBPFLusw9Jfukzmhg==} + hasBin: true + + caniuse-lite@1.0.30001689: + resolution: {integrity: sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==} + + capture-exit@2.0.0: + resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} + engines: {node: 6.* || 8.* || >= 10.*} + + cardinal@1.0.0: + resolution: {integrity: sha512-INsuF4GyiFLk8C91FPokbKTc/rwHqV4JnfatVZ6GPhguP1qmkRWX2dp5tepYboYdPpGWisLVLI+KsXoXFPRSMg==} + hasBin: true + + chai-as-promised@6.0.0: + resolution: {integrity: sha512-Zf5Dq6p4d0pApi662BtRe95oKYbEyNb+TLbIdwVSlewYxVhtMYwrTD3TAmcaf1XanuBw7egusnLxLXlMnv0myw==} + peerDependencies: + chai: '>= 2.1.2 < 4' + + chai-as-promised@7.1.2: + resolution: {integrity: sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==} + peerDependencies: + chai: '>= 2.1.2 < 6' + + chai-as-promised@8.0.2: + resolution: {integrity: sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==} + peerDependencies: + chai: '>= 2.1.2 < 7' + + chai-files@1.4.0: + resolution: {integrity: sha512-tPTx7H2kpR+wILWHRx8RxpXcRUdc2uH8su505C9R3p5GA+eYbZBXuxWC0RZbyElYi7X7Fp/V/S2PQjkakrT1mQ==} + + chai-jest-snapshot@2.0.0: + resolution: {integrity: sha512-u8jZZjw/0G1t5A8wDfH6K7DAVfMg3g0dsw9wKQURNUyrZX96VojHNrFMmLirq1m0kOvC5icgL/Qh/fu1MZyvUw==} + peerDependencies: + chai: '>=1.9.0' + + chai@3.5.0: + resolution: {integrity: sha512-eRYY0vPS2a9zt5w5Z0aCeWbrXTEyvk7u/Xf71EzNObrjSCPgMm1Nku/D/u2tiqHBX5j40wWhj54YJLtgn8g55A==} + engines: {node: '>= 0.4.0'} + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + charm@1.0.2: + resolution: {integrity: sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw==} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + + clean-base-url@1.0.0: + resolution: {integrity: sha512-9q6ZvUAhbKOSRFY7A/irCQ/rF0KIpa3uXpx6izm8+fp7b2H4hLeUJ+F1YYk9+gDQ/X8Q0MEyYs+tG3cht//HTg==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + clean-up-path@1.0.0: + resolution: {integrity: sha512-PHGlEF0Z6976qQyN6gM7kKH6EH0RdfZcc8V+QhFe36eRxV0SMH5OUBZG7Bxa9YcreNzyNbK63cGiZxdSZgosRw==} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} + engines: {node: '>= 0.2.0'} + + cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + codsen-utils@1.7.3: + resolution: {integrity: sha512-YIFQQ1n2NSgwoB3sCe7RpkZzsrPxTMek6jc7wC9fXOm1wwfWAKja9gLOMEjlXOUd3LKV3o6Jci7n9BoHs5Z8Sg==} + engines: {node: '>=14.18.0'} + + collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} + + combined-stream@0.0.7: + resolution: {integrity: sha512-qfexlmLp9MyrkajQVyjEDb0Vj+KhRgR/rxLiVhaihlT+ZkX0lReqtH6Ack40CvMDERR4b5eFp3CreskpBs1Pig==} + engines: {node: '>= 0.8'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@8.0.0: + resolution: {integrity: sha512-U51WPZg+o6FDFhBCV6yYFEspGMCoHvCgJSGFw9AwsW2u75gYBPoDlYca5XfDs4TdMUu3/y0R6FFdvvgyH31mKQ==} + engines: {node: '>=20'} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + console-ui@3.1.2: + resolution: {integrity: sha512-+5j3R4wZJcEYZeXk30whc4ZU/+fWW9JMTNntVuMYpjZJ9n26Cxr0tUBXco1NRjVZRpRVvZ4DDKKKIHNYeUG9Dw==} + engines: {node: 6.* || 8.* || >= 10.*} + + consolidate@1.0.4: + resolution: {integrity: sha512-RuZ3xnqEDsxiwaoIkqVeeK3gg9qxw7+YKYX2tKhLs1eukVKMgSr4VYI3iYFsRHi4TloHYDlugrz3kvkjs3nynA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.22.5 + arc-templates: ^0.5.3 + atpl: '>=0.7.6' + bracket-template: ^1.1.5 + coffee-script: ^1.12.7 + dot: ^1.1.3 + dust: ^0.3.0 + dustjs-helpers: ^1.7.4 + dustjs-linkedin: ^2.7.5 + eco: ^1.1.0-rc-3 + ect: ^0.5.9 + ejs: ^3.1.5 + haml-coffee: ^1.14.1 + hamlet: ^0.3.3 + hamljs: ^0.6.2 + handlebars: ^4.7.6 + hogan.js: ^3.0.2 + htmling: ^0.0.8 + jazz: ^0.0.18 + jqtpl: ~1.1.0 + just: ^0.1.8 + liquid-node: ^3.0.1 + liquor: ^0.0.5 + lodash: ^4.17.20 + mote: ^0.2.0 + mustache: ^4.0.1 + nunjucks: ^3.2.2 + plates: ~0.4.11 + pug: ^3.0.0 + qejs: ^3.0.5 + ractive: ^1.3.12 + react: '>=16.13.1' + react-dom: '>=16.13.1' + slm: ^2.0.0 + swig: ^1.4.2 + swig-templates: ^2.0.3 + teacup: ^2.0.0 + templayed: '>=0.2.3' + then-pug: '*' + tinyliquid: ^0.2.34 + toffee: ^0.3.6 + twig: ^1.15.2 + twing: ^5.0.2 + underscore: ^1.11.0 + vash: ^0.13.0 + velocityjs: ^2.0.1 + walrus: ^0.10.1 + whiskers: ^0.4.0 + peerDependenciesMeta: + '@babel/core': + optional: true + arc-templates: + optional: true + atpl: + optional: true + bracket-template: + optional: true + coffee-script: + optional: true + dot: + optional: true + dust: + optional: true + dustjs-helpers: + optional: true + dustjs-linkedin: + optional: true + eco: + optional: true + ect: + optional: true + ejs: + optional: true + haml-coffee: + optional: true + hamlet: + optional: true + hamljs: + optional: true + handlebars: + optional: true + hogan.js: + optional: true + htmling: + optional: true + jazz: + optional: true + jqtpl: + optional: true + just: + optional: true + liquid-node: + optional: true + liquor: + optional: true + lodash: + optional: true + mote: + optional: true + mustache: + optional: true + nunjucks: + optional: true + plates: + optional: true + pug: + optional: true + qejs: + optional: true + ractive: + optional: true + react: + optional: true + react-dom: + optional: true + slm: + optional: true + swig: + optional: true + swig-templates: + optional: true + teacup: + optional: true + templayed: + optional: true + then-pug: + optional: true + tinyliquid: + optional: true + toffee: + optional: true + twig: + optional: true + twing: + optional: true + underscore: + optional: true + vash: + optional: true + velocityjs: + optional: true + walrus: + optional: true + whiskers: + optional: true + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-tag@4.2.0: + resolution: {integrity: sha512-f/o+F3qSa4gg23I7RWy6cMDxP2nPo99YWusxw2bjne7ZC6Acqqf4uB/+87AekOq1ehTocHH7b7nMd2X4S3NHVw==} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + continuable-cache@0.3.1: + resolution: {integrity: sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + + core-object@3.1.5: + resolution: {integrity: sha512-sA2/4+/PZ/KV6CKgjrVrrUVBKCkdDO02CUlQ0YKTQoYUwPYNOtOAcWlbYhd5v/1JqYaA6oZ4sDlOU4ppVw6Wbg==} + engines: {node: '>= 4'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cryptiles@0.2.2: + resolution: {integrity: sha512-gvWSbgqP+569DdslUiCelxIv3IYK5Lgmq1UrRnk+s1WxQOQ16j3GPDcjdtgL5Au65DU/xQi6q3xPtf5Kta+3IQ==} + engines: {node: '>=0.8.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + + cssstyle@3.0.0: + resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} + engines: {node: '>=14'} + + ctype@0.5.3: + resolution: {integrity: sha512-T6CEkoSV4q50zW3TlTHMbzy1E5+zlnNcY+yb7tWVYlTwPhx9LpnfAkd4wecpWknDyptp4k97LUZeInlf6jdzBg==} + engines: {node: '>= 0.4'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + dag-map@2.0.2: + resolution: {integrity: sha512-xnsprIzYuDeiyu5zSKwilV/ajRHxnoMlAhEREfyfTgTSViMVY2fGP1ZcHJbtwup26oCkofySU/m6oKJ3HrkW7w==} + + data-urls@4.0.0: + resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} + engines: {node: '>=14'} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-eql@0.1.3: + resolution: {integrity: sha512-6sEotTRGBFiNcqVoeHwnfopbSpi5NbH1VWJmYCVkmxMmaVTT0bUTrNaGyBwhgP4MZL012W/mkzIn3Da+iDYweg==} + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-require-extensions@3.0.1: + resolution: {integrity: sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==} + engines: {node: '>=8'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + + delayed-stream@0.0.5: + resolution: {integrity: sha512-v+7uBd1pqe5YtgPacIIbZ8HuHeLFVNe4mUEyFDXL6KiqzEykjbw+5mXZXpGFgNVasdL4jWKgaKIXrEHiynN1LA==} + engines: {node: '>=0.4.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + + detect-indent@7.0.2: + resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} + engines: {node: '>=12.20'} + + detect-newline@4.0.1: + resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff@3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + + dot-prop@10.1.0: + resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} + engines: {node: '>=20'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editions@1.3.4: + resolution: {integrity: sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==} + engines: {node: '>=0.8'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.74: + resolution: {integrity: sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==} + + ember-cli-blueprint-test-helpers@0.19.2: + resolution: {integrity: sha512-otCKdGcNFK0+MkQo+LLjYbRD9EerApH6Z/odvvlL1hxrN+owHMV5E+jI2rbtdvNEH0/6w5ZqjH4kS232fvtCxQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-internal-test-helpers@0.9.1: + resolution: {integrity: sha512-ia+p7LrAe2tENG+Vewdi93kGlsI7OkjB7tEakTtCELkIvZpmPX+uYGhIi5nVOynLiej2M81MQmNqB8jX93ejqQ==} + + ember-cli-is-package-missing@1.0.0: + resolution: {integrity: sha512-9hEoZj6Au5onlSDdcoBqYEPT8ehlYntZPxH8pBKV0GO7LNel88otSAQsCfXvbi2eKE+MaSeLG/gNaCI5UdWm9g==} + + ember-cli-normalize-entity-name@1.0.0: + resolution: {integrity: sha512-rF4P1rW2P1gVX1ynZYPmuIf7TnAFDiJmIUFI1Xz16VYykUAyiOCme0Y22LeZq8rTzwBMiwBwoE3RO4GYWehXZA==} + + ember-cli-preprocess-registry@5.0.1: + resolution: {integrity: sha512-Jb2zbE5Kfe56Nf4IpdaQ10zZ72p/RyLdgE5j5/lKG3I94QHlq+7AkAd18nPpb5OUeRUT13yQTAYpU+MbjpKTtg==} + engines: {node: 16.* || >= 18} + + ember-cli-string-utils@1.1.0: + resolution: {integrity: sha512-PlJt4fUDyBrC/0X+4cOpaGCiMawaaB//qD85AXmDRikxhxVzfVdpuoec02HSiTGTTB85qCIzWBIh8lDOiMyyFg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.2: + resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + ensure-posix-path@1.1.1: + resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} + + entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + error@7.2.1: + resolution: {integrity: sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==} + + es-abstract@1.23.6: + resolution: {integrity: sha512-Ifco6n3yj2tMZDWNLyloZrytt9lqqlwvS83P3HtaETR0NUOYnIULGGHpktqYGObGy+8wc1okO25p8TjemhImvA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-chai-expect@3.1.0: + resolution: {integrity: sha512-a9F8b38hhJsR7fgDEfyMxppZXCnCW6OOHj7cQfygsm9guXqdSzfpwrHX5FT93gSExDqD71HQglF1lLkGBwhJ+g==} + engines: {node: 10.* || 12.* || || 14.* || 16.* || >= 18.*} + peerDependencies: + eslint: '>=2.0.0 <= 9.x' + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-mocha@10.5.0: + resolution: {integrity: sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==} + engines: {node: '>=14.0.0'} + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-n@17.24.0: + resolution: {integrity: sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-utils@3.0.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@3.0.0: + resolution: {integrity: sha512-xoBq/MIShSydNZOkjkoCEjqod963yHNXTLC40ypBhop6yPqflPz/vTinmCfSrGcywVLnSftRf6a0kJLdFdzemw==} + engines: {node: '>=0.10.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events-to-array@2.0.3: + resolution: {integrity: sha512-f/qE2gImHRa4Cp2y1stEOSgw8wTFyUdVJX7G//bMwbaV9JqISFxg99NbmVQeP7YLnDUZ2un851jlaDrlpmGehQ==} + engines: {node: '>=12'} + + exec-sh@0.3.6: + resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + exists-sync@0.0.3: + resolution: {integrity: sha512-/qPB5E0cRuA/Cs5vHrmKYSfhIBCPJs9Vm3e9aIejMwwbe6idMeNbGu1g5stvr/bXT6HywHckLPEkmY7HK6FlwA==} + deprecated: Please replace with usage of fs.existsSync + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + + extract-stack@2.0.0: + resolution: {integrity: sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==} + engines: {node: '>=8'} + + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-ordered-set@1.0.3: + resolution: {integrity: sha512-MxBW4URybFszOx1YlACEoK52P6lE3xiFcPaGCUZ7QQOZ6uJXKo++Se8wa31SjcZ+NC/fdAWX7UtKEfaGgHS2Vg==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-sourcemap-concat@2.1.1: + resolution: {integrity: sha512-7h9/x25c6AQwdU3mA8MZDUMR3UCy50f237egBrBkuwjnUZSmfu4ptCf91PZSKzON2Uh5VvIHozYKWcPPgcjxIw==} + engines: {node: 10.* || >= 12.*} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + filesize@11.0.17: + resolution: {integrity: sha512-oHLTvMLw6imZUl1se/RBQrFlyy50nXce4sU7yGR6Qc0JgCwqnfiFsAnEwotdGmfKLD7SArGUk2/5STU0k8LOBQ==} + engines: {node: '>= 10.8.0'} + + fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-index@1.1.1: + resolution: {integrity: sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@8.0.0: + resolution: {integrity: sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==} + engines: {node: '>=20'} + + find-yarn-workspace-root@2.0.0: + resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + + findup-sync@2.0.0: + resolution: {integrity: sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==} + engines: {node: '>= 0.10'} + + findup-sync@5.0.0: + resolution: {integrity: sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==} + engines: {node: '>= 10.13.0'} + + fixturify-project@2.1.1: + resolution: {integrity: sha512-sP0gGMTr4iQ8Kdq5Ez0CVJOZOGWqzP5dv/veOTdFNywioKjkNWCHBi1q65DMpcNGUGeoOUWehyji274Q2wRgxA==} + engines: {node: 10.* || >= 12.*} + + fixturify@0.3.4: + resolution: {integrity: sha512-Gx+KSB25b6gMc4bf7UFRTA85uE0iZR+RYur0JHh6dg4AGBh0EksOv4FCHyM7XpGmiJO7Bc7oV7vxENQBT+2WEQ==} + + fixturify@2.1.1: + resolution: {integrity: sha512-SRgwIMXlxkb6AUgaVjIX+jCEqdhyXu9hah7mcK+lWynjKtX73Ux1TDv71B7XyaQ+LJxkYRHl5yCL8IycAvQRUw==} + engines: {node: 10.* || >= 12.*} + + fixturify@3.0.0: + resolution: {integrity: sha512-PFOf/DT9/t2NCiVyiQ5cBMJtGZfWh3aeOV8XVqQQOPBlTv8r6l0k75/hm36JOaiJlrWFk/8aYFyOKAvOkrkjrw==} + engines: {node: 14.* || >= 16.*} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + foreground-child@2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forever-agent@0.5.2: + resolution: {integrity: sha512-PDG5Ef0Dob/JsZUxUltJOhm/Y9mlteAE+46y3M9RBz/Rd3QVENJ75aGRhN56yekTUboaBIkd8KVWX2NjF6+91A==} + + form-data@0.1.4: + resolution: {integrity: sha512-x8eE+nzFtAMA0YYlSxf/Qhq6vP1f8wSoZ7Aw1GuctBcmudCNuTUmmx45TfEplyb6cjsZO/jvh6+1VpZn24ez+w==} + engines: {node: '>= 0.8'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fromentries@1.3.2: + resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} + + fs-extra@0.24.0: + resolution: {integrity: sha512-w1RvhdLZdU9V3vQdL+RooGlo6b9R9WVoBanOfoJvosWlqSKvrjFlci2oVhwvLwZXBtM7khyPvZ8r3fwsim3o0A==} + + fs-extra@0.30.0: + resolution: {integrity: sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + engines: {node: '>=14.14'} + + fs-extra@4.0.3: + resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} + + fs-extra@5.0.0: + resolution: {integrity: sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-merger@3.2.1: + resolution: {integrity: sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-sync@1.0.6: + resolution: {integrity: sha512-OgbfyvmGVryknZfDXVVhua6OW8946R+AF3O2xxrCW/XFxCYZ4CO2Jrl7kYhrpjZLYvB9gxvWpLikEc9YL9HzCA==} + + fs-tree-diff@0.5.9: + resolution: {integrity: sha512-872G8ax0kHh01m9n/2KDzgYwouKza0Ad9iFltBpNykvROvf2AGtoOzPJgGx125aolGPER3JuC7uZFrQ7bG1AZw==} + + fs-tree-diff@2.0.1: + resolution: {integrity: sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==} + engines: {node: 6.* || 8.* || >= 10.*} + + fs-updater@1.0.4: + resolution: {integrity: sha512-0pJX4mJF/qLsNEwTct8CdnnRdagfb+LmjRPJ8sO+nCnAZLW0cTmz4rTgU25n+RvTuWSITiLKrGVJceJPBIPlKg==} + engines: {node: '>=6.0.0'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.7: + resolution: {integrity: sha512-2g4x+HqTJKM9zcJqBSpjoRmdcPFtJM60J3xJisTQSXBWka5XqyBN/2tNUgma1mztTXyDuUsEtYe5qcs7xYzYQA==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.2.6: + resolution: {integrity: sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + git-hooks-list@3.1.0: + resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} + + git-hooks-list@4.1.1: + resolution: {integrity: sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==} + + git-repo-info@2.1.1: + resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==} + engines: {node: '>= 4.0'} + + github-changelog@2.1.4: + resolution: {integrity: sha512-mZQF/YC9OR8XMGpYlLQqG66RiKwlaQZ7rXTZug28oOYkzOXd0acszuvYHVzPlYmns1aDDSwQkbzFxNOYpWhmig==} + engines: {node: 18.* || 20.* || >= 22} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@5.0.15: + resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + + has-ansi@3.0.0: + resolution: {integrity: sha512-5JRDTvNq6mVkaMHQVXrGnaCXHD6JfqxwCy8LA/DQSqLLqePR9uaJVm2u3Ek/UziJFQz+d1ul99RtfIhE2aorkQ==} + engines: {node: '>=4'} + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + + hash-for-dep@1.5.1: + resolution: {integrity: sha512-/dQ/A2cl7FBPI2pO0CANkvuuVi/IFS5oTyJ0PsOb6jW6WbVW1js5qJXMJTNbWHXBIPdFTWFbabjB+mE0d+gelw==} + + hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hawk@1.1.1: + resolution: {integrity: sha512-am8sVA2bCJIw8fuuVcKvmmNnGFUGW8spTkVtj2fXTEZVkfN42bwFZFtDem57eFi+NSxurJB8EQ7Jd3uCHLn8Vw==} + engines: {node: '>=0.8.0'} + deprecated: This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + heimdalljs-fs-monitor@1.1.2: + resolution: {integrity: sha512-M7OPf3Tu+ybhAXdiC07O1vUYFyhCgfew4L3vaG2nn4Be05xzNvtBcU6IKMTfHJ9AxWFa3w9rrmiJovkxHhpopw==} + + heimdalljs-graph@1.0.0: + resolution: {integrity: sha512-v2AsTERBss0ukm/Qv4BmXrkwsT5x6M1V5Om6E8NcDQ/ruGkERsfsuLi5T8jx8qWzKMGYlwzAd7c/idymxRaPzA==} + engines: {node: 8.* || >= 10.*} + + heimdalljs-logger@0.1.10: + resolution: {integrity: sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==} + + heimdalljs@0.2.6: + resolution: {integrity: sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hoek@0.9.1: + resolution: {integrity: sha512-ZZ6eGyzGjyMTmpSPYVECXy9uNfqBR7x5CavhUaLOeD6W0vWK1mp/b7O3f86XE0Mtfo9rZ6Bh3fnuw9Xr8MF9zA==} + engines: {node: '>=0.8.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hosted-git-info@8.0.2: + resolution: {integrity: sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==} + engines: {node: ^18.17.0 || >=20.5.0} + + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http-signature@0.10.1: + resolution: {integrity: sha512-coK8uR5rq2IMj+Hen+sKPA5ldgbCc1/spPdKCL1Fw6h+D0s/2LzMcRK0Cqufs1h0ryx/niwBHGFu8HC3hwU+lA==} + engines: {node: '>=0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflection@3.0.2: + resolution: {integrity: sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==} + engines: {node: '>=18.0.0'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@5.0.0: + resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} + engines: {node: ^18.17.0 || >=20.5.0} + + inquirer@13.4.3: + resolution: {integrity: sha512-EPd3IqieHSavSOXh+LZhrIkdQcOELWeRblLT6kslQr+cF9XTh/HxZdSt1YkHH1iq4dvqBnV42uwg2YlorgOy6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + inquirer@6.5.2: + resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} + engines: {node: '>=6.0.0'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + invert-kv@3.0.1: + resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==} + engines: {node: '>=8'} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.1: + resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-git-url@1.0.0: + resolution: {integrity: sha512-UCFta9F9rWFSavp9H3zHEHrARUfZbdJvmHKeEpds4BK3v7W2LdXoNypMtXXi5w5YBDEBCTYmbI+vsSwI8LYJaQ==} + engines: {node: '>=0.8'} + + is-glob@3.1.0: + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-language-code@5.1.3: + resolution: {integrity: sha512-LI43ua9ZYquG9kxzUl3laVQ2Ly8VGGr8vOfYv64DaK3uOGejz6ANDzteOvZlgPT40runzARzRMQZnRZg99ZW4g==} + engines: {node: '>=14.18.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + is-safe-filename@0.1.1: + resolution: {integrity: sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==} + engines: {node: '>=20'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.14: + resolution: {integrity: sha512-lQUsHzcTb7rH57dajbOuZEuMDXjs9f04ZloER4QOpjpKcaw4f98BRUrs8aiO9Z4G7i7B0Xhgarg6SCgYcYi8Nw==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.0: + resolution: {integrity: sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-hook@3.0.0: + resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-processinfo@2.0.3: + resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + istextorbinary@2.1.0: + resolution: {integrity: sha512-kT1g2zxZ5Tdabtpp9VSdOzW9lb6LXImyWbzbQeTxoRtHhurC9Ej9Wckngr2+uepPL09ky/mJHmN9jeJPML5t6A==} + engines: {node: '>=0.12'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jest-diff@21.2.1: + resolution: {integrity: sha512-E5fu6r7PvvPr5qAWE1RaUwIh/k6Zx/3OOkZ4rk5dBJkEWRrUuSgbMt2EO8IUTPTd6DOqU3LW6uTIwX5FRvXoFA==} + + jest-get-type@21.2.0: + resolution: {integrity: sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==} + + jest-matcher-utils@21.2.1: + resolution: {integrity: sha512-kn56My+sekD43dwQPrXBl9Zn9tAqwoy25xxe7/iY4u+mG8P3ALj5IK7MLHZ4Mi3xW7uWVCjGY8cm4PqgbsqMCg==} + + jest-snapshot@21.2.1: + resolution: {integrity: sha512-bpaeBnDpdqaRTzN8tWg0DqOTo2DvD3StOemxn67CUd1p1Po+BUpvePAp44jdJ7Pxcjfg+42o4NHw1SxdCA2rvg==} + + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsdom@21.1.2: + resolution: {integrity: sha512-sCpFmK2jv+1sjff4u7fzft+pUh2KSUbUrEHYHyfSIbGTIcmnjyp83qg6qLwdJ/I3LpTXx33ACxeRL7Lsyc6lGQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stable-stringify@1.2.0: + resolution: {integrity: sha512-ex8jk9BZHBolvbd5cRnAgwyaYcYB0qZldy1e+LCOdcF6+AUmVZ6LcGUMzsRTW83QMeu+GxZGrcLqxqrgfXGvIw==} + engines: {node: '>= 0.4'} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@2.4.0: + resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + klaw@1.3.1: + resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} + + ky@1.8.1: + resolution: {integrity: sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw==} + engines: {node: '>=18'} + + latest-version@9.0.0: + resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} + engines: {node: '>=18'} + + lcid@3.1.1: + resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==} + engines: {node: '>=8'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + linkify-it@1.2.4: + resolution: {integrity: sha512-eGHwtlABkp1NOJSiKUNqBf3SYAS5jPHtvRXPAgNaQwTqmkTahjtiLH9NtxdR5IOPhNvwNMN/diswSfZKzUkhGg==} + + linkify-it@2.2.0: + resolution: {integrity: sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + livereload-js@3.4.1: + resolution: {integrity: sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@8.0.0: + resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==} + engines: {node: '>=20'} + + lodash.flattendeep@4.4.0: + resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.values@4.3.0: + resolution: {integrity: sha512-r0RwvdCv8id9TUblb/O7rYPwVy6lerCbcawrfdo9iC/1t1wsNMJknO79WNBgwkH0hIeJ08jmvvESbFpNb4jH0Q==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + + markdown-it-terminal@0.4.0: + resolution: {integrity: sha512-NeXtgpIK6jBciHTm9UhiPnyHDdqyVIdRPJ+KdQtZaf/wR74gvhCNbw5li4TYsxRp5u3ZoHEF4DwpECeZqyCw+w==} + peerDependencies: + markdown-it: '>= 13.0.0' + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + markdown-it@4.4.0: + resolution: {integrity: sha512-Rl8dHHeLuAh3E72OPY0tY7CLvlxgHiLhlshIYswAAabAg4YDBLa6e/LTgNkkxBO2K61ESzoquPQFMw/iMrT1PA==} + hasBin: true + + markdown-it@8.4.2: + resolution: {integrity: sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==} + hasBin: true + + matcher-collection@1.1.2: + resolution: {integrity: sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g==} + + matcher-collection@2.0.1: + resolution: {integrity: sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + math-intrinsics@1.0.0: + resolution: {integrity: sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==} + engines: {node: '>= 0.4'} + + mdn-links@0.1.0: + resolution: {integrity: sha512-m+gI2Hrgro1O0SwqHd9cFkqN8VGzP56eprB63gxu6z9EFQDMeaR083wcNqMVADIbgiMP/TOCCe0ZIXHLBv2tUg==} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memory-streams@0.1.3: + resolution: {integrity: sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge-trees@2.0.0: + resolution: {integrity: sha512-5xBbmqYBalWqmhYm51XlohhkmVOua3VAUrrWh8t9iOkaLpS6ifqm/UVuUjQCeDVJ9Vx3g2l6ihfkbLSTeKsHbw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@1.0.2: + resolution: {integrity: sha512-echfutj/t5SoTL4WZpqjA1DCud1XO0WQF3/GJ48YBmc4ZMhCK77QA6Z/w6VTQERLKuJ4drze3kw2TUT8xZXVNw==} + engines: {node: '>= 0.8.0'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.2.11: + resolution: {integrity: sha512-Ysa2F/nqTNGHhhm9MV8ure4+Hc+Y8AWiqUdHxsO7xu8zc92ND9f3kpALHjaP026Ft17UfxrMt95c50PLUeynBw==} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mktemp@2.0.2: + resolution: {integrity: sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g==} + engines: {node: 20 || 22 || 24} + + mocha@11.7.6: + resolution: {integrity: sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + nock@14.0.15: + resolution: {integrity: sha512-S0a47C9pLvcYx/Ugf0H30BVBEcUgMMBDk9VJIDlJ8XGrfH2QDUD4Tgdp45qDIiHttokBG+IbsOtsvIjGR/j3bg==} + engines: {node: '>=18.20.0 <20 || >=20.12.1'} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + + node-preload@0.2.1: + resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==} + engines: {node: '>=8'} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + node-uuid@1.4.8: + resolution: {integrity: sha512-TkCET/3rr9mUuRp+CpO7qfgT++aAxfDRaalQhwPFzI9BY/2rCDn6OfpZOVggi1AXfTPpfkTrg5f5WQx5G1uLxA==} + deprecated: Use uuid module instead + hasBin: true + + nopt@3.0.6: + resolution: {integrity: sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==} + hasBin: true + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-install-checks@7.1.1: + resolution: {integrity: sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-package-arg@12.0.2: + resolution: {integrity: sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-package-arg@13.0.2: + resolution: {integrity: sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-pick-manifest@10.0.0: + resolution: {integrity: sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + + nyc@17.1.0: + resolution: {integrity: sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==} + engines: {node: '>=18'} + hasBin: true + + oauth-sign@0.3.0: + resolution: {integrity: sha512-Tr31Sh5FnK9YKm7xTUPyDMsNOvMqkVDND0zvK/Wgj7/H9q8mpye0qG2nVzrnsvLhcsX5DtqXD0la0ks6rkPCGQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + + os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + + os-locale@6.0.2: + resolution: {integrity: sha512-qIb8bzRqaN/vVqEYZ7lTAg6PonskO7xOmM7OClD28F6eFa4s5XGe4bGpHUHMoCHbNNuR0pDYFeSLiW5bnjWXIA==} + engines: {node: '>=12.20'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + osenv@0.1.5: + resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} + deprecated: This package is no longer supported. + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + p-defer@4.0.1: + resolution: {integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==} + engines: {node: '>=12'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-hash@4.0.0: + resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==} + engines: {node: '>=8'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-json@10.0.1: + resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} + engines: {node: '>=18'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-github-repo-url@1.4.1: + resolution: {integrity: sha512-bSWyzBKqcSL4RrncTpGsEKoJ7H8a4L3++ifTAbTFeMHyq2wRV+42DGmQcHIrJIvdcacjIOxEuKH/w4tthF17gg==} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-posix@1.0.0: + resolution: {integrity: sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==} + + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + portfinder@1.0.38: + resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} + engines: {node: '>= 10.12'} + + posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@21.2.1: + resolution: {integrity: sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==} + + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} + + printf@0.6.1: + resolution: {integrity: sha512-is0ctgGdPJ5951KulgfzvHGwJtZ5ck8l042vRkV6jrkpBzTmb/lueTqguWHy2JfVA+RY6gFVlaZgUS0j7S/dsw==} + engines: {node: '>= 0.9.0'} + + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-on-spawn@1.1.0: + resolution: {integrity: sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==} + engines: {node: '>=8'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-map-series@0.2.3: + resolution: {integrity: sha512-wx9Chrutvqu1N/NHzTayZjE1BgIwt6SJykQoCOic4IZ9yUDjKyVYrpLa/4YCNsV61eRENfs29hrEquVuB13Zlw==} + + promise-map-series@0.3.0: + resolution: {integrity: sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==} + engines: {node: 10.* || >= 12.*} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + promise.hash.helper@1.0.8: + resolution: {integrity: sha512-KYcnXctWUWyVD3W3Ye0ZDuA1N8Szrh85cVCxpG6xYrOk/0CttRtYCmU30nWsUch0NuExQQ63QXvzRE6FLimZmg==} + engines: {node: 10.* || >= 12.*} + + promise.prototype.finally@3.1.8: + resolution: {integrity: sha512-aVDtsXOml9iuMJzUco9J1je/UrIT3oMYfWkCTiUhkt+AvZw72q4dUZnR/R/eB3h5GeAagQVXvM1ApoYniJiwoA==} + engines: {node: '>= 0.4'} + + propagate@2.0.1: + resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} + engines: {node: '>= 8'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@1.0.2: + resolution: {integrity: sha512-tHuOP9TN/1VmDM/ylApGK1QF3PSIP8I6bHDEfoKNQeViREQ/sfu1bAUrA1hoDun8p8Tpm7jcsz47g+3PiGoYdg==} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.13.1: + resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} + engines: {node: '>=0.6'} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quibble@0.9.2: + resolution: {integrity: sha512-BrL7hrZcbyyt5ZDfePkGFDc3m82uUtxCPOnpRUrkOdtBnmV9ldQKxXORkKL8eIzToRNaCpIPyKyfdfq/tBlFAA==} + engines: {node: '>= 0.14.0'} + + quick-temp@0.1.9: + resolution: {integrity: sha512-yI0h7tIhKVObn03kD+Ln9JFi4OljD28lfaOsTdfpTR0xzrhGOod+q66CjGafUqYX2juUfT9oHIGrTBBo22mkRA==} + + rambda@7.5.0: + resolution: {integrity: sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@1.1.7: + resolution: {integrity: sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==} + engines: {node: '>= 0.8.0'} + deprecated: No longer maintained. Please upgrade to a stable version. + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + redeyed@1.0.1: + resolution: {integrity: sha512-8eEWsNCkV2rvwKLS1Cvp5agNjMhwRe2um+y32B2+3LqOzg4C9BBPs6vzAfV16Ivb8B9HPNKIqd8OrdBws8kNlQ==} + + reflect.getprototypeof@1.0.8: + resolution: {integrity: sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ==} + engines: {node: '>= 0.4'} + + regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + + regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} + + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + release-plan@0.17.4: + resolution: {integrity: sha512-CK+RrsvP6JXysgFuqUoOvprAT95J5x8usHzAQh3M1RMQqFScnAyfY6lb1LBsjqW/HUsvLjkLfSp8dJseRHEpEw==} + hasBin: true + + release-zalgo@1.0.0: + resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==} + engines: {node: '>=4'} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + request@2.40.0: + resolution: {integrity: sha512-waNoGB4Z7bPn+lgqPk7l7hhze4Vd68jKccnwLeS7vr9GMxz0iWQbYTbBNWzfIk87Urx7V44pu29qjF/omej+Fw==} + engines: {'0': node >= 0.8.0} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-package-path@1.2.7: + resolution: {integrity: sha512-fVEKHGeK85bGbVFuwO9o1aU0n3vqQGrezPc51JGu9UTXpFQfWq5qCeKxyaRUSvephs+06c5j5rPq/dzHGEo8+Q==} + + resolve-package-path@4.0.3: + resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==} + engines: {node: '>= 12'} + + resolve-path@1.4.0: + resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} + engines: {node: '>= 0.8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + rimraf@6.1.0: + resolution: {integrity: sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==} + engines: {node: 20 || >=22} + hasBin: true + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + + rsvp@3.2.1: + resolution: {integrity: sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==} + + rsvp@3.6.2: + resolution: {integrity: sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==} + engines: {node: 0.12.* || 4.* || 6.* || >= 7.*} + + rsvp@4.8.5: + resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} + engines: {node: 6.* || >= 7.*} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-json-parse@1.0.1: + resolution: {integrity: sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sane@4.1.0: + resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==} + engines: {node: 6.* || 8.* || >= 10.*} + deprecated: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added + hasBin: true + + sane@5.0.1: + resolution: {integrity: sha512-9/0CYoRz0MKKf04OMCO3Qk3RQl1PAwWAhPSQSym4ULiLpTZnrY1JoZU0IEikHu8kdk2HvKT/VwQMq/xFZ8kh1Q==} + engines: {node: 10.* || >= 12.*} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver-deprecate@1.1.0: + resolution: {integrity: sha512-5AcYPbRIw9vs80YkyRxmm3Lbe88STkwNIpTKKX0Kxyy/T0yR05BTZ6JhV9B5pjTyc81SjztcFPY93kDPRFwlyA==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + silent-error@1.1.1: + resolution: {integrity: sha512-n4iEKyNcg4v6/jpb3c0/iyH2G1nzUNl7Gpqtn/mHIJK9S/q/7MCfoO4rwVOoO59qPFIc0hVHvMbiOJ0NdtxKKw==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + + sntp@0.2.4: + resolution: {integrity: sha512-bDLrKa/ywz65gCl+LmOiIhteP1bhEsAAzhfMedPoiHP3dyYnAevlaJshdqb9Yu0sRifyP/fRqSt8t+5qGIWlGQ==} + engines: {node: '>=0.8.0'} + deprecated: This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. + + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks@2.8.4: + resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sort-object-keys@1.1.3: + resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} + + sort-object-keys@2.0.1: + resolution: {integrity: sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg==} + + sort-package-json@2.15.1: + resolution: {integrity: sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==} + hasBin: true + + sort-package-json@3.6.1: + resolution: {integrity: sha512-Chgejw1+10p2D0U2tB7au1lHtz6TkFnxmvZktyBCRyV0GgmF6nl1IxXxAsPtJVsUyg/fo+BfCMAVVFUVRkAHrQ==} + engines: {node: '>=20'} + hasBin: true + + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map-url@0.3.0: + resolution: {integrity: sha512-QU4fa0D6aSOmrT+7OHpUXw+jS84T0MLaQNtFs8xzLNe6Arj44Magd7WEbyVW5LNYoAPVV35aKs4azxIfVJrToQ==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.4.4: + resolution: {integrity: sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==} + engines: {node: '>=0.8.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawn-args@0.2.0: + resolution: {integrity: sha512-73BoniQDcRWgnLAf/suKH6V5H54gd1KLzwYN9FB6J/evqTV33htH9xwV/4BHek+++jzxpVlZQKKZkqstPQPmQg==} + + spawn-wrap@2.0.0: + resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} + engines: {node: '>=8'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-object-es5@2.5.0: + resolution: {integrity: sha512-vE7Xdx9ylG4JI16zy7/ObKUB+MtxuMcWlj/WHHr3+yAlQoN6sst2stU9E+2Qs3OrlJw/Pf3loWxL1GauEHf6MA==} + engines: {node: '>=0.10.0'} + + stringstream@0.0.6: + resolution: {integrity: sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==} + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + + styled_string@0.0.1: + resolution: {integrity: sha512-DU2KZiB6VbPkO2tGSqQ9n96ZstUPjW7X4sGO6V2m1myIQluX0p1Ol8BrA/l6/EesqhMqXOIXs3cJNOy1UuU2BA==} + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + symlink-or-copy@1.3.1: + resolution: {integrity: sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==} + + sync-disk-cache@1.3.4: + resolution: {integrity: sha512-GlkGeM81GPPEKz/lH7QUTbvqLq7K/IUTuaKDSMulP9XQ42glqNJIN/RKgSOw4y8vxL1gOVvj+W7ruEO4s36eCw==} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tap-parser@18.3.4: + resolution: {integrity: sha512-CiqzdpWn2CvONcWp7UNMF9/rCPJwCz0es+qykkgJruu1Y/rAS8A5MEQujmjx9NErfst3dGiZJU3lDS2jBsgbPA==} + engines: {node: 20 || >=22} + hasBin: true + + tap-yaml@4.4.2: + resolution: {integrity: sha512-03mQI7QhfVZHJqGgFyxNTgUbgsG41ZzpWSb7k1Gangmf9hF71Jpb0Fczs7KtOdUDaHx+KxlPUdM2pQJaijebGA==} + engines: {node: 20 || >=22} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + testdouble@3.20.2: + resolution: {integrity: sha512-790e9vJKdfddWNOaxW1/V9FcMk48cPEl3eJSj2i8Hh1fX89qArEJ6cp3DBnaECpGXc3xKJVWbc1jeNlWYWgiMg==} + engines: {node: '>= 16'} + + testem@3.20.0: + resolution: {integrity: sha512-SSFfJQK/SGruISFjoKG2jCYwK596wWNPJFj2Wo77GzeIUxZ8ZjuwpyF01uekTLu4ITL6i9R4m1sWaKPK/HsunA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + textextensions@2.6.0: + resolution: {integrity: sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==} + engines: {node: '>=0.8'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + theredoc@1.0.0: + resolution: {integrity: sha512-KU3SA3TjRRM932jpNfD3u4Ec3bSvedyo5ITPI7zgWYnKep7BwQQaxlhI9qbO+lKJoRnoAbEVfMcAHRuKVYikDA==} + + through2@3.0.2: + resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-lr@2.0.0: + resolution: {integrity: sha512-f6nh0VMRvhGx4KCeK1lQ/jaL0Zdb5WdR+Jk8q9OSUQnaSDxAEGH1fgqLZ+cMl5EW3F2MGnCsalBO1IsnnogW1Q==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@6.1.68: + resolution: {integrity: sha512-85TdlS/DLW/gVdf2oyyzqp3ocS30WxjaL4la85EArl9cHUR/nizifKAJPziWewSZjDZS71U517/i6ciUeqtB5Q==} + + tldts@6.1.68: + resolution: {integrity: sha512-JKF17jROiYkjJPT73hUTEiTp2OBCf+kAlB+1novk8i6Q6dWjHsgEjw9VLiipV4KTJavazXhY1QUXyQFSem2T7w==} + hasBin: true + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp-sync@1.1.2: + resolution: {integrity: sha512-npRDYJiMaPWhcLf6q06v/vA3o/ZG4hfHDiBuj1N3Yeh3GTkFQb1YLFs6inDGMWIHjGidl4Oc1+oXHNKKj5vkDQ==} + engines: {node: '>=0.8.0'} + + tmp@0.0.28: + resolution: {integrity: sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==} + engines: {node: '>=0.4.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmp@0.1.0: + resolution: {integrity: sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==} + engines: {node: '>=6'} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + + to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + + tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tree-sync@1.4.0: + resolution: {integrity: sha512-YvYllqh3qrR5TAYZZTXdspnIhlKAYezPYw11ntmweoceu4VK+keN356phHRIIo1d+RDmLpHZrUlmxga2gc9kSQ==} + + tree-sync@2.1.0: + resolution: {integrity: sha512-OLWW+Nd99NOM53aZ8ilT/YpEiOo6mXD3F4/wLbARqybSZ3Jb8IxHK5UGVbZaae0wtXAyQshVV+SeqVBik+Fbmw==} + engines: {node: '>=8'} + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.4.3: + resolution: {integrity: sha512-e0IoVDWx8SDHc/hwFTqJDQ7CCDTEeGhmcT9jkWJjoGQSpgBz20nAMr80E3Tpk7PatJ1b37DQDgJR3CNSzcMOZQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@0.1.1: + resolution: {integrity: sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==} + + type-detect@1.0.0: + resolution: {integrity: sha512-f9Uv6ezcpvCQjJU0Zqbg+65qdcszv3qUQsZfjdRbWiZ7AMenrX1u0lNk9EoWWX6e1F+NULyg27mtdeZ5WhpljA==} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + type-fest@0.11.0: + resolution: {integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==} + engines: {node: '>=8'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.3: + resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + underscore.string@3.3.6: + resolution: {integrity: sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==} + + underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + + username-sync@1.0.3: + resolution: {integrity: sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA==} + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@6.0.0: + resolution: {integrity: sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==} + engines: {node: ^18.17.0 || >=20.5.0} + + validate-npm-package-name@7.0.1: + resolution: {integrity: sha512-BM0Upcemlce8/9+HE+/VpWqn3u3mYh6Om/FEC8yPMnEHwf710fW5Q6fhjT1SQyRlZD1G9CJbgfH+rWgAcIvjlQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + + walk-sync@0.3.4: + resolution: {integrity: sha512-ttGcuHA/OBnN2pcM6johpYlEms7XpO5/fyKIr48541xXedan4roO8cS1Q2S/zbbjGH/BarYDAMeS2Mi9HE5Tig==} + + walk-sync@1.1.4: + resolution: {integrity: sha512-nowc9thB/Jg0KW4TgxoRjLLYRPvl3DB/98S89r4ZcJqq2B0alNcKDh6pzLkBSkPMzRSMsJghJHQi79qw0YWEkA==} + + walk-sync@2.2.0: + resolution: {integrity: sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==} + engines: {node: 8.* || >= 10.*} + + walk-sync@3.0.0: + resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} + engines: {node: 10.* || >= 12.*} + + walk-sync@4.0.1: + resolution: {integrity: sha512-oXP3IlkfG9Mqdgqh3JGYTPAcryRQd1J1CJOxOgsri2I1MD6N+k4OqxEVP4ZQ0xyYJfYPhBVPRMUVK+N5f13+jQ==} + engines: {node: '>= 20.*'} + + walk-sync@4.0.2: + resolution: {integrity: sha512-SPRy/z6vC+Fb20XQDzagaSVVNzX77EcLLPnBJsqNy0CFQgBS6cexbYP62kzRSqNdyIDdRGc7SOCybRrpkf+Pmg==} + engines: {node: '>= 20.*'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watch-detector@0.1.0: + resolution: {integrity: sha512-vfzMMfpjQc88xjETwl2HuE6PjEuxCBeyC4bQmqrHrofdfYWi/4mEJklYbNgSzpqM9PxubsiPIrE5SZ1FDyiQ2w==} + engines: {node: '>= 4'} + + watch-detector@1.0.2: + resolution: {integrity: sha512-MrJK9z7kD5Gl3jHBnnBVHvr1saVGAfmkyyrvuNzV/oe0Gr1nwZTy5VSA0Gw2j2Or0Mu8HcjUa44qlBvC2Ofnpg==} + engines: {node: '>= 8'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + websocket@1.0.35: + resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==} + engines: {node: '>=4.0.0'} + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@12.0.1: + resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} + engines: {node: '>=14'} + + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.16: + resolution: {integrity: sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + which@6.0.0: + resolution: {integrity: sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + workerpool@10.0.2: + resolution: {integrity: sha512-8PCeZlCwu0+8hXruze1ahYNsY+M0LOCmbmySZ9BWWqWIXP9TAXa6FZCxACTDL/0j47pFcC4xW98Gr8nAC5oymg==} + + workerpool@9.2.0: + resolution: {integrity: sha512-PKZqBOCo6CYkVOwAxWxQaSF2Fvb5Iv2fCeTP7buyWI2GiynWr46NcXSgK/idoV6e60dgCBfgYc+Un3HMvmqP8w==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaeti@0.0.6: + resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} + engines: {node: '>=0.10.32'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yam@1.0.0: + resolution: {integrity: sha512-Hv9xxHtsJ9228wNhk03xnlDReUuWVvHwM4rIbjdAXYvHLs17xjuyF50N6XXFMN6N0omBaqgOok/MCK3At9fTAg==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + yaml-types@0.4.0: + resolution: {integrity: sha512-XfbA30NUg4/LWUiplMbiufUiwYhgB9jvBhTWel7XQqjV+GaB79c2tROu/8/Tu7jO0HvDvnKWtBk5ksWRrhQ/0g==} + engines: {node: '>= 16', npm: '>= 7'} + peerDependencies: + yaml: ^2.3.0 + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + + yui@3.18.1: + resolution: {integrity: sha512-M4/mHnq5uGvpwKEpRBh3SclL70cpDEus9LNGnrK5ZBzp4HOoueY7EkXfgtRBd+9VOQHWlFukXL2udHE53N4Wqw==} + engines: {node: '>=0.8.0'} + + yuidoc-ember-cli-theme@1.0.4: + resolution: {integrity: sha512-5cgdxRTPm6XLvDpoM4LT/1V4JRwvWstd+4EeHU/za+uC0D9BJooQEyBoIPu3bKlo7w1o/KLgCnqsKmpnsickaA==} + + yuidocjs@0.10.2: + resolution: {integrity: sha512-g0ZrXsaCmQL9zsvkgD+RxWDsMNkHne5tK72iWYodro9JQlfKxePcV1dwbGhKMy/fl1XCIW3R3erZudohU+PcEw==} + engines: {node: '>=0.10.0'} + hasBin: true + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.3': {} + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.3': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.26.3': + dependencies: + '@babel/types': 7.26.3 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.26.4': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + debug: 4.4.3(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.3': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@cnakazawa/watch@1.0.4': + dependencies: + exec-sh: 0.3.6 + minimist: 1.2.8 + + '@ember/app-blueprint@7.2.0-alpha.1': + dependencies: + ejs: 3.1.10 + ember-cli-string-utils: 1.1.0 + lodash: 4.18.1 + sort-package-json: 3.6.1 + walk-sync: 4.0.2 + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@8.1.1) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@gar/promisify@1.1.3': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@inquirer/ansi@2.0.5': {} + + '@inquirer/checkbox@5.1.5(@types/node@22.10.2)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/confirm@6.0.13(@types/node@22.10.2)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/core@11.1.10(@types/node@22.10.2)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@22.10.2) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/editor@5.1.2(@types/node@22.10.2)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/external-editor': 3.0.0(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/expand@5.0.14(@types/node@22.10.2)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/external-editor@3.0.0(@types/node@22.10.2)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/figures@2.0.5': {} + + '@inquirer/input@5.0.13(@types/node@22.10.2)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/number@4.0.13(@types/node@22.10.2)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/password@5.0.13(@types/node@22.10.2)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/prompts@8.4.3(@types/node@22.10.2)': + dependencies: + '@inquirer/checkbox': 5.1.5(@types/node@22.10.2) + '@inquirer/confirm': 6.0.13(@types/node@22.10.2) + '@inquirer/editor': 5.1.2(@types/node@22.10.2) + '@inquirer/expand': 5.0.14(@types/node@22.10.2) + '@inquirer/input': 5.0.13(@types/node@22.10.2) + '@inquirer/number': 4.0.13(@types/node@22.10.2) + '@inquirer/password': 5.0.13(@types/node@22.10.2) + '@inquirer/rawlist': 5.2.9(@types/node@22.10.2) + '@inquirer/search': 4.1.9(@types/node@22.10.2) + '@inquirer/select': 5.1.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/rawlist@5.2.9(@types/node@22.10.2)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/search@4.1.9(@types/node@22.10.2)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/select@5.1.5(@types/node@22.10.2)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@22.10.2) + optionalDependencies: + '@types/node': 22.10.2 + + '@inquirer/type@4.0.5(@types/node@22.10.2)': + optionalDependencies: + '@types/node': 22.10.2 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@manypkg/find-root@2.2.3': + dependencies: + '@manypkg/tools': 1.1.2 + + '@manypkg/get-packages@2.2.2': + dependencies: + '@manypkg/find-root': 2.2.3 + '@manypkg/tools': 1.1.2 + + '@manypkg/tools@1.1.2': + dependencies: + fast-glob: 3.3.3 + jju: 1.4.0 + js-yaml: 4.1.0 + + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.8.1 + + '@npmcli/git@6.0.3': + dependencies: + '@npmcli/promise-spawn': 8.0.2 + ini: 5.0.0 + lru-cache: 10.4.3 + npm-pick-manifest: 10.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + semver: 7.8.1 + which: 5.0.0 + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@npmcli/package-json@6.1.1': + dependencies: + '@npmcli/git': 6.0.3 + glob: 10.4.5 + hosted-git-info: 8.0.2 + json-parse-even-better-errors: 4.0.0 + proc-log: 5.0.0 + semver: 7.8.1 + validate-npm-package-license: 3.0.4 + + '@npmcli/promise-spawn@8.0.2': + dependencies: + which: 5.0.0 + + '@octokit/auth-token@5.1.2': {} + + '@octokit/core@6.1.5': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.2 + '@octokit/request': 9.2.3 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.0.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@10.1.4': + dependencies: + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@8.2.2': + dependencies: + '@octokit/request': 9.2.3 + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@24.2.0': {} + + '@octokit/openapi-types@25.0.0': {} + + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.5)': + dependencies: + '@octokit/core': 6.1.5 + '@octokit/types': 13.10.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.5)': + dependencies: + '@octokit/core': 6.1.5 + + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.5)': + dependencies: + '@octokit/core': 6.1.5 + '@octokit/types': 13.10.0 + + '@octokit/request-error@6.1.8': + dependencies: + '@octokit/types': 14.0.0 + + '@octokit/request@9.2.3': + dependencies: + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.0.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.3 + + '@octokit/rest@21.1.1': + dependencies: + '@octokit/core': 6.1.5 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.5) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.5) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.5) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + + '@octokit/types@14.0.0': + dependencies: + '@octokit/openapi-types': 25.0.0 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.8.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/constants@1001.3.1': {} + + '@pnpm/error@1000.1.0': + dependencies: + '@pnpm/constants': 1001.3.1 + + '@pnpm/find-workspace-dir@1000.1.5': + dependencies: + '@pnpm/error': 1000.1.0 + find-up: 5.0.0 + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@socket.io/component-emitter@3.1.2': {} + + '@tootallnate/once@1.1.2': {} + + '@tootallnate/once@2.0.0': {} + + '@types/cookie@0.4.1': {} + + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.10.2 + + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 22.10.2 + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 22.10.2 + + '@types/glob@8.1.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.10.2 + + '@types/minimatch@3.0.5': {} + + '@types/minimatch@5.1.2': {} + + '@types/node@22.10.2': + dependencies: + undici-types: 6.20.0 + + '@types/rimraf@2.0.5': + dependencies: + '@types/glob': 8.1.0 + '@types/node': 22.10.2 + + '@types/rimraf@3.0.2': + dependencies: + '@types/glob': 8.1.0 + '@types/node': 22.10.2 + + '@types/symlink-or-copy@1.2.2': {} + + '@types/tmp@0.0.33': {} + + '@ungap/structured-clone@1.2.1': {} + + '@xmldom/xmldom@0.9.10': {} + + abab@2.0.6: {} + + abbrev@1.1.1: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + acorn-globals@7.0.1: + dependencies: + acorn: 8.14.0 + acorn-walk: 8.3.4 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + amdefine@1.0.1: {} + + ansi-escapes@3.2.0: {} + + ansi-html@0.0.9: {} + + ansi-regex@2.1.1: {} + + ansi-regex@3.0.1: {} + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@2.2.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + ansicolors@0.2.1: {} + + any-promise@1.3.0: {} + + anymatch@2.0.0: + dependencies: + micromatch: 3.1.10 + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + append-transform@2.0.0: + dependencies: + default-require-extensions: 3.0.1 + + archy@1.0.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.8 + is-array-buffer: 3.0.5 + + array-equal@1.0.2: {} + + array-flatten@1.1.1: {} + + array-unique@0.3.2: {} + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.6 + es-errors: 1.3.0 + get-intrinsic: 1.2.6 + is-array-buffer: 3.0.5 + + asap@2.0.6: {} + + asn1@0.1.11: + optional: true + + assert-never@1.4.0: {} + + assert-plus@0.1.5: + optional: true + + assertion-error@1.1.0: {} + + assign-symbols@1.0.0: {} + + async-disk-cache@1.3.5: + dependencies: + debug: 2.6.9 + heimdalljs: 0.2.6 + istextorbinary: 2.1.0 + mkdirp: 0.5.6 + rimraf: 2.7.1 + rsvp: 3.6.2 + username-sync: 1.0.3 + transitivePeerDependencies: + - supports-color + + async-promise-queue@1.0.5: + dependencies: + async: 2.6.4 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + async@0.9.2: + optional: true + + async@2.6.4: + dependencies: + lodash: 4.18.1 + + async@3.2.6: {} + + asynckit@0.4.0: {} + + atob@2.1.2: {} + + atomically@2.1.0: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + aws-sign2@0.5.0: + optional: true + + babel-remove-types@2.0.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + prettier: 3.8.3 + transitivePeerDependencies: + - supports-color + + backbone@1.6.1: + dependencies: + underscore: 1.13.7 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64id@2.0.0: {} + + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + before-after-hook@3.0.2: {} + + binaryextensions@2.3.0: {} + + blank-object@1.0.2: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.1: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@8.1.1) + http-errors: 2.0.0 + iconv-lite: 0.7.1 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + body@5.1.0: + dependencies: + continuable-cache: 0.3.1 + error: 7.2.1 + raw-body: 1.1.7 + safe-json-parse: 1.0.1 + + boom@0.4.2: + dependencies: + hoek: 0.9.1 + optional: true + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + broccoli-caching-writer@3.0.3: + dependencies: + broccoli-kitchen-sink-helpers: 0.3.1 + broccoli-plugin: 1.3.1 + debug: 2.6.9 + rimraf: 2.7.1 + rsvp: 3.6.2 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-concat@4.2.7: + dependencies: + broccoli-debug: 0.6.5 + broccoli-plugin: 4.0.7 + ensure-posix-path: 1.1.1 + fast-sourcemap-concat: 2.1.1 + find-index: 1.1.1 + fs-extra: 8.1.0 + fs-tree-diff: 2.0.1 + lodash: 4.18.1 + transitivePeerDependencies: + - supports-color + + broccoli-config-loader@1.0.1: + dependencies: + broccoli-caching-writer: 3.0.3 + transitivePeerDependencies: + - supports-color + + broccoli-config-replace@1.1.3: + dependencies: + broccoli-plugin: 1.3.1 + debug: 2.6.9 + fs-extra: 0.24.0 + transitivePeerDependencies: + - supports-color + + broccoli-debug@0.6.5: + dependencies: + broccoli-plugin: 1.3.1 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + symlink-or-copy: 1.3.1 + tree-sync: 1.4.0 + transitivePeerDependencies: + - supports-color + + broccoli-funnel-reducer@1.0.0: {} + + broccoli-funnel@2.0.2: + dependencies: + array-equal: 1.0.2 + blank-object: 1.0.2 + broccoli-plugin: 1.3.1 + debug: 2.6.9 + fast-ordered-set: 1.0.3 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + minimatch: 3.1.2 + mkdirp: 0.5.6 + path-posix: 1.0.0 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-funnel@3.0.8: + dependencies: + array-equal: 1.0.2 + broccoli-plugin: 4.0.7 + debug: 4.4.0 + fs-tree-diff: 2.0.1 + heimdalljs: 0.2.6 + minimatch: 3.1.2 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + broccoli-kitchen-sink-helpers@0.3.1: + dependencies: + glob: 5.0.15 + mkdirp: 0.5.6 + + broccoli-merge-trees@3.0.2: + dependencies: + broccoli-plugin: 1.3.1 + merge-trees: 2.0.0 + transitivePeerDependencies: + - supports-color + + broccoli-merge-trees@4.2.0: + dependencies: + broccoli-plugin: 4.0.7 + merge-trees: 2.0.0 + transitivePeerDependencies: + - supports-color + + broccoli-middleware@2.1.2: + dependencies: + ansi-html: 0.0.9 + handlebars: 4.7.8 + has-ansi: 3.0.0 + mime-types: 2.1.35 + + broccoli-node-api@1.7.0: {} + + broccoli-node-info@1.1.0: {} + + broccoli-node-info@2.2.0: {} + + broccoli-output-wrapper@3.2.5: + dependencies: + fs-extra: 8.1.0 + heimdalljs-logger: 0.1.10 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + broccoli-persistent-filter@2.3.1: + dependencies: + async-disk-cache: 1.3.5 + async-promise-queue: 1.0.5 + broccoli-plugin: 1.3.1 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.1 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + mkdirp: 0.5.6 + promise-map-series: 0.2.3 + rimraf: 2.7.1 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + sync-disk-cache: 1.3.4 + walk-sync: 1.1.4 + transitivePeerDependencies: + - supports-color + + broccoli-plugin@1.3.1: + dependencies: + promise-map-series: 0.2.3 + quick-temp: 0.1.9 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + + broccoli-plugin@2.1.0: + dependencies: + promise-map-series: 0.2.3 + quick-temp: 0.1.9 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + + broccoli-plugin@4.0.7: + dependencies: + broccoli-node-api: 1.7.0 + broccoli-output-wrapper: 3.2.5 + fs-merger: 3.2.1 + promise-map-series: 0.3.0 + quick-temp: 0.1.9 + rimraf: 3.0.2 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + broccoli-slow-trees@3.1.0: + dependencies: + heimdalljs: 0.2.6 + + broccoli-source@1.1.0: {} + + broccoli-source@3.0.1: + dependencies: + broccoli-node-api: 1.7.0 + + broccoli-stew@3.0.0: + dependencies: + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 3.0.2 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 2.1.0 + chalk: 2.4.2 + debug: 4.4.0 + ensure-posix-path: 1.1.1 + fs-extra: 8.1.0 + minimatch: 3.1.2 + resolve: 1.22.12 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + walk-sync: 1.1.4 + transitivePeerDependencies: + - supports-color + + broccoli-test-helper@2.0.0: + dependencies: + '@types/tmp': 0.0.33 + broccoli: 2.3.0 + fixturify: 0.3.4 + fs-tree-diff: 0.5.9 + tmp: 0.0.33 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli@2.3.0: + dependencies: + broccoli-node-info: 1.1.0 + broccoli-slow-trees: 3.1.0 + broccoli-source: 1.1.0 + commander: 2.20.3 + connect: 3.7.0 + esm: 3.2.25 + findup-sync: 2.0.0 + handlebars: 4.7.8 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + mime-types: 2.1.35 + promise.prototype.finally: 3.1.8 + resolve-path: 1.4.0 + rimraf: 2.7.1 + sane: 4.1.0 + tmp: 0.0.33 + tree-sync: 1.4.0 + underscore.string: 3.3.6 + watch-detector: 0.1.0 + transitivePeerDependencies: + - supports-color + + broccoli@4.0.0: + dependencies: + ansi-html: 0.0.9 + broccoli-node-info: 2.2.0 + broccoli-source: 3.0.1 + commander: 14.0.2 + connect: 3.7.0 + console-ui: 3.1.2 + findup-sync: 5.0.0 + handlebars: 4.7.8 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + mime-types: 3.0.1 + resolve-path: 1.4.0 + rimraf: 6.1.0 + sane: 5.0.1 + tmp: 0.2.5 + tree-sync: 2.1.0 + underscore.string: 3.3.6 + watch-detector: 1.0.2 + transitivePeerDependencies: + - supports-color + + browser-stdout@1.3.1: {} + + browserslist@4.24.3: + dependencies: + caniuse-lite: 1.0.30001689 + electron-to-chromium: 1.5.74 + node-releases: 2.0.19 + update-browserslist-db: 1.1.1(browserslist@4.24.3) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + bufferutil@4.0.8: + dependencies: + node-gyp-build: 4.8.4 + + bytes@1.0.0: {} + + bytes@3.1.2: {} + + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + + caching-transform@4.0.0: + dependencies: + hasha: 5.2.2 + make-dir: 3.1.0 + package-hash: 4.0.0 + write-file-atomic: 3.0.3 + + calculate-cache-key-for-tree@2.0.0: + dependencies: + json-stable-stringify: 1.2.0 + + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.6 + set-function-length: 1.2.2 + + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.6 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + can-symlink@1.0.0: + dependencies: + tmp: 0.0.28 + + caniuse-lite@1.0.30001689: {} + + capture-exit@2.0.0: + dependencies: + rsvp: 4.8.5 + + cardinal@1.0.0: + dependencies: + ansicolors: 0.2.1 + redeyed: 1.0.1 + + chai-as-promised@6.0.0(chai@3.5.0): + dependencies: + chai: 3.5.0 + check-error: 1.0.3 + + chai-as-promised@7.1.2(chai@4.5.0): + dependencies: + chai: 4.5.0 + check-error: 1.0.3 + + chai-as-promised@8.0.2(chai@6.2.2): + dependencies: + chai: 6.2.2 + check-error: 2.1.3 + + chai-files@1.4.0: + dependencies: + assertion-error: 1.1.0 + + chai-jest-snapshot@2.0.0(chai@6.2.2): + dependencies: + chai: 6.2.2 + jest-snapshot: 21.2.1 + lodash.values: 4.3.0 + + chai@3.5.0: + dependencies: + assertion-error: 1.1.0 + deep-eql: 0.1.3 + type-detect: 1.0.0 + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chai@6.2.2: {} + + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + chardet@0.7.0: {} + + chardet@2.1.1: {} + + charm@1.0.2: + dependencies: + inherits: 2.0.4 + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@2.0.0: {} + + ci-info@4.4.0: {} + + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + + clean-base-url@1.0.0: {} + + clean-stack@2.2.0: {} + + clean-up-path@1.0.0: {} + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + + cli-spinners@2.9.2: {} + + cli-table@0.3.11: + dependencies: + colors: 1.0.3 + + cli-width@2.2.1: {} + + cli-width@4.1.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + codsen-utils@1.7.3: + dependencies: + rfdc: 1.4.1 + + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colors@1.0.3: {} + + combined-stream@0.0.7: + dependencies: + delayed-stream: 0.0.5 + optional: true + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@14.0.2: {} + + commander@14.0.3: {} + + commander@2.20.3: {} + + commondir@1.0.1: {} + + component-emitter@1.3.1: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@8.0.0: + dependencies: + atomically: 2.1.0 + dot-prop: 10.1.0 + graceful-fs: 4.2.11 + is-safe-filename: 0.1.1 + xdg-basedir: 5.1.0 + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + console-ui@3.1.2: + dependencies: + chalk: 2.4.2 + inquirer: 6.5.2 + json-stable-stringify: 1.2.0 + ora: 3.4.0 + through2: 3.0.2 + + consolidate@1.0.4(@babel/core@7.29.0)(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.18.1)(mustache@4.2.0)(underscore@1.13.7): + optionalDependencies: + '@babel/core': 7.29.0 + ejs: 3.1.10 + handlebars: 4.7.8 + lodash: 4.18.1 + mustache: 4.2.0 + underscore: 1.13.7 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.0.1: {} + + content-tag@4.2.0: {} + + content-type@1.0.5: {} + + continuable-cache@0.3.1: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + cookiejar@2.1.4: {} + + copy-descriptor@0.1.1: {} + + core-object@3.1.5: + dependencies: + chalk: 2.4.2 + + core-util-is@1.0.3: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cryptiles@0.2.2: + dependencies: + boom: 0.4.2 + optional: true + + cssstyle@3.0.0: + dependencies: + rrweb-cssom: 0.6.0 + + ctype@0.5.3: + optional: true + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + dag-map@2.0.2: {} + + data-urls@4.0.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@1.2.0: {} + + decamelize@4.0.0: {} + + decimal.js@10.4.3: {} + + decode-uri-component@0.2.2: {} + + deep-eql@0.1.3: + dependencies: + type-detect: 0.1.1 + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + default-require-extensions@3.0.1: + dependencies: + strip-bom: 4.0.0 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.7 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.3 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.3 + isobject: 3.0.1 + + delayed-stream@0.0.5: + optional: true + + delayed-stream@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-file@1.0.0: {} + + detect-indent@7.0.2: {} + + detect-newline@4.0.1: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff@3.5.0: {} + + diff@7.0.0: {} + + diff@8.0.4: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + + dot-prop@10.1.0: + dependencies: + type-fest: 5.6.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + editions@1.3.4: {} + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-to-chromium@1.5.74: {} + + ember-cli-blueprint-test-helpers@0.19.2(patch_hash=67b4e6082643614a12fdca71c9ed35d2df9b46dfb0d24cb3ba3665b2048bac9a): + dependencies: + chai: 4.5.0 + chai-as-promised: 7.1.2(chai@4.5.0) + chai-files: 1.4.0 + debug: 4.4.0 + ember-cli-internal-test-helpers: 0.9.1 + fs-extra: 7.0.1 + testdouble: 3.20.2 + tmp-sync: 1.1.2 + transitivePeerDependencies: + - supports-color + + ember-cli-internal-test-helpers@0.9.1: + dependencies: + chai: 3.5.0 + chai-as-promised: 6.0.0(chai@3.5.0) + chai-files: 1.4.0 + chalk: 1.1.3 + debug: 2.6.9 + exists-sync: 0.0.3 + fs-extra: 0.30.0 + lodash: 4.18.1 + rsvp: 3.6.2 + symlink-or-copy: 1.3.1 + through: 2.3.8 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + ember-cli-is-package-missing@1.0.0: {} + + ember-cli-normalize-entity-name@1.0.0: + dependencies: + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli-preprocess-registry@5.0.1: + dependencies: + broccoli-funnel: 3.0.8 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + ember-cli-string-utils@1.1.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + engine.io-parser@5.2.3: {} + + engine.io@6.6.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 22.10.2 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + ensure-posix-path@1.1.1: {} + + entities@1.1.2: {} + + entities@4.5.0: {} + + err-code@2.0.3: {} + + error@7.2.1: + dependencies: + string-template: 0.2.1 + + es-abstract@1.23.6: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.7 + get-intrinsic: 1.2.6 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.3 + is-string: 1.1.1 + is-typed-array: 1.1.14 + is-weakref: 1.1.0 + math-intrinsics: 1.0.0 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.3 + safe-regex-test: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.3 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.16 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.6 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.6 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-error@4.1.1: {} + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-compat-utils@0.5.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + semver: 7.8.1 + + eslint-config-prettier@10.1.8(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-chai-expect@3.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-es-x@7.8.0(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + eslint: 8.57.1 + eslint-compat-utils: 0.5.1(eslint@8.57.1) + + eslint-plugin-mocha@10.5.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-utils: 3.0.0(eslint@8.57.1) + globals: 13.24.0 + rambda: 7.5.0 + + eslint-plugin-n@17.24.0(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + enhanced-resolve: 5.18.4 + eslint: 8.57.1 + eslint-plugin-es-x: 7.8.0(eslint@8.57.1) + get-tsconfig: 4.8.1 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.8.1 + ts-declaration-location: 1.0.7(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-utils@3.0.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.1 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + esm@3.2.25: {} + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + espree@9.6.1: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + + esprima@3.0.0: {} + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + eventemitter3@4.0.7: {} + + events-to-array@2.0.3: {} + + exec-sh@0.3.6: {} + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + exists-sync@0.0.3: {} + + exit@0.1.2: {} + + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + ext@1.7.0: + dependencies: + type: 2.7.3 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + extract-stack@2.0.0: {} + + fast-content-type-parse@2.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-ordered-set@1.0.3: + dependencies: + blank-object: 1.0.2 + + fast-safe-stringify@2.1.1: {} + + fast-sourcemap-concat@2.1.1: + dependencies: + chalk: 2.4.2 + fs-extra: 5.0.0 + heimdalljs-logger: 0.1.10 + memory-streams: 0.1.3 + mkdirp: 0.5.6 + source-map: 0.4.4 + source-map-url: 0.3.0 + transitivePeerDependencies: + - supports-color + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + filesize@11.0.17: {} + + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-index@1.1.1: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@8.0.0: + dependencies: + locate-path: 8.0.0 + unicorn-magic: 0.3.0 + + find-yarn-workspace-root@2.0.0: + dependencies: + micromatch: 4.0.8 + + findup-sync@2.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 3.1.0 + micromatch: 3.1.10 + resolve-dir: 1.0.1 + transitivePeerDependencies: + - supports-color + + findup-sync@5.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + + fixturify-project@2.1.1: + dependencies: + fixturify: 2.1.1 + tmp: 0.0.33 + type-fest: 0.11.0 + + fixturify@0.3.4: + dependencies: + fs-extra: 0.30.0 + matcher-collection: 1.1.2 + + fixturify@2.1.1: + dependencies: + '@types/fs-extra': 8.1.5 + '@types/minimatch': 3.0.5 + '@types/rimraf': 2.0.5 + fs-extra: 8.1.0 + matcher-collection: 2.0.1 + walk-sync: 2.2.0 + + fixturify@3.0.0: + dependencies: + '@types/fs-extra': 9.0.13 + '@types/minimatch': 3.0.5 + '@types/rimraf': 3.0.2 + fs-extra: 10.1.0 + matcher-collection: 2.0.1 + walk-sync: 3.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat@5.0.2: {} + + flatted@3.3.2: {} + + follow-redirects@1.15.9: {} + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + for-in@1.0.2: {} + + foreground-child@2.0.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 3.0.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forever-agent@0.5.2: {} + + form-data@0.1.4: + dependencies: + async: 0.9.2 + combined-stream: 0.0.7 + mime: 1.2.11 + optional: true + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.2.2 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fromentries@1.3.2: {} + + fs-extra@0.24.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 + + fs-extra@0.30.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + klaw: 1.3.1 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.3.5: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@4.0.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@5.0.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-merger@3.2.1: + dependencies: + broccoli-node-api: 1.7.0 + broccoli-node-info: 2.2.0 + fs-extra: 8.1.0 + fs-tree-diff: 2.0.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-sync@1.0.6: + dependencies: + glob: 7.2.3 + iconv-lite: 0.4.24 + lodash: 4.18.1 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + fs-tree-diff@0.5.9: + dependencies: + heimdalljs-logger: 0.1.10 + object-assign: 4.1.1 + path-posix: 1.0.0 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + fs-tree-diff@2.0.1: + dependencies: + '@types/symlink-or-copy': 1.2.2 + heimdalljs-logger: 0.1.10 + object-assign: 4.1.1 + path-posix: 1.0.0 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + fs-updater@1.0.4: + dependencies: + can-symlink: 1.0.0 + clean-up-path: 1.0.0 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + rimraf: 2.7.1 + transitivePeerDependencies: + - supports-color + + fs.realpath@1.0.0: {} + + function-bind@1.1.2: {} + + function.prototype.name@1.1.7: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.2.6: + dependencies: + call-bind-apply-helpers: 1.0.1 + dunder-proto: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + function-bind: 1.1.2 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.0.0 + + get-package-type@0.1.0: {} + + get-stdin@9.0.0: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.2 + + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.6 + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-value@2.0.6: {} + + git-hooks-list@3.1.0: {} + + git-hooks-list@4.1.1: {} + + git-repo-info@2.1.1: {} + + github-changelog@2.1.4: + dependencies: + '@manypkg/get-packages': 2.2.2 + chalk: 4.1.2 + cli-highlight: 2.1.11 + execa: 5.1.1 + hosted-git-info: 4.1.0 + make-fetch-happen: 9.1.0 + p-map: 3.0.0 + progress: 2.0.3 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.2.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@5.0.15: + dependencies: + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@15.15.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + growly@1.3.0: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + + has-ansi@3.0.0: + dependencies: + ansi-regex: 3.0.1 + + has-bigints@1.0.2: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + + hash-for-dep@1.5.1: + dependencies: + broccoli-kitchen-sink-helpers: 0.3.1 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + path-root: 0.1.1 + resolve: 1.22.12 + resolve-package-path: 1.2.7 + transitivePeerDependencies: + - supports-color + + hasha@5.2.2: + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hawk@1.1.1: + dependencies: + boom: 0.4.2 + cryptiles: 0.2.2 + hoek: 0.9.1 + sntp: 0.2.4 + optional: true + + he@1.2.0: {} + + heimdalljs-fs-monitor@1.1.2: + dependencies: + callsites: 3.1.0 + clean-stack: 2.2.0 + extract-stack: 2.0.0 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + transitivePeerDependencies: + - supports-color + + heimdalljs-graph@1.0.0: {} + + heimdalljs-logger@0.1.10: + dependencies: + debug: 2.6.9 + heimdalljs: 0.2.6 + transitivePeerDependencies: + - supports-color + + heimdalljs@0.2.6: + dependencies: + rsvp: 3.2.1 + + highlight.js@10.7.3: {} + + hoek@0.9.1: + optional: true + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hosted-git-info@8.0.2: + dependencies: + lru-cache: 10.4.3 + + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.2.2 + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + + html-escaper@2.0.2: {} + + http-cache-semantics@4.1.1: {} + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-parser-js@0.5.8: {} + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.9 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http-signature@0.10.1: + dependencies: + asn1: 0.1.11 + assert-plus: 0.1.5 + ctype: 0.5.3 + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + human-signals@1.1.1: {} + + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.1: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infer-owner@1.0.4: {} + + inflection@3.0.2: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@5.0.0: {} + + inquirer@13.4.3(@types/node@22.10.2): + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@22.10.2) + '@inquirer/prompts': 8.4.3(@types/node@22.10.2) + '@inquirer/type': 4.0.5(@types/node@22.10.2) + mute-stream: 3.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 22.10.2 + + inquirer@6.5.2: + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 3.1.0 + figures: 2.0.0 + lodash: 4.18.1 + mute-stream: 0.0.7 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 2.1.1 + strip-ansi: 5.2.0 + through: 2.3.8 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + invert-kv@3.0.1: {} + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + + ipaddr.js@1.9.1: {} + + is-accessor-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.6 + + is-async-function@2.0.0: + dependencies: + has-tostringtag: 1.0.2 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.0.2 + + is-boolean-object@1.2.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: {} + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.6 + is-typed-array: 1.1.14 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-descriptor@0.1.7: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.3: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-docker@2.2.1: {} + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.3 + + is-fullwidth-code-point@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-git-url@1.0.0: {} + + is-glob@3.1.0: + dependencies: + is-extglob: 2.1.1 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-lambda@1.0.1: {} + + is-language-code@5.1.3: + dependencies: + codsen-utils: 1.7.3 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@1.1.0: {} + + is-plain-obj@2.1.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-potential-custom-element-name@1.0.1: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-regexp@1.0.0: {} + + is-safe-filename@0.1.1: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.8 + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.3 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.14: + dependencies: + which-typed-array: 1.1.16 + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@2.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.0: + dependencies: + call-bound: 1.0.3 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.6 + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@0.0.1: {} + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isbinaryfile@5.0.7: {} + + isexe@2.0.0: {} + + isexe@3.1.1: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-hook@3.0.0: + dependencies: + append-transform: 2.0.0 + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.3 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-processinfo@2.0.3: + dependencies: + archy: 1.0.0 + cross-spawn: 7.0.6 + istanbul-lib-coverage: 3.2.2 + p-map: 3.0.0 + rimraf: 3.0.2 + uuid: 8.3.2 + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.0 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + istextorbinary@2.1.0: + dependencies: + binaryextensions: 2.3.0 + editions: 1.3.4 + textextensions: 2.6.0 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.4 + picocolors: 1.1.1 + + jest-diff@21.2.1: + dependencies: + chalk: 2.4.2 + diff: 3.5.0 + jest-get-type: 21.2.0 + pretty-format: 21.2.1 + + jest-get-type@21.2.0: {} + + jest-matcher-utils@21.2.1: + dependencies: + chalk: 2.4.2 + jest-get-type: 21.2.0 + pretty-format: 21.2.1 + + jest-snapshot@21.2.1: + dependencies: + chalk: 2.4.2 + jest-diff: 21.2.1 + jest-matcher-utils: 21.2.1 + mkdirp: 0.5.6 + natural-compare: 1.4.0 + pretty-format: 21.2.1 + + jju@1.4.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsbn@1.1.0: {} + + jsdom@21.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + abab: 2.0.6 + acorn: 8.14.0 + acorn-globals: 7.0.1 + cssstyle: 3.0.0 + data-urls: 4.0.0 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.5 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@4.0.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stable-stringify@1.2.0: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + + json-stringify-safe@5.0.1: {} + + json5@2.2.3: {} + + jsonfile@2.4.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonify@0.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@6.0.3: {} + + klaw@1.3.1: + optionalDependencies: + graceful-fs: 4.2.11 + + ky@1.8.1: {} + + latest-version@9.0.0: + dependencies: + package-json: 10.0.1 + + lcid@3.1.1: + dependencies: + invert-kv: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + linkify-it@1.2.4: + dependencies: + uc.micro: 1.0.6 + + linkify-it@2.2.0: + dependencies: + uc.micro: 1.0.6 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + livereload-js@3.4.1: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@8.0.0: + dependencies: + p-locate: 6.0.0 + + lodash.flattendeep@4.4.0: {} + + lodash.merge@4.6.2: {} + + lodash.values@4.3.0: {} + + lodash@4.18.1: {} + + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + lru-cache@10.4.3: {} + + lru-cache@11.2.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.1 + + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.1.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + map-cache@0.2.2: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + + markdown-it-terminal@0.4.0(markdown-it@14.1.1): + dependencies: + ansi-styles: 3.2.1 + cardinal: 1.0.0 + cli-table: 0.3.11 + lodash.merge: 4.6.2 + markdown-it: 14.1.1 + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-it@4.4.0: + dependencies: + argparse: 1.0.10 + entities: 1.1.2 + linkify-it: 1.2.4 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdown-it@8.4.2: + dependencies: + argparse: 1.0.10 + entities: 1.1.2 + linkify-it: 2.2.0 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + matcher-collection@1.1.2: + dependencies: + minimatch: 3.1.2 + + matcher-collection@2.0.1: + dependencies: + '@types/minimatch': 3.0.5 + minimatch: 3.1.2 + + math-intrinsics@1.0.0: {} + + mdn-links@0.1.0: {} + + mdurl@1.0.1: {} + + mdurl@2.0.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memory-streams@0.1.3: + dependencies: + readable-stream: 1.0.34 + + merge-descriptors@1.0.3: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge-trees@2.0.0: + dependencies: + fs-updater: 1.0.4 + heimdalljs: 0.2.6 + transitivePeerDependencies: + - supports-color + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@3.1.10: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@1.0.2: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.2.11: + optional: true + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@1.2.0: {} + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minipass@7.1.3: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + mktemp@2.0.2: {} + + mocha@11.7.6: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.4.5 + he: 1.2.0 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + log-symbols: 4.1.0 + minimatch: 9.0.5 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.2.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + + ms@2.1.3: {} + + mustache@4.2.0: {} + + mute-stream@0.0.7: {} + + mute-stream@3.0.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + next-tick@1.1.0: {} + + nice-try@1.0.5: {} + + nock@14.0.15: + dependencies: + '@mswjs/interceptors': 0.41.9 + json-stringify-safe: 5.0.1 + propagate: 2.0.1 + + node-gyp-build@4.8.4: {} + + node-int64@0.4.0: {} + + node-notifier@10.0.1: + dependencies: + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.8.1 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 + + node-preload@0.2.1: + dependencies: + process-on-spawn: 1.1.0 + + node-releases@2.0.19: {} + + node-uuid@1.4.8: {} + + nopt@3.0.6: + dependencies: + abbrev: 1.1.1 + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + normalize-path@3.0.0: {} + + npm-install-checks@7.1.1: + dependencies: + semver: 7.8.1 + + npm-normalize-package-bin@4.0.0: {} + + npm-package-arg@12.0.2: + dependencies: + hosted-git-info: 8.0.2 + proc-log: 5.0.0 + semver: 7.8.1 + validate-npm-package-name: 6.0.0 + + npm-package-arg@13.0.2: + dependencies: + hosted-git-info: 9.0.2 + proc-log: 6.1.0 + semver: 7.8.1 + validate-npm-package-name: 7.0.1 + + npm-pick-manifest@10.0.0: + dependencies: + npm-install-checks: 7.1.1 + npm-normalize-package-bin: 4.0.0 + npm-package-arg: 12.0.2 + semver: 7.8.1 + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nwsapi@2.2.16: {} + + nyc@17.1.0: + dependencies: + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + caching-transform: 4.0.0 + convert-source-map: 1.9.0 + decamelize: 1.2.0 + find-cache-dir: 3.3.2 + find-up: 4.1.0 + foreground-child: 3.3.1 + get-package-type: 0.1.0 + glob: 7.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-hook: 3.0.0 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-processinfo: 2.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + make-dir: 3.1.0 + node-preload: 0.2.1 + p-map: 3.0.0 + process-on-spawn: 1.1.0 + resolve-from: 5.0.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + spawn-wrap: 2.0.0 + test-exclude: 6.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - supports-color + + oauth-sign@0.3.0: + optional: true + + object-assign@4.1.1: {} + + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + + object-inspect@1.13.3: {} + + object-keys@1.1.1: {} + + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@3.4.0: + dependencies: + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + + os-homedir@1.0.2: {} + + os-locale@6.0.2: + dependencies: + lcid: 3.1.1 + + os-tmpdir@1.0.2: {} + + osenv@0.1.5: + dependencies: + os-homedir: 1.0.2 + os-tmpdir: 1.0.2 + + outvariant@1.4.3: {} + + p-defer@4.0.1: {} + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@3.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@2.2.0: {} + + package-hash@4.0.0: + dependencies: + graceful-fs: 4.2.11 + hasha: 5.2.2 + lodash.flattendeep: 4.4.0 + release-zalgo: 1.0.0 + + package-json-from-dist@1.0.1: {} + + package-json@10.0.1: + dependencies: + ky: 1.8.1 + registry-auth-token: 5.1.0 + registry-url: 6.0.1 + semver: 7.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-github-repo-url@1.4.1: {} + + parse-ms@4.0.0: {} + + parse-passwd@1.0.0: {} + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + parseurl@1.3.3: {} + + pascalcase@0.1.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-posix@1.0.0: {} + + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.2 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.3 + + path-to-regexp@0.1.12: {} + + path-to-regexp@8.3.0: {} + + pathval@1.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + portfinder@1.0.38: + dependencies: + async: 3.2.6 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + posix-character-classes@0.1.1: {} + + possible-typed-array-names@1.0.0: {} + + prelude-ls@1.2.1: {} + + prettier@3.7.4: {} + + prettier@3.8.3: {} + + pretty-format@21.2.1: + dependencies: + ansi-regex: 3.0.1 + ansi-styles: 3.2.1 + + pretty-ms@9.2.0: + dependencies: + parse-ms: 4.0.0 + + printf@0.6.1: {} + + proc-log@5.0.0: {} + + proc-log@6.1.0: {} + + process-on-spawn@1.1.0: + dependencies: + fromentries: 1.3.2 + + progress@2.0.3: {} + + promise-inflight@1.0.1: {} + + promise-map-series@0.2.3: + dependencies: + rsvp: 3.6.2 + + promise-map-series@0.3.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + promise.hash.helper@1.0.8: {} + + promise.prototype.finally@3.1.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.6 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + propagate@2.0.1: {} + + proto-list@1.2.4: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + qs@1.0.2: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.13.1: + dependencies: + side-channel: 1.1.0 + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + quibble@0.9.2: + dependencies: + lodash: 4.18.1 + resolve: 1.22.12 + + quick-temp@0.1.9: + dependencies: + mktemp: 2.0.2 + rimraf: 5.0.10 + underscore.string: 3.3.6 + + rambda@7.5.0: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@1.1.7: + dependencies: + bytes: 1.0.0 + string_decoder: 0.10.31 + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@1.0.34: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + + redeyed@1.0.1: + dependencies: + esprima: 3.0.0 + + reflect.getprototypeof@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + dunder-proto: 1.0.1 + es-abstract: 1.23.6 + es-errors: 1.3.0 + get-intrinsic: 1.2.6 + gopd: 1.2.0 + which-builtin-type: 1.2.1 + + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + + regexp.prototype.flags@1.5.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + + release-plan@0.17.4: + dependencies: + '@manypkg/get-packages': 2.2.2 + '@npmcli/package-json': 6.1.1 + '@octokit/rest': 21.1.1 + assert-never: 1.4.0 + chalk: 5.6.2 + cli-highlight: 2.1.11 + execa: 9.6.1 + fs-extra: 11.3.5 + github-changelog: 2.1.4 + js-yaml: 4.1.0 + latest-version: 9.0.0 + parse-github-repo-url: 1.4.1 + semver: 7.8.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + release-zalgo@1.0.0: + dependencies: + es6-error: 4.1.1 + + remove-trailing-separator@1.1.0: {} + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + + request@2.40.0: + dependencies: + forever-agent: 0.5.2 + json-stringify-safe: 5.0.1 + mime-types: 1.0.2 + node-uuid: 1.4.8 + qs: 1.0.2 + optionalDependencies: + aws-sign2: 0.5.0 + form-data: 0.1.4 + hawk: 1.1.1 + http-signature: 0.10.1 + oauth-sign: 0.3.0 + stringstream: 0.0.6 + tough-cookie: 5.0.0 + tunnel-agent: 0.4.3 + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + requires-port@1.0.0: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-package-path@1.2.7: + dependencies: + path-root: 0.1.1 + resolve: 1.22.12 + + resolve-package-path@4.0.3: + dependencies: + path-root: 0.1.1 + + resolve-path@1.4.0: + dependencies: + http-errors: 1.6.3 + path-is-absolute: 1.0.1 + + resolve-pkg-maps@1.0.0: {} + + resolve-url@0.2.1: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + ret@0.1.15: {} + + retry@0.12.0: {} + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + rimraf@6.1.0: + dependencies: + glob: 11.0.3 + package-json-from-dist: 1.0.1 + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + router@2.2.0: + dependencies: + debug: 4.4.0 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rrweb-cssom@0.6.0: {} + + rsvp@3.2.1: {} + + rsvp@3.6.2: {} + + rsvp@4.8.5: {} + + run-async@2.4.1: {} + + run-async@4.0.6: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.6 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-json-parse@1.0.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sane@4.1.0: + dependencies: + '@cnakazawa/watch': 1.0.4 + anymatch: 2.0.0 + capture-exit: 2.0.0 + exec-sh: 0.3.6 + execa: 1.0.0 + fb-watchman: 2.0.2 + micromatch: 3.1.10 + minimist: 1.2.8 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + sane@5.0.1: + dependencies: + '@cnakazawa/watch': 1.0.4 + anymatch: 3.1.3 + capture-exit: 2.0.0 + exec-sh: 0.3.6 + execa: 4.1.0 + fb-watchman: 2.0.2 + micromatch: 4.0.8 + minimist: 1.2.8 + walker: 1.0.8 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver-deprecate@1.1.0: + dependencies: + chalk: 5.6.2 + semver: 7.8.1 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.8.1: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.6 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + shellwords@0.1.1: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.6 + object-inspect: 1.13.3 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.6 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + silent-error@1.1.1: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + smart-buffer@4.2.0: {} + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + + sntp@0.2.4: + dependencies: + hoek: 0.9.1 + optional: true + + socket.io-adapter@2.5.5(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + debug: 4.3.7 + ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3(bufferutil@4.0.8)(utf-8-validate@5.0.10): + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.4.3(supports-color@8.1.1) + engine.io: 6.6.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + socket.io-adapter: 2.5.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + socks: 2.8.4 + transitivePeerDependencies: + - supports-color + + socks@2.8.4: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + + sort-object-keys@1.1.3: {} + + sort-object-keys@2.0.1: {} + + sort-package-json@2.15.1: + dependencies: + detect-indent: 7.0.2 + detect-newline: 4.0.1 + get-stdin: 9.0.0 + git-hooks-list: 3.1.0 + is-plain-obj: 4.1.0 + semver: 7.8.1 + sort-object-keys: 1.1.3 + tinyglobby: 0.2.15 + + sort-package-json@3.6.1: + dependencies: + detect-indent: 7.0.2 + detect-newline: 4.0.1 + git-hooks-list: 4.1.1 + is-plain-obj: 4.1.0 + semver: 7.8.1 + sort-object-keys: 2.0.1 + tinyglobby: 0.2.15 + + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + + source-map-url@0.3.0: {} + + source-map-url@0.4.1: {} + + source-map@0.4.4: + dependencies: + amdefine: 1.0.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + spawn-args@0.2.0: {} + + spawn-wrap@2.0.0: + dependencies: + foreground-child: 2.0.0 + is-windows: 1.0.2 + make-dir: 3.1.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + which: 2.0.2 + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + sprintf-js@1.0.3: {} + + sprintf-js@1.1.3: {} + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + strict-event-emitter@0.5.1: {} + + string-template@0.2.1: {} + + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.6 + es-object-atoms: 1.0.0 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string_decoder@0.10.31: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-object-es5@2.5.0: + dependencies: + is-plain-obj: 1.1.0 + is-regexp: 1.0.0 + + stringstream@0.0.6: + optional: true + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@4.0.0: {} + + strip-eof@1.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + + styled_string@0.0.1: {} + + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@8.1.1) + fast-safe-stringify: 2.1.1 + form-data: 4.0.5 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.1 + transitivePeerDependencies: + - supports-color + + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + + supports-color@2.0.0: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + symlink-or-copy@1.3.1: {} + + sync-disk-cache@1.3.4: + dependencies: + debug: 2.6.9 + heimdalljs: 0.2.6 + mkdirp: 0.5.6 + rimraf: 2.7.1 + username-sync: 1.0.3 + transitivePeerDependencies: + - supports-color + + tagged-tag@1.0.0: {} + + tap-parser@18.3.4: + dependencies: + events-to-array: 2.0.3 + tap-yaml: 4.4.2 + + tap-yaml@4.4.2: + dependencies: + yaml: 2.9.0 + yaml-types: 0.4.0(yaml@2.9.0) + + tapable@2.3.0: {} + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + testdouble@3.20.2: + dependencies: + lodash: 4.18.1 + quibble: 0.9.2 + stringify-object-es5: 2.5.0 + theredoc: 1.0.0 + + testem@3.20.0(@babel/core@7.29.0)(bufferutil@4.0.8)(ejs@3.1.10)(handlebars@4.7.8)(underscore@1.13.7)(utf-8-validate@5.0.10): + dependencies: + '@xmldom/xmldom': 0.9.10 + backbone: 1.6.1 + charm: 1.0.2 + chokidar: 5.0.0 + commander: 14.0.3 + compression: 1.8.1 + consolidate: 1.0.4(@babel/core@7.29.0)(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.18.1)(mustache@4.2.0)(underscore@1.13.7) + execa: 9.6.1 + express: 5.2.1 + glob: 13.0.6 + http-proxy: 1.18.1 + js-yaml: 4.1.1 + lodash: 4.18.1 + minimatch: 10.2.5 + mkdirp: 3.0.1 + mustache: 4.2.0 + node-notifier: 10.0.1 + printf: 0.6.1 + proc-log: 6.1.0 + rimraf: 6.1.3 + socket.io: 4.8.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) + spawn-args: 0.2.0 + styled_string: 0.0.1 + tap-parser: 18.3.4 + transitivePeerDependencies: + - '@babel/core' + - arc-templates + - atpl + - bracket-template + - bufferutil + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jazz + - jqtpl + - just + - liquid-node + - liquor + - mote + - nunjucks + - plates + - pug + - qejs + - ractive + - react + - react-dom + - slm + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - utf-8-validate + - vash + - velocityjs + - walrus + - whiskers + + text-table@0.2.0: {} + + textextensions@2.6.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + theredoc@1.0.0: {} + + through2@3.0.2: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + through@2.3.8: {} + + tiny-lr@2.0.0: + dependencies: + body: 5.1.0 + debug: 3.2.7 + faye-websocket: 0.11.4 + livereload-js: 3.4.1 + object-assign: 4.1.1 + qs: 6.13.1 + transitivePeerDependencies: + - supports-color + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@6.1.68: + optional: true + + tldts@6.1.68: + dependencies: + tldts-core: 6.1.68 + optional: true + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.5 + + tmp-sync@1.1.2: + dependencies: + fs-sync: 1.0.6 + osenv: 0.1.5 + + tmp@0.0.28: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.1.0: + dependencies: + rimraf: 2.7.1 + + tmp@0.2.5: {} + + tmpl@1.0.5: {} + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + + toidentifier@1.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tough-cookie@5.0.0: + dependencies: + tldts: 6.1.68 + optional: true + + tr46@4.1.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + tree-sync@1.4.0: + dependencies: + debug: 2.6.9 + fs-tree-diff: 0.5.9 + mkdirp: 0.5.6 + quick-temp: 0.1.9 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + tree-sync@2.1.0: + dependencies: + debug: 4.4.0 + fs-tree-diff: 2.0.1 + mkdirp: 0.5.6 + quick-temp: 0.1.9 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + ts-declaration-location@1.0.7(typescript@5.9.3): + dependencies: + picomatch: 4.0.3 + typescript: 5.9.3 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tunnel-agent@0.4.3: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@0.1.1: {} + + type-detect@1.0.0: {} + + type-detect@4.1.0: {} + + type-fest@0.11.0: {} + + type-fest@0.20.2: {} + + type-fest@0.8.1: {} + + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + type@2.7.3: {} + + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-typed-array: 1.1.14 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.14 + + typed-array-byte-offset@1.0.3: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.14 + reflect.getprototypeof: 1.0.8 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + is-typed-array: 1.1.14 + possible-typed-array-names: 1.0.0 + reflect.getprototypeof: 1.0.8 + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typescript@5.9.3: {} + + uc.micro@1.0.6: {} + + uc.micro@2.1.0: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.3 + has-bigints: 1.0.2 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + underscore.string@3.3.6: + dependencies: + sprintf-js: 1.1.3 + util-deprecate: 1.0.2 + + underscore@1.13.7: {} + + undici-types@6.20.0: {} + + unicorn-magic@0.3.0: {} + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + + universal-user-agent@7.0.3: {} + + universalify@0.1.2: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + update-browserslist-db@1.1.1(browserslist@4.24.3): + dependencies: + browserslist: 4.24.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + urix@0.1.0: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + use@3.1.1: {} + + username-sync@1.0.3: {} + + utf-8-validate@5.0.10: + dependencies: + node-gyp-build: 4.8.4 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@8.3.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@6.0.0: {} + + validate-npm-package-name@7.0.1: {} + + vary@1.1.2: {} + + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + + walk-sync@0.3.4: + dependencies: + ensure-posix-path: 1.1.1 + matcher-collection: 1.1.2 + + walk-sync@1.1.4: + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 1.1.2 + + walk-sync@2.2.0: + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 3.1.2 + + walk-sync@3.0.0: + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 3.1.2 + + walk-sync@4.0.1: + dependencies: + '@types/minimatch': 5.1.2 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 10.2.5 + + walk-sync@4.0.2: + dependencies: + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 10.2.5 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watch-detector@0.1.0: + dependencies: + heimdalljs-logger: 0.1.10 + quick-temp: 0.1.9 + rsvp: 4.8.5 + semver: 5.7.2 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + watch-detector@1.0.2: + dependencies: + heimdalljs-logger: 0.1.10 + silent-error: 1.1.1 + tmp: 0.1.0 + transitivePeerDependencies: + - supports-color + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@7.0.0: {} + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + websocket@1.0.35: + dependencies: + bufferutil: 4.0.8 + debug: 2.6.9 + es5-ext: 0.10.64 + typedarray-to-buffer: 3.1.5 + utf-8-validate: 5.0.10 + yaeti: 0.0.6 + transitivePeerDependencies: + - supports-color + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@3.0.0: {} + + whatwg-url@12.0.1: + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 + + when-exit@2.1.5: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.1 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.3 + function.prototype.name: 1.1.7 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.0.10 + is-regex: 1.2.1 + is-weakref: 1.1.0 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.16 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-module@2.0.1: {} + + which-typed-array@1.1.16: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.1 + + which@6.0.0: + dependencies: + isexe: 3.1.1 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + workerpool@10.0.2: {} + + workerpool@9.2.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + + ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + + xdg-basedir@5.1.0: {} + + xml-name-validator@4.0.0: {} + + xmlchars@2.2.0: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yaeti@0.0.6: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yam@1.0.0: + dependencies: + fs-extra: 4.0.3 + lodash.merge: 4.6.2 + + yaml-types@0.4.0(yaml@2.9.0): + dependencies: + yaml: 2.9.0 + + yaml@2.9.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} + + yoctocolors@2.1.1: {} + + yui@3.18.1: + dependencies: + request: 2.40.0 + + yuidoc-ember-cli-theme@1.0.4: + dependencies: + markdown-it: 8.4.2 + + yuidocjs@0.10.2: + dependencies: + express: 4.21.2 + graceful-fs: 4.2.11 + markdown-it: 4.4.0 + mdn-links: 0.1.0 + minimatch: 3.1.2 + rimraf: 2.7.1 + yui: 3.18.1 + transitivePeerDependencies: + - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..d9368ad81e --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - . + - packages/* diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js new file mode 100644 index 0000000000..9db95fe0bc --- /dev/null +++ b/tests/.eslintrc.js @@ -0,0 +1,46 @@ +'use strict'; + +module.exports = { + plugins: ['chai-expect', 'mocha'], + env: { + mocha: true, + }, + rules: { + // JSHint "expr", disabled due to chai expect assertions + 'no-unused-expressions': 0, + + // disabled for easier asserting of file contents + quotes: 0, + + // disabled because describe(), it(), etc. should not use arrow functions + 'prefer-arrow-callback': 0, + + camelcase: ['error', { allow: ['node_modules'] }], + + /*** chai-expect ***/ + + 'chai-expect/missing-assertion': 2, + 'chai-expect/terminating-properties': 2, + 'chai-expect/no-inner-compare': 2, + + /*** mocha ***/ + + 'mocha/no-exclusive-tests': 'error', + 'mocha/no-skipped-tests': 'off', + 'mocha/no-pending-tests': 'off', + 'mocha/handle-done-callback': 'error', + 'mocha/no-synchronous-tests': 'off', + 'mocha/no-global-tests': 'error', + 'mocha/no-return-and-callback': 'error', + 'mocha/valid-test-description': 'off', + 'mocha/valid-suite-description': 'off', + 'mocha/no-sibling-hooks': 'error', + 'mocha/no-mocha-arrows': 'error', + 'mocha/no-hooks': 'off', + 'mocha/no-hooks-for-single-case': 'off', + 'mocha/no-top-level-hooks': 'error', + 'mocha/no-identical-title': 'error', + 'mocha/max-top-level-suites': 'off', + 'mocha/no-nested-tests': 'error', + }, +}; diff --git a/tests/acceptance/addon-destroy-test.js b/tests/acceptance/addon-destroy-test.js deleted file mode 100644 index 51a6f2e107..0000000000 --- a/tests/acceptance/addon-destroy-test.js +++ /dev/null @@ -1,361 +0,0 @@ -/*jshint quotmark: false*/ - -'use strict'; - -var Promise = require('../../lib/ext/promise'); -var expect = require('chai').expect; -var assertFile = require('../helpers/assert-file'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var existsSync = require('exists-sync'); -var fs = require('fs-extra'); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); - -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); - -describe('Acceptance: ember destroy in-addon', function() { - this.timeout(20000); - - var tmpdir; - - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); - }); - - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); - }); - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - function initAddon() { - return ember([ - 'addon', - 'my-addon', - '--skip-npm', - '--skip-bower' - ]); - } - - function generateInAddon(args) { - var generateArgs = ['generate'].concat(args); - - return ember(generateArgs); - } - - function destroy(args) { - var destroyArgs = ['destroy'].concat(args); - return ember(destroyArgs); - } - - function assertFileNotExists(file) { - var filePath = path.join(process.cwd(), file); - expect(!existsSync(filePath), 'expected ' + file + ' not to exist'); - } - - function assertFilesExist(files) { - files.forEach(assertFile); - } - - function assertFilesNotExist(files) { - files.forEach(assertFileNotExists); - } - - function assertDestroyAfterGenerateInAddon(args, files) { - return initAddon() - .then(function() { - return generateInAddon(args); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - it('in-addon controller foo', function() { - var commandArgs = ['controller', 'foo']; - var files = [ - 'addon/controllers/foo.js', - 'app/controllers/foo.js', - 'tests/unit/controllers/foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon controller foo/bar', function() { - var commandArgs = ['controller', 'foo/bar']; - var files = [ - 'addon/controllers/foo/bar.js', - 'app/controllers/foo/bar.js', - 'tests/unit/controllers/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon component x-foo', function() { - var commandArgs = ['component', 'x-foo']; - var files = [ - 'addon/components/x-foo.js', - 'addon/templates/components/x-foo.hbs', - 'app/components/x-foo.js', - 'tests/integration/components/x-foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon helper foo-bar', function() { - var commandArgs = ['helper', 'foo-bar']; - var files = [ - 'addon/helpers/foo-bar.js', - 'app/helpers/foo-bar.js', - 'tests/unit/helpers/foo-bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon helper foo/bar-baz', function() { - var commandArgs = ['helper', 'foo/bar-baz']; - var files = [ - 'addon/helpers/foo/bar-baz.js', - 'app/helpers/foo/bar-baz.js', - 'tests/unit/helpers/foo/bar-baz-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon model foo', function() { - var commandArgs = ['model', 'foo']; - var files = [ - 'addon/models/foo.js', - 'app/models/foo.js', - 'tests/unit/models/foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon model foo/bar', function() { - var commandArgs = ['model', 'foo/bar']; - var files = [ - 'addon/models/foo/bar.js', - 'app/models/foo/bar.js', - 'tests/unit/models/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon template foo', function() { - var commandArgs = ['template', 'foo']; - var files = [ - 'addon/templates/foo.hbs', - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon template foo/bar', function() { - var commandArgs = ['template', 'foo/bar']; - var files = [ - 'addon/templates/foo/bar.hbs', - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon view foo', function() { - var commandArgs = ['view', 'foo']; - var files = [ - 'addon/views/foo.js', - 'app/views/foo.js', - 'tests/unit/views/foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon view foo/bar', function() { - var commandArgs = ['view', 'foo/bar']; - var files = [ - 'addon/views/foo/bar.js', - 'app/views/foo/bar.js', - 'tests/unit/views/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon initializer foo', function() { - var commandArgs = ['initializer', 'foo']; - var files = [ - 'addon/initializers/foo.js', - 'app/initializers/foo.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon initializer foo/bar', function() { - var commandArgs = ['initializer', 'foo/bar']; - var files = [ - 'addon/initializers/foo/bar.js', - 'app/initializers/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon mixin foo', function() { - var commandArgs = ['mixin', 'foo']; - var files = [ - 'addon/mixins/foo.js', - 'tests/unit/mixins/foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon mixin foo/bar', function() { - var commandArgs = ['mixin', 'foo/bar']; - var files = [ - 'addon/mixins/foo/bar.js', - 'tests/unit/mixins/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon adapter foo', function() { - var commandArgs = ['adapter', 'foo']; - var files = [ - 'addon/adapters/foo.js', - 'app/adapters/foo.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon adapter foo/bar', function() { - var commandArgs = ['adapter', 'foo/bar']; - var files = [ - 'addon/adapters/foo/bar.js', - 'app/adapters/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon serializer foo', function() { - var commandArgs = ['serializer', 'foo']; - var files = [ - 'addon/serializers/foo.js', - 'app/serializers/foo.js', - 'tests/unit/serializers/foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon serializer foo/bar', function() { - var commandArgs = ['serializer', 'foo/bar']; - var files = [ - 'addon/serializers/foo/bar.js', - 'app/serializers/foo/bar.js', - 'tests/unit/serializers/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon transform foo', function() { - var commandArgs = ['transform', 'foo']; - var files = [ - 'addon/transforms/foo.js', - 'app/transforms/foo.js', - 'tests/unit/transforms/foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon transform foo/bar', function() { - var commandArgs = ['transform', 'foo/bar']; - var files = [ - 'addon/transforms/foo/bar.js', - 'app/transforms/foo/bar.js', - 'tests/unit/transforms/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon util foo-bar', function() { - var commandArgs = ['util', 'foo-bar']; - var files = [ - 'addon/utils/foo-bar.js', - 'app/utils/foo-bar.js', - 'tests/unit/utils/foo-bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon util foo-bar/baz', function() { - var commandArgs = ['util', 'foo/bar-baz']; - var files = [ - 'addon/utils/foo/bar-baz.js', - 'app/utils/foo/bar-baz.js', - 'tests/unit/utils/foo/bar-baz-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon service foo', function() { - var commandArgs = ['service', 'foo']; - var files = [ - 'addon/services/foo.js', - 'app/services/foo.js', - 'tests/unit/services/foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-addon service foo/bar', function() { - var commandArgs = ['service', 'foo/bar']; - var files = [ - 'addon/services/foo/bar.js', - 'app/services/foo/bar.js', - 'tests/unit/services/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - -}); diff --git a/tests/acceptance/addon-dummy-destroy-test.js b/tests/acceptance/addon-dummy-destroy-test.js deleted file mode 100644 index 6cc7a44938..0000000000 --- a/tests/acceptance/addon-dummy-destroy-test.js +++ /dev/null @@ -1,350 +0,0 @@ -/*jshint quotmark: false*/ - -'use strict'; - -var Promise = require('../../lib/ext/promise'); -var expect = require('chai').expect; -var assertFile = require('../helpers/assert-file'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var existsSync = require('exists-sync'); -var fs = require('fs-extra'); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); - -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); - -describe('Acceptance: ember destroy in-addon-dummy', function() { - this.timeout(20000); - - var tmpdir; - - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); - }); - - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); - }); - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - function initAddon() { - return ember([ - 'addon', - 'my-addon', - '--skip-npm', - '--skip-bower' - ]); - } - - function generateInAddon(args) { - var generateArgs = ['generate'].concat(args); - - return ember(generateArgs); - } - - function destroy(args) { - var destroyArgs = ['destroy'].concat(args); - return ember(destroyArgs); - } - - function assertFileNotExists(file) { - var filePath = path.join(process.cwd(), file); - expect(!existsSync(filePath), 'expected ' + file + ' not to exist'); - } - - function assertFilesExist(files) { - files.forEach(assertFile); - } - - function assertFilesNotExist(files) { - files.forEach(assertFileNotExists); - } - - function assertDestroyAfterGenerateInAddonDummy(args, files) { - args = args.concat('--dummy'); - - return initAddon() - .then(function() { - return generateInAddon(args); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - it('in-addon-dummy controller foo', function() { - var commandArgs = ['controller', 'foo']; - var files = [ - 'tests/dummy/app/controllers/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy controller foo/bar', function() { - var commandArgs = ['controller', 'foo/bar']; - var files = [ - 'tests/dummy/app/controllers/foo/bar.js', - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy route foo', function() { - var commandArgs = ['route', 'foo']; - var files = [ - 'tests/dummy/app/routes/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files) - .then(function() { - assertFile('tests/dummy/app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); - }); - - it('in-addon-dummy route foo/bar', function() { - var commandArgs = ['route', 'foo/bar']; - var files = [ - 'tests/dummy/app/routes/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files) - .then(function() { - assertFile('tests/dummy/app/router.js', { - doesNotContain: "this.route('bar');" - }); - }); - }); - - it('in-addon-dummy component x-foo', function() { - var commandArgs = ['component', 'x-foo']; - var files = [ - 'tests/dummy/app/templates/components/x-foo.hbs', - 'tests/dummy/app/components/x-foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy helper foo-bar', function() { - var commandArgs = ['helper', 'foo-bar']; - var files = [ - 'tests/dummy/app/helpers/foo-bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy helper foo/bar-baz', function() { - var commandArgs = ['helper', 'foo/bar-baz']; - var files = [ - 'tests/dummy/app/helpers/foo/bar-baz.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy model foo', function() { - var commandArgs = ['model', 'foo']; - var files = [ - 'tests/dummy/app/models/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy model foo/bar', function() { - var commandArgs = ['model', 'foo/bar']; - var files = [ - 'tests/dummy/app/models/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy template foo', function() { - var commandArgs = ['template', 'foo']; - var files = [ - 'tests/dummy/app/templates/foo.hbs', - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy template foo/bar', function() { - var commandArgs = ['template', 'foo/bar']; - var files = [ - 'tests/dummy/app/templates/foo/bar.hbs', - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy view foo', function() { - var commandArgs = ['view', 'foo']; - var files = [ - 'tests/dummy/app/views/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy view foo/bar', function() { - var commandArgs = ['view', 'foo/bar']; - var files = [ - 'tests/dummy/app/views/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy initializer foo', function() { - var commandArgs = ['initializer', 'foo']; - var files = [ - 'tests/dummy/app/initializers/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy initializer foo/bar', function() { - var commandArgs = ['initializer', 'foo/bar']; - var files = [ - 'tests/dummy/app/initializers/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy mixin foo', function() { - var commandArgs = ['mixin', 'foo']; - var files = [ - 'tests/dummy/app/mixins/foo.js', - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy mixin foo/bar', function() { - var commandArgs = ['mixin', 'foo/bar']; - var files = [ - 'tests/dummy/app/mixins/foo/bar.js', - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy adapter foo', function() { - var commandArgs = ['adapter', 'foo']; - var files = [ - 'tests/dummy/app/adapters/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy adapter foo/bar', function() { - var commandArgs = ['adapter', 'foo/bar']; - var files = [ - 'tests/dummy/app/adapters/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy serializer foo', function() { - var commandArgs = ['serializer', 'foo']; - var files = [ - 'tests/dummy/app/serializers/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy serializer foo/bar', function() { - var commandArgs = ['serializer', 'foo/bar']; - var files = [ - 'tests/dummy/app/serializers/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy transform foo', function() { - var commandArgs = ['transform', 'foo']; - var files = [ - 'tests/dummy/app/transforms/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy transform foo/bar', function() { - var commandArgs = ['transform', 'foo/bar']; - var files = [ - 'tests/dummy/app/transforms/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy util foo-bar', function() { - var commandArgs = ['util', 'foo-bar']; - var files = [ - 'tests/dummy/app/utils/foo-bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy util foo-bar/baz', function() { - var commandArgs = ['util', 'foo/bar-baz']; - var files = [ - 'tests/dummy/app/utils/foo/bar-baz.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy service foo', function() { - var commandArgs = ['service', 'foo']; - var files = [ - 'tests/dummy/app/services/foo.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); - - it('in-addon-dummy service foo/bar', function() { - var commandArgs = ['service', 'foo/bar']; - var files = [ - 'tests/dummy/app/services/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInAddonDummy(commandArgs, files); - }); -}); diff --git a/tests/acceptance/addon-dummy-generate-test.js b/tests/acceptance/addon-dummy-generate-test.js index 90dc6f55dd..1cbdec1873 100644 --- a/tests/acceptance/addon-dummy-generate-test.js +++ b/tests/acceptance/addon-dummy-generate-test.js @@ -1,807 +1,122 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../../lib/ext/promise'); -var assertFile = require('../helpers/assert-file'); -var assertFileEquals = require('../helpers/assert-file-equals'); -var assertFileToNotExist = require('../helpers/assert-file-to-not-exist'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var fs = require('fs-extra'); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var EOL = require('os').EOL; -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); -var expect = require('chai').expect; +const ember = require('../helpers/ember'); +let root = process.cwd(); +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); +const tmp = require('tmp-promise'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); -describe('Acceptance: ember generate in-addon-dummy', function() { +describe('Acceptance: ember generate in-addon-dummy', function () { this.timeout(20000); - var tmpdir; - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); }); - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); }); - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); }); - afterEach(function() { + afterEach(function () { process.chdir(root); - return remove(tmproot); }); function initAddon() { - return ember([ - 'addon', - 'my-addon', - '--skip-npm', - '--skip-bower' - ]); + return ember(['addon', 'my-addon', '--skip-npm']); } function generateInAddon(args) { - var generateArgs = ['generate'].concat(args); + let generateArgs = ['generate'].concat(args); - return initAddon().then(function() { + return initAddon().then(function () { return ember(generateArgs); }); } - it('dummy controller foo', function() { - return generateInAddon(['controller', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/controllers/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFileToNotExist('app/controllers/foo-test.js'); - assertFileToNotExist('tests/unit/controllers/foo-test.js'); - }); - }); - - it('dummy controller foo/bar', function() { - return generateInAddon(['controller', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/controllers/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFileToNotExist('app/controllers/foo/bar.js'); - assertFileToNotExist('tests/unit/controllers/foo/bar-test.js'); - }); - }); - - it('dummy component x-foo', function() { - return generateInAddon(['component', 'x-foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/components/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('tests/dummy/app/templates/components/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFileToNotExist('app/components/x-foo.js'); - assertFileToNotExist('tests/unit/components/x-foo-test.js'); - }); - }); - - it('dummy component-test x-foo', function() { - return generateInAddon(['component-test', 'x-foo', '--dummy']).then(function() { - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'" - ] - }); - assertFileToNotExist('app/component-test/x-foo.js'); - }); - }); - - it('dummy component nested/x-foo', function() { - return generateInAddon(['component', 'nested/x-foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/components/nested/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('tests/dummy/app/templates/components/nested/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFileToNotExist('app/components/nested/x-foo.js'); - assertFileToNotExist('tests/unit/components/nested/x-foo-test.js'); - }); - }); + it('dummy blueprint foo', async function () { + await generateInAddon(['blueprint', 'foo', '--dummy']); - it('dummy helper foo-bar', function() { - return generateInAddon(['helper', 'foo-bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/helpers/foo-bar.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBar(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBar);" - }); - assertFileToNotExist('app/helpers/foo-bar.js'); - assertFileToNotExist('tests/unit/helpers/foo-bar-test.js'); - }); - }); - - it('dummy helper foo/bar-baz', function() { - return generateInAddon(['helper', 'foo/bar-baz', '--dummy']).then(function() { - assertFile('tests/dummy/app/helpers/foo/bar-baz.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBarBaz(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBarBaz);" - }); - assertFileToNotExist('app/helpers/foo/bar-baz.js'); - assertFileToNotExist('tests/unit/helpers/foo/bar-baz-test.js'); - }); - }); - - it('dummy model foo', function() { - return generateInAddon(['model', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/models/foo.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFileToNotExist('app/models/foo.js'); - assertFileToNotExist('tests/unit/models/foo-test.js'); - }); - }); - - it('dummy model foo with attributes', function() { - return generateInAddon([ - 'model', - 'foo', - 'noType', - 'firstName:string', - 'created_at:date', - 'is-published:boolean', - 'rating:number', - 'bars:has-many', - 'baz:belongs-to', - 'echo:hasMany', - 'bravo:belongs_to', - 'foo-names:has-many', - 'barName:has-many', - 'bazName:belongs-to', - 'test-name:belongs-to', - 'echoName:hasMany', - 'bravoName:belongs_to', - '--dummy' - ]).then(function() { - assertFile('tests/dummy/app/models/foo.js', { - contains: [ - "noType: DS.attr()", - "firstName: DS.attr('string')", - "createdAt: DS.attr('date')", - "isPublished: DS.attr('boolean')", - "rating: DS.attr('number')", - "bars: DS.hasMany('bar')", - "baz: DS.belongsTo('baz')", - "echos: DS.hasMany('echo')", - "bravo: DS.belongsTo('bravo')", - "fooNames: DS.hasMany('foo-name')", - "barNames: DS.hasMany('bar-name')", - "bazName: DS.belongsTo('baz-name')", - "testName: DS.belongsTo('test-name')", - "echoNames: DS.hasMany('echo-name')", - "bravoName: DS.belongsTo('bravo-name')" - ] - }); - assertFileToNotExist('app/models/foo.js'); - assertFileToNotExist('tests/unit/models/foo-test.js'); - }); + expect(file('blueprints/foo/index.js').content).to.matchSnapshot(); }); - it('dummy model foo/bar', function() { - return generateInAddon(['model', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/models/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFileToNotExist('app/models/foo/bar.js'); - assertFileToNotExist('tests/unit/models/foo/bar-test.js'); - }); - }); + it('dummy blueprint foo/bar', async function () { + await generateInAddon(['blueprint', 'foo/bar', '--dummy']); - it('dummy model-test foo', function() { - return generateInAddon(['model-test', 'foo', '--dummy']).then(function() { - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - assertFileToNotExist('app/model-test/foo.js'); - }); + expect(file('blueprints/foo/bar/index.js').content).to.matchSnapshot(); }); - it('dummy route foo', function() { - return generateInAddon(['route', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/routes/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFileToNotExist('app/routes/foo.js'); - assertFile('tests/dummy/app/templates/foo.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/dummy/app/router.js', { - contains: "this.route('foo');" - }); - assertFileToNotExist('app/templates/foo.js'); - assertFileToNotExist('tests/unit/routes/foo-test.js'); - }); - }); + it('dummy http-mock foo', async function () { + await generateInAddon(['http-mock', 'foo', '--dummy']); - it('dummy route foo/bar', function() { - return generateInAddon(['route', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/routes/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFileToNotExist('app/routes/foo/bar.js'); - assertFile('tests/dummy/app/templates/foo/bar.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/dummy/app/router.js', { - contains: [ - "this.route('foo', function() {", - "this.route('bar');", - ] - }); - assertFileToNotExist('tests/unit/routes/foo/bar-test.js'); - }); - }); + expect(file('server/index.js').content).to.matchSnapshot(); - it('dummy route-test foo', function() { - return generateInAddon(['route-test', 'foo']).then(function() { - assertFile('tests/unit/routes/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - assertFileToNotExist('app/route-test/foo.js'); - }); + expect(file('server/mocks/foo.js').content).to.matchSnapshot(); }); - it('dummy template foo', function() { - return generateInAddon(['template', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/templates/foo.hbs'); - }); - }); + it('dummy http-mock foo-bar', async function () { + await generateInAddon(['http-mock', 'foo-bar', '--dummy']); - it('dummy template foo/bar', function() { - return generateInAddon(['template', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/templates/foo/bar.hbs'); - }); - }); + expect(file('server/index.js').content).to.matchSnapshot(); - it('dummy view foo', function() { - return generateInAddon(['view', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/views/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFileToNotExist('app/views/foo.js'); - assertFileToNotExist('tests/unit/views/foo-test.js'); - }); + expect(file('server/mocks/foo-bar.js').content).to.matchSnapshot(); }); - it('dummy view foo/bar', function() { - return generateInAddon(['view', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/views/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFileToNotExist('app/views/foo/bar.js'); - assertFileToNotExist('tests/unit/views/foo/bar-test.js'); - }); - }); + it('dummy http-proxy foo', async function () { + await generateInAddon(['http-proxy', 'foo', 'http://localhost:5000', '--dummy']); - it('dummy resource foos', function() { - return generateInAddon(['resource', 'foos', '--dummy']).catch(function(error) { - expect(error.message).to.include('blueprint does not support ' + - 'generating inside addons.'); - }); - }); + expect(file('server/index.js').content).to.matchSnapshot(); - it('dummy initializer foo', function() { - return generateInAddon(['initializer', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/initializers/foo.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - assertFileToNotExist('app/initializers/foo.js'); - assertFileToNotExist('tests/unit/initializers/foo-test.js'); - }); + expect(file('server/proxies/foo.js').content).to.matchSnapshot(); }); - it('dummy initializer foo/bar', function() { - return generateInAddon(['initializer', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/initializers/foo/bar.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo/bar'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - assertFileToNotExist('app/initializers/foo/bar.js'); - assertFileToNotExist('tests/unit/initializers/foo/bar-test.js'); - }); + it('dummy server', async function () { + await generateInAddon(['server', '--dummy']); + expect(file('server/index.js')).to.exist; }); - it('dummy mixin foo', function() { - return generateInAddon(['mixin', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/mixins/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFileToNotExist('tests/unit/mixins/foo-test.js'); - assertFileToNotExist('app/mixins/foo.js'); + describe('--lang', function () { + // Good: Correct Usage + it('ember addon foo --lang=(valid code): no message + set `lang` in index.html', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--lang=en-US']); + expect(file('tests/dummy/app/index.html')).to.contain(''); }); - }); - it('dummy mixin foo/bar', function() { - return generateInAddon(['mixin', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/mixins/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFileToNotExist('tests/unit/mixins/foo/bar-test.js'); - assertFileToNotExist('app/mixins/foo/bar.js'); + // Edge Case: both valid code AND programming language abbreviation, possible misuse + it('ember addon foo --lang=(valid code + programming language abbreviation): emit warning + set `lang` in index.html', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--lang=css']); + expect(file('tests/dummy/app/index.html')).to.contain(''); }); - }); - it('dummy mixin foo/bar/baz', function() { - return generateInAddon(['mixin', 'foo/bar/baz', '--dummy']).then(function() { - assertFile('tests/dummy/app/mixins/foo/bar/baz.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFileToNotExist('tests/unit/mixins/foo/bar/baz-test.js'); - assertFileToNotExist('app/mixins/foo/bar/baz.js'); + // Misuse: possibly an attempt to set app programming language + it('ember addon foo --lang=(programming language): emit warning + do not set `lang` in index.html', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--lang=JavaScript']); + expect(file('tests/dummy/app/index.html')).to.contain(''); }); - }); - it('dummy adapter application', function() { - return generateInAddon(['adapter', 'application', '--dummy']).then(function() { - assertFile('tests/dummy/app/adapters/application.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFileToNotExist('app/adapters/application.js'); - assertFileToNotExist('tests/unit/adapters/application-test.js'); + // Misuse: possibly an attempt to set app programming language + it('ember addon foo --lang=(programming language abbreviation): emit warning + do not set `lang` in index.html', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--lang=JS']); + expect(file('tests/dummy/app/index.html')).to.contain(''); }); - }); - it('dummy adapter foo', function() { - return generateInAddon(['adapter', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/adapters/foo.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFileToNotExist('app/adapters/foo.js'); - assertFileToNotExist('tests/unit/adapters/foo-test.js'); + // Misuse: possibly an attempt to set app programming language + it('ember addon foo --lang=(programming language file extension): emit warning + do not set `lang` in index.html', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--lang=.js']); + expect(file('tests/dummy/app/index.html')).to.contain(''); }); - }); - it('dummy adapter foo/bar (with base class foo)', function() { - return generateInAddon(['adapter', 'foo/bar', '--base-class=foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/adapters/foo/bar.js', { - contains: [ - "import FooAdapter from \'../foo\';", - "export default FooAdapter.extend({" + EOL + "});" - ] - }); - assertFileToNotExist('app/adapters/foo/bar.js'); - assertFileToNotExist('tests/unit/adapters/foo/bar-test.js'); + // Misuse: Invalid Country Code + it('ember addon foo --lang=(invalid code): emit warning + do not set `lang` in index.html', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--lang=en-UK']); + expect(file('tests/dummy/app/index.html')).to.contain(''); }); }); - - it('dummy adapter-test foo', function() { - return generateInAddon(['adapter-test', 'foo', '--dummy']).then(function() { - assertFile('tests/unit/adapters/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo'" - ] - }); - assertFileToNotExist('app/adapter-test/foo.js'); - }); - }); - - it('dummy serializer foo', function() { - return generateInAddon(['serializer', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/serializers/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFileToNotExist('app/serializers/foo.js'); - assertFileToNotExist('tests/unit/serializers/foo-test.js'); - }); - }); - - it('dummy serializer foo/bar', function() { - return generateInAddon(['serializer', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/serializers/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFileToNotExist('app/serializers/foo/bar.js'); - assertFileToNotExist('tests/unit/serializers/foo/bar-test.js'); - }); - }); - - it('dummy serializer-test foo', function() { - return generateInAddon(['serializer-test', 'foo', '--dummy']).then(function() { - assertFile('tests/unit/serializers/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - assertFileToNotExist('app/serializer-test/foo.js'); - }); - }); - - it('dummy transform foo', function() { - return generateInAddon(['transform', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/transforms/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFileToNotExist('app/transforms/foo.js'); - assertFileToNotExist('tests/unit/transforms/foo-test.js'); - }); - }); - - it('dummy transform foo/bar', function() { - return generateInAddon(['transform', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/transforms/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - '' + EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFileToNotExist('app/transforms/foo/bar.js'); - assertFileToNotExist('tests/unit/transforms/foo/bar-test.js'); - }); - }); - - it('dummy util foo-bar', function() { - return generateInAddon(['util', 'foo-bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/utils/foo-bar.js', { - contains: 'export default function fooBar() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFileToNotExist('app/utils/foo-bar.js'); - assertFileToNotExist('tests/unit/utils/foo-bar-test.js'); - }); - }); - - it('dummy util foo-bar/baz', function() { - return generateInAddon(['util', 'foo/bar-baz', '--dummy']).then(function() { - assertFile('tests/dummy/app/utils/foo/bar-baz.js', { - contains: 'export default function fooBarBaz() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFileToNotExist('app/utils/foo/bar-baz.js'); - assertFileToNotExist('tests/unit/utils/foo/bar-baz-test.js'); - }); - }); - - it('dummy service foo', function() { - return generateInAddon(['service', 'foo', '--dummy']).then(function() { - assertFile('tests/dummy/app/services/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFileToNotExist('app/services/foo.js'); - assertFileToNotExist('tests/unit/services/foo-test.js'); - }); - }); - - it('dummy service foo/bar', function() { - return generateInAddon(['service', 'foo/bar', '--dummy']).then(function() { - assertFile('tests/dummy/app/services/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFileToNotExist('app/services/foo/bar.js'); - assertFileToNotExist('tests/unit/services/foo/bar-test.js'); - }); - }); - - - it('dummy service-test foo', function() { - return generateInAddon(['service-test', 'foo', '--dummy']).then(function() { - assertFile('tests/unit/services/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo'" - ] - }); - assertFileToNotExist('app/service-test/foo.js'); - }); - }); - - it('dummy blueprint foo', function() { - return generateInAddon(['blueprint', 'foo', '--dummy']).then(function() { - assertFile('blueprints/foo/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); - - it('dummy blueprint foo/bar', function() { - return generateInAddon(['blueprint', 'foo/bar', '--dummy']).then(function() { - assertFile('blueprints/foo/bar/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); - - it('dummy http-mock foo', function() { - return generateInAddon(['http-mock', 'foo', '--dummy']).then(function() { - assertFile('server/index.js', { - contains:"mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooRouter = express.Router();" + EOL + - EOL + - " fooRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo', fooRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('dummy http-mock foo-bar', function() { - return generateInAddon(['http-mock', 'foo-bar', '--dummy']).then(function() { - assertFile('server/index.js', { - contains: "mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo-bar.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooBarRouter = express.Router();" + EOL + - EOL + - " fooBarRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo-bar', fooBarRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('dummy http-proxy foo', function() { - return generateInAddon(['http-proxy', 'foo', 'http://localhost:5000', '--dummy']).then(function() { - assertFile('server/index.js', { - contains: "proxies.forEach(function(route) { route(app); });" - }); - assertFile('server/proxies/foo.js', { - contains: "var proxyPath = '/foo';" + EOL + - EOL + - "module.exports = function(app) {" + EOL + - " // For options, see:" + EOL + - " // https://github.com/nodejitsu/node-http-proxy" + EOL + - " var proxy = require('http-proxy').createProxyServer({});" + EOL + - EOL + - " proxy.on('error', function(err, req) {" + EOL + - " console.error(err, req.url);" + EOL + - " });" + EOL + - EOL + - " app.use(proxyPath, function(req, res, next){" + EOL + - " // include root path in proxied request" + EOL + - " req.url = proxyPath + '/' + req.url;" + EOL + - " proxy.web(req, res, { target: 'http://localhost:5000' });" + EOL + - " });" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('dummy server', function() { - return generateInAddon(['server', '--dummy']).then(function() { - assertFile('server/index.js'); - assertFile('server/.jshintrc'); - }); - }); - - it('dummy acceptance-test foo', function() { - return generateInAddon(['acceptance-test', 'foo', '--dummy']).then(function() { - var expected = path.join(__dirname, '../fixtures/generate/addon-acceptance-test-expected.js'); - - assertFileEquals('tests/acceptance/foo-test.js', expected); - assertFileToNotExist('app/acceptance-tests/foo.js'); - }); - }); - - it('dummy acceptance-test foo/bar', function() { - return generateInAddon(['acceptance-test', 'foo/bar', '--dummy']).then(function() { - var expected = path.join(__dirname, '../fixtures/generate/addon-acceptance-test-nested-expected.js'); - - assertFileEquals('tests/acceptance/foo/bar-test.js', expected); - assertFileToNotExist('app/acceptance-tests/foo/bar.js'); - }); - }); - }); diff --git a/tests/acceptance/addon-dummy-generate-test.js.snap b/tests/acceptance/addon-dummy-generate-test.js.snap new file mode 100644 index 0000000000..9f37c25e4d --- /dev/null +++ b/tests/acceptance/addon-dummy-generate-test.js.snap @@ -0,0 +1,327 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Acceptance: ember generate in-addon-dummy dummy blueprint foo 1`] = ` +"'use strict'; + +module.exports = { + description: '' + + // locals(options) { + // // Return custom template variables here. + // return { + // foo: options.entity.options.foo + // }; + // } + + // afterInstall(options) { + // // Perform extra work here. + // } +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy blueprint foo/bar 1`] = ` +"'use strict'; + +module.exports = { + description: '' + + // locals(options) { + // // Return custom template variables here. + // return { + // foo: options.entity.options.foo + // }; + // } + + // afterInstall(options) { + // // Perform extra work here. + // } +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-mock foo 1`] = ` +"'use strict'; + +// To use it create some files under \`mocks/\` +// e.g. \`server/mocks/ember-hamsters.js\` +// +// module.exports = function(app) { +// app.get('/ember-hamsters', function(req, res) { +// res.send('hello'); +// }); +// }; + +module.exports = function(app) { + const globSync = require('glob').sync; + const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); + const proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); + + // Log proxy requests + const morgan = require('morgan'); + app.use(morgan('dev')); + + mocks.forEach(route => route(app)); + proxies.forEach(route => route(app)); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-mock foo 2`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooRouter = express.Router(); + + fooRouter.get('/', function(req, res) { + res.send({ + 'foo': [] + }); + }); + + fooRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooRouter.get('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.put('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo', require('body-parser').json()); + app.use('/api/foo', fooRouter); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-mock foo 3`] = ` +"{ + \\"node\\": true +} +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-mock foo-bar 1`] = ` +"'use strict'; + +// To use it create some files under \`mocks/\` +// e.g. \`server/mocks/ember-hamsters.js\` +// +// module.exports = function(app) { +// app.get('/ember-hamsters', function(req, res) { +// res.send('hello'); +// }); +// }; + +module.exports = function(app) { + const globSync = require('glob').sync; + const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); + const proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); + + // Log proxy requests + const morgan = require('morgan'); + app.use(morgan('dev')); + + mocks.forEach(route => route(app)); + proxies.forEach(route => route(app)); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-mock foo-bar 2`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooBarRouter = express.Router(); + + fooBarRouter.get('/', function(req, res) { + res.send({ + 'foo-bar': [] + }); + }); + + fooBarRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooBarRouter.get('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.put('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo-bar', require('body-parser').json()); + app.use('/api/foo-bar', fooBarRouter); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-mock foo-bar 3`] = ` +"{ + \\"node\\": true +} +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-proxy foo 1`] = ` +"'use strict'; + +// To use it create some files under \`mocks/\` +// e.g. \`server/mocks/ember-hamsters.js\` +// +// module.exports = function(app) { +// app.get('/ember-hamsters', function(req, res) { +// res.send('hello'); +// }); +// }; + +module.exports = function(app) { + const globSync = require('glob').sync; + const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); + const proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); + + // Log proxy requests + const morgan = require('morgan'); + app.use(morgan('dev')); + + mocks.forEach(route => route(app)); + proxies.forEach(route => route(app)); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-proxy foo 2`] = ` +"'use strict'; + +const proxyPath = '/foo'; + +module.exports = function(app) { + // For options, see: + // https://github.com/nodejitsu/node-http-proxy + let proxy = require('http-proxy').createProxyServer({}); + + proxy.on('error', function(err, req) { + console.error(err, req.url); + }); + + app.use(proxyPath, function(req, res, next){ + // include root path in proxied request + req.url = proxyPath + '/' + req.url; + proxy.web(req, res, { target: 'http://localhost:5000' }); + }); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-proxy foo 3`] = ` +"{ + \\"node\\": true +} +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-proxy foo 4`] = ` +"'use strict'; + +// To use it create some files under \`mocks/\` +// e.g. \`server/mocks/ember-hamsters.js\` +// +// module.exports = function(app) { +// app.get('/ember-hamsters', function(req, res) { +// res.send('hello'); +// }); +// }; + +module.exports = function(app) { + const globSync = require('glob').sync; + const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); + const proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); + + // Log proxy requests + const morgan = require('morgan'); + app.use(morgan('dev')); + + mocks.forEach(route => route(app)); + proxies.forEach(route => route(app)); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-proxy foo 5`] = ` +"'use strict'; + +const proxyPath = '/foo'; + +module.exports = function(app) { + // For options, see: + // https://github.com/nodejitsu/node-http-proxy + let proxy = require('http-proxy').createProxyServer({}); + + proxy.on('error', function(err, req) { + console.error(err, req.url); + }); + + app.use(proxyPath, function(req, res, next){ + // include root path in proxied request + req.url = proxyPath + '/' + req.url; + proxy.web(req, res, { target: 'http://localhost:5001' }); + }); +}; +" +`; + +exports[`Acceptance: ember generate in-addon-dummy dummy http-proxy foo 6`] = ` +"{ + \\"node\\": true +} +" +`; diff --git a/tests/acceptance/addon-generate-test.js b/tests/acceptance/addon-generate-test.js index 227404f4b4..007a5025ff 100644 --- a/tests/acceptance/addon-generate-test.js +++ b/tests/acceptance/addon-generate-test.js @@ -1,1072 +1,144 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../../lib/ext/promise'); -var assertFile = require('../helpers/assert-file'); -var assertFileEquals = require('../helpers/assert-file-equals'); -var assertFileToNotExist = require('../helpers/assert-file-to-not-exist'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var fs = require('fs-extra'); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var EOL = require('os').EOL; -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); -var expect = require('chai').expect; +const ember = require('../helpers/ember'); +const path = require('path'); +const fs = require('fs-extra'); +let root = process.cwd(); +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); +const tmp = require('tmp-promise'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); -describe('Acceptance: ember generate in-addon', function() { +describe('Acceptance: ember generate in-addon', function () { this.timeout(20000); - var tmpdir; - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); }); - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); }); - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); }); - afterEach(function() { + afterEach(function () { process.chdir(root); - return remove(tmproot); }); function initAddon(name) { - return ember([ - 'addon', - name, - '--skip-npm', - '--skip-bower' - ]); + return ember(['addon', name, '--skip-npm']); } function generateInAddon(args) { - var name = 'my-addon'; - var generateArgs = ['generate'].concat(args); + let name = 'my-addon'; + let generateArgs = ['generate'].concat(args); if (arguments.length > 1) { name = arguments[1]; } - return initAddon(name).then(function() { + return initAddon(name).then(function () { return ember(generateArgs); }); } - it('in-addon controller foo', function() { - return generateInAddon(['controller', 'foo']).then(function() { - assertFile('addon/controllers/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('app/controllers/foo.js', { - contains: [ - "export { default } from 'my-addon/controllers/foo';" - ] - }); - assertFile('tests/unit/controllers/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo'" - ] - }); - }); + it('in-addon addon-import cannot be called directly', async function () { + try { + await generateInAddon(['addon-import', 'foo']); + } catch (error) { + expect(error.name).to.equal('SilentError'); + expect(error.message).to.equal('You cannot call the addon-import blueprint directly.'); + } }); - it('in-addon controller foo/bar', function() { - return generateInAddon(['controller', 'foo/bar']).then(function() { - assertFile('addon/controllers/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('app/controllers/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/controllers/foo/bar';" - ] - }); - assertFile('tests/unit/controllers/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo/bar'" - ] - }); - }); - }); + it('runs the `addon-import` blueprint from a classic addon', async function () { + await initAddon('my-addon'); - it('in-addon component x-foo', function() { - return generateInAddon(['component', 'x-foo']).then(function() { - assertFile('addon/components/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "import layout from '../templates/components/x-foo';", - "export default Ember.Component.extend({", - "layout: layout", - "});" - ] - }); - assertFile('addon/templates/components/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFile('app/components/x-foo.js', { - contains: [ - "export { default } from 'my-addon/components/x-foo';" - ] - }); - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); + await fs.outputFile( + 'blueprints/service/files/__root__/__path__/__name__.js', + "import Service from '@ember/service';\n" + 'export default Service.extend({ });\n' + ); - it('in-addon component-test x-foo', function() { - return generateInAddon(['component-test', 'x-foo']).then(function() { - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - assertFileToNotExist('app/component-test/x-foo.js'); - }); - }); + await ember(['generate', 'service', 'session']); - it('in-addon component-test x-foo --unit', function() { - return generateInAddon(['component-test', 'x-foo', '--unit']).then(function() { - assertFile('tests/unit/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "moduleForComponent('x-foo'", - "unit: true" - ] - }); - assertFileToNotExist('app/component-test/x-foo.js'); - }); + expect(file('app/services/session.js')).to.exist; }); - it('in-addon component nested/x-foo', function() { - return generateInAddon(['component', 'nested/x-foo']).then(function() { - assertFile('addon/components/nested/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "import layout from '../../templates/components/nested/x-foo';", - "export default Ember.Component.extend({", - "layout: layout", - "});" - ] - }); - assertFile('addon/templates/components/nested/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFile('app/components/nested/x-foo.js', { - contains: [ - "export { default } from 'my-addon/components/nested/x-foo';" - ] - }); - assertFile('tests/integration/components/nested/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('nested/x-foo'", - "integration: true", - "{{nested/x-foo}}", - "{{#nested/x-foo}}" - ] - }); - }); - }); + it('runs a custom "*-addon" blueprint from a classic addon', async function () { + await initAddon('my-addon'); - it('in-addon helper foo-bar', function() { - return generateInAddon(['helper', 'foo-bar']).then(function() { - assertFile('addon/helpers/foo-bar.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBar(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBar);" - }); - assertFile('app/helpers/foo-bar.js', { - contains: [ - "export { default, fooBar } from 'my-addon/helpers/foo-bar';" - ] - }); - assertFile('tests/unit/helpers/foo-bar-test.js', { - contains: "import { fooBar } from '../../../helpers/foo-bar';" - }); - }); - }); + await fs.outputFile( + 'blueprints/service/files/__root__/__path__/__name__.js', + "import Service from '@ember/service';\n" + 'export default Service.extend({ });\n' + ); - it('in-addon helper foo/bar-baz', function() { - return generateInAddon(['helper', 'foo/bar-baz']).then(function() { - assertFile('addon/helpers/foo/bar-baz.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBarBaz(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBarBaz);" - }); - assertFile('app/helpers/foo/bar-baz.js', { - contains: [ - "export { default, fooBarBaz } from 'my-addon/helpers/foo/bar-baz';" - ] - }); - assertFile('tests/unit/helpers/foo/bar-baz-test.js', { - contains: "import { fooBarBaz } from '../../../../helpers/foo/bar-baz';" - }); - }); - }); + await fs.outputFile( + 'blueprints/service-addon/files/app/services/session.js', + "export { default } from 'somewhere';\n" + ); - it('in-addon model foo', function() { - return generateInAddon(['model', 'foo']).then(function() { - assertFile('addon/models/foo.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('app/models/foo.js', { - contains: [ - "export { default } from 'my-addon/models/foo';" - ] - }); - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - }); - }); + await ember(['generate', 'service', 'session']); - it('in-addon model foo with attributes', function() { - return generateInAddon([ - 'model', - 'foo', - 'noType', - 'firstName:string', - 'created_at:date', - 'is-published:boolean', - 'rating:number', - 'bars:has-many', - 'baz:belongs-to', - 'echo:hasMany', - 'bravo:belongs_to', - 'foo-names:has-many', - 'barName:has-many', - 'bazName:belongs-to', - 'test-name:belongs-to', - 'echoName:hasMany', - 'bravoName:belongs_to' - ]).then(function() { - assertFile('addon/models/foo.js', { - contains: [ - "noType: DS.attr()", - "firstName: DS.attr('string')", - "createdAt: DS.attr('date')", - "isPublished: DS.attr('boolean')", - "rating: DS.attr('number')", - "bars: DS.hasMany('bar')", - "baz: DS.belongsTo('baz')", - "echos: DS.hasMany('echo')", - "bravo: DS.belongsTo('bravo')", - "fooNames: DS.hasMany('foo-name')", - "barNames: DS.hasMany('bar-name')", - "bazName: DS.belongsTo('baz-name')", - "testName: DS.belongsTo('test-name')", - "echoNames: DS.hasMany('echo-name')", - "bravoName: DS.belongsTo('bravo-name')" - ] - }); - assertFile('app/models/foo.js', { - contains: [ - "export { default } from 'my-addon/models/foo';" - ] - }); - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "needs: [", - "'model:bar',", - "'model:baz',", - "'model:echo',", - "'model:bravo',", - "'model:foo-name',", - "'model:bar-name',", - "'model:baz-name',", - "'model:echo-name',", - "'model:test-name',", - "'model:bravo-name'", - "]" - ] - }); - }); + expect(file('app/services/session.js')).to.exist; }); - it('in-addon model foo/bar', function() { - return generateInAddon(['model', 'foo/bar']).then(function() { - assertFile('addon/models/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('app/models/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/models/foo/bar';" - ] - }); - assertFile('tests/unit/models/foo/bar-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); + it('in-addon blueprint foo', async function () { + await generateInAddon(['blueprint', 'foo']); - it('in-addon model-test foo', function() { - return generateInAddon(['model-test', 'foo']).then(function() { - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - assertFileToNotExist('app/model-test/foo.js'); - }); + expect(file('blueprints/foo/index.js').content).to.matchSnapshot(); }); - it('in-addon route foo', function() { - return generateInAddon(['route', 'foo']).then(function() { - assertFile('addon/routes/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('app/routes/foo.js', { - contains: [ - "export { default } from 'my-addon/routes/foo';" - ] - }); - assertFile('addon/templates/foo.hbs', { - contains: '{{outlet}}' - }); - assertFile('app/templates/foo.js', { - contains: "export { default } from 'my-addon/templates/foo';" - }); - assertFile('tests/unit/routes/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - assertFile('tests/dummy/app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); - }); + it('in-addon blueprint foo/bar', async function () { + await generateInAddon(['blueprint', 'foo/bar']); - it('in-addon route foo/bar', function() { - return generateInAddon(['route', 'foo/bar']).then(function() { - assertFile('addon/routes/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('app/routes/foo/bar.js', { - contains: "export { default } from 'my-addon/routes/foo/bar';" - }); - assertFile('app/templates/foo/bar.js', { - contains: "export { default } from 'my-addon/templates/foo/bar';" - }); - assertFile('tests/unit/routes/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo/bar'" - ] - }); - assertFile('tests/dummy/app/router.js', { - doesNotContain: "this.route('bar');" - }); - }); + expect(file('blueprints/foo/bar/index.js').content).to.matchSnapshot(); }); - it('in-addon route-test foo', function() { - return generateInAddon(['route-test', 'foo']).then(function() { - assertFile('tests/unit/routes/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - assertFileToNotExist('app/route-test/foo.js'); - }); - }); + it('in-addon http-mock foo', async function () { + await generateInAddon(['http-mock', 'foo']); - it('in-addon template foo', function() { - return generateInAddon(['template', 'foo']).then(function() { - assertFile('addon/templates/foo.hbs'); - }); - }); + expect(file('server/index.js')).to.contain('mocks.forEach(route => route(app));'); - it('in-addon template foo/bar', function() { - return generateInAddon(['template', 'foo/bar']).then(function() { - assertFile('addon/templates/foo/bar.hbs'); - }); + expect(file('server/mocks/foo.js').content).to.matchSnapshot(); }); - it('in-addon view foo', function() { - return generateInAddon(['view', 'foo']).then(function() { - assertFile('addon/views/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('app/views/foo.js', { - contains: [ - "export { default } from 'my-addon/views/foo';" - ] - }); - assertFile('tests/unit/views/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo'" - ] - }); - }); - }); + it('in-addon http-mock foo-bar', async function () { + await generateInAddon(['http-mock', 'foo-bar']); - it('in-addon view foo/bar', function() { - return generateInAddon(['view', 'foo/bar']).then(function() { - assertFile('addon/views/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('app/views/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/views/foo/bar';" - ] - }); - assertFile('tests/unit/views/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo/bar'" - ] - }); - }); - }); + expect(file('server/index.js')).to.contain('mocks.forEach(route => route(app));'); - it('in-addon resource foos', function() { - return generateInAddon(['resource', 'foos']).catch(function(error) { - expect(error.message).to.include('blueprint does not support ' + - 'generating inside addons.'); - }); + expect(file('server/mocks/foo-bar.js').content).to.matchSnapshot(); }); - it('in-addon initializer foo', function() { - return generateInAddon(['initializer', 'foo']).then(function() { - assertFile('addon/initializers/foo.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - assertFile('app/initializers/foo.js', { - contains: [ - "export { default, initialize } from 'my-addon/initializers/foo';" - ] - }); - assertFile('tests/unit/initializers/foo-test.js'); - }); - }); + it('in-addon http-proxy foo', async function () { + await generateInAddon(['http-proxy', 'foo', 'http://localhost:5000']); - it('in-addon initializer foo/bar', function() { - return generateInAddon(['initializer', 'foo/bar']).then(function() { - assertFile('addon/initializers/foo/bar.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo/bar'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - assertFile('app/initializers/foo/bar.js', { - contains: [ - "export { default, initialize } from 'my-addon/initializers/foo/bar';" - ] - }); - assertFile('tests/unit/initializers/foo/bar-test.js'); - }); - }); - - it('in-addon mixin foo', function() { - return generateInAddon(['mixin', 'foo']).then(function() { - assertFile('addon/mixins/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo-test.js', { - contains: [ - "import FooMixin from '../../../mixins/foo';" - ] - }); - assertFileToNotExist('app/mixins/foo.js'); - }); - }); + expect(file('server/index.js')).to.contain('proxies.forEach(route => route(app));'); - it('in-addon mixin foo/bar', function() { - return generateInAddon(['mixin', 'foo/bar']).then(function() { - assertFile('addon/mixins/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo/bar-test.js', { - contains: [ - "import FooBarMixin from '../../../mixins/foo/bar';" - ] - }); - assertFileToNotExist('app/mixins/foo/bar.js'); - }); + expect(file('server/proxies/foo.js').content).to.matchSnapshot(); }); - it('in-addon mixin foo/bar/baz', function() { - return generateInAddon(['mixin', 'foo/bar/baz']).then(function() { - assertFile('addon/mixins/foo/bar/baz.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo/bar/baz-test.js', { - contains: [ - "import FooBarBazMixin from '../../../mixins/foo/bar/baz';" - ] - }); - assertFileToNotExist('app/mixins/foo/bar/baz.js'); - }); + it('in-addon server', async function () { + await generateInAddon(['server']); + expect(file('server/index.js')).to.exist; }); - it('in-addon adapter application', function() { - return generateInAddon(['adapter', 'application']).then(function() { - assertFile('addon/adapters/application.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFile('app/adapters/application.js', { - contains: [ - "export { default } from 'my-addon/adapters/application';" - ] - }); - assertFile('tests/unit/adapters/application-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:application'" - ] - }); - }); - }); + it('successfully generates the default blueprint for scoped addons', async function () { + await initAddon('@foo/bar'); + await ember(['g', 'blueprint', '@foo/bar']); + await fs.outputFile('blueprints/@foo/bar/files/__name__.js', ''); + await ember(['g', '@foo/bar', 'baz']); - it('in-addon adapter foo', function() { - return generateInAddon(['adapter', 'foo']).then(function() { - assertFile('addon/adapters/foo.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFile('app/adapters/foo.js', { - contains: [ - "export { default } from 'my-addon/adapters/foo';" - ] - }); - assertFile('tests/unit/adapters/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo'" - ] - }); - }); + expect(file('baz.js')).to.exist; }); - it('in-addon adapter foo/bar (with base class foo)', function() { - return generateInAddon(['adapter', 'foo/bar', '--base-class=foo']).then(function() { - assertFile('addon/adapters/foo/bar.js', { - contains: [ - "import FooAdapter from \'../foo\';", - "export default FooAdapter.extend({" + EOL + "});" - ] - }); - assertFile('app/adapters/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/adapters/foo/bar';" - ] - }); - assertFile('tests/unit/adapters/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo/bar'" - ] - }); - }); + it(`throws the unknown blueprint error when \`name\` matches a folder's name, but doesn't include the \`${path.sep}\` char`, async function () { + await expect(generateInAddon(['tests'])).to.be.rejectedWith('Unknown blueprint: tests'); }); - - it('in-addon adapter-test foo', function() { - return generateInAddon(['adapter-test', 'foo']).then(function() { - assertFile('tests/unit/adapters/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo'" - ] - }); - assertFileToNotExist('app/adapter-test/foo.js'); - }); - }); - - it('in-addon serializer foo', function() { - return generateInAddon(['serializer', 'foo']).then(function() { - assertFile('addon/serializers/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('app/serializers/foo.js', { - contains: [ - "export { default } from 'my-addon/serializers/foo';" - ] - }); - assertFile('tests/unit/serializers/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - ] - }); - }); - }); - - it('in-addon serializer foo/bar', function() { - return generateInAddon(['serializer', 'foo/bar']).then(function() { - assertFile('addon/serializers/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('app/serializers/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/serializers/foo/bar';" - ] - }); - assertFile('tests/unit/serializers/foo/bar-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); - - it('in-addon serializer-test foo', function() { - return generateInAddon(['serializer-test', 'foo']).then(function() { - assertFile('tests/unit/serializers/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - assertFileToNotExist('app/serializer-test/foo.js'); - }); - }); - - it('in-addon transform foo', function() { - return generateInAddon(['transform', 'foo']).then(function() { - assertFile('addon/transforms/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('app/transforms/foo.js', { - contains: [ - "export { default } from 'my-addon/transforms/foo';" - ] - }); - assertFile('tests/unit/transforms/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo'" - ] - }); - }); - }); - - it('in-addon transform foo/bar', function() { - return generateInAddon(['transform', 'foo/bar']).then(function() { - assertFile('addon/transforms/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - '' + EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('app/transforms/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/transforms/foo/bar';" - ] - }); - assertFile('tests/unit/transforms/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo/bar'" - ] - }); - }); - }); - - it('in-addon util foo-bar', function() { - return generateInAddon(['util', 'foo-bar']).then(function() { - assertFile('addon/utils/foo-bar.js', { - contains: 'export default function fooBar() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('app/utils/foo-bar.js', { - contains: [ - "export { default } from 'my-addon/utils/foo-bar';" - ] - }); - assertFile('tests/unit/utils/foo-bar-test.js', { - contains: [ - "import fooBar from '../../../utils/foo-bar';" - ] - }); - }); - }); - - it('in-addon util foo-bar/baz', function() { - return generateInAddon(['util', 'foo/bar-baz']).then(function() { - assertFile('addon/utils/foo/bar-baz.js', { - contains: 'export default function fooBarBaz() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('app/utils/foo/bar-baz.js', { - contains: [ - "export { default } from 'my-addon/utils/foo/bar-baz';" - ] - }); - assertFile('tests/unit/utils/foo/bar-baz-test.js', { - contains: [ - "import fooBarBaz from '../../../utils/foo/bar-baz';" - ] - }); - }); - }); - - it('in-addon service foo', function() { - return generateInAddon(['service', 'foo']).then(function() { - assertFile('addon/services/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('app/services/foo.js', { - contains: [ - "export { default } from 'my-addon/services/foo';" - ] - }); - assertFile('tests/unit/services/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo'" - ] - }); - }); - }); - - it('in-addon service foo/bar', function() { - return generateInAddon(['service', 'foo/bar']).then(function() { - assertFile('addon/services/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('app/services/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/services/foo/bar';" - ] - }); - assertFile('tests/unit/services/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo/bar'" - ] - }); - }); - }); - - - it('in-addon service-test foo', function() { - return generateInAddon(['service-test', 'foo']).then(function() { - assertFile('tests/unit/services/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo'" - ] - }); - assertFileToNotExist('app/service-test/foo.js'); - }); - }); - - it('in-addon blueprint foo', function() { - return generateInAddon(['blueprint', 'foo']).then(function() { - assertFile('blueprints/foo/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); - - it('in-addon blueprint foo/bar', function() { - return generateInAddon(['blueprint', 'foo/bar']).then(function() { - assertFile('blueprints/foo/bar/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); - - it('in-addon http-mock foo', function() { - return generateInAddon(['http-mock', 'foo']).then(function() { - assertFile('server/index.js', { - contains:"mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooRouter = express.Router();" + EOL + - EOL + - " fooRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo', fooRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('in-addon http-mock foo-bar', function() { - return generateInAddon(['http-mock', 'foo-bar']).then(function() { - assertFile('server/index.js', { - contains: "mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo-bar.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooBarRouter = express.Router();" + EOL + - EOL + - " fooBarRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo-bar', fooBarRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('in-addon http-proxy foo', function() { - return generateInAddon(['http-proxy', 'foo', 'http://localhost:5000']).then(function() { - assertFile('server/index.js', { - contains: "proxies.forEach(function(route) { route(app); });" - }); - assertFile('server/proxies/foo.js', { - contains: "var proxyPath = '/foo';" + EOL + - EOL + - "module.exports = function(app) {" + EOL + - " // For options, see:" + EOL + - " // https://github.com/nodejitsu/node-http-proxy" + EOL + - " var proxy = require('http-proxy').createProxyServer({});" + EOL + - EOL + - " proxy.on('error', function(err, req) {" + EOL + - " console.error(err, req.url);" + EOL + - " });" + EOL + - EOL + - " app.use(proxyPath, function(req, res, next){" + EOL + - " // include root path in proxied request" + EOL + - " req.url = proxyPath + '/' + req.url;" + EOL + - " proxy.web(req, res, { target: 'http://localhost:5000' });" + EOL + - " });" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('in-addon server', function() { - return generateInAddon(['server']).then(function() { - assertFile('server/index.js'); - assertFile('server/.jshintrc'); - }); - }); - - it('in-addon acceptance-test foo', function() { - return generateInAddon(['acceptance-test', 'foo']).then(function() { - var expected = path.join(__dirname, '../fixtures/generate/addon-acceptance-test-expected.js'); - - assertFileEquals('tests/acceptance/foo-test.js', expected); - assertFileToNotExist('app/acceptance-tests/foo.js'); - }); - }); - - it('in-addon acceptance-test foo/bar', function() { - return generateInAddon(['acceptance-test', 'foo/bar']).then(function() { - var expected = path.join(__dirname, '../fixtures/generate/addon-acceptance-test-nested-expected.js'); - - assertFileEquals('tests/acceptance/foo/bar-test.js', expected); - assertFileToNotExist('app/acceptance-tests/foo/bar.js'); - }); - }); }); diff --git a/tests/acceptance/addon-generate-test.js.snap b/tests/acceptance/addon-generate-test.js.snap new file mode 100644 index 0000000000..4b0df0c9c2 --- /dev/null +++ b/tests/acceptance/addon-generate-test.js.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Acceptance: ember generate in-addon in-addon blueprint foo 1`] = ` +"'use strict'; + +module.exports = { + description: '' + + // locals(options) { + // // Return custom template variables here. + // return { + // foo: options.entity.options.foo + // }; + // } + + // afterInstall(options) { + // // Perform extra work here. + // } +}; +" +`; + +exports[`Acceptance: ember generate in-addon in-addon blueprint foo/bar 1`] = ` +"'use strict'; + +module.exports = { + description: '' + + // locals(options) { + // // Return custom template variables here. + // return { + // foo: options.entity.options.foo + // }; + // } + + // afterInstall(options) { + // // Perform extra work here. + // } +}; +" +`; + +exports[`Acceptance: ember generate in-addon in-addon http-mock foo 1`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooRouter = express.Router(); + + fooRouter.get('/', function(req, res) { + res.send({ + 'foo': [] + }); + }); + + fooRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooRouter.get('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.put('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo', require('body-parser').json()); + app.use('/api/foo', fooRouter); +}; +" +`; + +exports[`Acceptance: ember generate in-addon in-addon http-mock foo-bar 1`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooBarRouter = express.Router(); + + fooBarRouter.get('/', function(req, res) { + res.send({ + 'foo-bar': [] + }); + }); + + fooBarRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooBarRouter.get('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.put('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo-bar', require('body-parser').json()); + app.use('/api/foo-bar', fooBarRouter); +}; +" +`; + +exports[`Acceptance: ember generate in-addon in-addon http-proxy foo 1`] = ` +"'use strict'; + +const proxyPath = '/foo'; + +module.exports = function(app) { + // For options, see: + // https://github.com/nodejitsu/node-http-proxy + let proxy = require('http-proxy').createProxyServer({}); + + proxy.on('error', function(err, req) { + console.error(err, req.url); + }); + + app.use(proxyPath, function(req, res, next){ + // include root path in proxied request + req.url = proxyPath + '/' + req.url; + proxy.web(req, res, { target: 'http://localhost:5000' }); + }); +}; +" +`; diff --git a/tests/acceptance/addon-smoke-test-slow.js b/tests/acceptance/addon-smoke-test-slow.js index 09c75819f5..9178e0db45 100644 --- a/tests/acceptance/addon-smoke-test-slow.js +++ b/tests/acceptance/addon-smoke-test-slow.js @@ -1,232 +1,173 @@ 'use strict'; -var Promise = require('../../lib/ext/promise'); -var path = require('path'); -var fs = require('fs-extra'); -var remove = Promise.denodeify(fs.remove); -var expect = require('chai').expect; -var addonName = 'some-cool-addon'; -var spawn = require('child_process').spawn; -var chalk = require('chalk'); -var expect = require('chai').expect; - -var runCommand = require('../helpers/run-command'); -var copyFixtureFiles = require('../helpers/copy-fixture-files'); -var killCliProcess = require('../helpers/kill-cli-process'); -var assertDirEmpty = require('../helpers/assert-dir-empty'); -var acceptance = require('../helpers/acceptance'); -var createTestTargets = acceptance.createTestTargets; -var teardownTestTargets = acceptance.teardownTestTargets; -var linkDependencies = acceptance.linkDependencies; -var cleanupRun = acceptance.cleanupRun; - -describe('Acceptance: addon-smoke-test', function() { +const path = require('path'); +const fs = require('fs-extra'); +const spawn = require('child_process').spawn; +const { execa } = require('execa'); +const { default: chalk } = require('chalk'); + +const runCommand = require('../helpers/run-command'); +const copyFixtureFiles = require('../helpers/copy-fixture-files'); +const { createAndInstallTestTargets } = require('../helpers/acceptance'); + +let root = path.resolve(__dirname, '..', '..'); + +const { expect } = require('chai'); +const { dir } = require('chai-files'); + +let addonName = 'some-cool-addon'; +let addonRoot; +let testSetup; + +describe('Acceptance: addon-smoke-test', function () { this.timeout(450000); - before(function() { - return createTestTargets(addonName, { - command: 'addon' - }); + beforeEach(async function () { + testSetup = await createAndInstallTestTargets(addonName, { command: 'addon' }); + addonRoot = testSetup.path; + process.chdir(addonRoot); + process.env.JOBS = '1'; }); - after(function() { - return teardownTestTargets(); + afterEach(async function () { + delete process.env.JOBS; + runCommand.killAll(); + process.chdir(root); + await testSetup.cleanup(); + expect(dir(addonRoot)).to.not.exist; }); - beforeEach(function() { - return linkDependencies(addonName); + it('generates package.json with proper metadata', function () { + let packageContents = fs.readJsonSync('package.json'); + + expect(packageContents.name).to.equal(addonName); + expect(packageContents.private).to.be.an('undefined'); + expect(packageContents.keywords).to.deep.equal(['ember-addon']); + expect(packageContents['ember-addon']).to.deep.equal({ configPath: 'tests/dummy/config' }); }); - afterEach(function() { - return cleanupRun().then(function() { - assertDirEmpty('tmp'); - }); + it('ember addon foo, clean from scratch', async function () { + let result = await runCommand('node_modules/ember-cli/bin/ember', 'test'); + expect(result.code).to.eql(0); }); - it('generates package.json and bower.json with proper metadata', function() { - var packageContents = JSON.parse(fs.readFileSync('package.json', { encoding: 'utf8' })); + it('works in most common scenarios for an example addon', async function () { + await copyFixtureFiles('addon/kitchen-sink'); - expect(packageContents.name).to.equal(addonName); - expect(packageContents.private).to.be.an('undefined'); - expect(packageContents.keywords).to.deep.equal([ 'ember-addon' ]); - expect(packageContents['ember-addon']).to.deep.equal({ 'configPath': 'tests/dummy/config' }); + let packageJsonPath = path.join(addonRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); - var bowerContents = JSON.parse(fs.readFileSync('bower.json', { encoding: 'utf8' })); + expect(packageJson.devDependencies['ember-source']).to.not.be.empty; - expect(bowerContents.name).to.equal(addonName); - }); + packageJson.dependencies = packageJson.dependencies || {}; + // add HTMLBars for templates (generators do this automatically when components/templates are added) + packageJson.dependencies['ember-cli-htmlbars'] = 'latest'; - it('ember addon foo, clean from scratch', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); - }); + fs.writeJsonSync(packageJsonPath, packageJson); - it('ember addon without addon/ directory', function() { - return remove('addon') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'server', '--port=54323','--live-reload=false', { - onOutput: function(string, child) { - if (string.match(/Build successful/)) { - killCliProcess(child); - } - } - }) - .catch(function() { - // just eat the rejection as we are testing what happens - }); - }); - }); + let result = await runCommand('node_modules/ember-cli/bin/ember', 'build'); - it('can render a component with a manually imported template', function() { - return copyFixtureFiles('addon/component-with-template') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', addonName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.dependencies['ember-cli-htmlbars'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); - }); - }); + expect(result.code).to.eql(0); + let contents; - it('can add things to `{{content-for "head"}}` section', function() { - return copyFixtureFiles('addon/content-for-head') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - }) - .then(function() { - var indexPath = path.join('dist', 'index.html'); - var contents = fs.readFileSync(indexPath, { encoding: 'utf8' }); - - expect(contents).to.contain('"SOME AWESOME STUFF"'); - }); - }); + let indexPath = path.join(addonRoot, 'dist', 'index.html'); + contents = fs.readFileSync(indexPath, { encoding: 'utf8' }); + expect(contents).to.contain('"SOME AWESOME STUFF"'); - it('build with only pod templates', function() { - return copyFixtureFiles('addon/pod-templates-only') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', addonName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.dependencies['ember-cli-htmlbars'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }).then(function(){ - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - }) - .then(function() { - var indexPath = path.join('dist', 'assets', 'vendor.js'); - var contents = fs.readFileSync(indexPath, { encoding: 'utf8' }); - expect(contents).to.contain('MY-COMPONENT-TEMPLATE-CONTENT'); - }); - }); + let cssPath = path.join(addonRoot, 'dist', 'assets', 'vendor.css'); + contents = fs.readFileSync(cssPath, { encoding: 'utf8' }); + expect(contents).to.equal('/* addon/styles/app.css is present */\n'); + + let robotsPath = path.join(addonRoot, 'dist', 'robots.txt'); + contents = fs.readFileSync(robotsPath, { encoding: 'utf8' }); + expect(contents).to.contain('tests/dummy/public/robots.txt is present'); - it('ember addon with addon/styles directory', function() { - return copyFixtureFiles('addon/with-styles') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - }) - .then(function() { - var cssPath = path.join('dist', 'assets', 'vendor.css'); - var contents = fs.readFileSync(cssPath, { encoding: 'utf8' }); - - expect(contents).to.contain('addon/styles/app.css is present'); - }); + result = await runCommand('node_modules/ember-cli/bin/ember', 'test'); + + expect(result.code).to.eql(0); }); - it('ember addon with tests/dummy/public directory', function() { - return copyFixtureFiles('addon/with-dummy-public') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - }) - .then(function() { - var robotsPath = path.join('dist', 'robots.txt'); - var contents = fs.readFileSync(robotsPath, { encoding: 'utf8' }); - - expect(contents).to.contain('tests/dummy/public/robots.txt is present'); - }); + it("works for addon's that specify a nested addon entry point", async function () { + await copyFixtureFiles('addon/nested-addon-main'); + + let packageJsonPath = path.join(addonRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + + // give the addon a custom entry point + packageJson['ember-addon'].main = 'src/main.js'; + + expect(packageJson.devDependencies['ember-source']).to.not.be.empty; + expect(packageJson.devDependencies['ember-cli']).to.not.be.empty; + + fs.writeJsonSync(packageJsonPath, packageJson); + + let result = await runCommand('node_modules/ember-cli/bin/ember', 'build'); + + expect(result.code).to.eql(0); + let contents; + + let indexPath = path.join(addonRoot, 'dist', 'assets', 'vendor.js'); + contents = fs.readFileSync(indexPath, { encoding: 'utf8' }); + expect(contents).to.contain('"some-cool-addon/components/simple-component"'); }); - it('npm pack does not include unnecessary files', function() { - console.log(' running the slow end-to-end it will take some time'); - var handleError = function(error, commandName) { - if(error.code === 'ENOENT') { - console.warn(chalk.yellow(' Your system does not provide ' + commandName + ' -> Skipped this test.')); + it('npm pack does not include unnecessary files', async function () { + let handleError = function (error, commandName) { + if (error.code === 'ENOENT') { + console.warn(chalk.yellow(` Your system does not provide ${commandName} -> Skipped this test.`)); } else { throw new Error(error); } }; - return new Promise(function(resolve, reject) { - var npmPack = spawn('npm', ['pack']); - npmPack.on('error', function(error) { - reject(error); - }); - npmPack.on('close', function() { - resolve(); - }); - }).then(function() { - return new Promise(function(resolve, reject) { - var output; - var tar = spawn('tar', ['-tf', addonName + '-0.0.0.tgz']); - tar.on('error', function(error) { - reject(error); - }); - tar.stdout.on('data', function(data) { - output = data.toString(); - }); - tar.on('close', function() { - resolve(output); - }); - }).then(function(output) { - var unnecessaryFiles = ['.gitkeep', '.travis.yml', 'ember-cli-build.js', '.editorconfig', 'testem.json', '.ember-cli', 'bower.json', '.bowerrc']; - var unnecessaryFolders = ['tests/', 'bower_components/']; - - unnecessaryFiles.concat(unnecessaryFolders).forEach(function(file) { - expect(output).to.not.match(new RegExp(file), 'expected packaged addon to not contain file or folder \'' + file + '\''); - }); - }, function(error) { - handleError(error, 'tar'); - }); - }, function(error) { - handleError(error, 'npm'); - }); + try { + await npmPack(); + } catch (error) { + return handleError(error, 'npm'); + } + + let output; + try { + let result = await tar(); + output = result.stdout; + } catch (error) { + return handleError(error, 'tar'); + } + + let necessaryFiles = ['package.json', 'index.js', 'LICENSE.md', 'README.md']; + let unnecessaryFiles = ['.gitkeep', '.editorconfig', 'testem.js', '.ember-cli']; + let unnecessaryFolders = [/^tests\//]; + let outputFiles = output + .split('\n') + .filter(Boolean) + .map((f) => f.replace(/^package\//, '')); + + expect(outputFiles, 'verify our assumptions about the output structure').to.include.members(necessaryFiles); + + expect(outputFiles).to.not.have.members(unnecessaryFiles); + + for (let unnecessaryFolder of unnecessaryFolders) { + for (let outputFile of outputFiles) { + expect(outputFile).to.not.match(unnecessaryFolder); + } + } }); +}); - function wait(time) { - return new Promise(function(resolve) { - setTimeout(function() { - resolve(); - }, time); - }); - } +function npmPack() { + return new Promise((resolve, reject) => { + let npmPack = spawn('npm', ['pack']); + npmPack.on('error', reject); + npmPack.on('close', () => resolve()); + }); +} - it('doesn\'t fail to build new files', function() { - var testemOutput = ''; - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--launch=PhantomJS', '--server', { - onOutput: function(string) { - testemOutput += string; - }, - onChildSpawned: function(child) { - return wait(12000).then(function() { - - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'generate', 'initializer', 'foo') - .then(function() { - return wait(5000); - }) - .then(function() { - child.stdin.write('q'); // quit test server - child.stdin.end(); - expect(testemOutput).to.contain('✔'); - expect(testemOutput).to.not.contain('✘'); - }); - - }); +async function tar() { + let fileName = `${addonName}-0.0.0.tgz`; - } - }); + if (fs.existsSync(fileName) === false) { + throw new Error(`unknown file: '${path.resolve(fileName)}'`); + } - }); -}); + return execa('tar', ['-tf', fileName]); +} diff --git a/tests/acceptance/addon-test-slow.js b/tests/acceptance/addon-test-slow.js new file mode 100644 index 0000000000..c8df4769fc --- /dev/null +++ b/tests/acceptance/addon-test-slow.js @@ -0,0 +1,40 @@ +'use strict'; + +const tmp = require('tmp-promise'); +const { execa } = require('execa'); +const { join, resolve } = require('node:path'); +const ember = require('../helpers/ember'); + +const emberCliRoot = resolve(join(__dirname, '../..')); +const root = process.cwd(); +let tmpDir; + +describe('Acceptance: ember addon (slow)', function () { + this.timeout(500000); + + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpDir = path; + process.chdir(path); + }); + + afterEach(function () { + process.chdir(root); + }); + + describe('ember addon', function () { + it('generates a new addon with no linting errors', async function () { + await ember(['addon', 'foo-addon', '--pnpm', '--skip-npm']); + // link current version of ember-cli in the newly generated app + await execa('pnpm', ['link', emberCliRoot]); + await execa('pnpm', ['lint'], { cwd: join(tmpDir, 'foo-addon') }); + }); + + it('generates a new TS addon with no linting errors', async function () { + await ember(['addon', 'foo-addon', '--pnpm', '--typescript', '--skip-npm']); + // link current version of ember-cli in the newly generated app + await execa('pnpm', ['link', emberCliRoot]); + await execa('pnpm', ['lint'], { cwd: join(tmpDir, 'foo-addon') }); + }); + }); +}); diff --git a/tests/acceptance/addon-test.js b/tests/acceptance/addon-test.js new file mode 100644 index 0000000000..8660b4298e --- /dev/null +++ b/tests/acceptance/addon-test.js @@ -0,0 +1,219 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const tmp = require('tmp-promise'); + +const { expect } = require('chai'); +const { dir, file } = require('chai-files'); + +const ember = require('../helpers/ember'); + +const { checkFile } = require('../helpers-internal/file-utils'); +const { + currentVersion, + currentAddonBlueprintVersion, + checkEslintConfig, + checkFileWithJSONReplacement, + checkEmberCLIBuild, +} = require('../helpers-internal/fixtures'); + +let root = process.cwd(); + +describe('Acceptance: ember addon', function () { + this.timeout(300000); + + let ORIGINAL_PROCESS_ENV_CI; + + beforeEach(async function () { + const { path } = await tmp.dir(); + + process.chdir(path); + + ORIGINAL_PROCESS_ENV_CI = process.env.CI; + }); + + afterEach(function () { + if (ORIGINAL_PROCESS_ENV_CI === undefined) { + delete process.env.CI; + } else { + process.env.CI = ORIGINAL_PROCESS_ENV_CI; + } + + process.chdir(root); + }); + + it('ember addon with --directory uses given directory name and has correct package name', async function () { + let workdir = process.cwd(); + + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--directory=bar']); + + expect(dir(path.join(workdir, 'foo'))).to.not.exist; + expect(dir(path.join(workdir, 'bar'))).to.exist; + + let cwd = process.cwd(); + expect(cwd).to.not.match(/foo/, 'does not use addon name for directory name'); + expect(cwd).to.match(/bar/, 'uses given directory name'); + + let pkgJson = fs.readJsonSync('package.json'); + expect(pkgJson.name).to.equal('foo', 'uses addon name for package name'); + }); + + it('ember addon @foo/bar when parent directory does not contain `foo`', async function () { + await ember(['addon', '@foo/bar', '--skip-npm', '--skip-git']); + + let directoryName = path.basename(process.cwd()); + + expect(directoryName).to.equal('foo-bar'); + + let pkgJson = fs.readJsonSync('package.json'); + expect(pkgJson.name).to.equal('@foo/bar', 'uses addon name for package name'); + }); + + it('ember addon @foo/bar when parent directory contains `foo`', async function () { + let scopedDirectoryPath = path.join(process.cwd(), 'foo'); + fs.mkdirsSync(scopedDirectoryPath); + process.chdir(scopedDirectoryPath); + + await ember(['addon', '@foo/bar', '--skip-npm', '--skip-git']); + + let directoryName = path.basename(process.cwd()); + + expect(directoryName).to.equal('bar'); + + let pkgJson = fs.readJsonSync('package.json'); + expect(pkgJson.name).to.equal('@foo/bar', 'uses addon name for package name'); + }); + + it('ember addon generates the correct directory name in `CONTRIBUTING.md` for scoped package names', async function () { + await ember(['addon', '@foo/bar', '--skip-npm', '--skip-git']); + + expect(file('CONTRIBUTING.md')).to.match(/- `cd foo-bar`/); + }); + + it('addon defaults', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git']); + + let namespace = 'addon'; + let fixturePath = `${namespace}/defaults`; + + [ + 'tests/dummy/config/ember-try.js', + 'tests/dummy/app/templates/application.gjs', + '.github/workflows/ci.yml', + 'README.md', + 'CONTRIBUTING.md', + '.ember-cli', + ].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkFileWithJSONReplacement( + fixturePath, + 'tests/dummy/config/ember-cli-update.json', + 'packages[0].version', + currentAddonBlueprintVersion + ); + + // option independent, but piggy-backing on an existing generate for speed + checkEslintConfig(namespace); + + // ember addon without --lang flag (default) has no lang attribute in dummy index.html + expect(file('tests/dummy/app/index.html')).to.contain(''); + + // no TypeScript files + [ + 'tsconfig.json', + 'tsconfig.declarations.json', + 'tests/dummy/app/config/environment.d.ts', + 'types/global.d.ts', + ].forEach((filePath) => { + expect(file(filePath)).to.not.exist; + }); + }); + + it('addon + yarn + welcome', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--yarn', '--welcome']); + + let fixturePath = 'addon/yarn'; + + [ + 'tests/dummy/config/ember-try.js', + 'tests/dummy/app/templates/application.gjs', + '.github/workflows/ci.yml', + 'README.md', + 'CONTRIBUTING.md', + ].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkFileWithJSONReplacement( + fixturePath, + 'tests/dummy/config/ember-cli-update.json', + 'packages[0].version', + currentAddonBlueprintVersion + ); + }); + + it('addon + pnpm + welcome', async function () { + await ember(['addon', 'foo', '--skip-npm', '--skip-git', '--pnpm', '--welcome']); + + let fixturePath = 'addon/pnpm'; + + [ + 'tests/dummy/config/ember-try.js', + 'tests/dummy/app/templates/application.gjs', + '.github/workflows/ci.yml', + 'README.md', + 'CONTRIBUTING.md', + '.npmrc', + ].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkFileWithJSONReplacement( + fixturePath, + 'tests/dummy/config/ember-cli-update.json', + 'packages[0].version', + currentAddonBlueprintVersion + ); + }); + + it('addon - no CI provider', async function () { + await ember(['addon', 'foo', '--ci-provider=none', '--skip-install', '--skip-git']); + + expect(file('.github/workflows/ci.yml')).to.not.exist; + expect(file('tests/dummy/config/ember-cli-update.json')).to.include('--ci-provider=none'); + }); + + it('addon + typescript', async function () { + await ember(['addon', 'foo', '--typescript', '--skip-npm', '--skip-git', '--yarn']); + + let fixturePath = 'addon/typescript'; + + // check fixtures + [ + '.ember-cli', + 'index.js', + 'tests/helpers/index.ts', + 'tsconfig.json', + 'tsconfig.declarations.json', + 'tests/dummy/app/config/environment.d.ts', + 'types/global.d.ts', + ].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + checkFileWithJSONReplacement( + fixturePath, + 'tests/dummy/config/ember-cli-update.json', + 'packages[0].version', + currentAddonBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkEmberCLIBuild(fixturePath, 'ember-cli-build.js'); + checkEslintConfig(fixturePath); + }); +}); diff --git a/tests/acceptance/blueprint-test-slow.js b/tests/acceptance/blueprint-test-slow.js deleted file mode 100644 index bc1de5bacf..0000000000 --- a/tests/acceptance/blueprint-test-slow.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -var path = require('path'); -var fs = require('fs'); -var expect = require('chai').expect; -var acceptance = require('../helpers/acceptance'); -var runCommand = require('../helpers/run-command'); -var assertDirEmpty = require('../helpers/assert-dir-empty'); -var createTestTargets = acceptance.createTestTargets; -var teardownTestTargets = acceptance.teardownTestTargets; -var linkDependencies = acceptance.linkDependencies; -var cleanupRun = acceptance.cleanupRun; - - -var appName = 'some-cool-app'; - -describe('Acceptance: blueprint smoke tests', function() { - this.timeout(400000); - - before(function() { - return createTestTargets(appName); - }); - - after(function() { - return teardownTestTargets(); - }); - - beforeEach(function() { - return linkDependencies(appName); - }); - - afterEach(function() { - return cleanupRun().then(function() { - assertDirEmpty('tmp'); - }); - }); - - it('generating an http-proxy installs packages to package.json', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'generate', - 'http-proxy', - 'api', - 'http://localhost/api', - '--silent') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - - expect(!packageJson.devDependencies['http-proxy']).to.not.be.an('undefined'); - expect(!packageJson.devDependencies['morgan']).to.not.be.an('undefined'); - }); - }); -}); diff --git a/tests/acceptance/brocfile-smoke-test-slow.js b/tests/acceptance/brocfile-smoke-test-slow.js index 8c1040b86b..9c152d0d75 100644 --- a/tests/acceptance/brocfile-smoke-test-slow.js +++ b/tests/acceptance/brocfile-smoke-test-slow.js @@ -1,422 +1,450 @@ 'use strict'; -var Promise = require('../../lib/ext/promise'); -var path = require('path'); -var fs = require('fs-extra'); -var remove = Promise.denodeify(fs.remove); - -var expect = require('chai').expect; -var EOL = require('os').EOL; - -var runCommand = require('../helpers/run-command'); -var acceptance = require('../helpers/acceptance'); -var copyFixtureFiles = require('../helpers/copy-fixture-files'); -var assertDirEmpty = require('../helpers/assert-dir-empty'); -var existsSync = require('exists-sync'); -var createTestTargets = acceptance.createTestTargets; -var teardownTestTargets = acceptance.teardownTestTargets; -var linkDependencies = acceptance.linkDependencies; -var cleanupRun = acceptance.cleanupRun; -var existsSync = require('exists-sync'); - -var appName = 'some-cool-app'; - -describe('Acceptance: brocfile-smoke-test', function() { - this.timeout(400000); - - before(function() { +const path = require('path'); +const fs = require('fs-extra'); + +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); +const runCommand = require('../helpers/run-command'); +const acceptance = require('../helpers/acceptance'); +const copyFixtureFiles = require('../helpers/copy-fixture-files'); +const DistChecker = require('../helpers/dist-checker'); +let createTestTargets = acceptance.createTestTargets; +let teardownTestTargets = acceptance.teardownTestTargets; +let linkDependencies = acceptance.linkDependencies; +let cleanupRun = acceptance.cleanupRun; + +const { expect } = require('chai'); +const { dir, file } = require('chai-files'); + +let appName = 'some-cool-app'; +let appRoot; + +describe('Acceptance: brocfile-smoke-test', function () { + this.timeout(500000); + + before(function () { return createTestTargets(appName); }); - after(function() { - return teardownTestTargets(); - }); + after(teardownTestTargets); - beforeEach(function() { - return linkDependencies(appName); + beforeEach(function () { + appRoot = linkDependencies(appName); }); - afterEach(function() { - return cleanupRun().then(function() { - assertDirEmpty('tmp'); - }); + afterEach(function () { + runCommand.killAll(); + cleanupRun(appName); + expect(dir(appRoot)).to.not.exist; }); - it('a custom EmberENV in config/environment.js is used for window.EmberENV', function() { - return copyFixtureFiles('brocfile-tests/custom-ember-env') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var vendorContents = fs.readFileSync(path.join('dist', 'assets', 'vendor.js'), { - encoding: 'utf8' - }); - - var expected = 'window.EmberENV = {"asdflkmawejf":";jlnu3yr23"};'; - expect(vendorContents).to.contain(expected, 'EmberENV should be in assets/vendor.js'); - }); - }); + it('a custom EmberENV in config/environment.js is used for window.EmberENV', async function () { + await copyFixtureFiles('brocfile-tests/custom-ember-env'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - it('a custom environment config can be used in Brocfile.js', function() { - return copyFixtureFiles('brocfile-tests/custom-environment-config') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--silent'); - }); - }); + let checker = new DistChecker(path.join(appRoot, 'dist')); + await checker.evalScripts(); + let { window } = checker; - it('using wrapInEval: true', function() { - return copyFixtureFiles('brocfile-tests/wrap-in-eval') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--silent'); - }); + // Changes in config/optional-features.json end up being set in EmberENV + expect(window.EmberENV.asdflkmawejf).to.eql(';jlnu3yr23'); + expect(window.EmberENV._APPLICATION_TEMPLATE_WRAPPER).to.be.false; + expect(window.EmberENV._DEFAULT_ASYNC_OBSERVERS).to.be.true; + expect(window.EmberENV._JQUERY_INTEGRATION).to.be.false; + expect(window.EmberENV._TEMPLATE_ONLY_GLIMMER_COMPONENTS).to.be.true; }); - it('without app/templates', function() { - return copyFixtureFiles('brocfile-tests/pods-templates') - .then(function(){ - // remove ./app/templates - return remove(path.join(process.cwd(), 'app/templates')); - }).then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); - }); + it('a custom environment config can be used in Brocfile.js', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/custom-environment-config'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); }); - it('strips app/styles or app/templates from JS', function() { - return copyFixtureFiles('brocfile-tests/styles-and-templates-stripped') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - }) - .then(function() { - var appFileContents = fs.readFileSync(path.join('.', 'dist', 'assets', appName + '.js'), { - encoding: 'utf8' - }); - - expect(appFileContents).to.include('//app/templates-stuff.js'); - expect(appFileContents).to.include('//app/styles-manager.js'); - }); + it('builds with an ES modules ember-cli-build.js', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await fs.writeFile( + 'ember-cli-build.js', + ` + import EmberApp from 'ember-cli/lib/broccoli/ember-app.js'; + + export default async function (defaults) { + const app = new EmberApp(defaults, { }); + + return app.toTree(); + }; + ` + ); + + let appPackageJson = await fs.readJson('package.json'); + appPackageJson.type = 'module'; + await fs.writeJson('package.json', appPackageJson); + + // lib/utilities/find-build-file.js uses await import and so can handle ES module ember-cli-build.js + // + // However, broccoli-config-loader uses require, so files like + // config/environment.js must be in commonjs format. The way to mix ES and + // commonjs formats in node is with multiple `package.json`s + await fs.writeJson('config/package.json', { type: 'commonjs' }); + console.log(process.cwd()); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); }); - it('should fall back to the Brocfile', function() { - return copyFixtureFiles('brocfile-tests/no-ember-cli-build').then(function() { - fs.removeSync('./ember-cli-build.js'); - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }).then(function() { - expect(existsSync(path.join('.', 'Brocfile.js'))).to.be.ok; - expect(existsSync(path.join('.', 'ember-cli-build.js'))).to.be.not.ok; - }); + it('without app/templates', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/pods-templates'); + await fs.remove(path.join(process.cwd(), 'app/templates')); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); }); - it('should use the Brocfile if both a Brocfile and ember-cli-build exist', function() { - return copyFixtureFiles('brocfile-tests/both-build-files').then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }).then(function(result) { - var vendorContents = fs.readFileSync(path.join('dist', 'assets', 'vendor.js'), { - encoding: 'utf8' - }); + it('strips app/styles or app/templates from JS', async function () { + await copyFixtureFiles('brocfile-tests/styles-and-templates-stripped'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - var expected = 'var usingBrocfile = true;'; + let checker = new DistChecker(path.join(appRoot, 'dist')); - expect(vendorContents).to.contain(expected, 'includes file imported from Brocfile'); - expect(result.output[0]).to.include('Brocfile.js has been deprecated'); - }); + expect(checker.contains('js', '//app/templates-stuff.js')).to.be; + expect(checker.contains('js', '//app/styles-manager.js')).to.be; }); - it('should throw if no build file is found', function() { + it('should throw if no build file is found', async function () { fs.removeSync('./ember-cli-build.js'); - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent').catch(function(err) { + try { + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + } catch (err) { expect(err.code).to.eql(1); - }); + } }); - it('using autoRun: true', function() { - return copyFixtureFiles('brocfile-tests/auto-run-true') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var appFileContents = fs.readFileSync(path.join('.', 'dist', 'assets', appName + '.js'), { - encoding: 'utf8' - }); - - expect(appFileContents).to.match(/\/app"\)\["default"\]\.create\(/); - }); - }); + it('using autoRun: true', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('using autoRun: false', function() { + await copyFixtureFiles('brocfile-tests/auto-run-true'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - return copyFixtureFiles('brocfile-tests/auto-run-false') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var appFileContents = fs.readFileSync(path.join('.', 'dist', 'assets', appName + '.js'), { - encoding: 'utf8' - }); + let checker = new DistChecker(path.join(appRoot, 'dist')); + await checker.evalScripts(); - expect(appFileContents).to.not.match(/\/app"\)\["default"\]\.create\(/); - }); + let { window } = checker; + + expect(window.APP_HAS_LOADED).to.be.true; }); - it('default development build does not fail', function() { - return copyFixtureFiles('brocfile-tests/query') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - }); + it('using autoRun: false', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/auto-run-false'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + await checker.evalScripts(); + + let { window } = checker; + + expect(window.APP_HAS_LOADED).to.be.undefined; }); - it('default development build tests', function() { - return copyFixtureFiles('brocfile-tests/default-development') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--silent'); + // we dont run postprocessTree in embroider + if (!isExperimentEnabled('EMBROIDER') && !isExperimentEnabled('VITE')) { + it('app.import works properly with test tree files', async function () { + await copyFixtureFiles('brocfile-tests/app-test-import'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-test-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + // we need to copy broccoli-plugin into the addon's node_modules to make up for the fact + // that the mock 'ember-test-addon' does not have a dependency install step + fs.mkdirSync(path.join(appRoot, 'node_modules', 'ember-test-addon', 'node_modules')); + await fs.copy( + path.join('..', '..', 'node_modules', 'broccoli-plugin'), + path.join(appRoot, 'node_modules', 'ember-test-addon', 'node_modules', 'broccoli-plugin') + ); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + expect(checker.contains('js', '// File for test tree imported and added via postprocessTree()')).to.be; + }); + } + + it('app.import works properly with non-js/css files', async function () { + await copyFixtureFiles('brocfile-tests/app-import'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-random-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let subjectFileContents = fs.readFileSync(path.join(appRoot, 'dist', 'assets', 'file-to-import.txt'), { + encoding: 'utf8', }); + + expect(subjectFileContents).to.equal('EXAMPLE TEXT FILE CONTENT\n'); }); - it('app.import works properly with non-js/css files', function() { - return copyFixtureFiles('brocfile-tests/app-import') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = JSON.parse(fs.readFileSync(packageJsonPath,'utf8')); - packageJson.devDependencies['ember-random-addon'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var subjectFileContents = fs.readFileSync(path.join('.', 'dist', 'assets', 'file-to-import.txt'), { - encoding: 'utf8' - }); - - expect(subjectFileContents).to.equal('EXAMPLE TEXT FILE CONTENT' + EOL); - }); + it('addons can have a public tree that is merged and returned namespaced by default', async function () { + await copyFixtureFiles('brocfile-tests/public-tree'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-random-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let subjectFileContents = fs.readFileSync(path.join(appRoot, 'dist', 'ember-random-addon', 'some-root-file.txt'), { + encoding: 'utf8', + }); + + expect(subjectFileContents).to.equal('ROOT FILE\n'); }); - it('app.import fails when options.type is not `vendor` or `test`', function(){ - return copyFixtureFiles('brocfile-tests/app-import') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = JSON.parse(fs.readFileSync(packageJsonPath,'utf8')); - packageJson.devDependencies['ember-bad-addon'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - expect(false, 'Build passed when it should have failed!'); - }, function() { - expect(true, 'Build failed with invalid options type.'); - }); + it('using pods based templates', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/pods-templates'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); }); - it('addons can have a public tree that is merged and returned namespaced by default', function() { - return copyFixtureFiles('brocfile-tests/public-tree') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = JSON.parse(fs.readFileSync(packageJsonPath,'utf8')); - packageJson.devDependencies['ember-random-addon'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var subjectFileContents = fs.readFileSync(path.join('.', 'dist', 'ember-random-addon', 'some-root-file.txt'), { - encoding: 'utf8' - }); - - expect(subjectFileContents).to.equal('ROOT FILE' + EOL); - }); + it('using pods based templates with a podModulePrefix', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/pods-with-prefix-templates'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); }); - it('using pods based templates', function() { - return copyFixtureFiles('brocfile-tests/pods-templates') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--silent'); - }); + it('addon trees are not jshinted', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/jshint-addon'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson['ember-addon'] = { + paths: ['./lib/ember-random-thing'], + }; + fs.writeJsonSync(packageJsonPath, packageJson); + + let ember = path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'); + + let error = await expect(runCommand(ember, 'test', '--filter=jshint')).to.eventually.be.rejected; + + expect(error.output.join('')).to.include('Error: No tests matched the filter "jshint"'); }); - it('using pods based templates with a podModulePrefix', function() { - return copyFixtureFiles('brocfile-tests/pods-with-prefix-templates') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--silent'); - }); + it('multiple css files in styles/ are output when a preprocessor is not used', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/multiple-css-files'); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let files = ['/assets/some-cool-app.css', '/assets/other.css']; + + let basePath = path.join(appRoot, 'dist'); + files.forEach(function (f) { + expect(file(path.join(basePath, f))).to.exist; + }); }); - it('addon trees are not jshinted', function() { - return copyFixtureFiles('brocfile-tests/jshint-addon') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = JSON.parse(fs.readFileSync(packageJsonPath,'utf8')); - packageJson['ember-addon'] = { - paths: ['./lib/ember-random-thing'] - }; - - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - - var badContent = 'var blah = ""' + EOL + 'export default Blah;'; - var appPath = path.join('.', 'lib', 'ember-random-thing', 'app', - 'routes', 'horrible-route.js'); - var testSupportPath = path.join('.', 'lib', 'ember-random-thing', 'test-support', - 'unit', 'routes', 'horrible-route-test.js'); - - fs.writeFileSync(appPath, badContent); - fs.writeFileSync(testSupportPath, badContent); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--silent', '--filter=jshint'); - }); + // skipping this as it seems this functionality doesn't work with ember-auto-import@2.2.3 + it.skip('specifying outputFile results in an explicitly generated assets', async function () { + await copyFixtureFiles('brocfile-tests/app-import-output-file'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let files = ['/assets/output-file.js', '/assets/output-file.css', '/assets/vendor.css', '/assets/vendor.js']; + + let basePath = path.join(appRoot, 'dist'); + files.forEach(function (f) { + expect(file(path.join(basePath, f))).to.exist; + }); }); - it('specifying custom output paths works properly', function() { - return copyFixtureFiles('brocfile-tests/custom-output-paths') - .then(function () { - var themeCSSPath = path.join(__dirname, '..', '..', 'tmp', appName, 'app', 'styles', 'theme.css'); - return fs.writeFileSync(themeCSSPath, 'html, body { margin: 20%; }'); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var files = [ - '/css/app.css', - '/css/theme/a.css', - '/js/app.js', - '/css/vendor.css', - '/js/vendor.js', - '/css/test-support.css', - '/js/test-support.js', - '/my-app.html' - ]; - - var basePath = path.join('.', 'dist'); - files.forEach(function(file) { - expect(existsSync(path.join(basePath, file)), file + ' exists'); - }); - }); + it('can use transformation to turn anonymous AMD into named AMD', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/app-import-anonymous-amd'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let outputJS = fs.readFileSync(path.join(appRoot, 'dist', 'assets', 'output.js'), { + encoding: 'utf8', + }); + + (function () { + let defineCount = 0; + // eslint-disable-next-line no-unused-vars + function define(name, deps, factory) { + expect(name).to.equal('hello-world'); + expect(deps).to.deep.equal([]); + expect(factory()()).to.equal('Hello World'); + defineCount++; + } + /* eslint-disable no-eval */ + eval(outputJS); + /* eslint-enable no-eval */ + expect(defineCount).to.eql(1); + })(); }); - it('multiple css files in app/styles/ are output when a preprocessor is not used', function() { - return copyFixtureFiles('brocfile-tests/multiple-css-files') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var files = [ - '/assets/some-cool-app.css', - '/assets/other.css' - ]; - - var basePath = path.join('.', 'dist'); - files.forEach(function(file) { - expect(existsSync(path.join(basePath, file)), file + ' exists'); - }); - }); + it('can use transformation to turn named UMD into named AMD', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/app-import-named-umd'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let outputJS = fs.readFileSync(path.join(appRoot, 'dist', 'assets', 'output.js'), { + encoding: 'utf8', + }); + + (function () { + let defineCount = 0; + // eslint-disable-next-line no-unused-vars + function define(name, deps, factory) { + expect(name).to.equal('hello-world'); + expect(deps).to.deep.equal([]); + expect(factory()()).to.equal('Hello World'); + defineCount++; + } + /* eslint-disable no-eval */ + eval(outputJS); + /* eslint-enable no-eval */ + expect(defineCount).to.eql(1); + })(); }); - it('specifying partial `outputPaths` hash deep merges options correctly', function() { - return copyFixtureFiles('brocfile-tests/custom-output-paths') - .then(function () { - - var themeCSSPath = path.join(__dirname, '..', '..', 'tmp', appName, 'app', 'styles', 'theme.css'); - fs.writeFileSync(themeCSSPath, 'html, body { margin: 20%; }'); - - var brocfilePath = path.join(__dirname, '..', '..', 'tmp', appName, 'ember-cli-build.js'); - var brocfile = fs.readFileSync(brocfilePath, 'utf8'); - - // remove outputPaths.app.js option - brocfile = brocfile.replace(/js: '\/js\/app.js'/, ''); - // remove outputPaths.app.css.app option - brocfile = brocfile.replace(/'app': '\/css\/app\.css',/, ''); - - fs.writeFileSync(brocfilePath, brocfile, 'utf8'); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var files = [ - '/css/theme/a.css', - '/assets/some-cool-app.js', - '/css/vendor.css', - '/js/vendor.js', - '/css/test-support.css', - '/js/test-support.js' - ]; - - var basePath = path.join('.', 'dist'); - files.forEach(function(file) { - expect(existsSync(path.join(basePath, file)), file + ' exists'); - }); - - expect(!existsSync(path.join(basePath, '/assets/some-cool-app.css')), 'default app.css should not exist'); - }); + it('can do amd transform from addon', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/app-import-custom-transform'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-transform-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let addonOutputJs = fs.readFileSync(path.join(appRoot, 'dist', 'assets', 'addon-output.js'), { + encoding: 'utf8', + }); + + (function () { + let defineCount = 0; + // eslint-disable-next-line no-unused-vars + function define(name, deps, factory) { + expect(name).to.equal('addon-vendor'); + expect(deps).to.deep.equal([]); + expect(factory()()).to.equal('Hello World'); + defineCount++; + } + /* eslint-disable no-eval */ + eval(addonOutputJs); + /* eslint-enable no-eval */ + expect(defineCount).to.eql(1); + })(); }); - it('multiple paths can be CSS preprocessed', function() { - return copyFixtureFiles('brocfile-tests/multiple-sass-files') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.devDependencies['broccoli-sass'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var mainCSS = fs.readFileSync(path.join('.', 'dist', 'assets', 'main.css'), { - encoding: 'utf8' - }); - var themeCSS = fs.readFileSync(path.join('.', 'dist', 'assets', 'theme', 'a.css'), { - encoding: 'utf8' - }); - - expect(mainCSS).to.equal('body { background: black; }' + EOL, 'main.css contains correct content'); - expect(themeCSS).to.equal('.theme { color: red; }' + EOL, 'theme/a.css contains correct content'); - }); + it('can use transformation to turn library into custom transformation', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/app-import-custom-transform'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-transform-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + expect( + checker.contains( + 'js', + 'if (typeof FastBoot === \'undefined\') { window.hello = "hello world"; }//# sourceMappingURL=output.map\n' + ) + ).to.be; }); - it('app.css is output to .css by default', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent') - .then(function() { - var exists = existsSync(path.join('.', 'dist', 'assets', appName + '.css')); + it('app.css is output to .css by default', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - expect(exists, appName + '.css exists'); - }); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + expect(file(`dist/assets/${appName}.css`)).to.exist; }); // for backwards compat. - it('app.scss is output to .css by default', function() { - return copyFixtureFiles('brocfile-tests/multiple-sass-files') - .then(function() { - var brocfilePath = path.join(__dirname, '..', '..', 'tmp', appName, 'ember-cli-build.js'); - var brocfile = fs.readFileSync(brocfilePath, 'utf8'); - - // remove custom preprocessCss paths, use app.scss instead - brocfile = brocfile.replace(/outputPaths.*/, ''); - - fs.writeFileSync(brocfilePath, brocfile, 'utf8'); - - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.devDependencies['broccoli-sass'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var mainCSS = fs.readFileSync(path.join('.', 'dist', 'assets', appName + '.css'), { - encoding: 'utf8' - }); - - expect(mainCSS).to.equal('body { background: green; }' + EOL, appName + '.css contains correct content'); - }); + it('app.scss is output to .css by default', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles('brocfile-tests/multiple-sass-files'); + + let brocfilePath = path.join(appRoot, 'ember-cli-build.js'); + let brocfile = fs.readFileSync(brocfilePath, 'utf8'); + + fs.writeFileSync(brocfilePath, brocfile, 'utf8'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-cli-sass'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + expect(file(`dist/assets/${appName}.css`)).to.equal('body { background: green; }\n'); + }); + + // skipping this as it seems this functionality doesn't work with ember-auto-import@2.2.3 + it.skip('additional trees can be passed to the app', async function () { + await copyFixtureFiles('brocfile-tests/additional-trees'); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', { verbose: true }); + + let files = [ + '/assets/custom-output-file.js', + '/assets/custom-output-file.css', + '/assets/vendor.css', + '/assets/vendor.js', + ]; + + let basePath = path.join(appRoot, 'dist'); + files.forEach(function (f) { + expect(file(path.join(basePath, f))).to.exist; + }); }); }); diff --git a/tests/acceptance/destroy-test.js b/tests/acceptance/destroy-test.js index 7e75a70d3c..717583132a 100644 --- a/tests/acceptance/destroy-test.js +++ b/tests/acceptance/destroy-test.js @@ -1,663 +1,191 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../../lib/ext/promise'); -var expect = require('chai').expect; -var assertFile = require('../helpers/assert-file'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var existsSync = require('exists-sync'); -var fs = require('fs-extra'); -var outputFile = Promise.denodeify(fs.outputFile); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var EOL = require('os').EOL; - -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); - -describe('Acceptance: ember destroy', function() { - var tmpdir; - - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); - }); +const ember = require('../helpers/ember'); +const fs = require('fs-extra'); +const path = require('path'); +let root = process.cwd(); +const tmp = require('tmp-promise'); + +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +describe('Acceptance: ember destroy', function () { + this.timeout(60000); + let tmpdir; - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); }); - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); }); - afterEach(function() { - this.timeout(10000); + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpdir = path; + process.chdir(path); + }); + afterEach(function () { process.chdir(root); - return remove(tmproot); }); function initApp() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]); - } - - function initAddon() { - return ember([ - 'addon', - 'my-addon', - '--skip-npm', - '--skip-bower' - ]); - } - - function initInRepoAddon() { - return initApp().then(function() { - return ember([ - 'generate', - 'in-repo-addon', - 'my-addon' - ]); - }); + return ember(['init', '--name=my-app', '--skip-npm']); } function generate(args) { - var generateArgs = ['generate'].concat(args); + let generateArgs = ['generate'].concat(args); return ember(generateArgs); } - function generateInAddon(args) { - var generateArgs = ['generate'].concat(args); - - return initAddon().then(function() { - return ember(generateArgs); - }); - } - - function generateInRepoAddon(args) { - var generateArgs = ['generate'].concat(args); - - return initInRepoAddon().then(function() { - return ember(generateArgs); - }); - } - function destroy(args) { - var destroyArgs = ['destroy'].concat(args); + let destroyArgs = ['destroy'].concat(args); return ember(destroyArgs); } - function assertFileNotExists(file) { - var filePath = path.join(process.cwd(), file); - expect(!existsSync(filePath), 'expected ' + file + ' not to exist'); - } - function assertFilesExist(files) { - files.forEach(assertFile); + files.forEach(function (f) { + expect(file(f)).to.exist; + }); } function assertFilesNotExist(files) { - files.forEach(assertFileNotExists); - } - - function assertDestroyAfterGenerate(args, files) { - return initApp() - .then(function() { - return generate(args); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - function assertDestroyAfterGenerateInAddon(args, files) { - return initAddon() - .then(function() { - return generateInAddon(args); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - function assertDestroyAfterGenerateInRepoAddon(args, files) { - return generateInRepoAddon(args) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); + files.forEach(function (f) { + expect(file(f)).to.not.exist; + }); } - it('controller foo', function() { - this.timeout(20000); - var commandArgs = ['controller', 'foo']; - var files = [ - 'app/controllers/foo.js', - 'tests/unit/controllers/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('controller foo/bar', function() { - this.timeout(20000); - var commandArgs = ['controller', 'foo/bar']; - var files = [ - 'app/controllers/foo/bar.js', - 'tests/unit/controllers/foo/bar-test.js' - ]; + const assertDestroyAfterGenerate = async function (args, files) { + await initApp(); - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('component x-foo', function() { - this.timeout(20000); - var commandArgs = ['component', 'x-foo']; - var files = [ - 'app/components/x-foo.js', - 'app/templates/components/x-foo.hbs', - 'tests/integration/components/x-foo-test.js' - ]; + await generate(args); + assertFilesExist(files); - return assertDestroyAfterGenerate(commandArgs, files); - }); + let result = await destroy(args); + expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); + assertFilesNotExist(files); + }; - it('helper foo-bar', function() { - this.timeout(20000); - var commandArgs = ['helper', 'foo-bar']; - var files = [ - 'app/helpers/foo-bar.js', - 'tests/unit/helpers/foo-bar-test.js' - ]; + it('blueprint foo', function () { + let commandArgs = ['blueprint', 'foo']; + let files = ['blueprints/foo/index.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('helper foo/bar-baz', function() { - this.timeout(20000); - var commandArgs = ['helper', 'foo/bar-baz']; - var files = [ - 'app/helpers/foo/bar-baz.js', - 'tests/unit/helpers/foo/bar-baz-test.js' - ]; + it('blueprint foo/bar', function () { + let commandArgs = ['blueprint', 'foo/bar']; + let files = ['blueprints/foo/bar/index.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('model foo', function() { - this.timeout(20000); - var commandArgs = ['model', 'foo']; - var files = [ - 'app/models/foo.js', - 'tests/unit/models/foo-test.js' - ]; + it('http-mock foo', function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('model foo/bar', function() { - this.timeout(20000); - var commandArgs = ['model', 'foo/bar']; - var files = [ - 'app/models/foo/bar.js', - 'tests/unit/models/foo/bar-test.js' - ]; + let commandArgs = ['http-mock', 'foo']; + let files = ['server/mocks/foo.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('route foo', function() { - this.timeout(20000); - var commandArgs = ['route', 'foo']; - var files = [ - 'app/routes/foo.js', - 'app/templates/foo.hbs', - 'tests/unit/routes/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files) - .then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); - }); - - it('route foo --skip-router', function() { - this.timeout(20000); - var commandArgs = ['route', 'foo', '--skip-router']; - var files = [ - 'app/routes/foo.js', - 'app/templates/foo.hbs', - 'tests/unit/routes/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files) - .then(function() { - assertFile('app/router.js', { - doesContain: "this.route('foo');" - }); - }); - }); - - it('route index', function() { - this.timeout(20000); - var commandArgs = ['route', 'index']; - var files = [ - 'app/routes/index.js', - 'app/templates/index.hbs', - 'tests/unit/routes/index-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('route basic', function() { - this.timeout(20000); - var commandArgs = ['route', 'basic']; - var files = [ - 'app/routes/basic.js', - 'app/templates/basic.hbs', - 'tests/unit/routes/basic-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); + it('http-mock throws', async function () { + if (isExperimentEnabled('VITE')) { + let commandArgs = ['http-mock', 'foo']; + let files = ['server/mocks/foo.js']; - it('resource foo', function() { - this.timeout(20000); - var commandArgs = ['resource', 'foo']; - var files = [ - 'app/models/foo.js', - 'tests/unit/models/foo-test.js', - 'app/routes/foo.js', - 'tests/unit/routes/foo-test.js', - 'app/templates/foo.hbs' - ]; - - return assertDestroyAfterGenerate(commandArgs, files) - .then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); + await expect(assertDestroyAfterGenerate(commandArgs, files)).to.be.rejectedWith( + 'The http-mock blueprint is not supported in Vite projects' + ); + } }); - it('resource foos', function() { - this.timeout(20000); - var commandArgs = ['resource', 'foos']; - var files = [ - 'app/models/foo.js', - 'tests/unit/models/foo-test.js', - 'app/routes/foos.js', - 'tests/unit/routes/foos-test.js', - 'app/templates/foos.hbs' - ]; - - return assertDestroyAfterGenerate(commandArgs, files) - .then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('foos');" - }); - }); - }); + it('http-proxy foo', function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('template foo', function() { - this.timeout(20000); - var commandArgs = ['template', 'foo']; - var files = ['app/templates/foo.hbs']; + let commandArgs = ['http-proxy', 'foo', 'bar']; + let files = ['server/proxies/foo.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('template foo/bar', function() { - this.timeout(20000); - var commandArgs = ['template', 'foo/bar']; - var files = ['app/templates/foo/bar.hbs']; + it('http-proxy throws', async function () { + if (isExperimentEnabled('VITE')) { + let commandArgs = ['http-proxy', 'foo', 'bar']; + let files = ['server/proxies/foo.js']; - return assertDestroyAfterGenerate(commandArgs, files); + await expect(assertDestroyAfterGenerate(commandArgs, files)).to.be.rejectedWith( + 'The http-proxy blueprint is not supported in Vite projects' + ); + } }); - it('view foo', function() { - this.timeout(20000); - var commandArgs = ['view', 'foo']; - var files = [ - 'app/views/foo.js', - 'tests/unit/views/foo-test.js' - ]; + it('deletes files generated using blueprint paths', async function () { + await fs.outputFile('path/to/blueprints/foo/files/foo/__name__.js', "console.log('bar');\n"); - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('view foo/bar', function() { - this.timeout(20000); - var commandArgs = ['view', 'foo/bar']; - var files = [ - 'app/views/foo/bar.js', - 'tests/unit/views/foo/bar-test.js' - ]; + let commandArgs = [path.join('path', 'to', 'blueprints', 'foo'), 'bar']; + let files = ['foo/bar.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('initializer foo', function() { - this.timeout(20000); - var commandArgs = ['initializer', 'foo']; - var files = ['app/initializers/foo.js']; + it('deletes files generated using blueprints from the project directory', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.js']; + await initApp(); - return assertDestroyAfterGenerate(commandArgs, files); - }); + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.js', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); - it('initializer foo/bar', function() { - this.timeout(20000); - var commandArgs = ['initializer', 'foo/bar']; - var files = ['app/initializers/foo/bar.js']; + await generate(commandArgs); + assertFilesExist(files); - return assertDestroyAfterGenerate(commandArgs, files); + await destroy(commandArgs); + assertFilesNotExist(files); }); - it('mixin foo', function() { - this.timeout(20000); - var commandArgs = ['mixin', 'foo']; - var files = [ - 'app/mixins/foo.js', - 'tests/unit/mixins/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); + it('correctly identifies the root of the project', async function () { + let commandArgs = ['controller', 'foo']; + let files = ['app/controllers/foo.js']; + await initApp(); - it('mixin foo/bar', function() { - this.timeout(20000); - var commandArgs = ['mixin', 'foo/bar']; - var files = [ - 'app/mixins/foo/bar.js', - 'tests/unit/mixins/foo/bar-test.js' - ]; + await fs.outputFile( + 'blueprints/controller/files/app/controllers/__name__.js', + "import Ember from 'ember';\n\n" + 'export default Ember.Controller.extend({ custom: true });\n' + ); - return assertDestroyAfterGenerate(commandArgs, files); - }); + await generate(commandArgs); + assertFilesExist(files); - it('adapter foo', function() { - this.timeout(20000); - var commandArgs = ['adapter', 'foo']; - var files = ['app/adapters/foo.js']; + process.chdir(path.join(tmpdir, 'app')); + await destroy(commandArgs); - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('adapter foo/bar', function() { - this.timeout(20000); - var commandArgs = ['adapter', 'foo/bar']; - var files = ['app/adapters/foo/bar.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('serializer foo', function() { - this.timeout(20000); - var commandArgs = ['serializer', 'foo']; - var files = [ - 'app/serializers/foo.js', - 'tests/unit/serializers/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('serializer foo/bar', function() { - this.timeout(20000); - var commandArgs = ['serializer', 'foo/bar']; - var files = [ - 'app/serializers/foo/bar.js', - 'tests/unit/serializers/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('transform foo', function() { - this.timeout(20000); - var commandArgs = ['transform', 'foo']; - var files = [ - 'app/transforms/foo.js', - 'tests/unit/transforms/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('transform foo/bar', function() { - this.timeout(20000); - var commandArgs = ['transform', 'foo/bar']; - var files = [ - 'app/transforms/foo/bar.js', - 'tests/unit/transforms/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('util foo-bar', function() { - this.timeout(20000); - var commandArgs = ['util', 'foo-bar']; - var files = [ - 'app/utils/foo-bar.js', - 'tests/unit/utils/foo-bar-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('util foo-bar/baz', function() { - this.timeout(20000); - var commandArgs = ['util', 'foo/bar-baz']; - var files = [ - 'app/utils/foo/bar-baz.js', - 'tests/unit/utils/foo/bar-baz-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('service foo', function() { - this.timeout(20000); - var commandArgs = ['service', 'foo']; - var files = [ - 'app/services/foo.js', - 'tests/unit/services/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('service foo/bar', function() { - this.timeout(20000); - var commandArgs = ['service', 'foo/bar']; - var files = [ - 'app/services/foo/bar.js', - 'tests/unit/services/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('blueprint foo', function() { - this.timeout(20000); - var commandArgs = ['blueprint', 'foo']; - var files = ['blueprints/foo/index.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('blueprint foo/bar', function() { - this.timeout(20000); - var commandArgs = ['blueprint', 'foo/bar']; - var files = ['blueprints/foo/bar/index.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('http-mock foo', function() { - this.timeout(20000); - var commandArgs = ['http-mock', 'foo']; - var files = ['server/mocks/foo.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('http-proxy foo', function() { - this.timeout(20000); - var commandArgs = ['http-proxy', 'foo', 'bar']; - var files = ['server/proxies/foo.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('in-addon component x-foo', function() { - this.timeout(20000); - var commandArgs = ['component', 'x-foo']; - var files = [ - 'addon/components/x-foo.js', - 'addon/templates/components/x-foo.hbs', - 'app/components/x-foo.js', - 'tests/integration/components/x-foo-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-repo-addon component x-foo', function() { - var commandArgs = ['component', 'x-foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/components/x-foo.js', - 'lib/my-addon/addon/templates/components/x-foo.hbs', - 'lib/my-addon/app/components/x-foo.js', - 'tests/integration/components/x-foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon component nested/x-foo', function() { - var commandArgs = ['component', 'nested/x-foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/components/nested/x-foo.js', - 'lib/my-addon/addon/templates/components/nested/x-foo.hbs', - 'lib/my-addon/app/components/nested/x-foo.js', - 'tests/integration/components/nested/x-foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('acceptance-test foo', function() { - this.timeout(20000); - var commandArgs = ['acceptance-test', 'foo']; - var files = ['tests/acceptance/foo-test.js']; - - return assertDestroyAfterGenerate(commandArgs, files); + process.chdir(tmpdir); + assertFilesNotExist(files); }); - it('deletes files generated using blueprints from the project directory', function() { - this.timeout(20000); - var commandArgs = ['foo', 'bar']; - var files = ['app/foos/bar.js']; - return initApp() - .then(function() { - return outputFile( - 'blueprints/foo/files/app/foos/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - 'export default Ember.Object.extend({ foo: true });' + EOL - ); - }) - .then(function() { - return generate(commandArgs); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(commandArgs); - }) - .then(function() { - assertFilesNotExist(files); - }); - }); + it('http-mock does not remove server/', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('correctly identifies the root of the project', function() { - this.timeout(20000); - var commandArgs = ['controller', 'foo']; - var files = ['app/controllers/foo.js']; - return initApp() - .then(function() { - return outputFile( - 'blueprints/controller/files/app/controllers/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - "export default Ember.Controller.extend({ custom: true });" + EOL - ); - }) - .then(function() { - return generate(commandArgs); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - process.chdir(path.join(tmpdir, 'app')); - }) - .then(function() { - return destroy(commandArgs); - }) - .then(function() { - process.chdir(tmpdir); - }) - .then(function() { - assertFilesNotExist(files); - }); - }); + await initApp(); + await generate(['http-mock', 'foo']); + await generate(['http-mock', 'bar']); + await destroy(['http-mock', 'foo']); - it('http-mock does not remove server/', function() { - this.timeout(20000); - return initApp() - .then(function() { return generate(['http-mock', 'foo']); }) - .then(function() { return generate(['http-mock', 'bar']); }) - .then(function() { return destroy(['http-mock', 'foo']); }) - .then(function() { - assertFile('server/index.js'); - assertFile('server/.jshintrc'); - }); + expect(file('server/index.js')).to.exist; }); - }); diff --git a/tests/acceptance/express-server-restart-slow.js b/tests/acceptance/express-server-restart-slow.js deleted file mode 100644 index 80051a8111..0000000000 --- a/tests/acceptance/express-server-restart-slow.js +++ /dev/null @@ -1,152 +0,0 @@ -'use strict'; - -var path = require('path'); -var expect = require('chai').expect; -var fs = require('fs-extra'); -var EOL = require('os').EOL; -var Promise = require('../../lib/ext/promise'); -var acceptance = require('../helpers/acceptance'); -var runCommand = require('../helpers/run-command'); -var remove = Promise.denodeify(fs.remove); -var createTestTargets = acceptance.createTestTargets; -var teardownTestTargets = acceptance.teardownTestTargets; -var linkDependencies = acceptance.linkDependencies; -var cleanupRun = acceptance.cleanupRun; - - -var copyFixtureFiles = require('../helpers/copy-fixture-files'); -var assertDirEmpty = require('../helpers/assert-dir-empty'); - -// skipped because brittle. needs some TLC -describe.skip('Acceptance: express server restart', function () { - var appName = 'express-server-restart-test-app'; - - before(function() { - this.timeout(360000); - - return createTestTargets(appName).then(function() { - process.chdir(appName); - return copyFixtureFiles('restart-express-server/app-root'); - }); - }); - - after(function() { - this.timeout(15000); - return teardownTestTargets(); - }); - - beforeEach(function() { - this.timeout(15000); - return linkDependencies(appName); - }); - - afterEach(function() { - this.timeout(15000); - return cleanupRun().then(function() { - assertDirEmpty('tmp'); - }); - }); - - function getRunCommandOptions(onChildSpawned) { - return { - onChildSpawned: onChildSpawned, - killAfterChildSpawnedPromiseResolution: true - }; - } - - var initialRoot = process.cwd(); - function ensureTestFileContents(expectedContents, message) { - var contents = fs.readFileSync(path.join(initialRoot, 'tmp', appName, 'foo.txt'), { encoding: 'utf8' }); - expect(contents).to.equal(expectedContents, message); - } - - function onChildSpawnedSingleCopy(copySrc, expectedContents) { - return function() { - process.chdir('server'); - return delay(6000) - .then(function() { - ensureTestFileContents('Initial contents of A.', 'Test file has correct contents after initial server start.'); - return copyFixtureFiles(path.join('restart-express-server', copySrc)); - }).then(function() { - return delay(4000); - }).then(function() { - ensureTestFileContents(expectedContents, 'Test file has correct contents after first copy.'); - }); - }; - } - - function onChildSpawnedMultipleCopies() { - return function() { - process.chdir('server'); - return delay(6000) - .then(function() { - ensureTestFileContents('Initial contents of A.', 'Test file has correct contents after initial server start.'); - return copyFixtureFiles(path.join('restart-express-server', 'copy1')); - }).then(function() { - return delay(4000); - }).then(function() { - ensureTestFileContents('Copy1 contents of A.', 'Test file has correct contents after first copy.'); - return copyFixtureFiles(path.join('restart-express-server', 'copy2')); - }).then(function() { - return delay(4000); - }).then(function() { - ensureTestFileContents('Copy2 contents of A. Copy2 contents of B.', 'Test file has correct contents after second copy.'); - return remove(path.join('restart-express-server', 'subfolder')); - }).then(function() { - return copyFixtureFiles(path.join('restart-express-server', 'copy3')); - }).then(function() { - return delay(4000); - }).then(function() { - ensureTestFileContents('true true', 'Test file has correct contents after second copy.'); - }); - }; - } - - function runServer(commandOptions) { - return new Promise(function(resolve, reject) { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), - 'serve', - '--silent', - '--live-reload-port', '32580', - '--port', '49741', commandOptions) - .then(function() { - throw new Error('The server should not have exited successfully.'); - }) - .catch(function(err) { - if (err.testingError) { - return reject(err.testingError); - } - - // This error was just caused by us having to kill the program - return resolve(); - }); - }); - } - - it('Server restarts successfully on copy1', function() { - this.timeout(30000); - - ensureTestFileContents('Initial Contents' + EOL, 'Test file initialized properly.'); - return runServer(getRunCommandOptions(onChildSpawnedSingleCopy('copy1', 'Copy1 contents of A.'))); - }); - - it('Server restarts successfully on copy2', function() { - this.timeout(30000); - - ensureTestFileContents('Initial Contents' + EOL, 'Test file initialized properly.'); - return runServer(getRunCommandOptions(onChildSpawnedSingleCopy('copy2', 'Copy2 contents of A. Copy2 contents of B.'))); - }); - - it('Server restarts successfully on multiple copies', function() { - this.timeout(90000); - - ensureTestFileContents('Initial Contents' + EOL, 'Test file initialized properly.'); - return runServer(getRunCommandOptions(onChildSpawnedMultipleCopies())); - }); -}); - -function delay(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, ms); - }); -} diff --git a/tests/acceptance/generate-test.js b/tests/acceptance/generate-test.js index eef3c8e7c6..b8a4f147ae 100755 --- a/tests/acceptance/generate-test.js +++ b/tests/acceptance/generate-test.js @@ -1,1261 +1,271 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../../lib/ext/promise'); -var assertFile = require('../helpers/assert-file'); -var assertFileEquals = require('../helpers/assert-file-equals'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var fs = require('fs-extra'); -var outputFile = Promise.denodeify(fs.outputFile); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var replaceFile = require('../helpers/file-utils').replaceFile; -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var EOL = require('os').EOL; -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); -var expect = require('chai').expect; -var MockUI = require('../helpers/mock-ui'); +const ember = require('../helpers/ember'); +const { outputFile } = require('fs-extra'); +const path = require('path'); +const replaceFile = require('ember-cli-internal-test-helpers/lib/helpers/file-utils').replaceFile; +let root = process.cwd(); +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); +const tmp = require('tmp-promise'); +const td = require('testdouble'); +const lintFix = require('../../lib/utilities/lint-fix'); + +const { expect } = require('chai'); +const { dir, file } = require('chai-files'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); -describe('Acceptance: ember generate', function() { - this.timeout(10000); +describe('Acceptance: ember generate', function () { + this.timeout(20000); - var tmpdir; + let tmpdir; - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); }); - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); }); - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpdir = path; + process.chdir(path); }); - afterEach(function() { + afterEach(function () { + td.reset(); process.chdir(root); - return remove(tmproot); }); function initApp() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]); + return ember(['init', '--name=my-app', '--skip-npm']); } function generate(args) { - var generateArgs = ['generate'].concat(args); + let generateArgs = ['generate'].concat(args); - return initApp().then(function() { + return initApp().then(function () { return ember(generateArgs); }); } - it('controller foo', function() { - return generate(['controller', 'foo']).then(function() { - assertFile('app/controllers/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/controllers/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo'" - ] - }); - }); - }); + it('blueprint foo', async function () { + await generate(['blueprint', 'foo']); - it('controller foo/bar', function() { - return generate(['controller', 'foo/bar']).then(function() { - assertFile('app/controllers/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/controllers/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo/bar'" - ] - }); - }); + expect(file('blueprints/foo/index.js')).to.contain( + 'module.exports = {\n' + + " description: ''\n" + + '\n' + + ' // locals(options) {\n' + + ' // // Return custom template variables here.\n' + + ' // return {\n' + + ' // foo: options.entity.options.foo\n' + + ' // };\n' + + ' // }\n' + + '\n' + + ' // afterInstall(options) {\n' + + ' // // Perform extra work here.\n' + + ' // }\n' + + '};' + ); }); - it('component x-foo', function() { - return generate(['component', 'x-foo']).then(function() { - assertFile('app/components/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/templates/components/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); + it('blueprint foo/bar', async function () { + await generate(['blueprint', 'foo/bar']); - it('component foo/x-foo', function() { - return generate(['component', 'foo/x-foo']).then(function() { - assertFile('app/components/foo/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/templates/components/foo/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/components/foo/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('foo/x-foo'", - "integration: true", - "{{foo/x-foo}}", - "{{#foo/x-foo}}" - ] - }); - }); + expect(file('blueprints/foo/bar/index.js').content).to.matchSnapshot(); }); - it('component x-foo ignores --path option', function() { - return generate(['component', 'x-foo', '--path', 'foo']).then(function() { - assertFile('app/components/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/templates/components/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); + it('http-mock foo', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('component-test x-foo', function() { - return generate(['component-test', 'x-foo']).then(function() { - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); + await generate(['http-mock', 'foo']); - it('component-test x-foo --unit', function() { - return generate(['component-test', 'x-foo', '--unit']).then(function() { - assertFile('tests/unit/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "moduleForComponent('x-foo'", - "unit: true" - ] - }); - }); - }); + expect(file('server/index.js')).to.contain('mocks.forEach(route => route(app));'); - it('helper foo-bar', function() { - return generate(['helper', 'foo-bar']).then(function() { - assertFile('app/helpers/foo-bar.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBar(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBar);" - }); - assertFile('tests/unit/helpers/foo-bar-test.js', { - contains: "import { fooBar } from '../../../helpers/foo-bar';" - }); - }); + expect(file('server/mocks/foo.js').content).to.matchSnapshot(); }); - it('helper foo/bar-baz', function() { - return generate(['helper', 'foo/bar-baz']).then(function() { - assertFile('app/helpers/foo/bar-baz.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBarBaz(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBarBaz);" - }); - assertFile('tests/unit/helpers/foo/bar-baz-test.js', { - contains: "import { fooBarBaz } from '../../../../helpers/foo/bar-baz';" - }); - }); + it('http-mock throws', async function () { + if (isExperimentEnabled('VITE')) { + await expect(generate(['http-mock', 'foo'])).to.be.rejectedWith( + 'The http-mock blueprint is not supported in Vite projects' + ); + } }); - it('model foo', function() { - return generate(['model', 'foo']).then(function() { - assertFile('app/models/foo.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'", - "needs: []" - ] - }); - }); - }); + it('http-mock foo-bar', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('model foo with attributes', function() { - return generate([ - 'model', - 'foo', - 'noType', - 'firstName:string', - 'created_at:date', - 'is-published:boolean', - 'rating:number', - 'bars:has-many', - 'baz:belongs-to', - 'echo:hasMany', - 'bravo:belongs_to', - 'foo-names:has-many', - 'barName:has-many', - 'bazName:belongs-to', - 'test-name:belongs-to', - 'metricData:custom-transform', - 'echoName:hasMany', - 'bravoName:belongs_to' - ]).then(function() { - assertFile('app/models/foo.js', { - contains: [ - "noType: DS.attr()", - "firstName: DS.attr('string')", - "createdAt: DS.attr('date')", - "isPublished: DS.attr('boolean')", - "rating: DS.attr('number')", - "bars: DS.hasMany('bar')", - "baz: DS.belongsTo('baz')", - "echos: DS.hasMany('echo')", - "bravo: DS.belongsTo('bravo')", - "fooNames: DS.hasMany('foo-name')", - "barNames: DS.hasMany('bar-name')", - "bazName: DS.belongsTo('baz-name')", - "testName: DS.belongsTo('test-name')", - "metricData: DS.attr('custom-transform')", - "echoNames: DS.hasMany('echo-name')", - "bravoName: DS.belongsTo('bravo-name')" - ] - }); - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "needs: [", - "'model:bar',", - "'model:baz',", - "'model:echo',", - "'model:bravo',", - "'model:foo-name',", - "'model:bar-name',", - "'model:baz-name',", - "'model:echo-name',", - "'model:test-name',", - "'model:bravo-name'", - "]" - ] - }); - }); - }); + await generate(['http-mock', 'foo-bar']); - it('model foo/bar', function() { - return generate(['model', 'foo/bar']).then(function() { - assertFile('app/models/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('tests/unit/models/foo/bar-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); + expect(file('server/index.js')).to.contain('mocks.forEach(route => route(app));'); - it('model-test foo', function() { - return generate(['model-test', 'foo']).then(function() { - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'", - "needs: []" - ] - }); - }); + expect(file('server/mocks/foo-bar.js').content).to.matchSnapshot(); }); - it('route foo', function() { - return generate(['route', 'foo']).then(function() { - assertFile('app/router.js', { - contains: 'this.route(\'foo\')' - }); - assertFile('app/routes/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('app/templates/foo.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/unit/routes/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - }); - }); + it('http-proxy foo', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('route foo with --skip-router', function() { - return generate(['route', 'foo', '--skip-router']).then(function() { - assertFile('app/router.js', { - doesNotContain: 'this.route(\'foo\')' - }); - assertFile('app/routes/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('app/templates/foo.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/unit/routes/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - }); - }); + await generate(['http-proxy', 'foo', 'http://localhost:5000']); - it('route foo with --path', function() { - return generate(['route', 'foo', '--path=:foo_id/show']).then(function() { - assertFile('app/router.js', { - contains: [ - 'this.route(\'foo\', {', - 'path: \':foo_id/show\'', - '});' - ] - }); - }); - }); + expect(file('server/index.js')).to.contain('proxies.forEach(route => route(app));'); - it('route index', function() { - return generate(['route', 'index']).then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('index');" - }); - }); + expect(file('server/proxies/foo.js').content).to.matchSnapshot(); }); - it('route application', function() { - // need to run `initApp` manually here instead of using `generate` helper - // because we need to remove the templates/application.hbs file to prevent - // a prompt (due to a conflict) - return initApp().then(function() { - return remove(path.join('app', 'templates', 'application.hbs')); - }) - .then(function() { - return ember(['generate', 'route', 'application']); - }) - .then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('application');" - }); - }); + it('http-proxy throws', async function () { + if (isExperimentEnabled('VITE')) { + await expect(generate(['http-proxy', 'foo', 'http://localhost:5000'])).to.be.rejectedWith( + 'The http-proxy blueprint is not supported in Vite projects' + ); + } }); - it('route basic isn\'t added to router', function() { - return generate(['route', 'basic']).then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('basic');" - }); - assertFile('app/routes/basic.js'); - }); - }); + it('uses blueprints from the project directory', async function () { + await initApp(); - it('template foo', function() { - return generate(['template', 'foo']).then(function() { - assertFile('app/templates/foo.hbs'); - }); - }); + await outputFile( + 'blueprints/foo/files/app/foos/__name__.js', + "import Ember from 'ember';\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); - it('template foo/bar', function() { - return generate(['template', 'foo/bar']).then(function() { - assertFile('app/templates/foo/bar.hbs'); - }); - }); + await ember(['generate', 'foo', 'bar']); - it('view foo', function() { - return generate(['view', 'foo']).then(function() { - assertFile('app/views/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('tests/unit/views/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo'" - ] - }); - }); + expect(file('app/foos/bar.js')).to.contain('foo: true'); }); - it('view foo/bar', function() { - return generate(['view', 'foo/bar']).then(function() { - assertFile('app/views/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('tests/unit/views/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo/bar'" - ] - }); - }); - }); - - it('resource foos', function() { - return generate(['resource', 'foos']).then(function() { - assertFile('app/router.js', { - contains: 'this.route(\'foos\');' - }); - assertFile('app/models/foo.js', { - contains: 'export default DS.Model.extend' - }); - assertFile('app/routes/foos.js', { - contains: 'export default Ember.Route.extend({' + EOL + '});' - }); - assertFile('app/templates/foos.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/unit/models/foo-test.js', { - contains: "moduleForModel('foo'" - }); - assertFile('tests/unit/routes/foos-test.js', { - contains: "moduleFor('route:foos'" - }); - }); - }); + it('allows custom blueprints to override built-ins', async function () { + await initApp(); + await outputFile( + 'blueprints/controller/files/app/controllers/__name__.js', + "import Ember from 'ember';\n\n" + 'export default Ember.Controller.extend({ custom: true });\n' + ); - it('resource without entity name does not throw exception', function() { + await ember(['generate', 'controller', 'foo']); - var restoreWriteError = MockUI.prototype.writeError; - MockUI.prototype.writeError = function(error) { - expect(error.message).to.equal('The `ember generate ` command requires an entity name to be specified. For more details, use `ember help`.'); - }; - - return generate(['resource']).then(function() { - MockUI.prototype.writeError = restoreWriteError; - }); - - }); - - it('resource foos with --path', function() { - return generate(['resource', 'foos', '--path=app/foos']).then(function() { - assertFile('app/router.js', { - contains: [ - 'this.route(\'foos\', {', - 'path: \'app/foos\'', - '});' - ] - }); - }); + expect(file('app/controllers/foo.js')).to.contain('custom: true'); }); - it('initializer foo', function() { - return generate(['initializer', 'foo']).then(function() { - assertFile('app/initializers/foo.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - - assertFile('tests/unit/initializers/foo-test.js', { - contains: "import { initialize } from '../../../initializers/foo';" - }); - }); - }); + it('allows a path to be specified to a blueprint', async function () { + await outputFile('path/to/blueprints/foo/files/foo/__name__.js', "console.log('bar');\n"); + await generate([path.join('path', 'to', 'blueprints', 'foo'), 'bar']); - it('initializer-test foo', function() { - return generate(['initializer-test', 'foo']).then(function() { - assertFile('tests/unit/initializers/foo-test.js', { - contains: [ - "import { initialize } from '../../../initializers/foo';", - "module('Unit | Initializer | foo'", - "var registry, application;", - "registry = application.registry;", - "initialize(registry, application);" - ] - }); - }); + expect(file('foo/bar.js')).to.contain("console.log('bar');\n"); }); - it('initializer foo/bar', function() { - return generate(['initializer', 'foo/bar']).then(function() { - assertFile('app/initializers/foo/bar.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo/bar'," + EOL + - " initialize: initialize" + EOL + - "};" - }); + it('passes custom cli arguments to blueprint options', async function () { + await initApp(); - assertFile('tests/unit/initializers/foo/bar-test.js', { - contains: "import { initialize } from '../../../../initializers/foo/bar';" - }); - }); - }); + await outputFile( + 'blueprints/customblue/files/app/__name__.js', + 'Q: Can I has custom command? A: <%= hasCustomCommand %>' + ); - it('mixin foo', function() { - return generate(['mixin', 'foo']).then(function() { - assertFile('app/mixins/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo-test.js', { - contains: [ - "import FooMixin from '../../../mixins/foo';" - ] - }); - }); - }); + await outputFile( + 'blueprints/customblue/index.js', + 'module.exports = {\n' + + ' locals(options) {\n' + + ' var loc = {};\n' + + " loc.hasCustomCommand = (options.customCommand) ? 'Yes!' : 'No. :C';\n" + + ' return loc;\n' + + ' },\n' + + '};\n' + ); - it('mixin foo/bar', function() { - return generate(['mixin', 'foo/bar']).then(function() { - assertFile('app/mixins/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo/bar-test.js', { - contains: [ - "import FooBarMixin from '../../../mixins/foo/bar';" - ] - }); - }); - }); + await ember(['generate', 'customblue', 'foo', '--custom-command']); - it('mixin foo/bar/baz', function() { - return generate(['mixin', 'foo/bar/baz']).then(function() { - assertFile('tests/unit/mixins/foo/bar/baz-test.js', { - contains: [ - "import FooBarBazMixin from '../../../mixins/foo/bar/baz';" - ] - }); - }); + expect(file('app/foo.js')).to.contain('A: Yes!'); }); - it('adapter application', function() { - return generate(['adapter', 'application']).then(function() { - assertFile('app/adapters/application.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/adapters/application-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:application'" - ] - }); - }); - }); + it('correctly identifies the root of the project', async function () { + await initApp(); - it('adapter foo', function() { - return generate(['adapter', 'foo']).then(function() { - assertFile('app/adapters/foo.js', { - contains: [ - "import ApplicationAdapter from \'./application\';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/adapters/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo'" - ] - }); - }); - }); + await outputFile( + 'blueprints/controller/files/app/controllers/__name__.js', + "import Ember from 'ember';\n\n" + 'export default Ember.Controller.extend({ custom: true });\n' + ); - it('adapter foo/bar', function() { - return generate(['adapter', 'foo/bar']).then(function() { - assertFile('app/adapters/foo/bar.js', { - contains: [ - "import ApplicationAdapter from \'../application\';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - }); - }); + process.chdir(path.join(tmpdir, 'app')); + await ember(['generate', 'controller', 'foo']); - it('adapter foo/bar/baz', function() { - return generate(['adapter', 'foo/bar/baz']).then(function() { - assertFile('app/adapters/foo/bar/baz.js', { - contains: [ - "import ApplicationAdapter from \'../../application\';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - - it('adapter application cannot extend from --base-class=application', function() { - return generate(['adapter', 'application', '--base-class=application']).then(function() { - expect(false); - }, function(err) { - expect(err.message).to.match(/Adapters cannot extend from themself/); - }); - }); - - it('adapter foo cannot extend from --base-class=foo', function() { - return generate(['adapter', 'foo', '--base-class=foo']).then(function() { - expect(false); - }, function(err) { - expect(err.message).to.match(/Adapters cannot extend from themself/); - }); - }); - - it('adapter extends from --base-class=bar', function() { - return generate(['adapter', 'foo', '--base-class=bar']).then(function() { - assertFile('app/adapters/foo.js', { - contains: [ - "import BarAdapter from './bar';", - "export default BarAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - - it('adapter extends from --base-class=foo/bar', function() { - return generate(['adapter', 'foo/baz', '--base-class=foo/bar']).then(function() { - assertFile('app/adapters/foo/baz.js', { - contains: [ - "import FooBarAdapter from '../foo/bar';", - "export default FooBarAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - - it('adapter extends from application adapter if present', function() { - return generate(['adapter', 'application']).then(function() { - return generate(['adapter', 'foo']).then(function() { - assertFile('app/adapters/foo.js', { - contains: [ - "import ApplicationAdapter from './application';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - }); - - it('adapter favors --base-class over application', function() { - return generate(['adapter', 'application']).then(function() { - return generate(['adapter', 'foo', '--base-class=bar']).then(function() { - assertFile('app/adapters/foo.js', { - contains: [ - "import BarAdapter from './bar';", - "export default BarAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - }); - - it('serializer foo', function() { - return generate(['serializer', 'foo']).then(function() { - assertFile('app/serializers/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/serializers/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - ] - }); - }); - }); - - it('serializer foo/bar', function() { - return generate(['serializer', 'foo/bar']).then(function() { - assertFile('app/serializers/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/serializers/foo/bar-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); - - it('transform foo', function() { - return generate(['transform', 'foo']).then(function() { - assertFile('app/transforms/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('tests/unit/transforms/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo'" - ] - }); - }); - }); - - it('transform foo/bar', function() { - return generate(['transform', 'foo/bar']).then(function() { - assertFile('app/transforms/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - '' + EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('tests/unit/transforms/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo/bar'" - ] - }); - }); - }); - - it('util foo-bar', function() { - return generate(['util', 'foo-bar']).then(function() { - assertFile('app/utils/foo-bar.js', { - contains: 'export default function fooBar() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('tests/unit/utils/foo-bar-test.js', { - contains: [ - "import fooBar from '../../../utils/foo-bar';" - ] - }); - }); - }); - - it('util foo-bar/baz', function() { - return generate(['util', 'foo/bar-baz']).then(function() { - assertFile('app/utils/foo/bar-baz.js', { - contains: 'export default function fooBarBaz() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('tests/unit/utils/foo/bar-baz-test.js', { - contains: [ - "import fooBarBaz from '../../../utils/foo/bar-baz';" - ] - }); - }); - }); - - it('service foo', function() { - return generate(['service', 'foo']).then(function() { - assertFile('app/services/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/services/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo'" - ] - }); - }); - }); - - it('service foo/bar', function() { - return generate(['service', 'foo/bar']).then(function() { - assertFile('app/services/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/services/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo/bar'" - ] - }); - }); - }); - - it('blueprint foo', function() { - return generate(['blueprint', 'foo']).then(function() { - assertFile('blueprints/foo/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); - - it('blueprint foo/bar', function() { - return generate(['blueprint', 'foo/bar']).then(function() { - assertFile('blueprints/foo/bar/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); - - it('http-mock foo', function() { - return generate(['http-mock', 'foo']).then(function() { - assertFile('server/index.js', { - contains:"mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooRouter = express.Router();" + EOL + - EOL + - " fooRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo', fooRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('http-mock foo-bar', function() { - return generate(['http-mock', 'foo-bar']).then(function() { - assertFile('server/index.js', { - contains: "mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo-bar.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooBarRouter = express.Router();" + EOL + - EOL + - " fooBarRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo-bar', fooBarRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); + process.chdir(tmpdir); + expect(file('app/controllers/foo.js')).to.contain('custom: true'); }); - it('http-proxy foo', function() { - return generate(['http-proxy', 'foo', 'http://localhost:5000']).then(function() { - assertFile('server/index.js', { - contains: "proxies.forEach(function(route) { route(app); });" - }); - assertFile('server/proxies/foo.js', { - contains: "var proxyPath = '/foo';" + EOL + - EOL + - "module.exports = function(app) {" + EOL + - " // For options, see:" + EOL + - " // https://github.com/nodejitsu/node-http-proxy" + EOL + - " var proxy = require('http-proxy').createProxyServer({});" + EOL + - EOL + - " proxy.on('error', function(err, req) {" + EOL + - " console.error(err, req.url);" + EOL + - " });" + EOL + - EOL + - " app.use(proxyPath, function(req, res, next){" + EOL + - " // include root path in proxied request" + EOL + - " req.url = proxyPath + '/' + req.url;" + EOL + - " proxy.web(req, res, { target: 'http://localhost:5000' });" + EOL + - " });" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); + it('server', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('uses blueprints from the project directory', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/foo/files/app/foos/__name__.js', - "import Ember from 'ember';" + EOL + - 'export default Ember.Object.extend({ foo: true });' + EOL - ); - }) - .then(function() { - return ember(['generate', 'foo', 'bar']); - }) - .then(function() { - assertFile('app/foos/bar.js', { - contains: 'foo: true' - }); - }); + await generate(['server']); + expect(file('server/index.js')).to.exist; }); - it('allows custom blueprints to override built-ins', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/controller/files/app/controllers/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - "export default Ember.Controller.extend({ custom: true });" + EOL - ); - }) - .then(function() { - return ember(['generate', 'controller', 'foo']); - }) - .then(function() { - assertFile('app/controllers/foo.js', { - contains: 'custom: true' - }); - }); + it('server throws', async function () { + if (isExperimentEnabled('VITE')) { + await expect(generate(['server'])).to.be.rejectedWith('The server blueprint is not supported in Vite projects.'); + } }); - it('uses custom model-test blueprint when generating resources', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/model-test/files/tests/unit/models/__test__.js', - "// custom model-test" + EOL - ); - }) - .then(function() { - return ember(['generate', 'resource', 'foo']); - }) - .then(function() { - assertFile('tests/unit/models/foo-test.js', { - contains: '// custom model-test' - }); - }); + it('lib', async function () { + await generate(['lib']); + expect(dir('lib')).to.exist; }); - it('uses custom route-test blueprint when generating resources', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/route-test/files/tests/unit/routes/__test__.js', - "// custom route-test" + EOL - ); - }) - .then(function() { - return ember(['generate', 'resource', 'foo']); - }) - .then(function() { - assertFile('tests/unit/routes/foo-test.js', { - contains: '// custom route-test' - }); - }); - }); + it('custom blueprint availableOptions', async function () { + await initApp(); + await ember(['generate', 'blueprint', 'foo']); - it('passes custom cli arguments to blueprint options', function() { - return initApp() - .then(function() { - outputFile( - 'blueprints/customblue/files/app/__name__.js', - "Q: Can I has custom command? A: <%= hasCustomCommand %>" - ); - return outputFile( - 'blueprints/customblue/index.js', - "module.exports = {" + EOL + - " locals: function(options) {" + EOL + - " var loc = {};" + EOL + - " loc.hasCustomCommand = (options.customCommand) ? 'Yes!' : 'No. :C';" + EOL + - " return loc;" + EOL + - " }," + EOL + - "};" + EOL - ); - }) - .then(function() { - return ember(['generate', 'customblue', 'foo', '--custom-command']); - }) - .then(function() { - assertFile('app/foo.js', { - contains: 'A: Yes!' - }); - }); - }); + replaceFile( + 'blueprints/foo/index.js', + 'module.exports = {', + 'module.exports = {\navailableOptions: [ \n' + + "{ name: 'foo',\ntype: String, \n" + + "values: ['one', 'two'],\n" + + "default: 'one',\n" + + "aliases: [ {'one': 'one'}, {'two': 'two'} ] } ],\n" + + 'locals(options) {\n' + + 'return { foo: options.foo };\n' + + '},' + ); - it('acceptance-test foo', function() { - return generate(['acceptance-test', 'foo']).then(function() { - var expected = path.join(__dirname, '../fixtures/generate/acceptance-test-expected.js'); + await outputFile( + 'blueprints/foo/files/app/foos/__name__.js', + "import Ember from 'ember';\n" + 'export default Ember.Object.extend({ foo: <%= foo %> });\n' + ); - assertFileEquals('tests/acceptance/foo-test.js', expected); - }); - }); + await ember(['generate', 'foo', 'bar', '-two']); - it('test-helper foo', function() { - return generate(['test-helper', 'foo']).then(function() { - assertFile('tests/helpers/foo.js', { - contains: "import Ember from 'ember';" + EOL + - EOL + - "export default Ember.Test.registerAsyncHelper('foo', function(app) {" + EOL + - EOL + - "});" - }); - }); + expect(file('app/foos/bar.js')).to.contain('export default Ember.Object.extend({ foo: two });'); }); - it('correctly identifies the root of the project', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/controller/files/app/controllers/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - "export default Ember.Controller.extend({ custom: true });" + EOL - ); - }) - .then(function() { - process.chdir(path.join(tmpdir, 'app')); - }) - .then(function() { - return ember(['generate', 'controller', 'foo']); - }) - .then(function() { - process.chdir(tmpdir); - }) - .then(function() { - assertFile('app/controllers/foo.js', { - contains: 'custom: true' - }); - }); - }); + it('calls lint fix function', async function () { + let lintFixStub = td.replace(lintFix, 'run'); - it('route foo --dry-run does not change router.js', function() { - return generate(['route', 'foo', '--dry-run']).then(function() { - assertFile('app/router.js', { - doesNotContain: "route('foo')" - }); - }); - }); + await generate(['blueprint', 'foo', '--lint-fix']); - it('server', function() { - return generate(['server']).then(function() { - assertFile('server/index.js'); - assertFile('server/.jshintrc'); - }); + td.verify(lintFixStub(), { ignoreExtraArgs: true, times: 1 }); }); - it('availableOptions work with aliases.', function() { - return generate(['route', 'foo', '-d']).then(function() { - assertFile('app/router.js', { - doesNotContain: "route('foo')" - }); - }); - }); + it('successfully generates a blueprint with a scoped name', async function () { + await initApp(); + await ember(['g', 'blueprint', '@foo/bar']); + await outputFile('blueprints/@foo/bar/files/__name__.js', ''); + await ember(['g', '@foo/bar', 'baz']); - it('lib', function() { - return generate(['lib']).then(function() { - assertFile('lib/.jshintrc'); - }); + expect(file('baz.js')).to.exist; }); - it('custom blueprint availableOptions', function() { - return initApp() - .then(function() { - return ember(['generate', 'blueprint', 'foo']) - .then(function() { - replaceFile('blueprints/foo/index.js', 'module.exports = {', - 'module.exports = {' + EOL + 'availableOptions: [ ' + EOL + - '{ name: \'foo\',' + EOL + 'type: String, '+ EOL + - 'values: [\'one\', \'two\'],' + EOL + - 'default: \'one\',' + EOL + - 'aliases: [ {\'one\': \'one\'}, {\'two\': \'two\'} ] } ],' + EOL + - 'locals: function(options) {' + EOL + - 'return { foo: options.foo };' + EOL + - '},'); - return outputFile( - 'blueprints/foo/files/app/foos/__name__.js', - "import Ember from 'ember';" + EOL + - 'export default Ember.Object.extend({ foo: <%= foo %> });' + EOL - ) - .then(function() { - return ember(['generate','foo','bar','-two']); - }); - }); - }) - .then(function() { - assertFile('app/foos/bar.js', { - contain: ['export default Ember.Object.extend({ foo: two });'] - }); - }); + it(`throws the unknown blueprint error when \`name\` matches a folder's name, but doesn't include the \`${path.sep}\` char`, async function () { + await expect(generate(['tests'])).to.be.rejectedWith('Unknown blueprint: tests'); }); }); diff --git a/tests/acceptance/generate-test.js.snap b/tests/acceptance/generate-test.js.snap new file mode 100644 index 0000000000..cf9baf00ae --- /dev/null +++ b/tests/acceptance/generate-test.js.snap @@ -0,0 +1,148 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Acceptance: ember generate blueprint foo/bar 1`] = ` +"'use strict'; + +module.exports = { + description: '' + + // locals(options) { + // // Return custom template variables here. + // return { + // foo: options.entity.options.foo + // }; + // } + + // afterInstall(options) { + // // Perform extra work here. + // } +}; +" +`; + +exports[`Acceptance: ember generate http-mock foo 1`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooRouter = express.Router(); + + fooRouter.get('/', function(req, res) { + res.send({ + 'foo': [] + }); + }); + + fooRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooRouter.get('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.put('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo', require('body-parser').json()); + app.use('/api/foo', fooRouter); +}; +" +`; + +exports[`Acceptance: ember generate http-mock foo-bar 1`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooBarRouter = express.Router(); + + fooBarRouter.get('/', function(req, res) { + res.send({ + 'foo-bar': [] + }); + }); + + fooBarRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooBarRouter.get('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.put('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo-bar', require('body-parser').json()); + app.use('/api/foo-bar', fooBarRouter); +}; +" +`; + +exports[`Acceptance: ember generate http-proxy foo 1`] = ` +"'use strict'; + +const proxyPath = '/foo'; + +module.exports = function(app) { + // For options, see: + // https://github.com/nodejitsu/node-http-proxy + let proxy = require('http-proxy').createProxyServer({}); + + proxy.on('error', function(err, req) { + console.error(err, req.url); + }); + + app.use(proxyPath, function(req, res, next){ + // include root path in proxied request + req.url = proxyPath + '/' + req.url; + proxy.web(req, res, { target: 'http://localhost:5000' }); + }); +}; +" +`; diff --git a/tests/acceptance/help-test.js b/tests/acceptance/help-test.js new file mode 100644 index 0000000000..5abeb8ae62 --- /dev/null +++ b/tests/acceptance/help-test.js @@ -0,0 +1,394 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { expect } = require('chai'); +const EOL = require('os').EOL; +const processHelpString = require('../helpers/process-help-string'); +const convertToJson = require('../helpers/convert-help-output-to-json'); +const commandOptions = require('../factories/command-options'); +const HelpCommand = require('../../lib/commands/help'); +const requireAsHash = require('../../lib/utilities/require-as-hash'); +const Command = require('../../lib/models/command'); + +let FooCommand = Command.extend({ + name: 'foo', + description: 'Initializes the warp drive.', + works: 'insideProject', + + availableOptions: [{ name: 'dry-run', type: Boolean, default: false, aliases: ['d'] }], + + anonymousOptions: [''], +}); + +describe('Acceptance: ember help in classic', function () { + let options, command; + + beforeEach(function () { + let commands = requireAsHash('../../lib/commands/*.js', Command); + + options = commandOptions({ + commands, + project: { + isEmberCLIProject() { + return true; + }, + blueprintLookupPaths() { + return []; + }, + }, + }); + + command = new HelpCommand(options); + }); + + it('works', function () { + command.run(options, []); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'classic', 'help.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + expect(output).to.equal(expected); + }); + + it('prints addon commands', function () { + options.project.eachAddonCommand = function (cb) { + cb('dummy-addon', { Foo: FooCommand }); + }; + + command.run(options, []); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'classic', 'help-with-addon.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + it('prints single addon commands', function () { + options.project.eachAddonCommand = function (cb) { + cb('dummy-addon', { Foo: FooCommand }); + }; + + command.run(options, ['foo']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'classic', 'foo.txt'); + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + it('prints all blueprints', function () { + command.run(options, ['generate']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'classic', 'generate.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + let expected = loadTextFixture(fixturePath); + + expect(output).to.contain(expected); + }); + + it('prints helpful message for unknown command', function () { + command.run(options, ['asdf']); + + let output = options.ui.output; + + expect(output).to.contain("No help entry for 'asdf'"); + expect(output).to.not.contain('undefined'); + }); + + it('prints a single blueprints', function () { + command.run(options, ['generate', 'blueprint']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'classic', 'generate-blueprint.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + it('prints blueprints from addons', function () { + options.project.blueprintLookupPaths = function () { + return [path.join(__dirname, '..', 'fixtures', 'blueprints')]; + }; + + command.run(options, ['generate']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'classic', 'generate-with-addon.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + describe('--json', function () { + beforeEach(function () { + options.json = true; + }); + + it('works', function () { + command.run(options, []); + + let json = convertToJson(options.ui.output); + const expected = require('../fixtures/help/classic/help.js'); + + expect(json).to.deep.equal(expected); + }); + + it('prints commands from addons', function () { + options.project.eachAddonCommand = function (cb) { + cb('dummy-addon', { Foo: FooCommand }); + }; + + command.run(options, []); + + let json = convertToJson(options.ui.output); + const expected = require('../fixtures/help/classic/with-addon-commands.js'); + + expect(json).to.deep.equal(expected); + }); + + it('prints blueprints from addons', function () { + options.project.blueprintLookupPaths = function () { + return [path.join(__dirname, '..', 'fixtures', 'blueprints')]; + }; + + command.run(options, []); + + let json = convertToJson(options.ui.output); + const expected = require('../fixtures/help/classic/with-addon-blueprints.js'); + + expect(json).to.deep.equal(expected); + }); + }); +}); + +describe('Acceptance: ember help in vite', function () { + let options, command; + + beforeEach(function () { + let commands = requireAsHash('../../lib/commands/*.js', Command); + + options = commandOptions({ + commands, + project: { + isEmberCLIProject() { + return true; + }, + isViteProject() { + return true; + }, + blueprintLookupPaths() { + return []; + }, + }, + }); + + command = new HelpCommand(options); + }); + + it('works', function () { + command.run(options, []); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'help.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + expect(output).to.equal(expected); + }); + + it('prints addon commands', function () { + options.project.eachAddonCommand = function (cb) { + cb('dummy-addon', { Foo: FooCommand }); + }; + + command.run(options, []); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'help-with-addon.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + it('prints single addon commands', function () { + options.project.eachAddonCommand = function (cb) { + cb('dummy-addon', { Foo: FooCommand }); + }; + + command.run(options, ['foo']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'foo.txt'); + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + it('prints all blueprints', function () { + command.run(options, ['generate']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'generate.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + let expected = loadTextFixture(fixturePath); + + expect(output).to.contain(expected); + }); + + it('prints helpful message for unknown command', function () { + command.run(options, ['asdf']); + + let output = options.ui.output; + + expect(output).to.contain("No help entry for 'asdf'"); + expect(output).to.not.contain('undefined'); + }); + + it('prints a single blueprints', function () { + command.run(options, ['generate', 'blueprint']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'generate-blueprint.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + it('prints blueprints from addons', function () { + options.project.blueprintLookupPaths = function () { + return [path.join(__dirname, '..', 'fixtures', 'blueprints')]; + }; + + command.run(options, ['generate']); + + let output = options.ui.output; + + let fixturePath = path.join(__dirname, '..', 'fixtures', 'help', 'generate-with-addon.txt'); + + // makes updating this fixture much much easier... + if (process.env.WRITE_HELP_FIXTURES) { + fs.writeFileSync(fixturePath, output, { encoding: 'utf-8' }); + } + + let expected = loadTextFixture(fixturePath); + + expect(output).to.equal(expected); + }); + + describe('--json', function () { + beforeEach(function () { + options.json = true; + }); + + it('works', function () { + command.run(options, []); + + let json = convertToJson(options.ui.output); + const expected = require('../fixtures/help/help.js'); + + expect(json).to.deep.equal(expected); + }); + + it('prints commands from addons', function () { + options.project.eachAddonCommand = function (cb) { + cb('dummy-addon', { Foo: FooCommand }); + }; + + command.run(options, []); + + let json = convertToJson(options.ui.output); + const expected = require('../fixtures/help/with-addon-commands.js'); + + expect(json).to.deep.equal(expected); + }); + + it('prints blueprints from addons', function () { + options.project.blueprintLookupPaths = function () { + return [path.join(__dirname, '..', 'fixtures', 'blueprints')]; + }; + + command.run(options, []); + + let json = convertToJson(options.ui.output); + const expected = require('../fixtures/help/with-addon-blueprints.js'); + + expect(json).to.deep.equal(expected); + }); + }); +}); + +function loadTextFixture(path) { + let content = fs.readFileSync(path, { encoding: 'utf8' }); + let decoded = decodeUnicode(content); + let processed = processHelpString(decoded); + return processed.replace(/\n/g, EOL); +} + +function decodeUnicode(str) { + return str.replace(/\\u([\d\w]{4})/gi, function (match, grp) { + return String.fromCharCode(parseInt(grp, 16)); + }); +} diff --git a/tests/acceptance/help/help-addon-json-test.js b/tests/acceptance/help/help-addon-json-test.js deleted file mode 100644 index b6f25abed2..0000000000 --- a/tests/acceptance/help/help-addon-json-test.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json addon', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'addon', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'addon', - description: 'Generates a new folder structure for building an addon, complete with test harness.', - aliases: [], - works: 'outsideProject', - availableOptions: [ - { - name: 'dry-run', - default: false, - aliases: ['d'], - key: 'dryRun', - required: false - }, - { - name: 'verbose', - default: false, - aliases: ['v'], - key: 'verbose', - required: false - }, - { - name: 'blueprint', - default: 'addon', - aliases: ['b'], - key: 'blueprint', - required: false - }, - { - name: 'skip-npm', - default: false, - aliases: ['sn'], - key: 'skipNpm', - required: false - }, - { - name: 'skip-bower', - default: false, - aliases: ['sb'], - key: 'skipBower', - required: false - }, - { - name: 'skip-git', - default: false, - aliases: ['sg'], - key: 'skipGit', - required: false - } - ], - anonymousOptions: [''] - }); - }); - }); -}); diff --git a/tests/acceptance/help/help-addon-test.js b/tests/acceptance/help/help-addon-test.js deleted file mode 100644 index 335c6156da..0000000000 --- a/tests/acceptance/help/help-addon-test.js +++ /dev/null @@ -1,54 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help addon', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'addon' - ]).then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember addon \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - Generates a new folder structure for building an addon, complete with test harness.' + EOL + '\ - \u001b[36m--dry-run\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -d\u001b[39m' + EOL + '\ - \u001b[36m--verbose\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -v\u001b[39m' + EOL + '\ - \u001b[36m--blueprint\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: addon)\u001b[39m' + EOL + '\ - \u001b[90maliases: -b \u001b[39m' + EOL + '\ - \u001b[36m--skip-npm\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sn\u001b[39m' + EOL + '\ - \u001b[36m--skip-bower\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sb\u001b[39m' + EOL + '\ - \u001b[36m--skip-git\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sg\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-build-json-test.js b/tests/acceptance/help/help-build-json-test.js deleted file mode 100644 index 3c19fd9ab8..0000000000 --- a/tests/acceptance/help/help-build-json-test.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json build', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'build', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'build', - description: 'Builds your app and places it into the output path (dist/ by default).', - aliases: ['b'], - works: 'insideProject', - availableOptions: [ - { - name: 'environment', - default: 'development', - aliases: [ - 'e', - { dev: 'development' }, - { prod: 'production' } - ], - key: 'environment', - required: false - }, - { - name: 'output-path', - type: 'path', - default: 'dist/', - aliases: ['o'], - key: 'outputPath', - required: false - }, - { - name: 'watch', - default: false, - aliases: ['w'], - key: 'watch', - required: false - }, - { - name: 'watcher', - key: 'watcher', - required: false - } - ], - anonymousOptions: [] - }); - }); - }); - - it('works with alias b', function() { - return ember([ - 'help', - 'b', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('build'); - }); - }); -}); diff --git a/tests/acceptance/help/help-build-test.js b/tests/acceptance/help/help-build-test.js deleted file mode 100644 index 8c1595bc4a..0000000000 --- a/tests/acceptance/help/help-build-test.js +++ /dev/null @@ -1,65 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help build', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'build' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember build \u001b[36m\u001b[39m' + EOL + '\ - Builds your app and places it into the output path (dist/ by default).' + EOL + '\ - \u001b[90maliases: b\u001b[39m' + EOL + '\ - \u001b[36m--environment\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: development)\u001b[39m' + EOL + '\ - \u001b[90maliases: -e , -dev (--environment=development), -prod (--environment=production)\u001b[39m' + EOL + '\ - \u001b[36m--output-path\u001b[39m \u001b[36m(Path)\u001b[39m \u001b[36m(Default: dist/)\u001b[39m' + EOL + '\ - \u001b[90maliases: -o \u001b[39m' + EOL + '\ - \u001b[36m--watch\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -w\u001b[39m' + EOL + '\ - \u001b[36m--watcher\u001b[39m \u001b[36m(String)\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias b', function() { - return ember([ - 'help', - 'b' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember build \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-destroy-json-test.js b/tests/acceptance/help/help-destroy-json-test.js deleted file mode 100644 index 3e5e45e8cf..0000000000 --- a/tests/acceptance/help/help-destroy-json-test.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json destroy', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'destroy', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'destroy', - description: 'Destroys code generated by `generate` command.', - aliases: ['d'], - works: 'insideProject', - availableOptions: [ - { - name: 'dry-run', - default: false, - aliases: ['d'], - key: 'dryRun', - required: false - }, - { - name: 'verbose', - default: false, - aliases: ['v'], - key: 'verbose', - required: false - }, - { - name: 'pod', - default: false, - aliases: ['p'], - key: 'pod', - required: false - }, - { - name: 'classic', - default: false, - aliases: ['c'], - key: 'classic', - required: false - }, - { - name: 'dummy', - default: false, - aliases: ['dum', 'id'], - key: 'dummy', - required: false - }, - { - name: 'in-repo-addon', - default: null, - aliases: ['in-repo', 'ir'], - key: 'inRepoAddon', - required: false - } - ], - anonymousOptions: [''] - }); - }); - }); - - it('works with alias d', function() { - return ember([ - 'help', - 'd', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('destroy'); - }); - }); -}); diff --git a/tests/acceptance/help/help-destroy-test.js b/tests/acceptance/help/help-destroy-test.js deleted file mode 100644 index b0ac089e65..0000000000 --- a/tests/acceptance/help/help-destroy-test.js +++ /dev/null @@ -1,73 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help destroy', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'destroy' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember destroy \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - Destroys code generated by `generate` command.' + EOL + '\ - \u001b[90maliases: d\u001b[39m' + EOL + '\ - \u001b[36m--dry-run\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -d\u001b[39m' + EOL + '\ - \u001b[36m--verbose\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -v\u001b[39m' + EOL + '\ - \u001b[36m--pod\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -p\u001b[39m' + EOL + '\ - \u001b[36m--classic\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -c\u001b[39m' + EOL + '\ - \u001b[36m--dummy\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -dum, -id\u001b[39m' + EOL + '\ - \u001b[36m--in-repo-addon\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: null)\u001b[39m' + EOL + '\ - \u001b[90maliases: -in-repo , -ir \u001b[39m' + EOL + '\ -' + EOL + '\ -' + EOL + '\ - Run `ember help generate` to view a list of available blueprints.' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias d', function() { - return ember([ - 'help', - 'd' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember destroy \u001b[33m\u001b[39m \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-generate-json-test.js b/tests/acceptance/help/help-generate-json-test.js deleted file mode 100644 index 5458bd6ed0..0000000000 --- a/tests/acceptance/help/help-generate-json-test.js +++ /dev/null @@ -1,615 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json generate', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'generate', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'generate', - description: 'Generates new code from blueprints.', - aliases: ['g'], - works: 'insideProject', - availableOptions: [ - { - name: 'dry-run', - default: false, - aliases: ['d'], - key: 'dryRun', - required: false - }, - { - name: 'verbose', - default: false, - aliases: ['v'], - key: 'verbose', - required: false - }, - { - name: 'pod', - default: false, - aliases: ['p'], - key: 'pod', - required: false - }, - { - name: 'classic', - default: false, - aliases: ['c'], - key: 'classic', - required: false - }, - { - name: 'dummy', - default: false, - aliases: ['dum', 'id'], - key: 'dummy', - required: false - }, - { - name: 'in-repo-addon', - default: null, - aliases: ['in-repo', 'ir'], - key: 'inRepoAddon', - required: false - } - ], - anonymousOptions: [''], - availableBlueprints: [ - { - 'ember-cli': [ - { - name: 'acceptance-test', - description: 'Generates an acceptance test for a feature.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'adapter', - description: 'Generates an ember-data adapter.', - availableOptions: [ - { - name: 'base-class', - key: 'baseClass', - required: false - } - ], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'adapter-test', - description: 'Generates an ember-data adapter unit test', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'addon', - description: 'The default blueprint for ember-cli addons.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'addon-import', - description: 'Generates an import wrapper.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'app', - description: 'The default blueprint for ember-cli projects.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'blueprint', - description: 'Generates a blueprint and definition.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'component', - description: 'Generates a component. Name must contain a hyphen.', - availableOptions: [ - { - name: 'path', - default: 'components', - aliases: [ - { 'no-path': '' } - ], - key: 'path', - required: false - } - ], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'component-addon', - description: 'Generates a component. Name must contain a hyphen.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'component-test', - description: 'Generates a component integration or unit test.', - availableOptions: [ - { - name: 'test-type', - type: ['integration', 'unit'], - default: 'integration', - aliases: [ - { i: 'integration' }, - { u: 'unit' }, - { integration: 'integration' }, - { unit: 'unit' } - ], - key: 'testType', - required: false - } - ], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'controller', - description: 'Generates a controller.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'controller-test', - description: 'Generates a controller unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'helper', - description: 'Generates a helper function.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'helper-addon', - description: 'Generates an import wrapper.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'helper-test', - description: 'Generates a helper unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'http-mock', - description: 'Generates a mock api endpoint in /api prefix.', - availableOptions: [], - anonymousOptions: ['endpoint-path'], - overridden: false - }, - { - name: 'http-proxy', - description: 'Generates a relative proxy to another server.', - availableOptions: [], - anonymousOptions: ['local-path', 'remote-url'], - overridden: false - }, - { - name: 'in-repo-addon', - description: 'The blueprint for addon in repo ember-cli addons.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'initializer', - description: 'Generates an initializer.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'initializer-addon', - description: 'Generates an import wrapper.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'initializer-test', - description: 'Generates an initializer unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'lib', - description: 'Generates a lib directory for in-repo addons.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'mixin', - description: 'Generates a mixin.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'mixin-test', - description: 'Generates a mixin unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'model', - description: 'Generates an ember-data model.', - availableOptions: [], - anonymousOptions: ['name', 'attr:type'], - overridden: false - }, - { - name: 'model-test', - description: 'Generates a model unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'resource', - description: 'Generates a model and route.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'route', - description: 'Generates a route and registers it with the router.', - availableOptions: [ - { - name: 'path', - default: '', - key: 'path', - required: false - }, - { - name: 'skip-router', - default: false, - key: 'skipRouter', - required: false - } - ], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'route-addon', - description: 'Generates import wrappers for a route and its template.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'route-test', - description: 'Generates a route unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'serializer', - description: 'Generates an ember-data serializer.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'serializer-test', - description: 'Generates a serializer unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'server', - description: 'Generates a server directory for mocks and proxies.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'service', - description: 'Generates a service.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'service-test', - description: 'Generates a service unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'template', - description: 'Generates a template.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'test-helper', - description: 'Generates a test helper.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'transform', - description: 'Generates an ember-data value transform.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'transform-test', - description: 'Generates a transform unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'util', - description: 'Generates a simple utility module/function.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'util-test', - description: 'Generates a util unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'view', - description: 'Generates a view subclass.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - }, - { - name: 'view-test', - description: 'Generates a view unit test.', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - } - ] - } - ] - }); - }); - }); - - it('works with alias g', function() { - return ember([ - 'help', - 'generate', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('generate'); - }); - }); - - it('lists overridden blueprints', function() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'generate', - 'blueprint', - 'component' - ]); - }) - .then(function() { - return ember([ - 'help', - 'generate', - 'component', - '--verbose', - '--json' - ]); - }) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.availableBlueprints).to.deep.equal([ - { - 'my-app': [ - { - name: 'component', - description: '', - availableOptions: [], - anonymousOptions: ['name'], - overridden: false - } - ] - }, - { - 'ember-cli': [ - { - name: 'component', - description: 'Generates a component. Name must contain a hyphen.', - availableOptions: [ - { - name: 'path', - default: 'components', - aliases: [ - { 'no-path': '' } - ], - key: 'path', - required: false - } - ], - anonymousOptions: ['name'], - overridden: true - } - ] - } - ]); - }); - }); - - it('handles missing blueprint', function() { - return ember([ - 'help', - 'generate', - 'asdf', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.availableBlueprints).to.deep.equal([ - { - 'ember-cli': [] - } - ]); - }); - }); - - it('works with single blueprint', function() { - return ember([ - 'help', - 'generate', - 'component', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.availableBlueprints).to.deep.equal([ - { - 'ember-cli': [ - { - name: 'component', - description: 'Generates a component. Name must contain a hyphen.', - availableOptions: [ - { - name: 'path', - default: 'components', - aliases: [ - { 'no-path': '' } - ], - key: 'path', - required: false - } - ], - anonymousOptions: ['name'], - overridden: false - } - ] - } - ]); - }); - }); - - it('handles extra help', function() { - return ember([ - 'help', - 'generate', - 'model', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.availableBlueprints[0]['ember-cli'][0].detailedHelp).to.equal('\ -You may generate models with as many attrs as you would like to pass. The following attribute types are supported:' + EOL + '\ - ' + EOL + '\ - :array' + EOL + '\ - :boolean' + EOL + '\ - :date' + EOL + '\ - :object' + EOL + '\ - :number' + EOL + '\ - :string' + EOL + '\ - :your-custom-transform' + EOL + '\ - :belongs-to:' + EOL + '\ - :has-many:' + EOL + '\ -' + EOL + '\ -For instance: \\`ember generate model taco filling:belongs-to:protein toppings:has-many:toppings name:string price:number misc\\`' + EOL + '\ -would result in the following model:' + EOL + '\ -' + EOL + '\ -```js' + EOL + '\ -import DS from \'ember-data\';' + EOL + '\ -export default DS.Model.extend({' + EOL + '\ - filling: DS.belongsTo(\'protein\'),' + EOL + '\ - toppings: DS.hasMany(\'topping\'),' + EOL + '\ - name: DS.attr(\'string\'),' + EOL + '\ - price: DS.attr(\'number\'),' + EOL + '\ - misc: DS.attr()' + EOL + '\ -});' + EOL + '\ -```' + EOL); - }); - }); -}); diff --git a/tests/acceptance/help/help-generate-test.js b/tests/acceptance/help/help-generate-test.js deleted file mode 100644 index 94f14b482e..0000000000 --- a/tests/acceptance/help/help-generate-test.js +++ /dev/null @@ -1,278 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help generate', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'generate' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember generate \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - Generates new code from blueprints.' + EOL + '\ - \u001b[90maliases: g\u001b[39m' + EOL + '\ - \u001b[36m--dry-run\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -d\u001b[39m' + EOL + '\ - \u001b[36m--verbose\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -v\u001b[39m' + EOL + '\ - \u001b[36m--pod\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -p\u001b[39m' + EOL + '\ - \u001b[36m--classic\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -c\u001b[39m' + EOL + '\ - \u001b[36m--dummy\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -dum, -id\u001b[39m' + EOL + '\ - \u001b[36m--in-repo-addon\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: null)\u001b[39m' + EOL + '\ - \u001b[90maliases: -in-repo , -ir \u001b[39m' + EOL + '\ -' + EOL + '\ -' + EOL + '\ - Available blueprints:' + EOL + '\ - ember-cli:' + EOL + '\ - acceptance-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an acceptance test for a feature.\u001b[39m' + EOL + '\ - adapter \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an ember-data adapter.\u001b[39m' + EOL + '\ - \u001b[36m--base-class\u001b[39m' + EOL + '\ - adapter-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an ember-data adapter unit test\u001b[39m' + EOL + '\ - addon \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mThe default blueprint for ember-cli addons.\u001b[39m' + EOL + '\ - addon-import \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an import wrapper.\u001b[39m' + EOL + '\ - app \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mThe default blueprint for ember-cli projects.\u001b[39m' + EOL + '\ - blueprint \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a blueprint and definition.\u001b[39m' + EOL + '\ - component \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a component. Name must contain a hyphen.\u001b[39m' + EOL + '\ - \u001b[36m--path\u001b[39m \u001b[36m(Default: components)\u001b[39m' + EOL + '\ - \u001b[90maliases: -no-path (--path=)\u001b[39m' + EOL + '\ - component-addon \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a component. Name must contain a hyphen.\u001b[39m' + EOL + '\ - component-test \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a component integration or unit test.\u001b[39m' + EOL + '\ - \u001b[36m--test-type\u001b[39m \u001b[36m(Default: integration)\u001b[39m' + EOL + '\ - \u001b[90maliases: -i (--test-type=integration), -u (--test-type=unit), -integration (--test-type=integration), -unit (--test-type=unit)\u001b[39m' + EOL + '\ - controller \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a controller.\u001b[39m' + EOL + '\ - controller-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a controller unit test.\u001b[39m' + EOL + '\ - helper \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a helper function.\u001b[39m' + EOL + '\ - helper-addon \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an import wrapper.\u001b[39m' + EOL + '\ - helper-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a helper unit test.\u001b[39m' + EOL + '\ - http-mock \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a mock api endpoint in /api prefix.\u001b[39m' + EOL + '\ - http-proxy \u001b[33m\u001b[39m \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a relative proxy to another server.\u001b[39m' + EOL + '\ - in-repo-addon \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mThe blueprint for addon in repo ember-cli addons.\u001b[39m' + EOL + '\ - initializer \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an initializer.\u001b[39m' + EOL + '\ - initializer-addon \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an import wrapper.\u001b[39m' + EOL + '\ - initializer-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an initializer unit test.\u001b[39m' + EOL + '\ - lib \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a lib directory for in-repo addons.\u001b[39m' + EOL + '\ - mixin \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a mixin.\u001b[39m' + EOL + '\ - mixin-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a mixin unit test.\u001b[39m' + EOL + '\ - model \u001b[33m\u001b[39m \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an ember-data model.\u001b[39m' + EOL + '\ - model-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a model unit test.\u001b[39m' + EOL + '\ - resource \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a model and route.\u001b[39m' + EOL + '\ - route \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a route and registers it with the router.\u001b[39m' + EOL + '\ - \u001b[36m--path\u001b[39m \u001b[36m(Default: )\u001b[39m' + EOL + '\ - \u001b[36m--skip-router\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - route-addon \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates import wrappers for a route and its template.\u001b[39m' + EOL + '\ - route-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a route unit test.\u001b[39m' + EOL + '\ - serializer \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an ember-data serializer.\u001b[39m' + EOL + '\ - serializer-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a serializer unit test.\u001b[39m' + EOL + '\ - server \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a server directory for mocks and proxies.\u001b[39m' + EOL + '\ - service \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a service.\u001b[39m' + EOL + '\ - service-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a service unit test.\u001b[39m' + EOL + '\ - template \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a template.\u001b[39m' + EOL + '\ - test-helper \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a test helper.\u001b[39m' + EOL + '\ - transform \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates an ember-data value transform.\u001b[39m' + EOL + '\ - transform-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a transform unit test.\u001b[39m' + EOL + '\ - util \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a simple utility module/function.\u001b[39m' + EOL + '\ - util-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a util unit test.\u001b[39m' + EOL + '\ - view \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a view subclass.\u001b[39m' + EOL + '\ - view-test \u001b[33m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a view unit test.\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias g', function() { - return ember([ - 'help', - 'g' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember generate \u001b[33m\u001b[39m \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); - - it('lists overridden blueprints', function() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'generate', - 'blueprint', - 'component' - ]); - }) - .then(function() { - return ember([ - 'help', - 'generate', - '--verbose' - ]); - }) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ - my-app:' + EOL + '\ - component \u001b[33m\u001b[39m' + EOL + '\ -' + EOL + '\ - ember-cli:' + EOL); - - expect(output).to.include(testString); - expect(output).to.include(processHelpString(EOL + '\ - \u001b[90m(overridden)\u001b[39m \u001b[90mcomponent\u001b[39m' + EOL)); - }); - }); - - it('handles missing blueprint', function() { - return ember([ - 'help', - 'generate', - 'asdf' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -\u001b[33mThe \'asdf\' blueprint does not exist in this project.\u001b[39m' + EOL + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with single blueprint', function() { - return ember([ - 'help', - 'generate', - 'component' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ - component \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - \u001b[90mGenerates a component. Name must contain a hyphen.\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('handles extra help', function() { - return ember([ - 'help', - 'generate', - 'model' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = EOL + '\ - \u001b[0m' + processHelpString('\u001b[90mYou may generate models with as many attrs as you would like to pass. The following attribute types are supported:\u001b[39m\n\ - \u001b[33m\u001b[39m\n\ - \u001b[33m\u001b[39m:array\n\ - \u001b[33m\u001b[39m:boolean\n\ - \u001b[33m\u001b[39m:date\n\ - \u001b[33m\u001b[39m:object\n\ - \u001b[33m\u001b[39m:number\n\ - \u001b[33m\u001b[39m:string\n\ - \u001b[33m\u001b[39m:your-custom-transform\n\ - \u001b[33m\u001b[39m:belongs-to:\u001b[33m\u001b[39m\n\ - \u001b[33m\u001b[39m:has-many:\u001b[33m\u001b[39m') + '\u001b[0m\n\ - \n\ - \u001b[0mFor instance: ' + processHelpString('\u001b[32m`ember generate model taco filling:belongs-to:protein toppings:has-many:toppings name:string price:number misc`\u001b[39m') + '\n\ - would result in the following model:\u001b[0m\n\ - \n\ - \n\ - \u001b[33m\u001b[94mimport\u001b[39m \u001b[37mDS\u001b[39m \u001b[37mfrom\u001b[39m \u001b[92m\'ember-data\'\u001b[39m\u001b[90m;\u001b[39m\n\ - \u001b[94mexport\u001b[39m \u001b[94mdefault\u001b[39m \u001b[37mDS\u001b[39m\u001b[32m.\u001b[39m\u001b[37mModel\u001b[39m\u001b[32m.\u001b[39m\u001b[37mextend\u001b[39m\u001b[90m(\u001b[39m\u001b[33m{\u001b[39m\n\ - \u001b[37mfilling\u001b[39m\u001b[93m:\u001b[39m \u001b[37mDS\u001b[39m\u001b[32m.\u001b[39m\u001b[37mbelongsTo\u001b[39m\u001b[90m(\u001b[39m\u001b[92m\'protein\'\u001b[39m\u001b[90m)\u001b[39m\u001b[32m,\u001b[39m\n\ - \u001b[37mtoppings\u001b[39m\u001b[93m:\u001b[39m \u001b[37mDS\u001b[39m\u001b[32m.\u001b[39m\u001b[37mhasMany\u001b[39m\u001b[90m(\u001b[39m\u001b[92m\'topping\'\u001b[39m\u001b[90m)\u001b[39m\u001b[32m,\u001b[39m\n\ - \u001b[37mname\u001b[39m\u001b[93m:\u001b[39m \u001b[37mDS\u001b[39m\u001b[32m.\u001b[39m\u001b[37mattr\u001b[39m\u001b[90m(\u001b[39m\u001b[92m\'string\'\u001b[39m\u001b[90m)\u001b[39m\u001b[32m,\u001b[39m\n\ - \u001b[37mprice\u001b[39m\u001b[93m:\u001b[39m \u001b[37mDS\u001b[39m\u001b[32m.\u001b[39m\u001b[37mattr\u001b[39m\u001b[90m(\u001b[39m\u001b[92m\'number\'\u001b[39m\u001b[90m)\u001b[39m\u001b[32m,\u001b[39m\n\ - \u001b[37mmisc\u001b[39m\u001b[93m:\u001b[39m \u001b[37mDS\u001b[39m\u001b[32m.\u001b[39m\u001b[37mattr\u001b[39m\u001b[90m(\u001b[39m\u001b[90m)\u001b[39m\n\ - \u001b[33m}\u001b[39m\u001b[90m)\u001b[39m\u001b[90m;\u001b[39m\n\ - \u001b[39m\n\ - \n\ - ' + EOL; - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-help-json-test.js b/tests/acceptance/help/help-help-json-test.js deleted file mode 100644 index 28f4d90a22..0000000000 --- a/tests/acceptance/help/help-help-json-test.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json help', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'help', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'help', - description: 'Outputs the usage instructions for all commands or the provided command', - aliases: [null, 'h', '--help', '-h'], - works: 'everywhere', - availableOptions: [ - { - name: 'verbose', - default: false, - aliases: ['v'], - key: 'verbose', - required: false - }, - { - name: 'json', - default: false, - key: 'json', - required: false - } - ], - anonymousOptions: [''] - }); - }); - }); - - it('works with alias h', function() { - return ember([ - 'help', - 'h', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('help'); - }); - }); - - it('works with alias --help', function() { - return ember([ - 'help', - '--help', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('help'); - }); - }); - - it('works with alias -h', function() { - return ember([ - 'help', - '-h', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('help'); - }); - }); -}); diff --git a/tests/acceptance/help/help-help-test.js b/tests/acceptance/help/help-help-test.js deleted file mode 100644 index 51119cfa40..0000000000 --- a/tests/acceptance/help/help-help-test.js +++ /dev/null @@ -1,88 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help help', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'help' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember help \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - Outputs the usage instructions for all commands or the provided command' + EOL + '\ - \u001b[90maliases: h, --help, -h\u001b[39m' + EOL + '\ - \u001b[36m--verbose\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -v\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias h', function() { - return ember([ - 'help', - 'h' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember help \u001b[33m\u001b[39m \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); - - it('works with alias --help', function() { - return ember([ - 'help', - '--help' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember help \u001b[33m\u001b[39m \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); - - it('works with alias -h', function() { - return ember([ - 'help', - '-h' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember help \u001b[33m\u001b[39m \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-init-json-test.js b/tests/acceptance/help/help-init-json-test.js deleted file mode 100644 index abc3474c23..0000000000 --- a/tests/acceptance/help/help-init-json-test.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json init', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'init', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'init', - description: 'Creates a new ember-cli project in the current folder.', - aliases: ['i'], - works: 'everywhere', - availableOptions: [ - { - name: 'dry-run', - default: false, - aliases: ['d'], - key: 'dryRun', - required: false - }, - { - name: 'verbose', - default: false, - aliases: ['v'], - key: 'verbose', - required: false - }, - { - name: 'blueprint', - aliases: ['b'], - key: 'blueprint', - required: false - }, - { - name: 'skip-npm', - default: false, - aliases: ['sn'], - key: 'skipNpm', - required: false - }, - { - name: 'skip-bower', - default: false, - aliases: ['sb'], - key: 'skipBower', - required: false - }, - { - name: 'name', - default: '', - aliases: ['n'], - key: 'name', - required: false - } - ], - anonymousOptions: [''] - }); - }); - }); - - it('works with alias i', function() { - return ember([ - 'help', - 'i', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('init'); - }); - }); -}); diff --git a/tests/acceptance/help/help-init-test.js b/tests/acceptance/help/help-init-test.js deleted file mode 100644 index 2378210621..0000000000 --- a/tests/acceptance/help/help-init-test.js +++ /dev/null @@ -1,70 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help init', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'init' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember init \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - Creates a new ember-cli project in the current folder.' + EOL + '\ - \u001b[90maliases: i\u001b[39m' + EOL + '\ - \u001b[36m--dry-run\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -d\u001b[39m' + EOL + '\ - \u001b[36m--verbose\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -v\u001b[39m' + EOL + '\ - \u001b[36m--blueprint\u001b[39m \u001b[36m(String)\u001b[39m' + EOL + '\ - \u001b[90maliases: -b \u001b[39m' + EOL + '\ - \u001b[36m--skip-npm\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sn\u001b[39m' + EOL + '\ - \u001b[36m--skip-bower\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sb\u001b[39m' + EOL + '\ - \u001b[36m--name\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: )\u001b[39m' + EOL + '\ - \u001b[90maliases: -n \u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias i', function() { - return ember([ - 'help', - 'i' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember init \u001b[33m\u001b[39m \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-install-json-test.js b/tests/acceptance/help/help-install-json-test.js deleted file mode 100644 index b18efb02bf..0000000000 --- a/tests/acceptance/help/help-install-json-test.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json install', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'install', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'install', - description: 'Installs an ember-cli addon from npm.', - aliases: [], - works: 'insideProject', - availableOptions: [], - anonymousOptions: [''] - }); - }); - }); -}); diff --git a/tests/acceptance/help/help-install-test.js b/tests/acceptance/help/help-install-test.js deleted file mode 100644 index 2badf94828..0000000000 --- a/tests/acceptance/help/help-install-test.js +++ /dev/null @@ -1,43 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help install', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'install' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember install \u001b[33m\u001b[39m' + EOL + '\ - Installs an ember-cli addon from npm.' + EOL); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-json-test-slow.js b/tests/acceptance/help/help-json-test-slow.js deleted file mode 100644 index 7880747970..0000000000 --- a/tests/acceptance/help/help-json-test-slow.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var runCommand = require('../../helpers/run-command'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var fs = require('fs-extra'); -var copy = Promise.denodeify(fs.copy); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json - slow', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('lists addons', function() { - var output = ''; - - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'generate', - 'in-repo-addon', - 'my-addon' - ]); - }) - .then(function() { - return copy('../../tests/fixtures/addon/commands/addon-command.js', 'lib/my-addon/index.js'); - }) - .then(function() { - // ember helper currently won't register the addon file - // return ember([ - // 'help', - // '--json' - // ]); - return runCommand(path.join(root, 'bin', 'ember'), - 'help', - '--json', { - onOutput: function(o) { - output += o; - } - }); - }) - .then(function() { - var json = convertToJson(output); - - expect(json.addons).to.deep.equal([ - { - name: 'help', - description: 'Outputs the usage instructions for all commands or the provided command', - aliases: [ null, 'h', '--help', '-h' ], - works: 'everywhere', - availableOptions: [ - { - name: 'verbose', - default: false, - aliases: [ 'v' ], - key: 'verbose', - required: false - }, - { - name: 'json', - default: false, - key: 'json', - required: false - } - ], - anonymousOptions: [ '' ], - commands: [ - { - name: 'addon-command', - description: null, - aliases: [ 'ac' ], - works: 'insideProject', - availableOptions: [], - anonymousOptions: [] - } - ] - } - ]); - }); - }); -}); diff --git a/tests/acceptance/help/help-json-test.js b/tests/acceptance/help/help-json-test.js deleted file mode 100644 index 0203e3372b..0000000000 --- a/tests/acceptance/help/help-json-test.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var commandNames = require('../../helpers/command-names'); -var Promise = require('../../../lib/ext/promise'); -var pluck = require('lodash/collection/pluck'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - expect(json.name).to.equal('ember'); - expect(json.description).to.equal(null); - expect(json.aliases).to.deep.equal([]); - expect(json.availableOptions).to.deep.equal([]); - expect(json.anonymousOptions).to.deep.equal(['']); - expect(pluck(json.commands, 'name')).to.deep.equal(commandNames); - expect(json.addons).to.deep.equal([]); - }); - }); - - it('works with alias h', function() { - return ember([ - 'h', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - expect(json.name).to.equal('ember'); - expect(json.description).to.equal(null); - expect(json.aliases).to.deep.equal([]); - expect(json.availableOptions).to.deep.equal([]); - expect(json.anonymousOptions).to.deep.equal(['']); - expect(pluck(json.commands, 'name')).to.deep.equal(commandNames); - expect(json.addons).to.deep.equal([]); - }); - }); - - it('works with alias --help', function() { - return ember([ - '--help', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - expect(json.name).to.equal('ember'); - expect(json.description).to.equal(null); - expect(json.aliases).to.deep.equal([]); - expect(json.availableOptions).to.deep.equal([]); - expect(json.anonymousOptions).to.deep.equal(['']); - expect(pluck(json.commands, 'name')).to.deep.equal(commandNames); - expect(json.addons).to.deep.equal([]); - }); - }); - - it('works with alias -h', function() { - return ember([ - '-h', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - expect(json.name).to.equal('ember'); - expect(json.description).to.equal(null); - expect(json.aliases).to.deep.equal([]); - expect(json.availableOptions).to.deep.equal([]); - expect(json.anonymousOptions).to.deep.equal(['']); - expect(pluck(json.commands, 'name')).to.deep.equal(commandNames); - expect(json.addons).to.deep.equal([]); - }); - }); - - it('handles missing command', function() { - return ember([ - 'help', - 'asdf', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - expect(json.commands.length).to.equal(0); - }); - }); -}); diff --git a/tests/acceptance/help/help-new-json-test.js b/tests/acceptance/help/help-new-json-test.js deleted file mode 100644 index 691e37490e..0000000000 --- a/tests/acceptance/help/help-new-json-test.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json new', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'new', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'new', - description: processHelpString('Creates a new directory and runs \u001b[32member init\u001b[39m in it.'), - aliases: [], - works: 'outsideProject', - availableOptions: [ - { - name: 'dry-run', - default: false, - aliases: ['d'], - key: 'dryRun', - required: false - }, - { - name: 'verbose', - default: false, - aliases: ['v'], - key: 'verbose', - required: false - }, - { - name: 'blueprint', - default: 'app', - aliases: ['b'], - key: 'blueprint', - required: false - }, - { - name: 'skip-npm', - default: false, - aliases: ['sn'], - key: 'skipNpm', - required: false - }, - { - name: 'skip-bower', - default: false, - aliases: ['sb'], - key: 'skipBower', - required: false - }, - { - name: 'skip-git', - default: false, - aliases: ['sg'], - key: 'skipGit', - required: false - }, - { - name: 'directory', - aliases: ['dir'], - key: 'directory', - required: false - } - ], - anonymousOptions: [''] - }); - }); - }); -}); diff --git a/tests/acceptance/help/help-new-test.js b/tests/acceptance/help/help-new-test.js deleted file mode 100644 index 241a8c0e18..0000000000 --- a/tests/acceptance/help/help-new-test.js +++ /dev/null @@ -1,57 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help new', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'new' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember new \u001b[33m\u001b[39m \u001b[36m\u001b[39m' + EOL + '\ - Creates a new directory and runs \u001b[32member init\u001b[39m in it.' + EOL + '\ - \u001b[36m--dry-run\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -d\u001b[39m' + EOL + '\ - \u001b[36m--verbose\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -v\u001b[39m' + EOL + '\ - \u001b[36m--blueprint\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: app)\u001b[39m' + EOL + '\ - \u001b[90maliases: -b \u001b[39m' + EOL + '\ - \u001b[36m--skip-npm\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sn\u001b[39m' + EOL + '\ - \u001b[36m--skip-bower\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sb\u001b[39m' + EOL + '\ - \u001b[36m--skip-git\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -sg\u001b[39m' + EOL + '\ - \u001b[36m--directory\u001b[39m \u001b[36m(String)\u001b[39m' + EOL + '\ - \u001b[90maliases: -dir \u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-serve-json-test.js b/tests/acceptance/help/help-serve-json-test.js deleted file mode 100644 index ce62b5a225..0000000000 --- a/tests/acceptance/help/help-serve-json-test.js +++ /dev/null @@ -1,167 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json serve', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'serve', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'serve', - description: 'Builds and serves your app, rebuilding on file changes.', - aliases: ['server', 's'], - works: 'insideProject', - availableOptions: [ - { - name: 'port', - default: 4200, - aliases: ['p'], - key: 'port', - required: false - }, - { - name: 'host', - default: '0.0.0.0', - aliases: ['H'], - key: 'host', - required: false - }, - { - name: 'proxy', - aliases: ['pr', 'pxy'], - key: 'proxy', - required: false - }, - { - name: 'insecure-proxy', - default: false, - description: 'Set false to proxy self-signed SSL certificates', - aliases: ['inspr'], - key: 'insecureProxy', - required: false - }, - { - name: 'watcher', - default: 'events', - aliases: ['w'], - key: 'watcher', - required: false - }, - { - name: 'live-reload', - default: true, - aliases: ['lr'], - key: 'liveReload', - required: false - }, - { - name: 'live-reload-host', - description: 'Defaults to host', - aliases: ['lrh'], - key: 'liveReloadHost', - required: false - }, - { - name: 'live-reload-port', - description: '(Defaults to port number within [49152...65535] )', - aliases: ['lrp'], - key: 'liveReloadPort', - required: false - }, - { - name: 'environment', - default: 'development', - aliases: [ - 'e', - { dev: 'development' }, - { prod: 'production' } - ], - key: 'environment', - required: false - }, - { - name: 'output-path', - type: 'path', - default: 'dist/', - aliases: ['op', 'out'], - key: 'outputPath', - required: false - }, - { - name: 'ssl', - default: false, - key: 'ssl', - required: false - }, - { - name: 'ssl-key', - default: 'ssl/server.key', - key: 'sslKey', - required: false - }, - { - name: 'ssl-cert', - default: 'ssl/server.crt', - key: 'sslCert', - required: false - } - ], - anonymousOptions: [] - }); - }); - }); - - it('works with alias server', function() { - return ember([ - 'help', - 'server', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('serve'); - }); - }); - - it('works with alias s', function() { - return ember([ - 'help', - 's', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('serve'); - }); - }); -}); diff --git a/tests/acceptance/help/help-serve-test.js b/tests/acceptance/help/help-serve-test.js deleted file mode 100644 index 8eb275b045..0000000000 --- a/tests/acceptance/help/help-serve-test.js +++ /dev/null @@ -1,95 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help serve', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'serve' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember serve \u001b[36m\u001b[39m' + EOL + '\ - Builds and serves your app, rebuilding on file changes.' + EOL + '\ - \u001b[90maliases: server, s\u001b[39m' + EOL + '\ - \u001b[36m--port\u001b[39m \u001b[36m(Number)\u001b[39m \u001b[36m(Default: 4200)\u001b[39m' + EOL + '\ - \u001b[90maliases: -p \u001b[39m' + EOL + '\ - \u001b[36m--host\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: 0.0.0.0)\u001b[39m' + EOL + '\ - \u001b[90maliases: -H \u001b[39m' + EOL + '\ - \u001b[36m--proxy\u001b[39m \u001b[36m(String)\u001b[39m' + EOL + '\ - \u001b[90maliases: -pr , -pxy \u001b[39m' + EOL + '\ - \u001b[36m--insecure-proxy\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m Set false to proxy self-signed SSL certificates' + EOL + '\ - \u001b[90maliases: -inspr\u001b[39m' + EOL + '\ - \u001b[36m--watcher\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: events)\u001b[39m' + EOL + '\ - \u001b[90maliases: -w \u001b[39m' + EOL + '\ - \u001b[36m--live-reload\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: true)\u001b[39m' + EOL + '\ - \u001b[90maliases: -lr\u001b[39m' + EOL + '\ - \u001b[36m--live-reload-host\u001b[39m \u001b[36m(String)\u001b[39m Defaults to host' + EOL + '\ - \u001b[90maliases: -lrh \u001b[39m' + EOL + '\ - \u001b[36m--live-reload-port\u001b[39m \u001b[36m(Number)\u001b[39m (Defaults to port number within [49152...65535] )' + EOL + '\ - \u001b[90maliases: -lrp \u001b[39m' + EOL + '\ - \u001b[36m--environment\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: development)\u001b[39m' + EOL + '\ - \u001b[90maliases: -e , -dev (--environment=development), -prod (--environment=production)\u001b[39m' + EOL + '\ - \u001b[36m--output-path\u001b[39m \u001b[36m(Path)\u001b[39m \u001b[36m(Default: dist/)\u001b[39m' + EOL + '\ - \u001b[90maliases: -op , -out \u001b[39m' + EOL + '\ - \u001b[36m--ssl\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[36m--ssl-key\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: ssl/server.key)\u001b[39m' + EOL + '\ - \u001b[36m--ssl-cert\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: ssl/server.crt)\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias server', function() { - return ember([ - 'help', - 'server' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember serve \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); - - it('works with alias s', function() { - return ember([ - 'help', - 's' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember serve \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-test-json-test.js b/tests/acceptance/help/help-test-json-test.js deleted file mode 100644 index b22b0cd09f..0000000000 --- a/tests/acceptance/help/help-test-json-test.js +++ /dev/null @@ -1,136 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json test', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'test', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'test', - description: 'Runs your app\'s test suite.', - aliases: ['t'], - works: 'insideProject', - availableOptions: [ - { - name: 'environment', - default: 'test', - aliases: ['e'], - key: 'environment', - required: false - }, - { - name: 'config-file', - default: './testem.json', - aliases: ['c', 'cf'], - key: 'configFile', - required: false - }, - { - name: 'server', - default: false, - aliases: ['s'], - key: 'server', - required: false - }, - { - name: 'host', - aliases: ['H'], - key: 'host', - required: false - }, - { - name: 'test-port', - default: 7357, - description: 'The test port to use when running with --server.', - aliases: ['tp'], - key: 'testPort', - required: false - }, - { - name: 'filter', - description: 'A string to filter tests to run', - aliases: ['f'], - key: 'filter', - required: false - }, - { - name: 'module', - description: 'The name of a test module to run', - aliases: ['m'], - key: 'module', - required: false - }, - { - name: 'watcher', - default: 'events', - aliases: ['w'], - key: 'watcher', - required: false - }, - { - name: 'launch', - default: false, - description: 'A comma separated list of browsers to launch for tests.', - key: 'launch', - required: false - }, - { - name: 'reporter', - description: 'Test reporter to use [tap|dot|xunit]', - aliases: ['r'], - key: 'reporter', - required: false - }, - { - name: 'test-page', - description: 'Test page to invoke', - key: 'testPage', - required: false - } - ], - anonymousOptions: [] - }); - }); - }); - - it('works with alias t', function() { - return ember([ - 'help', - 't', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('test'); - }); - }); -}); diff --git a/tests/acceptance/help/help-test-slow.js b/tests/acceptance/help/help-test-slow.js deleted file mode 100644 index 55810ca9ab..0000000000 --- a/tests/acceptance/help/help-test-slow.js +++ /dev/null @@ -1,71 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var runCommand = require('../../helpers/run-command'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var fs = require('fs-extra'); -var copy = Promise.denodeify(fs.copy); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help - slow', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('lists addons', function() { - var output = ''; - - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'generate', - 'in-repo-addon', - 'my-addon' - ]); - }) - .then(function() { - return copy('../../tests/fixtures/addon/commands/addon-command.js', 'lib/my-addon/index.js'); - }) - .then(function() { - // ember helper currently won't register the addon file - // return ember([ - // 'help' - // ]); - return runCommand(path.join(root, 'bin', 'ember'), - 'help', { - onOutput: function(o) { - output += o; - } - }); - }) - .then(function() { - var testString = processHelpString(EOL + '\ -Available commands from Ember CLI Addon Command Test:' + EOL + '\ -ember addon-command' + EOL + '\ - aliases: ac' + EOL); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-test-test.js b/tests/acceptance/help/help-test-test.js deleted file mode 100644 index 821eed277b..0000000000 --- a/tests/acceptance/help/help-test-test.js +++ /dev/null @@ -1,78 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help test', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'test' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember test \u001b[36m\u001b[39m' + EOL + '\ - Runs your app\'s test suite.' + EOL + '\ - \u001b[90maliases: t\u001b[39m' + EOL + '\ - \u001b[36m--environment\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: test)\u001b[39m' + EOL + '\ - \u001b[90maliases: -e \u001b[39m' + EOL + '\ - \u001b[36m--config-file\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: ./testem.json)\u001b[39m' + EOL + '\ - \u001b[90maliases: -c , -cf \u001b[39m' + EOL + '\ - \u001b[36m--server\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL + '\ - \u001b[90maliases: -s\u001b[39m' + EOL + '\ - \u001b[36m--host\u001b[39m \u001b[36m(String)\u001b[39m' + EOL + '\ - \u001b[90maliases: -H \u001b[39m' + EOL + '\ - \u001b[36m--test-port\u001b[39m \u001b[36m(Number)\u001b[39m \u001b[36m(Default: 7357)\u001b[39m The test port to use when running with --server.' + EOL + '\ - \u001b[90maliases: -tp \u001b[39m' + EOL + '\ - \u001b[36m--filter\u001b[39m \u001b[36m(String)\u001b[39m A string to filter tests to run' + EOL + '\ - \u001b[90maliases: -f \u001b[39m' + EOL + '\ - \u001b[36m--module\u001b[39m \u001b[36m(String)\u001b[39m The name of a test module to run' + EOL + '\ - \u001b[90maliases: -m \u001b[39m' + EOL + '\ - \u001b[36m--watcher\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: events)\u001b[39m' + EOL + '\ - \u001b[90maliases: -w \u001b[39m' + EOL + '\ - \u001b[36m--launch\u001b[39m \u001b[36m(String)\u001b[39m \u001b[36m(Default: false)\u001b[39m A comma separated list of browsers to launch for tests.' + EOL + '\ - \u001b[36m--reporter\u001b[39m \u001b[36m(String)\u001b[39m Test reporter to use [tap|dot|xunit]' + EOL + '\ - \u001b[90maliases: -r \u001b[39m' + EOL + '\ - \u001b[36m--test-page\u001b[39m \u001b[36m(String)\u001b[39m Test page to invoke' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias t', function() { - return ember([ - 'help', - 't' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember test \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-test.js b/tests/acceptance/help/help-test.js deleted file mode 100644 index b40a3cecb1..0000000000 --- a/tests/acceptance/help/help-test.js +++ /dev/null @@ -1,142 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var commandNames = require('../../helpers/command-names'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -Usage: ember \u001b[33m\u001b[39m' + EOL + '\ -' + EOL + '\ -Available commands in ember-cli:' + EOL + EOL); - var regex = new RegExp(commandNames.map(function(commandName) { - return EOL + EOL + 'ember ' + commandName + ' [\\s\\S]*'; - }).join('') + EOL + EOL); - - expect(output).to.include(testString); - expect(output).to.match(regex); - }); - }); - - it('works with alias h', function() { - return ember([ - 'h' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -Usage: ember \u001b[33m\u001b[39m' + EOL + '\ -' + EOL + '\ -Available commands in ember-cli:' + EOL + EOL); - var regex = new RegExp(commandNames.map(function(commandName) { - return EOL + EOL + 'ember ' + commandName + ' [\\s\\S]*'; - }).join('') + EOL + EOL); - - expect(output).to.include(testString); - expect(output).to.match(regex); - }); - }); - - it('works with alias --help', function() { - return ember([ - '--help' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -Usage: ember \u001b[33m\u001b[39m' + EOL + '\ -' + EOL + '\ -Available commands in ember-cli:' + EOL + EOL); - var regex = new RegExp(commandNames.map(function(commandName) { - return EOL + EOL + 'ember ' + commandName + ' [\\s\\S]*'; - }).join('') + EOL + EOL); - - expect(output).to.include(testString); - expect(output).to.match(regex); - }); - }); - - it('works with alias -h', function() { - return ember([ - '-h' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -Usage: ember \u001b[33m\u001b[39m' + EOL + '\ -' + EOL + '\ -Available commands in ember-cli:' + EOL + EOL); - var regex = new RegExp(commandNames.map(function(commandName) { - return EOL + EOL + 'ember ' + commandName + ' [\\s\\S]*'; - }).join('') + EOL + EOL); - - expect(output).to.include(testString); - expect(output).to.match(regex); - }); - }); - - it('works with single command', function() { - return ember([ - 'help', - 'addon' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -Requested ember-cli commands:' + EOL + '\ -' + EOL + '\ -ember addon '); - - expect(output).to.include(testString); - }); - }); - - it('handles missing command', function() { - return ember([ - 'help', - 'asdf' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -Requested ember-cli commands:' + EOL + '\ -' + EOL + '\ -\u001b[31mNo help entry for \'asdf\'\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/help/help-version-json-test.js b/tests/acceptance/help/help-version-json-test.js deleted file mode 100644 index ce08b33355..0000000000 --- a/tests/acceptance/help/help-version-json-test.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var ember = require('../../helpers/ember'); -var convertToJson = require('../../helpers/convert-help-output-to-json'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help --json version', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'version', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command).to.deep.equal({ - name: 'version', - description: 'outputs ember-cli version', - aliases: ['v', '--version', '-v'], - works: 'everywhere', - availableOptions: [ - { - name: 'verbose', - default: false, - key: 'verbose', - required: false - } - ], - anonymousOptions: [] - }); - }); - }); - - it('works with alias v', function() { - return ember([ - 'help', - 'v', - '--json' - ]) - .then(function(result) { - var json = convertToJson(result.ui.output); - - var command = json.commands[0]; - expect(command.name).to.equal('version'); - }); - }); -}); diff --git a/tests/acceptance/help/help-version-test.js b/tests/acceptance/help/help-version-test.js deleted file mode 100644 index 9f52f96d1d..0000000000 --- a/tests/acceptance/help/help-version-test.js +++ /dev/null @@ -1,59 +0,0 @@ -/*jshint multistr: true */ - -'use strict'; - -var path = require('path'); -var tmp = require('tmp-sync'); -var expect = require('chai').expect; -var EOL = require('os').EOL; -var ember = require('../../helpers/ember'); -var processHelpString = require('../../helpers/process-help-string'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmpdir; - -describe('Acceptance: ember help version', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - it('works', function() { - return ember([ - 'help', - 'version' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString(EOL + '\ -ember version \u001b[36m\u001b[39m' + EOL + '\ - outputs ember-cli version' + EOL + '\ - \u001b[90maliases: v, --version, -v\u001b[39m' + EOL + '\ - \u001b[36m--verbose\u001b[39m \u001b[36m(Boolean)\u001b[39m \u001b[36m(Default: false)\u001b[39m' + EOL); - - expect(output).to.include(testString); - }); - }); - - it('works with alias v', function() { - return ember([ - 'help', - 'v' - ]) - .then(function(result) { - var output = result.ui.output; - - var testString = processHelpString('ember version \u001b[36m\u001b[39m'); - - expect(output).to.include(testString); - }); - }); -}); diff --git a/tests/acceptance/in-option-destroy-test.js b/tests/acceptance/in-option-destroy-test.js new file mode 100644 index 0000000000..26315ae1db --- /dev/null +++ b/tests/acceptance/in-option-destroy-test.js @@ -0,0 +1,103 @@ +'use strict'; + +const ember = require('../helpers/ember'); +let root = process.cwd(); +const tmp = require('tmp-promise'); +const initApp = require('../helpers/init-app'); +const generateUtils = require('../helpers/generate-utils'); + +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); + +describe('Acceptance: ember destroy with --in option', function () { + this.timeout(20000); + + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); + }); + + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); + }); + + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); + }); + + afterEach(function () { + this.timeout(10000); + + process.chdir(root); + }); + + function generate(args) { + let generateArgs = ['generate'].concat(args); + return ember(generateArgs); + } + + function destroy(args) { + let destroyArgs = ['destroy'].concat(args); + return ember(destroyArgs); + } + + function assertFilesExist(files) { + files.forEach(function (f) { + expect(file(f)).to.exist; + }); + } + + function assertFilesNotExist(files) { + files.forEach(function (f) { + expect(file(f)).to.not.exist; + }); + } + + const assertDestroyAfterGenerate = async function (args, addonPath, files) { + await initApp(); + await generateUtils.inRepoAddon(addonPath); + await generateUtils.tempBlueprint(); + await generate(args); + + assertFilesExist(files); + + let result = await destroy(args); + expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); + assertFilesNotExist(files); + }; + + it('blueprint foo --in lib/other-thing', function () { + let addonPath = './lib/other-thing'; + let commandArgs = ['foo', 'bar', '--in', addonPath]; + let files = ['lib/other-thing/addon/foos/bar.js']; + + return assertDestroyAfterGenerate(commandArgs, addonPath, files); + }); + + it('blueprint foo --in ./non-lib/other-thing', function () { + let addonPath = './non-lib/other-thing'; + let commandArgs = ['foo', 'bar', '--in', addonPath]; + let files = ['non-lib/other-thing/addon/foos/bar.js']; + + return assertDestroyAfterGenerate(commandArgs, addonPath, files); + }); + + it('blueprint foo --in non-lib/other-thing', function () { + let addonPath = 'non-lib/other-thing'; + let commandArgs = ['foo', 'bar', '--in', addonPath]; + let files = ['non-lib/other-thing/addon/foos/bar.js']; + + return assertDestroyAfterGenerate(commandArgs, addonPath, files); + }); + + it('blueprint foo --in non-lib/nested/other-thing', function () { + let addonPath = 'non-lib/nested/other-thing'; + let commandArgs = ['foo', 'bar', '--in', addonPath]; + let files = ['non-lib/nested/other-thing/addon/foos/bar.js']; + + return assertDestroyAfterGenerate(commandArgs, addonPath, files); + }); +}); diff --git a/tests/acceptance/in-option-generate-test.js b/tests/acceptance/in-option-generate-test.js new file mode 100644 index 0000000000..f56b3c03ef --- /dev/null +++ b/tests/acceptance/in-option-generate-test.js @@ -0,0 +1,98 @@ +'use strict'; + +const ember = require('../helpers/ember'); +const fs = require('fs-extra'); +const path = require('path'); +let root = process.cwd(); +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); +const tmp = require('tmp-promise'); +const initApp = require('../helpers/init-app'); +const generateUtils = require('../helpers/generate-utils'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); + +describe('Acceptance: ember generate with --in option', function () { + this.timeout(20000); + + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); + }); + + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); + }); + + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); + }); + + afterEach(function () { + process.chdir(root); + }); + + function removeAddonPath() { + let packageJsonPath = path.join(process.cwd(), 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + + delete packageJson['ember-addon'].paths; + + return fs.writeJsonSync(packageJsonPath, packageJson); + } + + it('generate blueprint foo using lib', async function () { + // build an app with an in-repo addon in a non-standard path + await initApp(); + await generateUtils.inRepoAddon('lib/other-thing'); + await generateUtils.tempBlueprint(); + await ember(['generate', 'foo', 'bar', '--in=lib/other-thing']); + + expect(file('lib/other-thing/addon/foos/bar.js')).to.exist; + }); + + it('generate blueprint foo using custom path using current directory', async function () { + // build an app with an in-repo addon in a non-standard path + await initApp(); + await generateUtils.inRepoAddon('./non-lib/other-thing'); + await generateUtils.tempBlueprint(); + await ember(['generate', 'foo', 'bar', '--in=non-lib/other-thing']); + + expect(file('non-lib/other-thing/addon/foos/bar.js')).to.exist; + }); + + it('generate blueprint foo using custom path', async function () { + // build an app with an in-repo addon in a non-standard path + await initApp(); + await generateUtils.inRepoAddon('./non-lib/other-thing'); + // generate in project blueprint to allow easier testing of in-repo generation + await generateUtils.tempBlueprint(); + await ember(['generate', 'foo', 'bar', '--in=./non-lib/other-thing']); + + // confirm that we can generate into the non-lib path + expect(file('non-lib/other-thing/addon/foos/bar.js')).to.exist; + }); + + it('generate blueprint foo using custom nested path', async function () { + // build an app with an in-repo addon in a non-standard path + await initApp(); + await generateUtils.inRepoAddon('./non-lib/nested/other-thing'); + await generateUtils.tempBlueprint(); + await ember(['generate', 'foo', 'bar', '--in=./non-lib/nested/other-thing']); + + expect(file('non-lib/nested/other-thing/addon/foos/bar.js')).to.exist; + }); + + it('generate blueprint foo using sibling path', async function () { + // build an app with an in-repo addon in a non-standard path + await initApp(); + await fs.mkdirp('../sibling'); + await generateUtils.inRepoAddon('../sibling'); + await removeAddonPath(); + await generateUtils.tempBlueprint(); + await ember(['generate', 'foo', 'bar', '--in=../sibling']); + + expect(file('../sibling/addon/foos/bar.js')).to.exist; + }); +}); diff --git a/tests/acceptance/in-repo-addon-destroy-test.js b/tests/acceptance/in-repo-addon-destroy-test.js deleted file mode 100644 index 82eb3961fc..0000000000 --- a/tests/acceptance/in-repo-addon-destroy-test.js +++ /dev/null @@ -1,455 +0,0 @@ -/*jshint quotmark: false*/ - -'use strict'; - -var Promise = require('../../lib/ext/promise'); -var expect = require('chai').expect; -var assertFile = require('../helpers/assert-file'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var existsSync = require('exists-sync'); -var fs = require('fs-extra'); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); - -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); - -describe('Acceptance: ember destroy in-repo-addon', function() { - this.timeout(20000); - var tmpdir; - - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); - }); - - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); - }); - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - function initApp() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]); - } - - function initInRepoAddon() { - return initApp().then(function() { - return ember([ - 'generate', - 'in-repo-addon', - 'my-addon' - ]); - }); - } - - function generateInRepoAddon(args) { - var generateArgs = ['generate'].concat(args); - - return initInRepoAddon().then(function() { - return ember(generateArgs); - }); - } - - function destroy(args) { - var destroyArgs = ['destroy'].concat(args); - return ember(destroyArgs); - } - - function assertFileNotExists(file) { - var filePath = path.join(process.cwd(), file); - expect(!existsSync(filePath), 'expected ' + file + ' not to exist'); - } - - function assertFilesExist(files) { - files.forEach(assertFile); - } - - function assertFilesNotExist(files) { - files.forEach(assertFileNotExists); - } - - function assertDestroyAfterGenerateInRepoAddon(args, files) { - return generateInRepoAddon(args) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - it('in-repo-addon controller foo', function() { - var commandArgs = ['controller', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/controllers/foo.js', - 'lib/my-addon/app/controllers/foo.js', - 'tests/unit/controllers/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon controller foo/bar', function() { - var commandArgs = ['controller', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/controllers/foo/bar.js', - 'lib/my-addon/app/controllers/foo/bar.js', - 'tests/unit/controllers/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon component x-foo', function() { - var commandArgs = ['component', 'x-foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/components/x-foo.js', - 'lib/my-addon/addon/templates/components/x-foo.hbs', - 'lib/my-addon/app/components/x-foo.js', - 'tests/integration/components/x-foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon component nested/x-foo', function() { - var commandArgs = ['component', 'nested/x-foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/components/nested/x-foo.js', - 'lib/my-addon/addon/templates/components/nested/x-foo.hbs', - 'lib/my-addon/app/components/nested/x-foo.js', - 'tests/integration/components/nested/x-foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon helper foo-bar', function() { - var commandArgs = ['helper', 'foo-bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/helpers/foo-bar.js', - 'lib/my-addon/app/helpers/foo-bar.js', - 'tests/unit/helpers/foo-bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon helper foo/bar-baz', function() { - var commandArgs = ['helper', 'foo/bar-baz', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/helpers/foo/bar-baz.js', - 'lib/my-addon/app/helpers/foo/bar-baz.js', - 'tests/unit/helpers/foo/bar-baz-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon model foo', function() { - var commandArgs = ['model', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/models/foo.js', - 'lib/my-addon/app/models/foo.js', - 'tests/unit/models/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon model foo/bar', function() { - var commandArgs = ['model', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/models/foo/bar.js', - 'lib/my-addon/app/models/foo/bar.js', - 'tests/unit/models/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); -/* - it('in-repo-addon route foo', function() { - var commandArgs = ['route', 'foo']; - var files = [ - 'lib/my-addon/app/routes/foo.js', - 'lib/my-addon/app/templates/foo.hbs', - 'tests/unit/routes/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files) - .then(function() { - assertFile('lib/my-addon/app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); - }); - - it('in-repo-addon route index', function() { - var commandArgs = ['route', 'index']; - var files = [ - 'lib/my-addon/app/routes/index.js', - 'lib/my-addon/app/templates/index.hbs', - 'tests/unit/routes/index-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon route basic', function() { - var commandArgs = ['route', 'basic']; - var files = [ - 'lib/my-addon/app/routes/basic.js', - 'lib/my-addon/app/templates/basic.hbs', - 'tests/unit/routes/basic-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon resource foo', function() { - var commandArgs = ['resource', 'foo']; - var files = [ - 'lib/my-addon/app/models/foo.js', - 'tests/unit/models/foo-test.js', - 'lib/my-addon/app/routes/foo.js', - 'tests/unit/routes/foo-test.js', - 'lib/my-addon/app/templates/foo.hbs' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files) - .then(function() { - assertFile('lib/my-addon/app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); - }); - - it('in-repo-addon resource foos', function() { - var commandArgs = ['resource', 'foos']; - var files = [ - 'lib/my-addon/app/models/foo.js', - 'tests/unit/models/foo-test.js', - 'lib/my-addon/app/routes/foos.js', - 'tests/unit/routes/foos-test.js', - 'lib/my-addon/app/templates/foos.hbs' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files) - .then(function() { - assertFile('lib/my-addon/app/router.js', { - doesNotContain: "this.route('foos');" - }); - }); - }); -*/ - it('in-repo-addon template foo', function() { - var commandArgs = ['template', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/templates/foo.hbs', - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon template foo/bar', function() { - var commandArgs = ['template', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/templates/foo/bar.hbs', - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon view foo', function() { - var commandArgs = ['view', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/views/foo.js', - 'lib/my-addon/app/views/foo.js', - 'tests/unit/views/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon view foo/bar', function() { - var commandArgs = ['view', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/views/foo/bar.js', - 'lib/my-addon/app/views/foo/bar.js', - 'tests/unit/views/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon initializer foo', function() { - var commandArgs = ['initializer', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/initializers/foo.js', - 'lib/my-addon/app/initializers/foo.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon initializer foo/bar', function() { - var commandArgs = ['initializer', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/initializers/foo/bar.js', - 'lib/my-addon/app/initializers/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon mixin foo', function() { - var commandArgs = ['mixin', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/mixins/foo.js', - 'tests/unit/mixins/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon mixin foo/bar', function() { - var commandArgs = ['mixin', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/mixins/foo/bar.js', - 'tests/unit/mixins/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon adapter foo', function() { - var commandArgs = ['adapter', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/adapters/foo.js', - 'lib/my-addon/app/adapters/foo.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon adapter foo/bar', function() { - var commandArgs = ['adapter', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/adapters/foo/bar.js', - 'lib/my-addon/app/adapters/foo/bar.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon serializer foo', function() { - var commandArgs = ['serializer', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/serializers/foo.js', - 'lib/my-addon/app/serializers/foo.js', - 'tests/unit/serializers/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon serializer foo/bar', function() { - var commandArgs = ['serializer', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/serializers/foo/bar.js', - 'lib/my-addon/app/serializers/foo/bar.js', - 'tests/unit/serializers/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon transform foo', function() { - var commandArgs = ['transform', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/transforms/foo.js', - 'lib/my-addon/app/transforms/foo.js', - 'tests/unit/transforms/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon transform foo/bar', function() { - var commandArgs = ['transform', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/transforms/foo/bar.js', - 'lib/my-addon/app/transforms/foo/bar.js', - 'tests/unit/transforms/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon util foo-bar', function() { - var commandArgs = ['util', 'foo-bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/utils/foo-bar.js', - 'lib/my-addon/app/utils/foo-bar.js', - 'tests/unit/utils/foo-bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon util foo-bar/baz', function() { - var commandArgs = ['util', 'foo/bar-baz', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/utils/foo/bar-baz.js', - 'lib/my-addon/app/utils/foo/bar-baz.js', - 'tests/unit/utils/foo/bar-baz-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon service foo', function() { - var commandArgs = ['service', 'foo', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/services/foo.js', - 'lib/my-addon/app/services/foo.js', - 'tests/unit/services/foo-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon service foo/bar', function() { - var commandArgs = ['service', 'foo/bar', '--in-repo-addon=my-addon']; - var files = [ - 'lib/my-addon/addon/services/foo/bar.js', - 'lib/my-addon/app/services/foo/bar.js', - 'tests/unit/services/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - -}); diff --git a/tests/acceptance/in-repo-addon-generate-test.js b/tests/acceptance/in-repo-addon-generate-test.js index 3861f681df..767d23eaf8 100644 --- a/tests/acceptance/in-repo-addon-generate-test.js +++ b/tests/acceptance/in-repo-addon-generate-test.js @@ -1,824 +1,59 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../../lib/ext/promise'); -var assertFile = require('../helpers/assert-file'); -var assertFileEquals = require('../helpers/assert-file-equals'); -var assertFileToNotExist = require('../helpers/assert-file-to-not-exist'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var fs = require('fs-extra'); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var EOL = require('os').EOL; -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); -var expect = require('chai').expect; +const ember = require('../helpers/ember'); +const { outputFile } = require('fs-extra'); +let root = process.cwd(); +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); +const tmp = require('tmp-promise'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); -describe('Acceptance: ember generate in-repo-addon', function() { +describe('Acceptance: ember generate in-repo-addon', function () { this.timeout(20000); - var tmpdir; - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); }); - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); }); - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); }); - afterEach(function() { + afterEach(function () { process.chdir(root); - return remove(tmproot); }); function initApp() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]); + return ember(['init', '--name=my-app', '--skip-npm']); } - function initInRepoAddon() { - return initApp().then(function() { - return ember([ - 'generate', - 'in-repo-addon', - 'my-addon' - ]); - }); - } - - function generateInRepoAddon(args) { - var generateArgs = ['generate'].concat(args); - - return initInRepoAddon().then(function() { - return ember(generateArgs); - }); + async function initInRepoAddon() { + await initApp(); + return ember(['generate', 'in-repo-addon', 'my-addon']); } - it('in-repo-addon controller foo', function() { - return generateInRepoAddon(['controller', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/controllers/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('lib/my-addon/app/controllers/foo.js', { - contains: [ - "export { default } from 'my-addon/controllers/foo';" - ] - }); - assertFile('tests/unit/controllers/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo'" - ] - }); - }); - }); - - it('in-repo-addon controller foo/bar', function() { - return generateInRepoAddon(['controller', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/controllers/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('lib/my-addon/app/controllers/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/controllers/foo/bar';" - ] - }); - assertFile('tests/unit/controllers/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo/bar'" - ] - }); - }); - }); - - it('in-repo-addon component x-foo', function() { - return generateInRepoAddon(['component', 'x-foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/components/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "import layout from '../templates/components/x-foo';", - "export default Ember.Component.extend({", - "layout: layout", - "});" - ] - }); - assertFile('lib/my-addon/addon/templates/components/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFile('lib/my-addon/app/components/x-foo.js', { - contains: [ - "export { default } from 'my-addon/components/x-foo';" - ] - }); - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); - - it('in-repo-addon component-test x-foo', function() { - return generateInRepoAddon(['component-test', 'x-foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('tests/integration/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); - - it('in-repo-addon component-test x-foo --unit', function() { - return generateInRepoAddon(['component-test', 'x-foo', '--in-repo-addon=my-addon', '--unit']).then(function() { - assertFile('tests/unit/components/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "moduleForComponent('x-foo'", - "unit: true" - ] - }); - }); - }); - - it('in-repo-addon component nested/x-foo', function() { - return generateInRepoAddon(['component', 'nested/x-foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/components/nested/x-foo.js', { - contains: [ - "import Ember from 'ember';", - "import layout from '../../templates/components/nested/x-foo';", - "export default Ember.Component.extend({", - "layout: layout", - "});" - ] - }); - assertFile('lib/my-addon/addon/templates/components/nested/x-foo.hbs', { - contains: "{{yield}}" - }); - assertFile('lib/my-addon/app/components/nested/x-foo.js', { - contains: [ - "export { default } from 'my-addon/components/nested/x-foo';" - ] - }); - assertFile('tests/integration/components/nested/x-foo-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('nested/x-foo'", - "integration: true" - ] - }); - }); - }); - - it('in-repo-addon helper foo-bar', function() { - return generateInRepoAddon(['helper', 'foo-bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/helpers/foo-bar.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBar(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBar);" - }); - assertFile('lib/my-addon/app/helpers/foo-bar.js', { - contains: [ - "export { default, fooBar } from 'my-addon/helpers/foo-bar';" - ] - }); - assertFile('tests/unit/helpers/foo-bar-test.js', { - contains: "import { fooBar } from '../../../helpers/foo-bar';" - }); - }); - }); - - it('in-repo-addon helper foo/bar-baz', function() { - return generateInRepoAddon(['helper', 'foo/bar-baz', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/helpers/foo/bar-baz.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBarBaz(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBarBaz);" - }); - assertFile('lib/my-addon/app/helpers/foo/bar-baz.js', { - contains: [ - "export { default, fooBarBaz } from 'my-addon/helpers/foo/bar-baz';" - ] - }); - assertFile('tests/unit/helpers/foo/bar-baz-test.js', { - contains: "import { fooBarBaz } from '../../../../helpers/foo/bar-baz';" - }); - }); - }); - - it('in-repo-addon model foo', function() { - return generateInRepoAddon(['model', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/models/foo.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('lib/my-addon/app/models/foo.js', { - contains: [ - "export { default } from 'my-addon/models/foo';" - ] - }); - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - }); - }); - - it('in-repo-addon model foo with attributes', function() { - return generateInRepoAddon([ - 'model', - 'foo', - 'noType', - 'firstName:string', - 'created_at:date', - 'is-published:boolean', - 'rating:number', - 'bars:has-many', - 'baz:belongs-to', - 'echo:hasMany', - 'bravo:belongs_to', - 'foo-names:has-many', - 'barName:has-many', - 'bazName:belongs-to', - 'test-name:belongs-to', - 'echoName:hasMany', - 'bravoName:belongs_to', - '--in-repo-addon=my-addon' - ]).then(function() { - assertFile('lib/my-addon/addon/models/foo.js', { - contains: [ - "noType: DS.attr()", - "firstName: DS.attr('string')", - "createdAt: DS.attr('date')", - "isPublished: DS.attr('boolean')", - "rating: DS.attr('number')", - "bars: DS.hasMany('bar')", - "baz: DS.belongsTo('baz')", - "echos: DS.hasMany('echo')", - "bravo: DS.belongsTo('bravo')", - "fooNames: DS.hasMany('foo-name')", - "barNames: DS.hasMany('bar-name')", - "bazName: DS.belongsTo('baz-name')", - "testName: DS.belongsTo('test-name')", - "echoNames: DS.hasMany('echo-name')", - "bravoName: DS.belongsTo('bravo-name')" - ] - }); - assertFile('lib/my-addon/app/models/foo.js', { - contains: [ - "export { default } from 'my-addon/models/foo';" - ] - }); - assertFile('tests/unit/models/foo-test.js', { - contains: [ - "needs: [", - "'model:bar',", - "'model:baz',", - "'model:echo',", - "'model:bravo',", - "'model:foo-name',", - "'model:bar-name',", - "'model:baz-name',", - "'model:echo-name',", - "'model:test-name',", - "'model:bravo-name'", - "]" - ] - }); - }); - }); - - it('in-repo-addon model foo/bar', function() { - return generateInRepoAddon(['model', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/models/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('lib/my-addon/app/models/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/models/foo/bar';" - ] - }); - assertFile('tests/unit/models/foo/bar-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); - - it('in-repo-addon route foo', function() { - return generateInRepoAddon(['route', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/routes/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('lib/my-addon/app/routes/foo.js', { - contains: "export { default } from 'my-addon/routes/foo';" - }); - assertFile('lib/my-addon/addon/templates/foo.hbs', { - contains: '{{outlet}}' - }); - assertFile('lib/my-addon/app/templates/foo.js', { - contains: "export { default } from 'my-addon/templates/foo';" - }); - assertFile('tests/unit/routes/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - }); - }); - - it('in-repo-addon route foo/bar', function() { - return generateInRepoAddon(['route', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/routes/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('lib/my-addon/app/routes/foo/bar.js', { - contains: "export { default } from 'my-addon/routes/foo/bar';" - }); - assertFile('lib/my-addon/addon/templates/foo/bar.hbs', { - contains: '{{outlet}}' - }); - assertFile('lib/my-addon/app/templates/foo/bar.js', { - contains: "export { default } from 'my-addon/templates/foo/bar';" - }); - assertFile('tests/unit/routes/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo/bar'" - ] - }); - }); - }); - - it('in-repo-addon template foo', function() { - return generateInRepoAddon(['template', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/templates/foo.hbs'); - }); - }); - - it('in-repo-addon template foo/bar', function() { - return generateInRepoAddon(['template', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/templates/foo/bar.hbs'); - }); - }); - - it('in-repo-addon view foo', function() { - return generateInRepoAddon(['view', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/views/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('lib/my-addon/app/views/foo.js', { - contains: [ - "export { default } from 'my-addon/views/foo';" - ] - }); - assertFile('tests/unit/views/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo'" - ] - }); - }); - }); - - it('in-repo-addon view foo/bar', function() { - return generateInRepoAddon(['view', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/views/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('lib/my-addon/app/views/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/views/foo/bar';" - ] - }); - assertFile('tests/unit/views/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo/bar'" - ] - }); - }); - }); - - it('in-repo-addon resource foos', function() { - return generateInRepoAddon(['resource', 'foos', '--in-repo-addon=my-addon']).catch(function(error) { - expect(error.message).to.include('blueprint does not support ' + - 'generating inside addons.'); - }); - }); - - it('in-repo-addon initializer foo', function() { - return generateInRepoAddon(['initializer', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/initializers/foo.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - assertFile('lib/my-addon/app/initializers/foo.js', { - contains: [ - "export { default, initialize } from 'my-addon/initializers/foo';" - ] - }); - assertFile('tests/unit/initializers/foo-test.js'); - }); - }); - - it('in-repo-addon initializer foo/bar', function() { - return generateInRepoAddon(['initializer', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/initializers/foo/bar.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo/bar'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - assertFile('lib/my-addon/app/initializers/foo/bar.js', { - contains: [ - "export { default, initialize } from 'my-addon/initializers/foo/bar';" - ] - }); - assertFile('tests/unit/initializers/foo/bar-test.js'); - }); - }); - - it('in-repo-addon mixin foo', function() { - return generateInRepoAddon(['mixin', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/mixins/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo-test.js', { - contains: [ - "import FooMixin from '../../../mixins/foo';" - ] - }); - }); - }); - - it('in-repo-addon mixin foo/bar', function() { - return generateInRepoAddon(['mixin', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/mixins/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo/bar-test.js', { - contains: [ - "import FooBarMixin from '../../../mixins/foo/bar';" - ] - }); - }); - }); - - it('in-repo-addon mixin foo/bar/baz', function() { - return generateInRepoAddon(['mixin', 'foo/bar/baz', '--in-repo-addon=my-addon']).then(function() { - assertFile('tests/unit/mixins/foo/bar/baz-test.js', { - contains: [ - "import FooBarBazMixin from '../../../mixins/foo/bar/baz';" - ] - }); - }); - }); - - it('in-repo-addon adapter application', function() { - return generateInRepoAddon(['adapter', 'application', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/adapters/application.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFile('lib/my-addon/app/adapters/application.js', { - contains: [ - "export { default } from 'my-addon/adapters/application';" - ] - }); - assertFile('tests/unit/adapters/application-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:application'" - ] - }); - }); - }); - - it('in-repo-addon adapter foo', function() { - return generateInRepoAddon(['adapter', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/adapters/foo.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFile('lib/my-addon/app/adapters/foo.js', { - contains: [ - "export { default } from 'my-addon/adapters/foo';" - ] - }); - assertFile('tests/unit/adapters/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo'" - ] - }); - }); - }); - - it('in-repo-addon adapter foo/bar (with base class foo)', function() { - return generateInRepoAddon(['adapter', 'foo/bar', '--in-repo-addon=my-addon', '--base-class=foo']).then(function() { - assertFile('lib/my-addon/addon/adapters/foo/bar.js', { - contains: [ - "import FooAdapter from \'../foo\';", - "export default FooAdapter.extend({" + EOL + "});" - ] - }); - assertFile('lib/my-addon/app/adapters/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/adapters/foo/bar';" - ] - }); - assertFile('tests/unit/adapters/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo/bar'" - ] - }); - }); - }); - - - it('in-repo-addon serializer foo', function() { - return generateInRepoAddon(['serializer', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/serializers/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('lib/my-addon/app/serializers/foo.js', { - contains: [ - "export { default } from 'my-addon/serializers/foo';" - ] - }); - assertFile('tests/unit/serializers/foo-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - ] - }); - }); - }); - - it('in-repo-addon serializer foo/bar', function() { - return generateInRepoAddon(['serializer', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/serializers/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('lib/my-addon/app/serializers/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/serializers/foo/bar';" - ] - }); - assertFile('tests/unit/serializers/foo/bar-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); - - it('in-repo-addon transform foo', function() { - return generateInRepoAddon(['transform', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/transforms/foo.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('lib/my-addon/app/transforms/foo.js', { - contains: [ - "export { default } from 'my-addon/transforms/foo';" - ] - }); - assertFile('tests/unit/transforms/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo'" - ] - }); - }); - }); - - it('in-repo-addon transform foo/bar', function() { - return generateInRepoAddon(['transform', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/transforms/foo/bar.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - '' + EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('lib/my-addon/app/transforms/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/transforms/foo/bar';" - ] - }); - assertFile('tests/unit/transforms/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo/bar'" - ] - }); - }); - }); + it('in-repo-addon blueprint foo inside alternate path', async function () { + // build an app with an in-repo addon in a non-standard path + await initApp(); + await ember(['generate', 'in-repo-addon', './non-lib/other-thing']); + // generate in project blueprint to allow easier testing of in-repo generation + await outputFile('blueprints/foo/files/__root__/foos/__name__.js', '/* whoah, empty foo! */'); + // confirm that we can generate into the non-lib path + await ember(['generate', 'foo', 'bar', '--in-repo-addon=other-thing']); - it('in-repo-addon util foo-bar', function() { - return generateInRepoAddon(['util', 'foo-bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/utils/foo-bar.js', { - contains: 'export default function fooBar() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('lib/my-addon/app/utils/foo-bar.js', { - contains: [ - "export { default } from 'my-addon/utils/foo-bar';" - ] - }); - assertFile('tests/unit/utils/foo-bar-test.js', { - contains: [ - "import fooBar from '../../../utils/foo-bar';" - ] - }); - }); + expect(file('non-lib/other-thing/addon/foos/bar.js')).to.exist; }); - it('in-repo-addon util foo-bar/baz', function() { - return generateInRepoAddon(['util', 'foo/bar-baz', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/utils/foo/bar-baz.js', { - contains: 'export default function fooBarBaz() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('lib/my-addon/app/utils/foo/bar-baz.js', { - contains: [ - "export { default } from 'my-addon/utils/foo/bar-baz';" - ] - }); - assertFile('tests/unit/utils/foo/bar-baz-test.js', { - contains: [ - "import fooBarBaz from '../../../utils/foo/bar-baz';" - ] - }); - }); - }); + it('in-repo-addon adds path to lib', async function () { + await initInRepoAddon(); - it('in-repo-addon service foo', function() { - return generateInRepoAddon(['service', 'foo', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/services/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('lib/my-addon/app/services/foo.js', { - contains: [ - "export { default } from 'my-addon/services/foo';" - ] - }); - assertFile('tests/unit/services/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo'" - ] - }); - }); + expect(file('package.json')).to.contain('lib/my-addon'); }); - - it('in-repo-addon service foo/bar', function() { - return generateInRepoAddon(['service', 'foo/bar', '--in-repo-addon=my-addon']).then(function() { - assertFile('lib/my-addon/addon/services/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('lib/my-addon/app/services/foo/bar.js', { - contains: [ - "export { default } from 'my-addon/services/foo/bar';" - ] - }); - assertFile('tests/unit/services/foo/bar-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo/bar'" - ] - }); - }); - }); - it('in-repo-addon acceptance-test foo', function() { - return generateInRepoAddon(['acceptance-test', 'foo', '--in-repo-addon=my-addon']).then(function() { - var expected = path.join(__dirname, '../fixtures/generate/acceptance-test-expected.js'); - - assertFileEquals('tests/acceptance/foo-test.js', expected); - assertFileToNotExist('app/acceptance-tests/foo.js'); - }); - }); - - it('in-repo-addon adds path to lib', function() { - return initInRepoAddon().then(function() { - assertFile('package.json', { - contains: [ - 'lib/my-addon' - ] - }); - }); - }); - }); diff --git a/tests/acceptance/init-test.js b/tests/acceptance/init-test.js index 48cf0cb86b..b172921c71 100644 --- a/tests/acceptance/init-test.js +++ b/tests/acceptance/init-test.js @@ -1,226 +1,239 @@ 'use strict'; -var ember = require('../helpers/ember'); -var expect = require('chai').expect; -var walkSync = require('walk-sync'); -var glob = require('glob'); -var Blueprint = require('../../lib/models/blueprint'); -var path = require('path'); -var tmp = require('../helpers/tmp'); -var root = process.cwd(); -var util = require('util'); -var conf = require('../helpers/conf'); -var minimatch = require('minimatch'); -var intersect = require('lodash/array/intersection'); -var remove = require('lodash/array/remove'); -var forEach = require('lodash/collection/forEach'); -var any = require('lodash/collection/some'); -var EOL = require('os').EOL; - -var defaultIgnoredFiles = Blueprint.ignoredFiles; - -describe('Acceptance: ember init', function() { +const ember = require('../helpers/ember'); +const walkSync = require('walk-sync'); +const { globSync } = require('glob'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +let root = process.cwd(); +const util = require('util'); +const { minimatch } = require('minimatch'); +const intersect = require('lodash/intersection'); +const remove = require('lodash/remove'); +const EOL = require('os').EOL; +const td = require('testdouble'); +const lintFix = require('../../lib/utilities/lint-fix'); + +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +const { DEPRECATIONS } = require('../../lib/debug'); +const { confirmViteBlueprint } = require('../helpers-internal/blueprint'); + +const { expect } = require('chai'); +const { dir, file } = require('chai-files'); + +let defaultIgnoredFiles = Blueprint.ignoredFiles; + +describe('Acceptance: ember init', function () { this.timeout(20000); - before(function() { - conf.setup(); - }); + async function makeTempDir() { + let baseTmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'init-test')); + let projectDir = path.join(baseTmpDir, 'hello-world'); - after(function() { - conf.restore(); - }); + await fs.promises.mkdir(projectDir); - beforeEach(function() { + return projectDir; + } + + let tmpPath; + beforeEach(async function () { Blueprint.ignoredFiles = defaultIgnoredFiles; - return tmp.setup('./tmp') - .then(function() { - process.chdir('./tmp'); - }); + tmpPath = await makeTempDir(); + process.chdir(tmpPath); }); - afterEach(function() { - return tmp.teardown('./tmp'); + afterEach(function () { + td.reset(); + process.chdir(root); }); - function confirmBlueprinted() { - var blueprintPath = path.join(root, 'blueprints', 'app', 'files'); - var expected = walkSync(blueprintPath).sort(); - var actual = walkSync('.').sort(); + function confirmBlueprinted(typescript = false) { + if (isExperimentEnabled('VITE')) { + confirmViteBlueprint(); + return; + } + + let blueprintPath = path.join(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint')), 'files'); + // ignore TypeScript files + let expected = walkSync(blueprintPath, { + ignore: ['tsconfig.json', 'types', 'app/config'], + }).map((name) => (typescript ? name : name.replace(/\.ts$/, '.js'))); + + // This style of assertion can't handle conditionally available files + if (expected.some((x) => x.endsWith('eslint.config.mjs'))) { + expected = [...expected.filter((x) => !x.endsWith('eslint.config.mjs')), 'eslint.config.mjs']; + } + // GJS and GTS files are also conditionally available + expected = expected.filter((x) => !x.endsWith('.gjs') && !x.endsWith('.gts')); - forEach(Blueprint.renamedFiles, function(destFile, srcFile) { - expected[expected.indexOf(srcFile)] = destFile; + expected.sort(); + + let actual = walkSync('.').sort(); + + Object.keys(Blueprint.renamedFiles).forEach((srcFile) => { + expected[expected.indexOf(srcFile)] = Blueprint.renamedFiles[srcFile]; }); removeIgnored(expected); removeIgnored(actual); + removeTmp(expected); + removeTmp(actual); + expected.sort(); - expect(expected).to.deep.equal(actual, EOL + ' expected: ' + util.inspect(expected) + - EOL + ' but got: ' + util.inspect(actual)); + expect(expected).to.deep.equal( + actual, + `${EOL} expected: ${util.inspect(expected)}${EOL} but got: ${util.inspect(actual)}` + ); } function confirmGlobBlueprinted(pattern) { - var blueprintPath = path.join(root, 'blueprints', 'app', 'files'); - var actual = pickSync('.', pattern); - var expected = intersect(actual, pickSync(blueprintPath, pattern)); + let blueprintPath = path.join(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint')), 'files'); + let actual = pickSync('.', pattern); + let expected = intersect(actual, pickSync(blueprintPath, pattern)); removeIgnored(expected); removeIgnored(actual); + removeTmp(expected); + removeTmp(actual); + expected.sort(); - expect(expected).to.deep.equal(actual, EOL + ' expected: ' + util.inspect(expected) + - EOL + ' but got: ' + util.inspect(actual)); + expect(expected).to.deep.equal( + actual, + `${EOL} expected: ${util.inspect(expected)}${EOL} but got: ${util.inspect(actual)}` + ); } function pickSync(filePath, pattern) { - return glob.sync(path.join('**', pattern), { - cwd: filePath, - dot: true, - mark: true, - strict: true - }).sort(); + return globSync(`**/${pattern}`, { + cwd: filePath, + dot: true, + mark: true, + }).sort(); + } + + function removeTmp(array) { + remove(array, function (entry) { + return /^tmp[\\/]$/.test(entry); + }); } function removeIgnored(array) { - remove(array, function(fn) { - return any(Blueprint.ignoredFiles, function(ignoredFile) { + remove(array, function (fn) { + return Blueprint.ignoredFiles.some(function (ignoredFile) { return minimatch(fn, ignoredFile, { - matchBase: true + matchBase: true, }); }); }); } - it('ember init', function() { - return ember([ - 'init', - '--skip-npm', - '--skip-bower', - ]).then(confirmBlueprinted); + it('ember init', async function () { + await ember(['init', '--skip-npm']); + + confirmBlueprinted(); }); - it('ember init can run in created folder', function() { - return tmp.setup('./tmp/foo') - .then(function() { - process.chdir('./tmp/foo'); - }) - .then(function() { - return ember([ - 'init', - '--skip-npm', - '--skip-bower' - ]); - }) - .then(confirmBlueprinted) - .then(function() { - return tmp.teardown('./tmp/foo'); - }); + it("init an already init'd folder", async function () { + await ember(['init', '--skip-npm']); + + await ember(['init', '--skip-npm']); + + confirmBlueprinted(); + }); + + it('init a single file', async function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + await ember(['init', 'app.js', '--skip-npm']); + + confirmGlobBlueprinted('app.js'); }); - it('init an already init\'d folder', function() { - return ember([ - 'init', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'init', - '--skip-npm', - '--skip-bower' - ]); - }) - .then(confirmBlueprinted); + it("init a single file on already init'd folder", async function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + await ember(['init', '--skip-npm']); + + await ember(['init', 'app.js', '--skip-npm']); + + confirmBlueprinted(); }); - it('init a single file', function() { - return ember([ - 'init', - 'app.js', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { return 'app.js'; }) - .then(confirmGlobBlueprinted); + it('init multiple files by glob pattern', async function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + await ember(['init', 'app/**', '--skip-npm']); + + confirmGlobBlueprinted('app/**'); }); - it('init a single file on already init\'d folder', function() { - return ember([ - 'init', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'init', - 'app.js', - '--skip-npm', - '--skip-bower' - ]); - }) - .then(confirmBlueprinted); + it("init multiple files by glob pattern on already init'd folder", async function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + await ember(['init', '--skip-npm']); + + await ember(['init', 'app/**', '--skip-npm']); + + confirmBlueprinted(); }); - it('init multiple files by glob pattern', function() { - return ember([ - 'init', - 'app/**', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { return 'app/**'; }) - .then(confirmGlobBlueprinted); + it('init multiple files by glob patterns', async function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + await ember(['init', 'app/**', 'package.json', 'resolver.js', '--skip-npm']); + + confirmGlobBlueprinted('{app/**,package.json,resolver.js}'); }); - it('init multiple files by glob pattern on already init\'d folder', function() { - return ember([ - 'init', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'init', - 'app/**', - '--skip-npm', - '--skip-bower' - ]); - }) - .then(confirmBlueprinted); + it("init multiple files by glob patterns on already init'd folder", async function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + await ember(['init', '--skip-npm']); + + await ember(['init', 'app/**', 'package.json', 'resolver.js', '--skip-npm']); + + confirmBlueprinted(); }); - it('init multiple files by glob patterns', function() { - return ember([ - 'init', - 'app/**', - '{package,bower}.json', - 'resolver.js', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { return '{app/**,{package,bower}.json,resolver.js}'; }) - .then(confirmGlobBlueprinted); + it('should not create .git folder', async function () { + await ember(['init', '--skip-npm']); + + expect(dir('.git')).to.not.exist; }); - it('init multiple files by glob patterns on already init\'d folder', function() { - return ember([ - 'init', - '--skip-npm', - '--skip-bower' - ]) - .then(function() { - return ember([ - 'init', - 'app/**', - '{package,bower}.json', - 'resolver.js', - '--skip-npm', - '--skip-bower' - ]); - }) - .then(confirmBlueprinted); + it('calls lint fix function', async function () { + let lintFixStub = td.replace(lintFix, 'run'); + + await ember(['init', '--skip-npm', '--lint-fix']); + + td.verify(lintFixStub(), { ignoreExtraArgs: true, times: 1 }); + + confirmBlueprinted(); }); + it('no CI provider', async function () { + await ember(['init', '--ci-provider=none', '--skip-install', '--skip-git']); + + expect(file('.github/workflows/ci.yml')).to.not.exist; + expect(file('config/ember-cli-update.json')).to.include('--ci-provider=none'); + }); }); diff --git a/tests/acceptance/install-test-slow.js b/tests/acceptance/install-test-slow.js deleted file mode 100644 index bff2e9764c..0000000000 --- a/tests/acceptance/install-test-slow.js +++ /dev/null @@ -1,75 +0,0 @@ -/*jshint quotmark: false*/ - -'use strict'; - -var Promise = require('../../lib/ext/promise'); -var assertFile = require('../helpers/assert-file'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var path = require('path'); -var remove = Promise.denodeify(require('fs-extra').remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var expect = require('chai').expect; - -describe('Acceptance: ember install', function() { - this.timeout(60000); - var tmpdir; - - before(function() { - conf.setup(); - }); - - after(function() { - conf.restore(); - }); - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); - }); - - afterEach(function() { - process.chdir(root); - return remove(tmproot); - }); - - function initApp() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]); - } - - function installAddon(args) { - var generateArgs = ['install'].concat(args); - - return initApp().then(function() { - return ember(generateArgs); - }); - } - - it('installs addons via npm and runs generators', function() { - return installAddon(['ember-cli-fastclick', 'ember-cli-photoswipe']).then(function(result) { - assertFile('package.json', { - contains: [ - /"ember-cli-fastclick": ".*"/, - /"ember-cli-photoswipe": ".*"/ - ] - }); - - assertFile('bower.json', { - contains: [ - /"fastclick": ".*"/, - /"photoswipe": ".*"/ - ] - }); - - expect(result.ui.output).not.to.include('The `ember generate` command '+ - 'requires an entity name to be specified. For more details, use `ember help`.'); - }); - }); -}); diff --git a/tests/acceptance/missing-before-addon-test.js b/tests/acceptance/missing-before-addon-test.js index 7e445dec46..077c12ad43 100644 --- a/tests/acceptance/missing-before-addon-test.js +++ b/tests/acceptance/missing-before-addon-test.js @@ -1,19 +1,19 @@ 'use strict'; -var path = require('path'); -var ember = require('../helpers/ember'); -var root = process.cwd(); +const path = require('path'); +const ember = require('../helpers/ember'); +let root = process.cwd(); -describe('Acceptance: missing a before/after addon', function() { - before(function() { +describe('Acceptance: missing a before/after addon', function () { + beforeEach(function () { process.chdir(path.join(root, 'tests', 'fixtures', 'missing-before-addon')); }); - after(function() { + afterEach(function () { process.chdir(root); }); - it('does not break ember-cli', function() { + it('does not break ember-cli', function () { return ember(['help']); }); }); diff --git a/tests/acceptance/nested-addons-smoke-test-slow.js b/tests/acceptance/nested-addons-smoke-test-slow.js new file mode 100644 index 0000000000..f43f9735de --- /dev/null +++ b/tests/acceptance/nested-addons-smoke-test-slow.js @@ -0,0 +1,70 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs-extra'); + +const runCommand = require('../helpers/run-command'); +const acceptance = require('../helpers/acceptance'); +const copyFixtureFiles = require('../helpers/copy-fixture-files'); +const DistChecker = require('../helpers/dist-checker'); +let createTestTargets = acceptance.createTestTargets; +let teardownTestTargets = acceptance.teardownTestTargets; +let linkDependencies = acceptance.linkDependencies; +let cleanupRun = acceptance.cleanupRun; + +const { expect } = require('chai'); +const { dir } = require('chai-files'); + +let appName = 'some-cool-app'; +let appRoot; + +describe('Acceptance: nested-addons-smoke-test', function () { + this.timeout(360000); + + before(function () { + return createTestTargets(appName); + }); + + after(teardownTestTargets); + + beforeEach(function () { + appRoot = linkDependencies(appName); + }); + + afterEach(function () { + runCommand.killAll(); + cleanupRun(appName); + expect(dir(appRoot)).to.not.exist; + }); + + it('addons with nested addons compile correctly', async function () { + await copyFixtureFiles('addon/with-nested-addons'); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-top-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + expect(checker.contains('js', 'INNER_ADDON_IMPORT_WITH_APP_IMPORT')).to.be; + expect(checker.contains('js', 'INNER_ADDON_IMPORT_WITH_THIS_IMPORT')).to.be; + + // RAW comments should have been converted to PREPROCESSED by + // tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/preprocesstree-addon + // then from PREPROCESSED to POSTPROCESSED by + // tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/postprocesstree-addon + expect(checker.contains('js', 'POSTPROCESSED node_modules/ember-top-addon/addon/templates/application.hbs')).to.be; + expect(checker.contains('js', 'POSTPROCESSED node_modules/ember-top-addon/addon/index.js')).to.be; + expect(checker.contains('css', 'POSTPROCESSED node_modules/ember-top-addon/addon/styles/app.css')).to.be; + + // the pre/post process tree hooks above should *not* have changed RAW's in the current app + expect(checker.contains('js', 'RAW app/foo.js')).to.be; + + // should *not* have changed RAW's in sibling addons + expect(checker.contains('js', 'RAW node_modules/ember-top-addon/node_modules/ember-inner-addon/addon/index.js')).to + .be; + }); +}); diff --git a/tests/acceptance/new-test-slow.js b/tests/acceptance/new-test-slow.js new file mode 100644 index 0000000000..b4ec5afd9f --- /dev/null +++ b/tests/acceptance/new-test-slow.js @@ -0,0 +1,54 @@ +'use strict'; + +const tmp = require('tmp-promise'); +const { execa } = require('execa'); +const { join, resolve } = require('node:path'); +const ember = require('../helpers/ember'); + +const emberCliRoot = resolve(join(__dirname, '../..')); +const root = process.cwd(); +let tmpDir; + +describe('Acceptance: ember new (slow)', function () { + this.timeout(500000); + + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpDir = path; + process.chdir(path); + }); + + afterEach(function () { + process.chdir(root); + }); + + describe('ember new', function () { + it('generates a new app with no linting errors', async function () { + await ember(['new', 'foo-app', '--pnpm', '--skip-npm']); + // link current version of ember-cli in the newly generated app + await execa('pnpm', ['link', emberCliRoot]); + await execa('pnpm', ['lint'], { cwd: join(tmpDir, 'foo-app') }); + }); + + it('generates a new strict app with no linting errors', async function () { + await ember(['new', 'foo-app', '--strict', '--pnpm', '--skip-npm']); + // link current version of ember-cli in the newly generated app + await execa('pnpm', ['link', emberCliRoot]); + await execa('pnpm', ['lint'], { cwd: join(tmpDir, 'foo-app') }); + }); + + it('generates a new TS app with no linting errors', async function () { + await ember(['new', 'foo-app', '--pnpm', '--typescript', '--skip-npm']); + // link current version of ember-cli in the newly generated app + await execa('pnpm', ['link', emberCliRoot]); + await execa('pnpm', ['lint'], { cwd: join(tmpDir, 'foo-app') }); + }); + + it('generates a new strict TS app with no linting errors', async function () { + await ember(['new', 'foo-app', '--strict', '--pnpm', '--typescript', '--skip-npm']); + // link current version of ember-cli in the newly generated app + await execa('pnpm', ['link', emberCliRoot]); + await execa('pnpm', ['lint'], { cwd: join(tmpDir, 'foo-app') }); + }); + }); +}); diff --git a/tests/acceptance/new-test.js b/tests/acceptance/new-test.js index a3f68aac3f..2b433f1a97 100644 --- a/tests/acceptance/new-test.js +++ b/tests/acceptance/new-test.js @@ -1,218 +1,666 @@ 'use strict'; -var fs = require('fs-extra'); -var ember = require('../helpers/ember'); -var existsSync = require('exists-sync'); -var expect = require('chai').expect; -var forEach = require('lodash/collection/forEach'); -var walkSync = require('walk-sync'); -var Blueprint = require('../../lib/models/blueprint'); -var path = require('path'); -var tmp = require('../helpers/tmp'); -var root = process.cwd(); -var util = require('util'); -var conf = require('../helpers/conf'); -var EOL = require('os').EOL; - -describe('Acceptance: ember new', function() { - before(conf.setup); - - after(conf.restore); - - beforeEach(function() { - return tmp.setup('./tmp') - .then(function() { - process.chdir('./tmp'); - }); - }); +const fs = require('fs-extra'); +const walkSync = require('walk-sync'); +const path = require('path'); +const tmp = require('tmp-promise'); +let root = process.cwd(); +const util = require('util'); - afterEach(function() { - this.timeout(10000); +const EOL = require('os').EOL; - return tmp.teardown('./tmp'); - }); +const Blueprint = require('@ember-tooling/blueprint-model'); +const ember = require('../helpers/ember'); +const hasGlobalYarn = require('../helpers/has-global-yarn'); +const { DEPRECATIONS } = require('../../lib/debug'); - function confirmBlueprintedForDir(dir) { - return function() { - var blueprintPath = path.join(root, dir, 'files'); - var expected = walkSync(blueprintPath); - var actual = walkSync('.').sort(); - var directory = path.basename(process.cwd()); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); - forEach(Blueprint.renamedFiles, function(destFile, srcFile) { - expected[expected.indexOf(srcFile)] = destFile; - }); +const { expect } = require('chai'); +const { dir, file } = require('chai-files'); - expected.sort(); +const { checkFile } = require('../helpers-internal/file-utils'); +const { confirmViteBlueprint } = require('../helpers-internal/blueprint'); +const { + currentVersion, + currentAppBlueprintVersion, + checkEslintConfig, + checkFileWithJSONReplacement, + checkEmberCLIBuild, +} = require('../helpers-internal/fixtures'); - expect(directory).to.equal('foo'); - expect(expected).to.deep.equal(actual, EOL + ' expected: ' + util.inspect(expected) + - EOL + ' but got: ' + util.inspect(actual)); +let tmpDir; - }; - } +function confirmBlueprintedForDir(blueprintDir, expectedAppDir = 'foo', typescript = false) { + let blueprintPath = path.join(blueprintDir, 'files'); + // ignore TypeScript files + let expected = walkSync(blueprintPath, { + ignore: ['tsconfig.json', 'types', 'app/config'], + }).map((name) => (typescript ? name : name.replace(/\.ts$/, '.js'))); - function confirmBlueprinted() { - return confirmBlueprintedForDir('blueprints/app'); + // This style of assertion can't handle conditionally available files + if (expected.some((x) => x.endsWith('eslint.config.mjs'))) { + expected = [...expected.filter((x) => !x.endsWith('eslint.config.mjs')), 'eslint.config.mjs']; } + // GJS and GTS files are also conditionally available + expected = expected.filter((x) => !x.endsWith('.gjs') && !x.endsWith('.gts')); + + let actual = walkSync('.').sort(); + let directory = path.basename(process.cwd()); - it('ember new foo, where foo does not yet exist, works', function() { - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower' - ]).then(confirmBlueprinted); + Object.keys(Blueprint.renamedFiles).forEach((srcFile) => { + expected[expected.indexOf(srcFile)] = Blueprint.renamedFiles[srcFile]; }); - it('ember new with empty app name doesnt throw exception', function() { - return ember([ - 'new', - '' - ]); + expected.sort(); + + // since the test is quite dynamic we want to make sure that the + // directory and the expected aren't empty + expect(directory).to.not.be.empty; + expect(expected).to.not.be.empty; + + expect(directory).to.equal(expectedAppDir); + expect(expected).to.deep.equal( + actual, + `${EOL} expected: ${util.inspect(expected)}${EOL} but got: ${util.inspect(actual)}` + ); +} + +describe('Acceptance: ember new', function () { + this.timeout(300000); + let ORIGINAL_PROCESS_ENV_CI; + + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpDir = path; + process.chdir(path); + ORIGINAL_PROCESS_ENV_CI = process.env.CI; }); - it('ember new without app name doesnt throw exception', function() { - return ember([ - 'new' - ]); + afterEach(function () { + if (ORIGINAL_PROCESS_ENV_CI === undefined) { + delete process.env.CI; + } else { + process.env.CI = ORIGINAL_PROCESS_ENV_CI; + } + process.chdir(root); }); - it('ember new with app name creates new directory and has a dasherized package name', function() { - return ember([ - 'new', - 'FooApp', - '--skip-npm', - '--skip-bower', - '--skip-git' - ]).then(function() { - expect(!existsSync('FooApp')); - - var pkgJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); - expect(pkgJson.name).to.equal('foo-app'); + describe('built-in app blueprint', function () { + before(function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } }); - }); - it('Cannot run ember new, inside of ember-cli project', function() { - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower', - '--skip-git' - ]).then(function() { - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower', - '--skip-git' - ]).then(function() { - expect(!existsSync('foo')); + it('ember new adds ember-welcome-page by default', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git']); + + expect(file('package.json')).to.match(/"ember-welcome-page"/); + + expect(file('app/templates/application.hbs')).to.contain(''); + }); + + it('ember new @foo/bar, when parent directory does not contain `foo`', async function () { + await ember(['new', '@foo/bar', '--skip-npm']); + + confirmBlueprintedForDir(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint')), 'foo-bar'); + }); + + it('ember new @foo/bar, when direct parent directory contains `foo`', async function () { + let scopedDirectoryPath = path.join(process.cwd(), 'foo'); + fs.mkdirsSync(scopedDirectoryPath); + process.chdir(scopedDirectoryPath); + + await ember(['new', '@foo/bar', '--skip-npm']); + + confirmBlueprintedForDir(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint')), 'bar'); + }); + + it('ember new @foo/bar, when parent directory hierarchy contains `foo`', async function () { + let scopedDirectoryPath = path.join(process.cwd(), 'foo', 'packages'); + fs.mkdirsSync(scopedDirectoryPath); + process.chdir(scopedDirectoryPath); + + await ember(['new', '@foo/bar', '--skip-npm']); + + confirmBlueprintedForDir(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint')), 'bar'); + }); + + it('ember new --no-welcome skips installation of ember-welcome-page', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--no-welcome']); + + expect(file('package.json')).not.to.match(/"ember-welcome-page"/); + + expect(file('app/templates/application.hbs')).to.contain('Welcome to Ember'); + }); + + it('ember new generates the correct directory name in `README.md` for scoped package names', async function () { + await ember(['new', '@foo/bar', '--skip-npm', '--skip-git']); + + expect(file('README.md')).to.match(/- `cd foo-bar`/); + }); + + it('ember new without skip-git flag creates .git dir', async function () { + await ember(['new', 'foo', '--skip-npm'], { + skipGit: false, }); - }).then(confirmBlueprinted); - }); - it('ember new with blueprint uses the specified blueprint directory with a relative path', function() { - return tmp.setup('./tmp/my_blueprint') - .then(function() { - return tmp.setup('./tmp/my_blueprint/files'); - }) - .then(function() { - fs.writeFileSync('./tmp/my_blueprint/files/gitignore'); - process.chdir('./tmp'); - - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower', - '--skip-git', - '--blueprint=./my_blueprint' - ]); - }) - .then(confirmBlueprintedForDir('tmp/my_blueprint')); + expect(dir('.git')).to.exist; + }); + + it('ember new with --dry-run does not create new directory', async function () { + await ember(['new', 'foo', '--dry-run']); + + expect(process.cwd()).to.not.match(/foo/, 'does not change cwd to foo in a dry run'); + expect(dir('foo')).to.not.exist; + expect(dir('.git')).to.not.exist; + }); + + it('ember new with --directory uses given directory name and has correct package name', async function () { + let workdir = process.cwd(); + + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--directory=bar']); + + expect(dir(path.join(workdir, 'foo'))).to.not.exist; + expect(dir(path.join(workdir, 'bar'))).to.exist; + + let cwd = process.cwd(); + expect(cwd).to.not.match(/foo/, 'does not use app name for directory name'); + expect(cwd).to.match(/bar/, 'uses given directory name'); + + let pkgJson = fs.readJsonSync('package.json'); + expect(pkgJson.name).to.equal('foo', 'uses app name for package name'); + }); + + it('ember new foo, where foo does not yet exist, works', async function () { + await ember(['new', 'foo', '--skip-npm']); + + confirmBlueprintedForDir(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint'))); + }); + + it('ember new foo, blueprint targets match the default ember-cli targets', async function () { + await ember(['new', 'foo', '--skip-npm']); + + process.env.CI = true; + const defaultTargets = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; + const blueprintTargets = require(path.resolve('config/targets.js')).browsers; + expect(blueprintTargets).to.have.same.deep.members(defaultTargets); + }); + + it('ember new with app name creates new directory and has a dasherized package name', async function () { + await ember(['new', 'FooApp', '--skip-npm', '--skip-git']); + + expect(dir('FooApp')).to.not.exist; + expect(file('package.json')).to.exist; + + let pkgJson = fs.readJsonSync('package.json'); + expect(pkgJson.name).to.equal('foo-app'); + }); + + it('Can create new ember project in an existing empty directory', async function () { + fs.mkdirsSync('bar'); + + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--directory=bar']); + }); + + it('Cannot create new ember project in a populated directory', async function () { + fs.mkdirsSync('bar'); + fs.writeFileSync(path.join('bar', 'package.json'), '{}'); + + let error = await expect(ember(['new', 'foo', '--skip-npm', '--skip-git', '--directory=bar'])).to.be.rejected; + + expect(error.name).to.equal('SilentError'); + expect(error.message).to.equal("Directory 'bar' already exists."); + }); + + it('successfully runs `ember new` inside of an existing ember-cli project', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git']); + confirmBlueprintedForDir(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint'))); + + await ember(['new', 'bar', '--skip-npm', '--skip-git']); + confirmBlueprintedForDir(path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint')), 'bar'); + }); }); - it('ember new with blueprint uses the specified blueprint directory with an absolute path', function() { - return tmp.setup('./tmp/my_blueprint') - .then(function() { - return tmp.setup('./tmp/my_blueprint/files'); - }) - .then(function() { - fs.writeFileSync('./tmp/my_blueprint/files/gitignore'); - process.chdir('./tmp'); - - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower', - '--skip-git', - '--blueprint=' + path.resolve(process.cwd(), './my_blueprint') - ]); - }) - .then(confirmBlueprintedForDir('tmp/my_blueprint')); + describe('--lang', function () { + before(function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + }); + + // Good: Correct Usage + it('ember new foo --lang=(valid code): no message + set `lang` in index.html', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--lang=en-US']); + expect(file('app/index.html')).to.contain(''); + }); + + // Edge Case: both valid code AND programming language abbreviation, possible misuse + it('ember new foo --lang=(valid code + programming language abbreviation): emit warning + set `lang` in index.html', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--lang=css']); + expect(file('app/index.html')).to.contain(''); + }); + + // Misuse: possibly an attempt to set app programming language + it('ember new foo --lang=(programming language): emit warning + do not set `lang` in index.html', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--lang=JavaScript']); + expect(file('app/index.html')).to.contain(''); + }); + + // Misuse: possibly an attempt to set app programming language + it('ember new foo --lang=(programming language abbreviation): emit warning + do not set `lang` in index.html', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--lang=JS']); + expect(file('app/index.html')).to.contain(''); + }); + + // Misuse: possibly an attempt to set app programming language + it('ember new foo --lang=(programming language file extension): emit warning + do not set `lang` in index.html', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--lang=.js']); + expect(file('app/index.html')).to.contain(''); + }); + + // Misuse: Invalid Country Code + it('ember new foo --lang=(invalid code): emit warning + do not set `lang` in index.html', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--lang=en-UK']); + expect(file('app/index.html')).to.contain(''); + }); }); + describe('verify fixtures', function () { + before(function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + }); + + it('app defaults', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git']); + + let namespace = 'app'; + let fixturePath = `${namespace}/defaults`; + + ['app/templates/application.hbs', '.github/workflows/ci.yml', 'README.md', '.ember-cli'].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + if (isExperimentEnabled('EMBROIDER')) { + fixturePath = `${namespace}/embroider`; + } + + checkFileWithJSONReplacement( + fixturePath, + 'config/ember-cli-update.json', + 'packages[0].version', + currentAppBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkEmberCLIBuild(fixturePath, 'ember-cli-build.js'); + + // option independent, but piggy-backing on an existing generate for speed + checkEslintConfig(namespace); + + // ember new without --lang flag (default) has no lang attribute in index.html + expect(file('app/index.html')).to.contain(''); + + // no TypeScript files + [ + 'tsconfig.json', + 'tsconfig.declarations.json', + 'app/config/environment.d.ts', + 'types/global.d.ts', + 'types/foo/index.d.ts', + ].forEach((filePath) => { + expect(file(filePath)).to.not.exist; + }); + }); + + it('app + npm + !welcome', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--no-welcome']); + + let namespace = 'app'; + let fixturePath = `${namespace}/npm`; - it('ember new with git blueprint uses checks out the blueprint and uses it', function(){ - this.timeout(20000); // relies on GH network stuff + ['app/templates/application.hbs', '.github/workflows/ci.yml', 'README.md'].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + if (isExperimentEnabled('EMBROIDER')) { + fixturePath = 'app/embroider-no-welcome'; + } + + checkFileWithJSONReplacement( + fixturePath, + 'config/ember-cli-update.json', + 'packages[0].version', + currentAppBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + // option independent, but piggy-backing on an existing generate for speed + checkEslintConfig(namespace); + }); + + it('app + yarn + welcome', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--yarn']); + + let fixturePath = 'app/yarn'; + + ['app/templates/application.hbs', '.github/workflows/ci.yml', 'README.md'].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + if (isExperimentEnabled('EMBROIDER')) { + fixturePath = 'app/embroider-yarn'; + } + + checkFileWithJSONReplacement( + fixturePath, + 'config/ember-cli-update.json', + 'packages[0].version', + currentAppBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + }); + + it('app + pnpm + welcome', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--pnpm']); + + let fixturePath = 'app/pnpm'; + + ['app/templates/application.hbs', '.github/workflows/ci.yml', 'README.md'].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + if (isExperimentEnabled('EMBROIDER')) { + fixturePath = 'app/embroider-pnpm'; + } + + checkFileWithJSONReplacement( + fixturePath, + 'config/ember-cli-update.json', + 'packages[0].version', + currentAppBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + }); + + it('new - no CI provider', async function () { + await ember(['new', 'foo', '--ci-provider=none', '--skip-install', '--skip-git']); + + expect(file('.github/workflows/ci.yml')).to.not.exist; + expect(file('config/ember-cli-update.json')).to.include('--ci-provider=none'); + }); + + it('app + strict', async function () { + await ember(['new', 'foo', '--strict', '--skip-npm', '--skip-git']); + + let fixturePath = 'app/strict'; + + // check fixtures + ['app/templates/application.gjs', '.ember-cli'].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower', - '--skip-git', - '--blueprint=https://github.com/trek/app-blueprint-test.git' - ]).then(function() { - expect(existsSync('.ember-cli')); + expect(file('app/templates/application.gts')).to.not.exist; + expect(file('app/templates/application.hbs')).to.not.exist; + }); + + it('app + strict + typescript', async function () { + await ember(['new', 'foo', '--typescript', '--strict', '--skip-npm', '--skip-git']); + + let fixturePath = 'app/strict-typescript'; + + // check fixtures + ['app/templates/application.gts', '.ember-cli'].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + + expect(file('app/templates/application.gjs')).to.not.exist; + expect(file('app/templates/application.hbs')).to.not.exist; + }); + + it('app + typescript', async function () { + // we have to use yarn here, as npm fails on unresolvable peer dependencies, see https://github.com/emberjs/ember-test-helpers/issues/1236 + await ember(['new', 'foo', '--typescript', '--skip-npm', '--skip-git', '--yarn']); + + let fixturePath; + if (isExperimentEnabled('EMBROIDER')) { + fixturePath = 'app/typescript-embroider'; + } else { + fixturePath = 'app/typescript'; + } + + // check fixtures + [ + '.ember-cli', + 'tests/helpers/index.ts', + 'tsconfig.json', + 'app/config/environment.d.ts', + 'types/global.d.ts', + ].forEach((filePath) => { + checkFile(filePath, path.join(__dirname, '../fixtures', fixturePath, filePath)); + }); + checkFileWithJSONReplacement( + fixturePath, + 'config/ember-cli-update.json', + 'packages[0].version', + currentAppBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkEmberCLIBuild(fixturePath, 'ember-cli-build.js'); + checkEslintConfig(fixturePath); + + expect(file('tsconfig.declarations.json')).to.not.exist; + }); + + it('app + no-ember-data', async function () { + await ember(['new', 'foo', '--no-ember-data', '--skip-npm', '--skip-git']); + + let fixturePath; + if (isExperimentEnabled('EMBROIDER')) { + fixturePath = 'app/embroider-no-ember-data'; + } else { + fixturePath = 'app/no-ember-data'; + } + + checkFileWithJSONReplacement( + fixturePath, + 'config/ember-cli-update.json', + 'packages[0].version', + currentAppBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkEmberCLIBuild(fixturePath, 'ember-cli-build.js'); + }); + + it('app + typescript + no-ember-data', async function () { + await ember(['new', 'foo', '--typescript', '--no-ember-data', '--skip-npm', '--skip-git']); + + let fixturePath; + if (isExperimentEnabled('EMBROIDER')) { + fixturePath = 'app/typescript-embroider-no-ember-data'; + } else { + fixturePath = 'app/typescript-no-ember-data'; + } + + checkFileWithJSONReplacement( + fixturePath, + 'config/ember-cli-update.json', + 'packages[0].version', + currentAppBlueprintVersion + ); + checkFileWithJSONReplacement(fixturePath, 'package.json', 'devDependencies.ember-cli', `~${currentVersion}`); + checkEmberCLIBuild(fixturePath, 'ember-cli-build.js'); }); }); - it('ember new without skip-git flag creates .git dir', function(){ - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower' - ]).then(function() { - expect(existsSync('.git')); + describe('Experiment(EMBROIDER)', function () { + before(function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + }); + + if (!isExperimentEnabled('CLASSIC')) { + it('embroider experiment creates the correct files', async function () { + let ORIGINAL_PROCESS_ENV = process.env.EMBER_CLI_EMBROIDER; + process.env['EMBER_CLI_EMBROIDER'] = 'true'; + await ember(['new', 'foo', '--skip-npm', '--skip-git']); + + if (ORIGINAL_PROCESS_ENV === undefined) { + delete process.env['EMBER_CLI_EMBROIDER']; + } else { + process.env['EMBER_CLI_EMBROIDER'] = ORIGINAL_PROCESS_ENV; + } + + let pkgJson = fs.readJsonSync('package.json'); + expect(pkgJson.devDependencies['@embroider/compat']).to.exist; + expect(pkgJson.devDependencies['@embroider/core']).to.exist; + expect(pkgJson.devDependencies['@embroider/webpack']).to.exist; + }); + } + + it('embroider enabled with --embroider', async function () { + if (DEPRECATIONS.EMBROIDER.isRemoved) { + this.skip(); + } + + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--embroider']); + + let pkgJson = fs.readJsonSync('package.json'); + expect(pkgJson.devDependencies['@embroider/compat']).to.exist; + expect(pkgJson.devDependencies['@embroider/core']).to.exist; + expect(pkgJson.devDependencies['@embroider/webpack']).to.exist; }); }); - it('ember new with --dry-run does not create new directory', function(){ - return ember([ - 'new', - 'foo', - '--dry-run' - ]).then(function(){ - var cwd = process.cwd(); - expect(cwd).to.not.match(/foo/, 'does not change cwd to foo in a dry run'); - expect(!existsSync(path.join(cwd, 'foo')), 'does not create new directory'); - expect(!existsSync(path.join(cwd, '.git')), 'does not create git in current directory'); + describe('Experiment(VITE)', function () { + before(function () { + if (!isExperimentEnabled('VITE')) { + this.skip(); + } + }); + + it('uses the correct blueprint', async function () { + await ember(['new', 'foo', '--skip-npm', '--skip-git']); + + confirmViteBlueprint(); }); }); - it('ember new with --directory uses given directory name and has correct package name', function() { - return ember([ - 'new', - 'foo', - '--skip-npm', - '--skip-bower', - '--skip-git', - '--directory=bar' - ]).then(function() { - var cwd = process.cwd(); - expect(cwd).to.not.match(/foo/, 'does not use app name for directory name'); - expect(!existsSync(path.join(cwd, 'foo')), 'does not create new directory with app name'); + describe('--blueprint', function () { + it('ember new npm blueprint with old version', async function () { + await ember(['new', 'foo', '--blueprint', '@glimmer/blueprint@0.6.4', '--skip-npm']); - expect(cwd).to.match(/bar/, 'uses given directory name'); - expect(existsSync(path.join(cwd, 'bar')), 'creates new directory with specified name'); + expect(dir('src')).to.exist; + }); - var pkgJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); - expect(pkgJson.name).to.equal('foo', 'uses app name for package name'); + it('ember new with blueprint uses the specified blueprint directory with a relative path', async function () { + fs.mkdirsSync('my_blueprint/files'); + fs.writeFileSync('my_blueprint/files/gitignore', ''); + + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--blueprint=./my_blueprint']); + + confirmBlueprintedForDir(path.join(tmpDir, 'my_blueprint')); + }); + + it('ember new with blueprint uses the specified blueprint directory with an absolute path', async function () { + fs.mkdirsSync('my_blueprint/files'); + fs.writeFileSync('my_blueprint/files/gitignore', ''); + + await ember([ + 'new', + 'foo', + '--skip-npm', + '--skip-git', + `--blueprint=${path.resolve(process.cwd(), 'my_blueprint')}`, + ]); + + confirmBlueprintedForDir(path.join(tmpDir, 'my_blueprint')); + }); + + it('ember new with git blueprint checks out the blueprint and uses it', async function () { + this.timeout(20000); // relies on GH network stuff + + await ember([ + 'new', + 'foo', + '--skip-npm', + '--skip-git', + '--blueprint=https://github.com/ember-cli/app-blueprint-test.git', + ]); + + expect(file('.ember-cli')).to.exist; + }); + + it('ember new with git blueprint and ref checks out the blueprint with the correct ref and uses it', async function () { + this.timeout(20000); // relies on GH network stuff + + await ember([ + 'new', + 'foo', + '--skip-npm', + '--skip-git', + '--blueprint=https://github.com/ember-cli/app-blueprint-test.git#named-ref', + ]); + + expect(file('.named-ref')).to.exist; + }); + + it('ember new with shorthand git blueprint and ref checks out the blueprint with the correct ref and uses it', async function () { + this.timeout(20000); // relies on GH network stuff + + await ember(['new', 'foo', '--skip-npm', '--skip-git', '--blueprint=ember-cli/app-blueprint-test#named-ref']); + + expect(file('.named-ref')).to.exist; + }); + + it('ember new passes blueprint options through to blueprint', async function () { + fs.mkdirsSync('my_blueprint/files'); + fs.writeFileSync( + 'my_blueprint/index.js', + [ + 'module.exports = {', + " availableOptions: [ { name: 'custom-option' } ],", + ' locals(options) {', + ' return {', + ' customOption: options.customOption', + ' };', + ' }', + '};', + ].join('\n') + ); + fs.writeFileSync('my_blueprint/files/gitignore', '<%= customOption %>'); + + await ember([ + 'new', + 'foo', + '--skip-npm', + '--skip-git', + '--blueprint=./my_blueprint', + '--custom-option=customValue', + ]); + + expect(file('.gitignore')).to.contain('customValue'); + }); + + it('ember new uses yarn when blueprint has yarn.lock', async function () { + if (!hasGlobalYarn) { + this.skip(); + } + + fs.mkdirsSync('my_blueprint/files'); + fs.writeFileSync('my_blueprint/index.js', 'module.exports = {};'); + fs.writeFileSync( + 'my_blueprint/files/package.json', + '{ "name": "foo", "dependencies": { "ember-try-test-suite-helper": "*" }}' + ); + fs.writeFileSync('my_blueprint/files/yarn.lock', ''); + + await ember(['new', 'foo', '--skip-git', '--blueprint=./my_blueprint']); + + expect(file('yarn.lock')).to.not.be.empty; + expect(dir('node_modules/ember-try-test-suite-helper')).to.not.be.empty; }); }); }); diff --git a/tests/acceptance/pods-destroy-test.js b/tests/acceptance/pods-destroy-test.js index a9e339ec05..88c4846e60 100644 --- a/tests/acceptance/pods-destroy-test.js +++ b/tests/acceptance/pods-destroy-test.js @@ -1,669 +1,148 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../../lib/ext/promise'); -var expect = require('chai').expect; -var assertFile = require('../helpers/assert-file'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var existsSync = require('exists-sync'); -var fs = require('fs-extra'); -var replaceFile = require('../helpers/file-utils').replaceFile; -var outputFile = Promise.denodeify(fs.outputFile); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var EOL = require('os').EOL; - -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); - -describe('Acceptance: ember destroy pod', function() { - var tmpdir; +const ember = require('../helpers/ember'); +const { outputFile } = require('fs-extra'); +const replaceFile = require('ember-cli-internal-test-helpers/lib/helpers/file-utils').replaceFile; +let root = process.cwd(); +const tmp = require('tmp-promise'); + +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); +describe('Acceptance: ember destroy pod', function () { this.timeout(20000); - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); }); - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); }); - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); }); - afterEach(function() { + afterEach(function () { this.timeout(10000); process.chdir(root); - return remove(tmproot); }); function initApp() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]); - } - - function initAddon() { - return ember([ - 'addon', - 'my-addon', - '--skip-npm', - '--skip-bower' - ]); - } - - function initInRepoAddon() { - return initApp().then(function() { - return ember([ - 'generate', - 'in-repo-addon', - 'my-addon' - ]); - }); + return ember(['init', '--name=my-app', '--skip-npm']); } function generate(args) { - var generateArgs = ['generate'].concat(args); + let generateArgs = ['generate'].concat(args); return ember(generateArgs); } - function generateInAddon(args) { - var generateArgs = ['generate'].concat(args); - - return initAddon().then(function() { - return ember(generateArgs); - }); - } - - function generateInRepoAddon(args) { - var generateArgs = ['generate'].concat(args); - - return initInRepoAddon().then(function() { - return ember(generateArgs); - }); - } - function destroy(args) { - var destroyArgs = ['destroy'].concat(args); + let destroyArgs = ['destroy'].concat(args); return ember(destroyArgs); } - function assertFileNotExists(file) { - var filePath = path.join(process.cwd(), file); - expect(!existsSync(filePath), 'expected ' + file + ' not to exist'); - } - function assertFilesExist(files) { - files.forEach(assertFile); + files.forEach(function (f) { + expect(file(f)).to.exist; + }); } function assertFilesNotExist(files) { - files.forEach(assertFileNotExists); - } - - function assertDestroyAfterGenerate(args, files) { - return initApp() - .then(function() { - replaceFile('config/environment.js', "var ENV = {", "var ENV = {" + EOL + "podModulePrefix: 'app/pods', " + EOL); - return generate(args); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - function assertDestroyAfterGenerateWithUsePods(args, files) { - return initApp() - .then(function() { - replaceFile('.ember-cli', '"disableAnalytics": false', '"disableAnalytics": false,' + EOL + '"usePods" : true' + EOL); - return generate(args); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - function assertDestroyAfterGenerateInAddon(args, files) { - return initAddon() - .then(function() { - return generateInAddon(args); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - function assertDestroyAfterGenerateInRepoAddon(args, files) { - return generateInRepoAddon(args) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(args); - }) - .then(function(result) { - expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); - assertFilesNotExist(files); - }); - } - - function destroyAfterGenerateWithPodsByDefault(args) { - return initApp() - .then(function() { - replaceFile('config/environment.js', "var ENV = {", "var ENV = {" + EOL + "usePodsByDefault: true, " + EOL); - return generate(args); - }) - .then(function() { - return destroy(args); - }); + files.forEach(function (f) { + expect(file(f)).to.not.exist; + }); } - function destroyAfterGenerate(args) { - return initApp() - .then(function() { - replaceFile('config/environment.js', "var ENV = {", "var ENV = {" + EOL + "podModulePrefix: 'app/pods', " + EOL); - return generate(args); - }) - .then(function() { - return destroy(args); - }); - } + const assertDestroyAfterGenerate = async function (args, files) { + await initApp(); - it('.ember-cli usePods setting destroys in pod structure without --pod flag', function() { - var commandArgs = ['controller', 'foo']; - var files = [ - 'app/foo/controller.js', - 'tests/unit/foo/controller-test.js' - ]; + replaceFile('config/environment.js', '(var|let|const) ENV = {', "$1 ENV = {\npodModulePrefix: 'app/pods', \n"); - return assertDestroyAfterGenerateWithUsePods(commandArgs, files); - }); + await generate(args); + assertFilesExist(files); - it('.ember-cli usePods setting destroys in classic structure with --classic flag', function() { - var commandArgs = ['controller', 'foo', '--classic']; - var files = [ - 'app/controllers/foo.js', - 'tests/unit/controllers/foo-test.js' - ]; + let result = await destroy(args); + expect(result, 'destroy command did not exit with errorCode').to.be.an('object'); + assertFilesNotExist(files); + }; - return assertDestroyAfterGenerateWithUsePods(commandArgs, files); - }); + const destroyAfterGenerate = async function (args) { + await initApp(); - it('.ember-cli usePods setting correctly destroys component', function() { - var commandArgs = ['component', 'x-foo']; - var files = [ - 'app/components/x-foo/component.js', - 'app/components/x-foo/template.hbs', - 'tests/integration/components/x-foo/component-test.js' - ]; + replaceFile('config/environment.js', '(var|let|const) ENV = {', "$1 ENV = {\npodModulePrefix: 'app/pods', \n"); - return assertDestroyAfterGenerateWithUsePods(commandArgs, files); - }); + await generate(args); + return await destroy(args); + }; - it('controller foo --pod', function() { - var commandArgs = ['controller', 'foo', '--pod']; - var files = [ - 'app/pods/foo/controller.js', - 'tests/unit/pods/foo/controller-test.js' - ]; + it('blueprint foo --pod', function () { + let commandArgs = ['blueprint', 'foo', '--pod']; + let files = ['blueprints/foo/index.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('controller foo/bar --pod', function() { - var commandArgs = ['controller', 'foo/bar', '--pod']; - var files = [ - 'app/pods/foo/bar/controller.js', - 'tests/unit/pods/foo/bar/controller-test.js' - ]; + it('blueprint foo/bar --pod', function () { + let commandArgs = ['blueprint', 'foo/bar', '--pod']; + let files = ['blueprints/foo/bar/index.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('component x-foo --pod', function() { - var commandArgs = ['component', 'x-foo', '--pod']; - var files = [ - 'app/pods/components/x-foo/component.js', - 'app/pods/components/x-foo/template.hbs', - 'tests/integration/pods/components/x-foo/component-test.js' - ]; + it('http-mock foo --pod', function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + let commandArgs = ['http-mock', 'foo', '--pod']; + let files = ['server/mocks/foo.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('helper foo-bar --pod', function() { - var commandArgs = ['helper', 'foo-bar', '--pod']; - var files = [ - 'app/helpers/foo-bar.js', - 'tests/unit/helpers/foo-bar-test.js' - ]; + it('http-proxy foo --pod', function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + let commandArgs = ['http-proxy', 'foo', 'bar', '--pod']; + let files = ['server/proxies/foo.js']; return assertDestroyAfterGenerate(commandArgs, files); }); - it('helper foo/bar-baz --pod', function() { - var commandArgs = ['helper', 'foo/bar-baz', '--pod']; - var files = [ - 'app/helpers/foo/bar-baz.js', - 'tests/unit/helpers/foo/bar-baz-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); + it('deletes files generated using blueprints from the project directory', async function () { + let commandArgs = ['foo', 'bar', '--pod']; + let files = ['app/foos/bar.js']; - it('model foo --pod', function() { - var commandArgs = ['model', 'foo', '--pod']; - var files = [ - 'app/pods/foo/model.js', - 'tests/unit/pods/foo/model-test.js' - ]; + await initApp(); - return assertDestroyAfterGenerate(commandArgs, files); - }); + await outputFile( + 'blueprints/foo/files/app/foos/__name__.js', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); - it('model foo/bar --pod', function() { - var commandArgs = ['model', 'foo/bar', '--pod']; - var files = [ - 'app/pods/foo/bar/model.js', - 'tests/unit/pods/foo/bar/model-test.js' - ]; + await generate(commandArgs); + assertFilesExist(files); - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('route foo --pod', function() { - var commandArgs = ['route', 'foo', '--pod']; - var files = [ - 'app/pods/foo/route.js', - 'app/pods/foo/template.hbs', - 'tests/unit/pods/foo/route-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files) - .then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); - }); - - it('route index --pod', function() { - var commandArgs = ['route', 'index', '--pod']; - var files = [ - 'app/pods/index/route.js', - 'app/pods/index/template.hbs', - 'tests/unit/pods/index/route-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('route basic --pod', function() { - var commandArgs = ['route', 'basic', '--pod']; - var files = [ - 'app/pods/basic/route.js', - 'app/pods/basic/template.hbs', - 'tests/unit/pods/basic/route-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('resource foo --pod', function() { - var commandArgs = ['resource', 'foo', '--pod']; - var files = [ - 'app/pods/foo/model.js', - 'tests/unit/pods/foo/model-test.js', - 'app/pods/foo/route.js', - 'tests/unit/pods/foo/route-test.js', - 'app/pods/foo/template.hbs' - ]; - - return assertDestroyAfterGenerate(commandArgs, files) - .then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('foo');" - }); - }); - }); - - it('template foo --pod', function() { - var commandArgs = ['template', 'foo', '--pod']; - var files = ['app/pods/foo/template.hbs']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('template foo/bar --pod', function() { - var commandArgs = ['template', 'foo/bar', '--pod']; - var files = ['app/pods/foo/bar/template.hbs']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('view foo --pod', function() { - var commandArgs = ['view', 'foo', '--pod']; - var files = [ - 'app/pods/foo/view.js', - 'tests/unit/pods/foo/view-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('view foo/bar --pod', function() { - var commandArgs = ['view', 'foo/bar', '--pod']; - var files = [ - 'app/pods/foo/bar/view.js', - 'tests/unit/pods/foo/bar/view-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('initializer foo --pod', function() { - var commandArgs = ['initializer', 'foo', '--pod']; - var files = ['app/initializers/foo.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('initializer foo/bar', function() { - var commandArgs = ['initializer', 'foo/bar', '--pod']; - var files = ['app/initializers/foo/bar.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('mixin foo --pod', function() { - var commandArgs = ['mixin', 'foo', '--pod']; - var files = [ - 'app/mixins/foo.js', - 'tests/unit/mixins/foo-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('mixin foo/bar --pod', function() { - var commandArgs = ['mixin', 'foo/bar', '--pod']; - var files = [ - 'app/mixins/foo/bar.js', - 'tests/unit/mixins/foo/bar-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('adapter foo --pod', function() { - var commandArgs = ['adapter', 'foo', '--pod']; - var files = ['app/pods/foo/adapter.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('adapter foo/bar --pod', function() { - var commandArgs = ['adapter', 'foo/bar', '--pod']; - var files = ['app/pods/foo/bar/adapter.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('serializer foo --pod', function() { - var commandArgs = ['serializer', 'foo', '--pod']; - var files = [ - 'app/pods/foo/serializer.js', - 'tests/unit/pods/foo/serializer-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('serializer foo/bar --pod', function() { - var commandArgs = ['serializer', 'foo/bar', '--pod']; - var files = [ - 'app/pods/foo/bar/serializer.js', - 'tests/unit/pods/foo/bar/serializer-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('transform foo --pod', function() { - var commandArgs = ['transform', 'foo', '--pod']; - var files = [ - 'app/pods/foo/transform.js', - 'tests/unit/pods/foo/transform-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('transform foo/bar --pod', function() { - var commandArgs = ['transform', 'foo/bar', '--pod']; - var files = [ - 'app/pods/foo/bar/transform.js', - 'tests/unit/pods/foo/bar/transform-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('util foo-bar --pod', function() { - var commandArgs = ['util', 'foo-bar', '--pod']; - var files = [ - 'app/utils/foo-bar.js', - 'tests/unit/utils/foo-bar-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('util foo-bar/baz --pod', function() { - var commandArgs = ['util', 'foo/bar-baz', '--pod']; - var files = [ - 'app/utils/foo/bar-baz.js', - 'tests/unit/utils/foo/bar-baz-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('service foo --pod', function() { - var commandArgs = ['service', 'foo', '--pod']; - var files = [ - 'app/pods/foo/service.js', - 'tests/unit/pods/foo/service-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('service foo/bar --pod', function() { - var commandArgs = ['service', 'foo/bar', '--pod']; - var files = [ - 'app/pods/foo/bar/service.js', - 'tests/unit/pods/foo/bar/service-test.js' - ]; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('blueprint foo --pod', function() { - var commandArgs = ['blueprint', 'foo', '--pod']; - var files = ['blueprints/foo/index.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('blueprint foo/bar --pod', function() { - var commandArgs = ['blueprint', 'foo/bar', '--pod']; - var files = ['blueprints/foo/bar/index.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('http-mock foo --pod', function() { - var commandArgs = ['http-mock', 'foo', '--pod']; - var files = ['server/mocks/foo.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('http-proxy foo --pod', function() { - var commandArgs = ['http-proxy', 'foo', 'bar', '--pod']; - var files = ['server/proxies/foo.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('in-addon component x-foo --pod', function() { - var commandArgs = ['component', 'x-foo', '--pod']; - var files = [ - 'addon/components/x-foo/component.js', - 'addon/components/x-foo/template.hbs', - 'app/components/x-foo/component.js', - 'tests/integration/components/x-foo/component-test.js' - ]; - - return assertDestroyAfterGenerateInAddon(commandArgs, files); - }); - - it('in-repo-addon component x-foo --pod', function(){ - var commandArgs = ['component', 'x-foo', '--in-repo-addon=my-addon', '--pod']; - var files = [ - 'lib/my-addon/addon/components/x-foo/component.js', - 'lib/my-addon/addon/components/x-foo/template.hbs', - 'lib/my-addon/app/components/x-foo/component.js', - 'tests/integration/components/x-foo/component-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('in-repo-addon component nested/x-foo --pod', function(){ - var commandArgs = ['component', 'nested/x-foo', '--in-repo-addon=my-addon', '--pod']; - var files = [ - 'lib/my-addon/addon/components/nested/x-foo/component.js', - 'lib/my-addon/addon/components/nested/x-foo/template.hbs', - 'lib/my-addon/app/components/nested/x-foo/component.js', - 'tests/integration/components/nested/x-foo/component-test.js' - ]; - - return assertDestroyAfterGenerateInRepoAddon(commandArgs, files); - }); - - it('acceptance-test foo --pod', function() { - var commandArgs = ['acceptance-test', 'foo', '--pod']; - var files = ['tests/acceptance/foo-test.js']; - - return assertDestroyAfterGenerate(commandArgs, files); - }); - - it('deletes files generated using blueprints from the project directory', function() { - var commandArgs = ['foo', 'bar', '--pod']; - var files = ['app/foos/bar.js']; - return initApp() - .then(function() { - return outputFile( - 'blueprints/foo/files/app/foos/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - 'export default Ember.Object.extend({ foo: true });' + EOL - ); - }) - .then(function() { - return generate(commandArgs); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - return destroy(commandArgs); - }) - .then(function() { - assertFilesNotExist(files); - }); - }); - - it('correctly identifies the root of the project', function() { - var commandArgs = ['controller', 'foo', '--pod']; - var files = ['app/foo/controller.js']; - return initApp() - .then(function() { - return outputFile( - 'blueprints/controller/files/app/__path__/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - "export default Ember.Controller.extend({ custom: true });" + EOL - ); - }) - .then(function() { - return generate(commandArgs); - }) - .then(function() { - assertFilesExist(files); - }) - .then(function() { - process.chdir(path.join(tmpdir, 'app')); - }) - .then(function() { - return destroy(commandArgs); - }) - .then(function() { - process.chdir(tmpdir); - }) - .then(function() { - assertFilesNotExist(files); - }); + await destroy(commandArgs); + assertFilesNotExist(files); }); // Skip until podModulePrefix is deprecated - it.skip('podModulePrefix deprecation warning', function() { - return destroyAfterGenerate(['controller', 'foo', '--pod']).then(function(result) { - expect(result.ui.output).to.include("`podModulePrefix` is deprecated and will be"+ - " removed from future versions of ember-cli. Please move existing pods from"+ - " 'app/pods/' to 'app/'."); - }); - }); + it.skip('podModulePrefix deprecation warning', async function () { + let result = await destroyAfterGenerate(['controller', 'foo', '--pod']); - it('usePodsByDefault deprecation warning', function() { - return destroyAfterGenerateWithPodsByDefault(['controller', 'foo', '--pod']).then(function(result) { - expect(result.ui.output).to.include('`usePodsByDefault` is no longer supported in'+ - ' \'config/environment.js\', use `usePods` in \'.ember-cli\' instead.'); - }); + expect(result.outputStream.join()).to.include( + '`podModulePrefix` is deprecated and will be' + + ' removed from future versions of ember-cli. Please move existing pods from' + + " 'app/pods/' to 'app/'." + ); }); - }); diff --git a/tests/acceptance/pods-generate-test.js b/tests/acceptance/pods-generate-test.js index 97b7b977d6..dc0d20d112 100644 --- a/tests/acceptance/pods-generate-test.js +++ b/tests/acceptance/pods-generate-test.js @@ -1,1964 +1,187 @@ -/*jshint quotmark: false*/ - 'use strict'; -var Promise = require('../../lib/ext/promise'); -var assertFile = require('../helpers/assert-file'); -var assertFileEquals = require('../helpers/assert-file-equals'); -var conf = require('../helpers/conf'); -var ember = require('../helpers/ember'); -var replaceFile = require('../helpers/file-utils').replaceFile; -var fs = require('fs-extra'); -var outputFile = Promise.denodeify(fs.outputFile); -var path = require('path'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var EOL = require('os').EOL; -var expect = require('chai').expect; +const ember = require('../helpers/ember'); +const replaceFile = require('ember-cli-internal-test-helpers/lib/helpers/file-utils').replaceFile; +const fs = require('fs-extra'); +const path = require('path'); +let root = process.cwd(); +const tmp = require('tmp-promise'); -var BlueprintNpmTask = require('../helpers/disable-npm-on-blueprint'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); -describe('Acceptance: ember generate pod', function() { - this.timeout(5000); - var tmpdir; +const { expect } = require('chai'); +const { file } = require('chai-files'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); - before(function() { - BlueprintNpmTask.disableNPM(); - conf.setup(); - }); +describe('Acceptance: ember generate pod', function () { + this.timeout(60000); + + let tmpdir; - after(function() { - BlueprintNpmTask.restoreNPM(); - conf.restore(); + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); }); - beforeEach(function() { - tmpdir = tmp.in(tmproot); - process.chdir(tmpdir); + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); }); - afterEach(function() { - this.timeout(10000); + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpdir = path; + process.chdir(path); + }); + afterEach(function () { process.chdir(root); - return remove(tmproot); }); function initApp() { - return ember([ - 'init', - '--name=my-app', - '--skip-npm', - '--skip-bower' - ]); - } - - function initAddon() { - return ember([ - 'addon', - 'my-addon', - '--skip-npm', - '--skip-bower' - ]); - } - - function initInRepoAddon() { - return initApp().then(function() { - return ember([ - 'generate', - 'in-repo-addon', - 'my-addon' - ]); - }); - } - - function preGenerate(args) { - var generateArgs = ['generate'].concat(args); - - return initApp().then(function() { - return ember(generateArgs); - }); + return ember(['init', '--name=my-app', '--skip-npm']); } function generate(args) { - var generateArgs = ['generate'].concat(args); + let generateArgs = ['generate'].concat(args); - return initApp().then(function() { + return initApp().then(function () { return ember(generateArgs); }); } function generateWithPrefix(args) { - var generateArgs = ['generate'].concat(args); - - return initApp().then(function() { - replaceFile('config/environment.js', "var ENV = {", "var ENV = {" + EOL + "podModulePrefix: 'app/pods', " + EOL); - return ember(generateArgs); - }); - } - - function generateWithUsePods(args) { - var generateArgs = ['generate'].concat(args); - - return initApp().then(function() { - replaceFile('.ember-cli', '"disableAnalytics": false', '"disableAnalytics": false,' + EOL + '"usePods" : true' + EOL); - return ember(generateArgs); - }); - } - - function generateWithUsePodsDeprecated(args) { - var generateArgs = ['generate'].concat(args); - - return initApp().then(function() { - replaceFile('config/environment.js', "var ENV = {", "var ENV = {" + EOL + "usePodsByDefault: true, " + EOL); - return ember(generateArgs); - }); - } - - function generateInAddon(args) { - var generateArgs = ['generate'].concat(args); + let generateArgs = ['generate'].concat(args); - return initAddon().then(function() { + return initApp().then(function () { + replaceFile('config/environment.js', '(var|let|const) ENV = {', "$1 ENV = {\npodModulePrefix: 'app/pods', \n"); return ember(generateArgs); }); } - function generateInRepoAddon(args) { - var generateArgs = ['generate'].concat(args); - - return initInRepoAddon().then(function() { - return ember(generateArgs); - }); - } - - it('.ember-cli usePods setting generates in pod structure without --pod flag', function() { - return generateWithUsePods(['controller', 'foo']).then(function() { - assertFile('app/foo/controller.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/foo/controller-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo'" - ] - }); - }); - }); - - it('.ember-cli usePods setting generates in classic structure with --classic flag', function() { - return generateWithUsePods(['controller', 'foo', '--classic']).then(function() { - assertFile('app/controllers/foo.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/controllers/foo-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo'" - ] - }); - }); - }); - - it('.ember-cli usePods setting generates correct component structure', function() { - return generateWithUsePods(['component', 'x-foo']).then(function() { - assertFile('app/components/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/components/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/components/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true" - ] - }); - }); - }); - - it('controller foo --pod', function() { - return generate(['controller', 'foo', '--pod']).then(function() { - assertFile('app/foo/controller.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/foo/controller-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo'" - ] - }); - }); - }); - - it('controller foo --pod podModulePrefix', function() { - return generateWithPrefix(['controller', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/controller.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/pods/foo/controller-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo'" - ] - }); - }); - }); - - it('controller foo/bar --pod', function() { - return generate(['controller', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/controller.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/foo/bar/controller-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo/bar'" - ] - }); - }); - }); - - it('controller foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['controller', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/controller.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Controller.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/pods/foo/bar/controller-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('controller:foo/bar'" - ] - }); - }); - }); - - it('component x-foo --pod', function() { - return generate(['component', 'x-foo', '--pod']).then(function() { - assertFile('app/components/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/components/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/components/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true" - ] - }); - }); - }); - - it('component x-foo --pod podModulePrefix', function() { - return generateWithPrefix(['component', 'x-foo', '--pod']).then(function() { - assertFile('app/pods/components/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/components/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/components/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod', function() { - return generate(['component', 'foo/x-foo', '--pod']).then(function() { - assertFile('app/components/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/components/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/components/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('foo/x-foo'", - "integration: true", - "{{foo/x-foo}}", - "{{#foo/x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod podModulePrefix', function() { - return generateWithPrefix(['component', 'foo/x-foo', '--pod']).then(function() { - assertFile('app/pods/components/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/components/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/components/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('foo/x-foo'", - "integration: true", - "{{foo/x-foo}}", - "{{#foo/x-foo}}" - ] - }); - }); - }); - - it('component x-foo --pod --path', function() { - return generate(['component', 'x-foo', '--pod', '--path', 'bar']).then(function() { - assertFile('app/bar/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/bar/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/bar/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('bar/x-foo'", - "integration: true", - "{{bar/x-foo}}", - "{{#bar/x-foo}}" - ] - }); - }); - }); - - it('component x-foo --pod --path podModulePrefix', function() { - return generateWithPrefix(['component', 'x-foo', '--pod', '--path', 'bar']).then(function() { - assertFile('app/pods/bar/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/bar/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/bar/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('bar/x-foo'", - "integration: true", - "{{bar/x-foo}}", - "{{#bar/x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod --path', function() { - return generate(['component', 'foo/x-foo', '--pod', '--path', 'bar']).then(function() { - assertFile('app/bar/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/bar/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/bar/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('bar/foo/x-foo'", - "integration: true", - "{{bar/foo/x-foo}}", - "{{#bar/foo/x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod --path podModulePrefix', function() { - return generateWithPrefix(['component', 'foo/x-foo', '--pod', '--path', 'bar']).then(function() { - assertFile('app/pods/bar/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/bar/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/bar/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "moduleForComponent('bar/foo/x-foo'", - "integration: true", - "{{bar/foo/x-foo}}", - "{{#bar/foo/x-foo}}" - ] - }); - }); - }); - - it('component x-foo --pod --path nested', function() { - return generate(['component', 'x-foo', '--pod', '--path', 'bar/baz']).then(function() { - assertFile('app/bar/baz/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/bar/baz/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/bar/baz/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('bar/baz/x-foo'", - "integration: true", - "{{bar/baz/x-foo}}", - "{{#bar/baz/x-foo}}" - ] - }); - }); - }); - - it('component x-foo --pod --path nested podModulePrefix', function() { - return generateWithPrefix(['component', 'x-foo', '--pod', '--path', 'bar/baz']).then(function() { - assertFile('app/pods/bar/baz/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/bar/baz/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/bar/baz/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('bar/baz/x-foo'", - "integration: true", - "{{bar/baz/x-foo}}", - "{{#bar/baz/x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod --path nested', function() { - return generate(['component', 'foo/x-foo', '--pod', '--path', 'bar/baz']).then(function() { - assertFile('app/bar/baz/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/bar/baz/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/bar/baz/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('bar/baz/foo/x-foo'", - "integration: true", - "{{bar/baz/foo/x-foo}}", - "{{#bar/baz/foo/x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod --path nested podModulePrefix', function() { - return generateWithPrefix(['component', 'foo/x-foo', '--pod', '--path', 'bar/baz']).then(function() { - assertFile('app/pods/bar/baz/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/bar/baz/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/bar/baz/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('bar/baz/foo/x-foo'", - "integration: true", - "{{bar/baz/foo/x-foo}}", - "{{#bar/baz/foo/x-foo}}" - ] - }); - }); - }); - - it('component x-foo --pod -no-path', function() { - return generate(['component', 'x-foo', '--pod', '-no-path']).then(function() { - assertFile('app/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); - - it('component x-foo --pod -no-path podModulePrefix', function() { - return generateWithPrefix(['component', 'x-foo', '--pod', '-no-path']).then(function() { - assertFile('app/pods/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('x-foo'", - "integration: true", - "{{x-foo}}", - "{{#x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod -no-path', function() { - return generate(['component', 'foo/x-foo', '--pod', '-no-path']).then(function() { - assertFile('app/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('foo/x-foo'", - "integration: true", - "{{foo/x-foo}}", - "{{#foo/x-foo}}" - ] - }); - }); - }); - - it('component foo/x-foo --pod -no-path podModulePrefix', function() { - return generateWithPrefix(['component', 'foo/x-foo', '--pod', '-no-path']).then(function() { - assertFile('app/pods/foo/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Component.extend({", - "});" - ] - }); - assertFile('app/pods/foo/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('tests/integration/pods/foo/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "import hbs from 'htmlbars-inline-precompile';", - "moduleForComponent('foo/x-foo'", - "integration: true", - "{{foo/x-foo}}", - "{{#foo/x-foo}}" - ] - }); - }); - }); - - it('helper foo-bar --pod', function() { - return generate(['helper', 'foo-bar', '--pod']).then(function() { - assertFile('app/helpers/foo-bar.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBar(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBar);" - }); - assertFile('tests/unit/helpers/foo-bar-test.js', { - contains: "import { fooBar } from '../../../helpers/foo-bar';" - }); - }); - }); - - it('helper foo-bar --pod podModulePrefix', function() { - return generateWithPrefix(['helper', 'foo-bar', '--pod']).then(function() { - assertFile('app/helpers/foo-bar.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBar(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBar);" - }); - assertFile('tests/unit/helpers/foo-bar-test.js', { - contains: "import { fooBar } from '../../../helpers/foo-bar';" - }); - }); - }); - - it('helper foo/bar-baz --pod', function() { - return generate(['helper', 'foo/bar-baz', '--pod']).then(function() { - assertFile('app/helpers/foo/bar-baz.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBarBaz(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBarBaz);" - }); - assertFile('tests/unit/helpers/foo/bar-baz-test.js', { - contains: "import { fooBarBaz } from '../../../../helpers/foo/bar-baz';" - }); - }); - }); - - it('helper foo/bar-baz --pod podModulePrefix', function() { - return generateWithPrefix(['helper', 'foo/bar-baz', '--pod']).then(function() { - assertFile('app/helpers/foo/bar-baz.js', { - contains: "import Ember from 'ember';" + EOL + EOL + - "export function fooBarBaz(params/*, hash*/) {" + EOL + - " return params;" + EOL + - "}" + EOL + EOL + - "export default Ember.Helper.helper(fooBarBaz);" - }); - assertFile('tests/unit/helpers/foo/bar-baz-test.js', { - contains: "import { fooBarBaz } from '../../../../helpers/foo/bar-baz';" - }); - }); - }); - - it('model foo --pod', function() { - return generate(['model', 'foo', '--pod']).then(function() { - assertFile('app/foo/model.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('tests/unit/foo/model-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - }); - }); - - it('model foo --pod podModulePrefix', function() { - return generateWithPrefix(['model', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/model.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('tests/unit/pods/foo/model-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo'" - ] - }); - }); - }); - - it('model foo --pod with attributes', function() { - return generate([ - 'model', - 'foo', - 'noType', - 'firstName:string', - 'created_at:date', - 'is-published:boolean', - 'rating:number', - 'bars:has-many', - 'baz:belongs-to', - 'echo:hasMany', - 'bravo:belongs_to', - '--pod' - ]).then(function() { - assertFile('app/foo/model.js', { - contains: [ - "noType: DS.attr()", - "firstName: DS.attr('string')", - "createdAt: DS.attr('date')", - "isPublished: DS.attr('boolean')", - "rating: DS.attr('number')", - "bars: DS.hasMany('bar')", - "baz: DS.belongsTo('baz')", - "echos: DS.hasMany('echo')", - "bravo: DS.belongsTo('bravo')" - ] - }); - assertFile('tests/unit/foo/model-test.js', { - contains: "needs: ['model:bar', 'model:baz', 'model:echo', 'model:bravo']" - }); - }); - }); - - it('model foo/bar --pod', function() { - return generate(['model', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/model.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('tests/unit/foo/bar/model-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); - - it('model foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['model', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/model.js', { - contains: [ - "import DS from 'ember-data';", - "export default DS.Model.extend" - ] - }); - assertFile('tests/unit/pods/foo/bar/model-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); - - it('in-addon route foo --pod', function() { - return generateInAddon(['route', 'foo', '--pod']).then(function() { - assertFile('addon/foo/route.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('addon/foo/template.hbs', { - contains: "{{outlet}}" - }); - assertFile('app/foo/route.js', { - contains: [ - "export { default } from 'my-addon/foo/route';" - ] - }); - assertFile('app/foo/template.js', { - contains: [ - "export { default } from 'my-addon/foo/template';" - ] - }); - assertFile('tests/unit/foo/route-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - }); - }); - - it('route foo --pod', function() { - return generate(['route', 'foo', '--pod']).then(function() { - assertFile('app/router.js', { - contains: 'this.route(\'foo\')' - }); - assertFile('app/foo/route.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('app/foo/template.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/unit/foo/route-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - }); - }); - - it('route foo --pod with --path', function() { - return generate(['route', 'foo', '--pod', '--path=:foo_id/show']) - .then(function() { - assertFile('app/router.js', { - contains: [ - 'this.route(\'foo\', {', - 'path: \':foo_id/show\'', - '});' - ] - }); - }); - }); - - - it('route foo --pod podModulePrefix', function() { - return generateWithPrefix(['route', 'foo', '--pod']).then(function() { - assertFile('app/router.js', { - contains: 'this.route(\'foo\')' - }); - assertFile('app/pods/foo/route.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.Route.extend({" + EOL + "});" - ] - }); - assertFile('app/pods/foo/template.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/unit/pods/foo/route-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('route:foo'" - ] - }); - }); - }); - - it('route index --pod', function() { - return generate(['route', 'index', '--pod']).then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('index');" - }); - }); - }); - - it('route application --pod', function() { - // need to run `initApp` manually here instead of using `generate` helper - // because we need to remove the templates/application.hbs file to prevent - // a prompt (due to a conflict) - return initApp().then(function() { - remove(path.join('app', 'templates', 'application.hbs')); - }) - .then(function(){ - return ember(['generate', 'route', 'application', '--pod']); - }) - .then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('application');" - }); - }); - }); - - it('route basic --pod isn\'t added to router', function() { - return generate(['route', 'basic', '--pod']).then(function() { - assertFile('app/router.js', { - doesNotContain: "this.route('basic');" - }); - assertFile('app/basic/route.js'); - }); - }); - - it('template foo --pod', function() { - return generate(['template', 'foo', '--pod']).then(function() { - assertFile('app/foo/template.hbs'); - }); - }); - - it('template foo --pod podModulePrefix', function() { - return generateWithPrefix(['template', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/template.hbs'); - }); - }); - - it('template foo/bar --pod', function() { - return generate(['template', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/template.hbs'); - }); - }); - - it('template foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['template', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/template.hbs'); - }); - }); - - it('view foo --pod', function() { - return generate(['view', 'foo', '--pod']).then(function() { - assertFile('app/foo/view.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('tests/unit/foo/view-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo'" - ] - }); - }); - }); - - it('view foo --pod podModulePrefix', function() { - return generateWithPrefix(['view', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/view.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('tests/unit/pods/foo/view-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo'" - ] - }); - }); - }); - - it('view foo/bar --pod', function() { - return generate(['view', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/view.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('tests/unit/foo/bar/view-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo/bar'" - ] - }); - }); - }); - - it('view foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['view', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/view.js', { - contains: [ - "import Ember from 'ember';", - "export default Ember.View.extend({" + EOL + "})" - ] - }); - assertFile('tests/unit/pods/foo/bar/view-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('view:foo/bar'" - ] - }); - }); - }); - - it('resource foos --pod', function() { - return generate(['resource', 'foos', '--pod']).then(function() { - assertFile('app/router.js', { - contains: 'this.route(\'foos\');' - }); - assertFile('app/foo/model.js', { - contains: 'export default DS.Model.extend' - }); - assertFile('app/foos/route.js', { - contains: 'export default Ember.Route.extend({' + EOL + '});' - }); - assertFile('app/foos/template.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/unit/foo/model-test.js', { - contains: "moduleForModel('foo'" - }); - assertFile('tests/unit/foos/route-test.js', { - contains: "moduleFor('route:foos'" - }); - }); - }); - - it('resource foos --pod with --path', function() { - return generate(['resource', 'foos', '--pod', '--path=app/foos']) - .then(function() { - assertFile('app/router.js', { - contains: [ - 'this.route(\'foos\', {', - 'path: \'app/foos\'', - '});' - ] - }); - }); - }); - - it('resource foos --pod podModulePrefix', function() { - return generateWithPrefix(['resource', 'foos', '--pod']).then(function() { - assertFile('app/router.js', { - contains: 'this.route(\'foos\');' - }); - assertFile('app/pods/foo/model.js', { - contains: 'export default DS.Model.extend' - }); - assertFile('app/pods/foos/route.js', { - contains: 'export default Ember.Route.extend({' + EOL + '});' - }); - assertFile('app/pods/foos/template.hbs', { - contains: '{{outlet}}' - }); - assertFile('tests/unit/pods/foo/model-test.js', { - contains: "moduleForModel('foo'" - }); - assertFile('tests/unit/pods/foos/route-test.js', { - contains: "moduleFor('route:foos'" - }); - }); - }); - - it('initializer foo --pod', function() { - return generate(['initializer', 'foo', '--pod']).then(function() { - assertFile('app/initializers/foo.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - }); - }); - - it('initializer foo/bar --pod', function() { - return generate(['initializer', 'foo/bar', '--pod']).then(function() { - assertFile('app/initializers/foo/bar.js', { - contains: "export function initialize(/* container, application */) {" + EOL + - " // application.inject('route', 'foo', 'service:foo');" + EOL + - "}" + EOL + - "" + EOL+ - "export default {" + EOL + - " name: 'foo/bar'," + EOL + - " initialize: initialize" + EOL + - "};" - }); - }); - }); - - it('mixin foo --pod', function() { - return generate(['mixin', 'foo', '--pod']).then(function() { - assertFile('app/mixins/foo.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo-test.js', { - contains: [ - "import FooMixin from '../../../mixins/foo';" - ] - }); - }); - }); - - it('mixin foo/bar --pod', function() { - return generate(['mixin', 'foo/bar', '--pod']).then(function() { - assertFile('app/mixins/foo/bar.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Mixin.create({' + EOL + '});' - ] - }); - assertFile('tests/unit/mixins/foo/bar-test.js', { - contains: [ - "import FooBarMixin from '../../../mixins/foo/bar';" - ] - }); - }); - }); - - it('mixin foo/bar/baz --pod', function() { - return generate(['mixin', 'foo/bar/baz', '--pod']).then(function() { - assertFile('tests/unit/mixins/foo/bar/baz-test.js', { - contains: [ - "import FooBarBazMixin from '../../../mixins/foo/bar/baz';" - ] - }); - }); - }); - - it('adapter application --pod', function() { - return generate(['adapter', 'application', '--pod']).then(function() { - assertFile('app/application/adapter.js', { - contains: [ - "import DS from \'ember-data\';", - "export default DS.RESTAdapter.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/application/adapter-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:application'" - ] - }); - }); - }); - - it('adapter foo --pod', function() { - return generate(['adapter', 'foo', '--pod']).then(function() { - assertFile('app/foo/adapter.js', { - contains: [ - "import ApplicationAdapter from \'./application\';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/foo/adapter-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo'" - ] - }); - }); - }); - - it('adapter foo --pod podModulePrefix', function() { - return generateWithPrefix(['adapter', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/adapter.js', { - contains: [ - "import ApplicationAdapter from \'./application\';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - assertFile('tests/unit/pods/foo/adapter-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('adapter:foo'" - ] - }); - }); - }); - - it('adapter foo/bar --pod', function() { - return generate(['adapter', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/adapter.js', { - contains: [ - "import ApplicationAdapter from \'../application\';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - - it('adapter foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['adapter', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/adapter.js', { - contains: [ - "import ApplicationAdapter from \'../application\';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - - it('adapter application cannot extend from --base-class=application', function() { - return generate(['adapter', 'application', '--base-class=application', '--pod']).then(function() { - expect(false); - }, function(err) { - expect(err.message).to.match(/Adapters cannot extend from themself/); - }); - }); + it('blueprint foo --pod', async function () { + await generate(['blueprint', 'foo', '--pod']); - it('adapter foo cannot extend from --base-class=foo', function() { - return generate(['adapter', 'foo', '--base-class=foo', '--pod']).then(function() { - expect(false); - }, function(err) { - expect(err.message).to.match(/Adapters cannot extend from themself/); - }); + expect(file('blueprints/foo/index.js').content).to.matchSnapshot(); }); - it('adapter --pod extends from --base-class=bar', function() { - return generate(['adapter', 'foo', '--base-class=bar', '--pod']).then(function() { - assertFile('app/foo/adapter.js', { - contains: [ - "import BarAdapter from './bar';", - "export default BarAdapter.extend({" + EOL + "});" - ] - }); - }); - }); + it('blueprint foo/bar --pod', async function () { + await generate(['blueprint', 'foo/bar', '--pod']); - it('adapter --pod extends from --base-class=foo/bar', function() { - return generate(['adapter', 'foo/baz', '--base-class=foo/bar', '--pod']).then(function() { - assertFile('app/foo/baz/adapter.js', { - contains: [ - "import FooBarAdapter from '../foo/bar';", - "export default FooBarAdapter.extend({" + EOL + "});" - ] - }); - }); + expect(file('blueprints/foo/bar/index.js').content).to.matchSnapshot(); }); - it('adapter --pod extends from application adapter if present', function() { - return preGenerate(['adapter', 'application']).then(function() { - return generate(['adapter', 'foo', '--pod']).then(function() { - assertFile('app/foo/adapter.js', { - contains: [ - "import ApplicationAdapter from './application';", - "export default ApplicationAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - }); + it('http-mock foo --pod', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('adapter --pod favors --base-class over application', function() { - return preGenerate(['adapter', 'application']).then(function() { - return generate(['adapter', 'foo', '--base-class=bar', '--pod']).then(function() { - assertFile('app/foo/adapter.js', { - contains: [ - "import BarAdapter from './bar';", - "export default BarAdapter.extend({" + EOL + "});" - ] - }); - }); - }); - }); + await generate(['http-mock', 'foo', '--pod']); - it('serializer foo --pod', function() { - return generate(['serializer', 'foo', '--pod']).then(function() { - assertFile('app/foo/serializer.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/foo/serializer-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - ] - }); - }); - }); + expect(file('server/index.js')).to.contain('mocks.forEach(route => route(app));'); - it('serializer foo --pod podModulePrefix', function() { - return generateWithPrefix(['serializer', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/serializer.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/pods/foo/serializer-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - ] - }); - }); + expect(file('server/mocks/foo.js').content).to.matchSnapshot(); }); - it('serializer foo/bar --pod', function() { - return generate(['serializer', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/serializer.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/foo/bar/serializer-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); + it('http-mock foo-bar --pod', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('serializer foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['serializer', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/serializer.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.RESTSerializer.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/pods/foo/bar/serializer-test.js', { - contains: [ - "import { moduleForModel, test } from 'ember-qunit';", - "moduleForModel('foo/bar'" - ] - }); - }); - }); + await generate(['http-mock', 'foo-bar', '--pod']); - it('transform foo --pod', function() { - return generate(['transform', 'foo', '--pod']).then(function() { - assertFile('app/foo/transform.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('tests/unit/foo/transform-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo'" - ] - }); - }); - }); + expect(file('server/index.js')).to.contain('mocks.forEach(route => route(app));'); - it('transform foo --pod podModulePrefix', function() { - return generateWithPrefix(['transform', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/transform.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('tests/unit/pods/foo/transform-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo'" - ] - }); - }); + expect(file('server/mocks/foo-bar.js').content).to.matchSnapshot(); }); - it('transform foo/bar --pod', function() { - return generate(['transform', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/transform.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - '' + EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('tests/unit/foo/bar/transform-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo/bar'" - ] - }); - }); - }); + it('http-proxy foo --pod', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } - it('transform foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['transform', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/transform.js', { - contains: [ - "import DS from 'ember-data';", - 'export default DS.Transform.extend({' + EOL + - ' deserialize: function(serialized) {' + EOL + - ' return serialized;' + EOL + - ' },' + EOL + - '' + EOL + - ' serialize: function(deserialized) {' + EOL + - ' return deserialized;' + EOL + - ' }' + EOL + - '});' - ] - }); - assertFile('tests/unit/pods/foo/bar/transform-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('transform:foo/bar'" - ] - }); - }); - }); + await generate(['http-proxy', 'foo', 'http://localhost:5000', '--pod']); - it('util foo-bar --pod', function() { - return generate(['util', 'foo-bar', '--pod']).then(function() { - assertFile('app/utils/foo-bar.js', { - contains: 'export default function fooBar() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('tests/unit/utils/foo-bar-test.js', { - contains: [ - "import fooBar from '../../../utils/foo-bar';" - ] - }); - }); - }); + expect(file('server/index.js')).to.contain('proxies.forEach(route => route(app));'); - it('util foo-bar/baz --pod', function() { - return generate(['util', 'foo/bar-baz', '--pod']).then(function() { - assertFile('app/utils/foo/bar-baz.js', { - contains: 'export default function fooBarBaz() {' + EOL + - ' return true;' + EOL + - '}' - }); - assertFile('tests/unit/utils/foo/bar-baz-test.js', { - contains: [ - "import fooBarBaz from '../../../utils/foo/bar-baz';" - ] - }); - }); + expect(file('server/proxies/foo.js').content).to.matchSnapshot(); }); - it('service foo --pod', function() { - return generate(['service', 'foo', '--pod']).then(function() { - assertFile('app/foo/service.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/foo/service-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo'" - ] - }); - }); - }); + it('uses blueprints from the project directory', async function () { + await initApp(); + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.js', + "import Ember from 'ember';\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); - it('service foo/bar --pod', function() { - return generate(['service', 'foo/bar', '--pod']).then(function() { - assertFile('app/foo/bar/service.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/foo/bar/service-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo/bar'" - ] - }); - }); - }); + await ember(['generate', 'foo', 'bar', '--pod']); - it('service foo --pod podModulePrefix', function() { - return generateWithPrefix(['service', 'foo', '--pod']).then(function() { - assertFile('app/pods/foo/service.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/pods/foo/service-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo'" - ] - }); - }); + expect(file('app/foos/bar.js')).to.contain('foo: true'); }); - it('service foo/bar --pod podModulePrefix', function() { - return generateWithPrefix(['service', 'foo/bar', '--pod']).then(function() { - assertFile('app/pods/foo/bar/service.js', { - contains: [ - "import Ember from 'ember';", - 'export default Ember.Service.extend({' + EOL + '});' - ] - }); - assertFile('tests/unit/pods/foo/bar/service-test.js', { - contains: [ - "import { moduleFor, test } from 'ember-qunit';", - "moduleFor('service:foo/bar'" - ] - }); - }); - }); + it('allows custom blueprints to override built-ins', async function () { + await initApp(); + await fs.outputFile( + 'blueprints/controller/files/app/__path__/__name__.js', + "import Ember from 'ember';\n\n" + 'export default Ember.Controller.extend({ custom: true });\n' + ); - it('blueprint foo --pod', function() { - return generate(['blueprint', 'foo', '--pod']).then(function() { - assertFile('blueprints/foo/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); - - it('blueprint foo/bar --pod', function() { - return generate(['blueprint', 'foo/bar', '--pod']).then(function() { - assertFile('blueprints/foo/bar/index.js', { - contains: "module.exports = {" + EOL + - " description: ''"+ EOL + - EOL + - " // locals: function(options) {" + EOL + - " // // Return custom template variables here." + EOL + - " // return {" + EOL + - " // foo: options.entity.options.foo" + EOL + - " // };" + EOL + - " // }" + EOL + - EOL + - " // afterInstall: function(options) {" + EOL + - " // // Perform extra work here." + EOL + - " // }" + EOL + - "};" - }); - }); - }); + await ember(['generate', 'controller', 'foo', '--pod']); - it('http-mock foo --pod', function() { - return generate(['http-mock', 'foo', '--pod']).then(function() { - assertFile('server/index.js', { - contains:"mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooRouter = express.Router();" + EOL + - EOL + - " fooRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo', fooRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); - - it('http-mock foo-bar --pod', function() { - return generate(['http-mock', 'foo-bar', '--pod']).then(function() { - assertFile('server/index.js', { - contains: "mocks.forEach(function(route) { route(app); });" - }); - assertFile('server/mocks/foo-bar.js', { - contains: "module.exports = function(app) {" + EOL + - " var express = require('express');" + EOL + - " var fooBarRouter = express.Router();" + EOL + - EOL + - " fooBarRouter.get('/', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': []" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.post('/', function(req, res) {" + EOL + - " res.status(201).end();" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.get('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.put('/:id', function(req, res) {" + EOL + - " res.send({" + EOL + - " 'foo-bar': {" + EOL + - " id: req.params.id" + EOL + - " }" + EOL + - " });" + EOL + - " });" + EOL + - EOL + - " fooBarRouter.delete('/:id', function(req, res) {" + EOL + - " res.status(204).end();" + EOL + - " });" + EOL + - EOL + - " app.use('/api/foo-bar', fooBarRouter);" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); + expect(file('app/foo/controller.js')).to.contain('custom: true'); }); - it('http-proxy foo --pod', function() { - return generate(['http-proxy', 'foo', 'http://localhost:5000', '--pod']).then(function() { - assertFile('server/index.js', { - contains: "proxies.forEach(function(route) { route(app); });" - }); - assertFile('server/proxies/foo.js', { - contains: "var proxyPath = '/foo';" + EOL + - EOL + - "module.exports = function(app) {" + EOL + - " // For options, see:" + EOL + - " // https://github.com/nodejitsu/node-http-proxy" + EOL + - " var proxy = require('http-proxy').createProxyServer({});" + EOL + - EOL + - " proxy.on('error', function(err, req) {" + EOL + - " console.error(err, req.url);" + EOL + - " });" + EOL + - EOL + - " app.use(proxyPath, function(req, res, next){" + EOL + - " // include root path in proxied request" + EOL + - " req.url = proxyPath + '/' + req.url;" + EOL + - " proxy.web(req, res, { target: 'http://localhost:5000' });" + EOL + - " });" + EOL + - "};" - }); - assertFile('server/.jshintrc', { - contains: '{' + EOL + ' "node": true' + EOL + '}' - }); - }); - }); + it('passes custom cli arguments to blueprint options', async function () { + await initApp(); + await fs.outputFile( + 'blueprints/customblue/files/app/__name__.js', + 'Q: Can I has custom command? A: <%= hasCustomCommand %>' + ); - it('in-addon component x-foo --pod', function() { - return generateInAddon(['component', 'x-foo', '--pod']).then(function() { - assertFile('addon/components/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "import layout from './template';", - "export default Ember.Component.extend({", - "layout: layout", - "});" - ] - }); - assertFile('addon/components/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('app/components/x-foo/component.js', { - contains: [ - "export { default } from 'my-addon/components/x-foo/component';" - ] - }); - assertFile('tests/integration/components/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "moduleForComponent('x-foo'", - "integration: true" - ] - }); - }); - }); + await fs.outputFile( + 'blueprints/customblue/index.js', + 'module.exports = {\n' + + ' fileMapTokens(options) {\n' + + ' return {\n' + + ' __name__(options) {\n' + + ' return options.dasherizedModuleName;\n' + + ' }\n' + + ' };\n' + + ' },\n' + + ' locals(options) {\n' + + ' var loc = {};\n' + + " loc.hasCustomCommand = (options.customCommand) ? 'Yes!' : 'No. :C';\n" + + ' return loc;\n' + + ' },\n' + + '};\n' + ); - it('in-repo-addon component x-foo --pod', function() { - return generateInRepoAddon(['component', 'x-foo', '--in-repo-addon=my-addon', '--pod']).then(function() { - assertFile('lib/my-addon/addon/components/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "import layout from './template';", - "export default Ember.Component.extend({", - "layout: layout", - "});" - ] - }); - assertFile('lib/my-addon/addon/components/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('lib/my-addon/app/components/x-foo/component.js', { - contains: [ - "export { default } from 'my-addon/components/x-foo/component';" - ] - }); - assertFile('tests/integration/components/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "moduleForComponent('x-foo'", - "integration: true" - ] - }); - }); - }); + await ember(['generate', 'customblue', 'foo', '--custom-command', '--pod']); - it('in-repo-addon component nested/x-foo', function() { - return generateInRepoAddon(['component', 'nested/x-foo', '--in-repo-addon=my-addon', '--pod']).then(function() { - assertFile('lib/my-addon/addon/components/nested/x-foo/component.js', { - contains: [ - "import Ember from 'ember';", - "import layout from './template';", - "export default Ember.Component.extend({", - "layout: layout", - "});" - ] - }); - assertFile('lib/my-addon/addon/components/nested/x-foo/template.hbs', { - contains: "{{yield}}" - }); - assertFile('lib/my-addon/app/components/nested/x-foo/component.js', { - contains: [ - "export { default } from 'my-addon/components/nested/x-foo/component';" - ] - }); - assertFile('tests/integration/components/nested/x-foo/component-test.js', { - contains: [ - "import { moduleForComponent, test } from 'ember-qunit';", - "moduleForComponent('nested/x-foo'", - "integration: true" - ] - }); - }); + expect(file('app/foo.js')).to.contain('A: Yes!'); }); - it('uses blueprints from the project directory', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/foo/files/app/foos/__name__.js', - "import Ember from 'ember';" + EOL + - 'export default Ember.Object.extend({ foo: true });' + EOL - ); - }) - .then(function() { - return ember(['generate', 'foo', 'bar', '--pod']); - }) - .then(function() { - assertFile('app/foos/bar.js', { - contains: 'foo: true' - }); - }); - }); + it('correctly identifies the root of the project', async function () { + await initApp(); + await fs.outputFile( + 'blueprints/controller/files/app/__path__/__name__.js', + "import Ember from 'ember';\n\n" + 'export default Ember.Controller.extend({ custom: true });\n' + ); - it('allows custom blueprints to override built-ins', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/controller/files/app/__path__/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - "export default Ember.Controller.extend({ custom: true });" + EOL - ); - }) - .then(function() { - return ember(['generate', 'controller', 'foo', '--pod']); - }) - .then(function() { - assertFile('app/foo/controller.js', { - contains: 'custom: true' - }); - }); - }); + process.chdir(path.join(tmpdir, 'app')); + await ember(['generate', 'controller', 'foo', '--pod']); - it('passes custom cli arguments to blueprint options', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/customblue/files/app/__name__.js', - "Q: Can I has custom command? A: <%= hasCustomCommand %>" - ); - }) - .then(function() { - return outputFile( - 'blueprints/customblue/index.js', - "module.exports = {" + EOL + - " fileMapTokens: function(options) {" + EOL + - " return {" + EOL + - " __name__: function(options) {" + EOL + - " return options.dasherizedModuleName;" + EOL + - " }" + EOL + - " };" + EOL + - " }," + EOL + - " locals: function(options) {" + EOL + - " var loc = {};" + EOL + - " loc.hasCustomCommand = (options.customCommand) ? 'Yes!' : 'No. :C';" + EOL + - " return loc;" + EOL + - " }," + EOL + - "};" + EOL - ); - }) - .then(function() { - return ember(['generate', 'customblue', 'foo', '--custom-command', '--pod']); - }) - .then(function() { - assertFile('app/foo.js', { - contains: 'A: Yes!' - }); - }); - }); - - it('acceptance-test foo', function() { - return generate(['acceptance-test', 'foo', '--pod']).then(function() { - var expected = path.join(__dirname, '../fixtures/generate/acceptance-test-expected.js'); - - assertFileEquals('tests/acceptance/foo-test.js', expected); - }); - }); - - it('correctly identifies the root of the project', function() { - return initApp() - .then(function() { - return outputFile( - 'blueprints/controller/files/app/__path__/__name__.js', - "import Ember from 'ember';" + EOL + EOL + - "export default Ember.Controller.extend({ custom: true });" + EOL - ); - }) - .then(function() { - process.chdir(path.join(tmpdir, 'app')); - }) - .then(function() { - return ember(['generate', 'controller', 'foo', '--pod']); - }) - .then(function() { - process.chdir(tmpdir); - }) - .then(function() { - assertFile('app/foo/controller.js', { - contains: 'custom: true' - }); - }); + process.chdir(tmpdir); + expect(file('app/foo/controller.js')).to.contain('custom: true'); }); // Skip until podModulePrefix is deprecated - it.skip('podModulePrefix deprecation warning', function() { - return generateWithPrefix(['controller', 'foo', '--pod']).then(function(result) { - expect(result.ui.output).to.include("`podModulePrefix` is deprecated and will be"+ - " removed from future versions of ember-cli. Please move existing pods from"+ - " 'app/pods/' to 'app/'."); - }); - }); - - it('usePodsByDefault deprecation warning', function() { - return generateWithUsePodsDeprecated(['controller', 'foo', '--pod']).then(function(result) { - expect(result.ui.output).to.include('`usePodsByDefault` is no longer supported in'+ - ' \'config/environment.js\', use `usePods` in \'.ember-cli\' instead.'); - }); - }); - - it('route foo --dry-run --pod does not change router.js', function() { - return generate(['route', 'foo', '--dry-run', '--pod']).then(function() { - assertFile('app/router.js', { - doesNotContain: "route('foo')" - }); - }); - }); - - it('availableOptions work with aliases.', function() { - return generate(['route', 'foo', '-d', '-p']).then(function() { - assertFile('app/router.js', { - doesNotContain: "route('foo')" - }); - }); + it.skip('podModulePrefix deprecation warning', async function () { + let result = await generateWithPrefix(['controller', 'foo', '--pod']); + + expect(result.outputStream.join()).to.include( + '`podModulePrefix` is deprecated and will be' + + ' removed from future versions of ember-cli. Please move existing pods from' + + " 'app/pods/' to 'app/'." + ); }); }); diff --git a/tests/acceptance/pods-generate-test.js.snap b/tests/acceptance/pods-generate-test.js.snap new file mode 100644 index 0000000000..62a0cbfbdc --- /dev/null +++ b/tests/acceptance/pods-generate-test.js.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Acceptance: ember generate pod blueprint foo --pod 1`] = ` +"'use strict'; + +module.exports = { + description: '' + + // locals(options) { + // // Return custom template variables here. + // return { + // foo: options.entity.options.foo + // }; + // } + + // afterInstall(options) { + // // Perform extra work here. + // } +}; +" +`; + +exports[`Acceptance: ember generate pod blueprint foo/bar --pod 1`] = ` +"'use strict'; + +module.exports = { + description: '' + + // locals(options) { + // // Return custom template variables here. + // return { + // foo: options.entity.options.foo + // }; + // } + + // afterInstall(options) { + // // Perform extra work here. + // } +}; +" +`; + +exports[`Acceptance: ember generate pod http-mock foo --pod 1`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooRouter = express.Router(); + + fooRouter.get('/', function(req, res) { + res.send({ + 'foo': [] + }); + }); + + fooRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooRouter.get('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.put('/:id', function(req, res) { + res.send({ + 'foo': { + id: req.params.id + } + }); + }); + + fooRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo', require('body-parser').json()); + app.use('/api/foo', fooRouter); +}; +" +`; + +exports[`Acceptance: ember generate pod http-mock foo-bar --pod 1`] = ` +"'use strict'; + +module.exports = function(app) { + const express = require('express'); + let fooBarRouter = express.Router(); + + fooBarRouter.get('/', function(req, res) { + res.send({ + 'foo-bar': [] + }); + }); + + fooBarRouter.post('/', function(req, res) { + res.status(201).end(); + }); + + fooBarRouter.get('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.put('/:id', function(req, res) { + res.send({ + 'foo-bar': { + id: req.params.id + } + }); + }); + + fooBarRouter.delete('/:id', function(req, res) { + res.status(204).end(); + }); + + // The POST and PUT call will not contain a request body + // because the body-parser is not included by default. + // To use req.body, run: + + // npm install --save-dev body-parser + + // After installing, you need to \`use\` the body-parser for + // this mock uncommenting the following line: + // + //app.use('/api/foo-bar', require('body-parser').json()); + app.use('/api/foo-bar', fooBarRouter); +}; +" +`; + +exports[`Acceptance: ember generate pod http-proxy foo --pod 1`] = ` +"'use strict'; + +const proxyPath = '/foo'; + +module.exports = function(app) { + // For options, see: + // https://github.com/nodejitsu/node-http-proxy + let proxy = require('http-proxy').createProxyServer({}); + + proxy.on('error', function(err, req) { + console.error(err, req.url); + }); + + app.use(proxyPath, function(req, res, next){ + // include root path in proxied request + req.url = proxyPath + '/' + req.url; + proxy.web(req, res, { target: 'http://localhost:5000' }); + }); +}; +" +`; diff --git a/tests/acceptance/preprocessor-smoke-test-slow.js b/tests/acceptance/preprocessor-smoke-test-slow.js index b72dc36e0a..bcb2c8d29f 100644 --- a/tests/acceptance/preprocessor-smoke-test-slow.js +++ b/tests/acceptance/preprocessor-smoke-test-slow.js @@ -1,125 +1,82 @@ 'use strict'; -var path = require('path'); -var fs = require('fs'); -var expect = require('chai').expect; - -var runCommand = require('../helpers/run-command'); -var acceptance = require('../helpers/acceptance'); -var copyFixtureFiles = require('../helpers/copy-fixture-files'); -var assertDirEmpty = require('../helpers/assert-dir-empty'); -var createTestTargets = acceptance.createTestTargets; -var teardownTestTargets = acceptance.teardownTestTargets; -var linkDependencies = acceptance.linkDependencies; -var cleanupRun = acceptance.cleanupRun; - -var appName = 'some-cool-app'; - -describe('Acceptance: preprocessor-smoke-test', function() { - before(function() { - this.timeout(360000); +const path = require('path'); +const fs = require('fs-extra'); + +const runCommand = require('../helpers/run-command'); +const acceptance = require('../helpers/acceptance'); +const DistChecker = require('../helpers/dist-checker'); +const copyFixtureFiles = require('../helpers/copy-fixture-files'); +let createTestTargets = acceptance.createTestTargets; +let teardownTestTargets = acceptance.teardownTestTargets; +let linkDependencies = acceptance.linkDependencies; +let cleanupRun = acceptance.cleanupRun; +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +const { expect } = require('chai'); +const { dir } = require('chai-files'); + +let appName = 'some-cool-app'; +let appRoot; + +describe('Acceptance: preprocessor-smoke-test', function () { + this.timeout(360000); + + before(function () { return createTestTargets(appName); }); - after(function() { - this.timeout(15000); - return teardownTestTargets(); - }); + after(teardownTestTargets); - beforeEach(function() { - this.timeout(360000); - return linkDependencies(appName); + beforeEach(function () { + appRoot = linkDependencies(appName); }); - afterEach(function() { - this.timeout(15000); - return cleanupRun().then(function() { - assertDirEmpty('tmp'); - }); + afterEach(function () { + runCommand.killAll(); + cleanupRun(appName); + expect(dir(appRoot)).to.not.exist; }); - it('addons with standard preprocessors compile correctly', function() { - this.timeout(100000); - - return copyFixtureFiles('preprocessor-tests/app-with-addon-with-preprocessors') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.devDependencies['broccoli-sass'] = 'latest'; - packageJson.devDependencies['ember-cool-addon'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var mainCSS = fs.readFileSync(path.join('.', 'dist', 'assets', 'some-cool-app.css'), { - encoding: 'utf8' - }); - - var vendorCSS = fs.readFileSync(path.join('.', 'dist', 'assets', 'vendor.css'), { - encoding: 'utf8' - }); - - expect(mainCSS).to.contain('app styles included'); - expect(vendorCSS).to.contain('addon styles included'); - }); - }); + it('addons with standard preprocessors compile correctly', async function () { + await copyFixtureFiles(`preprocessor-tests/app-with-addon-with-preprocessors`); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-cli-sass'] = 'latest'; + packageJson.devDependencies['ember-cool-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); - it('addon registry entries are added in the proper order', function() { - this.timeout(100000); - - return copyFixtureFiles('preprocessor-tests/app-registry-ordering') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.devDependencies['first-dummy-preprocessor'] = 'latest'; - packageJson.devDependencies['second-dummy-preprocessor'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var appJs = fs.readFileSync(path.join('.', 'dist', 'assets', 'some-cool-app.js'), { - encoding: 'utf8' - }); - - expect(appJs).to.not.contain('__SECOND_PREPROCESSOR_REPLACEMENT_TOKEN__', 'token should not be contained'); - expect(appJs).to.not.contain('__FIRST_PREPROCESSOR_REPLACEMENT_TOKEN__', 'token should not be contained'); - expect(appJs).to.contain('replacedByPreprocessor', 'token should have been replaced in app bundle'); - }); + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + expect(checker.contains('css', 'app styles included')).to.be; + expect(checker.contains('css', 'addon styles included')).to.be; }); - it('addons without preprocessors compile correctly', function() { - this.timeout(100000); - - return copyFixtureFiles('preprocessor-tests/app-with-addon-without-preprocessors') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.devDependencies['broccoli-sass'] = 'latest'; - packageJson.devDependencies['ember-cool-addon'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var mainCSS = fs.readFileSync(path.join('.', 'dist', 'assets', 'some-cool-app.css'), { - encoding: 'utf8' - }); - - var vendorCSS = fs.readFileSync(path.join('.', 'dist', 'assets', 'vendor.css'), { - encoding: 'utf8' - }); - - expect(mainCSS).to.contain('app styles included'); - expect(vendorCSS).to.contain('addon styles included'); - }); + it('addon registry entries are added in the proper order', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles(`preprocessor-tests/app-registry-ordering`); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['first-dummy-preprocessor'] = 'latest'; + packageJson.devDependencies['second-dummy-preprocessor'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + expect(checker.contains('js', 'replacedByPreprocessor'), 'token should have been replaced in app bundle').to.be; + expect(checker.contains('js', '__SECOND_PREPROCESSOR_REPLACEMENT_TOKEN__'), 'token should not be contained').to.not + .be; + expect(checker.contains('js', '__FIRST_PREPROCESSOR_REPLACEMENT_TOKEN__'), 'token should not be contained').to.not + .be; }); /* @@ -129,34 +86,39 @@ describe('Acceptance: preprocessor-smoke-test', function() { | |-- preprocessor should not apply to this */ - it('addons depending on preprocessor addon preprocesses addon but not app', function() { - this.timeout(100000); - - return copyFixtureFiles('preprocessor-tests/app-with-addon-with-preprocessors-2') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.devDependencies['ember-cool-addon'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var appJs = fs.readFileSync(path.join('.', 'dist', 'assets', 'some-cool-app.js'), { - encoding: 'utf8' - }); - - var vendorJs = fs.readFileSync(path.join('.', 'dist', 'assets', 'vendor.js'), { - encoding: 'utf8' - }); - - expect(appJs).to.contain('__PREPROCESSOR_REPLACEMENT_TOKEN__', 'token should not have been replaced in app bundle'); - expect(appJs).to.not.contain('replacedByPreprocessor', 'token should not have been replaced in app bundle'); - expect(vendorJs).to.not.contain('__PREPROCESSOR_REPLACEMENT_TOKEN__', 'token should have been replaced in vendor bundle'); - expect(vendorJs).to.contain('replacedByPreprocessor', 'token should have been replaced in vendor bundle'); - }); + it('addons depending on preprocessor addon preprocesses addon but not app', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles(`preprocessor-tests/app-with-addon-with-preprocessors-2`); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-cool-addon'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + expect( + checker.contains('js', 'foo_in_app: __PREPROCESSOR_REPLACEMENT_TOKEN__'), + 'token should not have been replaced in app bundle' + ).to.be; + expect( + checker.contains('js', 'foo_in_app: "replacedByPreprocessor"'), + 'token should not have been replaced in app bundle' + ).to.not.be; + + expect( + checker.contains('js', 'foo_in_addon: "replacedByPreprocessor"'), + 'token should have been replaced in vendor bundle' + ).to.be; + expect( + checker.contains('js', 'foo_in_addon: __PREPROCESSOR_REPLACEMENT_TOKEN__'), + 'token should have been replaced in vendor bundle' + ).to.not.be; }); /* @@ -168,36 +130,47 @@ describe('Acceptance: preprocessor-smoke-test', function() { | |-- preprocessor should not apply to this */ - it('addon N levels deep depending on preprocessor preprocesses that parent addon only', function() { - this.timeout(100000); - - return copyFixtureFiles('preprocessor-tests/app-with-addon-with-preprocessors-3') - .then(function() { - var packageJsonPath = path.join(__dirname, '..', '..', 'tmp', appName, 'package.json'); - var packageJson = require(packageJsonPath); - packageJson.devDependencies['ember-shallow-addon'] = 'latest'; - - return fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson)); - }) - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--silent'); - }) - .then(function() { - var appJs = fs.readFileSync(path.join('.', 'dist', 'assets', 'some-cool-app.js'), { - encoding: 'utf8' - }); - - var vendorJs = fs.readFileSync(path.join('.', 'dist', 'assets', 'vendor.js'), { - encoding: 'utf8' - }); - - expect(appJs).to.contain('__PREPROCESSOR_REPLACEMENT_TOKEN__', 'token should not have been replaced in app bundle'); - expect(appJs).to.not.contain('replacedByPreprocessor', 'token should not have been replaced in app bundle'); - expect(vendorJs).to.not.contain('deep: __PREPROCESSOR_REPLACEMENT_TOKEN__', 'token should have been replaced in deep component'); - expect(vendorJs).to.contain('deep: "replacedByPreprocessor"', 'token should have been replaced in deep component'); - expect(vendorJs).to.contain('shallow: __PREPROCESSOR_REPLACEMENT_TOKEN__', 'token should not have been replaced in shallow component'); - expect(vendorJs).to.not.contain('shallow: "replacedByPreprocessor"', 'token should not have been replaced in shallow component'); - }); + it('addon N levels deep depending on preprocessor preprocesses that parent addon only', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + await copyFixtureFiles(`preprocessor-tests/app-with-addon-with-preprocessors-3`); + + let packageJsonPath = path.join(appRoot, 'package.json'); + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies['ember-shallow-addon'] = 'latest'; + + fs.writeJsonSync(packageJsonPath, packageJson); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + expect( + checker.contains('js', 'foo_in_app: __PREPROCESSOR_REPLACEMENT_TOKEN__'), + 'token should not have been replaced in app bundle' + ).to.be; + expect( + checker.contains('js', 'foo_in_app: "replacedByPreprocessor"'), + 'token should not have been replaced in app bundle' + ).to.not.be; + + expect( + checker.contains('js', 'deep: "replacedByPreprocessor"'), + 'token should have been replaced in deep component' + ).to.be; + expect( + checker.contains('js', 'shallow: __PREPROCESSOR_REPLACEMENT_TOKEN__'), + 'token should not have been replaced in shallow component' + ).to.be; + expect( + checker.contains('js', 'deep: __PREPROCESSOR_REPLACEMENT_TOKEN__'), + 'token should have been replaced in deep component' + ).to.not.be; + expect( + checker.contains('js', 'shallow: "replacedByPreprocessor"'), + 'token should not have been replaced in shallow component' + ).to.not.be; }); - }); diff --git a/tests/acceptance/smoke-test-slow.js b/tests/acceptance/smoke-test-slow.js index 112bc2eebb..7d956f9cdb 100644 --- a/tests/acceptance/smoke-test-slow.js +++ b/tests/acceptance/smoke-test-slow.js @@ -1,306 +1,521 @@ 'use strict'; -var path = require('path'); -var fs = require('fs'); -var crypto = require('crypto'); -var expect = require('chai').expect; -var walkSync = require('walk-sync'); -var appName = 'some-cool-app'; -var EOL = require('os').EOL; - -var runCommand = require('../helpers/run-command'); -var acceptance = require('../helpers/acceptance'); -var copyFixtureFiles = require('../helpers/copy-fixture-files'); -var killCliProcess = require('../helpers/kill-cli-process'); -var assertDirEmpty = require('../helpers/assert-dir-empty'); -var createTestTargets = acceptance.createTestTargets; -var teardownTestTargets = acceptance.teardownTestTargets; -var linkDependencies = acceptance.linkDependencies; -var cleanupRun = acceptance.cleanupRun; - -describe('Acceptance: smoke-test', function() { - this.timeout(400000); - before(function() { - return createTestTargets(appName); +const path = require('path'); +const fs = require('fs-extra'); +const crypto = require('crypto'); +const walkSync = require('walk-sync'); +const EOL = require('os').EOL; +const { execa } = require('execa'); + +const { createAndInstallTestTargets } = require('../helpers/acceptance'); +const copyFixtureFiles = require('../helpers/copy-fixture-files'); +const killCliProcess = require('../helpers/kill-cli-process'); +const ember = require('../helpers/ember'); +const runCommand = require('../helpers/run-command'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); +const DistChecker = require('../helpers/dist-checker'); + +const { expect } = require('chai'); +const { dir, file } = require('chai-files'); + +let root = path.resolve(__dirname, '..', '..'); + +let appName = 'some-cool-app'; +let appRoot; +let testSetup; + +// this mimics the vitest it.skipIf API so we can easily swap to it when we swap to vitest +function skipIf(condition) { + if (condition) { + return it.skip; + } else { + return it; + } +} + +describe('Acceptance: smoke-test', function () { + this.timeout(500000); + + beforeEach(async function () { + testSetup = await createAndInstallTestTargets(appName); + appRoot = testSetup.path; + process.chdir(appRoot); }); - after(function() { - return teardownTestTargets(); + afterEach(async function () { + delete process.env._TESTEM_CONFIG_JS_RAN; + runCommand.killAll(); + process.chdir(root); + await testSetup.cleanup(); + expect(dir(appRoot)).to.not.exist; }); - beforeEach(function() { - return linkDependencies(appName); + skipIf(isExperimentEnabled('VITE'))('ember new foo, clean from scratch', function () { + return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); }); - afterEach(function() { - return cleanupRun().then(function() { - assertDirEmpty('tmp'); - }); - }); + it('ember new foo, make sure addon template overwrites', async function () { + await ember(['generate', 'template', 'foo']); + await ember(['generate', 'in-repo-addon', 'my-addon']); - it('ember new foo, clean from scratch', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); + // this should work, but generating a template in an addon/in-repo-addon doesn't + // do the right thing: update once https://github.com/ember-cli/ember-cli/issues/5687 + // is fixed + //return ember(['generate', 'template', 'foo', '--in-repo-addon=my-addon']); + + // temporary work around + let templatePath = path.join('lib', 'my-addon', 'app', 'templates', 'foo.hbs'); + let packageJsonPath = path.join('lib', 'my-addon', 'package.json'); + + fs.mkdirsSync(path.dirname(templatePath)); + fs.writeFileSync(templatePath, 'Hi, Mom!', { encoding: 'utf8' }); + + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.dependencies = packageJson.dependencies || {}; + packageJson.dependencies['ember-cli-htmlbars'] = '*'; + + fs.writeJsonSync(packageJsonPath, packageJson); + + let result = await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + expect(result.code).to.equal(0); }); - it('ember test exits with non-zero when tests fail', function() { - return copyFixtureFiles('smoke-tests/failing-test') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test') - .then(function() { - expect(false, 'should have rejected with a failing test'); - }) - .catch(function(result) { - expect(result.code).to.equal(1); - }); - }); + it('ember test still runs when a JavaScript testem config exists', async function () { + await copyFixtureFiles('smoke-tests/js-testem-config'); + + let result = await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test'); + + let exitCode = result.code; + let output = result.output.join(EOL); + + expect(exitCode).to.eql(0); + expect(output).to.include('***CUSTOM_TESTEM_JS**'); }); - it('ember test exits with non-zero when build fails', function() { - return copyFixtureFiles('smoke-tests/test-with-syntax-error') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test') - .then(function() { - expect(false, 'should have rejected with a failing test'); - }) - .catch(function(result) { - expect(result.code).to.equal(1); - }); + if (!isExperimentEnabled('EMBROIDER')) { + it.skip('ember new foo, build production and verify fingerprint', async function () { + await runCommand( + path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), + 'build', + '--environment=production' + ); + + let dirPath = path.join(appRoot, 'dist', 'assets'); + let dir = fs.readdirSync(dirPath); + let files = []; + + dir.forEach(function (filepath) { + if (filepath === '.gitkeep') { + return; + } + + files.push(filepath); + + let file = fs.readFileSync(path.join(dirPath, filepath), { encoding: null }); + + let md5 = crypto.createHash('md5'); + md5.update(file); + let hex = md5.digest('hex'); + + expect(filepath).to.contain(hex, `${filepath} contains the fingerprint (${hex})`); }); - }); - it('ember test exits with non-zero when no tests are run', function() { - return copyFixtureFiles('smoke-tests/no-testem-launchers') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test') - .then(function() { - expect(false, 'should have rejected with a failing test'); - }) - .catch(function(result) { - expect(result.code).to.equal(1); - }); + let indexHtml = file('dist/index.html'); + files.forEach(function (filename) { + expect(indexHtml).to.contain(filename); }); + }); + } + + skipIf(isExperimentEnabled('VITE'))('ember test --environment=production', async function () { + await copyFixtureFiles('smoke-tests/passing-test'); + + let result = await runCommand( + path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), + 'test', + '--environment=production' + ); + + let exitCode = result.code; + let output = result.output.join(EOL); + + expect(exitCode).to.equal(0, 'exit code should be 0 for passing tests'); + expect(output).to.match(/fail\s+0/, 'no failures'); + expect(output).to.match(/pass\s+\d+/, 'many passing'); }); - // there is a bug in here when running the entire suite on Travis - // when run in isolation, it passes - // here is the error: - // test-support-80f2fe63fae0c44478fe0f8af73200a7.js contains the fingerprint (2871106928f813936fdd64f4d16005ac): expected 'test-support-80f2fe63fae0c44478fe0f8af73200a7.js' to include '2871106928f813936fdd64f4d16005ac' - it.skip('ember new foo, build production and verify fingerprint', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--environment=production') - .then(function() { - var dirPath = path.join('.', 'dist', 'assets'); - var dir = fs.readdirSync(dirPath); - var files = []; - - dir.forEach(function (filepath) { - if (filepath === '.gitkeep') { - return; - } + skipIf(isExperimentEnabled('VITE'))('ember test --path with previous build', async function () { + let originalWrite = process.stdout.write; + let output = []; - files.push(filepath); + await copyFixtureFiles('smoke-tests/passing-test'); - var file = fs.readFileSync(path.join(dirPath, filepath), { encoding: null }); + // TODO: Change to using ember() helper once it properly saves build artifacts + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - var md5 = crypto.createHash('md5'); - md5.update(file); - var hex = md5.digest('hex'); + // TODO: Figure out how to get this to write into the MockUI + process.stdout.write = (function () { + return function () { + output.push(arguments[0]); + }; + })(originalWrite); - expect(filepath).to.contain(hex, filepath + ' contains the fingerprint (' + hex + ')'); - }); + let result; + try { + result = await ember(['test', '--path=dist']); + } finally { + process.stdout.write = originalWrite; + } - var indexHtml = fs.readFileSync(path.join('.', 'dist', 'index.html'), { encoding: 'utf8' }); + expect(result.exitCode).to.equal(0, 'exit code should be 0 for passing tests'); - files.forEach(function (filename) { - expect(indexHtml).to.contain(filename); - }); - }); + output = output.join(EOL); + + expect(output).to.match(/fail\s+0/, 'no failures'); + expect(output).to.match(/pass\s+\d+/, 'many passing'); }); - it('ember test --environment=production', function() { - return copyFixtureFiles('smoke-tests/passing-test') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--environment=production'); - }) - .then(function(result) { - var exitCode = result.code; - var output = result.output.join(EOL); - - expect(exitCode).to.equal(0, 'exit code should be 0 for passing tests'); - expect(output).to.match(/JSHint/, 'JSHint should be run on production assets'); - expect(output).to.match(/fail\s+0/, 'no failures'); - expect(output).to.match(/pass\s+7/, '1 passing'); - }); + skipIf(isExperimentEnabled('VITE'))('ember test wasm', async function () { + let originalWrite = process.stdout.write; + let output = []; + + await copyFixtureFiles('smoke-tests/serve-wasm'); + + // TODO: Change to using ember() helper once it properly saves build artifacts + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + + // TODO: Figure out how to get this to write into the MockUI + process.stdout.write = (function () { + return function () { + output.push(arguments[0]); + }; + })(originalWrite); + + let result; + try { + result = await ember(['test', '--path=dist']); + } finally { + process.stdout.write = originalWrite; + } + + expect(result.exitCode).to.equal(0, 'exit code should be 0 for passing tests'); + + output = output.join(EOL); + + expect(output).to.match(/fail\s+0/, 'no failures'); + expect(output).to.match(/pass\s+\d+/, 'many passing'); }); - it('ember new foo, build development, and verify generated files', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build') - .then(function() { - var dirPath = path.join('.', 'dist'); - var paths = walkSync(dirPath); + it('ember new foo, build development, and verify generated files', async function () { + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); - expect(paths).to.have.length.below(21, 'expected fewer than 21 files in dist, found ' + paths.length); - }); + let dirPath = path.join(appRoot, 'dist'); + let paths = walkSync(dirPath); + + expect(paths).to.have.length.below(25, `expected fewer than 25 files in dist, found ${paths.length}`); }); - it('ember build exits with non-zero code when build fails', function () { - var appJsPath = path.join('.', 'app', 'app.js'); - var ouputContainsBuildFailed = false; + it('ember build exits with non-zero code when build fails', async function () { + let appJsPath = path.join(appRoot, 'app', 'app.js'); - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build') - .then(function (result) { - expect(result.code).to.equal(0, 'expected exit code to be zero, but got ' + result.code); + let result = await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build'); + expect(result.code).to.equal(0, `expected exit code to be zero, but got ${result.code}`); - // add something broken to the project to make build fail - fs.appendFileSync(appJsPath, '{(syntaxError>$@}{'); + // add something broken to the project to make build fail + fs.appendFileSync(appJsPath, '{(syntaxError>$@}{'); - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', { - onOutput: function(string) { - // discard output as there will be a lot of errors and a long stacktrace - // just mark that the output contains expected text - if (!ouputContainsBuildFailed && string.match(/Build failed/)) { - ouputContainsBuildFailed = true; - } - } - }); + result = await expect(runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build')).to.be + .rejected; - }).then(function () { - expect(false, 'should have rejected with a failing build'); - }).catch(function (result) { - expect(ouputContainsBuildFailed, 'command output must contain "Build failed" text'); - expect(result.code).to.not.equal(0, 'expected exit code to be non-zero, but got ' + result.code); - }); + expect(result.code).to.not.equal(0, `expected exit code to be non-zero, but got ${result.code}`); }); - it('ember new foo, build --watch development, and verify rebuilt after change', function() { - var touched = false; - var appJsPath = path.join('.', 'app', 'app.js'); - var builtJsPath = path.join('.', 'dist', 'assets', 'some-cool-app.js'); - var text = 'anotuhaonteuhanothunaothanoteh'; - var line = 'console.log("' + text + '");'; - - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--watch', { - onOutput: function(string, child) { - if (touched) { - if (string.match(/Build successful/)) { - // build after change to app.js - var contents = fs.readFileSync(builtJsPath).toString(); - expect(contents).to.contain(text, 'must contain changed line after rebuild'); - killCliProcess(child); - } - } else { - if (string.match(/Build successful/)) { - // first build - touched = true; - fs.appendFileSync(appJsPath, line); - } - } - } - }) - .catch(function() { - // swallowing because of SIGINT + it('ember build generates instrumentation files when viz is enabled', async function () { + process.env.BROCCOLI_VIZ = '1'; + + try { + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', { + env: { + BROCCOLI_VIZ: '1', + }, }); + } finally { + delete process.env.BROCCOLI_VIZ; + } + + [ + 'instrumentation.build.0.json', + 'instrumentation.command.json', + 'instrumentation.init.json', + 'instrumentation.shutdown.json', + ].forEach((instrumentationFile) => { + expect(fs.existsSync(instrumentationFile)).to.equal(true); + + let json = fs.readJsonSync(instrumentationFile); + expect(Object.keys(json)).to.eql(['summary', 'nodes']); + + expect(Array.isArray(json.nodes)).to.equal(true); + }); }); - it('ember new foo, build --watch development, and verify rebuilt after multiple changes', function() { - var buildCount = 0; - var touched = false; - var appJsPath = path.join('.', 'app', 'app.js'); - var builtJsPath = path.join('.', 'dist', 'assets', 'some-cool-app.js'); - var firstText = 'anotuhaonteuhanothunaothanoteh'; - var firstLine = 'console.log("' + firstText + '");'; - var secondText = 'aahsldfjlwioruoiiononociwewqwr'; - var secondLine = 'console.log("' + secondText + '");'; - - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--watch', { - onOutput: function(string, child) { - if (buildCount === 0) { - if (string.match(/Build successful/)) { + skipIf(isExperimentEnabled('VITE'))( + 'ember new foo, build --watch development, and verify rebuilt after change', + async function () { + let touched = false; + let appJsPath = path.join(appRoot, 'app', 'app.js'); + let text = 'anotuhaonteuhanothunaothanoteh'; + let line = `console.log("${text}");`; + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + try { + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--watch', { + onOutput(string, child) { + if (touched) { + if (string.match(/Build successful/)) { + expect(checker.contains('js', text)).to.be; + killCliProcess(child); + } + } else if (string.match(/Build successful/)) { // first build touched = true; - buildCount = 1; - fs.appendFileSync(appJsPath, firstLine); - } - } else if (buildCount === 1) { - if (string.match(/Build successful/)) { - // second build - touched = true; - buildCount = 2; - fs.appendFileSync(appJsPath, secondLine); + fs.appendFileSync(appJsPath, line); } - } else if (touched && buildCount === 2) { - if (string.match(/Build successful/)) { - // build after change to app.js - var contents = fs.readFileSync(builtJsPath).toString(); - expect(contents.indexOf(secondText) > 1, 'must contain second changed line after rebuild'); - killCliProcess(child); + }, + }); + } catch (error) { + // swallowing because of SIGINT + } + } + ); + + skipIf(isExperimentEnabled('VITE'))( + 'ember new foo, build --watch development, and verify rebuilt after multiple changes', + async function () { + let buildCount = 0; + let touched = false; + let appJsPath = path.join(appRoot, 'app', 'app.js'); + let firstText = 'anotuhaonteuhanothunaothanoteh'; + let firstLine = `console.log("${firstText}");`; + let secondText = 'aahsldfjlwioruoiiononociwewqwr'; + let secondLine = `console.log("${secondText}");`; + + let checker = new DistChecker(path.join(appRoot, 'dist')); + + try { + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--watch', { + onOutput(string, child) { + if (buildCount === 0) { + if (string.match(/Build successful/)) { + // first build + touched = true; + buildCount = 1; + fs.appendFileSync(appJsPath, firstLine); + } + } else if (buildCount === 1) { + if (string.match(/Build successful/)) { + // second build + touched = true; + buildCount = 2; + fs.appendFileSync(appJsPath, secondLine); + } + } else if (touched && buildCount === 2) { + if (string.match(/Build successful/)) { + expect(checker.contains('js', secondText)).to.be; + killCliProcess(child); + } } - } - } - }) - .catch(function() { + }, + }); + } catch (error) { // swallowing because of SIGINT - }); + } + } + ); + + skipIf(isExperimentEnabled('VITE'))('build failures should be logged correctly', async function () { + fs.writeFileSync( + `${process.cwd()}/ember-cli-build.js`, + ` +const Plugin = require('broccoli-plugin'); + +module.exports = function() { + return new class extends Plugin { + constructor() { + super([]); + } + build() { + throw new Error('I AM A BUILD FAILURE'); + } + } +} + ` + ); + + await runCommand( + path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), + 'server', + '--port=0', + '--live-reload=false', + { + onOutput(string, child) { + if (string.includes('I AM A BUILD FAILURE')) { + killCliProcess(child); + } + }, + onError(string, child) { + if (string.includes('I AM A BUILD FAILURE')) { + killCliProcess(child); + } + }, + } + ); }); - it('ember new foo, server, SIGINT clears tmp/', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'server', '--port=54323','--live-reload=false', { - onOutput: function(string, child) { + skipIf(isExperimentEnabled('VITE'))('ember new foo, server, SIGINT clears tmp/', async function () { + let result = await runCommand( + path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), + 'server', + '--port=54323', + '--live-reload=false', + { + onOutput(string, child) { if (string.match(/Build successful/)) { killCliProcess(child); } - } - }) - .catch(function() { - // just eat the rejection as we are testing what happens - }); + }, + } + ); + + expect(result.code, 'should be zero exit code').to.equal(0); + + let dirPath = path.join(appRoot, 'tmp'); + + // before broccoli2, various addons used tmp/ in the project. + // With broccoli2 that should not exist, they should be using os.tmpdir(). + // So we'll just check for "if tmp/ is there, are the contents correct?" + if (fs.existsSync(dirPath)) { + let dir = fs.readdirSync(dirPath).filter((file) => file !== '.metadata_never_index'); + expect(dir.length, `${dirPath} should be empty`).to.equal(0); + } }); - it('ember new foo, build production and verify css files are concatenated', function() { - return copyFixtureFiles('with-styles') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--environment=production') - .then(function() { - var dirPath = path.join('.', 'dist', 'assets'); - var dir = fs.readdirSync(dirPath); - var cssNameRE = new RegExp(appName + '-([a-f0-9]+)\\.css','i'); - dir.forEach(function (filepath) { - if(cssNameRE.test(filepath)) { - var appCss = fs.readFileSync(path.join('.', 'dist', 'assets', filepath), { encoding: 'utf8' }); - expect(appCss).to.contain('.some-weird-selector'); - expect(appCss).to.contain('.some-even-weirder-selector'); + skipIf(isExperimentEnabled('VITE'))( + 'ember new foo, test, SIGINT exits with error and clears tmp/', + async function () { + let result = await expect( + runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test', '--test-port=25522', { + onOutput(string, child) { + // wait for the first passed test and then exit + if (string.match(/^ok /)) { + killCliProcess(child); } - }); - }); + }, + }) + ).to.be.rejected; + + expect(result.code, 'should be error exit code').to.not.equal(0); + + let dirPath = path.join(appRoot, 'tmp'); + + // before broccoli2, various addons used tmp/ in the project. + // With broccoli2 that should not exist, they should be using os.tmpdir(). + // So we'll just check for "if tmp/ is there, are the contents correct?" + if (fs.existsSync(dirPath)) { + let dir = fs.readdirSync(dirPath).filter((file) => file !== '.metadata_never_index'); + expect(dir.length, `${dirPath} should be empty`).to.equal(0); + } + } + ); + + it('ember new foo, build production and verify css files are concatenated', async function () { + await copyFixtureFiles('with-styles'); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--environment=production'); + + let dirPath = path.join(appRoot, 'dist', 'assets'); + let dir = fs.readdirSync(dirPath); + let cssNameRE = new RegExp(`${appName}-([a-f0-9]+)\\.css`, 'i'); + dir.forEach(function (filepath) { + if (cssNameRE.test(filepath)) { + expect(file(`dist/assets/${filepath}`)) + .to.contain('.some-weird-selector') + .to.contain('.some-even-weirder-selector'); + } }); }); - it('ember new foo, build production and verify single "use strict";', function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--environment=production') - .then(function() { - var dirPath = path.join('.', 'dist', 'assets'); - var dir = fs.readdirSync(dirPath); - var appNameRE = new RegExp(appName + '-([a-f0-9]+)\\.js','i'); - dir.forEach(function(filepath) { - if (appNameRE.test(filepath)) { - var contents = fs.readFileSync(path.join('.', 'dist', 'assets', filepath), { encoding: 'utf8' }); - var count = (contents.match(/(["'])use strict\1;/g) || []).length; - expect(count).to.equal(1); - } - }); - }); + it('ember new foo, build production and verify css files are minified', async function () { + await copyFixtureFiles('with-unminified-styles'); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'build', '--environment=production'); + + let dirPath = path.join(appRoot, 'dist', 'assets'); + let dir = fs.readdirSync(dirPath); + let cssNameRE = new RegExp(`${appName}-([a-f0-9]+)\\.css`, 'i'); + dir.forEach(function (filepath) { + if (cssNameRE.test(filepath)) { + let contents = fs.readFileSync(path.join(appRoot, 'dist', 'assets', filepath), { encoding: 'utf8' }); + expect(contents).to.match(/^\S+$/, 'css file is minified'); + } + }); }); - it('ember can override and reuse the built-in blueprints', function() { - return copyFixtureFiles('addon/with-blueprint-override') - .then(function() { - return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'generate', 'component', 'foo-bar', '-p'); - }) - .then(function() { - // because we're overriding, the fileMapTokens is default, sans 'component' - var componentPath = path.join('app','foo-bar','component.js'); - var contents = fs.readFileSync(componentPath, { encoding: 'utf8' }); - - expect(contents).to.contain('generated component successfully'); - }); + it('ember can override and reuse the built-in blueprints', async function () { + await copyFixtureFiles('addon/with-blueprint-override'); + + await runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'generate', 'component', 'foo-bar'); + + let filePath = 'app/components/new-path/foo-bar.js'; + + // because we're overriding, the fileMapTokens is default, sans 'component' + expect(file(filePath)).to.contain('generated component successfully'); + }); + + skipIf(isExperimentEnabled('VITE'))( + 'template linting works properly for pods and classic structured templates', + async function () { + await copyFixtureFiles('smoke-tests/with-template-failing-linting'); + + let packageJsonPath = 'package.json'; + let packageJson = fs.readJsonSync(packageJsonPath); + packageJson.devDependencies = packageJson.devDependencies || {}; + packageJson.devDependencies['fake-template-linter'] = 'latest'; + fs.writeJsonSync(packageJsonPath, packageJson); + + let result = await expect(runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'test')).to.be + .rejected; + + let output = result.output.join(EOL); + expect(output).to.match(/TemplateLint:/, 'ran template linter'); + expect(output).to.match(/fail\s+2/, 'two templates failed linting'); + expect(result.code).to.equal(1); + } + ); + + describe('lint fixing after file generation', function () { + beforeEach(async function () { + await copyFixtureFiles('app/with-blueprint-override-lint-fail'); + }); + + let componentName = 'foo-bar'; + + it('does not fix lint errors with --no-lint-fix', async function () { + await ember(['generate', 'component', componentName, '--component-class=@ember/component', '--no-lint-fix']); + + await expect(execa('eslint', ['.'], { cwd: appRoot, preferLocal: true })).to.eventually.be.rejectedWith( + `${componentName}.js` + ); + await expect( + execa('ember-template-lint', ['.'], { cwd: appRoot, preferLocal: true }) + ).to.eventually.be.rejectedWith(`${componentName}.hbs`); + }); + + skipIf(isExperimentEnabled('VITE'))('does fix lint errors with --lint-fix', async function () { + await ember(['generate', 'component', componentName, '--component-class=@ember/component', '--lint-fix']); + + await expect(execa('eslint', ['.'], { cwd: appRoot, preferLocal: true })).to.eventually.be.ok; + await expect(execa('ember-template-lint', ['.'], { cwd: appRoot, preferLocal: true })).to.eventually.be.ok; + }); }); }); diff --git a/tests/acceptance/typescript-destroy-test.js b/tests/acceptance/typescript-destroy-test.js new file mode 100644 index 0000000000..a839e1457d --- /dev/null +++ b/tests/acceptance/typescript-destroy-test.js @@ -0,0 +1,316 @@ +'use strict'; + +const ember = require('../helpers/ember'); +const fs = require('fs-extra'); +let root = process.cwd(); +const tmp = require('tmp-promise'); + +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); + +describe('Acceptance: ember destroy with typescript blueprints', function () { + this.timeout(60000); + + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); + }); + + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); + }); + + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); + }); + + afterEach(function () { + process.chdir(root); + }); + + function initApp() { + return ember(['init', '--name=my-app', '--skip-npm']); + } + + function generate(args) { + let generateArgs = ['generate'].concat(args); + return ember(generateArgs); + } + + function destroy(args) { + let destroyArgs = ['destroy'].concat(args); + return ember(destroyArgs); + } + + function assertFilesExist(files) { + files.forEach(function (f) { + expect(file(f)).to.exist; + }); + } + + function assertFilesNotExist(files) { + files.forEach(function (f) { + expect(file(f)).to.not.exist; + }); + } + + it('deletes JS files generated from typescript blueprints and transformed', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.js']; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + assertFilesExist(files); + + await destroy(commandArgs); + assertFilesNotExist(files); + }); + + it('deletes TS files generated from typescript blueprints with --typescript', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.ts']; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate([...commandArgs, '--typescript']); + assertFilesExist(files); + + await destroy(commandArgs); + assertFilesNotExist(files); + }); + + it('deletes TS files generated from typescript blueprints when --typescript is passed', async function () { + let commandArgs = ['foo', 'bar', '--typescript']; + let files = ['app/foos/bar.ts']; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + assertFilesExist(files); + + await destroy(commandArgs); + assertFilesNotExist(files); + }); + + it('deletes TS files generated from typescript blueprints in a typescript project', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.ts']; + await initApp(); + + await fs.writeJson('.ember-cli', { + isTypeScriptProject: true, + }); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + assertFilesExist(files); + + await destroy(commandArgs); + assertFilesNotExist(files); + }); + + it('deletes TS files generated from typescript blueprints when {typescript: true} is present in .ember-cli', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.ts']; + await initApp(); + + await fs.writeJson('.ember-cli', { + typescript: true, + }); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + assertFilesExist(files); + + await destroy(commandArgs); + assertFilesNotExist(files); + }); + + it('does not delete anything if --typescript is passed and there are no TS files', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.js']; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + assertFilesExist(files); + + await destroy([...commandArgs, '--typescript']); + assertFilesExist(files); + }); + + it('does not delete anything if --no-typescript is passed and there are no JS files', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.ts']; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate([...commandArgs, '--typescript']); + assertFilesExist(files); + + await destroy([...commandArgs, '--no-typescript']); + assertFilesExist(files); + }); + + describe('when JS and TS files are present', function () { + it('deletes the TS file when --typescript is passed', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.ts', 'app/foos/bar.js']; + const [tsFile, jsFile] = files; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + await generate([...commandArgs, '--typescript']); + assertFilesExist(files); + + await destroy([...commandArgs, '--typescript']); + assertFilesNotExist([tsFile]); + assertFilesExist([jsFile]); + }); + + it('deletes the JS file when --no-typescript flag is passed', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.ts', 'app/foos/bar.js']; + const [tsFile, jsFile] = files; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + await generate([...commandArgs, '--typescript']); + assertFilesExist(files); + + await destroy([...commandArgs, '--no-typescript']); + assertFilesNotExist([jsFile]); + assertFilesExist([tsFile]); + }); + + it('deletes both files when no flags are passed', async function () { + let commandArgs = ['foo', 'bar']; + let files = ['app/foos/bar.ts', 'app/foos/bar.js']; + await initApp(); + + await fs.outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await fs.outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + "import Ember from 'ember';\n\n" + 'export default Ember.Object.extend({ foo: true });\n' + ); + + await generate(commandArgs); + await generate([...commandArgs, '--typescript']); + assertFilesExist(files); + + await destroy(commandArgs); + assertFilesNotExist(files); + }); + }); +}); diff --git a/tests/acceptance/typescript-generate-test.js b/tests/acceptance/typescript-generate-test.js new file mode 100755 index 0000000000..5cc5c5de67 --- /dev/null +++ b/tests/acceptance/typescript-generate-test.js @@ -0,0 +1,298 @@ +'use strict'; + +const ember = require('../helpers/ember'); +const { outputFile, writeJson } = require('fs-extra'); +let root = process.cwd(); +const Blueprint = require('@ember-tooling/blueprint-model'); +const BlueprintNpmTask = require('ember-cli-internal-test-helpers/lib/helpers/disable-npm-on-blueprint'); +const tmp = require('tmp-promise'); +const td = require('testdouble'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); + +describe('Acceptance: ember generate with typescript blueprints', function () { + this.timeout(20000); + + before(function () { + BlueprintNpmTask.disableNPM(Blueprint); + }); + + after(function () { + BlueprintNpmTask.restoreNPM(Blueprint); + }); + + beforeEach(async function () { + const { path } = await tmp.dir(); + process.chdir(path); + }); + + afterEach(function () { + td.reset(); + process.chdir(root); + }); + + function initApp() { + return ember(['init', '--name=my-app', '--skip-npm']); + } + + it('transpiles typescript by default', async function () { + await initApp(); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await outputFile( + 'blueprints/foo-test/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo-test/files/tests/foos/__name__-test.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await ember(['generate', 'foo', 'bar']); + // test that the resulting file both ends in .js and has had all the TS stuff removed + const generated = file('app/foos/bar.js'); + expect(generated).to.contain('export default function bar(a, b) {'); + + const testGenerated = file('tests/foos/bar-test.js'); + expect(testGenerated).to.contain('export default function bar(a, b) {'); + }); + + it('outputs typescript when isTypeScriptProject is true', async function () { + await initApp(); + await writeJson('.ember-cli', { + isTypeScriptProject: true, + }); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await outputFile( + 'blueprints/foo-test/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo-test/files/tests/foos/__name__-test.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await ember(['generate', 'foo', 'bar']); + + const generated = file('app/foos/bar.ts'); + expect(generated).to.equal(`export default function bar(a: string, b: number): string { + return a + b; + }`); + + const testGenerated = file('tests/foos/bar-test.ts'); + expect(testGenerated).to.contain('export default function bar(a: string, b: number): string {'); + }); + + it('outputs typescript {typescript: true} is present in .ember-cli', async function () { + await initApp(); + await writeJson('.ember-cli', { + typescript: true, + }); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await outputFile( + 'blueprints/foo-test/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo-test/files/tests/foos/__name__-test.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await ember(['generate', 'foo', 'bar']); + + const generated = file('app/foos/bar.ts'); + expect(generated).to.equal(`export default function bar(a: string, b: number): string { + return a + b; + }`); + + const testGenerated = file('tests/foos/bar-test.ts'); + expect(testGenerated).to.contain('export default function bar(a: string, b: number): string {'); + }); + + it('generates typescript when the `--typescript` flag is used and a typescript blueprint exists', async function () { + await initApp(); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await ember(['generate', 'foo', 'bar', '--typescript']); + const generated = file('app/foos/bar.ts'); + expect(generated).to.contain('export default function bar(a: string, b: number): string {'); + }); + + it('does not generate typescript when the `--typescript` flag is used but no typescript blueprint exists', async function () { + await initApp(); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.js', + `export default function <%= camelizedModuleName %>(a, b) { + return a + b; + }` + ); + + await ember(['generate', 'foo', 'bar', '--typescript']); + + const generated = file('app/foos/bar.js'); + + expect(generated).to.contain('export default function bar(a, b) {'); + }); + + it('does not generate typescript when the `--no-typescript` flag, even if a typescript blueprint exists', async function () { + await initApp(); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await ember(['generate', 'foo', 'bar', '--no-typescript']); + const generated = file('app/foos/bar.js'); + expect(generated).to.contain('export default function bar(a, b) {'); + }); + + it('does not generate typescript when the `--no-typescript` flag, even if isTypeScriptProject is true', async function () { + await initApp(); + + await writeJson('.ember-cli', { + isTypeScriptProject: true, + }); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + `export default function <%= camelizedModuleName %>(a: string, b: number): string { + return a + b; + }` + ); + + await ember(['generate', 'foo', 'bar', '--no-typescript']); + const generated = file('app/foos/bar.js'); + expect(generated).to.contain('export default function bar(a, b) {'); + }); + + it('can generate classes with decorators and class fields', async function () { + await initApp(); + + await outputFile( + 'blueprints/foo/index.js', + `module.exports = { + shouldTransformTypeScript: true + }` + ); + + await outputFile( + 'blueprints/foo/files/app/foos/__name__.ts', + `export default class <%= classifiedModuleName %> { + bar = 'bar'; + + @action + foo() { + return a + b; + } + }\n` + ); + + const expected = `export default class Bar { + bar = 'bar'; + + @action + foo() { + return a + b; + } +}\n`; + + await ember(['generate', 'foo', 'bar']); + const generated = file('app/foos/bar.js'); + expect(generated).to.equal(expected); + }); +}); diff --git a/tests/bootstrap.js b/tests/bootstrap.js new file mode 100644 index 0000000000..8749376925 --- /dev/null +++ b/tests/bootstrap.js @@ -0,0 +1,11 @@ +'use strict'; + +const { use } = require('chai'); + +const { default: chaiAsPromised } = require('chai-as-promised'); +const chaiFiles = require('chai-files'); +const chaiJestSnapshot = require('chai-jest-snapshot'); + +use(chaiFiles); +use(chaiAsPromised); +use(chaiJestSnapshot); diff --git a/tests/factories/command-options.js b/tests/factories/command-options.js index df42a490ed..dc1179e260 100644 --- a/tests/factories/command-options.js +++ b/tests/factories/command-options.js @@ -1,19 +1,27 @@ 'use strict'; -var defaults = require('lodash/object/defaults'); -var MockUI = require('../helpers/mock-ui'); -var MockAnalytics = require('../helpers/mock-analytics'); -var MockProject = require('../helpers/mock-project'); +const defaults = require('lodash/defaults'); +const MockUI = require('console-ui/mock'); +const MockProject = require('../helpers/mock-project'); -module.exports = function CommandOptionsFactory(options) { - var project = new MockProject(); - project.isEmberCLIProject = function() { return true; }; - project.config = function() { return {}; }; +function createProject() { + let project = new MockProject(); + project.isEmberCLIProject = function () { + return true; + }; + project.config = function () { + return {}; + }; + return project; +} - return defaults(options || { }, { - ui: new MockUI(), - analytics: new MockAnalytics(), - tasks: {}, - project: project +module.exports = function CommandOptionsFactory(options) { + options = options || {}; + return defaults(options, { + ui: new MockUI(), + tasks: {}, + project: options.project || createProject(), + commands: {}, + settings: {}, }); }; diff --git a/tests/fixtures/addon/commands/addon-command-class.js b/tests/fixtures/addon/commands/addon-command-class.js index b164a71943..c5db19bc16 100644 --- a/tests/fixtures/addon/commands/addon-command-class.js +++ b/tests/fixtures/addon/commands/addon-command-class.js @@ -1,4 +1,4 @@ -var Command = require('../../../../lib/models/command'); +const Command = require('../../../../lib/models/command'); function Addon() { this.name = "Ember CLI Addon Class Command Test" diff --git a/tests/fixtures/addon/commands/addon-override-intentional.js b/tests/fixtures/addon/commands/addon-override-intentional.js index a5dba75fce..a6683b223a 100644 --- a/tests/fixtures/addon/commands/addon-override-intentional.js +++ b/tests/fixtures/addon/commands/addon-override-intentional.js @@ -1,4 +1,4 @@ -var Command = require('../../../../lib/models/command'); +const Command = require('../../../../lib/models/command'); function Addon() { this.name = "Other Ember CLI Addon Command To Test Intentional Core Command Override" diff --git a/tests/fixtures/addon/component-with-template/tests/acceptance/main-test.js b/tests/fixtures/addon/component-with-template/tests/acceptance/main-test.js deleted file mode 100644 index 2910fed762..0000000000 --- a/tests/fixtures/addon/component-with-template/tests/acceptance/main-test.js +++ /dev/null @@ -1,30 +0,0 @@ -import Ember from 'ember'; -import startApp from '../helpers/start-app'; -import { module, test } from 'qunit'; - -module('Acceptance', { - beforeEach: function() { - this.application = startApp(); - }, - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - -test('renders properly', function(assert) { - visit('/'); - - andThen(function() { - var element = find('.basic-thing'); - assert.equal(element.first().text().trim(), 'WOOT!!'); - }); -}); - -test('renders imported component', function(assert) { - visit('/'); - - andThen(function() { - var element = find('.second-thing'); - assert.equal(element.first().text().trim(), 'SECOND!!'); - }); -}); diff --git a/tests/fixtures/addon/component-with-template/tests/dummy/app/templates/application.hbs b/tests/fixtures/addon/component-with-template/tests/dummy/app/templates/application.hbs deleted file mode 100644 index 885261e680..0000000000 --- a/tests/fixtures/addon/component-with-template/tests/dummy/app/templates/application.hbs +++ /dev/null @@ -1,2 +0,0 @@ -{{#basic-thing}}WOOT!!{{/basic-thing}} -{{#second-thing}}SECOND!!{{/second-thing}} diff --git a/tests/fixtures/addon/content-for-head/index.js b/tests/fixtures/addon/content-for-head/index.js deleted file mode 100644 index d83908df32..0000000000 --- a/tests/fixtures/addon/content-for-head/index.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - name: 'content-for-head', - - contentFor: function(type, config) { - return '"SOME AWESOME STUFF"'; - } -}; diff --git a/tests/fixtures/addon/defaults/.ember-cli b/tests/fixtures/addon/defaults/.ember-cli new file mode 100644 index 0000000000..8bd6e76fa2 --- /dev/null +++ b/tests/fixtures/addon/defaults/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": "strict", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": "strict" +} diff --git a/tests/fixtures/addon/defaults/.github/workflows/ci.yml b/tests/fixtures/addon/defaults/.github/workflows/ci.yml new file mode 100644 index 0000000000..00b9738675 --- /dev/null +++ b/tests/fixtures/addon/defaults/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: "Tests" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + - name: Install Dependencies + run: npm ci + - name: Lint + run: npm run lint + - name: Run Tests + run: npm run test:ember + + floating: + name: "Floating Dependencies" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + - name: Install Dependencies + run: npm install --no-shrinkwrap + - name: Run Tests + run: npm run test:ember + + try-scenarios: + name: ${{ matrix.try-scenario }} + runs-on: ubuntu-latest + needs: "test" + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + try-scenario: + - ember-lts-5.8 + - ember-lts-5.12 + - ember-release + - ember-beta + - ember-canary + - embroider-safe + - embroider-optimized + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + - name: Install Dependencies + run: npm ci + - name: Run Tests + run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} diff --git a/tests/fixtures/addon/defaults/CONTRIBUTING.md b/tests/fixtures/addon/defaults/CONTRIBUTING.md new file mode 100644 index 0000000000..8f16173f2a --- /dev/null +++ b/tests/fixtures/addon/defaults/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# How To Contribute + +## Installation + +- `git clone ` +- `cd foo` +- `npm install` + +## Linting + +- `npm run lint` +- `npm run lint:fix` + +## Running tests + +- `npm run test` – Runs the test suite on the current Ember version +- `npm run test:ember -- --server` – Runs the test suite in "watch mode" +- `npm run test:ember-compatibility` – Runs the test suite against multiple Ember versions + +## Running the dummy application + +- `npm run start` +- Visit the dummy application at [http://localhost:4200](http://localhost:4200). + +For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). diff --git a/tests/fixtures/addon/defaults/README.md b/tests/fixtures/addon/defaults/README.md new file mode 100644 index 0000000000..8035eafa5e --- /dev/null +++ b/tests/fixtures/addon/defaults/README.md @@ -0,0 +1,37 @@ +# foo + +[Short description of the addon.] + +## Compatibility + +- Ember.js v5.8 or above +- Ember CLI v5.8 or above +- Node.js v20 or above + +## Installation + +``` +ember install foo +``` + +## Usage + +[Longer description of how to use the addon in apps.] + +> TODO: Document the package's public API. +> +> For each public api (including components, helpers, modifiers, and other apis) include: +> +> - The import path for a consumer (e.g. `import MyAddonsComponent from 'my-addon/components/my-addons-component'`) +> - What it does +> - Parameters/options +> - Return value +> - Example usage + +## Contributing + +See the [Contributing](CONTRIBUTING.md) guide for details. + +## License + +This project is licensed under the [MIT License](LICENSE.md). diff --git a/tests/fixtures/addon/defaults/package.json b/tests/fixtures/addon/defaults/package.json new file mode 100644 index 0000000000..eec961629b --- /dev/null +++ b/tests/fixtures/addon/defaults/package.json @@ -0,0 +1,93 @@ +{ + "name": "foo", + "version": "0.0.0", + "description": "The default blueprint for ember-cli addons.", + "keywords": [ + "ember-addon" + ], + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test", + "test:ember-compatibility": "ember try:each" + }, + "dependencies": { + "@babel/core": "^7.29.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-template-imports": "^4.4.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@embroider/test-setup": "^4.0.0", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-load-initializers": "^3.0.1", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-source-channel-url": "^3.0.0", + "ember-template-lint": "^6.1.0", + "ember-try": "^4.0.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "peerDependencies": { + "ember-source": ">= 4.0.0" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "configPath": "tests/dummy/config" + } +} diff --git a/tests/fixtures/addon/defaults/tests/dummy/app/templates/application.gjs b/tests/fixtures/addon/defaults/tests/dummy/app/templates/application.gjs new file mode 100644 index 0000000000..606df22607 --- /dev/null +++ b/tests/fixtures/addon/defaults/tests/dummy/app/templates/application.gjs @@ -0,0 +1,9 @@ +import pageTitle from 'ember-page-title/helpers/page-title'; + + diff --git a/tests/fixtures/addon/defaults/tests/dummy/config/ember-cli-update.json b/tests/fixtures/addon/defaults/tests/dummy/config/ember-cli-update.json new file mode 100644 index 0000000000..8f4bcaf3ff --- /dev/null +++ b/tests/fixtures/addon/defaults/tests/dummy/config/ember-cli-update.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "isBaseBlueprint": true, + "options": [ + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/addon/defaults/tests/dummy/config/ember-try.js b/tests/fixtures/addon/defaults/tests/dummy/config/ember-try.js new file mode 100644 index 0000000000..386764ddbc --- /dev/null +++ b/tests/fixtures/addon/defaults/tests/dummy/config/ember-try.js @@ -0,0 +1,54 @@ +'use strict'; + +const getChannelURL = require('ember-source-channel-url'); +const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); + +module.exports = async function () { + return { + packageManager: 'npm', + scenarios: [ + { + name: 'ember-lts-5.8', + npm: { + devDependencies: { + 'ember-source': '~5.8.0', + }, + }, + }, + { + name: 'ember-lts-5.12', + npm: { + devDependencies: { + 'ember-source': '~5.12.0', + }, + }, + }, + { + name: 'ember-release', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('release'), + }, + }, + }, + { + name: 'ember-beta', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('beta'), + }, + }, + }, + { + name: 'ember-canary', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('canary'), + }, + }, + }, + embroiderSafe(), + embroiderOptimized(), + ], + }; +}; diff --git a/tests/fixtures/addon/developing-addon/addon/components/simple-component.js b/tests/fixtures/addon/developing-addon/addon/components/simple-component.js new file mode 100644 index 0000000000..dde1898656 --- /dev/null +++ b/tests/fixtures/addon/developing-addon/addon/components/simple-component.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; +import layout from '../templates/components/simple-component'; + +export default Component.extend({ + layout +}); \ No newline at end of file diff --git a/tests/fixtures/addon/developing-addon/addon/templates/components/simple-component.hbs b/tests/fixtures/addon/developing-addon/addon/templates/components/simple-component.hbs new file mode 100644 index 0000000000..bb3feb913a --- /dev/null +++ b/tests/fixtures/addon/developing-addon/addon/templates/components/simple-component.hbs @@ -0,0 +1 @@ +

This is a simple component

diff --git a/tests/fixtures/addon/developing-addon/app/components/simple-component.js b/tests/fixtures/addon/developing-addon/app/components/simple-component.js new file mode 100644 index 0000000000..2e2e3f4cb7 --- /dev/null +++ b/tests/fixtures/addon/developing-addon/app/components/simple-component.js @@ -0,0 +1 @@ +export { default } from 'developing-addon/components/simple-component'; diff --git a/tests/fixtures/addon/developing-addon/index.js b/tests/fixtures/addon/developing-addon/index.js new file mode 100644 index 0000000000..57cf2c24dc --- /dev/null +++ b/tests/fixtures/addon/developing-addon/index.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + name: require('./package').name, + + isDevelopingAddon() { + return true; + } +}; diff --git a/tests/fixtures/addon/developing-addon/package.json b/tests/fixtures/addon/developing-addon/package.json new file mode 100644 index 0000000000..e2f60c278a --- /dev/null +++ b/tests/fixtures/addon/developing-addon/package.json @@ -0,0 +1,10 @@ +{ + "name": "developing-addon", + "version": "0.0.1", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "ember-cli-htmlbars": "latest" + } +} diff --git a/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/index.js b/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/index.js index 3115429572..103c705090 100644 --- a/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/index.js +++ b/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/index.js @@ -1,6 +1,7 @@ module.exports = { - name: 'ember-foo-env-addon', - isEnabled: function() { + name: require('./package').name, + + isEnabled() { return this.app.env === 'foo'; } }; diff --git a/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/package.json b/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/package.json index 7a3eea0c3a..09c121d89f 100644 --- a/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/package.json +++ b/tests/fixtures/addon/env-addons/node_modules/ember-foo-env-addon/package.json @@ -5,4 +5,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/index.js b/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/index.js index 827237883a..2e1d1d8d5f 100644 --- a/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/index.js +++ b/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember Random Addon' + name: require('./package').name }; diff --git a/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/package.json b/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/package.json index a3a5093652..9d91e04fca 100644 --- a/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/package.json +++ b/tests/fixtures/addon/env-addons/node_modules/ember-random-addon/package.json @@ -5,4 +5,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/addon/env-addons/package.json b/tests/fixtures/addon/env-addons/package.json index 38b869f058..7e5f6e2439 100644 --- a/tests/fixtures/addon/env-addons/package.json +++ b/tests/fixtures/addon/env-addons/package.json @@ -3,7 +3,7 @@ "private": true, "devDependencies": { "ember-foo-env-addon": "latest", - "ember-random-addon": "latest" + "ember-random-addon": "latest", + "loader.js": "latest" } } - diff --git a/tests/fixtures/addon/eslint.config.mjs b/tests/fixtures/addon/eslint.config.mjs new file mode 100644 index 0000000000..a29e5cd3bd --- /dev/null +++ b/tests/fixtures/addon/eslint.config.mjs @@ -0,0 +1,126 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import globals from 'globals'; +import js from '@eslint/js'; + +import ember from 'eslint-plugin-ember/recommended'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const esmParserOptions = { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], + ], + }, +}; + +export default [ + js.configs.recommended, + eslintConfigPrettier, + ember.configs.base, + ember.configs.gjs, + /** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: esmParserOptions, + globals: { + ...globals.browser, + }, + }, + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: esmParserOptions, + globals: { + ...globals.node, + }, + }, + }, +]; diff --git a/tests/fixtures/addon/external-dependency/index.coffee b/tests/fixtures/addon/external-dependency/index.coffee new file mode 100644 index 0000000000..c4a5a2e408 --- /dev/null +++ b/tests/fixtures/addon/external-dependency/index.coffee @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + name: 'external-dependency', + + isDevelopingAddon() { + return true; + } +}; diff --git a/tests/fixtures/addon/external-dependency/index.js b/tests/fixtures/addon/external-dependency/index.js new file mode 100644 index 0000000000..57cf2c24dc --- /dev/null +++ b/tests/fixtures/addon/external-dependency/index.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + name: require('./package').name, + + isDevelopingAddon() { + return true; + } +}; diff --git a/tests/fixtures/addon/external-dependency/package.json b/tests/fixtures/addon/external-dependency/package.json new file mode 100644 index 0000000000..95da82eaba --- /dev/null +++ b/tests/fixtures/addon/external-dependency/package.json @@ -0,0 +1,12 @@ +{ + "name": "external-dependency", + "version": "0.0.1", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "ember-cli-string-utils": "latest", + "@pnpm/find-workspace-dir": "latest", + "ember-cli-blueprint-test-helpers": "latest" + } +} diff --git a/tests/fixtures/addon/external-dependency/some/other/path.js b/tests/fixtures/addon/external-dependency/some/other/path.js new file mode 100644 index 0000000000..c4a5a2e408 --- /dev/null +++ b/tests/fixtures/addon/external-dependency/some/other/path.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + name: 'external-dependency', + + isDevelopingAddon() { + return true; + } +}; diff --git a/tests/fixtures/addon/invalid-constructor/lib/ember-invalid-addon/index.js b/tests/fixtures/addon/invalid-constructor/lib/ember-invalid-addon/index.js new file mode 100644 index 0000000000..cd016b721f --- /dev/null +++ b/tests/fixtures/addon/invalid-constructor/lib/ember-invalid-addon/index.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function() { + throw new Error('invalid'); +}; diff --git a/tests/fixtures/addon/invalid-constructor/lib/ember-invalid-addon/package.json b/tests/fixtures/addon/invalid-constructor/lib/ember-invalid-addon/package.json new file mode 100644 index 0000000000..35b6eccd8e --- /dev/null +++ b/tests/fixtures/addon/invalid-constructor/lib/ember-invalid-addon/package.json @@ -0,0 +1,6 @@ +{ + "name": "ember-invalid-addon", + "keywords": [ + "ember-addon" + ] +} diff --git a/tests/fixtures/addon/invalid-constructor/package.json b/tests/fixtures/addon/invalid-constructor/package.json new file mode 100644 index 0000000000..88e02c3eae --- /dev/null +++ b/tests/fixtures/addon/invalid-constructor/package.json @@ -0,0 +1,9 @@ +{ + "name": "test-project", + "private": true, + "ember-addon": { + "paths": [ + "./lib/ember-invalid-addon" + ] + } +} diff --git a/tests/fixtures/addon/kitchen-sink/addon-test-support/helper.js b/tests/fixtures/addon/kitchen-sink/addon-test-support/helper.js new file mode 100644 index 0000000000..259c58832a --- /dev/null +++ b/tests/fixtures/addon/kitchen-sink/addon-test-support/helper.js @@ -0,0 +1,3 @@ +export default function truthyHelper() { + return true; +} diff --git a/tests/fixtures/addon/component-with-template/addon/components/basic-thing.js b/tests/fixtures/addon/kitchen-sink/addon/components/basic-thing.js similarity index 50% rename from tests/fixtures/addon/component-with-template/addon/components/basic-thing.js rename to tests/fixtures/addon/kitchen-sink/addon/components/basic-thing.js index 7ba03a3b11..f99bee3ff3 100644 --- a/tests/fixtures/addon/component-with-template/addon/components/basic-thing.js +++ b/tests/fixtures/addon/kitchen-sink/addon/components/basic-thing.js @@ -1,6 +1,6 @@ -import Ember from 'ember'; import template from '../templates/components/basic-thing'; +import Component from '@ember/component'; -export default Ember.Component.extend({ +export default Component.extend({ layout: template -}); +}); \ No newline at end of file diff --git a/tests/fixtures/addon/component-with-template/addon/index.js b/tests/fixtures/addon/kitchen-sink/addon/index.js similarity index 100% rename from tests/fixtures/addon/component-with-template/addon/index.js rename to tests/fixtures/addon/kitchen-sink/addon/index.js diff --git a/tests/fixtures/addon/kitchen-sink/addon/styles/app.css b/tests/fixtures/addon/kitchen-sink/addon/styles/app.css new file mode 100644 index 0000000000..441f327ae2 --- /dev/null +++ b/tests/fixtures/addon/kitchen-sink/addon/styles/app.css @@ -0,0 +1 @@ +/* addon/styles/app.css is present */ diff --git a/tests/fixtures/addon/component-with-template/addon/templates/components/basic-thing.hbs b/tests/fixtures/addon/kitchen-sink/addon/templates/components/basic-thing.hbs similarity index 100% rename from tests/fixtures/addon/component-with-template/addon/templates/components/basic-thing.hbs rename to tests/fixtures/addon/kitchen-sink/addon/templates/components/basic-thing.hbs diff --git a/tests/fixtures/addon/component-with-template/app/components/basic-thing.js b/tests/fixtures/addon/kitchen-sink/app/components/basic-thing.js similarity index 98% rename from tests/fixtures/addon/component-with-template/app/components/basic-thing.js rename to tests/fixtures/addon/kitchen-sink/app/components/basic-thing.js index ff2c1ae8c9..0d7067e65e 100644 --- a/tests/fixtures/addon/component-with-template/app/components/basic-thing.js +++ b/tests/fixtures/addon/kitchen-sink/app/components/basic-thing.js @@ -1,3 +1,2 @@ import BasicThing from 'some-cool-addon/components/basic-thing'; - export default BasicThing; diff --git a/tests/fixtures/addon/kitchen-sink/index.js b/tests/fixtures/addon/kitchen-sink/index.js new file mode 100644 index 0000000000..f5a8aad507 --- /dev/null +++ b/tests/fixtures/addon/kitchen-sink/index.js @@ -0,0 +1,9 @@ +module.exports = { + name: require('./package').name, + + contentFor(type, config) { + if (type === 'head') { + return '"SOME AWESOME STUFF"'; + } + } +}; diff --git a/tests/fixtures/addon/kitchen-sink/tests/acceptance/main-test.js b/tests/fixtures/addon/kitchen-sink/tests/acceptance/main-test.js new file mode 100644 index 0000000000..71ef67035b --- /dev/null +++ b/tests/fixtures/addon/kitchen-sink/tests/acceptance/main-test.js @@ -0,0 +1,23 @@ +import { setupApplicationTest } from 'ember-qunit'; +import { visit } from '@ember/test-helpers'; +import truthyHelper from 'some-cool-addon/test-support/helper'; +import { module, test } from 'qunit'; + +module('Acceptance', function(hooks) { + setupApplicationTest(hooks); + + test('renders properly', async function (assert) { + await visit('/'); + + var element = this.element.querySelector('.basic-thing'); + assert.strictEqual(element.textContent.trim(), 'WOOT!!'); + assert.ok(truthyHelper(), 'addon-test-support helper'); + }); + + test('renders imported component', async function (assert) { + await visit('/'); + + var element = this.element.querySelector('.second-thing'); + assert.strictEqual(element.textContent.trim(), 'SECOND!!'); + }); +}); diff --git a/tests/fixtures/addon/component-with-template/tests/dummy/app/components/second-thing.js b/tests/fixtures/addon/kitchen-sink/tests/dummy/app/components/second-thing.js similarity index 51% rename from tests/fixtures/addon/component-with-template/tests/dummy/app/components/second-thing.js rename to tests/fixtures/addon/kitchen-sink/tests/dummy/app/components/second-thing.js index 25edfaceeb..21c3170ef2 100644 --- a/tests/fixtures/addon/component-with-template/tests/dummy/app/components/second-thing.js +++ b/tests/fixtures/addon/kitchen-sink/tests/dummy/app/components/second-thing.js @@ -1,4 +1,5 @@ -import BasicThing from "some-cool-addon"; +import BasicThing from 'some-cool-addon/components/basic-thing'; + export default BasicThing.extend({ classNames: ['second-thing'] }); diff --git a/tests/fixtures/addon/kitchen-sink/tests/dummy/app/templates/application.hbs b/tests/fixtures/addon/kitchen-sink/tests/dummy/app/templates/application.hbs new file mode 100644 index 0000000000..50b5fd9f4e --- /dev/null +++ b/tests/fixtures/addon/kitchen-sink/tests/dummy/app/templates/application.hbs @@ -0,0 +1,2 @@ +WOOT!! +SECOND!! diff --git a/tests/fixtures/addon/with-dummy-public/tests/dummy/public/robots.txt b/tests/fixtures/addon/kitchen-sink/tests/dummy/public/robots.txt similarity index 100% rename from tests/fixtures/addon/with-dummy-public/tests/dummy/public/robots.txt rename to tests/fixtures/addon/kitchen-sink/tests/dummy/public/robots.txt diff --git a/tests/fixtures/addon/nested-addon-main/src/addon/components/simple-component.js b/tests/fixtures/addon/nested-addon-main/src/addon/components/simple-component.js new file mode 100644 index 0000000000..dde1898656 --- /dev/null +++ b/tests/fixtures/addon/nested-addon-main/src/addon/components/simple-component.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; +import layout from '../templates/components/simple-component'; + +export default Component.extend({ + layout +}); \ No newline at end of file diff --git a/tests/fixtures/addon/nested-addon-main/src/addon/templates/components/simple-component.hbs b/tests/fixtures/addon/nested-addon-main/src/addon/templates/components/simple-component.hbs new file mode 100644 index 0000000000..bb3feb913a --- /dev/null +++ b/tests/fixtures/addon/nested-addon-main/src/addon/templates/components/simple-component.hbs @@ -0,0 +1 @@ +

This is a simple component

diff --git a/tests/fixtures/addon/nested-addon-main/src/main.js b/tests/fixtures/addon/nested-addon-main/src/main.js new file mode 100644 index 0000000000..3ad817af82 --- /dev/null +++ b/tests/fixtures/addon/nested-addon-main/src/main.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + name: require('../package').name, +}; diff --git a/tests/fixtures/addon/pnpm/.github/workflows/ci.yml b/tests/fixtures/addon/pnpm/.github/workflows/ci.yml new file mode 100644 index 0000000000..4d09f7b60e --- /dev/null +++ b/tests/fixtures/addon/pnpm/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: "Tests" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + - name: Install Dependencies + run: pnpm install --frozen-lockfile + - name: Lint + run: pnpm lint + - name: Run Tests + run: pnpm test:ember + + floating: + name: "Floating Dependencies" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + - name: Install Dependencies + run: pnpm install --no-lockfile + - name: Run Tests + run: pnpm test:ember + + try-scenarios: + name: ${{ matrix.try-scenario }} + runs-on: ubuntu-latest + needs: "test" + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + try-scenario: + - ember-lts-5.8 + - ember-lts-5.12 + - ember-release + - ember-beta + - ember-canary + - embroider-safe + - embroider-optimized + + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + - name: Install Dependencies + run: pnpm install --frozen-lockfile + - name: Run Tests + run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} diff --git a/tests/fixtures/addon/pnpm/.npmrc b/tests/fixtures/addon/pnpm/.npmrc new file mode 100644 index 0000000000..a6eb43de64 --- /dev/null +++ b/tests/fixtures/addon/pnpm/.npmrc @@ -0,0 +1,8 @@ +# these settings allow us to be good stewards of the ecosystem +# end-consumers can flip these settings in their own projects, +# but library authors should not allow for less-than-strictest +# behavior. +# +# For more information: https://pnpm.io/npmrc +auto-install-peers=false +resolve-peers-from-workspace-root=false diff --git a/tests/fixtures/addon/pnpm/CONTRIBUTING.md b/tests/fixtures/addon/pnpm/CONTRIBUTING.md new file mode 100644 index 0000000000..e276dbf020 --- /dev/null +++ b/tests/fixtures/addon/pnpm/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# How To Contribute + +## Installation + +- `git clone ` +- `cd foo` +- `pnpm install` + +## Linting + +- `pnpm lint` +- `pnpm lint:fix` + +## Running tests + +- `pnpm test` – Runs the test suite on the current Ember version +- `pnpm test:ember --server` – Runs the test suite in "watch mode" +- `pnpm test:ember-compatibility` – Runs the test suite against multiple Ember versions + +## Running the dummy application + +- `pnpm start` +- Visit the dummy application at [http://localhost:4200](http://localhost:4200). + +For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). diff --git a/tests/fixtures/addon/pnpm/README.md b/tests/fixtures/addon/pnpm/README.md new file mode 100644 index 0000000000..8035eafa5e --- /dev/null +++ b/tests/fixtures/addon/pnpm/README.md @@ -0,0 +1,37 @@ +# foo + +[Short description of the addon.] + +## Compatibility + +- Ember.js v5.8 or above +- Ember CLI v5.8 or above +- Node.js v20 or above + +## Installation + +``` +ember install foo +``` + +## Usage + +[Longer description of how to use the addon in apps.] + +> TODO: Document the package's public API. +> +> For each public api (including components, helpers, modifiers, and other apis) include: +> +> - The import path for a consumer (e.g. `import MyAddonsComponent from 'my-addon/components/my-addons-component'`) +> - What it does +> - Parameters/options +> - Return value +> - Example usage + +## Contributing + +See the [Contributing](CONTRIBUTING.md) guide for details. + +## License + +This project is licensed under the [MIT License](LICENSE.md). diff --git a/tests/fixtures/addon/pnpm/package.json b/tests/fixtures/addon/pnpm/package.json new file mode 100644 index 0000000000..5624e834ef --- /dev/null +++ b/tests/fixtures/addon/pnpm/package.json @@ -0,0 +1,94 @@ +{ + "name": "foo", + "version": "0.0.0", + "description": "The default blueprint for ember-cli addons.", + "keywords": [ + "ember-addon" + ], + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"", + "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test", + "test:ember-compatibility": "ember try:each" + }, + "dependencies": { + "@babel/core": "^7.29.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-template-imports": "^4.4.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@embroider/test-setup": "^4.0.0", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-load-initializers": "^3.0.1", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-source-channel-url": "^3.0.0", + "ember-template-lint": "^6.1.0", + "ember-try": "^4.0.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "peerDependencies": { + "ember-source": ">= 4.0.0" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "configPath": "tests/dummy/config" + } +} diff --git a/tests/fixtures/addon/pnpm/tests/dummy/app/templates/application.gjs b/tests/fixtures/addon/pnpm/tests/dummy/app/templates/application.gjs new file mode 100644 index 0000000000..196d2c4b8b --- /dev/null +++ b/tests/fixtures/addon/pnpm/tests/dummy/app/templates/application.gjs @@ -0,0 +1,12 @@ +import pageTitle from 'ember-page-title/helpers/page-title'; +import WelcomePage from 'ember-welcome-page/components/welcome-page'; + + diff --git a/tests/fixtures/addon/pnpm/tests/dummy/config/ember-cli-update.json b/tests/fixtures/addon/pnpm/tests/dummy/config/ember-cli-update.json new file mode 100644 index 0000000000..663d7f1008 --- /dev/null +++ b/tests/fixtures/addon/pnpm/tests/dummy/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "isBaseBlueprint": true, + "options": [ + "--welcome", + "--pnpm", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/addon/pnpm/tests/dummy/config/ember-try.js b/tests/fixtures/addon/pnpm/tests/dummy/config/ember-try.js new file mode 100644 index 0000000000..0485883868 --- /dev/null +++ b/tests/fixtures/addon/pnpm/tests/dummy/config/ember-try.js @@ -0,0 +1,54 @@ +'use strict'; + +const getChannelURL = require('ember-source-channel-url'); +const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); + +module.exports = async function () { + return { + packageManager: 'pnpm', + scenarios: [ + { + name: 'ember-lts-5.8', + npm: { + devDependencies: { + 'ember-source': '~5.8.0', + }, + }, + }, + { + name: 'ember-lts-5.12', + npm: { + devDependencies: { + 'ember-source': '~5.12.0', + }, + }, + }, + { + name: 'ember-release', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('release'), + }, + }, + }, + { + name: 'ember-beta', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('beta'), + }, + }, + }, + { + name: 'ember-canary', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('canary'), + }, + }, + }, + embroiderSafe(), + embroiderOptimized(), + ], + }; +}; diff --git a/tests/fixtures/addon/pod-templates-only/addon/components/my-component/component.js b/tests/fixtures/addon/pod-templates-only/addon/components/my-component/component.js deleted file mode 100644 index b584e24b4d..0000000000 --- a/tests/fixtures/addon/pod-templates-only/addon/components/my-component/component.js +++ /dev/null @@ -1,6 +0,0 @@ -import Ember from 'ember'; -import template from './template'; - -export default Ember.Component.extend({ - layout: template -}); diff --git a/tests/fixtures/addon/pod-templates-only/addon/components/my-component/template.hbs b/tests/fixtures/addon/pod-templates-only/addon/components/my-component/template.hbs deleted file mode 100644 index 39ca035984..0000000000 --- a/tests/fixtures/addon/pod-templates-only/addon/components/my-component/template.hbs +++ /dev/null @@ -1 +0,0 @@ -MY-COMPONENT-TEMPLATE-CONTENT diff --git a/tests/fixtures/addon/pod-templates-only/app/components/my-component.js b/tests/fixtures/addon/pod-templates-only/app/components/my-component.js deleted file mode 100644 index 56a7a290c0..0000000000 --- a/tests/fixtures/addon/pod-templates-only/app/components/my-component.js +++ /dev/null @@ -1 +0,0 @@ -import { default } from 'some-cool-addon/components/my-component/component'; diff --git a/tests/fixtures/addon/shared-package/base/node_modules/blah-blah/package.json b/tests/fixtures/addon/shared-package/base/node_modules/blah-blah/package.json deleted file mode 100644 index c557aff546..0000000000 --- a/tests/fixtures/addon/shared-package/base/node_modules/blah-blah/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "blah-blah" -} diff --git a/tests/fixtures/addon/shared-package/base/node_modules/foo-bar/package.json b/tests/fixtures/addon/shared-package/base/node_modules/foo-bar/package.json deleted file mode 100644 index 0efc069b1a..0000000000 --- a/tests/fixtures/addon/shared-package/base/node_modules/foo-bar/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "foo-bar" -} diff --git a/tests/fixtures/addon/shared-package/base/node_modules/invalid-package/package.json b/tests/fixtures/addon/shared-package/base/node_modules/invalid-package/package.json deleted file mode 100644 index 0efc069b1a..0000000000 --- a/tests/fixtures/addon/shared-package/base/node_modules/invalid-package/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "foo-bar" -} diff --git a/tests/fixtures/addon/shared-package/node_modules/dev-foo-bar/package.json b/tests/fixtures/addon/shared-package/node_modules/dev-foo-bar/package.json deleted file mode 100644 index aa43595d48..0000000000 --- a/tests/fixtures/addon/shared-package/node_modules/dev-foo-bar/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "dev-foo-bar" -} diff --git a/tests/fixtures/addon/simple/bower.json b/tests/fixtures/addon/simple/bower.json deleted file mode 100644 index 071262180d..0000000000 --- a/tests/fixtures/addon/simple/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "test-project", - "dependencies": { - "jquery": "^1.11.1", - "ember": "1.7.0", - "ember-data": "1.0.0-beta.10", - "ember-resolver": "~0.1.7", - "loader.js": "ember-cli/loader.js#1.0.1", - "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", - "ember-cli-test-loader": "rwjblue/ember-cli-test-loader#0.0.4", - "ember-load-initializers": "ember-cli/ember-load-initializers#0.0.2" - }, - "devDependencies": { - "ember-qunit": "0.1.8", - "ember-qunit-notifications": "0.0.4", - "qunit": "~1.15.0" - } -} diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/index.js b/tests/fixtures/addon/simple/lib/ember-super-button/index.js index 7c367b1e62..0ca063d427 100644 --- a/tests/fixtures/addon/simple/lib/ember-super-button/index.js +++ b/tests/fixtures/addon/simple/lib/ember-super-button/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember Super Button', + name: require('./package').name, }; diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-ng/index.js b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-ng/index.js index 4e13431ca8..0ca063d427 100644 --- a/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-ng/index.js +++ b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-ng/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember Ng' + name: require('./package').name, }; diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/lib/main.js b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/lib/main.js new file mode 100644 index 0000000000..3ad817af82 --- /dev/null +++ b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/lib/main.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + name: require('../package').name, +}; diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/package.json b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/package.json new file mode 100644 index 0000000000..6cf2ccf370 --- /dev/null +++ b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/package.json @@ -0,0 +1,9 @@ +{ + "name": "ember-with-addon-main", + "keywords": [ + "ember-addon" + ], + "ember-addon": { + "main": "lib/main.js" + } +} diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/node_modules/ember-yagni/index.js b/tests/fixtures/addon/simple/lib/ember-super-button/node_modules/ember-yagni/index.js index 5d2fb524ba..2e1d1d8d5f 100644 --- a/tests/fixtures/addon/simple/lib/ember-super-button/node_modules/ember-yagni/index.js +++ b/tests/fixtures/addon/simple/lib/ember-super-button/node_modules/ember-yagni/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember Yagni' + name: require('./package').name }; diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/package.json b/tests/fixtures/addon/simple/lib/ember-super-button/package.json index d493c4084b..1ff87341d5 100644 --- a/tests/fixtures/addon/simple/lib/ember-super-button/package.json +++ b/tests/fixtures/addon/simple/lib/ember-super-button/package.json @@ -4,7 +4,10 @@ "ember-addon" ], "ember-addon": { - "paths": ["./lib/ember-ng"] + "paths": [ + "./lib/ember-ng", + "./lib/ember-with-addon-main" + ] }, "dependencies": { "ember-yagni": "0" diff --git a/tests/fixtures/addon/simple/node_modules/.gitignore b/tests/fixtures/addon/simple/node_modules/.gitignore new file mode 100644 index 0000000000..af25caa4e1 --- /dev/null +++ b/tests/fixtures/addon/simple/node_modules/.gitignore @@ -0,0 +1 @@ +symlinked-addon \ No newline at end of file diff --git a/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/index.js b/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/index.js index e12bc688e5..2794117551 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/index.js +++ b/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/index.js @@ -1,3 +1,3 @@ module.exports = { - name: 'Ember Addon With Dependencies' + name: require('./package').name } diff --git a/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/package.json b/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/package.json index 8f8ff27718..dfbd76b134 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-addon-with-dependencies/package.json @@ -10,4 +10,3 @@ "something-else": "latest" } } - diff --git a/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/index.js b/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/index.js index 53da9e139f..2e1d1d8d5f 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/index.js +++ b/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember After Addon' + name: require('./package').name }; diff --git a/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/package.json b/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/package.json index 8ccf86be09..3131824918 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-after-blueprint-addon/package.json @@ -9,4 +9,3 @@ "after": "ember-random-addon" } } - diff --git a/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/index.js b/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/index.js index 5aa2e57b53..2e1d1d8d5f 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/index.js +++ b/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember Before Addon' + name: require('./package').name }; diff --git a/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/package.json b/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/package.json index b9079c2d2c..700f55afdb 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-before-blueprint-addon/package.json @@ -9,4 +9,3 @@ "before": "ember-random-addon" } } - diff --git a/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/index.js b/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/index.js index 7bc11962c1..2794117551 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/index.js +++ b/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/index.js @@ -1,3 +1,3 @@ module.exports = { - name: "ember-devDeps-addon" + name: require('./package').name } diff --git a/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/node_modules/ember-cli-nested/package.json b/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/node_modules/ember-cli-nested/package.json index 4f30a53ed7..8d38339d88 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/node_modules/ember-cli-nested/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/node_modules/ember-cli-nested/package.json @@ -6,4 +6,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/package.json b/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/package.json index 03f5d49752..8ce30e57c9 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-devDeps-addon/package.json @@ -9,4 +9,3 @@ "ember-cli-nested": "*" } } - diff --git a/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/index.js b/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/index.js index 50739aefa8..2794117551 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/index.js +++ b/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/index.js @@ -1,3 +1,3 @@ module.exports = { - name: 'Ember CLI Generated with export' + name: require('./package').name } diff --git a/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/package.json b/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/package.json index 0868ac9333..617b4049d2 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-generated-with-export-addon/package.json @@ -6,4 +6,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/ember-index.js b/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/ember-index.js index 5d6f7d0842..2e1d1d8d5f 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/ember-index.js +++ b/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/ember-index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember Non Root Addon' + name: require('./package').name }; diff --git a/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/package.json b/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/package.json index 858982918f..061f488109 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-non-root-addon/package.json @@ -11,4 +11,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/addon/simple/node_modules/ember-random-addon/index.js b/tests/fixtures/addon/simple/node_modules/ember-random-addon/index.js index 827237883a..2e1d1d8d5f 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-random-addon/index.js +++ b/tests/fixtures/addon/simple/node_modules/ember-random-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'Ember Random Addon' + name: require('./package').name }; diff --git a/tests/fixtures/addon/simple/node_modules/ember-random-addon/package.json b/tests/fixtures/addon/simple/node_modules/ember-random-addon/package.json index 49d68fa7ac..9d79fd8a6b 100644 --- a/tests/fixtures/addon/simple/node_modules/ember-random-addon/package.json +++ b/tests/fixtures/addon/simple/node_modules/ember-random-addon/package.json @@ -6,4 +6,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/addon/simple/node_modules/non-ember-thingy/package.json b/tests/fixtures/addon/simple/node_modules/non-ember-thingy/package.json index de31cd0ca0..a3e7b0e44a 100644 --- a/tests/fixtures/addon/simple/node_modules/non-ember-thingy/package.json +++ b/tests/fixtures/addon/simple/node_modules/non-ember-thingy/package.json @@ -2,5 +2,5 @@ "name": "non-ember-thingy", "private": true, "version": "0.0.0", - "keywords": [ ] + "keywords": [] } diff --git a/tests/fixtures/addon/simple/node_modules/something-else/package.json b/tests/fixtures/addon/simple/node_modules/something-else/package.json index a3f665a142..4c6bf6f803 100644 --- a/tests/fixtures/addon/simple/node_modules/something-else/package.json +++ b/tests/fixtures/addon/simple/node_modules/something-else/package.json @@ -3,4 +3,3 @@ "version": "0.0.0", "private": true } - diff --git a/tests/fixtures/addon/simple/package.json b/tests/fixtures/addon/simple/package.json index cd6d580c96..0aa0ce31f6 100644 --- a/tests/fixtures/addon/simple/package.json +++ b/tests/fixtures/addon/simple/package.json @@ -5,9 +5,13 @@ "something-else": "latest" }, "ember-addon": { - "paths": ["./lib/ember-super-button"] + "paths": [ + "./lib/ember-super-button", + "./lib/ember-super-button/lib/ember-with-addon-main" + ] }, "devDependencies": { + "ember-resolver": "^13.2.0", "ember-cli": "latest", "ember-random-addon": "latest", "ember-non-root-addon": "latest", @@ -16,7 +20,7 @@ "ember-before-blueprint-addon": "latest", "ember-after-blueprint-addon": "latest", "ember-devDeps-addon": "latest", - "ember-addon-with-dependencies": "latest" + "ember-addon-with-dependencies": "latest", + "loader.js": "latest" } } - diff --git a/blueprints/blueprint/files/blueprints/__name__/files/.gitkeep b/tests/fixtures/addon/simple/tests/.gitkeep similarity index 100% rename from blueprints/blueprint/files/blueprints/__name__/files/.gitkeep rename to tests/fixtures/addon/simple/tests/.gitkeep diff --git a/tests/fixtures/bower-directory-tests/no-bowerrc/.gitkeep b/tests/fixtures/addon/simple/vendor/.gitkeep similarity index 100% rename from tests/fixtures/bower-directory-tests/no-bowerrc/.gitkeep rename to tests/fixtures/addon/simple/vendor/.gitkeep diff --git a/tests/fixtures/addon/typescript/.ember-cli b/tests/fixtures/addon/typescript/.ember-cli new file mode 100644 index 0000000000..9a28fc355d --- /dev/null +++ b/tests/fixtures/addon/typescript/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": true, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": "strict", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": "strict" +} diff --git a/tests/fixtures/addon/typescript/ember-cli-build.js b/tests/fixtures/addon/typescript/ember-cli-build.js new file mode 100644 index 0000000000..d522ae6401 --- /dev/null +++ b/tests/fixtures/addon/typescript/ember-cli-build.js @@ -0,0 +1,27 @@ +'use strict'; + +const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); + +module.exports = function (defaults) { + const app = new EmberAddon(defaults, { + 'ember-cli-babel': { enableTypeScriptTransform: true }, + + // Add options here + }); + + /* + This build file specifies the options for the dummy test app of this + addon, located in `/tests/dummy` + This build file does *not* influence how the addon or the app using it + behave. You most likely want to be modifying `./index.js` or app's build file + */ + + const { maybeEmbroider } = require('@embroider/test-setup'); + return maybeEmbroider(app, { + skipBabel: [ + { + package: 'qunit', + }, + ], + }); +}; diff --git a/tests/fixtures/addon/typescript/eslint.config.mjs b/tests/fixtures/addon/typescript/eslint.config.mjs new file mode 100644 index 0000000000..5232ee68dd --- /dev/null +++ b/tests/fixtures/addon/typescript/eslint.config.mjs @@ -0,0 +1,151 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import globals from 'globals'; +import js from '@eslint/js'; + +import ts from 'typescript-eslint'; + +import ember from 'eslint-plugin-ember/recommended'; + +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const parserOptions = { + esm: { + js: { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + [ + '@babel/plugin-proposal-decorators', + { decoratorsBeforeExport: true }, + ], + ], + }, + }, + ts: { + projectService: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, +}; + +export default ts.config( + js.configs.recommended, + ember.configs.base, + ember.configs.gjs, + ember.configs.gts, + eslintConfigPrettier, + /** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: parserOptions.esm.js, + globals: { + ...globals.browser, + }, + }, + }, + { + files: ['**/*.{ts,gts}'], + languageOptions: { + parser: ember.parser, + parserOptions: parserOptions.esm.ts, + }, + extends: [...ts.configs.recommendedTypeChecked, ember.configs.gts], + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs,ts,gts}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: parserOptions.esm.js, + globals: { + ...globals.node, + }, + }, + }, +); diff --git a/tests/fixtures/addon/typescript/index.js b/tests/fixtures/addon/typescript/index.js new file mode 100644 index 0000000000..602e58b5e2 --- /dev/null +++ b/tests/fixtures/addon/typescript/index.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + name: require('./package').name, + + options: { + 'ember-cli-babel': { enableTypeScriptTransform: true }, + }, +}; diff --git a/tests/fixtures/addon/typescript/package.json b/tests/fixtures/addon/typescript/package.json new file mode 100644 index 0000000000..7ad8391d79 --- /dev/null +++ b/tests/fixtures/addon/typescript/package.json @@ -0,0 +1,120 @@ +{ + "name": "foo", + "version": "0.0.0", + "description": "The default blueprint for ember-cli addons.", + "keywords": [ + "ember-addon" + ], + "repository": "", + "license": "MIT", + "author": "", + "typesVersions": { + "*": { + "test-support": [ + "declarations/addon-test-support/index.d.ts" + ], + "test-support/*": [ + "declarations/addon-test-support/*", + "declarations/addon-test-support/*/index.d.ts" + ], + "*": [ + "declarations/addon/*", + "declarations/addon/*/index.d.ts" + ] + } + }, + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"yarn:lint:css -- --fix\"", + "lint:fix": "concurrently \"yarn:lint:*:fix\" --names \"fix:\" --prefixColors auto && yarn format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "lint:types": "tsc --noEmit", + "prepack": "tsc --project tsconfig.declarations.json", + "postpack": "rimraf declarations", + "start": "ember serve", + "test": "concurrently \"yarn:lint\" \"yarn:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test", + "test:ember-compatibility": "ember try:each" + }, + "dependencies": { + "@babel/core": "^7.29.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-template-imports": "^4.4.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@embroider/test-setup": "^4.0.0", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.7.7", + "@tsconfig/ember": "^3.0.12", + "@types/qunit": "^2.19.14", + "@types/rsvp": "^4.0.9", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-load-initializers": "^3.0.1", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-source-channel-url": "^3.0.0", + "ember-template-lint": "^6.1.0", + "ember-try": "^4.0.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "rimraf": "^5.0.10", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4", + "webpack": "^5.107.1" + }, + "peerDependencies": { + "ember-source": ">= 4.0.0" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "configPath": "tests/dummy/config" + } +} diff --git a/tests/fixtures/addon/typescript/tests/dummy/app/config/environment.d.ts b/tests/fixtures/addon/typescript/tests/dummy/app/config/environment.d.ts new file mode 100644 index 0000000000..91eec535db --- /dev/null +++ b/tests/fixtures/addon/typescript/tests/dummy/app/config/environment.d.ts @@ -0,0 +1,14 @@ +/** + * Type declarations for + * import config from 'dummy/config/environment' + */ +declare const config: { + environment: string; + modulePrefix: string; + podModulePrefix: string; + locationType: 'history' | 'hash' | 'none'; + rootURL: string; + APP: Record; +}; + +export default config; diff --git a/tests/fixtures/addon/typescript/tests/dummy/config/ember-cli-update.json b/tests/fixtures/addon/typescript/tests/dummy/config/ember-cli-update.json new file mode 100644 index 0000000000..4c0bb239d2 --- /dev/null +++ b/tests/fixtures/addon/typescript/tests/dummy/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "isBaseBlueprint": true, + "options": [ + "--yarn", + "--ci-provider=github", + "--typescript" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/addon/typescript/tests/helpers/index.ts b/tests/fixtures/addon/typescript/tests/helpers/index.ts new file mode 100644 index 0000000000..e190f567ed --- /dev/null +++ b/tests/fixtures/addon/typescript/tests/helpers/index.ts @@ -0,0 +1,43 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, + setupTest as upstreamSetupTest, + type SetupTestOptions, +} from 'ember-qunit'; + +// This file exists to provide wrappers around ember-qunit's +// test setup functions. This way, you can easily extend the setup that is +// needed per test type. + +function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupApplicationTest(hooks, options); + + // Additional setup for application tests can be done here. + // + // For example, if you need an authenticated session for each + // application test, you could do: + // + // hooks.beforeEach(async function () { + // await authenticateSession(); // ember-simple-auth + // }); + // + // This is also a good place to call test setup functions coming + // from other addons: + // + // setupIntl(hooks, 'en-us'); // ember-intl + // setupMirage(hooks); // ember-cli-mirage +} + +function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupRenderingTest(hooks, options); + + // Additional setup for rendering tests can be done here. +} + +function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupTest(hooks, options); + + // Additional setup for unit tests can be done here. +} + +export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/tests/fixtures/addon/typescript/tsconfig.declarations.json b/tests/fixtures/addon/typescript/tsconfig.declarations.json new file mode 100644 index 0000000000..5a21df72e5 --- /dev/null +++ b/tests/fixtures/addon/typescript/tsconfig.declarations.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationDir": "declarations", + "emitDeclarationOnly": true, + "noEmit": false, + "rootDir": "." + }, + "include": ["addon", "addon-test-support"] +} diff --git a/tests/fixtures/addon/typescript/tsconfig.json b/tests/fixtures/addon/typescript/tsconfig.json new file mode 100644 index 0000000000..e354f2a368 --- /dev/null +++ b/tests/fixtures/addon/typescript/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@tsconfig/ember/tsconfig.json", + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "compilerOptions": { + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "dummy/tests/*": ["tests/*"], + "dummy/*": ["tests/dummy/app/*", "app/*"], + "foo": ["addon"], + "foo/*": ["addon/*"], + "foo/test-support": ["addon-test-support"], + "foo/test-support/*": ["addon-test-support/*"], + "*": ["types/*"] + }, + "types": ["ember-source/types"] + } +} diff --git a/tests/fixtures/addon/typescript/types/global.d.ts b/tests/fixtures/addon/typescript/types/global.d.ts new file mode 100644 index 0000000000..2c531e29af --- /dev/null +++ b/tests/fixtures/addon/typescript/types/global.d.ts @@ -0,0 +1 @@ +import '@glint/environment-ember-loose'; diff --git a/tests/fixtures/addon/with-addon-pod-templates/addon/components/some-foo/template.hbs b/tests/fixtures/addon/with-addon-pod-templates/addon/components/some-foo/template.hbs deleted file mode 100644 index 32f72011fb..0000000000 --- a/tests/fixtures/addon/with-addon-pod-templates/addon/components/some-foo/template.hbs +++ /dev/null @@ -1 +0,0 @@ -

Hi!

diff --git a/tests/fixtures/addon/with-addon-templates/addon/templates/test.hbs b/tests/fixtures/addon/with-addon-templates/addon/templates/test.hbs deleted file mode 100644 index de78552628..0000000000 --- a/tests/fixtures/addon/with-addon-templates/addon/templates/test.hbs +++ /dev/null @@ -1 +0,0 @@ -

Derp

diff --git a/tests/fixtures/addon/with-app-styles/app/styles/foo-bar.css b/tests/fixtures/addon/with-app-styles/app/styles/foo-bar.css new file mode 100644 index 0000000000..acdac2ae70 --- /dev/null +++ b/tests/fixtures/addon/with-app-styles/app/styles/foo-bar.css @@ -0,0 +1 @@ +/* app/styles/foo-ba.css contents */ diff --git a/tests/fixtures/addon/with-app-styles/package.json b/tests/fixtures/addon/with-app-styles/package.json new file mode 100644 index 0000000000..08d477d2fc --- /dev/null +++ b/tests/fixtures/addon/with-app-styles/package.json @@ -0,0 +1,19 @@ +{ + "name": "test-project", + "private": true, + "dependencies": { + "something-else": "latest" + }, + "devDependencies": { + "ember-resolver": "^13.2.0", + "ember-cli": "latest", + "ember-random-addon": "latest", + "ember-non-root-addon": "latest", + "ember-generated-with-export-addon": "latest", + "non-ember-thingy": "latest", + "ember-before-blueprint-addon": "latest", + "ember-after-blueprint-addon": "latest", + "ember-devDeps-addon": "latest", + "ember-addon-with-dependencies": "latest" + } +} diff --git a/tests/fixtures/addon/with-blueprint-override/blueprints/component/files/__root__/__path__/__name__.js b/tests/fixtures/addon/with-blueprint-override/blueprints/component/files/__root__/__path__/__name__.js index d96d2a1246..0e79c7e4a4 100644 --- a/tests/fixtures/addon/with-blueprint-override/blueprints/component/files/__root__/__path__/__name__.js +++ b/tests/fixtures/addon/with-blueprint-override/blueprints/component/files/__root__/__path__/__name__.js @@ -1,5 +1,6 @@ -import Ember from 'ember'; +import Component from '@ember/component'; <%= importTemplate %> -export default Ember.Component.extend({<%= contents %> + +export default Component.extend({<%= contents %> }); //generated component successfully \ No newline at end of file diff --git a/tests/fixtures/addon/with-blueprint-override/blueprints/component/index.js b/tests/fixtures/addon/with-blueprint-override/blueprints/component/index.js index adaefc9de1..7f9c665bbf 100644 --- a/tests/fixtures/addon/with-blueprint-override/blueprints/component/index.js +++ b/tests/fixtures/addon/with-blueprint-override/blueprints/component/index.js @@ -1,5 +1,24 @@ +const path = require('path'); + module.exports = { - locals: function(options) { - return this.lookupBlueprint('component').locals(options); - } -}; \ No newline at end of file + + filesPath: function() { + return path.join(this.path, 'files'); + }, + + fileMapTokens: function() { + return { + __path__: function(options) { + return path.join('components', 'new-path'); + }, + }; + }, + + locals(options) { + return { + importTemplate: '', + contents: '', + path: '' + }; + } +}; diff --git a/tests/fixtures/addon/with-blueprint-override/blueprints/component/module-unification-files/__root__/__path__/component.js b/tests/fixtures/addon/with-blueprint-override/blueprints/component/module-unification-files/__root__/__path__/component.js new file mode 100644 index 0000000000..0e79c7e4a4 --- /dev/null +++ b/tests/fixtures/addon/with-blueprint-override/blueprints/component/module-unification-files/__root__/__path__/component.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; +<%= importTemplate %> + +export default Component.extend({<%= contents %> +}); +//generated component successfully \ No newline at end of file diff --git a/tests/fixtures/addon/with-nested-addons/app/foo.js b/tests/fixtures/addon/with-nested-addons/app/foo.js new file mode 100644 index 0000000000..55a91a3190 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/app/foo.js @@ -0,0 +1 @@ +// RAW app/foo.js diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/index.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/index.js new file mode 100644 index 0000000000..d4f9738ea8 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/index.js @@ -0,0 +1 @@ +// RAW node_modules/ember-top-addon/addon/index.js diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/styles/app.css b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/styles/app.css new file mode 100644 index 0000000000..2dc7987100 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/styles/app.css @@ -0,0 +1 @@ +/* RAW node_modules/ember-top-addon/addon/styles/app.css */ diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/templates/application.hbs b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/templates/application.hbs new file mode 100644 index 0000000000..e926291cd4 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/addon/templates/application.hbs @@ -0,0 +1 @@ + diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/index.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/index.js new file mode 100644 index 0000000000..c62349fa1e --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + name: require('./package').name +}; + diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/addon/index.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/addon/index.js new file mode 100644 index 0000000000..b5874b2dc9 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/addon/index.js @@ -0,0 +1 @@ +// RAW node_modules/ember-top-addon/node_modules/ember-inner-addon/addon/index.js diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/index.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/index.js new file mode 100644 index 0000000000..0836660132 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/index.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = { + name: require('./package').name, + included(app) { + app.import('vendor/inner.js'); + } +}; diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/package.json b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/package.json new file mode 100644 index 0000000000..45d47456e5 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/package.json @@ -0,0 +1,14 @@ +{ + "name": "ember-inner-addon", + "version": "0.0.0", + "private": true, + "keywords": [ + "ember-addon" + ], + "dependencies": { + "ember-cli-babel": "latest" + }, + "devDependencies": { + "loader.js": "latest" + } +} diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/vendor/inner.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/vendor/inner.js new file mode 100644 index 0000000000..072e547f74 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon/vendor/inner.js @@ -0,0 +1 @@ +//INNER_ADDON_IMPORT_WITH_APP_IMPORT diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/index.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/index.js new file mode 100644 index 0000000000..49d09e61f6 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/index.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = { + name: require('./package').name, + included() { + this.import('vendor/inner2.js'); + } +}; diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/package.json b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/package.json new file mode 100644 index 0000000000..a0550c4338 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/package.json @@ -0,0 +1,11 @@ +{ + "name": "ember-inner-addon2", + "version": "0.0.0", + "private": true, + "keywords": [ + "ember-addon" + ], + "devDependencies": { + "loader.js": "latest" + } +} diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/vendor/inner2.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/vendor/inner2.js new file mode 100644 index 0000000000..f04a321e29 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/ember-inner-addon2/vendor/inner2.js @@ -0,0 +1 @@ +//INNER_ADDON_IMPORT_WITH_THIS_IMPORT diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/postprocesstree-addon/index.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/postprocesstree-addon/index.js new file mode 100644 index 0000000000..00d02194d6 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/postprocesstree-addon/index.js @@ -0,0 +1,11 @@ +'use strict'; + +const stew = require('broccoli-stew'); + +module.exports = { + name: require('./package').name, + + postprocessTree(type, tree) { + return stew.map(tree, contents => contents.replace('PREPROCESSED', 'POSTPROCESSED')); + }, +}; diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/postprocesstree-addon/package.json b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/postprocesstree-addon/package.json new file mode 100644 index 0000000000..4df771d0d6 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/postprocesstree-addon/package.json @@ -0,0 +1,9 @@ +{ + "name": "postprocesstree-addon", + "version": "0.0.0", + "private": true, + "keywords": [ + "ember-addon" + ], + "dependencies": {} +} diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/preprocesstree-addon/index.js b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/preprocesstree-addon/index.js new file mode 100644 index 0000000000..44a59b429a --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/preprocesstree-addon/index.js @@ -0,0 +1,11 @@ +'use strict'; + +const stew = require('broccoli-stew'); + +module.exports = { + name: require('./package').name, + + preprocessTree(type, tree) { + return stew.map(tree, contents => contents.replace('RAW', 'PREPROCESSED')); + }, +}; diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/preprocesstree-addon/package.json b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/preprocesstree-addon/package.json new file mode 100644 index 0000000000..2fec36d966 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/node_modules/preprocesstree-addon/package.json @@ -0,0 +1,9 @@ +{ + "name": "preprocesstree-addon", + "version": "0.0.0", + "private": true, + "keywords": [ + "ember-addon" + ], + "dependencies": {} +} diff --git a/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/package.json b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/package.json new file mode 100644 index 0000000000..087145aa69 --- /dev/null +++ b/tests/fixtures/addon/with-nested-addons/node_modules/ember-top-addon/package.json @@ -0,0 +1,16 @@ +{ + "name": "ember-top-addon", + "version": "0.0.0", + "private": true, + "keywords": [ + "ember-addon" + ], + "dependencies": { + "ember-cli-babel": "latest", + "ember-inner-addon": "latest", + "ember-inner-addon2": "latest", + "ember-cli-htmlbars": "*", + "preprocesstree-addon": "*", + "postprocesstree-addon": "*" + } +} diff --git a/tests/fixtures/addon/yarn/.github/workflows/ci.yml b/tests/fixtures/addon/yarn/.github/workflows/ci.yml new file mode 100644 index 0000000000..ec87302816 --- /dev/null +++ b/tests/fixtures/addon/yarn/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: "Tests" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Lint + run: yarn lint + - name: Run Tests + run: yarn test:ember + + floating: + name: "Floating Dependencies" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + - name: Install Dependencies + run: yarn install --no-lockfile + - name: Run Tests + run: yarn test:ember + + try-scenarios: + name: ${{ matrix.try-scenario }} + runs-on: ubuntu-latest + needs: "test" + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + try-scenario: + - ember-lts-5.8 + - ember-lts-5.12 + - ember-release + - ember-beta + - ember-canary + - embroider-safe + - embroider-optimized + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Run Tests + run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} diff --git a/tests/fixtures/addon/yarn/CONTRIBUTING.md b/tests/fixtures/addon/yarn/CONTRIBUTING.md new file mode 100644 index 0000000000..8716e9dbd1 --- /dev/null +++ b/tests/fixtures/addon/yarn/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# How To Contribute + +## Installation + +- `git clone ` +- `cd foo` +- `yarn install` + +## Linting + +- `yarn lint` +- `yarn lint:fix` + +## Running tests + +- `yarn test` – Runs the test suite on the current Ember version +- `yarn test:ember --server` – Runs the test suite in "watch mode" +- `yarn test:ember-compatibility` – Runs the test suite against multiple Ember versions + +## Running the dummy application + +- `yarn start` +- Visit the dummy application at [http://localhost:4200](http://localhost:4200). + +For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). diff --git a/tests/fixtures/addon/yarn/README.md b/tests/fixtures/addon/yarn/README.md new file mode 100644 index 0000000000..8035eafa5e --- /dev/null +++ b/tests/fixtures/addon/yarn/README.md @@ -0,0 +1,37 @@ +# foo + +[Short description of the addon.] + +## Compatibility + +- Ember.js v5.8 or above +- Ember CLI v5.8 or above +- Node.js v20 or above + +## Installation + +``` +ember install foo +``` + +## Usage + +[Longer description of how to use the addon in apps.] + +> TODO: Document the package's public API. +> +> For each public api (including components, helpers, modifiers, and other apis) include: +> +> - The import path for a consumer (e.g. `import MyAddonsComponent from 'my-addon/components/my-addons-component'`) +> - What it does +> - Parameters/options +> - Return value +> - Example usage + +## Contributing + +See the [Contributing](CONTRIBUTING.md) guide for details. + +## License + +This project is licensed under the [MIT License](LICENSE.md). diff --git a/tests/fixtures/addon/yarn/package.json b/tests/fixtures/addon/yarn/package.json new file mode 100644 index 0000000000..8f7588cebf --- /dev/null +++ b/tests/fixtures/addon/yarn/package.json @@ -0,0 +1,94 @@ +{ + "name": "foo", + "version": "0.0.0", + "description": "The default blueprint for ember-cli addons.", + "keywords": [ + "ember-addon" + ], + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"yarn:lint:css -- --fix\"", + "lint:fix": "concurrently \"yarn:lint:*:fix\" --names \"fix:\" --prefixColors auto && yarn format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"yarn:lint\" \"yarn:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test", + "test:ember-compatibility": "ember try:each" + }, + "dependencies": { + "@babel/core": "^7.29.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-template-imports": "^4.4.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@embroider/test-setup": "^4.0.0", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-load-initializers": "^3.0.1", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-source-channel-url": "^3.0.0", + "ember-template-lint": "^6.1.0", + "ember-try": "^4.0.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "peerDependencies": { + "ember-source": ">= 4.0.0" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "configPath": "tests/dummy/config" + } +} diff --git a/tests/fixtures/addon/yarn/tests/dummy/app/templates/application.gjs b/tests/fixtures/addon/yarn/tests/dummy/app/templates/application.gjs new file mode 100644 index 0000000000..196d2c4b8b --- /dev/null +++ b/tests/fixtures/addon/yarn/tests/dummy/app/templates/application.gjs @@ -0,0 +1,12 @@ +import pageTitle from 'ember-page-title/helpers/page-title'; +import WelcomePage from 'ember-welcome-page/components/welcome-page'; + + diff --git a/tests/fixtures/addon/yarn/tests/dummy/config/ember-cli-update.json b/tests/fixtures/addon/yarn/tests/dummy/config/ember-cli-update.json new file mode 100644 index 0000000000..757a5f6954 --- /dev/null +++ b/tests/fixtures/addon/yarn/tests/dummy/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-addon-blueprint", + "isBaseBlueprint": true, + "options": [ + "--welcome", + "--yarn", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/addon/yarn/tests/dummy/config/ember-try.js b/tests/fixtures/addon/yarn/tests/dummy/config/ember-try.js new file mode 100644 index 0000000000..5eb47a0464 --- /dev/null +++ b/tests/fixtures/addon/yarn/tests/dummy/config/ember-try.js @@ -0,0 +1,54 @@ +'use strict'; + +const getChannelURL = require('ember-source-channel-url'); +const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); + +module.exports = async function () { + return { + packageManager: 'yarn', + scenarios: [ + { + name: 'ember-lts-5.8', + npm: { + devDependencies: { + 'ember-source': '~5.8.0', + }, + }, + }, + { + name: 'ember-lts-5.12', + npm: { + devDependencies: { + 'ember-source': '~5.12.0', + }, + }, + }, + { + name: 'ember-release', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('release'), + }, + }, + }, + { + name: 'ember-beta', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('beta'), + }, + }, + }, + { + name: 'ember-canary', + npm: { + devDependencies: { + 'ember-source': await getChannelURL('canary'), + }, + }, + }, + embroiderSafe(), + embroiderOptimized(), + ], + }; +}; diff --git a/tests/fixtures/app/defaults/.ember-cli b/tests/fixtures/app/defaults/.ember-cli new file mode 100644 index 0000000000..be3ac4ed1e --- /dev/null +++ b/tests/fixtures/app/defaults/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": "loose", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": "loose" +} diff --git a/tests/fixtures/app/defaults/.github/workflows/ci.yml b/tests/fixtures/app/defaults/.github/workflows/ci.yml new file mode 100644 index 0000000000..8a43ff0d42 --- /dev/null +++ b/tests/fixtures/app/defaults/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + - name: Install Dependencies + run: npm ci + - name: Lint + run: npm run lint + + test: + name: "Test" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + - name: Install Dependencies + run: npm ci + - name: Run Tests + run: npm test diff --git a/tests/fixtures/app/defaults/README.md b/tests/fixtures/app/defaults/README.md new file mode 100644 index 0000000000..08cb37864d --- /dev/null +++ b/tests/fixtures/app/defaults/README.md @@ -0,0 +1,56 @@ +# foo + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) (with npm) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd foo` +- `npm install` + +## Running / Development + +- `npm run start` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `npm run test` +- `npm run test:ember -- --server` + +### Linting + +- `npm run lint` +- `npm run lint:fix` + +### Building + +- `npm exec ember build` (development) +- `npm run build` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/fixtures/app/defaults/app/templates/application.hbs b/tests/fixtures/app/defaults/app/templates/application.hbs new file mode 100644 index 0000000000..00697db19a --- /dev/null +++ b/tests/fixtures/app/defaults/app/templates/application.hbs @@ -0,0 +1,7 @@ +{{page-title "Foo"}} + +{{outlet}} + +{{! The following component displays Ember's default welcome message. }} + +{{! Feel free to remove this! }} \ No newline at end of file diff --git a/tests/fixtures/app/defaults/config/ember-cli-update.json b/tests/fixtures/app/defaults/config/ember-cli-update.json new file mode 100644 index 0000000000..51492185c1 --- /dev/null +++ b/tests/fixtures/app/defaults/config/ember-cli-update.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/defaults/ember-cli-build.js b/tests/fixtures/app/defaults/ember-cli-build.js new file mode 100644 index 0000000000..0582fad75e --- /dev/null +++ b/tests/fixtures/app/defaults/ember-cli-build.js @@ -0,0 +1,20 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + emberData: { + deprecations: { + // New projects can safely leave this deprecation disabled. + // If upgrading, to opt-into the deprecated behavior, set this to true and then follow: + // https://deprecations.emberjs.com/id/ember-data-deprecate-store-extends-ember-object + // before upgrading to Ember Data 6.0 + DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: false, + }, + }, + // Add options here + }); + + return app.toTree(); +}; diff --git a/tests/fixtures/app/defaults/package.json b/tests/fixtures/app/defaults/package.json new file mode 100644 index 0000000000..f57e864140 --- /dev/null +++ b/tests/fixtures/app/defaults/package.json @@ -0,0 +1,83 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/defaults/tests/helpers/index.js b/tests/fixtures/app/defaults/tests/helpers/index.js new file mode 100644 index 0000000000..ab04c162dd --- /dev/null +++ b/tests/fixtures/app/defaults/tests/helpers/index.js @@ -0,0 +1,42 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, + setupTest as upstreamSetupTest, +} from 'ember-qunit'; + +// This file exists to provide wrappers around ember-qunit's +// test setup functions. This way, you can easily extend the setup that is +// needed per test type. + +function setupApplicationTest(hooks, options) { + upstreamSetupApplicationTest(hooks, options); + + // Additional setup for application tests can be done here. + // + // For example, if you need an authenticated session for each + // application test, you could do: + // + // hooks.beforeEach(async function () { + // await authenticateSession(); // ember-simple-auth + // }); + // + // This is also a good place to call test setup functions coming + // from other addons: + // + // setupIntl(hooks, 'en-us'); // ember-intl + // setupMirage(hooks); // ember-cli-mirage +} + +function setupRenderingTest(hooks, options) { + upstreamSetupRenderingTest(hooks, options); + + // Additional setup for rendering tests can be done here. +} + +function setupTest(hooks, options) { + upstreamSetupTest(hooks, options); + + // Additional setup for unit tests can be done here. +} + +export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/tests/fixtures/app/embroider-no-ember-data/config/ember-cli-update.json b/tests/fixtures/app/embroider-no-ember-data/config/ember-cli-update.json new file mode 100644 index 0000000000..65c8ccb3cf --- /dev/null +++ b/tests/fixtures/app/embroider-no-ember-data/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--embroider", + "--ci-provider=github", + "--no-ember-data" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/embroider-no-ember-data/ember-cli-build.js b/tests/fixtures/app/embroider-no-ember-data/ember-cli-build.js new file mode 100644 index 0000000000..f7f127db35 --- /dev/null +++ b/tests/fixtures/app/embroider-no-ember-data/ember-cli-build.js @@ -0,0 +1,22 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + // Add options here + }); + + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + staticAddonTestSupportTrees: true, + staticAddonTrees: true, + staticEmberSource: true, + staticInvokables: true, + skipBabel: [ + { + package: 'qunit', + }, + ], + }); +}; diff --git a/tests/fixtures/app/embroider-no-ember-data/package.json b/tests/fixtures/app/embroider-no-ember-data/package.json new file mode 100644 index 0000000000..4810742ab2 --- /dev/null +++ b/tests/fixtures/app/embroider-no-ember-data/package.json @@ -0,0 +1,83 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10", + "@embroider/macros": "^1.20.2", + "@embroider/webpack": "^4.1.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/embroider-no-welcome/config/ember-cli-update.json b/tests/fixtures/app/embroider-no-welcome/config/ember-cli-update.json new file mode 100644 index 0000000000..20cdff4c61 --- /dev/null +++ b/tests/fixtures/app/embroider-no-welcome/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--no-welcome", + "--embroider", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/embroider-no-welcome/package.json b/tests/fixtures/app/embroider-no-welcome/package.json new file mode 100644 index 0000000000..6d0b786cb2 --- /dev/null +++ b/tests/fixtures/app/embroider-no-welcome/package.json @@ -0,0 +1,83 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10", + "@embroider/macros": "^1.20.2", + "@embroider/webpack": "^4.1.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/embroider-pnpm/config/ember-cli-update.json b/tests/fixtures/app/embroider-pnpm/config/ember-cli-update.json new file mode 100644 index 0000000000..3a09671f69 --- /dev/null +++ b/tests/fixtures/app/embroider-pnpm/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--pnpm", + "--embroider", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/embroider-pnpm/package.json b/tests/fixtures/app/embroider-pnpm/package.json new file mode 100644 index 0000000000..4cd3a5518f --- /dev/null +++ b/tests/fixtures/app/embroider-pnpm/package.json @@ -0,0 +1,84 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"", + "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10", + "@embroider/macros": "^1.20.2", + "@embroider/webpack": "^4.1.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/embroider-yarn/config/ember-cli-update.json b/tests/fixtures/app/embroider-yarn/config/ember-cli-update.json new file mode 100644 index 0000000000..9c87285c3b --- /dev/null +++ b/tests/fixtures/app/embroider-yarn/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--yarn", + "--embroider", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/embroider-yarn/package.json b/tests/fixtures/app/embroider-yarn/package.json new file mode 100644 index 0000000000..2b3f25a211 --- /dev/null +++ b/tests/fixtures/app/embroider-yarn/package.json @@ -0,0 +1,84 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"yarn:lint:css -- --fix\"", + "lint:fix": "concurrently \"yarn:lint:*:fix\" --names \"fix:\" --prefixColors auto && yarn format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"yarn:lint\" \"yarn:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10", + "@embroider/macros": "^1.20.2", + "@embroider/webpack": "^4.1.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/embroider/config/ember-cli-update.json b/tests/fixtures/app/embroider/config/ember-cli-update.json new file mode 100644 index 0000000000..5df64a07c9 --- /dev/null +++ b/tests/fixtures/app/embroider/config/ember-cli-update.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--embroider", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/embroider/ember-cli-build.js b/tests/fixtures/app/embroider/ember-cli-build.js new file mode 100644 index 0000000000..3c70314ae8 --- /dev/null +++ b/tests/fixtures/app/embroider/ember-cli-build.js @@ -0,0 +1,31 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + emberData: { + deprecations: { + // New projects can safely leave this deprecation disabled. + // If upgrading, to opt-into the deprecated behavior, set this to true and then follow: + // https://deprecations.emberjs.com/id/ember-data-deprecate-store-extends-ember-object + // before upgrading to Ember Data 6.0 + DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: false, + }, + }, + // Add options here + }); + + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + staticAddonTestSupportTrees: true, + staticAddonTrees: true, + staticEmberSource: true, + staticInvokables: true, + skipBabel: [ + { + package: 'qunit', + }, + ], + }); +}; diff --git a/tests/fixtures/app/embroider/package.json b/tests/fixtures/app/embroider/package.json new file mode 100644 index 0000000000..769adb046f --- /dev/null +++ b/tests/fixtures/app/embroider/package.json @@ -0,0 +1,84 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10", + "@embroider/macros": "^1.20.2", + "@embroider/webpack": "^4.1.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/eslint.config.mjs b/tests/fixtures/app/eslint.config.mjs new file mode 100644 index 0000000000..a29e5cd3bd --- /dev/null +++ b/tests/fixtures/app/eslint.config.mjs @@ -0,0 +1,126 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import globals from 'globals'; +import js from '@eslint/js'; + +import ember from 'eslint-plugin-ember/recommended'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const esmParserOptions = { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], + ], + }, +}; + +export default [ + js.configs.recommended, + eslintConfigPrettier, + ember.configs.base, + ember.configs.gjs, + /** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: esmParserOptions, + globals: { + ...globals.browser, + }, + }, + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: esmParserOptions, + globals: { + ...globals.node, + }, + }, + }, +]; diff --git a/tests/fixtures/app/nested-project/actual-project/app/templates/application.hbs b/tests/fixtures/app/nested-project/actual-project/app/templates/application.hbs new file mode 100644 index 0000000000..47296e2d6d --- /dev/null +++ b/tests/fixtures/app/nested-project/actual-project/app/templates/application.hbs @@ -0,0 +1,7 @@ +{{page-title "Actual Project"}} + +{{! The following component displays Ember's default welcome message. }} + +{{! Feel free to remove this! }} + +{{outlet}} diff --git a/tests/fixtures/app/nested-project/actual-project/ember-cli-build.js b/tests/fixtures/app/nested-project/actual-project/ember-cli-build.js new file mode 100644 index 0000000000..ed991bd98f --- /dev/null +++ b/tests/fixtures/app/nested-project/actual-project/ember-cli-build.js @@ -0,0 +1,11 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + // Add options here + }); + + return app.toTree(); +}; diff --git a/tests/fixtures/app/nested-project/actual-project/package.json b/tests/fixtures/app/nested-project/actual-project/package.json new file mode 100644 index 0000000000..38649f3ec4 --- /dev/null +++ b/tests/fixtures/app/nested-project/actual-project/package.json @@ -0,0 +1,76 @@ +{ + "name": "actual-project", + "version": "0.0.0", + "private": true, + "description": "Small description for actual-project goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^2.3.0", + "@ember/test-helpers": "^5.4.1", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~5.8.2", + "ember-fetch": "^8.1.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^7.0.2", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-prettier": "^3.4.1", + "eslint-plugin-qunit": "^8.2.6", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "stylelint-prettier": "^5.0.3" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/nested-project/package.json b/tests/fixtures/app/nested-project/package.json new file mode 100644 index 0000000000..53c333e49d --- /dev/null +++ b/tests/fixtures/app/nested-project/package.json @@ -0,0 +1,6 @@ +{ + "name": "nested-project", + "ember-addon": { + "projectRoot": "./actual-project" + } +} diff --git a/tests/fixtures/app/no-ember-data/config/ember-cli-update.json b/tests/fixtures/app/no-ember-data/config/ember-cli-update.json new file mode 100644 index 0000000000..378e474c35 --- /dev/null +++ b/tests/fixtures/app/no-ember-data/config/ember-cli-update.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--ci-provider=github", + "--no-ember-data" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/no-ember-data/ember-cli-build.js b/tests/fixtures/app/no-ember-data/ember-cli-build.js new file mode 100644 index 0000000000..ed991bd98f --- /dev/null +++ b/tests/fixtures/app/no-ember-data/ember-cli-build.js @@ -0,0 +1,11 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + // Add options here + }); + + return app.toTree(); +}; diff --git a/tests/fixtures/app/no-ember-data/package.json b/tests/fixtures/app/no-ember-data/package.json new file mode 100644 index 0000000000..a1a4e1ce4b --- /dev/null +++ b/tests/fixtures/app/no-ember-data/package.json @@ -0,0 +1,82 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/npm/.github/workflows/ci.yml b/tests/fixtures/app/npm/.github/workflows/ci.yml new file mode 100644 index 0000000000..8a43ff0d42 --- /dev/null +++ b/tests/fixtures/app/npm/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + - name: Install Dependencies + run: npm ci + - name: Lint + run: npm run lint + + test: + name: "Test" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + - name: Install Dependencies + run: npm ci + - name: Run Tests + run: npm test diff --git a/tests/fixtures/app/npm/README.md b/tests/fixtures/app/npm/README.md new file mode 100644 index 0000000000..08cb37864d --- /dev/null +++ b/tests/fixtures/app/npm/README.md @@ -0,0 +1,56 @@ +# foo + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) (with npm) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd foo` +- `npm install` + +## Running / Development + +- `npm run start` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `npm run test` +- `npm run test:ember -- --server` + +### Linting + +- `npm run lint` +- `npm run lint:fix` + +### Building + +- `npm exec ember build` (development) +- `npm run build` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/blueprints/app/files/app/templates/application.hbs b/tests/fixtures/app/npm/app/templates/application.hbs similarity index 54% rename from blueprints/app/files/app/templates/application.hbs rename to tests/fixtures/app/npm/app/templates/application.hbs index f8bc38e7b6..76f2c5e009 100644 --- a/blueprints/app/files/app/templates/application.hbs +++ b/tests/fixtures/app/npm/app/templates/application.hbs @@ -1,3 +1,5 @@ +{{page-title "Foo"}} +

Welcome to Ember

-{{outlet}} +{{outlet}} \ No newline at end of file diff --git a/tests/fixtures/app/npm/config/ember-cli-update.json b/tests/fixtures/app/npm/config/ember-cli-update.json new file mode 100644 index 0000000000..cac230bf41 --- /dev/null +++ b/tests/fixtures/app/npm/config/ember-cli-update.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--no-welcome", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/npm/package.json b/tests/fixtures/app/npm/package.json new file mode 100644 index 0000000000..b258354176 --- /dev/null +++ b/tests/fixtures/app/npm/package.json @@ -0,0 +1,82 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/pnpm/.github/workflows/ci.yml b/tests/fixtures/app/pnpm/.github/workflows/ci.yml new file mode 100644 index 0000000000..5494bdcf5c --- /dev/null +++ b/tests/fixtures/app/pnpm/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + - name: Install Dependencies + run: pnpm install --frozen-lockfile + - name: Lint + run: pnpm lint + + test: + name: "Test" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + - name: Install Dependencies + run: pnpm install --frozen-lockfile + - name: Run Tests + run: pnpm test diff --git a/tests/fixtures/app/pnpm/README.md b/tests/fixtures/app/pnpm/README.md new file mode 100644 index 0000000000..6f6d5a2b56 --- /dev/null +++ b/tests/fixtures/app/pnpm/README.md @@ -0,0 +1,57 @@ +# foo + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) +- [pnpm](https://pnpm.io/) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd foo` +- `pnpm install` + +## Running / Development + +- `pnpm start` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `pnpm test` +- `pnpm test:ember --server` + +### Linting + +- `pnpm lint` +- `pnpm lint:fix` + +### Building + +- `pnpm ember build` (development) +- `pnpm build` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/fixtures/app/pnpm/app/templates/application.hbs b/tests/fixtures/app/pnpm/app/templates/application.hbs new file mode 100644 index 0000000000..00697db19a --- /dev/null +++ b/tests/fixtures/app/pnpm/app/templates/application.hbs @@ -0,0 +1,7 @@ +{{page-title "Foo"}} + +{{outlet}} + +{{! The following component displays Ember's default welcome message. }} + +{{! Feel free to remove this! }} \ No newline at end of file diff --git a/tests/fixtures/app/pnpm/config/ember-cli-update.json b/tests/fixtures/app/pnpm/config/ember-cli-update.json new file mode 100644 index 0000000000..fb9adcaccb --- /dev/null +++ b/tests/fixtures/app/pnpm/config/ember-cli-update.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--pnpm", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/pnpm/package.json b/tests/fixtures/app/pnpm/package.json new file mode 100644 index 0000000000..05214dafdf --- /dev/null +++ b/tests/fixtures/app/pnpm/package.json @@ -0,0 +1,83 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"", + "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/blueprints/app/files/app/styles/app.css b/tests/fixtures/app/project-root-with-ember-cli-build/ember-cli-build.js similarity index 100% rename from blueprints/app/files/app/styles/app.css rename to tests/fixtures/app/project-root-with-ember-cli-build/ember-cli-build.js diff --git a/tests/fixtures/app/project-root-with-ember-cli-build/package.json b/tests/fixtures/app/project-root-with-ember-cli-build/package.json new file mode 100644 index 0000000000..f4563eb557 --- /dev/null +++ b/tests/fixtures/app/project-root-with-ember-cli-build/package.json @@ -0,0 +1,6 @@ +{ + "name": "project-root-with-ember-cli-build", + "ember-addon": { + "projectRoot": "./actual-project" + } +} diff --git a/tests/fixtures/app/strict-typescript/.ember-cli b/tests/fixtures/app/strict-typescript/.ember-cli new file mode 100644 index 0000000000..9a28fc355d --- /dev/null +++ b/tests/fixtures/app/strict-typescript/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": true, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": "strict", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": "strict" +} diff --git a/tests/fixtures/app/strict-typescript/app/templates/application.gts b/tests/fixtures/app/strict-typescript/app/templates/application.gts new file mode 100644 index 0000000000..2027aa47a5 --- /dev/null +++ b/tests/fixtures/app/strict-typescript/app/templates/application.gts @@ -0,0 +1,12 @@ +import pageTitle from 'ember-page-title/helpers/page-title'; +import WelcomePage from 'ember-welcome-page/components/welcome-page'; + + diff --git a/tests/fixtures/app/strict/.ember-cli b/tests/fixtures/app/strict/.ember-cli new file mode 100644 index 0000000000..8bd6e76fa2 --- /dev/null +++ b/tests/fixtures/app/strict/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": "strict", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": "strict" +} diff --git a/tests/fixtures/app/strict/app/templates/application.gjs b/tests/fixtures/app/strict/app/templates/application.gjs new file mode 100644 index 0000000000..2027aa47a5 --- /dev/null +++ b/tests/fixtures/app/strict/app/templates/application.gjs @@ -0,0 +1,12 @@ +import pageTitle from 'ember-page-title/helpers/page-title'; +import WelcomePage from 'ember-welcome-page/components/welcome-page'; + + diff --git a/tests/fixtures/app/typescript-embroider-no-ember-data/config/ember-cli-update.json b/tests/fixtures/app/typescript-embroider-no-ember-data/config/ember-cli-update.json new file mode 100644 index 0000000000..546fdabe12 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider-no-ember-data/config/ember-cli-update.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--embroider", + "--ci-provider=github", + "--typescript", + "--no-ember-data" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/typescript-embroider-no-ember-data/ember-cli-build.js b/tests/fixtures/app/typescript-embroider-no-ember-data/ember-cli-build.js new file mode 100644 index 0000000000..ec43ac11ae --- /dev/null +++ b/tests/fixtures/app/typescript-embroider-no-ember-data/ember-cli-build.js @@ -0,0 +1,24 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + 'ember-cli-babel': { enableTypeScriptTransform: true }, + + // Add options here + }); + + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + staticAddonTestSupportTrees: true, + staticAddonTrees: true, + staticEmberSource: true, + staticInvokables: true, + skipBabel: [ + { + package: 'qunit', + }, + ], + }); +}; diff --git a/tests/fixtures/app/typescript-embroider-no-ember-data/package.json b/tests/fixtures/app/typescript-embroider-no-ember-data/package.json new file mode 100644 index 0000000000..561cdafe10 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider-no-ember-data/package.json @@ -0,0 +1,92 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "lint:types": "tsc --noEmit", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10", + "@embroider/macros": "^1.20.2", + "@embroider/webpack": "^4.1.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.7.7", + "@tsconfig/ember": "^3.0.12", + "@types/qunit": "^2.19.14", + "@types/rsvp": "^4.0.9", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/typescript-embroider-no-ember-data/tsconfig.json b/tests/fixtures/app/typescript-embroider-no-ember-data/tsconfig.json new file mode 100644 index 0000000000..9d13a9b175 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider-no-ember-data/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/ember", + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "compilerOptions": { + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "foo/tests/*": ["tests/*"], + "foo/*": ["app/*"], + "*": ["types/*"] + }, + "types": [ + "ember-source/types" + ] + } +} diff --git a/tests/fixtures/app/typescript-embroider/.ember-cli b/tests/fixtures/app/typescript-embroider/.ember-cli new file mode 100644 index 0000000000..1df2255044 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": true, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": "loose", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": "loose" +} diff --git a/tests/fixtures/app/typescript-embroider/app/config/environment.d.ts b/tests/fixtures/app/typescript-embroider/app/config/environment.d.ts new file mode 100644 index 0000000000..8f58c600d2 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/app/config/environment.d.ts @@ -0,0 +1,14 @@ +/** + * Type declarations for + * import config from 'foo/config/environment' + */ +declare const config: { + environment: string; + modulePrefix: string; + podModulePrefix: string; + locationType: 'history' | 'hash' | 'none'; + rootURL: string; + APP: Record; +}; + +export default config; diff --git a/tests/fixtures/app/typescript-embroider/config/ember-cli-update.json b/tests/fixtures/app/typescript-embroider/config/ember-cli-update.json new file mode 100644 index 0000000000..9b720b26a9 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/config/ember-cli-update.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--yarn", + "--embroider", + "--ci-provider=github", + "--typescript" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/typescript-embroider/ember-cli-build.js b/tests/fixtures/app/typescript-embroider/ember-cli-build.js new file mode 100644 index 0000000000..21f49d2f10 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/ember-cli-build.js @@ -0,0 +1,33 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + emberData: { + deprecations: { + // New projects can safely leave this deprecation disabled. + // If upgrading, to opt-into the deprecated behavior, set this to true and then follow: + // https://deprecations.emberjs.com/id/ember-data-deprecate-store-extends-ember-object + // before upgrading to Ember Data 6.0 + DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: false, + }, + }, + 'ember-cli-babel': { enableTypeScriptTransform: true }, + + // Add options here + }); + + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + staticAddonTestSupportTrees: true, + staticAddonTrees: true, + staticEmberSource: true, + staticInvokables: true, + skipBabel: [ + { + package: 'qunit', + }, + ], + }); +}; diff --git a/tests/fixtures/app/typescript-embroider/eslint.config.mjs b/tests/fixtures/app/typescript-embroider/eslint.config.mjs new file mode 100644 index 0000000000..5232ee68dd --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/eslint.config.mjs @@ -0,0 +1,151 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import globals from 'globals'; +import js from '@eslint/js'; + +import ts from 'typescript-eslint'; + +import ember from 'eslint-plugin-ember/recommended'; + +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const parserOptions = { + esm: { + js: { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + [ + '@babel/plugin-proposal-decorators', + { decoratorsBeforeExport: true }, + ], + ], + }, + }, + ts: { + projectService: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, +}; + +export default ts.config( + js.configs.recommended, + ember.configs.base, + ember.configs.gjs, + ember.configs.gts, + eslintConfigPrettier, + /** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: parserOptions.esm.js, + globals: { + ...globals.browser, + }, + }, + }, + { + files: ['**/*.{ts,gts}'], + languageOptions: { + parser: ember.parser, + parserOptions: parserOptions.esm.ts, + }, + extends: [...ts.configs.recommendedTypeChecked, ember.configs.gts], + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs,ts,gts}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: parserOptions.esm.js, + globals: { + ...globals.node, + }, + }, + }, +); diff --git a/tests/fixtures/app/typescript-embroider/package.json b/tests/fixtures/app/typescript-embroider/package.json new file mode 100644 index 0000000000..fd97e9c911 --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/package.json @@ -0,0 +1,104 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"yarn:lint:css -- --fix\"", + "lint:fix": "concurrently \"yarn:lint:*:fix\" --names \"fix:\" --prefixColors auto && yarn format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "lint:types": "tsc --noEmit", + "start": "ember serve", + "test": "concurrently \"yarn:lint\" \"yarn:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember-data/adapter": "~5.8.2", + "@ember-data/graph": "~5.8.2", + "@ember-data/json-api": "~5.8.2", + "@ember-data/legacy-compat": "~5.8.2", + "@ember-data/model": "~5.8.2", + "@ember-data/request": "~5.8.2", + "@ember-data/request-utils": "~5.8.2", + "@ember-data/serializer": "~5.8.2", + "@ember-data/store": "~5.8.2", + "@warp-drive/ember": "~5.8.2", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/compat": "^3.9.4", + "@embroider/core": "^3.5.10", + "@embroider/macros": "^1.20.2", + "@embroider/webpack": "^4.1.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.7.7", + "@tsconfig/ember": "^3.0.12", + "@types/qunit": "^2.19.14", + "@types/rsvp": "^4.0.9", + "@warp-drive/core-types": "~5.8.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/typescript-embroider/tests/helpers/index.ts b/tests/fixtures/app/typescript-embroider/tests/helpers/index.ts new file mode 100644 index 0000000000..e190f567ed --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/tests/helpers/index.ts @@ -0,0 +1,43 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, + setupTest as upstreamSetupTest, + type SetupTestOptions, +} from 'ember-qunit'; + +// This file exists to provide wrappers around ember-qunit's +// test setup functions. This way, you can easily extend the setup that is +// needed per test type. + +function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupApplicationTest(hooks, options); + + // Additional setup for application tests can be done here. + // + // For example, if you need an authenticated session for each + // application test, you could do: + // + // hooks.beforeEach(async function () { + // await authenticateSession(); // ember-simple-auth + // }); + // + // This is also a good place to call test setup functions coming + // from other addons: + // + // setupIntl(hooks, 'en-us'); // ember-intl + // setupMirage(hooks); // ember-cli-mirage +} + +function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupRenderingTest(hooks, options); + + // Additional setup for rendering tests can be done here. +} + +function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupTest(hooks, options); + + // Additional setup for unit tests can be done here. +} + +export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/tests/fixtures/app/typescript-embroider/tsconfig.json b/tests/fixtures/app/typescript-embroider/tsconfig.json new file mode 100644 index 0000000000..750f5b2e8f --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@tsconfig/ember", + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "compilerOptions": { + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "foo/tests/*": ["tests/*"], + "foo/*": ["app/*"], + "*": ["types/*"] + }, + "types": ["ember-source/types"] + } +} diff --git a/tests/fixtures/app/typescript-embroider/types/global.d.ts b/tests/fixtures/app/typescript-embroider/types/global.d.ts new file mode 100644 index 0000000000..2c531e29af --- /dev/null +++ b/tests/fixtures/app/typescript-embroider/types/global.d.ts @@ -0,0 +1 @@ +import '@glint/environment-ember-loose'; diff --git a/tests/fixtures/app/typescript-no-ember-data/config/ember-cli-update.json b/tests/fixtures/app/typescript-no-ember-data/config/ember-cli-update.json new file mode 100644 index 0000000000..5b4361ab4c --- /dev/null +++ b/tests/fixtures/app/typescript-no-ember-data/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--ci-provider=github", + "--typescript", + "--no-ember-data" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/typescript-no-ember-data/ember-cli-build.js b/tests/fixtures/app/typescript-no-ember-data/ember-cli-build.js new file mode 100644 index 0000000000..12f8dea587 --- /dev/null +++ b/tests/fixtures/app/typescript-no-ember-data/ember-cli-build.js @@ -0,0 +1,13 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + 'ember-cli-babel': { enableTypeScriptTransform: true }, + + // Add options here + }); + + return app.toTree(); +}; diff --git a/tests/fixtures/app/typescript-no-ember-data/package.json b/tests/fixtures/app/typescript-no-ember-data/package.json new file mode 100644 index 0000000000..8c2d37fbac --- /dev/null +++ b/tests/fixtures/app/typescript-no-ember-data/package.json @@ -0,0 +1,91 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "lint:types": "tsc --noEmit", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.7.7", + "@tsconfig/ember": "^3.0.12", + "@types/qunit": "^2.19.14", + "@types/rsvp": "^4.0.9", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/typescript/.ember-cli b/tests/fixtures/app/typescript/.ember-cli new file mode 100644 index 0000000000..1df2255044 --- /dev/null +++ b/tests/fixtures/app/typescript/.ember-cli @@ -0,0 +1,19 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": true, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "strict" is the default. + */ + "componentAuthoringFormat": "loose", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "strict" is the default + */ + "routeAuthoringFormat": "loose" +} diff --git a/tests/fixtures/app/typescript/app/config/environment.d.ts b/tests/fixtures/app/typescript/app/config/environment.d.ts new file mode 100644 index 0000000000..8f58c600d2 --- /dev/null +++ b/tests/fixtures/app/typescript/app/config/environment.d.ts @@ -0,0 +1,14 @@ +/** + * Type declarations for + * import config from 'foo/config/environment' + */ +declare const config: { + environment: string; + modulePrefix: string; + podModulePrefix: string; + locationType: 'history' | 'hash' | 'none'; + rootURL: string; + APP: Record; +}; + +export default config; diff --git a/tests/fixtures/app/typescript/config/ember-cli-update.json b/tests/fixtures/app/typescript/config/ember-cli-update.json new file mode 100644 index 0000000000..42bd8c889b --- /dev/null +++ b/tests/fixtures/app/typescript/config/ember-cli-update.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--yarn", + "--ci-provider=github", + "--typescript" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/typescript/ember-cli-build.js b/tests/fixtures/app/typescript/ember-cli-build.js new file mode 100644 index 0000000000..d05950bb44 --- /dev/null +++ b/tests/fixtures/app/typescript/ember-cli-build.js @@ -0,0 +1,22 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + emberData: { + deprecations: { + // New projects can safely leave this deprecation disabled. + // If upgrading, to opt-into the deprecated behavior, set this to true and then follow: + // https://deprecations.emberjs.com/id/ember-data-deprecate-store-extends-ember-object + // before upgrading to Ember Data 6.0 + DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: false, + }, + }, + 'ember-cli-babel': { enableTypeScriptTransform: true }, + + // Add options here + }); + + return app.toTree(); +}; diff --git a/tests/fixtures/app/typescript/eslint.config.mjs b/tests/fixtures/app/typescript/eslint.config.mjs new file mode 100644 index 0000000000..5232ee68dd --- /dev/null +++ b/tests/fixtures/app/typescript/eslint.config.mjs @@ -0,0 +1,151 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import globals from 'globals'; +import js from '@eslint/js'; + +import ts from 'typescript-eslint'; + +import ember from 'eslint-plugin-ember/recommended'; + +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const parserOptions = { + esm: { + js: { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + [ + '@babel/plugin-proposal-decorators', + { decoratorsBeforeExport: true }, + ], + ], + }, + }, + ts: { + projectService: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, +}; + +export default ts.config( + js.configs.recommended, + ember.configs.base, + ember.configs.gjs, + ember.configs.gts, + eslintConfigPrettier, + /** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: parserOptions.esm.js, + globals: { + ...globals.browser, + }, + }, + }, + { + files: ['**/*.{ts,gts}'], + languageOptions: { + parser: ember.parser, + parserOptions: parserOptions.esm.ts, + }, + extends: [...ts.configs.recommendedTypeChecked, ember.configs.gts], + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs,ts,gts}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: parserOptions.esm.js, + globals: { + ...globals.node, + }, + }, + }, +); diff --git a/tests/fixtures/app/typescript/package.json b/tests/fixtures/app/typescript/package.json new file mode 100644 index 0000000000..aa212fc0a3 --- /dev/null +++ b/tests/fixtures/app/typescript/package.json @@ -0,0 +1,103 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"yarn:lint:css -- --fix\"", + "lint:fix": "concurrently \"yarn:lint:*:fix\" --names \"fix:\" --prefixColors auto && yarn format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "lint:types": "tsc --noEmit", + "start": "ember serve", + "test": "concurrently \"yarn:lint\" \"yarn:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember-data/adapter": "~5.8.2", + "@ember-data/graph": "~5.8.2", + "@ember-data/json-api": "~5.8.2", + "@ember-data/legacy-compat": "~5.8.2", + "@ember-data/model": "~5.8.2", + "@ember-data/request": "~5.8.2", + "@ember-data/request-utils": "~5.8.2", + "@ember-data/serializer": "~5.8.2", + "@ember-data/store": "~5.8.2", + "@warp-drive/ember": "~5.8.2", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.7.7", + "@tsconfig/ember": "^3.0.12", + "@types/qunit": "^2.19.14", + "@types/rsvp": "^4.0.9", + "@warp-drive/core-types": "~5.8.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.4", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/app/typescript/tests/helpers/index.ts b/tests/fixtures/app/typescript/tests/helpers/index.ts new file mode 100644 index 0000000000..e190f567ed --- /dev/null +++ b/tests/fixtures/app/typescript/tests/helpers/index.ts @@ -0,0 +1,43 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, + setupTest as upstreamSetupTest, + type SetupTestOptions, +} from 'ember-qunit'; + +// This file exists to provide wrappers around ember-qunit's +// test setup functions. This way, you can easily extend the setup that is +// needed per test type. + +function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupApplicationTest(hooks, options); + + // Additional setup for application tests can be done here. + // + // For example, if you need an authenticated session for each + // application test, you could do: + // + // hooks.beforeEach(async function () { + // await authenticateSession(); // ember-simple-auth + // }); + // + // This is also a good place to call test setup functions coming + // from other addons: + // + // setupIntl(hooks, 'en-us'); // ember-intl + // setupMirage(hooks); // ember-cli-mirage +} + +function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupRenderingTest(hooks, options); + + // Additional setup for rendering tests can be done here. +} + +function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupTest(hooks, options); + + // Additional setup for unit tests can be done here. +} + +export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/tests/fixtures/app/typescript/tsconfig.json b/tests/fixtures/app/typescript/tsconfig.json new file mode 100644 index 0000000000..750f5b2e8f --- /dev/null +++ b/tests/fixtures/app/typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@tsconfig/ember", + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "compilerOptions": { + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "foo/tests/*": ["tests/*"], + "foo/*": ["app/*"], + "*": ["types/*"] + }, + "types": ["ember-source/types"] + } +} diff --git a/tests/fixtures/app/typescript/types/global.d.ts b/tests/fixtures/app/typescript/types/global.d.ts new file mode 100644 index 0000000000..2c531e29af --- /dev/null +++ b/tests/fixtures/app/typescript/types/global.d.ts @@ -0,0 +1 @@ +import '@glint/environment-ember-loose'; diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/.prettierrc.js b/tests/fixtures/app/with-blueprint-override-lint-fail/.prettierrc.js new file mode 100644 index 0000000000..6d05baa3d4 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/.prettierrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + singleQuote: true, // blueprint overrides use double quotes +}; diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/.template-lintrc.js b/tests/fixtures/app/with-blueprint-override-lint-fail/.template-lintrc.js new file mode 100644 index 0000000000..8360c930a4 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/.template-lintrc.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = { + extends: 'recommended', + rules: { + 'require-button-type': true, // blueprint override has missing button type + }, +}; diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/README.md b/tests/fixtures/app/with-blueprint-override-lint-fail/README.md new file mode 100644 index 0000000000..b28014ce73 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/README.md @@ -0,0 +1,56 @@ +# foo + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) (with npm) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd foo` +- `npm install` + +## Running / Development + +- `npm start` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `npm run test:ember` +- `npm run test:ember --server` + +### Linting + +- `npm run lint` +- `npm run lint:fix` + +### Building + +- `npm exec ember build` (development) +- `npm run build` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/app/templates/application.hbs b/tests/fixtures/app/with-blueprint-override-lint-fail/app/templates/application.hbs new file mode 100644 index 0000000000..0d0ebcc939 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/app/templates/application.hbs @@ -0,0 +1,7 @@ +{{page-title "Foo"}} + +{{! The following component displays Ember's default welcome message. }} + +{{! Feel free to remove this! }} + +{{outlet}} \ No newline at end of file diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/blueprints/component/files/__root__/__path__/__name__.hbs b/tests/fixtures/app/with-blueprint-override-lint-fail/blueprints/component/files/__root__/__path__/__name__.hbs new file mode 100644 index 0000000000..f72a7ea3e6 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/blueprints/component/files/__root__/__path__/__name__.hbs @@ -0,0 +1 @@ + diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/blueprints/component/files/__root__/__path__/__name__.js b/tests/fixtures/app/with-blueprint-override-lint-fail/blueprints/component/files/__root__/__path__/__name__.js new file mode 100644 index 0000000000..535c9547c8 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/blueprints/component/files/__root__/__path__/__name__.js @@ -0,0 +1,5 @@ +const foo = 'bar'; +// https://eslint.org/docs/latest/rules/no-extra-boolean-cast: +if (!!foo) { + console.log(foo); +} diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/config/ember-cli-update.json b/tests/fixtures/app/with-blueprint-override-lint-fail/config/ember-cli-update.json new file mode 100644 index 0000000000..4ef8ba54e3 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/config/ember-cli-update.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [] + } + ] + } + ] +} diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/ember-cli-build.js b/tests/fixtures/app/with-blueprint-override-lint-fail/ember-cli-build.js new file mode 100644 index 0000000000..ed991bd98f --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/ember-cli-build.js @@ -0,0 +1,11 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + // Add options here + }); + + return app.toTree(); +}; diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/eslint.config.mjs b/tests/fixtures/app/with-blueprint-override-lint-fail/eslint.config.mjs new file mode 100644 index 0000000000..a29e5cd3bd --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/eslint.config.mjs @@ -0,0 +1,126 @@ +/** + * Debugging: + * https://eslint.org/docs/latest/use/configure/debug + * ---------------------------------------------------- + * + * Print a file's calculated configuration + * + * npx eslint --print-config path/to/file.js + * + * Inspecting the config + * + * npx eslint --inspect-config + * + */ +import globals from 'globals'; +import js from '@eslint/js'; + +import ember from 'eslint-plugin-ember/recommended'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import babelParser from '@babel/eslint-parser'; + +const esmParserOptions = { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + requireConfigFile: false, + babelOptions: { + plugins: [ + ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], + ], + }, +}; + +export default [ + js.configs.recommended, + eslintConfigPrettier, + ember.configs.base, + ember.configs.gjs, + /** + * Ignores must be in their own object + * https://eslint.org/docs/latest/use/configure/ignore + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + }, + /** + * https://eslint.org/docs/latest/use/configure/configuration-files#configuring-linter-options + */ + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + }, + }, + { + files: ['**/*.{js,gjs}'], + languageOptions: { + parserOptions: esmParserOptions, + globals: { + ...globals.browser, + }, + }, + }, + { + ...qunit.configs.recommended, + files: ['tests/**/*-test.{js,gjs}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + ...n.configs['flat/recommended-script'], + files: [ + '**/*.cjs', + 'config/**/*.js', + 'tests/dummy/config/**/*.js', + 'testem.js', + 'testem*.js', + 'index.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + ...n.configs['flat/recommended-module'], + files: ['**/*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: esmParserOptions, + globals: { + ...globals.node, + }, + }, + }, +]; diff --git a/tests/fixtures/app/with-blueprint-override-lint-fail/package.json b/tests/fixtures/app/with-blueprint-override-lint-fail/package.json new file mode 100644 index 0000000000..465a4f8233 --- /dev/null +++ b/tests/fixtures/app/with-blueprint-override-lint-fail/package.json @@ -0,0 +1,77 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~5.8.2", + "ember-fetch": "^8.1.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "stylelint-prettier": "^5.0.3" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/installation-checker/empty/.gitkeep b/tests/fixtures/app/with-default-ember-debug/.gitkeep similarity index 100% rename from tests/fixtures/installation-checker/empty/.gitkeep rename to tests/fixtures/app/with-default-ember-debug/.gitkeep diff --git a/tests/fixtures/app/with-default-ember-debug/package.json b/tests/fixtures/app/with-default-ember-debug/package.json new file mode 100644 index 0000000000..af18c2df9d --- /dev/null +++ b/tests/fixtures/app/with-default-ember-debug/package.json @@ -0,0 +1,6 @@ +{ + "name": "cool-app", + "devDependencies": { + "loader.js": "latest" + } +} diff --git a/tests/fixtures/installation-checker/valid-bower-installation/bower_components/.gitkeep b/tests/fixtures/app/without-ember-debug/.gitkeep similarity index 100% rename from tests/fixtures/installation-checker/valid-bower-installation/bower_components/.gitkeep rename to tests/fixtures/app/without-ember-debug/.gitkeep diff --git a/blueprints/template/files/__root__/__path__/__name__.hbs b/tests/fixtures/app/without-ember-debug/bower_components/ember/ember.js similarity index 100% rename from blueprints/template/files/__root__/__path__/__name__.hbs rename to tests/fixtures/app/without-ember-debug/bower_components/ember/ember.js diff --git a/tests/fixtures/app/without-ember-debug/package.json b/tests/fixtures/app/without-ember-debug/package.json new file mode 100644 index 0000000000..af18c2df9d --- /dev/null +++ b/tests/fixtures/app/without-ember-debug/package.json @@ -0,0 +1,6 @@ +{ + "name": "cool-app", + "devDependencies": { + "loader.js": "latest" + } +} diff --git a/tests/fixtures/app/yarn/.github/workflows/ci.yml b/tests/fixtures/app/yarn/.github/workflows/ci.yml new file mode 100644 index 0000000000..5e8dcd9186 --- /dev/null +++ b/tests/fixtures/app/yarn/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Lint + run: yarn lint + + test: + name: "Test" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Run Tests + run: yarn test diff --git a/tests/fixtures/app/yarn/README.md b/tests/fixtures/app/yarn/README.md new file mode 100644 index 0000000000..b029dd6e5d --- /dev/null +++ b/tests/fixtures/app/yarn/README.md @@ -0,0 +1,57 @@ +# foo + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) +- [Yarn](https://yarnpkg.com/) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd foo` +- `yarn install` + +## Running / Development + +- `yarn start` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `yarn test` +- `yarn test:ember --server` + +### Linting + +- `yarn lint` +- `yarn lint:fix` + +### Building + +- `yarn ember build` (development) +- `yarn build` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/fixtures/app/yarn/app/templates/application.hbs b/tests/fixtures/app/yarn/app/templates/application.hbs new file mode 100644 index 0000000000..00697db19a --- /dev/null +++ b/tests/fixtures/app/yarn/app/templates/application.hbs @@ -0,0 +1,7 @@ +{{page-title "Foo"}} + +{{outlet}} + +{{! The following component displays Ember's default welcome message. }} + +{{! Feel free to remove this! }} \ No newline at end of file diff --git a/tests/fixtures/app/yarn/config/ember-cli-update.json b/tests/fixtures/app/yarn/config/ember-cli-update.json new file mode 100644 index 0000000000..27b4c05443 --- /dev/null +++ b/tests/fixtures/app/yarn/config/ember-cli-update.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "version": "<%= blueprintVersion %>", + "blueprints": [ + { + "name": "@ember-tooling/classic-build-app-blueprint", + "isBaseBlueprint": true, + "options": [ + "--yarn", + "--ci-provider=github" + ] + } + ] + } + ] +} diff --git a/tests/fixtures/app/yarn/package.json b/tests/fixtures/app/yarn/package.json new file mode 100644 index 0000000000..6c914b4414 --- /dev/null +++ b/tests/fixtures/app/yarn/package.json @@ -0,0 +1,83 @@ +{ + "name": "foo", + "version": "0.0.0", + "private": true, + "description": "Small description for foo goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "format": "prettier . --cache --write", + "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", + "lint:css": "stylelint \"**/*.css\"", + "lint:css:fix": "concurrently \"yarn:lint:css -- --fix\"", + "lint:fix": "concurrently \"yarn:lint:*:fix\" --names \"fix:\" --prefixColors auto && yarn format", + "lint:format": "prettier . --cache --check", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"yarn:lint\" \"yarn:test:*\" --names \"lint,test:\" --prefixColors auto", + "test:ember": "ember test" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-proposal-decorators": "^7.29.0", + "@ember/optional-features": "^3.0.0", + "@ember/test-helpers": "^5.4.2", + "@embroider/macros": "^1.20.2", + "@eslint/js": "^9.39.4", + "@glimmer/component": "^2.1.1", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^9.2.1", + "ember-auto-import": "^2.13.1", + "ember-cli": "~<%= emberCLIVersion %>", + "ember-cli-app-version": "^7.0.0", + "ember-cli-babel": "^8.3.1", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.4.0", + "ember-cli-deprecation-workflow": "^4.0.1", + "ember-cli-htmlbars": "^7.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~5.8.2", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.3.0", + "ember-page-title": "^9.0.3", + "ember-qunit": "^9.0.4", + "ember-resolver": "^13.2.0", + "ember-source": "~7.2.0-alpha.3", + "ember-template-imports": "^4.4.0", + "ember-template-lint": "^6.1.0", + "ember-welcome-page": "^8.0.5", + "eslint": "^9.39.4", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-ember": "^12.7.5", + "eslint-plugin-n": "^17.24.0", + "eslint-plugin-qunit": "^8.2.6", + "globals": "^15.15.0", + "loader.js": "^4.7.0", + "prettier": "^3.8.3", + "prettier-plugin-ember-template-tag": "^2.1.6", + "qunit": "^2.25.0", + "qunit-dom": "^3.5.1", + "stylelint": "^16.26.1", + "stylelint-config-standard": "^36.0.1", + "webpack": "^5.107.1" + }, + "engines": { + "node": ">= 20.19" + }, + "ember": { + "edition": "octane" + } +} diff --git a/tests/fixtures/babel-error-stack/input.txt b/tests/fixtures/babel-error-stack/input.txt deleted file mode 100644 index 881af0042e..0000000000 --- a/tests/fixtures/babel-error-stack/input.txt +++ /dev/null @@ -1,18 +0,0 @@ - 10 | podModulePrefix: config.podModulePrefix, - 11 | Resolver: Resolver -> 12 | asdf - | ^ - 13 | }); - 14 | - 15 | loadInitializers(App, config.modulePrefix); -SyntaxError: ember-jobs/app.js: Unexpected token (12:2) - at raise (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:343:15) - at unexpected (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:2278:5) - at expect (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:2266:18) - at parseObj (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3434:9) - at parseExprAtom (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3258:14) - at parseExprSubscripts (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3117:16) - at parseMaybeUnary (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3099:16) - at parseExprOps (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3053:16) - at parseMaybeConditional (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3029:16) - at parseMaybeAssign (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3008:16) diff --git a/tests/fixtures/babel-error-stack/output.txt b/tests/fixtures/babel-error-stack/output.txt deleted file mode 100644 index 5c64bd380c..0000000000 --- a/tests/fixtures/babel-error-stack/output.txt +++ /dev/null @@ -1,11 +0,0 @@ -SyntaxError: ember-jobs/app.js: Unexpected token (12:2) - at raise (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:343:15) - at unexpected (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:2278:5) - at expect (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:2266:18) - at parseObj (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3434:9) - at parseExprAtom (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3258:14) - at parseExprSubscripts (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3117:16) - at parseMaybeUnary (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3099:16) - at parseExprOps (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3053:16) - at parseMaybeConditional (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3029:16) - at parseMaybeAssign (/Users/stefanpenner/tmp/ember-jobs/node_modules/ember-cli-babel/node_modules/broccoli-babel-transpiler/node_modules/babel-core/node_modules/acorn-babel/acorn_csp.js:3008:16) diff --git a/tests/fixtures/blueprints/basic-esm/files/.ember-cli b/tests/fixtures/blueprints/basic-esm/files/.ember-cli new file mode 100644 index 0000000000..62a457e2bc --- /dev/null +++ b/tests/fixtures/blueprints/basic-esm/files/.ember-cli @@ -0,0 +1,7 @@ +{ + "port": 4200, + "host": "0.0.0.0", + "live-reload": true, + "environment": "development", + "checkForUpdates": false +} diff --git a/tests/fixtures/addon/shared-package/base/node_modules/blah-blah/index.js b/tests/fixtures/blueprints/basic-esm/files/.gitignore similarity index 100% rename from tests/fixtures/addon/shared-package/base/node_modules/blah-blah/index.js rename to tests/fixtures/blueprints/basic-esm/files/.gitignore diff --git a/tests/fixtures/blueprints/basic-esm/files/__root__/__path__/__name__.txt b/tests/fixtures/blueprints/basic-esm/files/__root__/__path__/__name__.txt new file mode 100644 index 0000000000..02f6335fc4 --- /dev/null +++ b/tests/fixtures/blueprints/basic-esm/files/__root__/__path__/__name__.txt @@ -0,0 +1 @@ +a file diff --git a/tests/fixtures/addon/shared-package/base/node_modules/foo-bar/index.js b/tests/fixtures/blueprints/basic-esm/files/bar similarity index 100% rename from tests/fixtures/addon/shared-package/base/node_modules/foo-bar/index.js rename to tests/fixtures/blueprints/basic-esm/files/bar diff --git a/tests/fixtures/addon/shared-package/node_modules/dev-foo-bar/index.js b/tests/fixtures/blueprints/basic-esm/files/file-to-remove.txt similarity index 100% rename from tests/fixtures/addon/shared-package/node_modules/dev-foo-bar/index.js rename to tests/fixtures/blueprints/basic-esm/files/file-to-remove.txt diff --git a/tests/fixtures/blueprints/basic-esm/files/foo.txt b/tests/fixtures/blueprints/basic-esm/files/foo.txt new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/tests/fixtures/blueprints/basic-esm/files/foo.txt @@ -0,0 +1 @@ +foo diff --git a/tests/fixtures/blueprints/basic-esm/files/test.txt b/tests/fixtures/blueprints/basic-esm/files/test.txt new file mode 100644 index 0000000000..504408399f --- /dev/null +++ b/tests/fixtures/blueprints/basic-esm/files/test.txt @@ -0,0 +1 @@ +I AM <%= replacementTest %> diff --git a/tests/fixtures/blueprints/basic-esm/index.js b/tests/fixtures/blueprints/basic-esm/index.js new file mode 100644 index 0000000000..250a0bfd5b --- /dev/null +++ b/tests/fixtures/blueprints/basic-esm/index.js @@ -0,0 +1,12 @@ +'use strict'; + +import Blueprint from '@ember-tooling/blueprint-model'; + +export default class BasicEsm extends Blueprint { + description = 'A basic blueprint'; + beforeInstall(options, locals) { + return Promise.resolve().then(function () { + locals.replacementTest = 'TESTY'; + }); + } +} diff --git a/tests/fixtures/blueprints/basic-esm/package.json b/tests/fixtures/blueprints/basic-esm/package.json new file mode 100644 index 0000000000..5529b591a7 --- /dev/null +++ b/tests/fixtures/blueprints/basic-esm/package.json @@ -0,0 +1,4 @@ +{ + "name": "basic-esm", + "type": "module" +} diff --git a/tests/fixtures/blueprints/basic/files/__root__/__path__/__name__.txt b/tests/fixtures/blueprints/basic/files/__root__/__path__/__name__.txt new file mode 100644 index 0000000000..02f6335fc4 --- /dev/null +++ b/tests/fixtures/blueprints/basic/files/__root__/__path__/__name__.txt @@ -0,0 +1 @@ +a file diff --git a/tests/fixtures/brocfile-tests/query/app/resolver.js b/tests/fixtures/blueprints/basic/files/file-to-remove.txt similarity index 100% rename from tests/fixtures/brocfile-tests/query/app/resolver.js rename to tests/fixtures/blueprints/basic/files/file-to-remove.txt diff --git a/tests/fixtures/blueprints/basic/index.js b/tests/fixtures/blueprints/basic/index.js index 2b9c745c8a..4ab6205c2f 100644 --- a/tests/fixtures/blueprints/basic/index.js +++ b/tests/fixtures/blueprints/basic/index.js @@ -1,11 +1,10 @@ 'use strict'; -var Blueprint = require('../../../../lib/models/blueprint'); -var Promise = require('../../../../lib/ext/promise'); +const Blueprint = require ('@ember-tooling/blueprint-model'); module.exports = Blueprint.extend({ description: 'A basic blueprint', - beforeInstall: function(options, locals){ + beforeInstall(options, locals){ return Promise.resolve().then(function(){ locals.replacementTest = 'TESTY'; }); diff --git a/tests/fixtures/blueprints/basic_2/index.js b/tests/fixtures/blueprints/basic_2/index.js index af91127905..1f7f4593f0 100644 --- a/tests/fixtures/blueprints/basic_2/index.js +++ b/tests/fixtures/blueprints/basic_2/index.js @@ -1,3 +1,5 @@ module.exports = { - description: 'Another basic blueprint' + description: 'Another basic blueprint', + + filesToRemove: ['file-to-remove.txt'] }; diff --git a/tests/fixtures/bower-directory-tests/bowerrc-with-directory/.bowerrc b/tests/fixtures/bower-directory-tests/bowerrc-with-directory/.bowerrc deleted file mode 100644 index 6866ac2ca7..0000000000 --- a/tests/fixtures/bower-directory-tests/bowerrc-with-directory/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "vendor" -} diff --git a/tests/fixtures/bower-directory-tests/bowerrc-without-directory/.bowerrc b/tests/fixtures/bower-directory-tests/bowerrc-without-directory/.bowerrc deleted file mode 100644 index 2c63c08510..0000000000 --- a/tests/fixtures/bower-directory-tests/bowerrc-without-directory/.bowerrc +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/tests/fixtures/bower-directory-tests/invalid-bowerrc/.bowerrc b/tests/fixtures/bower-directory-tests/invalid-bowerrc/.bowerrc deleted file mode 100644 index 43691615e4..0000000000 --- a/tests/fixtures/bower-directory-tests/invalid-bowerrc/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -({ - "directory": "vendor" -}) diff --git a/tests/fixtures/brocfile-tests/additional-trees/ember-cli-build.js b/tests/fixtures/brocfile-tests/additional-trees/ember-cli-build.js new file mode 100644 index 0000000000..a9220b574c --- /dev/null +++ b/tests/fixtures/brocfile-tests/additional-trees/ember-cli-build.js @@ -0,0 +1,21 @@ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const Funnel = require('broccoli-funnel'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, {}); + + let funnel = new Funnel('vendor', { + srcDir: '/', + destDir: '/assets' + }); + + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + extraPublicTrees: [funnel] + }); + } + + return app.toTree(funnel); +}; diff --git a/tests/fixtures/brocfile-tests/additional-trees/vendor/custom-output-file.css b/tests/fixtures/brocfile-tests/additional-trees/vendor/custom-output-file.css new file mode 100644 index 0000000000..067dada537 --- /dev/null +++ b/tests/fixtures/brocfile-tests/additional-trees/vendor/custom-output-file.css @@ -0,0 +1,3 @@ +body:after { + content: "hello from css custom output file" +} diff --git a/tests/fixtures/brocfile-tests/additional-trees/vendor/custom-output-file.js b/tests/fixtures/brocfile-tests/additional-trees/vendor/custom-output-file.js new file mode 100644 index 0000000000..20c572029c --- /dev/null +++ b/tests/fixtures/brocfile-tests/additional-trees/vendor/custom-output-file.js @@ -0,0 +1 @@ +console.log("hello from js custom output file"); diff --git a/tests/fixtures/brocfile-tests/app-import-anonymous-amd/ember-cli-build.js b/tests/fixtures/brocfile-tests/app-import-anonymous-amd/ember-cli-build.js new file mode 100644 index 0000000000..8795a1c888 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-anonymous-amd/ember-cli-build.js @@ -0,0 +1,28 @@ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +module.exports = function (defaults) { + var app = new EmberApp(defaults, { + }); + + app.import('vendor/anonymous-amd-example.js', { + using: [ + { transformation: 'amd', as: 'hello-world'} + ], + outputFile: '/assets/output.js' + }); + + app.import('vendor/anonymous-amd-example.js', { + using: [ + { transformation: 'amd', as: 'hello-world'} + ], + outputFile: '/assets/output.js' + }); + + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack); + } + + return app.toTree(); +}; diff --git a/tests/fixtures/brocfile-tests/app-import-anonymous-amd/vendor/anonymous-amd-example.js b/tests/fixtures/brocfile-tests/app-import-anonymous-amd/vendor/anonymous-amd-example.js new file mode 100644 index 0000000000..e6fb4334c9 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-anonymous-amd/vendor/anonymous-amd-example.js @@ -0,0 +1,10 @@ +(function() { + function helloWorld() { + return "Hello World"; + } + if (typeof define === "function" && define.amd) { + define([], function () { return helloWorld; }); + } else { + throw new Error("No amd loader found"); + } +})(); diff --git a/tests/fixtures/brocfile-tests/app-import-custom-transform/bower_components/ember/ember.js b/tests/fixtures/brocfile-tests/app-import-custom-transform/bower_components/ember/ember.js new file mode 100644 index 0000000000..650c50855e --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-custom-transform/bower_components/ember/ember.js @@ -0,0 +1,3 @@ +window.Ember = { + foo: 'bar'; +}; diff --git a/tests/fixtures/brocfile-tests/app-import-custom-transform/bower_components/jquery/dist/jquery.js b/tests/fixtures/brocfile-tests/app-import-custom-transform/bower_components/jquery/dist/jquery.js new file mode 100644 index 0000000000..d59c33630f --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-custom-transform/bower_components/jquery/dist/jquery.js @@ -0,0 +1,3 @@ +window.$ = function() { + console.log('this is jQuery!'); +}; diff --git a/tests/fixtures/brocfile-tests/app-import-custom-transform/ember-cli-build.js b/tests/fixtures/brocfile-tests/app-import-custom-transform/ember-cli-build.js new file mode 100644 index 0000000000..233aa1e6e3 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-custom-transform/ember-cli-build.js @@ -0,0 +1,23 @@ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +module.exports = function (defaults) { + var app = new EmberApp(defaults, { + }); + + app.import('vendor/custom-transform-example.js', { + using: [ + { + transformation: 'fastbootShim' + } + ], + outputFile: '/assets/output.js' + }); + + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack); + } + + return app.toTree(); +}; diff --git a/tests/fixtures/brocfile-tests/app-import-custom-transform/node_modules/ember-transform-addon/index.js b/tests/fixtures/brocfile-tests/app-import-custom-transform/node_modules/ember-transform-addon/index.js new file mode 100644 index 0000000000..e2e1ad8f76 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-custom-transform/node_modules/ember-transform-addon/index.js @@ -0,0 +1,28 @@ +'use strict'; + +const map = require('broccoli-stew').map; + +module.exports = { + name: require('./package').name, + + importTransforms: () => { + return { + fastbootShim: tree => { + return map(tree, content => { + return `if (typeof FastBoot === 'undefined') { ${content} }`; + }); + } + } + }, + + included(app) { + this._super.included.apply(this, arguments); + + app.import('vendor/addon-vendor.js', { + using: [ + { transformation: 'amd', as: 'addon-vendor' } + ], + outputFile: '/assets/addon-output.js' + }); + } +}; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/package.json b/tests/fixtures/brocfile-tests/app-import-custom-transform/node_modules/ember-transform-addon/package.json similarity index 70% rename from tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/package.json rename to tests/fixtures/brocfile-tests/app-import-custom-transform/node_modules/ember-transform-addon/package.json index caad24f26f..ba221c47fa 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/package.json +++ b/tests/fixtures/brocfile-tests/app-import-custom-transform/node_modules/ember-transform-addon/package.json @@ -1,9 +1,8 @@ { - "name": "ember-cool-addon", + "name": "ember-transform-addon", "private": true, "version": "0.0.0", "keywords": [ "ember-addon" ] } - diff --git a/tests/fixtures/brocfile-tests/app-import-custom-transform/vendor/addon-vendor.js b/tests/fixtures/brocfile-tests/app-import-custom-transform/vendor/addon-vendor.js new file mode 100644 index 0000000000..dca1b04b2d --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-custom-transform/vendor/addon-vendor.js @@ -0,0 +1,10 @@ +(function() { + function helloWorld() { + return "Hello World"; + } + if (typeof define === "function" && define.amd) { + define([], function () { return helloWorld; }); + } else { + throw new Error("No amd loader found"); + } +})(); \ No newline at end of file diff --git a/tests/fixtures/brocfile-tests/app-import-custom-transform/vendor/custom-transform-example.js b/tests/fixtures/brocfile-tests/app-import-custom-transform/vendor/custom-transform-example.js new file mode 100644 index 0000000000..1ed52b466d --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-custom-transform/vendor/custom-transform-example.js @@ -0,0 +1 @@ +window.hello = "hello world"; \ No newline at end of file diff --git a/tests/fixtures/brocfile-tests/app-import-named-umd/ember-cli-build.js b/tests/fixtures/brocfile-tests/app-import-named-umd/ember-cli-build.js new file mode 100644 index 0000000000..946c2e5d7b --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-named-umd/ember-cli-build.js @@ -0,0 +1,28 @@ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +module.exports = function (defaults) { + var app = new EmberApp(defaults, { + }); + + app.import('vendor/named-umd-example.js', { + using: [ + { transformation: 'amd'} + ], + outputFile: '/assets/output.js' + }); + + app.import('vendor/named-umd-example.js', { + using: [ + { transformation: 'amd'} + ], + outputFile: '/assets/output.js' + }); + + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack); + } + + return app.toTree(); +}; diff --git a/tests/fixtures/brocfile-tests/app-import-named-umd/vendor/named-umd-example.js b/tests/fixtures/brocfile-tests/app-import-named-umd/vendor/named-umd-example.js new file mode 100644 index 0000000000..4438314d39 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-named-umd/vendor/named-umd-example.js @@ -0,0 +1,11 @@ +!function(e, t) { + if ("function" == typeof define && define.amd) { + define("hello-world", [], t); + } else { + throw new Error("No amd loader found"); + } +}(this, function() { + return function helloWorld() { + return "Hello World"; + } +}); diff --git a/tests/fixtures/brocfile-tests/app-import-output-file/ember-cli-build.js b/tests/fixtures/brocfile-tests/app-import-output-file/ember-cli-build.js new file mode 100644 index 0000000000..f6732e1eea --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-output-file/ember-cli-build.js @@ -0,0 +1,16 @@ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +module.exports = function (defaults) { + var app = new EmberApp(defaults, {}); + + app.import('vendor/custom-output-file.js', {outputFile: '/assets/output-file.js'}); + app.import('vendor/custom-output-file.css', {outputFile: '/assets/output-file.css'}); + + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack); + } + + return app.toTree(); +}; diff --git a/tests/fixtures/brocfile-tests/app-import-output-file/vendor/custom-output-file.css b/tests/fixtures/brocfile-tests/app-import-output-file/vendor/custom-output-file.css new file mode 100644 index 0000000000..067dada537 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-output-file/vendor/custom-output-file.css @@ -0,0 +1,3 @@ +body:after { + content: "hello from css custom output file" +} diff --git a/tests/fixtures/brocfile-tests/app-import-output-file/vendor/custom-output-file.js b/tests/fixtures/brocfile-tests/app-import-output-file/vendor/custom-output-file.js new file mode 100644 index 0000000000..20c572029c --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-import-output-file/vendor/custom-output-file.js @@ -0,0 +1 @@ +console.log("hello from js custom output file"); diff --git a/tests/fixtures/installation-checker/valid-npm-installation/node_modules/.gitkeep b/tests/fixtures/brocfile-tests/app-import/.gitkeep similarity index 100% rename from tests/fixtures/installation-checker/valid-npm-installation/node_modules/.gitkeep rename to tests/fixtures/brocfile-tests/app-import/.gitkeep diff --git a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/index.js b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/index.js index ceb89d7050..0362cafcc7 100644 --- a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/index.js +++ b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/index.js @@ -1,9 +1,9 @@ 'use strict'; module.exports = { - name: 'Ember Random Addon', + name: require('./package').name, - included: function included(app) { + included(app) { this._super.included(app); app.import('vendor/ember-random-addon/file-to-import.js', { type: 'jabascript' }); diff --git a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/package.json b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/package.json index 270e68ede4..9c4abfbfec 100644 --- a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/package.json +++ b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-bad-addon/package.json @@ -6,4 +6,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/index.js b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/index.js index dd99b65046..c1238d216e 100644 --- a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/index.js +++ b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/index.js @@ -1,9 +1,9 @@ 'use strict'; module.exports = { - name: 'Ember Random Addon', + name: require('./package').name, - included: function included(app) { + included(app) { this._super.included(app); app.import('vendor/ember-random-addon/file-to-import.txt', { destDir: 'assets' }); diff --git a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/package.json b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/package.json index 49d68fa7ac..9d79fd8a6b 100644 --- a/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/package.json +++ b/tests/fixtures/brocfile-tests/app-import/node_modules/ember-random-addon/package.json @@ -6,4 +6,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/installation-checker/invalid-bower-and-npm/bower.json b/tests/fixtures/brocfile-tests/app-test-import/.gitkeep similarity index 100% rename from tests/fixtures/installation-checker/invalid-bower-and-npm/bower.json rename to tests/fixtures/brocfile-tests/app-test-import/.gitkeep diff --git a/tests/fixtures/brocfile-tests/app-test-import/node_modules/ember-test-addon/index.js b/tests/fixtures/brocfile-tests/app-test-import/node_modules/ember-test-addon/index.js new file mode 100644 index 0000000000..94b1223c76 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-test-import/node_modules/ember-test-addon/index.js @@ -0,0 +1,40 @@ +'use strict'; + +const fs = require('fs'); +const Plugin = require('broccoli-plugin'); +const mergeTrees = require('broccoli-merge-trees'); +const path = require('path'); + +class AddCustomFile extends Plugin { + constructor(inputNodes, options) { + super(...arguments); + this.path = options.path; + } + + build() { + let outPath = path.join(this.outputPath, this.path); + fs.mkdirSync(path.dirname(outPath)); + fs.writeFileSync(outPath, '// File for test tree imported and added via postprocessTree()'); + } +} + +const customTestFilePath = "my-custom-test-assets/test-dependency.js"; + +module.exports = { + name: require('./package').name, + + included(app) { + this._super.included(app); + + app.import(customTestFilePath, { type: 'test' }); + }, + + postprocessTree(type, tree){ + if (type === 'test') { + return mergeTrees([tree, new AddCustomFile([tree], { + path: customTestFilePath + })]); + } + return tree; + } +}; diff --git a/tests/fixtures/brocfile-tests/app-test-import/node_modules/ember-test-addon/package.json b/tests/fixtures/brocfile-tests/app-test-import/node_modules/ember-test-addon/package.json new file mode 100644 index 0000000000..a4b0fb1528 --- /dev/null +++ b/tests/fixtures/brocfile-tests/app-test-import/node_modules/ember-test-addon/package.json @@ -0,0 +1,11 @@ +{ + "name": "ember-test-addon", + "private": true, + "version": "0.0.0", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "broccoli-plugin": "^4.0.7" + } +} diff --git a/tests/fixtures/brocfile-tests/auto-run-false/app/app.js b/tests/fixtures/brocfile-tests/auto-run-false/app/app.js new file mode 100644 index 0000000000..d3809c8ca7 --- /dev/null +++ b/tests/fixtures/brocfile-tests/auto-run-false/app/app.js @@ -0,0 +1,17 @@ +import Application from '@ember/application'; +import Resolver from 'ember-resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from 'some-cool-app/config/environment'; + +export default class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = Resolver; + + init() { + super.init(); + window.APP_HAS_LOADED = true; + } +} + +loadInitializers(App, config.modulePrefix); \ No newline at end of file diff --git a/tests/fixtures/brocfile-tests/auto-run-false/config/targets.js b/tests/fixtures/brocfile-tests/auto-run-false/config/targets.js new file mode 100644 index 0000000000..b55f37e561 --- /dev/null +++ b/tests/fixtures/brocfile-tests/auto-run-false/config/targets.js @@ -0,0 +1,10 @@ +const browsers = [ + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions', +]; + +module.exports = { + browsers, + node: 'current', +}; diff --git a/tests/fixtures/brocfile-tests/auto-run-false/ember-cli-build.js b/tests/fixtures/brocfile-tests/auto-run-false/ember-cli-build.js index 05e9cedece..91eb7f7859 100644 --- a/tests/fixtures/brocfile-tests/auto-run-false/ember-cli-build.js +++ b/tests/fixtures/brocfile-tests/auto-run-false/ember-cli-build.js @@ -1,11 +1,15 @@ -/* global require, module */ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function(defaults) { +module.exports = function (defaults) { var app = new EmberApp(defaults, { autoRun: false }); + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack); + } + return app.toTree(); }; diff --git a/tests/fixtures/brocfile-tests/auto-run-true/app/app.js b/tests/fixtures/brocfile-tests/auto-run-true/app/app.js new file mode 100644 index 0000000000..d3809c8ca7 --- /dev/null +++ b/tests/fixtures/brocfile-tests/auto-run-true/app/app.js @@ -0,0 +1,17 @@ +import Application from '@ember/application'; +import Resolver from 'ember-resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from 'some-cool-app/config/environment'; + +export default class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = Resolver; + + init() { + super.init(); + window.APP_HAS_LOADED = true; + } +} + +loadInitializers(App, config.modulePrefix); \ No newline at end of file diff --git a/tests/fixtures/brocfile-tests/auto-run-true/config/targets.js b/tests/fixtures/brocfile-tests/auto-run-true/config/targets.js new file mode 100644 index 0000000000..b55f37e561 --- /dev/null +++ b/tests/fixtures/brocfile-tests/auto-run-true/config/targets.js @@ -0,0 +1,10 @@ +const browsers = [ + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions', +]; + +module.exports = { + browsers, + node: 'current', +}; diff --git a/tests/fixtures/brocfile-tests/auto-run-true/ember-cli-build.js b/tests/fixtures/brocfile-tests/auto-run-true/ember-cli-build.js index af5912bf0b..c2e1dd8a56 100644 --- a/tests/fixtures/brocfile-tests/auto-run-true/ember-cli-build.js +++ b/tests/fixtures/brocfile-tests/auto-run-true/ember-cli-build.js @@ -1,11 +1,15 @@ -/* global require, module */ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function(defaults) { +module.exports = function (defaults) { var app = new EmberApp(defaults, { autoRun: true }); + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack); + } + return app.toTree(); }; diff --git a/tests/fixtures/brocfile-tests/both-build-files/Brocfile.js b/tests/fixtures/brocfile-tests/both-build-files/Brocfile.js deleted file mode 100644 index 1a1bb9eef7..0000000000 --- a/tests/fixtures/brocfile-tests/both-build-files/Brocfile.js +++ /dev/null @@ -1,6 +0,0 @@ -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); -var app = new EmberApp(); - -app.import('vendor/brocfile-script.js'); - -module.exports = app.toTree(); diff --git a/tests/fixtures/brocfile-tests/both-build-files/ember-cli-build.js b/tests/fixtures/brocfile-tests/both-build-files/ember-cli-build.js deleted file mode 100644 index 18d824cda5..0000000000 --- a/tests/fixtures/brocfile-tests/both-build-files/ember-cli-build.js +++ /dev/null @@ -1,10 +0,0 @@ -/* global require, module */ -var EmberApp = require('ember-cli/lib/broccoli/ember-addon'); - -module.exports = function(defaults) { - var app = new EmberApp(defaults, { - - }); - - return app.toTree(); -}; diff --git a/tests/fixtures/brocfile-tests/both-build-files/vendor/brocfile-script.js b/tests/fixtures/brocfile-tests/both-build-files/vendor/brocfile-script.js deleted file mode 100644 index 89b4defdd4..0000000000 --- a/tests/fixtures/brocfile-tests/both-build-files/vendor/brocfile-script.js +++ /dev/null @@ -1 +0,0 @@ -var usingBrocfile = true; diff --git a/tests/fixtures/brocfile-tests/custom-ember-env/config/environment.js b/tests/fixtures/brocfile-tests/custom-ember-env/config/environment.js index 43cb47cb99..19c57d2ff2 100644 --- a/tests/fixtures/brocfile-tests/custom-ember-env/config/environment.js +++ b/tests/fixtures/brocfile-tests/custom-ember-env/config/environment.js @@ -1,8 +1,12 @@ module.exports = function() { return { modulePrefix: 'some-cool-app', + rootURL: '/', EmberENV: { asdflkmawejf: ';jlnu3yr23' + }, + APP: { + autoboot: false } }; }; diff --git a/tests/fixtures/brocfile-tests/custom-ember-env/config/targets.js b/tests/fixtures/brocfile-tests/custom-ember-env/config/targets.js new file mode 100644 index 0000000000..b55f37e561 --- /dev/null +++ b/tests/fixtures/brocfile-tests/custom-ember-env/config/targets.js @@ -0,0 +1,10 @@ +const browsers = [ + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions', +]; + +module.exports = { + browsers, + node: 'current', +}; diff --git a/tests/fixtures/brocfile-tests/custom-environment-config/config/environment.js b/tests/fixtures/brocfile-tests/custom-environment-config/config/environment.js index ad0acb32ee..3060e30109 100644 --- a/tests/fixtures/brocfile-tests/custom-environment-config/config/environment.js +++ b/tests/fixtures/brocfile-tests/custom-environment-config/config/environment.js @@ -2,7 +2,7 @@ module.exports = function() { return { modulePrefix: 'some-cool-app', fileUsed: 'config/environment.js', - baseURL: '/', - locationType: 'auto' + rootURL: '/', + locationType: 'history', }; }; diff --git a/tests/fixtures/brocfile-tests/custom-environment-config/config/something-else.js b/tests/fixtures/brocfile-tests/custom-environment-config/config/something-else.js index 497f198064..ccce2eac71 100644 --- a/tests/fixtures/brocfile-tests/custom-environment-config/config/something-else.js +++ b/tests/fixtures/brocfile-tests/custom-environment-config/config/something-else.js @@ -2,7 +2,10 @@ module.exports = function() { return { modulePrefix: 'some-cool-app', fileUsed: 'config/something-else.js', - baseURL: '/', - locationType: 'auto' + rootURL: '/', + locationType: 'history', + APP: { + autoboot: false + } }; }; diff --git a/tests/fixtures/brocfile-tests/custom-environment-config/ember-cli-build.js b/tests/fixtures/brocfile-tests/custom-environment-config/ember-cli-build.js index 09b81c03aa..8e3517b810 100644 --- a/tests/fixtures/brocfile-tests/custom-environment-config/ember-cli-build.js +++ b/tests/fixtures/brocfile-tests/custom-environment-config/ember-cli-build.js @@ -1,11 +1,19 @@ -/* global require, module */ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function(defaults) { +module.exports = function (defaults) { var app = new EmberApp(defaults, { configPath: 'config/something-else' }); + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + skipBabel: [{ + package: 'qunit' + }] + }); + } + return app.toTree(); }; diff --git a/tests/fixtures/brocfile-tests/custom-environment-config/tests/unit/config-test.js b/tests/fixtures/brocfile-tests/custom-environment-config/tests/unit/config-test.js index 14d904f6c4..ebd1de34b8 100644 --- a/tests/fixtures/brocfile-tests/custom-environment-config/tests/unit/config-test.js +++ b/tests/fixtures/brocfile-tests/custom-environment-config/tests/unit/config-test.js @@ -1,8 +1,6 @@ -/*jshint strict:false */ -/* globals QUnit */ - +import { test } from 'qunit'; import config from '../../config/environment'; -QUnit.test('the correct config is used', function(assert) { - assert.equal(config.fileUsed, 'config/something-else.js'); +test('the correct config is used', function(assert) { + assert.strictEqual(config.fileUsed, 'config/something-else.js'); }); diff --git a/tests/fixtures/brocfile-tests/custom-output-paths/ember-cli-build.js b/tests/fixtures/brocfile-tests/custom-output-paths/ember-cli-build.js deleted file mode 100644 index fe1d53a76c..0000000000 --- a/tests/fixtures/brocfile-tests/custom-output-paths/ember-cli-build.js +++ /dev/null @@ -1,28 +0,0 @@ -/* global require, module */ - -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - var app = new EmberApp(defaults, { - outputPaths: { - app: { - html: 'my-app.html', - css: { - 'app': '/css/app.css', - 'theme': '/css/theme/a.css' - }, - js: '/js/app.js' - }, - vendor: { - css: '/css/vendor.css', - js: '/js/vendor.js' - }, - testSupport: { - css: '/css/test-support.css', - js: '/js/test-support.js' - } - } - }); - - return app.toTree(); -}; diff --git a/tests/fixtures/brocfile-tests/default-development/tests/integration/app-boots-test.js b/tests/fixtures/brocfile-tests/default-development/tests/integration/app-boots-test.js deleted file mode 100644 index 348d301788..0000000000 --- a/tests/fixtures/brocfile-tests/default-development/tests/integration/app-boots-test.js +++ /dev/null @@ -1,26 +0,0 @@ -/*jshint strict:false */ -/* globals visit, andThen */ - -import Ember from 'ember'; -import startApp from '../helpers/start-app'; -import { module, test } from 'qunit'; - -module('default-development - Integration', { - beforeEach: function() { - this.application = startApp(); - }, - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - - -test('the application boots properly', function(assert) { - assert.expect(1); - - visit('/'); - - andThen(function() { - assert.equal(Ember.$('#title').text(), 'Welcome to Ember'); - }); -}); diff --git a/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/app/routes/horrible-route.js b/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/app/routes/horrible-route.js new file mode 100644 index 0000000000..b6038d4406 --- /dev/null +++ b/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/app/routes/horrible-route.js @@ -0,0 +1,2 @@ +var blah = "" +export default blah; diff --git a/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/index.js b/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/index.js index 0086447e5a..bd29a4e196 100644 --- a/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/index.js +++ b/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'ember-random-thing' + name: require('./package').name, } diff --git a/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/test-support/unit/routes/horrible-route-test.js b/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/test-support/unit/routes/horrible-route-test.js new file mode 100644 index 0000000000..b6038d4406 --- /dev/null +++ b/tests/fixtures/brocfile-tests/jshint-addon/lib/ember-random-thing/test-support/unit/routes/horrible-route-test.js @@ -0,0 +1,2 @@ +var blah = "" +export default blah; diff --git a/tests/fixtures/brocfile-tests/multiple-sass-files/ember-cli-build.js b/tests/fixtures/brocfile-tests/multiple-sass-files/ember-cli-build.js index 1ce446e45c..d8bfdef706 100644 --- a/tests/fixtures/brocfile-tests/multiple-sass-files/ember-cli-build.js +++ b/tests/fixtures/brocfile-tests/multiple-sass-files/ember-cli-build.js @@ -1,12 +1,15 @@ -/* global require, module */ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function(defaults) { +module.exports = function (defaults) { var app = new EmberApp(defaults, { name: require('./package.json').name, - outputPaths: { app: { css: { 'main': '/assets/main.css', 'theme/a': '/assets/theme/a.css' } } } }); + if (isExperimentEnabled('EMBROIDER')) { + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack); + } + return app.toTree(); }; diff --git a/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/broccoli-sass/index.js b/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/broccoli-sass/index.js index bad437a18f..9080dc10e9 100644 --- a/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/broccoli-sass/index.js +++ b/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/broccoli-sass/index.js @@ -1,8 +1,8 @@ 'use strict'; -var fs = require('fs'); -var path = require('path'); -var Writer = require('broccoli-writer'); +const fs = require('fs'); +const path = require('path'); +const Plugin = require('broccoli-plugin'); function copyPreserveSync (src, dest) { var srcStats = fs.statSync(src); @@ -24,23 +24,17 @@ function copyPreserveSync (src, dest) { } } -module.exports = SassCompiler; -SassCompiler.prototype = Object.create(Writer.prototype); -SassCompiler.prototype.constructor = SassCompiler; -function SassCompiler (sourceTrees, inputFile, outputFile, options) { - if (!(this instanceof SassCompiler)) return new SassCompiler(sourceTrees, inputFile, outputFile, options); - if (!Array.isArray(sourceTrees)) throw new Error('Expected array for first argument - did you mean [tree] instead of tree?'); - this.sourceTrees = sourceTrees; - this.inputFile = inputFile; - this.outputFile = outputFile; -} - -SassCompiler.prototype.write = function (readTree, destDir) { - var self = this; +class SassCompiler extends Plugin { + constructor(inputNodes, inputFile, outputFile, options) { + super(inputNodes, options); + this.inputFile = inputFile; + this.outputFile = outputFile; + } - return readTree(this.sourceTrees[0]).then(function (srcDir) { + build() { copyPreserveSync( - path.join(srcDir, self.inputFile), - path.join(destDir, self.outputFile)); - }); -}; + path.join(this.inputPaths[0], this.inputFile), + path.join(this.outputPath, this.outputFile)); + } +} +module.exports = SassCompiler; diff --git a/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/ember-cli-sass/index.js b/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/ember-cli-sass/index.js new file mode 100644 index 0000000000..0af2c4cf95 --- /dev/null +++ b/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/ember-cli-sass/index.js @@ -0,0 +1,37 @@ +'use strict'; + +const path = require('path'); +const mergeTrees = require('broccoli-merge-trees'); +const SassCompiler = require('../broccoli-sass'); + +function SASSPlugin() { + this.name = 'ember-cli-sass'; + this.ext = ['scss', 'sass']; +} + +SASSPlugin.prototype.toTree = function(tree, inputPath, outputPath, inputOptions) { + var options = inputOptions; + + var inputTrees = [tree]; + if (options.includePaths) { + inputTrees = inputTrees.concat(options.includePaths); + } + + var ext = options.extension || 'scss'; + var paths = options.outputPaths; + var trees = Object.keys(paths).map(function(file) { + var input = path.join(inputPath, file + '.' + ext); + var output = paths[file]; + return new SassCompiler(inputTrees, input, output, options); + }); + + return mergeTrees(trees); +}; + +module.exports = { + name: require('./package').name, + + setupPreprocessorRegistry(type, registry) { + registry.add('css', new SASSPlugin()); + }, +}; diff --git a/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/ember-cli-sass/package.json b/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/ember-cli-sass/package.json new file mode 100644 index 0000000000..4076438315 --- /dev/null +++ b/tests/fixtures/brocfile-tests/multiple-sass-files/node_modules/ember-cli-sass/package.json @@ -0,0 +1,12 @@ +{ + "name": "ember-cli-sass", + "private": true, + "version": "1.0.0", + "main": "index.js", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "broccoli-sass": "latest" + } +} diff --git a/tests/fixtures/brocfile-tests/no-ember-cli-build/Brocfile.js b/tests/fixtures/brocfile-tests/no-ember-cli-build/Brocfile.js deleted file mode 100644 index a8b03c5232..0000000000 --- a/tests/fixtures/brocfile-tests/no-ember-cli-build/Brocfile.js +++ /dev/null @@ -1,4 +0,0 @@ -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); -var app = new EmberApp(); - -module.exports = app.toTree(); \ No newline at end of file diff --git a/tests/fixtures/brocfile-tests/pods-templates/tests/integration/pods-template-test.js b/tests/fixtures/brocfile-tests/pods-templates/tests/integration/pods-template-test.js index 22bd427276..ecd258a4e4 100644 --- a/tests/fixtures/brocfile-tests/pods-templates/tests/integration/pods-template-test.js +++ b/tests/fixtures/brocfile-tests/pods-templates/tests/integration/pods-template-test.js @@ -1,26 +1,16 @@ -/*jshint strict:false */ -/* globals visit, andThen */ - -import Ember from 'ember'; -import startApp from '../helpers/start-app'; +import { setupApplicationTest } from 'ember-qunit'; +import { visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; -module('pods based templates', { - beforeEach: function() { - this.application = startApp(); - }, - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - +module('pods based templates', function(hooks) { + setupApplicationTest(hooks); -test('the application boots properly with pods based templates', function(assert) { - assert.expect(1); + test('the application boots properly with pods based templates', async function (assert) { + assert.expect(1); - visit('/'); + await visit('/'); - andThen(function() { - assert.equal(Ember.$('#title').text(), 'ZOMG, PODS WORKS!!'); + let actual = this.element.querySelector('#title').textContent + assert.strictEqual(actual, 'ZOMG, PODS WORKS!!'); }); }); diff --git a/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/app.js b/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/app.js index fecbb69b48..07328a0e65 100644 --- a/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/app.js +++ b/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/app.js @@ -1,16 +1,11 @@ -import Ember from 'ember'; -import Resolver from 'ember/resolver'; -import loadInitializers from 'ember/load-initializers'; +import Application from '@ember/application'; +import Resolver from 'ember-resolver'; +import loadInitializers from 'ember-load-initializers'; -Ember.MODEL_FACTORY_INJECTIONS = true; - -var App = Ember.Application.extend({ - modulePrefix: 'some-cool-app', - podModulePrefix: 'some-cool-app/pods', - Resolver: Resolver -}); +export default class App extends Application { + modulePrefix = 'some-cool-app'; + podModulePrefix = 'some-cool-app/pods'; + Resolver = Resolver; +} loadInitializers(App, 'some-cool-app'); - -export default App; - diff --git a/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/resolver.js b/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/resolver.js index e69de29bb2..3aa83c3115 100644 --- a/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/resolver.js +++ b/tests/fixtures/brocfile-tests/pods-with-prefix-templates/app/resolver.js @@ -0,0 +1 @@ +export { default } from 'ember-resolver'; diff --git a/tests/fixtures/brocfile-tests/pods-with-prefix-templates/tests/integration/pods-template-test.js b/tests/fixtures/brocfile-tests/pods-with-prefix-templates/tests/integration/pods-template-test.js index 004a64b748..a151dd0801 100644 --- a/tests/fixtures/brocfile-tests/pods-with-prefix-templates/tests/integration/pods-template-test.js +++ b/tests/fixtures/brocfile-tests/pods-with-prefix-templates/tests/integration/pods-template-test.js @@ -1,26 +1,16 @@ -/*jshint strict:false */ -/* globals visit, andThen */ - -import Ember from 'ember'; -import startApp from '../helpers/start-app'; +import { setupApplicationTest } from 'ember-qunit'; +import { visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; -module('pods based templates', { - beforeEach: function() { - this.application = startApp(); - }, - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - +module('pods based templates', function(hooks) { + setupApplicationTest(hooks); -test('the application boots properly with pods based templates with a podModulePrefix set', function(assert) { - assert.expect(1); + test('the application boots properly with pods based templates with a podModulePrefix set', async function (assert) { + assert.expect(1); - visit('/'); + await visit('/'); - andThen(function() { - assert.equal(Ember.$('#title').text(), 'ZOMG, PODS WORKS!!'); + let actual = this.element.querySelector('#title').textContent + assert.strictEqual(actual, 'ZOMG, PODS WORKS!!'); }); }); diff --git a/tests/fixtures/installation-checker/invalid-bower-and-npm/package.json b/tests/fixtures/brocfile-tests/public-tree/.gitkeep similarity index 100% rename from tests/fixtures/installation-checker/invalid-bower-and-npm/package.json rename to tests/fixtures/brocfile-tests/public-tree/.gitkeep diff --git a/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/index.js b/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/index.js index 45535810a8..2e1d1d8d5f 100644 --- a/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/index.js +++ b/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'ember-random-addon' + name: require('./package').name }; diff --git a/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/package.json b/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/package.json index 49d68fa7ac..9d79fd8a6b 100644 --- a/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/package.json +++ b/tests/fixtures/brocfile-tests/public-tree/node_modules/ember-random-addon/package.json @@ -6,4 +6,3 @@ "ember-addon" ] } - diff --git a/tests/fixtures/brocfile-tests/query/app/app.js b/tests/fixtures/brocfile-tests/query/app/app.js deleted file mode 100644 index aba5466564..0000000000 --- a/tests/fixtures/brocfile-tests/query/app/app.js +++ /dev/null @@ -1,16 +0,0 @@ -import Ember from 'ember'; -import Resolver from 'ember/resolver'; -import loadInitializers from 'ember/load-initializers'; - -Ember.MODEL_FACTORY_INJECTIONS = true; - -var App = Ember.Application.extend({ - modulePrefix: 'query', - podModulePrefix: 'app/pods', - Resolver: Resolver -}); - -loadInitializers(App, 'query'); - -export default App; - diff --git a/tests/fixtures/brocfile-tests/query/app/pods/application/template.hbs b/tests/fixtures/brocfile-tests/query/app/pods/application/template.hbs deleted file mode 100644 index dc9900c702..0000000000 --- a/tests/fixtures/brocfile-tests/query/app/pods/application/template.hbs +++ /dev/null @@ -1 +0,0 @@ -

ZOMG, PODS WORKS!!

diff --git a/tests/fixtures/brocfile-tests/query/package.json b/tests/fixtures/brocfile-tests/query/package.json deleted file mode 100644 index fdca49b924..0000000000 --- a/tests/fixtures/brocfile-tests/query/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "query", - "dependencies": { - "ember-cli": "*", - "ember-cli-htmlbars": "0.7.9" - } -} diff --git a/tests/fixtures/brocfile-tests/wrap-in-eval/ember-cli-build.js b/tests/fixtures/brocfile-tests/wrap-in-eval/ember-cli-build.js deleted file mode 100644 index 6651967382..0000000000 --- a/tests/fixtures/brocfile-tests/wrap-in-eval/ember-cli-build.js +++ /dev/null @@ -1,16 +0,0 @@ -/* global require, module */ - -var EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function(defaults) { - var app = new EmberApp(defaults, { - name: require('./package.json').name, - wrapInEval: true, - getEnvJSON: require('./config/environment') - }); - - return app.toTree(); -}; - - - diff --git a/tests/fixtures/brocfile-tests/wrap-in-eval/tests/integration/wrap-in-eval-test.js b/tests/fixtures/brocfile-tests/wrap-in-eval/tests/integration/wrap-in-eval-test.js deleted file mode 100644 index 693d8b21e5..0000000000 --- a/tests/fixtures/brocfile-tests/wrap-in-eval/tests/integration/wrap-in-eval-test.js +++ /dev/null @@ -1,26 +0,0 @@ -/*jshint strict:false */ -/* globals visit, andThen */ - -import Ember from 'ember'; -import startApp from '../helpers/start-app'; -import { module, test } from 'qunit'; - -module('wrapInEval in-app test', { - beforeEach: function() { - this.application = startApp(); - }, - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - - -test('the application boots properly with wrapInEval', function(assert) { - assert.expect(1); - - visit('/'); - - andThen(function() { - assert.equal(Ember.$('#title').text(), 'Welcome to Ember'); - }); -}); diff --git a/tests/fixtures/build/node-esm/app/hello.txt b/tests/fixtures/build/node-esm/app/hello.txt new file mode 100644 index 0000000000..802992c422 --- /dev/null +++ b/tests/fixtures/build/node-esm/app/hello.txt @@ -0,0 +1 @@ +Hello world diff --git a/tests/fixtures/build/node-esm/app/intro.md b/tests/fixtures/build/node-esm/app/intro.md new file mode 100644 index 0000000000..5dfd0725a4 --- /dev/null +++ b/tests/fixtures/build/node-esm/app/intro.md @@ -0,0 +1,3 @@ +# Introduction + +This is the introduction markdown file diff --git a/tests/fixtures/build/node-esm/app/outro.md b/tests/fixtures/build/node-esm/app/outro.md new file mode 100644 index 0000000000..1e7c322b6c --- /dev/null +++ b/tests/fixtures/build/node-esm/app/outro.md @@ -0,0 +1,3 @@ +# Outro + +This is the outro diff --git a/tests/fixtures/build/node-esm/app/test.txt b/tests/fixtures/build/node-esm/app/test.txt new file mode 100644 index 0000000000..0527e6bd2d --- /dev/null +++ b/tests/fixtures/build/node-esm/app/test.txt @@ -0,0 +1 @@ +This is a test diff --git a/tests/fixtures/build/node-esm/dist/intro.md b/tests/fixtures/build/node-esm/dist/intro.md new file mode 100644 index 0000000000..5dfd0725a4 --- /dev/null +++ b/tests/fixtures/build/node-esm/dist/intro.md @@ -0,0 +1,3 @@ +# Introduction + +This is the introduction markdown file diff --git a/tests/fixtures/build/node-esm/dist/outro.md b/tests/fixtures/build/node-esm/dist/outro.md new file mode 100644 index 0000000000..1e7c322b6c --- /dev/null +++ b/tests/fixtures/build/node-esm/dist/outro.md @@ -0,0 +1,3 @@ +# Outro + +This is the outro diff --git a/tests/fixtures/build/node-esm/dist/text.txt b/tests/fixtures/build/node-esm/dist/text.txt new file mode 100644 index 0000000000..531ac4dfd5 --- /dev/null +++ b/tests/fixtures/build/node-esm/dist/text.txt @@ -0,0 +1,3 @@ +Hello world + +This is a test diff --git a/tests/fixtures/build/node-esm/ember-cli-build.cjs b/tests/fixtures/build/node-esm/ember-cli-build.cjs new file mode 100644 index 0000000000..d3de00fe2f --- /dev/null +++ b/tests/fixtures/build/node-esm/ember-cli-build.cjs @@ -0,0 +1,17 @@ +const mergeTrees = require('broccoli-merge-trees'); +const concat = require('broccoli-concat'); +const funnel = require('broccoli-funnel'); + +module.exports = function() { + const txt = funnel('app', { + include: ['*.txt'], + }); + const md = funnel('app', { + include: ['*.md'], + }); + const concated = concat(txt, { + outputFile: 'text.txt', + }); + + return mergeTrees([concated, md], {annotation: 'The final merge'}); +} \ No newline at end of file diff --git a/tests/fixtures/build/simple/app/hello.txt b/tests/fixtures/build/simple/app/hello.txt new file mode 100644 index 0000000000..802992c422 --- /dev/null +++ b/tests/fixtures/build/simple/app/hello.txt @@ -0,0 +1 @@ +Hello world diff --git a/tests/fixtures/build/simple/app/intro.md b/tests/fixtures/build/simple/app/intro.md new file mode 100644 index 0000000000..5dfd0725a4 --- /dev/null +++ b/tests/fixtures/build/simple/app/intro.md @@ -0,0 +1,3 @@ +# Introduction + +This is the introduction markdown file diff --git a/tests/fixtures/build/simple/app/outro.md b/tests/fixtures/build/simple/app/outro.md new file mode 100644 index 0000000000..1e7c322b6c --- /dev/null +++ b/tests/fixtures/build/simple/app/outro.md @@ -0,0 +1,3 @@ +# Outro + +This is the outro diff --git a/tests/fixtures/build/simple/app/test.txt b/tests/fixtures/build/simple/app/test.txt new file mode 100644 index 0000000000..0527e6bd2d --- /dev/null +++ b/tests/fixtures/build/simple/app/test.txt @@ -0,0 +1 @@ +This is a test diff --git a/tests/fixtures/build/simple/dist/intro.md b/tests/fixtures/build/simple/dist/intro.md new file mode 100644 index 0000000000..5dfd0725a4 --- /dev/null +++ b/tests/fixtures/build/simple/dist/intro.md @@ -0,0 +1,3 @@ +# Introduction + +This is the introduction markdown file diff --git a/tests/fixtures/build/simple/dist/outro.md b/tests/fixtures/build/simple/dist/outro.md new file mode 100644 index 0000000000..1e7c322b6c --- /dev/null +++ b/tests/fixtures/build/simple/dist/outro.md @@ -0,0 +1,3 @@ +# Outro + +This is the outro diff --git a/tests/fixtures/build/simple/dist/text.txt b/tests/fixtures/build/simple/dist/text.txt new file mode 100644 index 0000000000..531ac4dfd5 --- /dev/null +++ b/tests/fixtures/build/simple/dist/text.txt @@ -0,0 +1,3 @@ +Hello world + +This is a test diff --git a/tests/fixtures/build/simple/ember-cli-build.js b/tests/fixtures/build/simple/ember-cli-build.js new file mode 100644 index 0000000000..d3de00fe2f --- /dev/null +++ b/tests/fixtures/build/simple/ember-cli-build.js @@ -0,0 +1,17 @@ +const mergeTrees = require('broccoli-merge-trees'); +const concat = require('broccoli-concat'); +const funnel = require('broccoli-funnel'); + +module.exports = function() { + const txt = funnel('app', { + include: ['*.txt'], + }); + const md = funnel('app', { + include: ['*.md'], + }); + const concated = concat(txt, { + outputFile: 'text.txt', + }); + + return mergeTrees([concated, md], {annotation: 'The final merge'}); +} \ No newline at end of file diff --git a/tests/fixtures/deprecate-override/index.mjs b/tests/fixtures/deprecate-override/index.mjs new file mode 100644 index 0000000000..444637b64a --- /dev/null +++ b/tests/fixtures/deprecate-override/index.mjs @@ -0,0 +1,13 @@ +import deprecate from "../../../lib/debug/deprecate.js"; + +deprecate('you can do this for a while longer eh', false, { + for: 'ember-cli', // this needs to be ember-cli so that it triggers our internal system + id: 'deprecate-override-test', + since: { + available: '4.0.0', + enabled: '4.0.0', + }, + until: '15.0.0', +}); + +console.log('success'); diff --git a/tests/fixtures/dummy-project-outdated/package.json b/tests/fixtures/dummy-project-outdated/package.json deleted file mode 100644 index cdad0ea02b..0000000000 --- a/tests/fixtures/dummy-project-outdated/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "dummy-project", - "version": "0.0.0", - "private": true, - "directories": { - "doc": "doc", - "test": "test" - }, - "scripts": { - "start": "ember server", - "build": "ember build", - "test": "ember test" - }, - "repository": "https://github.com/ember-cli/ember-cli", - "engines": { - "node": ">= 0.10.0" - }, - "author": "", - "license": "MIT", - "devDependencies": { - "body-parser": "^1.2.0", - "broccoli-asset-rev": "^2.1.2", - "broccoli-ember-hbs-template-compiler": "^1.5.0", - "ember-cli": "0.0.1", - "ember-cli-ember-data": "0.1.1", - "ember-cli-ic-ajax": "0.1.1", - "express": "^4.1.1", - "glob": "^3.2.9" - } -} \ No newline at end of file diff --git a/tests/fixtures/express-server/tests/test-file.txt b/tests/fixtures/express-server/tests/test-file.txt index ee7dd3eed2..f70d6b1398 100644 --- a/tests/fixtures/express-server/tests/test-file.txt +++ b/tests/fixtures/express-server/tests/test-file.txt @@ -1 +1 @@ -some contents \ No newline at end of file +some contents diff --git a/tests/fixtures/express-server/vendor/foo.wasm b/tests/fixtures/express-server/vendor/foo.wasm new file mode 100644 index 0000000000..e5b37c1c5b Binary files /dev/null and b/tests/fixtures/express-server/vendor/foo.wasm differ diff --git a/tests/fixtures/file-info/.gitattributes b/tests/fixtures/file-info/.gitattributes new file mode 100644 index 0000000000..9f4c4d387c --- /dev/null +++ b/tests/fixtures/file-info/.gitattributes @@ -0,0 +1,2 @@ +test_crlf.js binary +test_lf.js binary diff --git a/tests/fixtures/file-info/test_crlf.js b/tests/fixtures/file-info/test_crlf.js new file mode 100644 index 0000000000..c4dfc642b7 --- /dev/null +++ b/tests/fixtures/file-info/test_crlf.js @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 \ No newline at end of file diff --git a/tests/fixtures/file-info/test_lf.js b/tests/fixtures/file-info/test_lf.js new file mode 100644 index 0000000000..8cf2f17fe1 --- /dev/null +++ b/tests/fixtures/file-info/test_lf.js @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 \ No newline at end of file diff --git a/tests/fixtures/generate/acceptance-test-expected.js b/tests/fixtures/generate/acceptance-test-expected.js deleted file mode 100644 index ec783f082d..0000000000 --- a/tests/fixtures/generate/acceptance-test-expected.js +++ /dev/null @@ -1,21 +0,0 @@ -import Ember from 'ember'; -import { module, test } from 'qunit'; -import startApp from 'my-app/tests/helpers/start-app'; - -module('Acceptance | foo', { - beforeEach: function() { - this.application = startApp(); - }, - - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - -test('visiting /foo', function(assert) { - visit('/foo'); - - andThen(function() { - assert.equal(currentURL(), '/foo'); - }); -}); diff --git a/tests/fixtures/generate/addon-acceptance-test-expected.js b/tests/fixtures/generate/addon-acceptance-test-expected.js deleted file mode 100644 index 926439dba6..0000000000 --- a/tests/fixtures/generate/addon-acceptance-test-expected.js +++ /dev/null @@ -1,21 +0,0 @@ -import Ember from 'ember'; -import { module, test } from 'qunit'; -import startApp from '../../tests/helpers/start-app'; - -module('Acceptance | foo', { - beforeEach: function() { - this.application = startApp(); - }, - - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - -test('visiting /foo', function(assert) { - visit('/foo'); - - andThen(function() { - assert.equal(currentURL(), '/foo'); - }); -}); diff --git a/tests/fixtures/generate/addon-acceptance-test-nested-expected.js b/tests/fixtures/generate/addon-acceptance-test-nested-expected.js deleted file mode 100644 index aabba337f8..0000000000 --- a/tests/fixtures/generate/addon-acceptance-test-nested-expected.js +++ /dev/null @@ -1,21 +0,0 @@ -import Ember from 'ember'; -import { module, test } from 'qunit'; -import startApp from '../../../tests/helpers/start-app'; - -module('Acceptance | foo/bar', { - beforeEach: function() { - this.application = startApp(); - }, - - afterEach: function() { - Ember.run(this.application, 'destroy'); - } -}); - -test('visiting /foo/bar', function(assert) { - visit('/foo/bar'); - - andThen(function() { - assert.equal(currentURL(), '/foo/bar'); - }); -}); diff --git a/tests/fixtures/help/classic/foo.txt b/tests/fixtures/help/classic/foo.txt new file mode 100644 index 0000000000..cffb99fc74 --- /dev/null +++ b/tests/fixtures/help/classic/foo.txt @@ -0,0 +1,7 @@ +Requested ember-cli commands: + +ember foo   + Initializes the warp drive. + --dry-run (Boolean) (Default: false) + aliases: -d + diff --git a/tests/fixtures/help/classic/generate-blueprint.txt b/tests/fixtures/help/classic/generate-blueprint.txt new file mode 100644 index 0000000000..ce934a0c33 --- /dev/null +++ b/tests/fixtures/help/classic/generate-blueprint.txt @@ -0,0 +1,26 @@ +Requested ember-cli commands: + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + --dummy (Boolean) (Default: false) + aliases: -dum, -id + + blueprint  + Generates a blueprint and definition. + + diff --git a/tests/fixtures/help/classic/generate-with-addon.txt b/tests/fixtures/help/classic/generate-with-addon.txt new file mode 100644 index 0000000000..ff5d2048d5 --- /dev/null +++ b/tests/fixtures/help/classic/generate-with-addon.txt @@ -0,0 +1,55 @@ +Requested ember-cli commands: + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + --dummy (Boolean) (Default: false) + aliases: -dum, -id + + + Available blueprints: + fixtures: + basic  + A basic blueprint + basic-esm  + A basic blueprint + basic_2  + Another basic blueprint + exporting-object  + A blueprint that exports an object + with-templating  + A blueprint with templating + ember-cli: + addon  + The default blueprint for ember-cli addons. + addon-import  + Generates an import wrapper. + app  + The default blueprint for ember-cli projects. + blueprint  + Generates a blueprint and definition. + http-mock  + [Classic Only] Generates a mock api endpoint in /api prefix. + http-proxy   + [Classic Only] Generates a relative proxy to another server. + in-repo-addon  + The blueprint for addon in repo ember-cli addons. + lib  + Generates a lib directory for in-repo addons. + server  + [Classic Only] Generates a server directory for mocks and proxies. + diff --git a/tests/fixtures/help/classic/generate.txt b/tests/fixtures/help/classic/generate.txt new file mode 100644 index 0000000000..d8f3401941 --- /dev/null +++ b/tests/fixtures/help/classic/generate.txt @@ -0,0 +1,44 @@ +Requested ember-cli commands: + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + --dummy (Boolean) (Default: false) + aliases: -dum, -id + + + Available blueprints: + ember-cli: + addon  + The default blueprint for ember-cli addons. + addon-import  + Generates an import wrapper. + app  + The default blueprint for ember-cli projects. + blueprint  + Generates a blueprint and definition. + http-mock  + [Classic Only] Generates a mock api endpoint in /api prefix. + http-proxy   + [Classic Only] Generates a relative proxy to another server. + in-repo-addon  + The blueprint for addon in repo ember-cli addons. + lib  + Generates a lib directory for in-repo addons. + server  + [Classic Only] Generates a server directory for mocks and proxies. + diff --git a/tests/fixtures/help/classic/help-with-addon.txt b/tests/fixtures/help/classic/help-with-addon.txt new file mode 100644 index 0000000000..3c8a095b41 --- /dev/null +++ b/tests/fixtures/help/classic/help-with-addon.txt @@ -0,0 +1,238 @@ +Usage: ember  + +Available commands in ember-cli: + +ember addon   + Generates a new folder structure for building an addon, complete with test harness. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: addon) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the addon's own test application via index.html + --lint-fix (Boolean) (Default: true) + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --typescript (Boolean) (Default: false) Set up the addon to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember asset-sizes  + Shows the sizes of your asset files. + --output-path (Path) (Default: dist/) + aliases: -o  + --json (Boolean) (Default: false) + +ember build  + Builds your app and places it into the output path (dist/ by default). + aliases: b + --environment (String) (Default: development) Possible values are "development", "production", and "test". + aliases: -e , -dev (--environment=development), -prod (--environment=production) + --suppress-sizes (Boolean) (Default: false) + --output-path (Path) (Default: dist/) + aliases: -o  + --watch (Boolean) (Default: false) + aliases: -w + --watcher (String) + +ember destroy   + Destroys code generated by `generate` command. + aliases: d + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output. + aliases: -ts + --dummy (Boolean) (Default: false) + aliases: -dum, -id + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + --dummy (Boolean) (Default: false) + aliases: -dum, -id + +ember help   + Outputs the usage instructions for all commands or the provided command + aliases: h, --help, -h + --verbose (Boolean) (Default: false) + aliases: -v + --json (Boolean) (Default: false) + +ember init   + Reinitializes a new ember-cli project in the current folder. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --lint-fix (Boolean) (Default: true) + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --name (String) (Default: "") + aliases: -n  + --lang (String) Sets the base human language of the application via index.html + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember install   + Installs an ember-cli addon from npm. + aliases: i + --save (Boolean) (Default: false) + aliases: -S + --save-dev (Boolean) (Default: true) + aliases: -D + --save-exact (Boolean) (Default: false) + aliases: -E, --exact + --package-manager (npm, pnpm, yarn) Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project. + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + +ember new   + Creates a new directory and runs ember init in it. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: app) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the application via index.html + --lint-fix (Boolean) (Default: true) + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --interactive (Boolean) (Default: false) Create a new Ember app/addon in an interactive way. + aliases: -i + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember serve  + Builds and serves your app, rebuilding on file changes. + aliases: server, s + --port (Number) (Default: 4200) Overrides $PORT (currently blank). If the port 0 or the default port 4200 is passed, ember will use any available port starting from 4200. + aliases: -p  + --host (String) Listens on all interfaces by default + aliases: -H  + --proxy (String) + aliases: -pr , -pxy  + --proxy-in-timeout (Number) (Default: 120000) When using --proxy: timeout (in ms) for incoming requests + aliases: -pit  + --proxy-out-timeout (Number) (Default: 0) When using --proxy: timeout (in ms) for outgoing requests + aliases: -pot  + --secure-proxy (Boolean) (Default: true) Set to false to proxy self-signed SSL certificates + aliases: -spr + --transparent-proxy (Boolean) (Default: true) Set to false to omit x-forwarded-* headers when proxying + aliases: --transp + --watcher (String) (Default: events) + aliases: -w  + --live-reload (Boolean) (Default: true) + aliases: -lr + --live-reload-host (String) Defaults to host + aliases: -lrh  + --live-reload-base-url (String) Defaults to rootURL + aliases: -lrbu  + --live-reload-port (Number) Defaults to same port as ember app + aliases: -lrp  + --live-reload-prefix (String) (Default: _lr) Default to _lr + aliases: --lrprefix  + --environment (String) (Default: development) Possible values are "development", "production", and "test". + aliases: -e , -dev (--environment=development), -prod (--environment=production) + --output-path (Path) (Default: dist/) + aliases: -op , -out  + --ssl (Boolean) (Default: false) Set to true to configure Ember CLI to serve using SSL. + --ssl-key (String) (Default: ssl/server.key) Specify the private key to use for SSL. + --ssl-cert (String) (Default: ssl/server.crt) Specify the certificate to use for SSL. + --path (Path) Reuse an existing build at given path. + +ember test  + Runs your app's test suite. + aliases: t + --environment (String) (Default: test) Possible values are "development", "production", and "test". + aliases: -e  + --config-file (String) + aliases: -c , -cf  + --host (String) + aliases: -H  + --test-port (Number) (Default: 7357) The test port to use when running tests. Pass 0 to automatically pick an available port + aliases: -tp  + --filter (String) A string to filter tests to run + aliases: -f  + --module (String) The name of a test module to run + aliases: -m  + --watcher (String) (Default: events) + aliases: -w  + --launch (String) (Default: false) A comma separated list of browsers to launch for tests. + --reporter (String) Test reporter to use [tap|dot|xunit] (default: tap) + aliases: -r  + --silent (Boolean) (Default: false) Suppress any output except for the test report + --ssl (Boolean) (Default: false) Set to true to configure testem to run the test suite using SSL. + --ssl-key (String) (Default: ssl/server.key) Specify the private key to use for SSL. + --ssl-cert (String) (Default: ssl/server.crt) Specify the certificate to use for SSL. + --testem-debug (String) File to write a debug log from testem + --test-page (String) Test page to invoke + --path (Path) Reuse an existing build at given path. + --query (String) A query string to append to the test page URL. + --server (Boolean) (Default: false) + aliases: -s + --output-path (Path) + aliases: -o  + +ember version  + outputs ember-cli version + aliases: v, --version, -v + --verbose (Boolean) (Default: false) + + +Available commands from dummy-addon: +ember foo   + Initializes the warp drive. + --dry-run (Boolean) (Default: false) + aliases: -d + diff --git a/tests/fixtures/help/classic/help.js b/tests/fixtures/help/classic/help.js new file mode 100644 index 0000000000..c92f9cd993 --- /dev/null +++ b/tests/fixtures/help/classic/help.js @@ -0,0 +1,977 @@ +const processHelpString = require('../../../helpers/process-help-string'); +const versionUtils = require('../../../../lib/utilities/version-utils'); +var emberCLIVersion = versionUtils.emberCLIVersion; + +module.exports = { + name: 'ember', + description: null, + aliases: [], + works: 'insideProject', + availableOptions: [], + anonymousOptions: [''], + version: emberCLIVersion(), + commands: [ + { + name: 'addon', + description: 'Generates a new folder structure for building an addon, complete with test harness.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'addon', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: "Sets the base human language of the addon's own test application via index.html", + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the addon to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'asset-sizes', + description: 'Shows the sizes of your asset files.', + works: 'insideProject', + aliases: [], + anonymousOptions: [], + availableOptions: [ + { + name: 'output-path', + default: 'dist/', + key: 'outputPath', + required: false, + aliases: ['o'], + type: 'Path' + }, + { + default: false, + key: 'json', + name: 'json', + required: false + } + ] + }, + { + name: 'build', + description: 'Builds your app and places it into the output path (dist/ by default).', + aliases: ['b'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'suppress-sizes', + default: false, + key: 'suppressSizes', + required: false + }, + { + name: 'output-path', + default: 'dist/', + aliases: ['o'], + key: 'outputPath', + required: false, + type: 'Path' + }, + { + name: 'watch', + default: false, + aliases: ['w'], + key: 'watch', + required: false + }, + { + name: 'watcher', + key: 'watcher', + required: false + } + ], + anonymousOptions: [] + }, + { + name: 'destroy', + description: 'Destroys code generated by `generate` command.', + aliases: ['d'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output.', + key: 'typescript', + required: false + }, + { + name: 'dummy', + default: false, + aliases: ['dum', 'id'], + key: 'dummy', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'generate', + description: 'Generates new code from blueprints.', + aliases: ['g'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Generates a version of the blueprint written in TypeScript (if available).', + key: 'typescript', + required: false + }, + { + name: 'dummy', + default: false, + aliases: ['dum', 'id'], + key: 'dummy', + required: false + } + ], + anonymousOptions: [''], + availableBlueprints: [ + { + 'ember-cli': [ + { + name: 'addon', + description: 'The default blueprint for ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'addon-import', + description: 'Generates an import wrapper.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'app', + description: 'The default blueprint for ember-cli projects.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'blueprint', + description: 'Generates a blueprint and definition.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'http-mock', + description: '[Classic Only] Generates a mock api endpoint in /api prefix.', + availableOptions: [], + anonymousOptions: ['endpoint-path'], + overridden: false + }, + { + name: 'http-proxy', + description: '[Classic Only] Generates a relative proxy to another server.', + availableOptions: [], + anonymousOptions: ['local-path', 'remote-url'], + overridden: false + }, + { + name: 'in-repo-addon', + description: 'The blueprint for addon in repo ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'lib', + description: 'Generates a lib directory for in-repo addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'server', + description: '[Classic Only] Generates a server directory for mocks and proxies.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + } + ] + }, + { + name: 'help', + description: 'Outputs the usage instructions for all commands or the provided command', + aliases: [null, 'h', '--help', '-h'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'json', + default: false, + key: 'json', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'init', + description: 'Reinitializes a new ember-cli project in the current folder.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'name', + default: '', + aliases: ['n'], + key: 'name', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false, + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'install', + description: 'Installs an ember-cli addon from npm.', + aliases: ['i'], + works: 'insideProject', + availableOptions: [ + { + name: 'save', + default: false, + aliases: ['S'], + key: 'save', + required: false + }, + { + name: 'save-dev', + default: true, + aliases: ['D'], + key: 'saveDev', + required: false + }, + { + name: 'save-exact', + default: false, + aliases: ['E', 'exact'], + key: 'saveExact', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + description: + 'Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project.', + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + ], + anonymousOptions: [''] + }, + { + name: 'new', + description: processHelpString('Creates a new directory and runs \u001b[32member init\u001b[39m in it.'), + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'app', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false, + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + name: 'interactive', + default: false, + aliases: ['i'], + description: 'Create a new Ember app/addon in an interactive way.', + key: 'interactive', + required: false, + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'serve', + description: 'Builds and serves your app, rebuilding on file changes.', + aliases: ['server', 's'], + works: 'insideProject', + availableOptions: [ + { + name: 'port', + default: 4200, + description: 'Overrides $PORT (currently blank). If the port 0 or the default port 4200 is passed, ember will use any available port starting from 4200.', + aliases: ['p'], + key: 'port', + required: false + }, + { + name: 'host', + description: 'Listens on all interfaces by default', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'proxy', + aliases: ['pr', 'pxy'], + key: 'proxy', + required: false + }, + { + name: 'proxy-in-timeout', + default: 120000, + description: 'When using --proxy: timeout (in ms) for incoming requests', + aliases: ['pit'], + key: 'proxyInTimeout', + required: false + }, + { + name: 'proxy-out-timeout', + default: 0, + description: 'When using --proxy: timeout (in ms) for outgoing requests', + aliases: ['pot'], + key: 'proxyOutTimeout', + required: false + }, + { + name: 'secure-proxy', + default: true, + description: 'Set to false to proxy self-signed SSL certificates', + aliases: ['spr'], + key: 'secureProxy', + required: false + }, + { + name: 'transparent-proxy', + default: true, + description: 'Set to false to omit x-forwarded-* headers when proxying', + aliases: ['transp'], + key: 'transparentProxy', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'live-reload', + default: true, + aliases: ['lr'], + key: 'liveReload', + required: false + }, + { + name: 'live-reload-host', + description: 'Defaults to host', + aliases: ['lrh'], + key: 'liveReloadHost', + required: false + }, + { + aliases: ['lrbu'], + description: 'Defaults to rootURL', + key: 'liveReloadBaseUrl', + name: 'live-reload-base-url', + required: false + }, + { + name: 'live-reload-port', + description: 'Defaults to same port as ember app', + aliases: ['lrp'], + key: 'liveReloadPort', + required: false + }, + { + name: 'live-reload-prefix', + default: '_lr', + description: 'Default to _lr', + aliases: ['lrprefix'], + key: 'liveReloadPrefix', + required: false + }, + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'output-path', + default: 'dist/', + aliases: ['op', 'out'], + key: 'outputPath', + required: false, + type: 'Path', + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure Ember CLI to serve using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path' + } + ], + anonymousOptions: [] + }, + { + name: 'test', + description: 'Runs your app\'s test suite.', + aliases: ['t'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'test', + aliases: ['e'], + key: 'environment', + required: false + }, + { + name: 'config-file', + aliases: ['c', 'cf'], + key: 'configFile', + required: false + }, + { + name: 'host', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'test-port', + default: 7357, + description: 'The test port to use when running tests. Pass 0 to automatically pick an available port', + aliases: ['tp'], + key: 'testPort', + required: false + }, + { + name: 'filter', + description: 'A string to filter tests to run', + aliases: ['f'], + key: 'filter', + required: false + }, + { + name: 'module', + description: 'The name of a test module to run', + aliases: ['m'], + key: 'module', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'launch', + default: false, + description: 'A comma separated list of browsers to launch for tests.', + key: 'launch', + required: false + }, + { + name: 'reporter', + description: 'Test reporter to use [tap|dot|xunit] (default: tap)', + aliases: ['r'], + key: 'reporter', + required: false + }, + { + name: 'silent', + default: false, + description: 'Suppress any output except for the test report', + key: 'silent', + required: false + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure testem to run the test suite using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'testem-debug', + description: 'File to write a debug log from testem', + key: 'testemDebug', + required: false + }, + { + name: 'test-page', + description: 'Test page to invoke', + key: 'testPage', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path', + }, + { + name: 'query', + description: 'A query string to append to the test page URL.', + key: 'query', + required: false + }, + { + name: 'server', + default: false, + aliases: ['s'], + key: 'server', + required: false + }, + { + name: 'output-path', + aliases: ['o'], + key: 'outputPath', + required: false, + type: 'Path' + } + ], + anonymousOptions: [] + }, + { + name: 'version', + description: 'outputs ember-cli version', + aliases: ['v', '--version', '-v'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + key: 'verbose', + required: false + } + ], + anonymousOptions: [] + } + ], + addons: [] +}; diff --git a/tests/fixtures/help/classic/help.txt b/tests/fixtures/help/classic/help.txt new file mode 100644 index 0000000000..edab0b6c4d --- /dev/null +++ b/tests/fixtures/help/classic/help.txt @@ -0,0 +1,231 @@ +Usage: ember  + +Available commands in ember-cli: + +ember addon   + Generates a new folder structure for building an addon, complete with test harness. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: addon) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the addon's own test application via index.html + --lint-fix (Boolean) (Default: true) + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --typescript (Boolean) (Default: false) Set up the addon to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember asset-sizes  + Shows the sizes of your asset files. + --output-path (Path) (Default: dist/) + aliases: -o  + --json (Boolean) (Default: false) + +ember build  + Builds your app and places it into the output path (dist/ by default). + aliases: b + --environment (String) (Default: development) Possible values are "development", "production", and "test". + aliases: -e , -dev (--environment=development), -prod (--environment=production) + --suppress-sizes (Boolean) (Default: false) + --output-path (Path) (Default: dist/) + aliases: -o  + --watch (Boolean) (Default: false) + aliases: -w + --watcher (String) + +ember destroy   + Destroys code generated by `generate` command. + aliases: d + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output. + aliases: -ts + --dummy (Boolean) (Default: false) + aliases: -dum, -id + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + --dummy (Boolean) (Default: false) + aliases: -dum, -id + +ember help   + Outputs the usage instructions for all commands or the provided command + aliases: h, --help, -h + --verbose (Boolean) (Default: false) + aliases: -v + --json (Boolean) (Default: false) + +ember init   + Reinitializes a new ember-cli project in the current folder. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --lint-fix (Boolean) (Default: true) + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --name (String) (Default: "") + aliases: -n  + --lang (String) Sets the base human language of the application via index.html + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember install   + Installs an ember-cli addon from npm. + aliases: i + --save (Boolean) (Default: false) + aliases: -S + --save-dev (Boolean) (Default: true) + aliases: -D + --save-exact (Boolean) (Default: false) + aliases: -E, --exact + --package-manager (npm, pnpm, yarn) Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project. + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + +ember new   + Creates a new directory and runs ember init in it. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: app) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the application via index.html + --lint-fix (Boolean) (Default: true) + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --interactive (Boolean) (Default: false) Create a new Ember app/addon in an interactive way. + aliases: -i + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember serve  + Builds and serves your app, rebuilding on file changes. + aliases: server, s + --port (Number) (Default: 4200) Overrides $PORT (currently blank). If the port 0 or the default port 4200 is passed, ember will use any available port starting from 4200. + aliases: -p  + --host (String) Listens on all interfaces by default + aliases: -H  + --proxy (String) + aliases: -pr , -pxy  + --proxy-in-timeout (Number) (Default: 120000) When using --proxy: timeout (in ms) for incoming requests + aliases: -pit  + --proxy-out-timeout (Number) (Default: 0) When using --proxy: timeout (in ms) for outgoing requests + aliases: -pot  + --secure-proxy (Boolean) (Default: true) Set to false to proxy self-signed SSL certificates + aliases: -spr + --transparent-proxy (Boolean) (Default: true) Set to false to omit x-forwarded-* headers when proxying + aliases: --transp + --watcher (String) (Default: events) + aliases: -w  + --live-reload (Boolean) (Default: true) + aliases: -lr + --live-reload-host (String) Defaults to host + aliases: -lrh  + --live-reload-base-url (String) Defaults to rootURL + aliases: -lrbu  + --live-reload-port (Number) Defaults to same port as ember app + aliases: -lrp  + --live-reload-prefix (String) (Default: _lr) Default to _lr + aliases: --lrprefix  + --environment (String) (Default: development) Possible values are "development", "production", and "test". + aliases: -e , -dev (--environment=development), -prod (--environment=production) + --output-path (Path) (Default: dist/) + aliases: -op , -out  + --ssl (Boolean) (Default: false) Set to true to configure Ember CLI to serve using SSL. + --ssl-key (String) (Default: ssl/server.key) Specify the private key to use for SSL. + --ssl-cert (String) (Default: ssl/server.crt) Specify the certificate to use for SSL. + --path (Path) Reuse an existing build at given path. + +ember test  + Runs your app's test suite. + aliases: t + --environment (String) (Default: test) Possible values are "development", "production", and "test". + aliases: -e  + --config-file (String) + aliases: -c , -cf  + --host (String) + aliases: -H  + --test-port (Number) (Default: 7357) The test port to use when running tests. Pass 0 to automatically pick an available port + aliases: -tp  + --filter (String) A string to filter tests to run + aliases: -f  + --module (String) The name of a test module to run + aliases: -m  + --watcher (String) (Default: events) + aliases: -w  + --launch (String) (Default: false) A comma separated list of browsers to launch for tests. + --reporter (String) Test reporter to use [tap|dot|xunit] (default: tap) + aliases: -r  + --silent (Boolean) (Default: false) Suppress any output except for the test report + --ssl (Boolean) (Default: false) Set to true to configure testem to run the test suite using SSL. + --ssl-key (String) (Default: ssl/server.key) Specify the private key to use for SSL. + --ssl-cert (String) (Default: ssl/server.crt) Specify the certificate to use for SSL. + --testem-debug (String) File to write a debug log from testem + --test-page (String) Test page to invoke + --path (Path) Reuse an existing build at given path. + --query (String) A query string to append to the test page URL. + --server (Boolean) (Default: false) + aliases: -s + --output-path (Path) + aliases: -o  + +ember version  + outputs ember-cli version + aliases: v, --version, -v + --verbose (Boolean) (Default: false) + diff --git a/tests/fixtures/help/classic/with-addon-blueprints.js b/tests/fixtures/help/classic/with-addon-blueprints.js new file mode 100644 index 0000000000..c0e4c6609b --- /dev/null +++ b/tests/fixtures/help/classic/with-addon-blueprints.js @@ -0,0 +1,1016 @@ +const processHelpString = require('../../../helpers/process-help-string'); +const versionUtils = require('../../../../lib/utilities/version-utils'); +var emberCLIVersion = versionUtils.emberCLIVersion; + +module.exports = { + name: 'ember', + description: null, + aliases: [], + works: 'insideProject', + availableOptions: [], + anonymousOptions: [''], + version: emberCLIVersion(), + commands: [ + { + name: 'addon', + description: 'Generates a new folder structure for building an addon, complete with test harness.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'addon', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: "Sets the base human language of the addon's own test application via index.html", + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the addon to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'asset-sizes', + description: 'Shows the sizes of your asset files.', + works: 'insideProject', + aliases: [], + anonymousOptions: [], + availableOptions: [ + { + name: 'output-path', + default: 'dist/', + key: 'outputPath', + required: false, + aliases: ['o'], + type: 'Path' + }, + { + default: false, + key: 'json', + name: 'json', + required: false + } + ] + }, + { + name: 'build', + description: 'Builds your app and places it into the output path (dist/ by default).', + aliases: ['b'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'suppress-sizes', + default: false, + key: 'suppressSizes', + required: false + }, + { + name: 'output-path', + default: 'dist/', + aliases: ['o'], + key: 'outputPath', + required: false, + type: 'Path', + }, + { + name: 'watch', + default: false, + aliases: ['w'], + key: 'watch', + required: false + }, + { + name: 'watcher', + key: 'watcher', + required: false + } + ], + anonymousOptions: [] + }, + { + name: 'destroy', + description: 'Destroys code generated by `generate` command.', + aliases: ['d'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output.', + key: 'typescript', + required: false + }, + { + name: 'dummy', + default: false, + aliases: ['dum', 'id'], + key: 'dummy', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'generate', + description: 'Generates new code from blueprints.', + aliases: ['g'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Generates a version of the blueprint written in TypeScript (if available).', + key: 'typescript', + required: false + }, + { + name: 'dummy', + default: false, + aliases: ['dum', 'id'], + key: 'dummy', + required: false + } + ], + anonymousOptions: [''], + availableBlueprints: [ + { + fixtures: [ + { + name: 'basic', + description: 'A basic blueprint', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'basic-esm', + description: 'A basic blueprint', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'basic_2', + description: 'Another basic blueprint', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'exporting-object', + description: 'A blueprint that exports an object', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'with-templating', + description: 'A blueprint with templating', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + }, + { + 'ember-cli': [ + { + name: 'addon', + description: 'The default blueprint for ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'addon-import', + description: 'Generates an import wrapper.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'app', + description: 'The default blueprint for ember-cli projects.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'blueprint', + description: 'Generates a blueprint and definition.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'http-mock', + description: '[Classic Only] Generates a mock api endpoint in /api prefix.', + availableOptions: [], + anonymousOptions: ['endpoint-path'], + overridden: false + }, + { + name: 'http-proxy', + description: '[Classic Only] Generates a relative proxy to another server.', + availableOptions: [], + anonymousOptions: ['local-path', 'remote-url'], + overridden: false + }, + { + name: 'in-repo-addon', + description: 'The blueprint for addon in repo ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'lib', + description: 'Generates a lib directory for in-repo addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'server', + description: '[Classic Only] Generates a server directory for mocks and proxies.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + } + ] + }, + { + name: 'help', + description: 'Outputs the usage instructions for all commands or the provided command', + aliases: [null, 'h', '--help', '-h'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'json', + default: false, + key: 'json', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'init', + description: 'Reinitializes a new ember-cli project in the current folder.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'name', + default: '', + aliases: ['n'], + key: 'name', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'install', + description: 'Installs an ember-cli addon from npm.', + aliases: ['i'], + works: 'insideProject', + availableOptions: [ + { + name: 'save', + default: false, + aliases: ['S'], + key: 'save', + required: false + }, + { + name: 'save-dev', + default: true, + aliases: ['D'], + key: 'saveDev', + required: false + }, + { + name: 'save-exact', + default: false, + aliases: ['E', 'exact'], + key: 'saveExact', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + description: + 'Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project.', + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + ], + anonymousOptions: [''] + }, + { + name: 'new', + description: processHelpString('Creates a new directory and runs \u001b[32member init\u001b[39m in it.'), + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'app', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + name: 'interactive', + default: false, + aliases: ['i'], + description: 'Create a new Ember app/addon in an interactive way.', + key: 'interactive', + required: false, + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'serve', + description: 'Builds and serves your app, rebuilding on file changes.', + aliases: ['server', 's'], + works: 'insideProject', + availableOptions: [ + { + name: 'port', + default: 4200, + description: 'Overrides $PORT (currently blank). If the port 0 or the default port 4200 is passed, ember will use any available port starting from 4200.', + aliases: ['p'], + key: 'port', + required: false + }, + { + name: 'host', + description: 'Listens on all interfaces by default', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'proxy', + aliases: ['pr', 'pxy'], + key: 'proxy', + required: false + }, + { + name: 'proxy-in-timeout', + default: 120000, + description: 'When using --proxy: timeout (in ms) for incoming requests', + aliases: ['pit'], + key: 'proxyInTimeout', + required: false + }, + { + name: 'proxy-out-timeout', + default: 0, + description: 'When using --proxy: timeout (in ms) for outgoing requests', + aliases: ['pot'], + key: 'proxyOutTimeout', + required: false + }, + { + name: 'secure-proxy', + default: true, + description: 'Set to false to proxy self-signed SSL certificates', + aliases: ['spr'], + key: 'secureProxy', + required: false + }, + { + name: 'transparent-proxy', + default: true, + description: 'Set to false to omit x-forwarded-* headers when proxying', + aliases: ['transp'], + key: 'transparentProxy', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'live-reload', + default: true, + aliases: ['lr'], + key: 'liveReload', + required: false + }, + { + name: 'live-reload-host', + description: 'Defaults to host', + aliases: ['lrh'], + key: 'liveReloadHost', + required: false + }, + { + aliases: ['lrbu'], + description: 'Defaults to rootURL', + key: 'liveReloadBaseUrl', + name: 'live-reload-base-url', + required: false + }, + { + name: 'live-reload-port', + description: 'Defaults to same port as ember app', + aliases: ['lrp'], + key: 'liveReloadPort', + required: false + }, + { + name: 'live-reload-prefix', + default: '_lr', + description: 'Default to _lr', + aliases: ['lrprefix'], + key: 'liveReloadPrefix', + required: false + }, + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'output-path', + default: 'dist/', + aliases: ['op', 'out'], + key: 'outputPath', + required: false, + type: 'Path', + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure Ember CLI to serve using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path' + } + ], + anonymousOptions: [] + }, + { + name: 'test', + description: 'Runs your app\'s test suite.', + aliases: ['t'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'test', + aliases: ['e'], + key: 'environment', + required: false + }, + { + name: 'config-file', + aliases: ['c', 'cf'], + key: 'configFile', + required: false + }, + { + name: 'host', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'test-port', + default: 7357, + description: 'The test port to use when running tests. Pass 0 to automatically pick an available port', + aliases: ['tp'], + key: 'testPort', + required: false + }, + { + name: 'filter', + description: 'A string to filter tests to run', + aliases: ['f'], + key: 'filter', + required: false + }, + { + name: 'module', + description: 'The name of a test module to run', + aliases: ['m'], + key: 'module', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'launch', + default: false, + description: 'A comma separated list of browsers to launch for tests.', + key: 'launch', + required: false + }, + { + name: 'reporter', + description: 'Test reporter to use [tap|dot|xunit] (default: tap)', + aliases: ['r'], + key: 'reporter', + required: false + }, + { + name: 'silent', + default: false, + description: 'Suppress any output except for the test report', + key: 'silent', + required: false + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure testem to run the test suite using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'testem-debug', + description: 'File to write a debug log from testem', + key: 'testemDebug', + required: false + }, + { + name: 'test-page', + description: 'Test page to invoke', + key: 'testPage', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path', + }, + { + name: 'query', + description: 'A query string to append to the test page URL.', + key: 'query', + required: false + }, + { + name: 'server', + default: false, + aliases: ['s'], + key: 'server', + required: false + }, + { + name: 'output-path', + aliases: ['o'], + key: 'outputPath', + required: false, + type: 'Path' + } + ], + anonymousOptions: [] + }, + { + name: 'version', + description: 'outputs ember-cli version', + aliases: ['v', '--version', '-v'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + key: 'verbose', + required: false + } + ], + anonymousOptions: [] + } + ], + addons: [] +}; diff --git a/tests/fixtures/help/classic/with-addon-commands.js b/tests/fixtures/help/classic/with-addon-commands.js new file mode 100644 index 0000000000..efed0ee4cf --- /dev/null +++ b/tests/fixtures/help/classic/with-addon-commands.js @@ -0,0 +1,1003 @@ +const processHelpString = require('../../../helpers/process-help-string'); +const versionUtils = require('../../../../lib/utilities/version-utils'); +var emberCLIVersion = versionUtils.emberCLIVersion; + +module.exports = { + name: 'ember', + description: null, + aliases: [], + works: 'insideProject', + availableOptions: [], + anonymousOptions: [''], + version: emberCLIVersion(), + commands: [ + { + name: 'addon', + description: 'Generates a new folder structure for building an addon, complete with test harness.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'addon', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: "Sets the base human language of the addon's own test application via index.html", + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the addon to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'asset-sizes', + description: 'Shows the sizes of your asset files.', + works: 'insideProject', + aliases: [], + anonymousOptions: [], + availableOptions: [ + { + name: 'output-path', + default: 'dist/', + key: 'outputPath', + required: false, + aliases: ['o'], + type: 'Path' + }, + { + default: false, + key: 'json', + name: 'json', + required: false + } + ] + }, + { + name: 'build', + description: 'Builds your app and places it into the output path (dist/ by default).', + aliases: ['b'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'suppress-sizes', + default: false, + key: 'suppressSizes', + required: false + }, + { + name: 'output-path', + default: 'dist/', + aliases: ['o'], + key: 'outputPath', + required: false, + type: 'Path', + }, + { + name: 'watch', + default: false, + aliases: ['w'], + key: 'watch', + required: false + }, + { + name: 'watcher', + key: 'watcher', + required: false + } + ], + anonymousOptions: [] + }, + { + name: 'destroy', + description: 'Destroys code generated by `generate` command.', + aliases: ['d'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output.', + key: 'typescript', + required: false + }, + { + name: 'dummy', + default: false, + aliases: ['dum', 'id'], + key: 'dummy', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'generate', + description: 'Generates new code from blueprints.', + aliases: ['g'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Generates a version of the blueprint written in TypeScript (if available).', + key: 'typescript', + required: false + }, + { + name: 'dummy', + default: false, + aliases: ['dum', 'id'], + key: 'dummy', + required: false + } + ], + anonymousOptions: [''], + availableBlueprints: [ + { + 'ember-cli': [ + { + name: 'addon', + description: 'The default blueprint for ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'addon-import', + description: 'Generates an import wrapper.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'app', + description: 'The default blueprint for ember-cli projects.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'blueprint', + description: 'Generates a blueprint and definition.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'http-mock', + description: '[Classic Only] Generates a mock api endpoint in /api prefix.', + availableOptions: [], + anonymousOptions: ['endpoint-path'], + overridden: false + }, + { + name: 'http-proxy', + description: '[Classic Only] Generates a relative proxy to another server.', + availableOptions: [], + anonymousOptions: ['local-path', 'remote-url'], + overridden: false + }, + { + name: 'in-repo-addon', + description: 'The blueprint for addon in repo ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'lib', + description: 'Generates a lib directory for in-repo addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'server', + description: '[Classic Only] Generates a server directory for mocks and proxies.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + } + ] + }, + { + name: 'help', + description: 'Outputs the usage instructions for all commands or the provided command', + aliases: [null, 'h', '--help', '-h'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'json', + default: false, + key: 'json', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'init', + description: 'Reinitializes a new ember-cli project in the current folder.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'name', + default: '', + aliases: ['n'], + key: 'name', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'install', + description: 'Installs an ember-cli addon from npm.', + aliases: ['i'], + works: 'insideProject', + availableOptions: [ + { + name: 'save', + default: false, + aliases: ['S'], + key: 'save', + required: false + }, + { + name: 'save-dev', + default: true, + aliases: ['D'], + key: 'saveDev', + required: false + }, + { + name: 'save-exact', + default: false, + aliases: ['E', 'exact'], + key: 'saveExact', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + description: + 'Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project.', + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + ], + anonymousOptions: [''] + }, + { + name: 'new', + description: processHelpString('Creates a new directory and runs \u001b[32member init\u001b[39m in it.'), + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'app', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + name: 'interactive', + default: false, + aliases: ['i'], + description: 'Create a new Ember app/addon in an interactive way.', + key: 'interactive', + required: false, + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'serve', + description: 'Builds and serves your app, rebuilding on file changes.', + aliases: ['server', 's'], + works: 'insideProject', + availableOptions: [ + { + name: 'port', + default: 4200, + description: 'Overrides $PORT (currently blank). If the port 0 or the default port 4200 is passed, ember will use any available port starting from 4200.', + aliases: ['p'], + key: 'port', + required: false + }, + { + name: 'host', + description: 'Listens on all interfaces by default', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'proxy', + aliases: ['pr', 'pxy'], + key: 'proxy', + required: false + }, + { + name: 'proxy-in-timeout', + default: 120000, + description: 'When using --proxy: timeout (in ms) for incoming requests', + aliases: ['pit'], + key: 'proxyInTimeout', + required: false + }, + { + name: 'proxy-out-timeout', + default: 0, + description: 'When using --proxy: timeout (in ms) for outgoing requests', + aliases: ['pot'], + key: 'proxyOutTimeout', + required: false + }, + { + name: 'secure-proxy', + default: true, + description: 'Set to false to proxy self-signed SSL certificates', + aliases: ['spr'], + key: 'secureProxy', + required: false + }, + { + name: 'transparent-proxy', + default: true, + description: 'Set to false to omit x-forwarded-* headers when proxying', + aliases: ['transp'], + key: 'transparentProxy', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'live-reload', + default: true, + aliases: ['lr'], + key: 'liveReload', + required: false + }, + { + name: 'live-reload-host', + description: 'Defaults to host', + aliases: ['lrh'], + key: 'liveReloadHost', + required: false + }, + { + aliases: ['lrbu'], + description: 'Defaults to rootURL', + key: 'liveReloadBaseUrl', + name: 'live-reload-base-url', + required: false + }, + { + name: 'live-reload-port', + description: 'Defaults to same port as ember app', + aliases: ['lrp'], + key: 'liveReloadPort', + required: false + }, + { + name: 'live-reload-prefix', + default: '_lr', + description: 'Default to _lr', + aliases: ['lrprefix'], + key: 'liveReloadPrefix', + required: false + }, + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'output-path', + default: 'dist/', + aliases: ['op', 'out'], + key: 'outputPath', + required: false, + type: 'Path', + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure Ember CLI to serve using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path' + } + ], + anonymousOptions: [] + }, + { + name: 'test', + description: 'Runs your app\'s test suite.', + aliases: ['t'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'test', + aliases: ['e'], + key: 'environment', + required: false + }, + { + name: 'config-file', + aliases: ['c', 'cf'], + key: 'configFile', + required: false + }, + { + name: 'host', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'test-port', + default: 7357, + description: 'The test port to use when running tests. Pass 0 to automatically pick an available port', + aliases: ['tp'], + key: 'testPort', + required: false + }, + { + name: 'filter', + description: 'A string to filter tests to run', + aliases: ['f'], + key: 'filter', + required: false + }, + { + name: 'module', + description: 'The name of a test module to run', + aliases: ['m'], + key: 'module', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'launch', + default: false, + description: 'A comma separated list of browsers to launch for tests.', + key: 'launch', + required: false + }, + { + name: 'reporter', + description: 'Test reporter to use [tap|dot|xunit] (default: tap)', + aliases: ['r'], + key: 'reporter', + required: false + }, + { + name: 'silent', + default: false, + description: 'Suppress any output except for the test report', + key: 'silent', + required: false + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure testem to run the test suite using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'testem-debug', + description: 'File to write a debug log from testem', + key: 'testemDebug', + required: false + }, + { + name: 'test-page', + description: 'Test page to invoke', + key: 'testPage', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path', + }, + { + name: 'query', + description: 'A query string to append to the test page URL.', + key: 'query', + required: false + }, + { + name: 'server', + default: false, + aliases: ['s'], + key: 'server', + required: false + }, + { + name: 'output-path', + aliases: ['o'], + key: 'outputPath', + required: false, + type: 'Path' + } + ], + anonymousOptions: [] + }, + { + name: 'version', + description: 'outputs ember-cli version', + aliases: ['v', '--version', '-v'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + key: 'verbose', + required: false + } + ], + anonymousOptions: [] + } + ], + addons: [ + { + name: 'dummy-addon', + commands: [ + { + name: 'foo', + description: 'Initializes the warp drive.', + aliases: [], + works: 'insideProject', + availableOptions: [ + { + aliases: [ + 'd' + ], + default: false, + key: 'dryRun', + name: 'dry-run', + required: false + } + ], + anonymousOptions: [ + '' + ] + } + ] + } + ] +}; diff --git a/tests/fixtures/help/foo.txt b/tests/fixtures/help/foo.txt new file mode 100644 index 0000000000..cffb99fc74 --- /dev/null +++ b/tests/fixtures/help/foo.txt @@ -0,0 +1,7 @@ +Requested ember-cli commands: + +ember foo   + Initializes the warp drive. + --dry-run (Boolean) (Default: false) + aliases: -d + diff --git a/tests/fixtures/help/generate-blueprint.txt b/tests/fixtures/help/generate-blueprint.txt new file mode 100644 index 0000000000..7193539319 --- /dev/null +++ b/tests/fixtures/help/generate-blueprint.txt @@ -0,0 +1,24 @@ +Requested ember-cli commands: + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + + blueprint  + Generates a blueprint and definition. + + diff --git a/tests/fixtures/help/generate-with-addon.txt b/tests/fixtures/help/generate-with-addon.txt new file mode 100644 index 0000000000..5237b81cf2 --- /dev/null +++ b/tests/fixtures/help/generate-with-addon.txt @@ -0,0 +1,53 @@ +Requested ember-cli commands: + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + + + Available blueprints: + fixtures: + basic  + A basic blueprint + basic-esm  + A basic blueprint + basic_2  + Another basic blueprint + exporting-object  + A blueprint that exports an object + with-templating  + A blueprint with templating + ember-cli: + addon  + The default blueprint for ember-cli addons. + addon-import  + Generates an import wrapper. + app  + The default blueprint for ember-cli projects. + blueprint  + Generates a blueprint and definition. + http-mock  + [Classic Only] Generates a mock api endpoint in /api prefix. + http-proxy   + [Classic Only] Generates a relative proxy to another server. + in-repo-addon  + The blueprint for addon in repo ember-cli addons. + lib  + Generates a lib directory for in-repo addons. + server  + [Classic Only] Generates a server directory for mocks and proxies. + diff --git a/tests/fixtures/help/generate.txt b/tests/fixtures/help/generate.txt new file mode 100644 index 0000000000..8298499a27 --- /dev/null +++ b/tests/fixtures/help/generate.txt @@ -0,0 +1,42 @@ +Requested ember-cli commands: + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + + + Available blueprints: + ember-cli: + addon  + The default blueprint for ember-cli addons. + addon-import  + Generates an import wrapper. + app  + The default blueprint for ember-cli projects. + blueprint  + Generates a blueprint and definition. + http-mock  + [Classic Only] Generates a mock api endpoint in /api prefix. + http-proxy   + [Classic Only] Generates a relative proxy to another server. + in-repo-addon  + The blueprint for addon in repo ember-cli addons. + lib  + Generates a lib directory for in-repo addons. + server  + [Classic Only] Generates a server directory for mocks and proxies. + diff --git a/tests/fixtures/help/help-with-addon.txt b/tests/fixtures/help/help-with-addon.txt new file mode 100644 index 0000000000..f10e78e957 --- /dev/null +++ b/tests/fixtures/help/help-with-addon.txt @@ -0,0 +1,183 @@ +Usage: ember  + +Available commands in ember-cli: + +ember addon   + Generates a new folder structure for building an addon, complete with test harness. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: addon) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the addon's own test application via index.html + --lint-fix (Boolean) (Default: true) + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --typescript (Boolean) (Default: false) Set up the addon to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember build  + Vestigial command in Vite-based projects. Use the `build` script from package.json instead. + aliases: b + --environment (String) (Default: development) Possible values are "development", "production", and "test". + aliases: -e , -dev (--environment=development), -prod (--environment=production) + --suppress-sizes (Boolean) (Default: false) + --output-path (Path) (Default: dist/) + aliases: -o  + +ember destroy   + Destroys code generated by `generate` command. + aliases: d + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output. + aliases: -ts + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + +ember help   + Outputs the usage instructions for all commands or the provided command + aliases: h, --help, -h + --verbose (Boolean) (Default: false) + aliases: -v + --json (Boolean) (Default: false) + +ember init   + Reinitializes a new ember-cli project in the current folder. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --lint-fix (Boolean) (Default: true) + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --name (String) (Default: "") + aliases: -n  + --lang (String) Sets the base human language of the application via index.html + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember install   + Installs an ember-cli addon from npm. + aliases: i + --save (Boolean) (Default: false) + aliases: -S + --save-dev (Boolean) (Default: true) + aliases: -D + --save-exact (Boolean) (Default: false) + aliases: -E, --exact + --package-manager (npm, pnpm, yarn) Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project. + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + +ember new   + Creates a new directory and runs ember init in it. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: app) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the application via index.html + --lint-fix (Boolean) (Default: true) + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --interactive (Boolean) (Default: false) Create a new Ember app/addon in an interactive way. + aliases: -i + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember test  + Runs your app's test suite. + aliases: t + --environment (String) (Default: test) Possible values are "development", "production", and "test". + aliases: -e  + --config-file (String) + aliases: -c , -cf  + --host (String) + aliases: -H  + --test-port (Number) (Default: 7357) The test port to use when running tests. Pass 0 to automatically pick an available port + aliases: -tp  + --filter (String) A string to filter tests to run + aliases: -f  + --module (String) The name of a test module to run + aliases: -m  + --watcher (String) (Default: events) + aliases: -w  + --launch (String) (Default: false) A comma separated list of browsers to launch for tests. + --reporter (String) Test reporter to use [tap|dot|xunit] (default: tap) + aliases: -r  + --silent (Boolean) (Default: false) Suppress any output except for the test report + --ssl (Boolean) (Default: false) Set to true to configure testem to run the test suite using SSL. + --ssl-key (String) (Default: ssl/server.key) Specify the private key to use for SSL. + --ssl-cert (String) (Default: ssl/server.crt) Specify the certificate to use for SSL. + --testem-debug (String) File to write a debug log from testem + --test-page (String) Test page to invoke + --path (Path) Reuse an existing build at given path. + --query (String) A query string to append to the test page URL. + +ember version  + outputs ember-cli version + aliases: v, --version, -v + --verbose (Boolean) (Default: false) + + +Available commands from dummy-addon: +ember foo   + Initializes the warp drive. + --dry-run (Boolean) (Default: false) + aliases: -d + diff --git a/tests/fixtures/help/help.js b/tests/fixtures/help/help.js new file mode 100644 index 0000000000..b0ee6237bf --- /dev/null +++ b/tests/fixtures/help/help.js @@ -0,0 +1,761 @@ +const processHelpString = require('../../helpers/process-help-string'); +const versionUtils = require('../../../lib/utilities/version-utils'); +var emberCLIVersion = versionUtils.emberCLIVersion; + +module.exports = { + name: 'ember', + description: null, + aliases: [], + works: 'insideProject', + availableOptions: [], + anonymousOptions: [''], + version: emberCLIVersion(), + commands: [ + { + name: 'addon', + description: 'Generates a new folder structure for building an addon, complete with test harness.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'addon', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: "Sets the base human language of the addon's own test application via index.html", + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the addon to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'build', + description: 'Vestigial command in Vite-based projects. Use the `build` script from package.json instead.', + aliases: ['b'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'suppress-sizes', + default: false, + key: 'suppressSizes', + required: false + }, + { + name: 'output-path', + aliases: ['o'], + default: 'dist/', + key: 'outputPath', + required: false, + type: 'Path' + }, + ], + anonymousOptions: [] + }, + { + name: 'destroy', + description: 'Destroys code generated by `generate` command.', + aliases: ['d'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output.', + key: 'typescript', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'generate', + description: 'Generates new code from blueprints.', + aliases: ['g'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Generates a version of the blueprint written in TypeScript (if available).', + key: 'typescript', + required: false + } + ], + anonymousOptions: [''], + availableBlueprints: [ + { + 'ember-cli': [ + { + name: 'addon', + description: 'The default blueprint for ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'addon-import', + description: 'Generates an import wrapper.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'app', + description: 'The default blueprint for ember-cli projects.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'blueprint', + description: 'Generates a blueprint and definition.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'http-mock', + description: '[Classic Only] Generates a mock api endpoint in /api prefix.', + availableOptions: [], + anonymousOptions: ['endpoint-path'], + overridden: false + }, + { + name: 'http-proxy', + description: '[Classic Only] Generates a relative proxy to another server.', + availableOptions: [], + anonymousOptions: ['local-path', 'remote-url'], + overridden: false + }, + { + name: 'in-repo-addon', + description: 'The blueprint for addon in repo ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'lib', + description: 'Generates a lib directory for in-repo addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'server', + description: '[Classic Only] Generates a server directory for mocks and proxies.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + } + ] + }, + { + name: 'help', + description: 'Outputs the usage instructions for all commands or the provided command', + aliases: [null, 'h', '--help', '-h'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'json', + default: false, + key: 'json', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'init', + description: 'Reinitializes a new ember-cli project in the current folder.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'name', + default: '', + aliases: ['n'], + key: 'name', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false, + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'install', + description: 'Installs an ember-cli addon from npm.', + aliases: ['i'], + works: 'insideProject', + availableOptions: [ + { + name: 'save', + default: false, + aliases: ['S'], + key: 'save', + required: false + }, + { + name: 'save-dev', + default: true, + aliases: ['D'], + key: 'saveDev', + required: false + }, + { + name: 'save-exact', + default: false, + aliases: ['E', 'exact'], + key: 'saveExact', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + description: + 'Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project.', + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + ], + anonymousOptions: [''] + }, + { + name: 'new', + description: processHelpString('Creates a new directory and runs \u001b[32member init\u001b[39m in it.'), + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'app', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false, + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + name: 'interactive', + default: false, + aliases: ['i'], + description: 'Create a new Ember app/addon in an interactive way.', + key: 'interactive', + required: false, + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'test', + description: 'Runs your app\'s test suite.', + aliases: ['t'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'test', + aliases: ['e'], + key: 'environment', + required: false + }, + { + name: 'config-file', + aliases: ['c', 'cf'], + key: 'configFile', + required: false + }, + { + name: 'host', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'test-port', + default: 7357, + description: 'The test port to use when running tests. Pass 0 to automatically pick an available port', + aliases: ['tp'], + key: 'testPort', + required: false + }, + { + name: 'filter', + description: 'A string to filter tests to run', + aliases: ['f'], + key: 'filter', + required: false + }, + { + name: 'module', + description: 'The name of a test module to run', + aliases: ['m'], + key: 'module', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'launch', + default: false, + description: 'A comma separated list of browsers to launch for tests.', + key: 'launch', + required: false + }, + { + name: 'reporter', + description: 'Test reporter to use [tap|dot|xunit] (default: tap)', + aliases: ['r'], + key: 'reporter', + required: false + }, + { + name: 'silent', + default: false, + description: 'Suppress any output except for the test report', + key: 'silent', + required: false + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure testem to run the test suite using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'testem-debug', + description: 'File to write a debug log from testem', + key: 'testemDebug', + required: false + }, + { + name: 'test-page', + description: 'Test page to invoke', + key: 'testPage', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path', + }, + { + name: 'query', + description: 'A query string to append to the test page URL.', + key: 'query', + required: false + } + ], + anonymousOptions: [] + }, + { + name: 'version', + description: 'outputs ember-cli version', + aliases: ['v', '--version', '-v'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + key: 'verbose', + required: false + } + ], + anonymousOptions: [] + } + ], + addons: [] +}; diff --git a/tests/fixtures/help/help.txt b/tests/fixtures/help/help.txt new file mode 100644 index 0000000000..ea1fbbdf10 --- /dev/null +++ b/tests/fixtures/help/help.txt @@ -0,0 +1,176 @@ +Usage: ember  + +Available commands in ember-cli: + +ember addon   + Generates a new folder structure for building an addon, complete with test harness. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: addon) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the addon's own test application via index.html + --lint-fix (Boolean) (Default: true) + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --typescript (Boolean) (Default: false) Set up the addon to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember build  + Vestigial command in Vite-based projects. Use the `build` script from package.json instead. + aliases: b + --environment (String) (Default: development) Possible values are "development", "production", and "test". + aliases: -e , -dev (--environment=development), -prod (--environment=production) + --suppress-sizes (Boolean) (Default: false) + --output-path (Path) (Default: dist/) + aliases: -o  + +ember destroy   + Destroys code generated by `generate` command. + aliases: d + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output. + aliases: -ts + +ember generate   + Generates new code from blueprints. + aliases: g + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --pod (Boolean) (Default: false) + aliases: -p, -pods + --classic (Boolean) (Default: false) + aliases: -c + --in-repo-addon (String) (Default: null) + aliases: --in-repo , -ir  + --lint-fix (Boolean) (Default: true) + --in (String) (Default: null) Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project. + --typescript (Boolean) Generates a version of the blueprint written in TypeScript (if available). + aliases: -ts + +ember help   + Outputs the usage instructions for all commands or the provided command + aliases: h, --help, -h + --verbose (Boolean) (Default: false) + aliases: -v + --json (Boolean) (Default: false) + +ember init   + Reinitializes a new ember-cli project in the current folder. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --lint-fix (Boolean) (Default: true) + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --name (String) (Default: "") + aliases: -n  + --lang (String) Sets the base human language of the application via index.html + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) (Default: github) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember install   + Installs an ember-cli addon from npm. + aliases: i + --save (Boolean) (Default: false) + aliases: -S + --save-dev (Boolean) (Default: true) + aliases: -D + --save-exact (Boolean) (Default: false) + aliases: -E, --exact + --package-manager (npm, pnpm, yarn) Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project. + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + +ember new   + Creates a new directory and runs ember init in it. + --dry-run (Boolean) (Default: false) + aliases: -d + --verbose (Boolean) (Default: false) + aliases: -v + --blueprint (String) (Default: app) + aliases: -b  + --skip-npm (Boolean) (Default: false) + aliases: -sn, --skip-install, -si + --skip-git (Boolean) (Default: false) + aliases: -sg + --welcome (Boolean) (Default: true) Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it. + --package-manager (npm, pnpm, yarn) + aliases: -npm (--package-manager=npm), -pnpm (--package-manager=pnpm), -yarn (--package-manager=yarn) + --directory (String) + aliases: -dir  + --lang (String) Sets the base human language of the application via index.html + --lint-fix (Boolean) (Default: true) + --embroider (Boolean) (Default: false) Deprecated: Enables the build system to use Embroider + --ci-provider (github, none) Installs the optional default CI blueprint. Only Github Actions is supported at the moment. + --ember-data (Boolean) (Default: true) Include ember-data dependencies and configuration + --interactive (Boolean) (Default: false) Create a new Ember app/addon in an interactive way. + aliases: -i + --typescript (Boolean) (Default: false) Set up the app to use TypeScript + aliases: -ts + --strict (Boolean) (Default: true) Use GJS/GTS templates by default for generated components, tests, and route templates + +ember test  + Runs your app's test suite. + aliases: t + --environment (String) (Default: test) Possible values are "development", "production", and "test". + aliases: -e  + --config-file (String) + aliases: -c , -cf  + --host (String) + aliases: -H  + --test-port (Number) (Default: 7357) The test port to use when running tests. Pass 0 to automatically pick an available port + aliases: -tp  + --filter (String) A string to filter tests to run + aliases: -f  + --module (String) The name of a test module to run + aliases: -m  + --watcher (String) (Default: events) + aliases: -w  + --launch (String) (Default: false) A comma separated list of browsers to launch for tests. + --reporter (String) Test reporter to use [tap|dot|xunit] (default: tap) + aliases: -r  + --silent (Boolean) (Default: false) Suppress any output except for the test report + --ssl (Boolean) (Default: false) Set to true to configure testem to run the test suite using SSL. + --ssl-key (String) (Default: ssl/server.key) Specify the private key to use for SSL. + --ssl-cert (String) (Default: ssl/server.crt) Specify the certificate to use for SSL. + --testem-debug (String) File to write a debug log from testem + --test-page (String) Test page to invoke + --path (Path) Reuse an existing build at given path. + --query (String) A query string to append to the test page URL. + +ember version  + outputs ember-cli version + aliases: v, --version, -v + --verbose (Boolean) (Default: false) + diff --git a/tests/fixtures/help/with-addon-blueprints.js b/tests/fixtures/help/with-addon-blueprints.js new file mode 100644 index 0000000000..546049b79a --- /dev/null +++ b/tests/fixtures/help/with-addon-blueprints.js @@ -0,0 +1,800 @@ +const processHelpString = require('../../helpers/process-help-string'); +const versionUtils = require('../../../lib/utilities/version-utils'); +var emberCLIVersion = versionUtils.emberCLIVersion; + +module.exports = { + name: 'ember', + description: null, + aliases: [], + works: 'insideProject', + availableOptions: [], + anonymousOptions: [''], + version: emberCLIVersion(), + commands: [ + { + name: 'addon', + description: 'Generates a new folder structure for building an addon, complete with test harness.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'addon', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: "Sets the base human language of the addon's own test application via index.html", + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the addon to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'build', + description: 'Vestigial command in Vite-based projects. Use the `build` script from package.json instead.', + aliases: ['b'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'suppress-sizes', + default: false, + key: 'suppressSizes', + required: false + }, + { + name: 'output-path', + aliases: ['o'], + default: 'dist/', + key: 'outputPath', + required: false, + type: 'Path' + }, + ], + anonymousOptions: [] + }, + { + name: 'destroy', + description: 'Destroys code generated by `generate` command.', + aliases: ['d'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output.', + key: 'typescript', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'generate', + description: 'Generates new code from blueprints.', + aliases: ['g'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Generates a version of the blueprint written in TypeScript (if available).', + key: 'typescript', + required: false + }, + ], + anonymousOptions: [''], + availableBlueprints: [ + { + fixtures: [ + { + name: 'basic', + description: 'A basic blueprint', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'basic-esm', + description: 'A basic blueprint', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'basic_2', + description: 'Another basic blueprint', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'exporting-object', + description: 'A blueprint that exports an object', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'with-templating', + description: 'A blueprint with templating', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + }, + { + 'ember-cli': [ + { + name: 'addon', + description: 'The default blueprint for ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'addon-import', + description: 'Generates an import wrapper.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'app', + description: 'The default blueprint for ember-cli projects.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'blueprint', + description: 'Generates a blueprint and definition.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'http-mock', + description: '[Classic Only] Generates a mock api endpoint in /api prefix.', + availableOptions: [], + anonymousOptions: ['endpoint-path'], + overridden: false + }, + { + name: 'http-proxy', + description: '[Classic Only] Generates a relative proxy to another server.', + availableOptions: [], + anonymousOptions: ['local-path', 'remote-url'], + overridden: false + }, + { + name: 'in-repo-addon', + description: 'The blueprint for addon in repo ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'lib', + description: 'Generates a lib directory for in-repo addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'server', + description: '[Classic Only] Generates a server directory for mocks and proxies.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + } + ] + }, + { + name: 'help', + description: 'Outputs the usage instructions for all commands or the provided command', + aliases: [null, 'h', '--help', '-h'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'json', + default: false, + key: 'json', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'init', + description: 'Reinitializes a new ember-cli project in the current folder.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'name', + default: '', + aliases: ['n'], + key: 'name', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'install', + description: 'Installs an ember-cli addon from npm.', + aliases: ['i'], + works: 'insideProject', + availableOptions: [ + { + name: 'save', + default: false, + aliases: ['S'], + key: 'save', + required: false + }, + { + name: 'save-dev', + default: true, + aliases: ['D'], + key: 'saveDev', + required: false + }, + { + name: 'save-exact', + default: false, + aliases: ['E', 'exact'], + key: 'saveExact', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + description: + 'Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project.', + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + ], + anonymousOptions: [''] + }, + { + name: 'new', + description: processHelpString('Creates a new directory and runs \u001b[32member init\u001b[39m in it.'), + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'app', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + name: 'interactive', + default: false, + aliases: ['i'], + description: 'Create a new Ember app/addon in an interactive way.', + key: 'interactive', + required: false, + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'test', + description: 'Runs your app\'s test suite.', + aliases: ['t'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'test', + aliases: ['e'], + key: 'environment', + required: false + }, + { + name: 'config-file', + aliases: ['c', 'cf'], + key: 'configFile', + required: false + }, + { + name: 'host', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'test-port', + default: 7357, + description: 'The test port to use when running tests. Pass 0 to automatically pick an available port', + aliases: ['tp'], + key: 'testPort', + required: false + }, + { + name: 'filter', + description: 'A string to filter tests to run', + aliases: ['f'], + key: 'filter', + required: false + }, + { + name: 'module', + description: 'The name of a test module to run', + aliases: ['m'], + key: 'module', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'launch', + default: false, + description: 'A comma separated list of browsers to launch for tests.', + key: 'launch', + required: false + }, + { + name: 'reporter', + description: 'Test reporter to use [tap|dot|xunit] (default: tap)', + aliases: ['r'], + key: 'reporter', + required: false + }, + { + name: 'silent', + default: false, + description: 'Suppress any output except for the test report', + key: 'silent', + required: false + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure testem to run the test suite using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'testem-debug', + description: 'File to write a debug log from testem', + key: 'testemDebug', + required: false + }, + { + name: 'test-page', + description: 'Test page to invoke', + key: 'testPage', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path', + }, + { + name: 'query', + description: 'A query string to append to the test page URL.', + key: 'query', + required: false + } + ], + anonymousOptions: [] + }, + { + name: 'version', + description: 'outputs ember-cli version', + aliases: ['v', '--version', '-v'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + key: 'verbose', + required: false + } + ], + anonymousOptions: [] + } + ], + addons: [] +}; diff --git a/tests/fixtures/help/with-addon-commands.js b/tests/fixtures/help/with-addon-commands.js new file mode 100644 index 0000000000..7885382cbc --- /dev/null +++ b/tests/fixtures/help/with-addon-commands.js @@ -0,0 +1,787 @@ +const processHelpString = require('../../helpers/process-help-string'); +const versionUtils = require('../../../lib/utilities/version-utils'); +var emberCLIVersion = versionUtils.emberCLIVersion; + +module.exports = { + name: 'ember', + description: null, + aliases: [], + works: 'insideProject', + availableOptions: [], + anonymousOptions: [''], + version: emberCLIVersion(), + commands: [ + { + name: 'addon', + description: 'Generates a new folder structure for building an addon, complete with test harness.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'addon', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: "Sets the base human language of the addon's own test application via index.html", + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the addon to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'build', + description: 'Vestigial command in Vite-based projects. Use the `build` script from package.json instead.', + aliases: ['b'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'development', + aliases: [ + 'e', + { dev: 'development' }, + { prod: 'production' } + ], + key: 'environment', + required: false + }, + { + name: 'suppress-sizes', + default: false, + key: 'suppressSizes', + required: false + }, + { + name: 'output-path', + aliases: ['o'], + default: 'dist/', + key: 'outputPath', + required: false, + type: 'Path' + }, + ], + anonymousOptions: [] + }, + { + name: 'destroy', + description: 'Destroys code generated by `generate` command.', + aliases: ['d'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Specifically destroys the TypeScript output of the `generate` command. Run `--no-typescript` to instead target the JavaScript output.', + key: 'typescript', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'generate', + description: 'Generates new code from blueprints.', + aliases: ['g'], + works: 'insideProject', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'pod', + default: false, + aliases: ['p', 'pods'], + key: 'pod', + required: false + }, + { + name: 'classic', + default: false, + aliases: ['c'], + key: 'classic', + required: false + }, + { + name: 'in-repo-addon', + default: null, + aliases: ['in-repo', 'ir'], + key: 'inRepoAddon', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'in', + default: null, + description: 'Runs a blueprint against an in repo addon. A path is expected, relative to the root of the project.', + key: 'in', + required: false + }, + { + name: 'typescript', + aliases: ['ts'], + description: 'Generates a version of the blueprint written in TypeScript (if available).', + key: 'typescript', + required: false + } + ], + anonymousOptions: [''], + availableBlueprints: [ + { + 'ember-cli': [ + { + name: 'addon', + description: 'The default blueprint for ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'addon-import', + description: 'Generates an import wrapper.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'app', + description: 'The default blueprint for ember-cli projects.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'blueprint', + description: 'Generates a blueprint and definition.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'http-mock', + description: '[Classic Only] Generates a mock api endpoint in /api prefix.', + availableOptions: [], + anonymousOptions: ['endpoint-path'], + overridden: false + }, + { + name: 'http-proxy', + description: '[Classic Only] Generates a relative proxy to another server.', + availableOptions: [], + anonymousOptions: ['local-path', 'remote-url'], + overridden: false + }, + { + name: 'in-repo-addon', + description: 'The blueprint for addon in repo ember-cli addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'lib', + description: 'Generates a lib directory for in-repo addons.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + }, + { + name: 'server', + description: '[Classic Only] Generates a server directory for mocks and proxies.', + availableOptions: [], + anonymousOptions: ['name'], + overridden: false + } + ] + } + ] + }, + { + name: 'help', + description: 'Outputs the usage instructions for all commands or the provided command', + aliases: [null, 'h', '--help', '-h'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'json', + default: false, + key: 'json', + required: false + } + ], + anonymousOptions: [''] + }, + { + name: 'init', + description: 'Reinitializes a new ember-cli project in the current folder.', + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'name', + default: '', + aliases: ['n'], + key: 'name', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + default: 'github', + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'install', + description: 'Installs an ember-cli addon from npm.', + aliases: ['i'], + works: 'insideProject', + availableOptions: [ + { + name: 'save', + default: false, + aliases: ['S'], + key: 'save', + required: false + }, + { + name: 'save-dev', + default: true, + aliases: ['D'], + key: 'saveDev', + required: false + }, + { + name: 'save-exact', + default: false, + aliases: ['E', 'exact'], + key: 'saveExact', + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + description: + 'Use this option to force the usage of a specific package manager. By default, ember-cli will try to detect the right package manager from any lockfiles that exist in your project.', + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + ], + anonymousOptions: [''] + }, + { + name: 'new', + description: processHelpString('Creates a new directory and runs \u001b[32member init\u001b[39m in it.'), + aliases: [], + works: 'everywhere', + availableOptions: [ + { + name: 'dry-run', + default: false, + aliases: ['d'], + key: 'dryRun', + required: false + }, + { + name: 'verbose', + default: false, + aliases: ['v'], + key: 'verbose', + required: false + }, + { + name: 'blueprint', + default: 'app', + aliases: ['b'], + key: 'blueprint', + required: false + }, + { + name: 'skip-npm', + default: false, + aliases: ['sn', 'skip-install', 'si'], + key: 'skipNpm', + required: false + }, + { + name: 'skip-git', + default: false, + aliases: ['sg'], + key: 'skipGit', + required: false + }, + { + name: 'welcome', + key: 'welcome', + description: 'Installs and uses {{ember-welcome-page}}. Use --no-welcome to skip it.', + default: true, + required: false + }, + { + aliases: [{ npm: 'npm' }, { pnpm: 'pnpm' }, { yarn: 'yarn' }], + key: 'packageManager', + name: 'package-manager', + required: false, + type: ['npm', 'pnpm', 'yarn'], + }, + { + name: 'directory', + aliases: ['dir'], + key: 'directory', + required: false + }, + { + name: 'lang', + key: 'lang', + description: 'Sets the base human language of the application via index.html', + required: false + }, + { + name: 'lint-fix', + default: true, + key: 'lintFix', + required: false + }, + { + default: false, + key: 'embroider', + name: 'embroider', + description: 'Deprecated: Enables the build system to use Embroider', + required: false + }, + { + name: 'ci-provider', + key: 'ciProvider', + type: ['github', 'none'], + description: 'Installs the optional default CI blueprint. Only Github Actions is supported at the moment.', + required: false, + }, + { + name: 'ember-data', + key: 'emberData', + description: 'Include ember-data dependencies and configuration', + required: false, + default: true + }, + { + name: 'interactive', + default: false, + aliases: ['i'], + description: 'Create a new Ember app/addon in an interactive way.', + key: 'interactive', + required: false, + }, + { + aliases: ['ts'], + default: false, + key: 'typescript', + name: 'typescript', + description: 'Set up the app to use TypeScript', + required: false, + }, + { + default: true, + description: 'Use GJS/GTS templates by default for generated components, tests, and route templates', + key: 'strict', + name: 'strict', + required: false + }, + ], + anonymousOptions: [''] + }, + { + name: 'test', + description: 'Runs your app\'s test suite.', + aliases: ['t'], + works: 'insideProject', + availableOptions: [ + { + name: 'environment', + description: 'Possible values are "development", "production", and "test".', + default: 'test', + aliases: ['e'], + key: 'environment', + required: false + }, + { + name: 'config-file', + aliases: ['c', 'cf'], + key: 'configFile', + required: false + }, + { + name: 'host', + aliases: ['H'], + key: 'host', + required: false + }, + { + name: 'test-port', + default: 7357, + description: 'The test port to use when running tests. Pass 0 to automatically pick an available port', + aliases: ['tp'], + key: 'testPort', + required: false + }, + { + name: 'filter', + description: 'A string to filter tests to run', + aliases: ['f'], + key: 'filter', + required: false + }, + { + name: 'module', + description: 'The name of a test module to run', + aliases: ['m'], + key: 'module', + required: false + }, + { + name: 'watcher', + default: 'events', + aliases: ['w'], + key: 'watcher', + required: false + }, + { + name: 'launch', + default: false, + description: 'A comma separated list of browsers to launch for tests.', + key: 'launch', + required: false + }, + { + name: 'reporter', + description: 'Test reporter to use [tap|dot|xunit] (default: tap)', + aliases: ['r'], + key: 'reporter', + required: false + }, + { + name: 'silent', + default: false, + description: 'Suppress any output except for the test report', + key: 'silent', + required: false + }, + { + name: 'ssl', + default: false, + description: 'Set to true to configure testem to run the test suite using SSL.', + key: 'ssl', + required: false + }, + { + name: 'ssl-key', + default: 'ssl/server.key', + description: 'Specify the private key to use for SSL.', + key: 'sslKey', + required: false + }, + { + name: 'ssl-cert', + default: 'ssl/server.crt', + description: 'Specify the certificate to use for SSL.', + key: 'sslCert', + required: false + }, + { + name: 'testem-debug', + description: 'File to write a debug log from testem', + key: 'testemDebug', + required: false + }, + { + name: 'test-page', + description: 'Test page to invoke', + key: 'testPage', + required: false + }, + { + name: 'path', + description: 'Reuse an existing build at given path.', + key: 'path', + required: false, + type: 'Path', + }, + { + name: 'query', + description: 'A query string to append to the test page URL.', + key: 'query', + required: false + } + ], + anonymousOptions: [] + }, + { + name: 'version', + description: 'outputs ember-cli version', + aliases: ['v', '--version', '-v'], + works: 'everywhere', + availableOptions: [ + { + name: 'verbose', + default: false, + key: 'verbose', + required: false + } + ], + anonymousOptions: [] + } + ], + addons: [ + { + name: 'dummy-addon', + commands: [ + { + name: 'foo', + description: 'Initializes the warp drive.', + aliases: [], + works: 'insideProject', + availableOptions: [ + { + aliases: [ + 'd' + ], + default: false, + key: 'dryRun', + name: 'dry-run', + required: false + } + ], + anonymousOptions: [ + '' + ] + } + ] + } + ] +}; diff --git a/tests/fixtures/installation-checker/invalid-bower-installation/bower.json b/tests/fixtures/installation-checker/invalid-bower-installation/bower.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/fixtures/installation-checker/invalid-npm-installation/package.json b/tests/fixtures/installation-checker/invalid-npm-installation/package.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/fixtures/installation-checker/valid-bower-empty-installation/bower.json b/tests/fixtures/installation-checker/valid-bower-empty-installation/bower.json deleted file mode 100644 index 4208a9853e..0000000000 --- a/tests/fixtures/installation-checker/valid-bower-empty-installation/bower.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dependencies": [] -} diff --git a/tests/fixtures/installation-checker/valid-bower-installation/bower.json b/tests/fixtures/installation-checker/valid-bower-installation/bower.json deleted file mode 100644 index 4208a9853e..0000000000 --- a/tests/fixtures/installation-checker/valid-bower-installation/bower.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dependencies": [] -} diff --git a/tests/fixtures/installation-checker/valid-npm-installation/package.json b/tests/fixtures/installation-checker/valid-npm-installation/package.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/fixtures/instrumentation-disabled-config/.ember-cli b/tests/fixtures/instrumentation-disabled-config/.ember-cli new file mode 100644 index 0000000000..4c91bd9924 --- /dev/null +++ b/tests/fixtures/instrumentation-disabled-config/.ember-cli @@ -0,0 +1,3 @@ +{ + "enableInstrumentation": false +} diff --git a/tests/fixtures/instrumentation-enabled-config/.ember-cli b/tests/fixtures/instrumentation-enabled-config/.ember-cli new file mode 100644 index 0000000000..a4213d7a8e --- /dev/null +++ b/tests/fixtures/instrumentation-enabled-config/.ember-cli @@ -0,0 +1,3 @@ +{ + "enableInstrumentation": true +} diff --git a/tests/fixtures/leek-config/.ember-cli b/tests/fixtures/leek-config/.ember-cli deleted file mode 100644 index ec684d5cf5..0000000000 --- a/tests/fixtures/leek-config/.ember-cli +++ /dev/null @@ -1,10 +0,0 @@ -{ - "leekOptions": { - "adapterUrls": { - "event": "http://www.example.com/event", - "exception": "http://www.example.com/error", - "timing": "http://www.example.com/timing", - "appview": "http://www.example.com/track" - } - } -} diff --git a/tests/fixtures/missing-before-addon/lib/sample-addon/index.js b/tests/fixtures/missing-before-addon/lib/sample-addon/index.js index d96f621d40..3a77c77d62 100644 --- a/tests/fixtures/missing-before-addon/lib/sample-addon/index.js +++ b/tests/fixtures/missing-before-addon/lib/sample-addon/index.js @@ -1,3 +1,3 @@ module.exports = { - name: 'sample-addon' + name: require('./package').name, }; diff --git a/tests/fixtures/missing-before-addon/package.json b/tests/fixtures/missing-before-addon/package.json index 10d8b3f352..77932ebfb4 100644 --- a/tests/fixtures/missing-before-addon/package.json +++ b/tests/fixtures/missing-before-addon/package.json @@ -5,5 +5,4 @@ "lib/sample-addon" ] } - } diff --git a/tests/fixtures/preprocessor-tests/app-registry-ordering/app/components/should-be-preprocessed.js b/tests/fixtures/preprocessor-tests/app-registry-ordering/app/components/should-be-preprocessed.js index 5f6b817ff7..b63a51739d 100644 --- a/tests/fixtures/preprocessor-tests/app-registry-ordering/app/components/should-be-preprocessed.js +++ b/tests/fixtures/preprocessor-tests/app-registry-ordering/app/components/should-be-preprocessed.js @@ -1,5 +1,5 @@ -import Ember from 'ember'; +import Component from '@ember/component'; -export default Ember.Component.extend({ - foo: /__SECOND_PREPROCESSOR_REPLACEMENT_TOKEN__/ //jshint ignore:line +export default Component.extend({ + foo: /__SECOND_PREPROCESSOR_REPLACEMENT_TOKEN__/ }); diff --git a/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/first-dummy-preprocessor/index.js b/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/first-dummy-preprocessor/index.js index 095756dcd6..a2103a59b4 100644 --- a/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/first-dummy-preprocessor/index.js +++ b/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/first-dummy-preprocessor/index.js @@ -1,14 +1,9 @@ 'use strict'; -var Filter = require('broccoli-filter'); +const Filter = require('broccoli-filter'); function DummyFilter(inputTree, options) { - if (!(this instanceof DummyFilter)) { - return new DummyFilter(inputTree, options); - } - - this.inputTree = inputTree; - this.options = options || {}; + Filter.call(this, inputTree, options); } DummyFilter.prototype = Object.create(Filter.prototype); @@ -22,14 +17,14 @@ DummyFilter.prototype.processString = function (string, relativePath) { }; module.exports = { - name: 'first-dummy-preprocessor', - setupPreprocessorRegistry: function(type, registry) { + name: require('./package').name, + setupPreprocessorRegistry(type, registry) { if (type !== 'parent') { return; } var plugin = { name: 'first-dummy-preprocessor', ext: 'js', - toTree: function(tree) { - return DummyFilter(tree, {}); + toTree(tree) { + return new DummyFilter(tree, {}); } }; diff --git a/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/second-dummy-preprocessor/index.js b/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/second-dummy-preprocessor/index.js index aac6df8adb..855f597db7 100644 --- a/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/second-dummy-preprocessor/index.js +++ b/tests/fixtures/preprocessor-tests/app-registry-ordering/node_modules/second-dummy-preprocessor/index.js @@ -1,14 +1,9 @@ 'use strict'; -var Filter = require('broccoli-filter'); +const Filter = require('broccoli-filter'); function DummyFilter(inputTree, options) { - if (!(this instanceof DummyFilter)) { - return new DummyFilter(inputTree, options); - } - - this.inputTree = inputTree; - this.options = options || {}; + Filter.call(this, inputTree, options); } DummyFilter.prototype = Object.create(Filter.prototype); @@ -22,14 +17,14 @@ DummyFilter.prototype.processString = function (string, relativePath) { }; module.exports = { - name: 'second-dummy-preprocessor', - setupPreprocessorRegistry: function(type, registry) { + name: require('./package').name, + setupPreprocessorRegistry(type, registry) { if (type !== 'parent') { return; } var plugin = { name: 'second-dummy-preprocessor', ext: 'js', - toTree: function(tree) { - return DummyFilter(tree, {}); + toTree(tree) { + return new DummyFilter(tree, {}); } }; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/app/components/should-not-be-preprocessed.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/app/components/should-not-be-preprocessed.js index 8d761780bc..003ba9c8a7 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/app/components/should-not-be-preprocessed.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/app/components/should-not-be-preprocessed.js @@ -1,5 +1,5 @@ -import Ember from 'ember'; +import Component from '@ember/component'; -export default Ember.Component.extend({ - foo: __PREPROCESSOR_REPLACEMENT_TOKEN__ //jshint ignore:line +export default Component.extend({ + foo_in_app: __PREPROCESSOR_REPLACEMENT_TOKEN__ }); diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/addon/components/clitest-example.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/addon/components/clitest-example.js index da91a67ee5..b83229f6d2 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/addon/components/clitest-example.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/addon/components/clitest-example.js @@ -1,5 +1,5 @@ -import Ember from 'ember'; +import Component from '@ember/component'; -export default Ember.Component.extend({ - foo: __PREPROCESSOR_REPLACEMENT_TOKEN__ +export default Component.extend({ + foo_in_addon: __PREPROCESSOR_REPLACEMENT_TOKEN__ }); diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/index.js index 733f4299c9..2e1d1d8d5f 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/index.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'ember-cool-addon' + name: require('./package').name }; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/node_modules/dummy-preprocessor/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/node_modules/dummy-preprocessor/index.js index cb681ad996..e40de88eb5 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/node_modules/dummy-preprocessor/index.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/node_modules/dummy-preprocessor/index.js @@ -1,14 +1,9 @@ 'use strict'; -var Filter = require('broccoli-filter'); +const Filter = require('broccoli-filter'); function DummyFilter(inputTree, options) { - if (!(this instanceof DummyFilter)) { - return new DummyFilter(inputTree, options); - } - - this.inputTree = inputTree; - this.options = options || {}; + Filter.call(this, inputTree, options); } DummyFilter.prototype = Object.create(Filter.prototype); @@ -26,14 +21,14 @@ DummyFilter.prototype.processString = function (string, relativePath) { }; module.exports = { - name: 'dummy-preprocessor', - setupPreprocessorRegistry: function(type, registry) { + name: require('./package').name, + setupPreprocessorRegistry(type, registry) { if (type !== 'parent') { return; } var plugin = { name: 'dummy-preprocessor', ext: 'js', - toTree: function(tree) { - return DummyFilter(tree, {}); + toTree(tree) { + return new DummyFilter(tree, {}); } }; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/package.json b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/package.json index eecd1f6b6c..d9f9d75aa3 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/package.json +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-2/node_modules/ember-cool-addon/package.json @@ -9,4 +9,3 @@ "dummy-preprocessor": "latest" } } - diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/app/components/should-not-be-preprocessed.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/app/components/should-not-be-preprocessed.js index 8d761780bc..003ba9c8a7 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/app/components/should-not-be-preprocessed.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/app/components/should-not-be-preprocessed.js @@ -1,5 +1,5 @@ -import Ember from 'ember'; +import Component from '@ember/component'; -export default Ember.Component.extend({ - foo: __PREPROCESSOR_REPLACEMENT_TOKEN__ //jshint ignore:line +export default Component.extend({ + foo_in_app: __PREPROCESSOR_REPLACEMENT_TOKEN__ }); diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/addon/components/clitest-shallow-example.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/addon/components/clitest-shallow-example.js index 5afa78ac70..21af936ba2 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/addon/components/clitest-shallow-example.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/addon/components/clitest-shallow-example.js @@ -2,5 +2,5 @@ import Ember from 'ember'; import DeepExample from 'ember-deep-addon/components/clitest-deep-example'; export default DeepExample.extend({ - shallow: __PREPROCESSOR_REPLACEMENT_TOKEN__ //jshint ignore:line + shallow: __PREPROCESSOR_REPLACEMENT_TOKEN__ }); diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/index.js index f482005153..2e1d1d8d5f 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/index.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'ember-shallow-addon' + name: require('./package').name }; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/addon/components/clitest-deep-example.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/addon/components/clitest-deep-example.js index 632d6093dd..51f426f05e 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/addon/components/clitest-deep-example.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/addon/components/clitest-deep-example.js @@ -1,5 +1,5 @@ -import Ember from 'ember'; +import Component from '@ember/component'; -export default Ember.Component.extend({ +export default Component.extend({ deep: __PREPROCESSOR_REPLACEMENT_TOKEN__ }); diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/index.js index 6180986e2b..2e1d1d8d5f 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/index.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'ember-deep-addon' + name: require('./package').name }; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/node_modules/dummy-preprocessor/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/node_modules/dummy-preprocessor/index.js index cb681ad996..e40de88eb5 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/node_modules/dummy-preprocessor/index.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/node_modules/dummy-preprocessor/index.js @@ -1,14 +1,9 @@ 'use strict'; -var Filter = require('broccoli-filter'); +const Filter = require('broccoli-filter'); function DummyFilter(inputTree, options) { - if (!(this instanceof DummyFilter)) { - return new DummyFilter(inputTree, options); - } - - this.inputTree = inputTree; - this.options = options || {}; + Filter.call(this, inputTree, options); } DummyFilter.prototype = Object.create(Filter.prototype); @@ -26,14 +21,14 @@ DummyFilter.prototype.processString = function (string, relativePath) { }; module.exports = { - name: 'dummy-preprocessor', - setupPreprocessorRegistry: function(type, registry) { + name: require('./package').name, + setupPreprocessorRegistry(type, registry) { if (type !== 'parent') { return; } var plugin = { name: 'dummy-preprocessor', ext: 'js', - toTree: function(tree) { - return DummyFilter(tree, {}); + toTree(tree) { + return new DummyFilter(tree, {}); } }; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/package.json b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/package.json index 11ed6d5535..9b42be2d37 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/package.json +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/node_modules/ember-deep-addon/package.json @@ -9,4 +9,3 @@ "dummy-preprocessor": "latest" } } - diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/package.json b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/package.json index 8717559f06..7897ff62aa 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/package.json +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors-3/node_modules/ember-shallow-addon/package.json @@ -6,7 +6,7 @@ "ember-addon" ], "dependencies": { + "ember-cli-babel": "latest", "ember-deep-addon": "latest" } } - diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/broccoli-sass/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/broccoli-sass/index.js index bad437a18f..48dfda3f04 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/broccoli-sass/index.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/broccoli-sass/index.js @@ -1,8 +1,8 @@ 'use strict'; -var fs = require('fs'); -var path = require('path'); -var Writer = require('broccoli-writer'); +const fs = require('fs'); +const path = require('path'); +const Plugin = require('broccoli-plugin'); function copyPreserveSync (src, dest) { var srcStats = fs.statSync(src); @@ -24,23 +24,17 @@ function copyPreserveSync (src, dest) { } } -module.exports = SassCompiler; -SassCompiler.prototype = Object.create(Writer.prototype); -SassCompiler.prototype.constructor = SassCompiler; -function SassCompiler (sourceTrees, inputFile, outputFile, options) { - if (!(this instanceof SassCompiler)) return new SassCompiler(sourceTrees, inputFile, outputFile, options); - if (!Array.isArray(sourceTrees)) throw new Error('Expected array for first argument - did you mean [tree] instead of tree?'); - this.sourceTrees = sourceTrees; - this.inputFile = inputFile; - this.outputFile = outputFile; -} - -SassCompiler.prototype.write = function (readTree, destDir) { - var self = this; +class SassCompiler extends Plugin { + constructor(inputNodes, inputFile, outputFile, options) { + super(inputNodes, options); + this.inputFile = inputFile; + this.outputFile = outputFile; + } - return readTree(this.sourceTrees[0]).then(function (srcDir) { + build() { copyPreserveSync( - path.join(srcDir, self.inputFile), - path.join(destDir, self.outputFile)); - }); -}; + path.join(this.inputPaths[0], this.inputFile), + path.join(this.outputPath, this.outputFile)); + } +} +module.exports = SassCompiler; \ No newline at end of file diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cli-sass/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cli-sass/index.js new file mode 100644 index 0000000000..0af2c4cf95 --- /dev/null +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cli-sass/index.js @@ -0,0 +1,37 @@ +'use strict'; + +const path = require('path'); +const mergeTrees = require('broccoli-merge-trees'); +const SassCompiler = require('../broccoli-sass'); + +function SASSPlugin() { + this.name = 'ember-cli-sass'; + this.ext = ['scss', 'sass']; +} + +SASSPlugin.prototype.toTree = function(tree, inputPath, outputPath, inputOptions) { + var options = inputOptions; + + var inputTrees = [tree]; + if (options.includePaths) { + inputTrees = inputTrees.concat(options.includePaths); + } + + var ext = options.extension || 'scss'; + var paths = options.outputPaths; + var trees = Object.keys(paths).map(function(file) { + var input = path.join(inputPath, file + '.' + ext); + var output = paths[file]; + return new SassCompiler(inputTrees, input, output, options); + }); + + return mergeTrees(trees); +}; + +module.exports = { + name: require('./package').name, + + setupPreprocessorRegistry(type, registry) { + registry.add('css', new SASSPlugin()); + }, +}; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cli-sass/package.json b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cli-sass/package.json new file mode 100644 index 0000000000..4076438315 --- /dev/null +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cli-sass/package.json @@ -0,0 +1,12 @@ +{ + "name": "ember-cli-sass", + "private": true, + "version": "1.0.0", + "main": "index.js", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "broccoli-sass": "latest" + } +} diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/index.js b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/index.js index 733f4299c9..2e1d1d8d5f 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/index.js +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/index.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - name: 'ember-cool-addon' + name: require('./package').name }; diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/package.json b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/package.json index d6675b9b5c..32578abbe8 100644 --- a/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/package.json +++ b/tests/fixtures/preprocessor-tests/app-with-addon-with-preprocessors/node_modules/ember-cool-addon/package.json @@ -6,7 +6,7 @@ "ember-addon" ], "dependencies": { - "broccoli-sass": "latest" + "ember-cli-babel": "latest", + "ember-cli-sass": "latest" } } - diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/app/styles/app.scss b/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/app/styles/app.scss deleted file mode 100644 index e6790d969b..0000000000 --- a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/app/styles/app.scss +++ /dev/null @@ -1 +0,0 @@ -/* app styles included */ diff --git a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/addon/styles/addon.css b/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/addon/styles/addon.css deleted file mode 100644 index 10e83e114e..0000000000 --- a/tests/fixtures/preprocessor-tests/app-with-addon-without-preprocessors/node_modules/ember-cool-addon/addon/styles/addon.css +++ /dev/null @@ -1 +0,0 @@ -/* addon styles included */ diff --git a/tests/fixtures/project-with-handlebars/bower.json b/tests/fixtures/project-with-handlebars/bower.json deleted file mode 100644 index b0f6bed46f..0000000000 --- a/tests/fixtures/project-with-handlebars/bower.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "test-project", - "dependencies": { - "handlebars": "1.3.0", - "jquery": "^1.11.1", - "ember": "1.7.0", - "ember-data": "1.0.0-beta.10", - "ember-resolver": "~0.1.7", - "loader.js": "ember-cli/loader.js#1.0.1", - "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", - "ember-cli-test-loader": "rwjblue/ember-cli-test-loader#0.0.4", - "ember-load-initializers": "ember-cli/ember-load-initializers#0.0.2" - }, - "devDependencies": { - "ember-qunit": "0.1.8", - "ember-qunit-notifications": "0.0.4", - "qunit": "~1.15.0" - } -} diff --git a/tests/fixtures/project-with-handlebars/package.json b/tests/fixtures/project-with-handlebars/package.json deleted file mode 100644 index 4bf5309c69..0000000000 --- a/tests/fixtures/project-with-handlebars/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "test-project", - "private": true, - "dependencies": { - }, - "ember-addon": { - }, - "devDependencies": { - "ember-cli": "latest" - } -} - diff --git a/tests/fixtures/restart-express-server/app-root/foo.txt b/tests/fixtures/restart-express-server/app-root/foo.txt deleted file mode 100644 index 79a43fb7fd..0000000000 --- a/tests/fixtures/restart-express-server/app-root/foo.txt +++ /dev/null @@ -1 +0,0 @@ -Initial Contents diff --git a/tests/fixtures/restart-express-server/app-root/server/index.js b/tests/fixtures/restart-express-server/app-root/server/index.js deleted file mode 100644 index 7b22b5345c..0000000000 --- a/tests/fixtures/restart-express-server/app-root/server/index.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var a = require('./subfolder/a'); - -module.exports = function () { - fs.writeFileSync('foo.txt', a()); -}; diff --git a/tests/fixtures/restart-express-server/app-root/server/subfolder/a.js b/tests/fixtures/restart-express-server/app-root/server/subfolder/a.js deleted file mode 100644 index bf25b3bf84..0000000000 --- a/tests/fixtures/restart-express-server/app-root/server/subfolder/a.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'Initial contents of A.'; -}; diff --git a/tests/fixtures/restart-express-server/app-root/server/subfolder/b.js b/tests/fixtures/restart-express-server/app-root/server/subfolder/b.js deleted file mode 100644 index b8a2c60a4f..0000000000 --- a/tests/fixtures/restart-express-server/app-root/server/subfolder/b.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'Initial contents of a'; -}; diff --git a/tests/fixtures/restart-express-server/copy1/subfolder/a.js b/tests/fixtures/restart-express-server/copy1/subfolder/a.js deleted file mode 100644 index 70f5283d8c..0000000000 --- a/tests/fixtures/restart-express-server/copy1/subfolder/a.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'Copy1 contents of A.'; -}; diff --git a/tests/fixtures/restart-express-server/copy1/subfolder/b.js b/tests/fixtures/restart-express-server/copy1/subfolder/b.js deleted file mode 100644 index 1caf9eaa65..0000000000 --- a/tests/fixtures/restart-express-server/copy1/subfolder/b.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'Copy1 contents of B.'; -}; diff --git a/tests/fixtures/restart-express-server/copy1/subfolder/c.js b/tests/fixtures/restart-express-server/copy1/subfolder/c.js deleted file mode 100644 index d018db6b71..0000000000 --- a/tests/fixtures/restart-express-server/copy1/subfolder/c.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'Copy1 contents of C.'; -}; diff --git a/tests/fixtures/restart-express-server/copy2/index.js b/tests/fixtures/restart-express-server/copy2/index.js deleted file mode 100644 index bddf8b4d0f..0000000000 --- a/tests/fixtures/restart-express-server/copy2/index.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var a = require('./subfolder/a'); -var b = require('./subfolder/b'); - -module.exports = function () { - fs.writeFileSync('foo.txt', a() + ' ' + b()); -}; diff --git a/tests/fixtures/restart-express-server/copy2/subfolder/a.js b/tests/fixtures/restart-express-server/copy2/subfolder/a.js deleted file mode 100644 index 21539d6f16..0000000000 --- a/tests/fixtures/restart-express-server/copy2/subfolder/a.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'Copy2 contents of A.'; -}; diff --git a/tests/fixtures/restart-express-server/copy2/subfolder/b.js b/tests/fixtures/restart-express-server/copy2/subfolder/b.js deleted file mode 100644 index 494a953e0a..0000000000 --- a/tests/fixtures/restart-express-server/copy2/subfolder/b.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function () { - return 'Copy2 contents of B.'; -}; diff --git a/tests/fixtures/restart-express-server/copy3/index.js b/tests/fixtures/restart-express-server/copy3/index.js deleted file mode 100644 index a7e5cc7146..0000000000 --- a/tests/fixtures/restart-express-server/copy3/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var path = require('path'); -var aCache = require.cache[path.resolve('./subfolder/a')]; -var bCache = require.cache[path.resolve('./subfolder/b')]; - -module.exports = function () { - fs.writeFileSync('foo.txt', !aCache + ' ' + !bCache); -}; diff --git a/tests/fixtures/smoke-tests/failing-test/tests/unit/some-test.js b/tests/fixtures/smoke-tests/failing-test/tests/unit/some-test.js deleted file mode 100644 index 060f825311..0000000000 --- a/tests/fixtures/smoke-tests/failing-test/tests/unit/some-test.js +++ /dev/null @@ -1,7 +0,0 @@ -/*jshint strict:false */ -/* globals QUnit */ - -QUnit.test('failing test', function(assert) { - assert.ok(false, 'test should fail to confirm ember test exit code'); -}); - diff --git a/tests/fixtures/smoke-tests/js-testem-config/testem.js b/tests/fixtures/smoke-tests/js-testem-config/testem.js new file mode 100644 index 0000000000..e60c6e0417 --- /dev/null +++ b/tests/fixtures/smoke-tests/js-testem-config/testem.js @@ -0,0 +1,19 @@ +console.log('***CUSTOM_TESTEM_JS**'); + +module.exports = { + "framework": "qunit", + "test_page": "tests/index.html?hidepassed", + "disable_watching": true, + "launch_in_ci": ["Chrome"], + "launch_in_dev": ["Chrome"], + "browser_args": { + "Chrome": [ + // --no-sandbox is needed when running Chrome inside a container + process.env.CI ? '--no-sandbox' : null, + + "--headless", + "--remote-debugging-port=0", + "--window-size=1440,900", + ].filter(Boolean), + }, +}; diff --git a/tests/fixtures/smoke-tests/no-testem-launchers/testem.json b/tests/fixtures/smoke-tests/no-testem-launchers/testem.json deleted file mode 100644 index 1c85b77b7c..0000000000 --- a/tests/fixtures/smoke-tests/no-testem-launchers/testem.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "framework": "qunit", - "test_page": "tests/index.html", - "launch_in_ci": [ ], - "launch_in_dev": [ ] -} diff --git a/tests/fixtures/smoke-tests/passing-test/tests/acceptance/acceptance-test.js b/tests/fixtures/smoke-tests/passing-test/tests/acceptance/acceptance-test.js new file mode 100644 index 0000000000..a6da5ad481 --- /dev/null +++ b/tests/fixtures/smoke-tests/passing-test/tests/acceptance/acceptance-test.js @@ -0,0 +1,20 @@ +import { setupApplicationTest } from 'ember-qunit'; +import QUnit, { module, test } from 'qunit'; + +let firstArgument; + +module('Module', function(hooks) { + setupApplicationTest(hooks); + + hooks.beforeEach(function(assert) { + firstArgument = assert; + }); + + test('it works', function(assert) { + assert.ok(this.owner, 'setupApplicationTest binds to the context'); + assert.ok( + Object.getPrototypeOf(firstArgument) === QUnit.assert, + 'first argument is QUnit assert' + ); + }); +}); diff --git a/tests/fixtures/smoke-tests/passing-test/tests/unit/some-test.js b/tests/fixtures/smoke-tests/passing-test/tests/unit/some-test.js index dff4b489c3..4d12dc60e7 100644 --- a/tests/fixtures/smoke-tests/passing-test/tests/unit/some-test.js +++ b/tests/fixtures/smoke-tests/passing-test/tests/unit/some-test.js @@ -1,7 +1,5 @@ -/*jshint strict:false */ -/* globals QUnit */ +import { test } from 'qunit'; -QUnit.test('passing test', function(assert) { +test('passing test', function(assert) { assert.ok(true, 'test should pass'); }); - diff --git a/tests/fixtures/smoke-tests/serve-wasm/public/foo.wasm b/tests/fixtures/smoke-tests/serve-wasm/public/foo.wasm new file mode 100644 index 0000000000..e5b37c1c5b Binary files /dev/null and b/tests/fixtures/smoke-tests/serve-wasm/public/foo.wasm differ diff --git a/tests/fixtures/smoke-tests/serve-wasm/tests/wasm-test.js b/tests/fixtures/smoke-tests/serve-wasm/tests/wasm-test.js new file mode 100644 index 0000000000..831bde5703 --- /dev/null +++ b/tests/fixtures/smoke-tests/serve-wasm/tests/wasm-test.js @@ -0,0 +1,8 @@ +import { test } from 'qunit'; + +test('wasm test', function(assert) { + assert.expect(1); + return WebAssembly.instantiateStreaming(fetch('/foo.wasm')).then(mod => { + assert.ok(mod.instance, 'has instance'); + }); +}); diff --git a/tests/fixtures/smoke-tests/test-with-syntax-error/tests/unit/some-test.js b/tests/fixtures/smoke-tests/test-with-syntax-error/tests/unit/some-test.js deleted file mode 100644 index fd47599689..0000000000 --- a/tests/fixtures/smoke-tests/test-with-syntax-error/tests/unit/some-test.js +++ /dev/null @@ -1,7 +0,0 @@ -/*jshint strict:false */ -/* globals QUnit */ - -QUnit.test('syntax error', function(assert) { - # syntax error -}); - diff --git a/tests/fixtures/smoke-tests/with-template-failing-linting/app/pod-template/template.hbs b/tests/fixtures/smoke-tests/with-template-failing-linting/app/pod-template/template.hbs new file mode 100644 index 0000000000..b883e220d5 --- /dev/null +++ b/tests/fixtures/smoke-tests/with-template-failing-linting/app/pod-template/template.hbs @@ -0,0 +1 @@ +BADJUJU diff --git a/tests/fixtures/smoke-tests/with-template-failing-linting/app/templates/blah.hbs b/tests/fixtures/smoke-tests/with-template-failing-linting/app/templates/blah.hbs new file mode 100644 index 0000000000..3b5e56663c --- /dev/null +++ b/tests/fixtures/smoke-tests/with-template-failing-linting/app/templates/blah.hbs @@ -0,0 +1 @@ +Good file! diff --git a/tests/fixtures/smoke-tests/with-template-failing-linting/app/templates/foo-bar.hbs b/tests/fixtures/smoke-tests/with-template-failing-linting/app/templates/foo-bar.hbs new file mode 100644 index 0000000000..b883e220d5 --- /dev/null +++ b/tests/fixtures/smoke-tests/with-template-failing-linting/app/templates/foo-bar.hbs @@ -0,0 +1 @@ +BADJUJU diff --git a/tests/fixtures/smoke-tests/with-template-failing-linting/node_modules/fake-template-linter/index.js b/tests/fixtures/smoke-tests/with-template-failing-linting/node_modules/fake-template-linter/index.js new file mode 100644 index 0000000000..cbf502a98b --- /dev/null +++ b/tests/fixtures/smoke-tests/with-template-failing-linting/node_modules/fake-template-linter/index.js @@ -0,0 +1,48 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const Plugin = require('broccoli-plugin'); +const walkSync = require('walk-sync'); + +module.exports = { + name: require('./package').name, + + lintTree(type, tree) { + if (type === 'templates') { + return new TemplateLinter(tree, { + annotation: 'TemplateLinter' + }); + } + } +}; + +class TemplateLinter extends Plugin { + constructor(inputNode, options) { + super([inputNode], options); + } + + build() { + var basePath = this.inputPaths[0]; + var templateFiles = walkSync(basePath) + .filter(function(entry) { + return /\.hbs$/.test(entry); + }); + + for (var i = 0; i < templateFiles.length; i++) { + var templateFilePath = templateFiles[i]; + + var contents = fs.readFileSync(path.join(basePath, templateFilePath), { encoding: 'utf8' }); + var testContents = 'QUnit.test("TemplateLint: ' + templateFilePath + '", function(assert) {\n' + + 'assert.ok(false, "DO NOT USE BADJUJU");\n' + + '})'; + var testFilePath = path.join(this.outputPath, templateFilePath + '-test.js'); + + if (/BADJUJU/.test(contents)) { + fs.mkdirsSync(path.dirname(testFilePath)); + + fs.writeFileSync(testFilePath, testContents, { encoding: 'utf8' }); + } + } + } +} diff --git a/tests/fixtures/smoke-tests/with-template-failing-linting/node_modules/fake-template-linter/package.json b/tests/fixtures/smoke-tests/with-template-failing-linting/node_modules/fake-template-linter/package.json new file mode 100644 index 0000000000..ac06406290 --- /dev/null +++ b/tests/fixtures/smoke-tests/with-template-failing-linting/node_modules/fake-template-linter/package.json @@ -0,0 +1,7 @@ +{ + "name": "fake-template-linter", + "version": "1.0.0", + "keywords": [ + "ember-addon" + ] +} diff --git a/tests/fixtures/tasks/builder/ember-cli-build.js b/tests/fixtures/tasks/builder/ember-cli-build.js new file mode 100644 index 0000000000..8b63ae04a4 --- /dev/null +++ b/tests/fixtures/tasks/builder/ember-cli-build.js @@ -0,0 +1,5 @@ +const path = require('path'); + +module.exports = function() { + return path.join(__dirname, 'some-dir'); +}; diff --git a/tests/fixtures/tasks/builder/some-dir/foo.txt b/tests/fixtures/tasks/builder/some-dir/foo.txt new file mode 100644 index 0000000000..934f7a704a --- /dev/null +++ b/tests/fixtures/tasks/builder/some-dir/foo.txt @@ -0,0 +1 @@ +Some file named foo.txt diff --git a/tests/fixtures/tasks/testem-config/testem-dummy.json b/tests/fixtures/tasks/testem-config/testem-dummy.json new file mode 100644 index 0000000000..303080ff39 --- /dev/null +++ b/tests/fixtures/tasks/testem-config/testem-dummy.json @@ -0,0 +1,9 @@ +{ + "framework": "qunit", + "launch": "Dummy", + "launchers": { + "Dummy" : { + "command": "exit" + } + } +} diff --git a/tests/fixtures/tasks/testem-config/testem-with-query-string.json b/tests/fixtures/tasks/testem-config/testem-with-query-string.json index 42a4ddb22a..f6c8ef7c84 100644 --- a/tests/fixtures/tasks/testem-config/testem-with-query-string.json +++ b/tests/fixtures/tasks/testem-config/testem-with-query-string.json @@ -1,11 +1,19 @@ { "framework": "qunit", "test_page": "tests/index.html?hidepassed", + "disable_watching": true, "launch_in_ci": [ - "PhantomJS" + "Chrome" ], "launch_in_dev": [ - "PhantomJS", "Chrome" - ] + ], + "browser_args": { + "Chrome": [ + "--no-sandbox", + "--headless", + "--remote-debugging-port=0", + "--window-size=1440,900" + ] + } } diff --git a/tests/fixtures/tasks/testem-config/testem.json b/tests/fixtures/tasks/testem-config/testem.json index eff93f9b65..b366a5d30d 100644 --- a/tests/fixtures/tasks/testem-config/testem.json +++ b/tests/fixtures/tasks/testem-config/testem.json @@ -2,10 +2,17 @@ "framework": "qunit", "test_page": "tests/index.html", "launch_in_ci": [ - "PhantomJS" + "Chrome" ], "launch_in_dev": [ - "PhantomJS", "Chrome" - ] + ], + "browser_args": { + "Chrome": [ + "--no-sandbox", + "--headless", + "--remote-debugging-port=0", + "--window-size=1440,900" + ] + } } diff --git a/tests/fixtures/with-unminified-styles/app/styles/app.css b/tests/fixtures/with-unminified-styles/app/styles/app.css new file mode 100644 index 0000000000..b2cd1ce731 --- /dev/null +++ b/tests/fixtures/with-unminified-styles/app/styles/app.css @@ -0,0 +1,18 @@ +html { + line-height: 1.15; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +h1 { + font-size: 2em; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} diff --git a/tests/helpers-internal/blueprint.js b/tests/helpers-internal/blueprint.js new file mode 100644 index 0000000000..d1583d281e --- /dev/null +++ b/tests/helpers-internal/blueprint.js @@ -0,0 +1,21 @@ +'use strict'; + +const fs = require('fs-extra'); + +const { globSync } = require('glob'); +const { expect } = require('chai'); + +function confirmViteBlueprint() { + let pkgJson = fs.readJsonSync('package.json'); + let viteConfig = globSync('vite.config.{js,cjs,mjs}'); + let emberCliUpdate = fs.readJsonSync('config/ember-cli-update.json'); + + expect(pkgJson.devDependencies['vite'], 'Installs vite in "devDependencies"').to.exist; + expect(pkgJson.scripts['start']).to.contain('vite'); + expect(viteConfig.length, 'creates a vite configuration').to.equal(1); + expect(emberCliUpdate.packages[0].name).to.equal('@ember/app-blueprint'); +} + +module.exports = { + confirmViteBlueprint, +}; diff --git a/tests/helpers-internal/file-utils.js b/tests/helpers-internal/file-utils.js new file mode 100644 index 0000000000..f2d6544f0d --- /dev/null +++ b/tests/helpers-internal/file-utils.js @@ -0,0 +1,22 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); + +function checkFile(inputPath, outputPath) { + if (process.env.WRITE_FIXTURES) { + let content = fs.readFileSync(inputPath, { encoding: 'utf-8' }); + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, content, { encoding: 'utf-8' }); + } + + expect(file(inputPath)).to.equal(file(outputPath)); +} + +module.exports = { + checkFile, +}; diff --git a/tests/helpers-internal/fixtures.js b/tests/helpers-internal/fixtures.js new file mode 100644 index 0000000000..62b2dec8b9 --- /dev/null +++ b/tests/helpers-internal/fixtures.js @@ -0,0 +1,53 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); +const { set, get, cloneDeep } = require('lodash'); + +const currentVersion = require('../../package').version; +const currentAppBlueprintVersion = require('@ember-tooling/classic-build-app-blueprint/package').version; +const currentAddonBlueprintVersion = require('@ember-tooling/classic-build-addon-blueprint/package').version; + +function checkEslintConfig(fixturePath) { + expect(file('eslint.config.mjs')).to.equal( + file(path.join(__dirname, '../fixtures', fixturePath, 'eslint.config.mjs')) + ); +} + +function checkFileWithJSONReplacement(fixtureName, fileName, targetPath, value) { + let fixturePath = path.join(__dirname, '../fixtures', fixtureName, fileName); + let fixtureContents = fs.readFileSync(fixturePath, { encoding: 'utf-8' }); + let fixtureData = JSON.parse(fixtureContents); + + let candidateContents = fs.readFileSync(fileName, 'utf8'); + let candidateData = JSON.parse(candidateContents); + + if (process.env.WRITE_FIXTURES) { + let newFixtureData = cloneDeep(candidateData); + set(newFixtureData, targetPath, get(fixtureData, targetPath)); + fs.mkdirSync(path.dirname(fixturePath), { recursive: true }); + fs.writeFileSync(fixturePath, `${JSON.stringify(newFixtureData, null, 2)}\n`, { encoding: 'utf-8' }); + } + + set(fixtureData, targetPath, value); + + expect(JSON.stringify(candidateData, null, 2)).to.equal(JSON.stringify(fixtureData, null, 2)); +} + +function checkEmberCLIBuild(fixtureName, fileName) { + let fixturePath = path.join(__dirname, '../fixtures', fixtureName, fileName); + let fixtureContents = fs.readFileSync(fixturePath, { encoding: 'utf-8' }); + expect(file(fileName)).to.equal(fixtureContents); +} + +module.exports = { + currentVersion, + currentAppBlueprintVersion, + currentAddonBlueprintVersion, + checkEslintConfig, + checkFileWithJSONReplacement, + checkEmberCLIBuild, +}; diff --git a/tests/helpers/acceptance.js b/tests/helpers/acceptance.js index d3f260b6a6..c208fbcac6 100644 --- a/tests/helpers/acceptance.js +++ b/tests/helpers/acceptance.js @@ -1,190 +1,174 @@ 'use strict'; -var symlinkOrCopySync = require('symlink-or-copy').sync; -var path = require('path'); -var fs = require('fs-extra'); -var runCommand = require('./run-command'); -var Promise = require('../../lib/ext/promise'); -var tmp = require('./tmp'); -var conf = require('./conf'); -var existsSync = require('exists-sync'); -var copy = Promise.denodeify(require('cpr')); -var root = process.cwd(); -var exec = Promise.denodeify(require('child_process').exec); - -var onOutput = { - onOutput: function() { - return; // no output for initial application build - } -}; +const symlinkOrCopySync = require('symlink-or-copy').sync; +const path = require('path'); +const fs = require('fs-extra'); +// eslint-disable-next-line n/no-unpublished-require +const tmp = require('tmp-promise'); +const { execa } = require('execa'); -function handleResult(result) { - console.log(result.output.join('\n')); - console.log(result.errors.join('\n')); - throw result; -} +const runCommand = require('./run-command'); +const hasGlobalYarn = require('../helpers/has-global-yarn'); -function downloaded(item) { - var exists = false; - switch (item) { - case 'node_modules': - exists = existsSync(path.join(root, '.node_modules-tmp')); - break; - case 'bower_components': - exists = existsSync(path.join(root, '.bower_components-tmp')); - break; - } +let root = path.resolve(__dirname, '..', '..'); - return exists; -} +const PackageCache = require('../../tests/helpers/package-cache'); -function mvRm(from, to) { - var dir = path.join(root, to); - from = path.resolve(from); +const quickTemp = require('quick-temp'); +let dirs = {}; - if (!existsSync(dir)) { - fs.mkdirsSync(dir); - fs.copySync(from, to); - fs.removeSync(from); - } -} +let runCommandOptions = { + // Note: We must override the default logOnFailure logging, because we are + // not inside a test. + log() { + return; // no output for initial application build + }, +}; -function symLinkDir(projectPath, from, to) { - symlinkOrCopySync(path.resolve(root, from), path.resolve(projectPath, to)); +function handleResult(result) { + if (result.output) { + console.log(result.output.join('\n')); + } + if (result.errors) { + console.log(result.errors.join('\n')); + } + throw result; } function applyCommand(command, name /*, ...flags*/) { - var flags = [].slice.call(arguments, 2, arguments.length); - var args = [path.join('..', 'bin', 'ember'), command, '--disable-analytics', '--watcher=node', '--skip-git', name, onOutput]; + let flags = [].slice.call(arguments, 2, arguments.length); + let binaryPath = path.resolve(path.join(__dirname, '..', '..', 'bin', 'ember')); + let args = [binaryPath, command, name, '--watcher=node', '--skip-git', runCommandOptions]; - flags.forEach(function(flag) { + flags.forEach(function (flag) { args.splice(2, 0, flag); }); return runCommand.apply(undefined, args); } -function createTmp(command) { - return tmp.setup('./common-tmp').then(function() { - process.chdir('./common-tmp'); - conf.setup(); - return command(); - }); -} +/** + * @class PrivateTestHelpers + */ /** * Use `createTestTargets` in the before hook to do the initial * setup of a project. This will ensure that we limit the amount of times * we go to the network to fetch dependencies. - * @param {String} projectName The name of the project. Can be a app or addon. + * + * @method createTestTargets + * @param {String} projectName The name of the project. Can be an app or addon. * @param {Object} options * @property {String} options.command The command you want to run * @return {Promise} The result of the running the command */ function createTestTargets(projectName, options) { - var command; + console.warn(`********************************************** +** DO NOT USE createTestTargets ANY MORE! ** +********************************************** + +Use createAndInstallTestTargets() which doesn't use a complicated packageCache system +and relies on the pnpm store for caching instead.`); + + let outputDir = quickTemp.makeOrReuse(dirs, projectName); + options = options || {}; options.command = options.command || 'new'; - var noNodeModules = !downloaded('node_modules'); - // Fresh install - if (noNodeModules && !downloaded('bower_components')) { - command = function() { - return applyCommand(options.command, projectName); - }; - // bower_components but no node_modules - } else if (noNodeModules && downloaded('bower_components')) { - command = function() { - return applyCommand(options.command, projectName, '--skip-bower'); - }; - // node_modules but no bower_components - } else if (!downloaded('bower_components') && downloaded('node_modules')) { - command = function() { - return applyCommand(options.command, projectName, '--skip-npm'); - }; - } else { - // Everything is already there - command = function() { - return applyCommand(options.command, projectName, '--skip-npm', '--skip-bower'); - }; - } - - return createTmp(function() { - return command(). - catch(handleResult). - then(function(value) { - if (noNodeModules) { - return exec('npm install ember-disable-prototype-extensions').then(function() { - return value; - }); - } - - return value; - }); - }); + return applyCommand(options.command, projectName, '--skip-npm', `--directory=${outputDir}`).catch(handleResult); } /** * Tears down the targeted project download directory - * and restores conf. - * @return {Promise} */ function teardownTestTargets() { - return tmp.teardown('./common-tmp').then(function() { - conf.restore(); - }); + // Remove all tmp directories created in this run. + let dirKeys = Object.keys(dirs); + for (let i = 0; i < dirKeys.length; i++) { + quickTemp.remove(dirs, dirKeys[i]); + } } /** * Creates symbolic links from the dependency temp directories * to the project that is under test. + * + * @method linkDependencies * @param {String} projectName The name of the project under test - * @return {Promise} + * @return {String} The path to the hydrated fixture. */ function linkDependencies(projectName) { - var targetPath = './tmp/' + projectName; - return tmp.setup('./tmp').then(function() { - return copy('./common-tmp/' + projectName, targetPath); - }).then(function() { - var nodeModulesPath = targetPath + '/node_modules/'; - var bowerComponentsPath = targetPath + '/bower_components/'; + let sourceFixture = dirs[projectName]; // original fixture for this acceptance test. + let runFixture = quickTemp.makeOrRemake(dirs, `${projectName}-clone`); - mvRm(nodeModulesPath, '.node_modules-tmp'); - mvRm(bowerComponentsPath, '.bower_components-tmp'); + fs.copySync(sourceFixture, runFixture); + let nodeManifest = fs.readFileSync(path.join(runFixture, 'package.json')); - if (!existsSync(nodeModulesPath)) { - symLinkDir(targetPath, '.node_modules-tmp', 'node_modules'); - } + let packageCache = new PackageCache(root); + let packager = hasGlobalYarn ? 'yarn' : 'npm'; - if (!existsSync(bowerComponentsPath)) { - symLinkDir(targetPath, '.bower_components-tmp', 'bower_components'); - } + packageCache.create('node', packager, nodeManifest, [{ name: 'ember-cli', path: root }]); - process.chdir('./tmp'); - var appsECLIPath = path.join(projectName, 'node_modules', 'ember-cli'); - var pwd = process.cwd(); - fs.removeSync(projectName + '/node_modules/ember-cli'); + let nodeModulesPath = path.join(runFixture, 'node_modules'); + symlinkOrCopySync(path.join(packageCache.get('node'), 'node_modules'), nodeModulesPath); - // Need to junction on windows since we likely don't have persmission to symlink - // 3rd arg is ignored on systems other than windows - fs.symlinkSync(path.join(pwd, '..'), appsECLIPath, 'junction'); - process.chdir(projectName); + process.chdir(runFixture); - }); + return runFixture; } /** - * Clean a test run and optionally assert. - * @return {Promise} + * Clean a test run. */ -function cleanupRun() { - return tmp.teardown('./tmp'); +function cleanupRun(projectName) { + process.chdir(root); + quickTemp.remove(dirs, `${projectName}-clone`); +} + +/** + * + * @param {string} projectName + * @param {string} options.command defaults to new + * @returns {Promise<{ result: string, path: string, cleanup: () => void }>} an object containing command result, path, and a cleanup function; + */ +async function createAndInstallTestTargets(projectName, options) { + let outputDir = await tmp.dir({ unsafeCleanup: true }); + + let command = options?.command ?? 'new'; + + let result = await applyCommand(command, projectName, '--skip-npm', `--directory=${outputDir.path}`); + + // we need to link these packages before we try to run pnpm install so it always uses the current version of ember-cli and the sub-packages + for (let pkg of [ + '.', + 'packages/blueprint-model', + 'packages/blueprint-blueprint', + 'packages/app-blueprint', + 'packages/addon-blueprint', + ]) { + await execa('pnpm', ['link', path.join(__dirname, `../../${pkg}`)], { + preferLocal: true, + cwd: outputDir.path, + }); + } + + await execa('pnpm', ['install', '--prefer-offline'], { + preferLocal: true, + cwd: outputDir.path, + }); + + return { + result, + cleanup: outputDir.cleanup, + path: outputDir.path, + }; } module.exports = { - createTestTargets: createTestTargets, - linkDependencies: linkDependencies, - teardownTestTargets: teardownTestTargets, - cleanupRun: cleanupRun + createAndInstallTestTargets, + createTestTargets, + linkDependencies, + teardownTestTargets, + cleanupRun, }; diff --git a/tests/helpers/assert-dir-empty.js b/tests/helpers/assert-dir-empty.js deleted file mode 100644 index aded142f5f..0000000000 --- a/tests/helpers/assert-dir-empty.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -var EOL = require('os').EOL; -var expect = require('chai').expect; -var walkSync = require('walk-sync'); -var existsSync = require('exists-sync'); - -/* - Asserts that the given directory is empty. - - @method assertDirEmpty. - @param dir The directory to check. -*/ -module.exports = function assertDirEmpty(dir) { - if (!existsSync(dir)) { - return; - } - - var paths = walkSync(dir) - .filter(function(path) { - return !path.match(/output\//); - }); - - expect(paths).to.deep.equal([], dir + '/ should be empty after `ember` tasks. Contained: ' + paths.join(EOL)); -}; diff --git a/tests/helpers/assert-file-equals.js b/tests/helpers/assert-file-equals.js deleted file mode 100644 index ddc516e4e4..0000000000 --- a/tests/helpers/assert-file-equals.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var fs = require('fs'); - -/* - Assert that a given file matches another. - - @method assertFileEqual - @param {String} pathToActual - @param {String} pathToExpected -*/ -module.exports = function assertFileEquals(pathToActual, pathToExpected) { - var actual = fs.readFileSync(pathToActual, { encoding: 'utf-8' }); - var expected = fs.readFileSync(pathToExpected, { encoding: 'utf-8' }); - - expect(actual).to.equal(expected); -}; diff --git a/tests/helpers/assert-file-to-not-exist.js b/tests/helpers/assert-file-to-not-exist.js deleted file mode 100644 index c67ac72bcd..0000000000 --- a/tests/helpers/assert-file-to-not-exist.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var fs = require('fs'); - -/* - Assert that a file does not exist, for ensuring certain files aren't generated - - @method assertFileToNotExist - @param {String} pathToCheck -*/ -module.exports = function assertFileToNotExist(pathToCheck) { - var exists; - try { - exists = fs.readFileSync(pathToCheck, { encoding: 'utf-8' }); - } catch (e) { - if (e.code === 'ENOENT') { - exists = null; - } else { - throw e; - } - } - expect(exists).to.not.exist; -}; diff --git a/tests/helpers/assert-file.js b/tests/helpers/assert-file.js deleted file mode 100644 index 2f0c2ecf25..0000000000 --- a/tests/helpers/assert-file.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var flatten = require('lodash/array/flatten'); -var contains = require('lodash/collection/contains'); -var fs = require('fs-extra'); -var path = require('path'); -var EOL = require('os').EOL; -var existsSync = require('exists-sync'); - -/* - Asserts that a given file exists. - - ```js - assertFile('some/file.js'); - ``` - - You can also make assertions about the file’s contents using - `contains` and `doesNotContain`: - - ```js - assertFile('some/file.js', { - contains: [ - 'foo', - /[0-9]+/ - ], - doesNotContain: 'bar' - }); - ``` - - @method assertFile - @param {String} file - @param {Object} options - Optional extra assertions to perform on the file. - @param {String, Array} options.contains - Strings or regular expressions the file must contain. - @param {String, Array} options.doesNotContain - Strings or regular expressions the file must *not* contain. -*/ -module.exports = function assertFile(file, options) { - var filePath = path.join(process.cwd(), file); - - expect(existsSync(filePath)).to.equal(true, 'expected ' + file + ' to exist'); - - if (!options) { - return; - } - - var actual = fs.readFileSync(filePath, { encoding: 'utf-8' }); - - if (options.contains) { - flatten([options.contains]).forEach(function(expected) { - var pass; - - if (expected.test) { - pass = expected.test(actual); - } else { - pass = contains(actual, expected); - } - - expect(pass).to.equal(true, EOL + EOL + 'expected ' + file + ':' + EOL + EOL + - actual + - EOL + 'to contain:' + EOL + EOL + - expected + EOL); - }); - } - - if (options.doesNotContain) { - flatten([options.doesNotContain]).forEach(function(unexpected) { - var pass; - - if (unexpected.test) { - pass = !unexpected.test(actual); - } else { - pass = !contains(actual, unexpected); - } - - expect(pass).to.equal(true, EOL + EOL + 'expected ' + file + ':' + EOL + EOL + - actual + EOL + - 'not to contain:' + EOL + EOL + - unexpected + EOL); - }); - } -}; diff --git a/tests/helpers/assert.js b/tests/helpers/assert.js deleted file mode 100644 index 1584c084e9..0000000000 --- a/tests/helpers/assert.js +++ /dev/null @@ -1,6 +0,0 @@ -var chai = require('chai'); -var chaiAsPromised = require('chai-as-promised'); - -chai.use(chaiAsPromised); - -module.exports = chai.assert; diff --git a/tests/helpers/build-error.js b/tests/helpers/build-error.js index ad96412c65..dbc88cc3ed 100644 --- a/tests/helpers/build-error.js +++ b/tests/helpers/build-error.js @@ -2,7 +2,7 @@ module.exports = BuildError; -function BuildError(input){ +function BuildError(input) { Error.call(this); this.message = input.message; this.file = input.file; diff --git a/tests/helpers/command-generator.js b/tests/helpers/command-generator.js new file mode 100644 index 0000000000..09da825e1d --- /dev/null +++ b/tests/helpers/command-generator.js @@ -0,0 +1,50 @@ +'use strict'; + +const { execaSync } = require('execa'); + +/** + * A simple tool to make behavior and API consistent between commands + * the user wraps in this. + * + * Usage: + * + * ``` + * let npm = new CommandGenerator('npm'); + * npm.invoke('install', 'ember-source', '--save-dev'); + * ``` + * + * @private + * @class CommandGenerator + * @constructor + * @param {Path} program The path to the command. + * @return {Function} A command helper. + */ +module.exports = class CommandGenerator { + constructor(program) { + this.program = program; + } + + /** + * The `invoke` method is responsible for building the final executable command. + * + * @private + * @method command + * @param {String} [...arguments] Arguments to be passed into the command. + * @param {Object} [options={}] The options passed into child_process.spawnSync. + * (https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options) + */ + invoke() { + let args = Array.prototype.slice.call(arguments); + let options = {}; + + if (typeof args[args.length - 1] === 'object') { + options = args.pop(); + } + + return this._invoke(args, options); + } + + _invoke(args, options) { + return execaSync(this.program, args, options); + } +}; diff --git a/tests/helpers/command-names.js b/tests/helpers/command-names.js deleted file mode 100644 index 4e1f80200c..0000000000 --- a/tests/helpers/command-names.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -module.exports = [ - 'addon', - 'build', - 'destroy', - 'generate', - 'help', - 'init', - 'install', - 'new', - 'serve', - 'test', - 'version' -]; diff --git a/tests/helpers/conf.js b/tests/helpers/conf.js deleted file mode 100644 index 20d03a24fe..0000000000 --- a/tests/helpers/conf.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -var Configstore = require('configstore'); - -var conf = new Configstore('insight-ember-cli'); -var optOut = conf.get('optOut'); - -module.exports.setup = function() { - conf.set('optOut', true); -}; - -module.exports.restore = function() { - conf.set('optOut', optOut); -}; diff --git a/tests/helpers/convert-help-output-to-json.js b/tests/helpers/convert-help-output-to-json.js index 56d69f9e68..585439fd08 100644 --- a/tests/helpers/convert-help-output-to-json.js +++ b/tests/helpers/convert-help-output-to-json.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = function(output) { +module.exports = function (output) { return JSON.parse(output.substr(output.indexOf('{'))); }; diff --git a/tests/helpers/copy-fixture-files.js b/tests/helpers/copy-fixture-files.js index 703a069835..afee037043 100644 --- a/tests/helpers/copy-fixture-files.js +++ b/tests/helpers/copy-fixture-files.js @@ -1,14 +1,13 @@ 'use strict'; -var path = require('path'); -var Promise = require('../../lib/ext/promise'); -var copy = Promise.denodeify(require('cpr')); +const fs = require('fs-extra'); +const path = require('path'); -var rootPath = process.cwd(); +let rootPath = process.cwd(); module.exports = function copyFixtureFiles(sourceDir) { - return copy(path.join(rootPath, 'tests', 'fixtures', sourceDir), '.', { - clobber: true, - stopOnErr: true + return fs.copy(path.join(rootPath, 'tests', 'fixtures', sourceDir), '.', { + dereference: true, + overwrite: true, }); }; diff --git a/tests/helpers/default-packager.js b/tests/helpers/default-packager.js new file mode 100644 index 0000000000..68f09096c7 --- /dev/null +++ b/tests/helpers/default-packager.js @@ -0,0 +1,315 @@ +'use strict'; + +/* + * A list of "helper" functions to automate tedious tasks of generating and + * validating folder structures. + */ + +const assert = require('assert'); + +const DEFAULT_ADDON_MODULES = { + 'ember-cli-foobar': '^1.0.0', +}; + +const DEFAULT_NODE_MODULES = { + 'broccoli-asset-rev': { + 'index.js': 'module.exports = function() { return { name: "broccoli-asset-rev" }; };', + 'package.json': JSON.stringify({ + name: 'broccoli-asset-rev', + keywords: ['ember-addon'], + }), + }, + 'ember-cli-htmlbars': { + 'index.js': `module.exports = { + name: 'ember-cli-htmlbars', + setupPreprocessorRegistry(type, registry) { + registry.add('template', { + name: 'ember-cli-htmlbars', + ext: 'hbs', + toTree(tree) { return tree; } + }); + } + }; + `, + 'package.json': JSON.stringify({ + name: 'ember-cli-htmlbars', + version: '2.0.1', + keywords: ['ember-addon'], + }), + }, + 'ember-source': { + 'index.js': `module.exports = function() { + return { + name: 'ember-source', + paths: { + debug: 'vendor/ember/ember.debug.js', + prod: 'vendor/ember/ember.prod.js', + testing: 'vendor/ember/ember-testing.js', + shims: undefined, + jquery: 'vendor/ember/jquery/jquery.js', + } + }; + }; + `, + 'package.json': JSON.stringify({ + name: 'ember-source', + version: '3.0.0-beta.1', + keywords: ['ember-addon'], + paths: { + debug: 'vendor/ember/ember.debug.js', + prod: 'vendor/ember/ember.prod.js', + testing: 'vendor/ember/ember-testing.js', + shims: undefined, + jquery: 'vendor/ember/jquery/jquery.js', + }, + }), + }, +}; + +const DEFAULT_VENDOR = { + loader: { + 'loader.js': 'W', + }, + ember: { + 'ember.js': 'window.Ember = {};', + 'ember.debug.js': 'window.Ember = {};', + }, + 'ember-cli': { + 'app-boot.js': '!!!', + 'app-config.js': 'SPARTA', + 'app-prefix.js': 'THIS', + 'app-suffix.js': 'IS', + 'test-support-prefix.js': 'If a clod be washed away by the sea,', + 'test-support-suffix.js': 'Europe is the less.', + 'tests-prefix.js': 'As well as if a promontory were.', + 'tests-suffix.js': 'As well as if a manor of thine own', + 'vendor-prefix.js': 'HELLO', + 'vendor-suffix.js': '!', + }, + 'ember-cli-shims': { + 'app-shims.js': 'L', + }, + 'ember-resolver': { + 'legacy-shims.js': 'D', + }, +}; + +const DEFAULT_SOURCE = { + 'index.html': '', + 'router.js': 'export default class {}', + 'resolver.js': `export default class {}`, + routes: { + 'index.js': 'export default class {}', + }, + styles: { + 'app.css': 'html,body{height:100%}', + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + config: { + 'environment.js': `module.exports = function() { return { modulePrefix: 'the-best-app-ever' }; };`, + }, + 'package.json': JSON.stringify({ + name: 'the-best-app-ever', + devDependencies: { + 'broccoli-asset-rev': '^2.4.5', + 'ember-ajax': '^3.0.0', + 'ember-cli': '~3.0.0-beta.1', + 'ember-cli-app-version': '^3.0.0', + 'ember-cli-babel': '^6.6.0', + 'ember-cli-dependency-checker': '^3.1.0', + 'ember-cli-eslint': '^4.2.1', + 'ember-cli-htmlbars': '^3.0.0', + 'ember-cli-htmlbars-inline-precompile': '^1.0.0', + 'ember-cli-inject-live-reload': '^1.4.1', + 'ember-cli-sass': '^7.1.3', + 'ember-cli-shims': '^1.2.0', + 'ember-cli-sri': '^2.1.0', + 'ember-cli-uglify': '^2.0.0', + 'ember-data': '~3.0.0-beta.1', + 'ember-load-initializers': '^1.0.0', + 'ember-qunit': '^3.4.1', + 'ember-resolver': '^4.0.0', + 'ember-source': '~3.0.0-beta.1', + 'loader.js': '^4.2.3', + }, + }), + tests: { + 'test-helper.js': '// test-helper.js', + integration: { + components: { + 'foo-bar-test.js': '// foo-bar-test.js', + }, + }, + }, +}; + +/* + * Generates an object that represents an unpackaged Ember application tree, + * including application source, addons, vendor and node.js files. + * + * @param {String} name The name of the app + * @param {Object} options Customize output object + * + * @return {Object} + */ +function getDefaultUnpackagedDist(name, options) { + options = options || {}; + + const addonModules = Object.assign({}, DEFAULT_ADDON_MODULES, options.addonModules); + const nodeModules = options.nodeModules || DEFAULT_NODE_MODULES; + const application = Object.assign({}, DEFAULT_SOURCE, options.source); + const vendor = Object.assign({}, DEFAULT_VENDOR, options.vendor); + + return { + 'addon-modules': addonModules, + node_modules: nodeModules, + [name]: application, + vendor, + }; +} + +/* + * Validates that passed-in object has the following shape: + * + * ```javascript + * { + * assets: { + * [name].js + * [name].map + * vendor.js + * vendor.map + * } + * } + * ``` + * + * This shape corresponds to the "dist" folder structure that Ember CLI creates + * after the build. + * + * If the shape does not correspond to the expected value, it throws an + * exception. + */ +function validateDefaultPackagedDist(name, obj) { + if (obj !== undefined || obj.assets !== undefined) { + let result = Object.keys(obj.assets).sort(); + + let valid = [`${name}.js`, `${name}.map`, 'vendor.js', 'vendor.map']; + + assert.deepStrictEqual(result, valid, `Expected [${valid}] but got [${result}]`); + } else { + throw new Error('Validation Error: Packaged files must be nested under `assets` folder'); + } +} + +/* + * Generates an object that represents a "dependency" on the disk. + * Could be used for generating node dependencies. + * + * ```javascript + * getDependencyFor('moment', { + * 'file1.js: 'content', + * 'file2.js': 'content' + * }); + * ``` + */ +function getDependencyFor(key, value) { + return { + [key]: value, + }; +} + +/* + * Generates the object that represents an application's registry where all + * file processors are stored. + * + * It takes two arguments: a type of files you want to register custom processor + * for and a function that takes a Broccoli tree and must return a Broccoli tree + * as well. + * + * @param {String} registryType i.e. 'template', 'js', 'css', 'all' + * @param {Function} fn Transormation that is applied to the input tree + */ +function setupRegistryFor(registryType, fn) { + return { + extensionsForType(type) { + if (type === registryType) { + return ['hbs']; + } + + return []; + }, + + load(type) { + if (type === registryType) { + return [ + { + toTree() { + return fn.apply(this, arguments); + }, + }, + ]; + } + + return []; + }, + }; +} + +/* + * Generates the object that represents an application's registry where all + * file processors are stored. + * + * It takes one argument: an object with the mapping from file type to a + * "process" function. For example: + * + * ``` + * { + * js: tree => tree + * } + * ``` + * + * @param {Object} registryMap + * @return {Object} + */ +function setupRegistry(registryMap) { + return { + load(type) { + if (registryMap[type]) { + return [ + { + toTree(tree) { + return registryMap[type](tree); + }, + }, + ]; + } + + return []; + }, + }; +} + +function setupProject(rootPath) { + const path = require('path'); + const Project = require('../../lib/models/project'); + const MockCLI = require('./mock-cli'); + + const packageContents = require(path.join(rootPath, 'package.json')); + let cli = new MockCLI(); + + return new Project(rootPath, packageContents, cli.ui, cli); +} + +module.exports = { + setupProject, + setupRegistry, + setupRegistryFor, + validateDefaultPackagedDist, + getDefaultUnpackagedDist, + getDependencyFor, + DEFAULT_SOURCE, + DEFAULT_VENDOR, + DEFAULT_NODE_MODULES, + DEFAULT_ADDON_MODULES, +}; diff --git a/tests/helpers/disable-npm-on-blueprint.js b/tests/helpers/disable-npm-on-blueprint.js deleted file mode 100644 index 0c1156f37e..0000000000 --- a/tests/helpers/disable-npm-on-blueprint.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; -var Blueprint = require('../../lib/models/blueprint'); -var originTaskFor = Blueprint.prototype.taskFor; -var assert = require('../helpers/assert'); -var Promise = require('../../lib/ext/promise'); - -module.exports = { - disableNPM: function() { - Blueprint.prototype.taskFor = function(taskName) { - // we don't actually need to run the npm-install task, so lets mock it to - // speedup tests that need it - assert.equal(taskName, 'npm-install'); - - return { - run: function() { - return Promise.resolve(); - } - }; - }; - }, - - restoreNPM: function() { - Blueprint.prototype.taskFor = originTaskFor; - } -}; diff --git a/tests/helpers/dist-checker.js b/tests/helpers/dist-checker.js new file mode 100644 index 0000000000..f93bd6304c --- /dev/null +++ b/tests/helpers/dist-checker.js @@ -0,0 +1,106 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +// eslint-disable-next-line n/no-unpublished-require +const jsdom = require('jsdom'); +const { AssertionError } = require('assert'); + +class CustomResourceLoader extends jsdom.ResourceLoader { + constructor(distPath) { + super(); + this.distPath = distPath; + } + + fetch(url) { + let content = fs.readFileSync(path.join(this.distPath, url.replace('file://./', '')), 'utf8'); + return Promise.resolve(Buffer.from(content)); + } +} + +class DistChecker { + constructor(distPath) { + this.distPath = distPath; + } + + _evalHtml(options = {}) { + let html = fs.readFileSync(path.join(this.distPath, 'index.html'), 'utf8'); + this.dom = new jsdom.JSDOM(html, options); + this.scripts = this.dom.window.document.querySelectorAll('script'); + this.links = this.dom.window.document.querySelectorAll('link'); + } + + evalScripts(timeout = 15000) { + this._evalHtml({ + url: `file://.`, + runScripts: 'dangerously', + resources: new CustomResourceLoader(this.distPath), + }); + + // ember-data expects window.crypto to exists however JSDom does not + // impliment this: https://github.com/jsdom/jsdom/issues/1612 + this.dom.window.crypto = () => {}; + return new Promise((resolve, reject) => { + // reject if the scripts take longer than 15 seconds to load. + let timeoutId = setTimeout(() => { + reject( + new AssertionError({ + operator: 'evalScripts[Timeout]', + message: `[dist-checker:${this.distPath}] timeout exceeded: ${timeout}`, + stackStartFn: this.evalScripts, + }) + ); + }, timeout); + + this.dom.window.addEventListener('error', (e) => { + reject( + new AssertionError({ + operator: 'evalScripts', + // this `e` has no stack, so we must make due + message: `error thrown during evalScript of '${this.distPath}' \n error details: \n message: '${e.message}'\n file: '${e.filename}:${e.colno}'`, + stackStartFn: this.evalScripts, + }) + ); + }); + + this.dom.window.addEventListener('load', () => { + clearTimeout(timeoutId); + return resolve(); + }); + }); + } + + get window() { + return this.dom.window; + } + + contains(fileType, token) { + if (!this.dom) { + this._evalHtml({}); + } + + if (fileType === 'js') { + for (let element of this.scripts) { + let src = element.getAttribute('src'); + let content = fs.readFileSync(path.join(this.distPath, src), 'utf8'); + + if (content.includes(token)) { + return true; + } + } + } else if (fileType === 'css') { + for (let element of this.links) { + let src = element.getAttribute('href'); + let content = fs.readFileSync(path.join(this.distPath, src), 'utf8'); + + if (content.includes(token)) { + return true; + } + } + } + + return false; + } +} + +module.exports = DistChecker; diff --git a/tests/helpers/ember.js b/tests/helpers/ember.js index 61ba7054d8..a678d32433 100644 --- a/tests/helpers/ember.js +++ b/tests/helpers/ember.js @@ -1,22 +1,108 @@ 'use strict'; -var MockUI = require('./mock-ui'); -var MockAnalytics = require('./mock-analytics'); -var Cli = require('../../lib/cli'); +const MockUI = require('console-ui/mock'); +const cli = require('../../lib/cli'); +const MockProcess = require('./mock-process'); +const path = require('path'); +const willInterruptProcess = require('../../lib/utilities/will-interrupt-process'); -module.exports = function ember(args) { - var cli; +/* + Accepts a single array argument, that contains the + `process.argv` style arguments. + + Example: + + ``` + ember test --no-build --test-port=4567 + ``` + + In this example, `process.argv` would be something similar to: + + ``` + ['path/to/node', 'path/to/bin/ember', 'test', '--no-build', '--test-port=4567'] + ``` + + + And this could be emulated by calling: + + ``` + ember(['test', '--no-build', '--test-port=4567']); + ``` + + --- + + The return value of this helper is a promise that resolves + with an object that contains the following properties: + + * `exitCode` is the normal exit code in standard command invocation + * `ui` is the `ui` object that was used for the invocation (which is + a `MockUI` instance), this can be used to inspect the commands output. + +*/ +module.exports = function ember(args, options) { + let cliInstance; + let ui = (options && options.UI) || MockUI; + let packagePath = (options && options.package) || path.resolve(__dirname, '..', '..'); + let disableDependencyChecker = (options && options.disableDependencyChecker) || true; + let inputStream = []; + let outputStream = []; + let errorLog = []; + let commandName = args[0]; + + if (commandName === 'test') { + /* + we are forced to invalidate testem config module cache + to ensure that each test always reads from the file system, + not the memory cache. + */ + delete require.cache[path.join(process.cwd(), 'testem.js')]; + } - args.push('--disable-analytics'); args.push('--watcher=node'); - cli = new Cli({ - inputStream: [], - outputStream: [], - cliArgs: args, - Leek: MockAnalytics, - UI: MockUI, - testing: true + + if (!options || options.skipGit !== false) { + args.push('--skip-git'); + } + + // Most tests don't npm install so let's disable lint fixing by default + if (!args.some((arg) => arg.startsWith('--lint-fix'))) { + args.push('--no-lint-fix'); + } + + willInterruptProcess.release(); + + cliInstance = cli({ + process: new MockProcess(), + inputStream, + outputStream, + errorLog, + cliArgs: args, + UI: ui, + testing: true, + disableDependencyChecker, + cli: { + // This prevents ember-cli from detecting any other package.json files + // forcing ember-cli to act as the globally installed package + npmPackage: 'ember-cli', + root: packagePath, + }, }); - return cli; + function returnTestState(statusCode) { + let result = { + exitCode: statusCode, + statusCode, + inputStream, + outputStream, + errorLog, + }; + + if (statusCode) { + throw result; + } else { + return result; + } + } + + return cliInstance.then(returnTestState); }; diff --git a/tests/helpers/file-utils.js b/tests/helpers/file-utils.js index c42f1b4c4f..80dc33a5d1 100644 --- a/tests/helpers/file-utils.js +++ b/tests/helpers/file-utils.js @@ -1,22 +1,21 @@ 'use strict'; -var fs = require('fs-extra'); -var existsSync = require('exists-sync'); +const fs = require('fs-extra'); function touch(path, obj) { - if (!existsSync(path)) { + if (!fs.existsSync(path)) { fs.createFileSync(path); fs.writeJsonSync(path, obj || {}); } } function replaceFile(path, findString, replaceString) { - if (existsSync(path)) { - var newFile; - var file = fs.readFileSync(path, 'utf-8'); - var find = new RegExp(findString); - var match = new RegExp(replaceString); - if (!file.match(match)) { + if (fs.existsSync(path)) { + let newFile; + let file = fs.readFileSync(path, 'utf-8'); + let find = new RegExp(findString); + let match = new RegExp(replaceString); + if (!match.test(file)) { newFile = file.replace(find, replaceString); fs.writeFileSync(path, newFile, 'utf-8'); } @@ -24,6 +23,6 @@ function replaceFile(path, findString, replaceString) { } module.exports = { - touch: touch, - replaceFile: replaceFile + touch, + replaceFile, }; diff --git a/tests/helpers/fixturify-project.js b/tests/helpers/fixturify-project.js new file mode 100644 index 0000000000..1717c323f9 --- /dev/null +++ b/tests/helpers/fixturify-project.js @@ -0,0 +1,396 @@ +'use strict'; + +const path = require('path'); +const merge = require('lodash/merge'); +// this is a test-only dependency +// eslint-disable-next-line n/no-unpublished-require +const FixturifyProject = require('fixturify-project'); +const Project = require('../../lib/models/project'); +const MockCLI = require('./mock-cli'); + +// used in these tests to ensure we are only +// operating on the addons added here +class ProjectWithoutInternalAddons extends Project { + supportedInternalAddonPaths() { + return []; + } +} + +function prepareAddon(addon, options) { + addon.pkg.keywords.push('ember-addon'); + addon.pkg['ember-addon'] = {}; + addon.files['index.js'] = + options.addonEntryPoint || + `module.exports = { + name: require("./package").name, + allowCachingPerBundle: ${Boolean(options.allowCachingPerBundle)}, + ${options.additionalContent || ''} + };`; +} + +/** + * Gets a normalized object with provided defaults. If the 2nd argument is a function, + * we add this to the returned object with `callback` as its key. + * + * @name getOptionsObjectWithCallbackFunction + * @param {Object} defaultOptions The default options + * @param {Object|Function} optionsOrCallback The options object or callback function + * @returns {Object} The normalized options object + */ +function getOptionsObjectWithCallbackFunction(defaultOptions, optionsOrCallback) { + return Object.assign( + {}, + defaultOptions, + typeof optionsOrCallback === 'function' ? { callback: optionsOrCallback } : optionsOrCallback + ); +} + +// Essentially a copy of the function in node-fixturify-project, converted from TS to JS. +// We need this for use during toJSON(). +function parseScoped(name) { + let matched = name.match(/(@[^@/]+)\/(.*)/); + if (matched) { + return { + scope: matched[1], + name: matched[2], + }; + } + return null; +} + +module.exports = class EmberCLIFixturifyProject extends FixturifyProject { + writeSync() { + super.writeSync(...arguments); + this._hasWritten = true; + } + + addFiles(filesObj) { + merge(this.files, filesObj); + } + + buildProjectModel(ProjectClass = ProjectWithoutInternalAddons) { + if (!this._hasWritten) { + this.writeSync(); + } + + let pkg = JSON.parse(this.toJSON('package.json')); + let cli = new MockCLI(); + let root = path.join(this.root, this.name); + + return new ProjectClass(root, pkg, cli.ui, cli); + } + + buildProjectModelForInRepoAddon(addonName, ProjectClass = ProjectWithoutInternalAddons) { + if (!this._hasWritten) { + this.writeSync(); + } + + let pkg = JSON.parse(this.files.lib[addonName]['package.json']); + let cli = new MockCLI(); + let root = path.join(this.root, this.name, 'lib', addonName); + + return new ProjectClass(root, pkg, cli.ui, cli); + } + + /** + * Add an entry for this object's `dependencies` list. When this object is written out, the + * dependency will also then write out appropriate files in this object's `node_modules' subdirectory. + * + * @param {String} name name of the dependency to add + * @param {String} version version of the dependency to add + * @param {Object|Function} optionsOrCallback options to configure the new FixturifyProject, or a callback function to call after creating + * the dependency's FixturifyProject. If the parameter is a function, it will be assumed to be a callback function. If instead + * the parameter is an object, a callback function can be provided using the property 'callback' in the object. + * @returns the new FixturifyProject + */ + addDependency(name, version, optionsOrCallback) { + const options = getOptionsObjectWithCallbackFunction(optionsOrCallback); + return super.addDependency(name, version, options.callback); + } + + /** + * Add a 'reference' entry to this object's `dependencies` list. A 'reference' dependency is + * an entry in `dependencies` where the caller knows the dependency's source files are being + * created elsewhere in the project tree, so no source files should be created locally in + * `node_modules`, which is the standard FixturifyProject (and node-fixturify-project) behavior. + * We do this by adding the necessary reference to `dependencies` during `toJSON`. + * + * This is used when two addons wish to share a single definition on disk for a dependency (various parts of + * ember-cli optimize processing based on paths on disk.) + * + * Because there is no FixturifyProject being created, no callback is given as in other methods. + * + * @param {String} name name of the dependency + * @param {String} version version of the dependency, defaults to '*'. For our purposes, '*' means + * "whatever version was specified elsewhere." + */ + addReferenceDependency(name, version = '*') { + if (!this._referenceDependencies) { + this._referenceDependencies = {}; + } + + this._referenceDependencies[name] = version; + } + + /** + * Add an entry to this object's `devDependencies` list. When this object is written out, the + * dependency will also then write out appropriate files in this object's `node_modules' subdirectory. + * + * @param {String} name name of the dev dependency to add + * @param {String} version version of the dev dependency to add + * @param {Object|Function} optionsOrCallback options to configure the new FixturifyProject, or a callback function to call after creating + * the dependency's FixturifyProject. If the parameter is a function, it will be assumed to be a callback function. If instead + * the parameter is an object, a callback function can be provided using the property 'callback' in the object. + * @returns the new FixturifyProject + */ + addDevDependency(name, version, optionsOrCallback) { + const options = getOptionsObjectWithCallbackFunction(optionsOrCallback); + return super.addDevDependency(name, version, options.callback); + } + + /** + * Add a 'reference' entry to this object's `devDependencies` list. A 'reference' devDependency is + * an entry in `devDependencies` where the caller knows the dependency's source files are being + * created elsewhere in the project tree, so no source files should be created locally in + * `node_modules`, which is the standard FixturifyProject (and node-fixturify-project) behavior. + * We do this by adding the necessary reference to `devDependencies` during `toJSON`. + * + * This is used when two addons wish to share a single definition on disk for a devDependency + * (various parts of ember-cli optimize processing based on paths on disk.) + * + * Because there is no FixturifyProject being created, no callback is given as in other methods. + * + * @param {String} name name of the devDependency + * @param {String} version version of the devDependency, defaults to '*'. For our purposes, '*' means + * "whatever version was specified elsewhere." + */ + addReferenceDevDependency(name, version = '*') { + if (!this._referenceDevDependencies) { + this._referenceDevDependencies = {}; + } + + this._referenceDevDependencies[name] = version; + } + + /** + * Add an addon to this object's `dependencies` list. The addon files will be written in + * this object's `node_modules/` directory when this object is written out. + * + * @param {String} name name of the addon + * @param {String} version version of the addon, defaults to '0.0.0' + * @param {Object|Function} optionsOrCallback an object consisting of properties and values to apply when creating + * the addon, or a callback function to pass the newly-created FixturifyProject to. Important options + * include 'allowCachingPerBundle' (true if the addon can be proxied, defaults to false) and 'callback' (if you want to include + * a callback function while also specifying other properties.) + * @returns {FixturifyProject} the newly-created addon + */ + addAddon(name, version = '0.0.0', optionsOrCallback) { + const options = getOptionsObjectWithCallbackFunction({ allowCachingPerBundle: false }, optionsOrCallback); + + return this.addDependency(name, version, { + ...options, + callback: (addon) => { + prepareAddon(addon, options); + + // call original `options.callback` if it exists + if (typeof options.callback === 'function') { + options.callback(addon); + } + }, + }); + } + + /** + * Add an addon to this object's `devDependencies` list. The addon files will be written in + * this object's `node_modules/` directory when this object is written out. + * + * @param {String} name name of the addon + * @param {String} version version of the addon, defaults to '0.0.0' + * @param {Object|Function} optionsOrCallback an object consisting of properties and values to apply when creating + * the addon, or a callback function to pass the newly-created FixturifyProject to. Important options + * include 'allowCachingPerBundle' (true if the addon can be proxied, defaults to false) and 'callback' (if you want to include + * a callback function while also specifying other properties.) + * @returns {FixturifyProject} the newly-created addon + */ + addDevAddon(name, version = '0.0.0', optionsOrCallback) { + const options = getOptionsObjectWithCallbackFunction({ allowCachingPerBundle: false }, optionsOrCallback); + + return this.addDevDependency(name, version, { + ...options, + callback: (addon) => { + prepareAddon(addon, options); + + // call original `options.callback` if it exists + if (typeof options.callback === 'function') { + options.callback(addon); + } + }, + }); + } + + /** + * Add an addon to this object's `dependencies` list. The engine's addon files will be written in + * this object's `node_modules/` directory when this object is written out. + * + * @param {String} name name of the engine + * @param {String} version version of the engine, defaults to '0.0.0' + * @param {Object|Function} optionsOrCallback an object consisting of properties and values to apply when creating + * the engine, or a callback function to pass the newly-created FixturifyProject to. Important options + * include 'allowCachingPerBundle' (true if the engine can be proxied, defaults to false), 'enableLazyLoading' (true + * if the engine is to be lazily loaded, defaults to false) and 'callback' (if you want to include + * a callback function while also specifying other properties.) + * @returns {FixturifyProject} the newly-created engine addon + */ + addEngine(name, version = '0.0.0', options = { allowCachingPerBundle: false, enableLazyLoading: false }) { + const callback = (engine) => { + engine.pkg.keywords.push('ember-engine'); + + // call original callback if it exists + if (typeof options.callback === 'function') { + options.callback(engine); + } + }; + + if (options.enableLazyLoading) { + return this.addAddon(name, version, { + ...options, + additionalContent: 'lazyLoading: { enabled: true },', + callback, + }); + } + + return this.addAddon(name, version, { ...options, callback }); + } + + /** + * Add an in-repo addon to this object. The addon files will be written in + * this object's `lib/` directory when this object is written out. + * + * @param {String} name name of the addon + * @param {String} version version of the addon, defaults to '0.0.0' + * @param {Object|Function} optionsOrCallback an object consisting of properties and values to apply when creating + * the addon, or a callback function to pass the newly-created FixturifyProject to. Important options + * include 'allowCachingPerBundle' (true if the addon can be proxied, defaults to false) and 'callback' (if you want to include + * a callback function while also specifying other properties.) + * @returns {FixturifyProject} the newly-created addon + */ + addInRepoAddon(name, version = '0.0.0', optionsOrCallback) { + const options = getOptionsObjectWithCallbackFunction({ allowCachingPerBundle: false }, optionsOrCallback); + + const inRepoAddon = new EmberCLIFixturifyProject(name, version, (addon) => { + prepareAddon(addon, options); + + if (typeof options.callback === 'function') { + options.callback(addon); + } + }); + + // configure the current project to have an ember-addon configured at the + // appropriate path, i.e. under a common root directory (lib). + const addonRootDir = 'lib'; + + // Add to ember-addon.paths list + let addon = (this.pkg['ember-addon'] = this.pkg['ember-addon'] || {}); + addon.paths = addon.paths || []; + + const addonPath = `${addonRootDir}/${name}`; + + if (addon.paths.find((path) => path.toLowerCase() === addonPath.toLowerCase())) { + throw new Error(`project: ${this.name} already contains the in-repo-addon: ${name}`); + } + + addon.paths.push(addonPath); + + this.files[addonRootDir] = this.files[addonRootDir] || {}; + + let addonJSON = inRepoAddon.toJSON(); + Object.assign(this.files[addonRootDir], addonJSON); + return inRepoAddon; + } + + /** + * Add an in-repo engine to this object. The engine files will be written in + * this object's `lib/` directory when this object is written out. + * + * @param {String} name name of the engine + * @param {String} version version of the engine, defaults to '0.0.0' + * @param {Object|Function} optionsOrCallback an object consisting of properties and values to apply when creating + * the engine, or a callback function to pass the newly-created FixturifyProject to. Important options + * include 'allowCachingPerBundle' (true if the addon can be proxied, defaults to false), 'enableLazyLoading' (true + * if the engine is to be lazily loaded, defaults to false) and 'callback' (if you want to include + * a callback function while also specifying other properties.) + * @returns {FixturifyProject} the newly-created addon + */ + addInRepoEngine(name, version = '0.0.0', options = { allowCachingPerBundle: false, enableLazyLoading: false }) { + const callback = (engine) => { + engine.pkg.keywords.push('ember-engine'); + + // call original callback if it exists + if (typeof options.callback === 'function') { + options.callback(engine); + } + }; + + if (options.enableLazyLoading) { + return this.addInRepoAddon(name, version, { + ...options, + additionalContent: 'lazyLoading: { enabled: true },', + callback, + }); + } + + return this.addInRepoAddon(name, version, { ...options, callback }); + } + + /** + * Convert the object into a data structure suitable for passing to `fixturify`. + * + * @param {String} key optional key. If specified, the object will be run through toJSON, then the given + * property extracted and returned. + * @returns {Object} the `toJSON` value of the object (wrapped) or the toJSON value of the specified field + * (not wrapped.) + */ + toJSON(key) { + if (key) { + return super.toJSON(key); + } + + let jsonData = super.toJSON(); + + let scoped = parseScoped(this.name); + + // Allowing for scoped names, get the object in the JSON structure that corresponds + // to this FixturifyProject. + let container = scoped ? jsonData[scoped.scope][scoped.name] : jsonData[this.name]; + + if (this._referenceDependencies || this._referenceDevDependencies) { + let pkg = JSON.parse(container['package.json']); + + if (this._referenceDependencies) { + if (!pkg.dependencies) { + pkg.dependencies = {}; + } + + Object.assign(pkg.dependencies, this._referenceDependencies); + } + + if (this._referenceDevDependencies) { + if (!pkg.devDependencies) { + pkg.devDependencies = {}; + } + + Object.assign(pkg.devDependencies, this._referenceDevDependencies); + } + + container['package.json'] = JSON.stringify(pkg, undefined, 2); + } + + // an optimization to remove any node_modules declaration that has nothing in it, + // to avoid creating extra directories for no reason. + if (container['node_modules'] && Object.keys(container['node_modules']).length === 0) { + delete container['node_modules']; + } + + return jsonData; + } +}; diff --git a/tests/helpers/generate-utils.js b/tests/helpers/generate-utils.js new file mode 100644 index 0000000000..fd538ab172 --- /dev/null +++ b/tests/helpers/generate-utils.js @@ -0,0 +1,17 @@ +'use strict'; + +const fs = require('fs-extra'); +const ember = require('./ember'); + +function inRepoAddon(path) { + return ember(['generate', 'in-repo-addon', path]); +} + +function tempBlueprint() { + return fs.outputFile('blueprints/foo/files/__root__/foos/__name__.js', '/* whoah, empty foo! */'); +} + +module.exports = { + inRepoAddon, + tempBlueprint, +}; diff --git a/tests/helpers/has-global-yarn.js b/tests/helpers/has-global-yarn.js new file mode 100644 index 0000000000..cb41a47777 --- /dev/null +++ b/tests/helpers/has-global-yarn.js @@ -0,0 +1,6 @@ +'use strict'; + +// eslint-disable-next-line n/no-unpublished-require +const which = require('which'); + +module.exports = which.sync('yarn', { nothrow: true }); diff --git a/tests/helpers/init-app.js b/tests/helpers/init-app.js new file mode 100644 index 0000000000..18e1640eea --- /dev/null +++ b/tests/helpers/init-app.js @@ -0,0 +1,9 @@ +'use strict'; + +const ember = require('./ember'); + +function initApp() { + return ember(['init', '--name=my-app', '--skip-npm']); +} + +module.exports = initApp; diff --git a/tests/helpers/kill-cli-process.js b/tests/helpers/kill-cli-process.js index a1fbea55f4..aef4d0318c 100644 --- a/tests/helpers/kill-cli-process.js +++ b/tests/helpers/kill-cli-process.js @@ -1,9 +1,15 @@ 'use strict'; -module.exports = function(childProcess) { +module.exports = function (childProcess) { + // Calling `.kill` or `.send` when the process has already exited causes + // errors on Windows, when an `.exitCode` is non-null the process has exited + if (childProcess.exitCode !== null) { + return; + } + if (process.platform === 'win32') { - childProcess.send({kill: true}); + childProcess.send({ kill: true }); } else { childProcess.kill('SIGINT'); } -}; \ No newline at end of file +}; diff --git a/tests/helpers/log-on-failure.js b/tests/helpers/log-on-failure.js new file mode 100644 index 0000000000..7bfca90439 --- /dev/null +++ b/tests/helpers/log-on-failure.js @@ -0,0 +1,30 @@ +/* eslint mocha/no-top-level-hooks: 0 */ + +'use strict'; + +let logSink; + +beforeEach(function () { + logSink = []; +}); + +afterEach(function () { + if (this.currentTest && this.currentTest.state !== 'passed') { + // It would be preferable to attach the log output to the error object + // (this.currentTest.err) and have Mocha report it somehow, so that the + // error message and log output show up in the same place. This doesn't + // seem to be possible though. + console.log(logSink.join('\n')); + } + logSink = null; +}); + +function logOnFailure(s) { + if (logSink === null) { + throw new Error('logOnFailure called outside of test'); + } else { + logSink.push(s); + } +} + +module.exports = logOnFailure; diff --git a/tests/helpers/mock-analytics.js b/tests/helpers/mock-analytics.js deleted file mode 100644 index 114540256f..0000000000 --- a/tests/helpers/mock-analytics.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -module.exports = MockAnalytics; -function MockAnalytics() { - this.tracks = []; - this.trackTimings = []; - this.trackErrors = []; -} - -MockAnalytics.prototype = Object.create({}); -MockAnalytics.prototype.track = function(arg) { - this.tracks.push(arg); -}; - -MockAnalytics.prototype.trackTiming = function(arg) { - this.trackTimings.push(arg); -}; - -MockAnalytics.prototype.trackError = function(arg) { - this.trackErrors.push(arg); -}; - -MockAnalytics.prototype.constructor = MockAnalytics; diff --git a/tests/helpers/mock-broccoli-watcher.js b/tests/helpers/mock-broccoli-watcher.js new file mode 100644 index 0000000000..0c9556160d --- /dev/null +++ b/tests/helpers/mock-broccoli-watcher.js @@ -0,0 +1,17 @@ +'use strict'; + +const EventEmitter = require('events').EventEmitter; +const path = require('path'); + +class MockBroccoliWatcher extends EventEmitter { + start() {} + + then() { + let promise = Promise.resolve({ + directory: path.resolve(__dirname, '../fixtures/express-server'), + }); + return promise.then.apply(promise, arguments); + } +} + +module.exports = MockBroccoliWatcher; diff --git a/tests/helpers/mock-cli.js b/tests/helpers/mock-cli.js new file mode 100644 index 0000000000..7dda99f034 --- /dev/null +++ b/tests/helpers/mock-cli.js @@ -0,0 +1,20 @@ +'use strict'; + +const path = require('path'); +const MockUI = require('console-ui/mock'); +const Instrumentation = require('../../lib/models/instrumentation'); +const PackageInfoCache = require('../../lib/models/package-info-cache'); + +class MockCLI { + constructor(options) { + options = options || {}; + + this.ui = options.ui || new MockUI(); + this.root = path.join(__dirname, '..', '..'); + this.npmPackage = options.npmPackage || 'ember-cli'; + this.instrumentation = options.instrumentation || new Instrumentation({}); + this.packageInfoCache = new PackageInfoCache(this.ui); + } +} + +module.exports = MockCLI; diff --git a/tests/helpers/mock-express-server.js b/tests/helpers/mock-express-server.js index 53543d0cc4..2999bf9fd9 100644 --- a/tests/helpers/mock-express-server.js +++ b/tests/helpers/mock-express-server.js @@ -1,23 +1,15 @@ 'use strict'; -var RSVP = require('rsvp'); -var EventEmitter = require('events').EventEmitter; -var path = require('path'); - -function MockExpressServer() { - EventEmitter.apply(this, arguments); - this.tracks = []; - this.trackTimings = []; - this.trackErrors = []; +const EventEmitter = require('events').EventEmitter; +const path = require('path'); + +class MockExpressServer extends EventEmitter { + then() { + let promise = Promise.resolve({ + directory: path.resolve(__dirname, '../fixtures/express-server'), + }); + return promise.then.apply(promise, arguments); + } } module.exports = MockExpressServer; - -MockExpressServer.prototype = Object.create(EventEmitter.prototype); - -MockExpressServer.prototype.then = function() { - var promise = RSVP.resolve({ - directory: path.resolve(__dirname, '../fixtures/express-server') - }); - return promise.then.apply(promise, arguments); -}; diff --git a/tests/helpers/mock-process.js b/tests/helpers/mock-process.js new file mode 100644 index 0000000000..15f08718d3 --- /dev/null +++ b/tests/helpers/mock-process.js @@ -0,0 +1,39 @@ +'use strict'; + +let EventEmitter = require('events'); + +module.exports = class MockProcess extends EventEmitter { + constructor(options) { + super(); + + options = options || {}; + + this.platform = 'MockOS'; + + const stdin = Object.assign( + new EventEmitter(), + { + isRaw: process.stdin.isRaw, + setRawMode: (flag) => { + stdin.isRaw = flag; + }, + }, + options.stdin || {} + ); + + Object.assign(this, options, { stdin }); + } + + getSignalListenerCounts() { + return { + SIGINT: this.listenerCount('SIGINT'), + SIGTERM: this.listenerCount('SIGTERM'), + message: this.listenerCount('message'), + }; + } + + exit() { + // we are unable to reliable unit test `process.exit()` + throw new Error('MockProcess.exit() was called'); + } +}; diff --git a/tests/helpers/mock-project.js b/tests/helpers/mock-project.js index 332c64dd20..09d2c172f5 100644 --- a/tests/helpers/mock-project.js +++ b/tests/helpers/mock-project.js @@ -1,55 +1,68 @@ 'use strict'; -var Project = require('../../lib/models/project'); +const Project = require('../../lib/models/project'); +const Instrumentation = require('../../lib/models/instrumentation'); +const MockUI = require('console-ui/mock'); -function MockProject() { - var root = process.cwd(); - var pkg = {}; - Project.apply(this, [root, pkg]); -} +class MockProject extends Project { + constructor(options = {}) { + let root = options.root || process.cwd(); + let pkg = options.pkg || {}; + let ui = options.ui || new MockUI(); + let instr = new Instrumentation({ + ui, + initInstrumentation: { + token: null, + node: null, + }, + }); + let cli = options.cli || { + instrumentation: instr, + }; -MockProject.prototype.require = function(file) { - if (file === './server') { - return function() { - return { - listen: function() { arguments[arguments.length-1](); } + super(root, pkg, ui, cli); + } + + require(file) { + if (file === './server') { + return function () { + return { + listen() { + arguments[arguments.length - 1](); + }, + }; }; - }; + } + } + + config() { + return ( + this._config || { + rootURL: '/', + locationType: 'history', + } + ); + } + + has(key) { + return /server/.test(key); } -}; - -MockProject.prototype.config = function() { - return this._config || { - baseURL: '/', - locationType: 'auto' - }; -}; - -MockProject.prototype.has = function(key) { - return (/server/.test(key)); -}; - -MockProject.prototype.name = function() { - return 'mock-project'; -}; - -MockProject.prototype.initializeAddons = Project.prototype.initializeAddons; -MockProject.prototype.hasDependencies = function() { - return true; -}; -MockProject.prototype.discoverAddons = Project.prototype.discoverAddons; -MockProject.prototype.addIfAddon = Project.prototype.addIfAddon; -MockProject.prototype.supportedInternalAddonPaths = Project.prototype.supportedInternalAddonPaths; -MockProject.prototype.setupBowerDirectory = Project.prototype.setupBowerDirectory; -MockProject.prototype.setupNodeModulesPath = Project.prototype.setupNodeModulesPath; -MockProject.prototype.isEmberCLIProject = Project.prototype.isEmberCLIProject; -MockProject.prototype.isEmberCLIAddon = Project.prototype.isEmberCLIAddon; -MockProject.prototype.findAddonByName = Project.prototype.findAddonByName; -MockProject.prototype.dependencies = function() { - return []; -}; -MockProject.prototype.isEmberCLIAddon = function() { - return false; -}; + + name() { + return 'mock-project'; + } + + hasDependencies() { + return true; + } + + dependencies() { + return []; + } + + isEmberCLIAddon() { + return false; + } +} module.exports = MockProject; diff --git a/tests/helpers/mock-server-watcher.js b/tests/helpers/mock-server-watcher.js index dc7671c3d5..57519c379f 100644 --- a/tests/helpers/mock-server-watcher.js +++ b/tests/helpers/mock-server-watcher.js @@ -1,23 +1,15 @@ 'use strict'; -var RSVP = require('rsvp'); -var EventEmitter = require('events').EventEmitter; -var path = require('path'); - -function MockServerWatcher() { - EventEmitter.apply(this, arguments); - this.tracks = []; - this.trackTimings = []; - this.trackErrors = []; +const EventEmitter = require('events').EventEmitter; +const path = require('path'); + +class MockServerWatcher extends EventEmitter { + then() { + let promise = Promise.resolve({ + directory: path.resolve(__dirname, '../fixtures/express-server'), + }); + return promise.then.apply(promise, arguments); + } } module.exports = MockServerWatcher; - -MockServerWatcher.prototype = Object.create(EventEmitter.prototype); - -MockServerWatcher.prototype.then = function() { - var promise = RSVP.resolve({ - directory: path.resolve(__dirname, '../fixtures/express-server') - }); - return promise.then.apply(promise, arguments); -}; diff --git a/tests/helpers/mock-ui.js b/tests/helpers/mock-ui.js deleted file mode 100644 index 3425db2990..0000000000 --- a/tests/helpers/mock-ui.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -var UI = require('../../lib/ui'); -var through = require('through'); -var Promise = require('../../lib/ext/promise'); - -module.exports = MockUI; -function MockUI() { - this.output = ''; - - UI.call(this, { - inputStream: through(), - outputStream: through(function(data) { - this.output += data; - }.bind(this)) - }); -} - -MockUI.prototype = Object.create(UI.prototype); -MockUI.prototype.constructor = MockUI; -MockUI.prototype.clear = function(){ - this.output = ''; -}; - -MockUI.prototype.waitForPrompt = function() { - if (!this._waitingForPrompt) { - var promise, resolver; - promise = new Promise(function(resolve){ - resolver = resolve; - }); - this._waitingForPrompt = promise; - this._promptResolver = resolver; - } - return this._waitingForPrompt; -}; - -MockUI.prototype.prompt = function(opts, cb) { - if (this._waitingForPrompt) { - this._waitingForPrompt = null; - this._promptResolver(); - } - return UI.prototype.prompt.call(this, opts, cb); -}; diff --git a/tests/helpers/mock-watcher.js b/tests/helpers/mock-watcher.js index d8779a7d83..bef29f43ac 100644 --- a/tests/helpers/mock-watcher.js +++ b/tests/helpers/mock-watcher.js @@ -1,23 +1,15 @@ 'use strict'; -var RSVP = require('rsvp'); -var EventEmitter = require('events').EventEmitter; -var path = require('path'); - -function MockWatcher() { - EventEmitter.apply(this, arguments); - this.tracks = []; - this.trackTimings = []; - this.trackErrors = []; +const EventEmitter = require('events').EventEmitter; +const path = require('path'); + +class MockWatcher extends EventEmitter { + then() { + let promise = Promise.resolve({ + directory: path.resolve(__dirname, '../fixtures/express-server'), + }); + return promise.then.apply(promise, arguments); + } } module.exports = MockWatcher; - -MockWatcher.prototype = Object.create(EventEmitter.prototype); - -MockWatcher.prototype.then = function() { - var promise = RSVP.resolve({ - directory: path.resolve(__dirname, '../fixtures/express-server') - }); - return promise.then.apply(promise, arguments); -}; diff --git a/tests/helpers/package-cache.js b/tests/helpers/package-cache.js new file mode 100644 index 0000000000..1e8c0d5e74 --- /dev/null +++ b/tests/helpers/package-cache.js @@ -0,0 +1,661 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const quickTemp = require('quick-temp'); +const { default: Configstore } = require('configstore'); +const CommandGenerator = require('./command-generator'); +const stableStringify = require('safe-stable-stringify'); +const symlinkOrCopySync = require('symlink-or-copy').sync; + +let originalWorkingDirectory = process.cwd(); + +// Module scoped variable to store whether a particular cache has been +// attempted to be upgraded. +let upgraded = {}; + +/* +List of keys which could possibly result in the package manager installing +something. This is the "err on the side of caution" approach. It actually +doesn't matter if something is or isn't automatically installed in any of the +cases where we use this. +*/ +let DEPENDENCY_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']; + +/** + * The `npm` command helper. + * + * @private + * @method npm + * @param {String} subcommand The subcommand to be passed into npm. + * @param {String} [...arguments] Arguments to be passed into the npm subcommand. + * @param {Object} [options={}] The options passed into child_process.spawnSync. + * (https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options) + */ +let npm = new CommandGenerator('npm'); + +/** + * The `yarn` command helper. + * + * @private + * @method yarn + * @param {String} subcommand The subcommand to be passed into yarn. + * @param {String} [...arguments] Arguments to be passed into the yarn subcommand. + * @param {Object} [options={}] The options passed into child_process.spawnSync. + * (https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options) + */ +let yarn = new CommandGenerator('yarn'); + +// This lookup exists to make it possible to look the commands up based upon context. +let originals; +let commands = { + npm, + yarn, +}; + +// The definition list of translation terms. +let lookups = { + manifest: { + npm: 'package.json', + yarn: 'package.json', + }, + path: { + npm: 'node_modules', + yarn: 'node_modules', + }, + upgrade: { + npm: 'install', + yarn: 'upgrade', + }, +}; + +/** + * The `translate` command is used to turn a consistent argument into + * appropriate values based upon the context in which it is used. It's + * a convenience helper to avoid littering lookups throughout the code. + * + * @private + * @method translate + * @param {String} type Either 'npm' or 'yarn'. + * @param {String} lookup Either 'manifest', 'path', or 'upgrade'. + */ +function translate(type, lookup) { + return lookups[lookup][type]; +} + +/** + * The PackageCache wraps all package management functions. It also + * handles initial global state setup. + * + * Usage: + * + * ``` + * let cache = new PackageCache(); + * let dir = cache.create('your-cache', 'yarn', '{ + * "dependencies": { + * "lodash": "*", + * "ember-cli": "*" + * } + * }'); + * // => process.cwd()/tmp/your-cache-A3B4C6D + * ``` + * + * This will generate a persistent cache which contains the results + * of a clean installation of the `dependencies` as you specified in + * the manifest argument. It will save the results of these in a + * temporary folder, returned as `dir`. On a second invocation + * (in the same process, or in a subsequent run) PackageCache will first + * compare the manifest to the previously installed one, using the manifest + * as the cache key, and make a decision as to the fastest way to get + * the cache up-to-date. PackageCache guarantees that your cache will + * always be up-to-date. + * + * If done in the same process, this simply returns the existing cache + * directory with no change, making the following invocation simply a + * cache validation check: + * + * ``` + * let dir2 = cache.create('your-cache', 'yarn', '{ + * "dependencies": { + * "lodash": "*", + * "ember-cli": "*" + * } + * }'); + * // => process.cwd()/tmp/your-cache-A3B4C6D + * ``` + * + * If you wish to modify a cache you can do so using the `update` API: + * + * ``` + * let dir3 = cache.update('your-cache', 'yarn', '{ + * "dependencies": { + * "": "*", + * "lodash": "*", + * "ember-cli": "*" + * } + * }'); + * // => process.cwd()/tmp/your-cache-A3B4C6D + * ``` + * + * Underneath the hood `create` and `update` are identical–which + * makes clear the simplicity of this tool. It will always do the + * right thing. You can think of the outcome of any `create` or + * `update` call as identical to `rm -rf node_modules && npm install` + * except as performant as possible. + * + * If you need to make modifications to a cache but wish to retain + * the original you can invoke the `clone` command: + * + * ``` + * let newDir = cache.clone('your-cache', 'modified-cache'); + * let manifest = fs.readJsonSync(path.join(newDir, 'package.json')); + * manifest.dependencies['express'] = '*'; + * cache.update('modified-cache', 'yarn', JSON.stringify(manifest)); + * // => process.cwd()/tmp/modified-cache-F8D5C8B + * ``` + * + * This mental model makes it easy to prevent coding mistakes, state + * leakage across multiple test runs by making multiple caches cheap, + * and has tremendous performance benefits. + * + * You can even programatically update a cache: + * + * ``` + * let CommandGenerator = require('./command-generator'); + * let yarn = new CommandGenerator('yarn'); + * + * let dir = cache.create('your-cache', 'yarn', '{ ... }'); + * + * yarn.invoke('add', 'some-addon', { cwd: dir }); + * ``` + * + * The programmatic approach enables the entire set of usecases that + * the underlying package manager supports while continuing to wrap it + * in a persistent cache. You should not directly modify any files in the + * cache other than the manifest unless you really know what you're doing as + * that can put the cache into a possibly invalid state. + * + * PackageCache also supports linking. If you pass an array of module names to + * in the fourth position it will ensure that those are linked, and remain + * linked for the lifetime of the cache. When you link a package it uses the + * current global link provided by the underlying package manager and invokes + * their built-in `link` command. + * + * ``` + * let dir = cache.create('your-cache', 'yarn', '{ ... }', ['ember-cli']); + * // => `yarn link ember-cli` happens along the way. + * ``` + * + * Sometimes this global linking behavior is not what you want. Instead you wish + * to link in some other working directory. PackageCache makes a best effort + * attempt at supporting this workflow by allowing you to specify an object in + * the `links` argument array passed to `create`. + * + * let dir = cache.create('your-cache', 'yarn', '{ ... }', [ + * { name: 'ember-cli', path: '/the/absolute/path/to/the/package' }, + * 'other-package' + * ]); + * + * This creates a symlink at the named package path to the specified directory. + * As package managers do different things for their own linking behavior this + * is "best effort" only. Be sure to review upstream behavior to identify if you + * rely on those features for your code to function: + * + * - [npm](https://github.com/npm/npm/blob/latest/lib/link.js) + * - [yarn](https://github.com/yarnpkg/yarn/blob/master/src/cli/commands/link.js) + * + * As the only caveat, PackageCache _is_ persistent. The consuming + * code is responsible for ensuring that the cache size does not + * grow unbounded. + * + * @private + * @class PackageCache + * @constructor + * @param {String} rootPath Root of the directory for `PackageCache`. + */ +module.exports = class PackageCache { + constructor(rootPath) { + this.rootPath = rootPath || originalWorkingDirectory; + + let configPath = path.join(this.rootPath, 'tmp', 'package-cache.json'); + this._conf = new Configstore( + 'package-cache', + {}, + { + configPath, + } + ); + + this._cleanDirs(); + } + + get dirs() { + return this._conf.all; + } + + /** + * The `__setupForTesting` modifies things in module scope. + * + * @private + * @method __setupForTesting + */ + __setupForTesting(stubs) { + originals = commands; + commands = stubs.commands; + } + + /** + * The `__resetForTesting` puts things back in module scope. + * + * @private + * @method __resetForTesting + */ + __resetForTesting() { + commands = originals; + } + + /** + * The `_cleanDirs` method deals with sync issues between the + * Configstore and what exists on disk. Non-existent directories + * are removed from `this.dirs`. + * + * @private + * @method _cleanDirs + */ + _cleanDirs() { + let labels = Object.keys(this.dirs); + + let label, directory; + for (let i = 0; i < labels.length; i++) { + label = labels[i]; + directory = this.dirs[label]; + if (!fs.existsSync(directory)) { + this._conf.delete(label); + } + } + } + + /** + * The `_readManifest` method reads the on-disk manifest for the current + * cache and returns its value. + * + * @private + * @method _readManifest + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + * @return {String} The manifest file contents on disk. + */ + _readManifest(label, type) { + let readManifestDir = this.dirs[label]; + + if (!readManifestDir) { + return null; + } + + let inputPath = path.join(readManifestDir, translate(type, 'manifest')); + + let result = null; + try { + result = fs.readFileSync(inputPath, 'utf8'); + } catch (error) { + // Swallow non-exceptional errors. + if (error.code !== 'ENOENT') { + throw error; + } + } + return result; + } + + /** + * The `_writeManifest` method generates the on-disk folder for the package cache + * and saves the manifest into it. If it is a yarn package cache it will remove + * the existing lock file. + * + * @private + * @method _writeManifest + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + * @param {String} manifest The contents of the manifest file to write to disk. + */ + _writeManifest(label, type, manifest) { + process.chdir(this.rootPath); + let outputDir = quickTemp.makeOrReuse(this.dirs, label); + process.chdir(originalWorkingDirectory); + + this._conf.set(label, outputDir); + + let outputFile = path.join(outputDir, translate(type, 'manifest')); + fs.outputFileSync(outputFile, manifest); + + // Remove any existing yarn.lock file so that it doesn't try to incorrectly use it as a base. + if (type === 'yarn') { + try { + fs.unlinkSync(path.join(outputDir, 'yarn.lock')); + } catch (error) { + // Catch unexceptional error but rethrow if something is truly wrong. + if (error.code !== 'ENOENT') { + throw error; + } + } + } + } + + /** + * The `_removeLinks` method removes from the dependencies of the manifest the + * assets which will be linked in so that we don't duplicate install. It saves + * off the values in the internal `PackageCache` metadata for restoration after + * linking as those values may be necessary. + * + * It is also responsible for removing these links prior to making any changes + * to the specified cache. + * + * @private + * @method _removeLinks + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + */ + _removeLinks(label, type) { + let cachedManifest = this._readManifest(label, type); + if (!cachedManifest) { + return; + } + + let jsonManifest = JSON.parse(cachedManifest); + let links = jsonManifest._packageCache.links; + + // Blindly remove existing links whether or not they appear in the manifest. + let link, linkPath; + for (let i = 0; i < links.length; i++) { + link = links[i]; + if (typeof link === 'string') { + commands[type].invoke('unlink', link, { cwd: this.dirs[label] }); + } else { + linkPath = path.join(this.dirs[label], translate(type, 'path'), link.name); + try { + fs.removeSync(linkPath); + } catch (error) { + // Catch unexceptional error but rethrow if something is truly wrong. + if (error.code !== 'ENOENT') { + throw error; + } + } + } + } + + // Remove things from the manifest which we know we'll link back in. + let originals = {}; + let key, linkName; + for (let i = 0; i < DEPENDENCY_KEYS.length; i++) { + key = DEPENDENCY_KEYS[i]; + if (jsonManifest[key]) { + // Get a clone of the original object. + originals[key] = JSON.parse(JSON.stringify(jsonManifest[key])); + } + for (let j = 0; j < links.length; j++) { + link = links[j]; + + // Support object-style invocation for "manual" linking. + if (typeof link === 'string') { + linkName = link; + } else { + linkName = link.name; + } + + if (jsonManifest[key] && jsonManifest[key][linkName]) { + delete jsonManifest[key][linkName]; + } + } + } + + jsonManifest._packageCache.originals = originals; + let manifest = JSON.stringify(jsonManifest); + + this._writeManifest(label, type, manifest); + } + + /** + * The `_restoreLinks` method restores the dependencies from the internal + * `PackageCache` metadata so that the manifest matches its original state after + * performing the links. + * + * It is also responsible for restoring these links into the `PackageCache`. + * + * @private + * @method _restoreLinks + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + */ + _restoreLinks(label, type) { + let cachedManifest = this._readManifest(label, type); + if (!cachedManifest) { + return; + } + + let jsonManifest = JSON.parse(cachedManifest); + let links = jsonManifest._packageCache.links; + + // Blindly restore links. + let link, linkPath; + for (let i = 0; i < links.length; i++) { + link = links[i]; + if (typeof link === 'string') { + commands[type].invoke('link', link, { cwd: this.dirs[label] }); + } else { + linkPath = path.join(this.dirs[label], translate(type, 'path'), link.name); + fs.mkdirsSync(path.dirname(linkPath)); // Just in case the path doesn't exist. + symlinkOrCopySync(link.path, linkPath); + } + } + + // Restore to original state. + let keys = Object.keys(jsonManifest._packageCache.originals); + let key; + for (let i = 0; i < keys.length; i++) { + key = keys[i]; + jsonManifest[key] = jsonManifest._packageCache.originals[key]; + } + + // Get rid of the originals. + delete jsonManifest._packageCache.originals; + + // Serialize back to disk. + let manifest = JSON.stringify(jsonManifest); + this._writeManifest(label, type, manifest); + } + + /** + * The `_checkManifest` method compares the desired manifest to that which + * exists in the cache. + * + * @private + * @method _checkManifest + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + * @param {String} manifest The contents of the manifest file to compare to cache. + * @return {Boolean} `true` if identical. + */ + _checkManifest(label, type, manifest) { + let cachedManifest = this._readManifest(label, type); + + if (cachedManifest === null) { + return false; + } + + let parsedCached = JSON.parse(cachedManifest); + let parsedNew = JSON.parse(manifest); + + // Only inspect the keys we care about. + // Invalidate the cache based off the private _packageCache key as well. + let keys = [].concat(DEPENDENCY_KEYS, '_packageCache'); + + let key, before, after; + for (let i = 0; i < keys.length; i++) { + key = keys[i]; + + // Empty keys are identical to undefined keys. + before = stableStringify(parsedCached[key]) || '{}'; + after = stableStringify(parsedNew[key]) || '{}'; + + if (before !== after) { + return false; + } + } + + return true; + } + + /** + * The `_install` method installs the contents of the manifest into the + * specified package cache. + * + * @private + * @method _install + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + */ + _install(label, type) { + this._removeLinks(label, type); + commands[type].invoke('install', { cwd: this.dirs[label] }); + this._restoreLinks(label, type); + + // If we just did a clean install we can treat it as up-to-date. + upgraded[label] = true; + } + + /** + * The `_upgrade` method guarantees that the contents of the manifest are + * allowed to drift in a SemVer compatible manner. It ensures that CI is + * always running against the latest versions of all dependencies. + * + * @private + * @method _upgrade + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + */ + _upgrade(label, type) { + // Lock out upgrade calls after the first time upgrading the cache. + if (upgraded[label]) { + return; + } + + if (!this._canUpgrade(label, type)) { + // Only way to get repeatable behavior in npm: start over. + // We turn an `_upgrade` task into an `_install` task. + fs.removeSync(path.join(this.dirs[label], translate(type, 'path'))); + return this._install(label, type); + } + + this._removeLinks(label, type); + commands[type].invoke(translate(type, 'upgrade'), { cwd: this.dirs[label] }); + this._restoreLinks(label, type); + + upgraded[label] = true; + } + + _canUpgrade(label, type) { + return type === 'yarn' && fs.existsSync(path.join(this.dirs[label], 'yarn.lock')); + } + + // PUBLIC API BELOW + + /** + * The `create` method adds a new package cache entry. + * + * @method create + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + * @param {String} manifest The contents of the manifest file for the cache. + * @param {Array} links Packages to omit for install and link. + * @return {String} The directory on disk which contains the cache. + */ + create(label, type, manifest, links) { + links = links || []; + + // Save metadata about the PackageCache invocation in the manifest. + let packageManagerVersion = commands[type].invoke('--version').stdout; + + let jsonManifest = JSON.parse(manifest); + jsonManifest._packageCache = { + node: process.version, + packageManager: type, + packageManagerVersion, + links, + }; + + manifest = JSON.stringify(jsonManifest); + + // Compare any existing manifest to the ideal per current blueprint. + let identical = this._checkManifest(label, type, manifest); + + if (identical) { + // Use what we have, but opt in to SemVer drift. + this._upgrade(label, type); + } else { + // Tell the package manager to start semi-fresh. + this._writeManifest(label, type, manifest); + this._install(label, type); + } + + return this.dirs[label]; + } + + /** + * The `update` method aliases the `create` method. + * + * @method update + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + * @param {String} manifest The contents of the manifest file for the cache. + * @param {Array} links Packages to elide for install and link. + * @return {String} The directory on disk which contains the cache. + */ + update(/*label, type, manifest, links*/) { + return this.create.apply(this, arguments); + } + + /** + * The `get` method returns the directory for the cache. + * + * @method get + * @param {String} label The label for the cache. + * @return {String} The directory on disk which contains the cache. + */ + get(label) { + return this.dirs[label]; + } + + /** + * The `destroy` method removes all evidence of the package cache. + * + * @method destroy + * @param {String} label The label for the cache. + * @param {String} type The type of package cache. + */ + destroy(label) { + process.chdir(this.rootPath); + quickTemp.remove(this.dirs, label); + process.chdir(originalWorkingDirectory); + + this._conf.delete(label); + } + + /** + * The `clone` method duplicates a cache. Some package managers can + * leverage a pre-existing state to speed up their installation. + * + * @method destroy + * @param {String} fromLabel The label for the cache to clone. + * @param {String} toLabel The label for the new cache. + */ + clone(fromLabel, toLabel) { + process.chdir(this.rootPath); + let outputDir = quickTemp.makeOrReuse(this.dirs, toLabel); + process.chdir(originalWorkingDirectory); + + this._conf.set(toLabel, outputDir); + + fs.copySync(this.get(fromLabel), outputDir); + + return this.dirs[toLabel]; + } +}; diff --git a/tests/helpers/per-bundle-addon-cache.js b/tests/helpers/per-bundle-addon-cache.js new file mode 100644 index 0000000000..4df1e4b4d6 --- /dev/null +++ b/tests/helpers/per-bundle-addon-cache.js @@ -0,0 +1,192 @@ +'use strict'; + +const FixturifyProject = require('./fixturify-project'); +const { TARGET_INSTANCE } = require('../../lib/models/per-bundle-addon-cache/target-instance'); +const isLazyEngine = require('../../lib/utilities/is-lazy-engine'); + +/** + * This collects all addons by name within a given host; it stops traversing when it + * encounters another host (i.e., a lazy engine). Within a given host we should expect + * at most 1 real addon, otherwise this is an error condition. We otherwise add all + * proxies to `config.proxies` + * + * @name getAllAddonsByNameWithinHost + * @param {Project|Addon} projectOrAddon + * @param {string} addonName + * @param {Object} [config] + * @returns {{proxies: Proxy[], realAddon: Addon}} + */ +function getAllAddonsByNameWithinHost(projectOrAddon, addonName, config = { proxies: [] }) { + if (!config.originalHost) { + config.originalHost = projectOrAddon; + } + + projectOrAddon.addons.forEach((addon) => { + if (addon.name === addonName) { + if (config.realAddon && !addon[TARGET_INSTANCE]) { + throw new Error( + `The real addon (\`${addon.name}\`) has already been set for a given host (\`${ + typeof config.originalHost.name === 'function' ? config.originalHost.name() : config.originalHost.name + }\`); the proxy for addon caching is not working correctly` + ); + } + + if (addon[TARGET_INSTANCE]) { + config.proxies.push(addon); + } else { + config.realAddon = addon; + } + } + + // stop traversing within another host + if (!isLazyEngine(addon)) { + getAllAddonsByNameWithinHost(addon, addonName, config); + } + }); + + return config; +} + +/** + * Returns whether all instances within a given host are equal (i.e., that there's a single + * "real addon") and all proxies have the `TARGET_INSTANCE` property that's strictly equal to + * the aforementioned real addon + * + * @name areAllInstancesEqualWithinHost + * @param {Project|Addon} projectOrAddon + * @param {string} addonName + * @returns {boolean} + */ +function areAllInstancesEqualWithinHost(projectOrAddon, addonName) { + const { realAddon, proxies } = getAllAddonsByNameWithinHost(projectOrAddon, addonName); + return proxies.length > 0 && proxies.every((proxy) => proxy[TARGET_INSTANCE] === realAddon); +} + +/** + * For a given project/addon, this counts addon instances within said project/addon; + * specifically we're interested in the number of "real" addon instances, and proxy + * objects. + * + * @name countAddons + * @param {Project|Addon} projectOrAddon + * @param {Object} [config] + * @returns {{byName: Object, proxyCount: number, realAddonInstanceCount: number}} + */ +function countAddons(projectOrAddon, config = { byName: {}, proxyCount: 0, realAddonInstanceCount: 0 }) { + projectOrAddon.addons.forEach((addon) => { + const addonName = addon.name; + + if (!config.byName[addonName]) { + config.byName[addonName] = { + addons: [], + proxyCount: 0, + realAddonInstanceCount: 0, + }; + } + + if (addon[TARGET_INSTANCE]) { + config.proxyCount++; + config.byName[addonName].proxyCount++; + } else { + config.realAddonInstanceCount++; + config.byName[addonName].realAddonInstanceCount++; + } + + config.byName[addonName].addons.push(addon); + countAddons(addon, config); + }); + + return config; +} + +/** + * Generate the file structure used for the cache-bundle-hosts and enable-cache tests. + * Puts it into the usual temporary location defined by ECFP. + * + * In this fixture, all the addon definitions are to be held in PROJECT/lib, even + * though the project itself doesn't directly depend on a few of them. This is so + * it's easier to create a single reference to a particular addon path, to enable + * the proxy code to function. + * + * @name createStandardCacheFixture + */ +function createStandardCacheFixture() { + let project = new FixturifyProject('test-ember-project', '1.0.0'); + + project.addInRepoAddon('test-addon-a', '1.0.0', { + callback: (addonA) => { + addonA.addInRepoAddon('test-addon-dep', '1.0.0'); + + // At this point, TAD has been run through toJSON inside of TAA. + // TAD itself has no issues. + // in TAA, we want to store all the inrepo addons, at any level, in + // PROJ/lib, so move TAD from TAA and change its path in TAA. + addonA.pkg['ember-addon'].paths = ['../test-addon-dep']; + project.files.lib = project.files.lib || {}; + project.files.lib['test-addon-dep'] = addonA.files.lib['test-addon-dep']; + delete addonA.files.lib; + }, + }); + + project.addInRepoEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + callback: (lazyEngineA) => { + lazyEngineA.addInRepoAddon('test-engine-dep', '1.0.0'); + + // Similar to above + lazyEngineA.pkg['ember-addon'].paths = ['../test-engine-dep']; + project.files.lib['test-engine-dep'] = lazyEngineA.files.lib['test-engine-dep']; + delete lazyEngineA.files.lib; + }, + }); + + project.addInRepoEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + callback: (lazyEngineB) => { + // These two addon definitions have already been moved to project, so just + // fix the ember-addon.paths and remove the files.lib entry. + lazyEngineB.pkg['ember-addon'].paths = ['../test-engine-dep', '../test-addon-dep']; + delete lazyEngineB.files.lib; + }, + }); + + project.addInRepoEngine('regular-engine-c', '1.0.0', { + callback: (regularEngineC) => { + regularEngineC.pkg['ember-addon'].paths = ['../test-engine-dep']; + delete regularEngineC.files.lib; + }, + }); + + return project; +} + +/** + * For help with testing, given a bundleHostName and an addon name, return + * a list of the addon cache entries that have that addon name. + * + * @name findAddonCacheEntriesByName + */ +function findAddonCacheEntriesByName(perBundleAddonCacheInstance, bundleHostPkgInfo, addonName) { + let bundleHostCacheEntry = perBundleAddonCacheInstance.bundleHostCache.get(bundleHostPkgInfo); + + if (!bundleHostCacheEntry) { + return null; + } + + let addonInstanceCache = bundleHostCacheEntry.addonInstanceCache; + let cacheEntries = Array.from(addonInstanceCache.values()); + let addonEntries = cacheEntries.filter((entry) => entry[TARGET_INSTANCE].name === addonName); + + return addonEntries; +} + +/** + * Simple utilities to help test the PerBundleAddonCache feature. + */ +module.exports = { + findAddonCacheEntriesByName, + createStandardCacheFixture, + getAllAddonsByNameWithinHost, + areAllInstancesEqualWithinHost, + countAddons, +}; diff --git a/tests/helpers/process-help-string.js b/tests/helpers/process-help-string.js index 58267b356e..0107dc92da 100644 --- a/tests/helpers/process-help-string.js +++ b/tests/helpers/process-help-string.js @@ -1,9 +1,11 @@ 'use strict'; -var stripAnsi = require('strip-ansi'); -var supportsColor = require('supports-color'); +const { supportsColor } = require('chalk'); +// eslint-disable-next-line n/no-unpublished-require +const { default: stripAnsi } = require('strip-ansi'); -module.exports = function(helpString) { +module.exports = function (helpString) { + // currently windows if (supportsColor) { return helpString; } diff --git a/tests/helpers/proxy-server.js b/tests/helpers/proxy-server.js index 4bac1d2e6b..e9b21a0f1d 100644 --- a/tests/helpers/proxy-server.js +++ b/tests/helpers/proxy-server.js @@ -1,18 +1,44 @@ 'use strict'; -var http = require('http'); - -var ProxyServer = function() { - var _this = this; - this.called = false; - this.lastReq = null; - this.httpServer = http.createServer(function(req, res) { - _this.called = true; - _this.lastReq = req; - res.writeHead(200, {'Content-Type': 'text/plain'}); - res.end('okay'); - }); - this.httpServer.listen(3001); -}; +const http = require('http'); +// eslint-disable-next-line n/no-unpublished-require +const WebSocketServer = require('websocket').server; + +class ProxyServer { + constructor() { + this.called = false; + this.lastReq = null; + this.httpServer = http.createServer((req, res) => { + this.called = true; + this.lastReq = req; + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('okay'); + }); + this.httpServer.listen(3001); + + let wsServer = new WebSocketServer({ + httpServer: this.httpServer, + autoAcceptConnections: true, + }); + + let websocketEvents = (this.websocketEvents = []); + wsServer.on('connect', (connection) => { + websocketEvents.push('connect'); + + connection.on('message', (message) => { + websocketEvents.push(`message: ${message.utf8Data}`); + connection.sendUTF(message.utf8Data); + }); + + connection.on('error', (error) => { + websocketEvents.push(`error: ${error}`); + }); + + connection.on('close', () => { + websocketEvents.push(`close`); + }); + }); + } +} module.exports = ProxyServer; diff --git a/tests/helpers/run-command.js b/tests/helpers/run-command.js index 5215033a13..ca92a76c07 100644 --- a/tests/helpers/run-command.js +++ b/tests/helpers/run-command.js @@ -1,73 +1,80 @@ 'use strict'; -var RSVP = require('rsvp'); -var Promise = require('../../lib/ext/promise'); -var chalk = require('chalk'); -var spawn = require('child_process').spawn; -var defaults = require('lodash/object/defaults'); -var killCliProcess = require('./kill-cli-process'); - +const { default: chalk } = require('chalk'); +const spawn = require('child_process').spawn; +const defaults = require('lodash/defaults'); +const killCliProcess = require('./kill-cli-process'); +const logOnFailure = require('./log-on-failure'); +let debug = require('heimdalljs-logger')('run-command'); +const { captureExit, onExit } = require('capture-exit'); + +// when running the full test suite, `process.exit` has already been captured +// however, when running specific files (e.g. `mocha some/path/to/file.js`) +// exit may not be captured before `runCommand` attempts to call `onExit` +captureExit(); + +let RUNS = []; module.exports = function run(/* command, args, options */) { - var command = arguments[0]; - var args = Array.prototype.slice.call(arguments, 1); - var options = {}; + let command = arguments[0]; + let args = Array.prototype.slice.call(arguments, 1); + let options = {}; if (typeof args[args.length - 1] === 'object') { options = args.pop(); } options = defaults(options, { - verbose: true, + // If true, pass through stdout/stderr. + // If false, only pass through stdout/stderr if the current test fails. + verbose: false, - onOutput: function(string) { - if (options.verbose) { console.log(string); } + onOutput(string) { + options.log(string); }, - onError: function(string) { - if (options.verbose) { console.error(chalk.red(string)); } - } + onError(string) { + options.log(chalk.red(string)); + }, + + log(string) { + debug.debug(string); + if (options.verbose) { + console.log(string); + } else { + logOnFailure(string); + } + }, }); - return new RSVP.Promise(function(resolve, reject) { - console.log(' Running: ' + command + ' ' + args.join(' ') + ' in: ' + process.cwd()); + let child; + const promise = new Promise(function (resolve, reject) { + options.log(` Running: ${command} ${args.join(' ')} in: ${process.cwd()}`); - var opts = {}; + let opts = {}; + args = [`--unhandled-rejections=strict`, `${command}`].concat(args); + command = 'node'; if (process.platform === 'win32') { - args = ['"' + command + '"'].concat(args); - command = 'node'; opts.windowsVerbatimArguments = true; opts.stdio = [null, null, null, 'ipc']; } - var child = spawn(command, args, opts); - var result = { + if (options.env) { + opts.env = defaults(options.env, process.env); + } + + debug.info('runCommand: %s, args: %o', command, args); + child = spawn(command, args, opts); + RUNS.push(child); + // ensure we tear down the child process on exit; + onExit(() => killCliProcess(child)); + + let result = { output: [], errors: [], - code: null + code: null, }; - if (options.onChildSpawned) { - var onChildSpawnedPromise = new Promise(function (childSpawnedResolve, childSpawnedReject) { - try { - options.onChildSpawned(child).then(childSpawnedResolve, childSpawnedReject); - } catch (err) { - childSpawnedReject(err); - } - }); - onChildSpawnedPromise - .then(function () { - if (options.killAfterChildSpawnedPromiseResolution) { - killCliProcess(child); - } - }, function (err) { - result.testingError = err; - if (options.killAfterChildSpawnedPromiseResolution) { - killCliProcess(child); - } - }); - } - child.stdout.on('data', function (data) { - var string = data.toString(); + let string = data.toString(); options.onOutput(string, child); @@ -75,7 +82,7 @@ module.exports = function run(/* command, args, options */) { }); child.stderr.on('data', function (data) { - var string = data.toString(); + let string = data.toString(); options.onError(string, child); @@ -93,4 +100,22 @@ module.exports = function run(/* command, args, options */) { } }); }); + + promise.kill = function () { + killCliProcess(child); + }; + + return promise; +}; + +module.exports.killAll = function () { + RUNS.forEach((run) => { + try { + killCliProcess(run); + } catch (e) { + console.error(e); + // during teardown, issues can arise, but teardown must complete it's operation + } + }); + RUNS.length = 0; }; diff --git a/tests/helpers/stub.js b/tests/helpers/stub.js deleted file mode 100644 index 2df449b3cd..0000000000 --- a/tests/helpers/stub.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -module.exports = { - stub: function stub(obj, name, value) { - var original = obj[name]; - - obj[name] = function() { - obj[name].called++; - obj[name].calledWith.push(arguments); - return value; - }; - - obj[name].restore = function() { - obj[name] = original; - }; - - obj[name].called = 0; - obj[name].calledWith = []; - return obj[name]; - }, - stubPath: function stubPath(path) { - return { - basename: function() { - return path; - } - }; - }, - stubBlueprint: function stubBlueprint() { - return function Blueprint() { - return { - install: function() { } - }; - }; - } -}; diff --git a/tests/helpers/tmp.js b/tests/helpers/tmp.js index d1e39de49e..17774f4001 100644 --- a/tests/helpers/tmp.js +++ b/tests/helpers/tmp.js @@ -1,26 +1,18 @@ 'use strict'; -var fs = require('fs-extra'); -var existsSync = require('exists-sync'); -var Promise = require('../../lib/ext/promise'); -var remove = Promise.denodeify(fs.remove); -var root = process.cwd(); +const fs = require('fs-extra'); -module.exports.setup = function(path) { +let root = process.cwd(); + +module.exports.setup = function (path) { process.chdir(root); - return remove(path) - .then(function() { - fs.mkdirsSync(path); - }); + return fs.remove(path).then(function () { + fs.mkdirsSync(path); + }); }; -module.exports.teardown = function(path) { +module.exports.teardown = function (path) { process.chdir(root); - - if (existsSync(path)) { - return remove(path); - } else { - return Promise.resolve(); - } + return fs.remove(path); }; diff --git a/tests/integration/models/blueprint-test.js b/tests/integration/models/blueprint-test.js new file mode 100644 index 0000000000..4160ee8b7d --- /dev/null +++ b/tests/integration/models/blueprint-test.js @@ -0,0 +1,1552 @@ +'use strict'; + +const { existsSync, readFile, remove } = require('fs-extra'); +const Task = require('../../../lib/models/task'); +const MockProject = require('../../helpers/mock-project'); +const MockUI = require('console-ui/mock'); +const { expect } = require('chai'); +const path = require('path'); +const { globSync } = require('glob'); +const walkSync = require('walk-sync'); + +const EOL = require('os').EOL; +let root = process.cwd(); +let tempRoot = path.join(root, 'tmp'); +const SilentError = require('silent-error'); +const tmp = require('tmp-promise'); +const td = require('testdouble'); +const Blueprint = require('@ember-tooling/blueprint-model'); + +let localsCalled; +let normalizeEntityNameCalled; +let fileMapTokensCalled; +let filesPathCalled; +let beforeUninstallCalled; +let beforeInstallCalled; +let afterInstallCalled; +let afterUninstallCalled; + +function resetCalled() { + localsCalled = false; + normalizeEntityNameCalled = false; + fileMapTokensCalled = false; + filesPathCalled = false; + beforeUninstallCalled = false; + beforeInstallCalled = false; + afterInstallCalled = false; + afterUninstallCalled = false; +} + +let instrumented = { + locals(/* opts */) { + localsCalled = true; + return this._super.locals.apply(this, arguments); + }, + + normalizeEntityName(/* name */) { + normalizeEntityNameCalled = true; + return this._super.normalizeEntityName.apply(this, arguments); + }, + + fileMapTokens() { + fileMapTokensCalled = true; + return this._super.fileMapTokens.apply(this, arguments); + }, + + filesPath(/* opts */) { + filesPathCalled = true; + return this._super.filesPath.apply(this, arguments); + }, + + beforeInstall(/* opts */) { + beforeInstallCalled = true; + return this._super.beforeInstall.apply(this, arguments); + }, + + afterInstall(/* opts */) { + afterInstallCalled = true; + return this._super.afterInstall.apply(this, arguments); + }, + + beforeUninstall() { + beforeUninstallCalled = true; + return this._super.beforeUninstall.apply(this, arguments); + }, + + afterUninstall() { + afterUninstallCalled = true; + return this._super.afterUninstall.apply(this, arguments); + }, +}; + +let fixtureBlueprints = path.resolve(__dirname, '..', '..', 'fixtures', 'blueprints'); +let basicBlueprint = path.join(fixtureBlueprints, 'basic'); +let basicNewBlueprint = path.join(fixtureBlueprints, 'basic_2'); +let basicEsmBlueprint = path.join(fixtureBlueprints, 'basic-esm'); + +let basicBlueprintFiles = [ + '.ember-cli', + '.gitignore', + 'app/', + 'app/basics/', + 'app/basics/mock-project.txt', + 'bar', + 'file-to-remove.txt', + 'foo.txt', + 'test.txt', +]; + +let basicBlueprintFilesAfterBasic2 = [ + '.ember-cli', + '.gitignore', + 'app/', + 'app/basics/', + 'app/basics/mock-project.txt', + 'bar', + 'foo.txt', + 'test.txt', +]; + +describe('Blueprint', function () { + const BasicBlueprintClass = require(basicBlueprint); + let InstrumentedBasicBlueprint = BasicBlueprintClass.extend(instrumented); + + beforeEach(function () { + resetCalled(); + }); + + afterEach(function () { + td.reset(); + }); + + describe('.fileMapTokens', function () { + it('adds additional tokens from fileMapTokens hook', function () { + let blueprint = Blueprint.lookup(basicBlueprint); + blueprint.fileMapTokens = function () { + return { + __foo__() { + return 'foo'; + }, + }; + }; + let tokens = blueprint._fileMapTokens(); + expect(tokens.__foo__()).to.equal('foo'); + }); + }); + + describe('.generateFileMap', function () { + it('should not have locals in the fileMap', function () { + let blueprint = Blueprint.lookup(basicBlueprint); + + let fileMapVariables = { + pod: true, + podPath: 'pods', + isAddon: false, + blueprintName: 'test', + dasherizedModuleName: 'foo-baz', + locals: { SOME_LOCAL_ARG: 'ARGH' }, + }; + + let fileMap = blueprint.generateFileMap(fileMapVariables); + let expected = { + __name__: 'foo-baz', + __path__: 'tests', + __root__: 'app', + __test__: 'foo-baz-test', + }; + + expect(fileMap).to.deep.equal(expected); + }); + }); + + describe('.lookup', function () { + it('uses an explicit path if one is given', function () { + const expectedClass = require(basicBlueprint); + let blueprint = Blueprint.lookup(basicBlueprint); + + expect(blueprint.name).to.equal('basic'); + expect(blueprint.path).to.equal(basicBlueprint); + expect(blueprint instanceof expectedClass).to.equal(true); + }); + + it('finds blueprints within given lookup paths', function () { + const expectedClass = require(basicBlueprint); + let blueprint = Blueprint.lookup('basic', { + paths: [fixtureBlueprints], + }); + + expect(blueprint.name).to.equal('basic'); + expect(blueprint.path).to.equal(basicBlueprint); + expect(blueprint instanceof expectedClass).to.equal(true); + }); + + it('finds blueprints in the ember-cli package', function () { + let expectedPath = path.dirname(require.resolve('@ember-tooling/classic-build-app-blueprint')); + let expectedClass = Blueprint; + + let blueprint = Blueprint.lookup('app'); + + // TODO it's really strange that a blueprint's name is defined by its folder + // do we need to fix this? + expect(blueprint.name).to.equal('app-blueprint'); + expect(blueprint.path).to.equal(expectedPath); + expect(blueprint instanceof expectedClass).to.equal(true); + }); + + it('can instantiate a blueprint that exports an object instead of a constructor', function () { + let blueprint = Blueprint.lookup('exporting-object', { + paths: [fixtureBlueprints], + }); + + expect(blueprint.woot).to.equal('someValueHere'); + expect(blueprint instanceof Blueprint).to.equal(true); + }); + + it('throws an error if no blueprint is found', function () { + expect(() => { + Blueprint.lookup('foo'); + }).to.throw('Unknown blueprint: foo'); + }); + + it('returns undefined if no blueprint is found and ignoredMissing is passed', function () { + let blueprint = Blueprint.lookup('foo', { + ignoreMissing: true, + }); + + expect(blueprint).to.equal(undefined); + }); + }); + + it('exists', function () { + let blueprint = new Blueprint(basicBlueprint); + expect(!!blueprint).to.equal(true); + }); + + it('derives name from path', function () { + let blueprint = new Blueprint(basicBlueprint); + expect(blueprint.name).to.equal('basic'); + }); + + describe('filesPath', function () { + it('returns the blueprints default files path', function () { + let blueprint = new Blueprint(basicBlueprint); + + expect(blueprint.filesPath()).to.equal(path.join(basicBlueprint, 'files')); + }); + }); + + describe('basic blueprint installation', function () { + let blueprint; + let ui; + let project; + let options; + let tmpdir; + + beforeEach(async function () { + const { path: dir } = await tmp.dir(); + tmpdir = dir; + blueprint = new InstrumentedBasicBlueprint(basicBlueprint); + ui = new MockUI(); + td.replace(ui, 'prompt'); + + project = new MockProject(); + options = { + ui, + project, + target: tmpdir, + }; + }); + + afterEach(async function () { + await remove(tempRoot); + }); + + it('installs basic files', async function () { + expect(!!blueprint).to.equal(true); + + await blueprint.install(options); + + let actualFiles = walkSync(tmpdir).sort(); + let output = ui.output.trim().split(EOL); + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/create.* .ember-cli/); + expect(output.shift()).to.match(/create.* .gitignore/); + expect(output.shift()).to.match(/create.* app[/\\]basics[/\\]mock-project.txt/); + expect(output.shift()).to.match(/create.* bar/); + expect(output.shift()).to.match(/create.* file-to-remove.txt/); + expect(output.shift()).to.match(/create.* foo.txt/); + expect(output.shift()).to.match(/create.* test.txt/); + expect(output.length).to.equal(0); + expect(actualFiles).to.deep.equal(basicBlueprintFiles); + expect(() => { + readFile(path.join(tmpdir, 'test.txt'), 'utf-8', function (err, content) { + if (err) { + throw 'error'; + } + expect(content).to.match(/I AM TESTY/); + }); + }).not.to.throw(); + }); + + it('re-installing identical files', async function () { + await blueprint.install(options); + + let output = ui.output.trim().split(EOL); + ui.output = ''; + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/create.* .ember-cli/); + expect(output.shift()).to.match(/create.* .gitignore/); + expect(output.shift()).to.match(/create.* app[/\\]basics[/\\]mock-project.txt/); + expect(output.shift()).to.match(/create.* bar/); + expect(output.shift()).to.match(/create.* file-to-remove.txt/); + expect(output.shift()).to.match(/create.* foo.txt/); + expect(output.shift()).to.match(/create.* test.txt/); + expect(output.length).to.equal(0); + + await blueprint.install(options); + + let actualFiles = walkSync(tmpdir).sort(); + output = ui.output.trim().split(EOL); + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/identical.* .ember-cli/); + expect(output.shift()).to.match(/identical.* .gitignore/); + expect(output.shift()).to.match(/identical.* app[/\\]basics[/\\]mock-project.txt/); + expect(output.shift()).to.match(/identical.* bar/); + expect(output.shift()).to.match(/identical.* file-to-remove.txt/); + expect(output.shift()).to.match(/identical.* foo.txt/); + expect(output.shift()).to.match(/identical.* test.txt/); + expect(output.length).to.equal(0); + + expect(actualFiles).to.deep.equal(basicBlueprintFiles); + }); + + it('re-installing conflicting files', async function () { + td.when(ui.prompt(td.matchers.anything())).thenReturn( + Promise.resolve({ answer: 'skip' }), + Promise.resolve({ answer: 'overwrite' }) + ); + + await blueprint.install(options); + + let output = ui.output.trim().split(EOL); + ui.output = ''; + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/create.* .ember-cli/); + expect(output.shift()).to.match(/create.* .gitignore/); + expect(output.shift()).to.match(/create.* app[/\\]basics[/\\]mock-project.txt/); + expect(output.shift()).to.match(/create.* bar/); + expect(output.shift()).to.match(/create.* file-to-remove.txt/); + expect(output.shift()).to.match(/create.* foo.txt/); + expect(output.shift()).to.match(/create.* test.txt/); + expect(output.length).to.equal(0); + + let blueprintNew = Blueprint.lookup(basicNewBlueprint); + + await blueprintNew.install(options); + + td.verify(ui.prompt(td.matchers.anything()), { times: 2 }); + + let actualFiles = walkSync(tmpdir).sort(); + // Prompts contain \n EOL + // Split output on \n since it will have the same affect as spliting on OS specific EOL + output = ui.output.trim().split('\n'); + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/identical.* \.ember-cli/); + expect(output.shift()).to.match(/identical.* \.gitignore/); + expect(output.shift()).to.match(/skip.* foo.txt/); + expect(output.shift()).to.match(/overwrite.* test.txt/); + expect(output.shift()).to.match(/remove.* file-to-remove.txt/); + expect(output.length).to.equal(0); + + expect(actualFiles).to.deep.equal(basicBlueprintFilesAfterBasic2); + }); + + it('installs path globPattern file', async function () { + options.targetFiles = ['foo.txt']; + await blueprint.install(options); + let actualFiles = walkSync(tmpdir).sort(); + let globFiles = globSync('**/foo.txt', { + cwd: tmpdir, + dot: true, + mark: true, + }).sort(); + let output = ui.output.trim().split(EOL); + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/create.* foo.txt/); + expect(output.length).to.equal(0); + + expect(actualFiles).to.deep.equal(globFiles); + }); + + it('installs multiple globPattern files', async function () { + options.targetFiles = ['foo.txt', 'test.txt']; + await blueprint.install(options); + let actualFiles = walkSync(tmpdir).sort(); + let globFiles = globSync('**/*.txt', { + cwd: tmpdir, + dot: true, + mark: true, + }).sort(); + let output = ui.output.trim().split(EOL); + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/create.* foo.txt/); + expect(output.shift()).to.match(/create.* test.txt/); + expect(output.length).to.equal(0); + + expect(actualFiles).to.deep.equal(globFiles); + }); + + describe('called on an existing project', function () { + beforeEach(function () { + Blueprint.ignoredUpdateFiles.push('foo.txt'); + }); + + it('ignores files in ignoredUpdateFiles', async function () { + td.when(ui.prompt(), { ignoreExtraArgs: true }).thenReturn(Promise.resolve({ answer: 'skip' })); + await blueprint.install(options); + + let output = ui.output.trim().split(EOL); + ui.output = ''; + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/create.* .ember-cli/); + expect(output.shift()).to.match(/create.* .gitignore/); + expect(output.shift()).to.match(/create.* app[/\\]basics[/\\]mock-project.txt/); + expect(output.shift()).to.match(/create.* bar/); + expect(output.shift()).to.match(/create.* file-to-remove.txt/); + expect(output.shift()).to.match(/create.* foo.txt/); + expect(output.shift()).to.match(/create.* test.txt/); + expect(output.length).to.equal(0); + + let blueprintNew = new Blueprint(basicNewBlueprint); + + options.project.isEmberCLIProject = function () { + return true; + }; + + await blueprintNew.install(options); + + let actualFiles = walkSync(tmpdir).sort(); + // Prompts contain \n EOL + // Split output on \n since it will have the same affect as spliting on OS specific EOL + output = ui.output.trim().split('\n'); + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/identical.* \.ember-cli/); + expect(output.shift()).to.match(/identical.* \.gitignore/); + expect(output.shift()).to.match(/skip.* test.txt/); + expect(output.length).to.equal(0); + + expect(actualFiles).to.deep.equal(basicBlueprintFiles); + }); + }); + + describe('called on a new project', function () { + beforeEach(function () { + Blueprint.ignoredUpdateFiles.push('foo.txt'); + }); + + it('does not ignores files in ignoredUpdateFiles', async function () { + td.when(ui.prompt(), { ignoreExtraArgs: true }).thenReturn(Promise.resolve({ answer: 'skip' })); + await blueprint.install(options); + + let output = ui.output.trim().split(EOL); + ui.output = ''; + + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/create.* .ember-cli/); + expect(output.shift()).to.match(/create.* .gitignore/); + expect(output.shift()).to.match(/create.* app[/\\]basics[/\\]mock-project.txt/); + expect(output.shift()).to.match(/create.* bar/); + expect(output.shift()).to.match(/create.* file-to-remove.txt/); + expect(output.shift()).to.match(/create.* foo.txt/); + expect(output.shift()).to.match(/create.* test.txt/); + expect(output.length).to.equal(0); + + let blueprintNew = new Blueprint(basicNewBlueprint); + + options.project.isEmberCLIProject = function () { + return false; + }; + + await blueprintNew.install(options); + + let actualFiles = walkSync(tmpdir).sort(); + // Prompts contain \n EOL + // Split output on \n since it will have the same affect as spliting on OS specific EOL + output = ui.output.trim().split('\n'); + expect(output.shift()).to.match(/^installing/); + expect(output.shift()).to.match(/identical.* \.ember-cli/); + expect(output.shift()).to.match(/identical.* \.gitignore/); + expect(output.shift()).to.match(/skip.* foo.txt/); + expect(output.shift()).to.match(/skip.* test.txt/); + expect(output.length).to.equal(0); + + expect(actualFiles).to.deep.equal(basicBlueprintFiles); + }); + }); + + it('throws error when there is a trailing forward slash in entityName', async function () { + try { + options.entity = { name: 'foo/' }; + await blueprint.install(options); + expect.fail('expected rejection'); + } catch (e) { + expect(e.message).to.match( + /You specified "foo\/", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./ + ); + } + + try { + options.entity = { name: 'foo\\' }; + await blueprint.install(options); + expect.fail('expected rejection'); + } catch (e) { + expect(e.message).to.match( + /You specified "foo\\", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./ + ); + } + + options.entity = { name: 'foo' }; + await blueprint.install(options); + }); + + it('throws error when an entityName is not provided', async function () { + try { + options.entity = {}; + await blueprint.install(options); + expect.fail('expected rejection)'); + } catch (e) { + expect(e).to.be.instanceof(SilentError); + expect(e.message).to.match( + /The `ember generate ` command requires an entity name to be specified./ + ); + } + }); + + it('throws error when an action does not exist', async function () { + blueprint._actions = {}; + try { + await blueprint.install(options); + expect.fail('expected rejection'); + } catch (e) { + expect(e.message).to.equal('Tried to call action "write" but it does not exist'); + } + }); + + it('calls normalizeEntityName hook during install', async function () { + const wait = new Promise((resolve) => { + blueprint.normalizeEntityName = function () { + resolve(); + }; + }); + options.entity = { name: 'foo' }; + await blueprint.install(options); + await wait; + }); + + it('normalizeEntityName hook can modify the entity name', async function () { + blueprint.normalizeEntityName = function () { + return 'foo'; + }; + options.entity = { name: 'bar' }; + + await blueprint.install(options); + let actualFiles = walkSync(tmpdir).sort(); + + expect(actualFiles).to.contain('app/basics/foo.txt'); + expect(actualFiles).to.not.contain('app/basics/mock-project.txt'); + }); + + it('calls normalizeEntityName before locals hook is called', async function () { + blueprint.normalizeEntityName = function () { + return 'foo'; + }; + let done; + const waitForLocals = new Promise((resolve) => (done = resolve)); + blueprint.locals = function (options) { + expect(options.entity.name).to.equal('foo'); + done(); + }; + options.entity = { name: 'bar' }; + await blueprint.install(options); + await waitForLocals; + }); + + it('calls appropriate hooks with correct arguments', async function () { + options.entity = { name: 'foo' }; + + await blueprint.install(options); + expect(localsCalled).to.be.true; + expect(normalizeEntityNameCalled).to.be.true; + expect(fileMapTokensCalled).to.be.true; + expect(filesPathCalled).to.be.true; + expect(beforeInstallCalled).to.be.true; + expect(afterInstallCalled).to.be.true; + expect(beforeUninstallCalled).to.be.false; + expect(afterUninstallCalled).to.be.false; + }); + + it("doesn't throw when running uninstall without installing first", function () { + return blueprint.uninstall(options); + }); + }); + + describe('basic blueprint uninstallation', function () { + const BasicBlueprintClass = require(basicBlueprint); + let blueprint; + let ui; + let project; + let options; + let tmpdir; + + function refreshUI() { + ui = new MockUI(); + options.ui = ui; + } + + beforeEach(async function () { + const { path: dir } = await tmp.dir(); + + tmpdir = dir; + blueprint = new BasicBlueprintClass(basicBlueprint); + project = new MockProject(); + options = { + project, + target: tmpdir, + }; + refreshUI(); + + await blueprint.install(options); + refreshUI(); + }); + + afterEach(async function () { + await remove(tempRoot); + }); + + it('uninstalls basic files', async function () { + expect(!!blueprint).to.equal(true); + + await blueprint.uninstall(options); + let actualFiles = walkSync(tmpdir); + let output = ui.output.trim().split(EOL); + + expect(output.shift()).to.match(/^uninstalling/); + expect(output.shift()).to.match(/remove.* .ember-cli/); + expect(output.shift()).to.match(/remove.* .gitignore/); + expect(output.shift()).to.match(/remove.* app[/\\]basics[/\\]mock-project.txt/); + expect(output.shift()).to.match(/remove.* bar/); + expect(output.shift()).to.match(/remove.* file-to-remove.txt/); + expect(output.shift()).to.match(/remove.* foo.txt/); + expect(output.shift()).to.match(/remove.* test.txt/); + expect(output.length).to.equal(0); + + expect(actualFiles.length).to.equal(0); + + expect(existsSync(path.join(tmpdir, 'test.txt'))).to.be.false; + }); + + it("uninstall doesn't remove non-empty folders", async function () { + options.entity = { name: 'foo' }; + + await blueprint.install(options); + let actualFiles = walkSync(tmpdir); + + expect(actualFiles).to.contain('app/basics/foo.txt'); + expect(actualFiles).to.contain('app/basics/mock-project.txt'); + + await blueprint.uninstall(options); + actualFiles = walkSync(tmpdir); + + expect(actualFiles).to.not.contain('app/basics/foo.txt'); + expect(actualFiles).to.contain('app/basics/mock-project.txt'); + }); + + it("uninstall doesn't log remove messages when file does not exist", async function () { + options.entity = { name: 'does-not-exist' }; + + await blueprint.uninstall(options); + let output = ui.output.trim().split(EOL); + expect(output.shift()).to.match(/^uninstalling/); + expect(output.shift()).to.match(/remove.* .ember-cli/); + expect(output.shift()).to.match(/remove.* .gitignore/); + expect(output.shift()).to.not.match(/remove.* app[/\\]basics[/\\]does-not-exist.txt/); + }); + }); + + describe('instrumented blueprint uninstallation', function () { + let blueprint; + let ui; + let project; + let options; + let tmpdir; + + function refreshUI() { + ui = new MockUI(); + options.ui = ui; + } + + beforeEach(async function () { + const { path: dir } = await tmp.dir(); + tmpdir = dir; + blueprint = new InstrumentedBasicBlueprint(basicBlueprint); + project = new MockProject(); + options = { + project, + target: tmpdir, + }; + refreshUI(); + await blueprint.install(options); + resetCalled(); + refreshUI(); + }); + + it('calls appropriate hooks with correct arguments', async function () { + options.entity = { name: 'foo' }; + + await blueprint.uninstall(options); + expect(localsCalled).to.be.true; + expect(normalizeEntityNameCalled).to.be.true; + expect(fileMapTokensCalled).to.be.true; + expect(filesPathCalled).to.be.true; + expect(beforeUninstallCalled).to.be.true; + expect(afterUninstallCalled).to.be.true; + + expect(beforeInstallCalled).to.be.false; + expect(afterInstallCalled).to.be.false; + }); + }); + + describe('addPackageToProject', function () { + let blueprint; + + beforeEach(function () { + blueprint = new Blueprint(basicBlueprint); + }); + + it('passes a packages array for addPackagesToProject', function () { + blueprint.addPackagesToProject = function (packages) { + expect(packages).to.deep.equal([{ name: 'foo-bar' }]); + }; + + blueprint.addPackageToProject('foo-bar'); + }); + + it('passes a packages array with target for addPackagesToProject', function () { + blueprint.addPackagesToProject = function (packages) { + expect(packages).to.deep.equal([{ name: 'foo-bar', target: '^123.1.12' }]); + }; + + blueprint.addPackageToProject('foo-bar', '^123.1.12'); + }); + }); + + describe('addPackagesToProject', function () { + let blueprint; + let ui; + let NpmInstallTask; + let taskNameLookedUp; + + beforeEach(function () { + blueprint = new Blueprint(basicBlueprint); + ui = new MockUI(); + blueprint.taskFor = function (name) { + taskNameLookedUp = name; + return new NpmInstallTask(); + }; + }); + + afterEach(async function () { + await remove(tempRoot); + }); + + it('looks up the `npm-install` task', function () { + NpmInstallTask = class extends Task { + run() {} + }; + + blueprint.addPackagesToProject([{ name: 'foo-bar' }]); + + expect(taskNameLookedUp).to.equal('npm-install'); + }); + + it('calls the task with package names', function () { + let packages; + + NpmInstallTask = class extends Task { + run(options) { + packages = options.packages; + } + }; + + blueprint.addPackagesToProject([{ name: 'foo-bar' }, { name: 'bar-foo' }]); + + expect(packages).to.deep.equal(['foo-bar', 'bar-foo']); + }); + + it('calls the task with package names and versions', function () { + let packages; + + NpmInstallTask = class extends Task { + run(options) { + packages = options.packages; + } + }; + + blueprint.addPackagesToProject([ + { name: 'foo-bar', target: '^123.1.12' }, + { name: 'bar-foo', target: '0.0.7' }, + ]); + + expect(packages).to.deep.equal(['foo-bar@^123.1.12', 'bar-foo@0.0.7']); + }); + + it('writes information to the ui log for a single package', function () { + blueprint.ui = ui; + + blueprint.addPackagesToProject([{ name: 'foo-bar', target: '^123.1.12' }]); + + let output = ui.output.trim(); + + expect(output).to.match(/install package.*foo-bar/); + }); + + it('writes information to the ui log for multiple packages', function () { + blueprint.ui = ui; + + blueprint.addPackagesToProject([ + { name: 'foo-bar', target: '^123.1.12' }, + { name: 'bar-foo', target: '0.0.7' }, + ]); + + let output = ui.output.trim(); + + expect(output).to.match(/install packages.*foo-bar, bar-foo/); + }); + + it('does not error if ui is not present', function () { + delete blueprint.ui; + + blueprint.addPackagesToProject([{ name: 'foo-bar', target: '^123.1.12' }]); + + let output = ui.output.trim(); + + expect(output).to.not.match(/install package.*foo-bar/); + }); + + it('runs task with --save-dev', function () { + let saveDev; + + NpmInstallTask = class extends Task { + run(options) { + saveDev = options['save-dev']; + } + }; + + blueprint.addPackagesToProject([ + { name: 'foo-bar', target: '^123.1.12' }, + { name: 'bar-foo', target: '0.0.7' }, + ]); + + expect(!!saveDev).to.equal(true); + }); + + it('does not use verbose mode with the task', function () { + let verbose; + + NpmInstallTask = class extends Task { + run(options) { + verbose = options.verbose; + } + }; + + blueprint.addPackagesToProject([ + { name: 'foo-bar', target: '^123.1.12' }, + { name: 'bar-foo', target: '0.0.7' }, + ]); + + expect(verbose).to.equal(false); + }); + }); + + describe('removePackageFromProject', function () { + let blueprint; + let NpmUninstallTask; + let taskNameLookedUp; + let project; + + beforeEach(function () { + project = new MockProject(); + + blueprint = new Blueprint(basicBlueprint); + blueprint.project = project; + blueprint.taskFor = function (name) { + taskNameLookedUp = name; + return new NpmUninstallTask(); + }; + }); + + afterEach(async function () { + await remove(tempRoot); + }); + + it('looks up the `npm-uninstall` task', function () { + NpmUninstallTask = class extends Task { + run() {} + }; + + project.dependencies = function () { + return { + 'foo-bar': '1.0.0', + }; + }; + blueprint.removePackageFromProject({ name: 'foo-bar' }); + + expect(taskNameLookedUp).to.equal('npm-uninstall'); + }); + }); + + describe('removePackagesFromProject', function () { + let blueprint; + let ui; + let NpmUninstallTask; + let taskNameLookedUp; + let project; + + beforeEach(function () { + project = new MockProject(); + + blueprint = new Blueprint(basicBlueprint); + ui = new MockUI(); + blueprint.project = project; + blueprint.taskFor = function (name) { + taskNameLookedUp = name; + return new NpmUninstallTask(); + }; + }); + + afterEach(function () { + return remove(tempRoot); + }); + + it('looks up the `npm-uninstall` task', function () { + NpmUninstallTask = class extends Task { + run() {} + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }]); + + expect(taskNameLookedUp).to.equal('npm-uninstall'); + }); + + it('calls the task with only existing packages', function () { + let packages; + + NpmUninstallTask = class extends Task { + run(options) { + packages = options.packages; + } + }; + + project.dependencies = function () { + return { + 'foo-bar': '1.0.0', + 'bar-zoo': '2.0.0', + }; + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }, { name: 'bar-foo' }]); + + expect(packages).to.deep.equal(['foo-bar']); + }); + + it('skips uninstall if no matching package exists', function () { + let packages; + + NpmUninstallTask = class extends Task { + run(options) { + packages = options.packages; + } + }; + + project.dependencies = function () { + return { + 'foo-baz': '1.0.0', + 'bar-zoo': '2.0.0', + }; + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }, { name: 'bar-foo' }]); + + expect(packages).to.deep.equal(undefined); + }); + + it('calls the task with package names', function () { + let packages; + + NpmUninstallTask = class extends Task { + run(options) { + packages = options.packages; + } + }; + + project.dependencies = function () { + return { + 'foo-bar': '1.0.0', + 'bar-foo': '2.0.0', + }; + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }, { name: 'bar-foo' }]); + + expect(packages).to.deep.equal(['foo-bar', 'bar-foo']); + }); + + it('writes information to the ui log for a single package', function () { + blueprint.ui = ui; + + project.dependencies = function () { + return { + 'foo-bar': '1.0.0', + }; + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }]); + + let output = ui.output.trim(); + + expect(output).to.match(/uninstall package.*foo-bar/); + }); + + it('writes information to the ui log for multiple packages', function () { + blueprint.ui = ui; + + project.dependencies = function () { + return { + 'foo-bar': '1.0.0', + 'bar-foo': '2.0.0', + }; + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }, { name: 'bar-foo' }]); + + let output = ui.output.trim(); + + expect(output).to.match(/uninstall packages.*foo-bar, bar-foo/); + }); + + it('does not error if ui is not present', function () { + delete blueprint.ui; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }]); + + let output = ui.output.trim(); + + expect(output).to.not.match(/uninstall package.*foo-bar/); + }); + + it('runs task with --save-dev', function () { + let saveDev; + + NpmUninstallTask = class extends Task { + run(options) { + saveDev = options['save-dev']; + } + }; + + project.dependencies = function () { + return { + 'foo-bar': '1.0.0', + 'bar-foo': '2.0.0', + }; + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }, { name: 'bar-foo' }]); + + expect(!!saveDev).to.equal(true); + }); + + it('does not use verbose mode with the task', function () { + let verbose; + + NpmUninstallTask = class extends Task { + run(options) { + verbose = options.verbose; + } + }; + + project.dependencies = function () { + return { + 'foo-bar': '1.0.0', + 'bar-foo': '2.0.0', + }; + }; + + blueprint.removePackagesFromProject([{ name: 'foo-bar' }, { name: 'bar-foo' }]); + + expect(verbose).to.equal(false); + }); + }); + + describe('addAddonToProject', function () { + let blueprint; + + beforeEach(function () { + blueprint = new Blueprint(basicBlueprint); + }); + + afterEach(async function () { + await remove(tempRoot); + }); + + it('passes a packages array for addAddonsToProject', function () { + blueprint.addAddonsToProject = function (options) { + expect(options.packages).to.deep.equal(['foo-bar']); + }; + + blueprint.addAddonToProject('foo-bar'); + }); + + it('passes a packages array with target for addAddonsToProject', function () { + blueprint.addAddonsToProject = function (options) { + expect(options.packages).to.deep.equal([{ name: 'foo-bar', target: '^123.1.12' }]); + }; + + blueprint.addAddonToProject({ name: 'foo-bar', target: '^123.1.12' }); + }); + }); + + describe('addAddonsToProject', function () { + let blueprint; + let ui; + let AddonInstallTask; + let taskNameLookedUp; + + beforeEach(function () { + blueprint = new Blueprint(basicBlueprint); + ui = new MockUI(); + blueprint.taskFor = function (name) { + taskNameLookedUp = name; + return new AddonInstallTask(); + }; + blueprint.project = { + isEmberCLIProject() { + return true; + }, + }; + }); + + afterEach(async function () { + await remove(tempRoot); + }); + + it('looks up the `addon-install` task', function () { + AddonInstallTask = class extends Task { + run() {} + }; + + blueprint.addAddonsToProject({ packages: ['foo-bar'] }); + + expect(taskNameLookedUp).to.equal('addon-install'); + }); + + it('calls the task with package name', function () { + let pkg; + + AddonInstallTask = class extends Task { + run(options) { + pkg = options['packages']; + } + }; + + blueprint.addAddonsToProject({ packages: ['foo-bar', 'baz-bat'] }); + + expect(pkg).to.deep.equal(['foo-bar', 'baz-bat']); + }); + + it('calls the task with correctly parsed options', function () { + let pkg, args, bluOpts; + + AddonInstallTask = class extends Task { + run(options) { + pkg = options['packages']; + args = options['extraArgs']; + bluOpts = options['blueprintOptions']; + } + }; + + blueprint.addAddonsToProject({ + packages: [ + { + name: 'foo-bar', + target: '1.0.0', + }, + 'stuff-things', + 'baz-bat@0.0.1', + ], + extraArgs: ['baz'], + blueprintOptions: '-foo', + }); + + expect(pkg).to.deep.equal(['foo-bar@1.0.0', 'stuff-things', 'baz-bat@0.0.1']); + expect(args).to.deep.equal(['baz']); + expect(bluOpts).to.equal('-foo'); + }); + + it('writes information to the ui log for a single package', function () { + blueprint.ui = ui; + + blueprint.addAddonsToProject({ + packages: [ + { + name: 'foo-bar', + target: '^123.1.12', + }, + ], + }); + + let output = ui.output.trim(); + + expect(output).to.match(/install addon.*foo-bar/); + }); + + it('writes information to the ui log for multiple packages', function () { + blueprint.ui = ui; + + blueprint.addAddonsToProject({ + packages: [ + { + name: 'foo-bar', + target: '1.0.0', + }, + 'stuff-things', + 'baz-bat@0.0.1', + ], + }); + + let output = ui.output.trim(); + + expect(output).to.match(/install addons.*foo-bar@1.0.0,.*stuff-things,.*baz-bat@0.0.1/); + }); + + it('does not error if ui is not present', function () { + delete blueprint.ui; + + blueprint.addAddonsToProject({ + packages: [ + { + name: 'foo-bar', + target: '^123.1.12', + }, + ], + }); + + let output = ui.output.trim(); + + expect(output).to.not.match(/install addon.*foo-bar/); + }); + }); + + describe('load', function () { + it('loads and returns a blueprint object', function () { + let blueprint = Blueprint.load(basicBlueprint); + expect(blueprint).to.be.an('object'); + expect(blueprint.name).to.equal('basic'); + }); + + it('loads and returns a blueprint object (from esm)', function () { + let blueprint = Blueprint.load(basicEsmBlueprint); + expect(blueprint).to.be.an('object'); + expect(blueprint.name).to.equal('basic-esm'); + }); + + it('loads only blueprints with an index.js', function () { + expect(Blueprint.load(path.join(fixtureBlueprints, '.notablueprint'))).to.not.exist; + }); + }); + + describe('lookupBlueprint', function () { + let blueprint; + let tmpdir; + let project; + + beforeEach(async function () { + const { path: dir } = await tmp.dir(); + tmpdir = dir; + blueprint = new Blueprint(basicBlueprint); + project = new MockProject(); + // normally provided by `install`, but mocked here for testing + project.root = tmpdir; + blueprint.project = project; + project.blueprintLookupPaths = function () { + return [fixtureBlueprints]; + }; + }); + + afterEach(async function () { + await remove(tempRoot); + }); + + it('can lookup other Blueprints from the project blueprintLookupPaths', function () { + let result = blueprint.lookupBlueprint('basic_2'); + + expect(result.description).to.equal('Another basic blueprint'); + }); + + it('can find internal blueprints', function () { + let result = blueprint.lookupBlueprint('blueprint'); + + expect(result.description).to.equal('Generates a blueprint and definition.'); + }); + }); + + describe('._generateFileMapVariables', function () { + let blueprint; + let project; + let moduleName; + let locals; + let options; + let result; + let expectation; + + beforeEach(function () { + blueprint = new Blueprint(basicBlueprint); + project = new MockProject(); + moduleName = project.name(); + locals = {}; + + blueprint.project = project; + + options = { + project, + }; + + expectation = { + blueprintName: 'basic', + dasherizedModuleName: 'mock-project', + hasPathToken: undefined, + inAddon: false, + in: undefined, + inDummy: false, + inRepoAddon: undefined, + locals: {}, + originBlueprintName: 'basic', + pod: undefined, + podPath: '', + }; + }); + + it('should create the correct default fileMapVariables', function () { + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it('should use the moduleName method argument for moduleName', function () { + moduleName = 'foo'; + expectation.dasherizedModuleName = 'foo'; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it('should use the locals method argument for its locals value', function () { + locals = { foo: 'bar' }; + expectation.locals = locals; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it('should use the option.originBlueprintName value as its originBlueprintName if included in the options hash', function () { + options.originBlueprintName = 'foo'; + expectation.originBlueprintName = 'foo'; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it("should include a podPath if the project's podModulePrefix is defined", function () { + blueprint.project.config = function () { + return { + podModulePrefix: 'foo/bar', + }; + }; + + expectation.podPath = 'bar'; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it('should include an inAddon and inDummy flag of true if the project is an addon', function () { + options.dummy = true; + + blueprint.project.isEmberCLIAddon = function () { + return true; + }; + + expectation.inAddon = true; + expectation.inDummy = true; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it('should include an inAddon and inRepoAddon flag of true if options.inRepoAddon is true', function () { + options.inRepoAddon = true; + + expectation.inRepoAddon = true; + expectation.inAddon = true; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it('should include an in flag of true if options.in is true', function () { + options.in = true; + + expectation.in = true; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + + it('should have a hasPathToken flag of true if the blueprint hasPathToken is true', function () { + blueprint.hasPathToken = true; + + expectation.hasPathToken = true; + + result = blueprint._generateFileMapVariables(moduleName, locals, options); + + expect(result).to.eql(expectation); + }); + }); + + describe('._locals', function () { + let blueprint; + let project; + let options; + let result; + let expectation; + + beforeEach(function () { + blueprint = new Blueprint(basicBlueprint); + project = new MockProject(); + + blueprint._generateFileMapVariables = function () { + return {}; + }; + + blueprint.generateFileMap = function () { + return {}; + }; + + options = { + project, + }; + + expectation = { + camelizedModuleName: 'mockProject', + classifiedModuleName: 'MockProject', + classifiedPackageName: 'MockProject', + dasherizedModuleName: 'mock-project', + dasherizedPackageName: 'mock-project', + decamelizedModuleName: 'mock-project', + fileMap: {}, + }; + }); + + it('should return a default object if no custom options are passed', async function () { + result = await blueprint._locals(options); + + expect(result).to.deep.include(expectation); + }); + + it('it should call the locals method with the correct arguments', function () { + blueprint.locals = function (opts) { + expect(opts).to.equal(options); + }; + + blueprint._locals(options); + }); + + it('should call _generateFileMapVariables with the correct arguments', function () { + blueprint.locals = function () { + return { foo: 'bar' }; + }; + + blueprint._generateFileMapVariables = function (modName, lcls, opts) { + expect(modName).to.equal('mock-project'); + expect(lcls).to.eql({ foo: 'bar' }); + expect(opts).to.eql(opts); + }; + + blueprint._locals(options); + }); + + it('should call generateFileMap with the correct arguments', function () { + blueprint._generateFileMapVariables = function () { + return { bar: 'baz' }; + }; + + blueprint.generateFileMap = function (fileMapVariables) { + expect(fileMapVariables).to.eql({ bar: 'baz' }); + }; + + blueprint._locals(options); + }); + + it('should use the options.entity.name as its moduleName if its value is defined', async function () { + options.entity = { + name: 'foo', + }; + + expectation.camelizedModuleName = 'foo'; + expectation.classifiedModuleName = 'Foo'; + expectation.dasherizedModuleName = 'foo'; + expectation.decamelizedModuleName = 'foo'; + + result = await blueprint._locals(options); + + expect(result).to.deep.include(expectation); + }); + + it('should update its fileMap values to match the generateFileMap result', async function () { + blueprint.generateFileMap = function () { + return { foo: 'bar' }; + }; + + expectation.fileMap = { foo: 'bar' }; + + result = await blueprint._locals(options); + + expect(result).to.deep.include(expectation); + }); + + it('should return an object containing custom local values', async function () { + blueprint.locals = function () { + return { foo: 'bar' }; + }; + + expectation.foo = 'bar'; + + result = await blueprint._locals(options); + + expect(result).to.deep.include(expectation); + }); + }); +}); diff --git a/tests/integration/tasks/build-test.js b/tests/integration/tasks/build-test.js new file mode 100644 index 0000000000..316b2f180b --- /dev/null +++ b/tests/integration/tasks/build-test.js @@ -0,0 +1,100 @@ +'use strict'; + +const fs = require('fs-extra'); +const { expect } = require('chai'); +const { file } = require('chai-files'); +const walkSync = require('walk-sync'); +const BuildTask = require('../../../lib/tasks/build'); +const MockProject = require('../../helpers/mock-project'); +const MockProcess = require('../../helpers/mock-process'); +const copyFixtureFiles = require('../../helpers/copy-fixture-files'); +const willInterruptProcess = require('../../../lib/utilities/will-interrupt-process'); +let root = process.cwd(); +const tmp = require('tmp-promise'); + +describe('build task test', function () { + let project, ui, _process; + + beforeEach(async function () { + _process = new MockProcess(); + willInterruptProcess.capture(_process); + + const { path } = await tmp.dir(); + process.chdir(path); + + await copyFixtureFiles('tasks/builder'); + + project = new MockProject(); + ui = project.ui; + }); + + afterEach(function () { + willInterruptProcess.release(); + process.chdir(root); + delete process.env.BROCCOLI_VIZ; + }); + + it('can build', function () { + let outputPath = 'dist'; + let task = new BuildTask({ + project, + ui, + }); + + let runOptions = { + outputPath, + environment: 'development', + }; + + return task.run(runOptions).then(() => { + expect(walkSync(outputPath)).to.eql(['foo.txt']); + expect(file('dist/foo.txt')).to.equal('Some file named foo.txt\n'); + }); + }); + + it('generates valid visualization output', function () { + process.env.BROCCOLI_VIZ = '1'; + + let outputPath = 'dist'; + let task = new BuildTask({ + project, + ui, + }); + + let runOptions = { + outputPath, + environment: 'development', + }; + + return task.run(runOptions).then(function () { + let vizOutputPath = 'instrumentation.build.0.json'; + expect(file(vizOutputPath)).to.exist; + + // confirm it is valid json + let output = fs.readJsonSync(vizOutputPath); + expect(Object.keys(output)).to.eql(['summary', 'nodes']); + + expect(output.summary.build.type).to.equal('initial'); + expect(output.summary.buildSteps).to.equal(1); + + expect(Array.isArray(output.nodes)).to.equal(true); + }); + }); + + it('it displays environment', function () { + let outputPath = 'dist'; + let task = new BuildTask({ + project, + ui, + }); + + let runOptions = { + outputPath, + environment: 'development', + }; + + return task.run(runOptions).then(() => { + expect(ui.output).to.include('Environment: development'); + }); + }); +}); diff --git a/tests/integration/utilities/clean-remove-test.js b/tests/integration/utilities/clean-remove-test.js new file mode 100644 index 0000000000..6129354b46 --- /dev/null +++ b/tests/integration/utilities/clean-remove-test.js @@ -0,0 +1,57 @@ +'use strict'; + +const { expect } = require('chai'); +const cleanRemove = require('@ember-tooling/blueprint-model/utilities/clean-remove'); +const os = require('os'); +const path = require('path'); +const fs = require('fs-extra'); + +describe('clean-remove', function () { + let tempDir; + let originalCwd = process.cwd(); + let fileInfo; + let nestedPath = 'nested1/nested2'; + + beforeEach(function () { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'clean-remove-')); + process.chdir(tempDir); + + fileInfo = { + outputBasePath: tempDir, + }; + }); + + afterEach(function () { + process.chdir(originalCwd); + fs.removeSync(tempDir); + }); + + it('removes empty folders', async function () { + let displayPath = path.join(nestedPath, 'file.txt'); + fileInfo.outputPath = path.join(tempDir, displayPath); + fileInfo.displayPath = displayPath; + + await fs.outputFile(displayPath, ''); + let stats = await fs.stat(displayPath); + expect(stats).to.be.ok; + await cleanRemove(fileInfo); + return expect(fs.stat('nested1')).to.be.rejected; + }); + + it('preserves filled folders', async function () { + let removedDisplayPath = path.join(nestedPath, 'file.txt'); + let preservedDisplayPath = path.join(nestedPath, 'file2.txt'); + fileInfo.outputPath = path.join(tempDir, removedDisplayPath); + fileInfo.displayPath = removedDisplayPath; + + await fs.outputFile(removedDisplayPath, ''); + await fs.outputFile(preservedDisplayPath, ''); + + expect(await fs.stat(preservedDisplayPath)).to.be.ok; + + await cleanRemove(fileInfo); + await expect(fs.stat(removedDisplayPath)).to.be.rejected; + + expect(await fs.stat(preservedDisplayPath)).to.be.ok; + }); +}); diff --git a/tests/integration/utilities/deprecate-test.js b/tests/integration/utilities/deprecate-test.js new file mode 100644 index 0000000000..7142ad3e8d --- /dev/null +++ b/tests/integration/utilities/deprecate-test.js @@ -0,0 +1,36 @@ +'use strict'; + +const { resolve } = require('node:path'); +const { execaNode } = require('execa'); +const { expect } = require('chai'); + +const scriptPath = resolve(__dirname, '../../fixtures/deprecate-override/index.mjs'); + +describe('deprecate-override-remove', function () { + it('does not error by default', async function () { + // make sure we override env here so the CI job that runs the whole test suite with + // OVERRIDE_DEPRECATION_VERSION doesn't mess with the test. We need to provide some sort of + // env object plus the `extendEnv: false` for this to have the desired effect + const result = await execaNode(scriptPath, { env: { something: 'anything' }, extendEnv: false }); + expect(result.stdout).to.contain('success'); + }); + + it('does error when we override the ember-cli version', async function () { + let errorResult; + + try { + await execaNode(scriptPath, { + env: { + OVERRIDE_DEPRECATION_VERSION: '55.0.0', + }, + extendEnv: false, + }); + } catch (err) { + errorResult = err; + } + + expect(errorResult.stderr).to.contain( + 'Error: The API deprecated by deprecate-override-test was removed in ember-cli@15.0.0. The message was: you can do this for a while longer eh. Please see undefined for more details' + ); + }); +}); diff --git a/tests/runner.js b/tests/runner.js index 83cf6fe07d..8af666787f 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -1,73 +1,74 @@ 'use strict'; -var glob = require('glob'); -var Mocha = require('mocha'); -var RSVP = require('rsvp'); -var rimraf = require('rimraf'); -var mochaOnlyDetector = require('mocha-only-detector'); - -if (process.env.EOLNEWLINE) { - require('os').EOL = '\n'; -} +const path = require('path'); +const captureExit = require('capture-exit'); +captureExit.captureExit(); + +const { globSync } = require('glob'); +const Mocha = require('mocha'); +const mochaConfig = require(path.join(__dirname, '../.mocharc')); + +require('./bootstrap'); + +const { expect } = require('chai'); -rimraf.sync('.node_modules-tmp'); -rimraf.sync('.bower_components-tmp'); +const mocha = new Mocha(mochaConfig); -var root = 'tests/{unit,acceptance}'; -var _checkOnlyInTests = RSVP.denodeify(mochaOnlyDetector.checkFolder.bind(null, root + '/**/*{-test,-slow}.js')); -var optionOrFile = process.argv[2]; -var mocha = new Mocha({ - timeout: 5000, - reporter: 'spec' -}); -var testFiles = glob.sync(root + '/**/*-test.js'); -var jshintPosition = testFiles.indexOf('tests/unit/jshint-test.js'); -var jshint = testFiles.splice(jshintPosition, 1); +let root = 'tests/{unit,integration,acceptance}'; +let optionOrFile = process.argv[2]; +// default to `tap` reporter in CI otherwise default to `spec` +let testFiles = globSync(`${root}/**/*-test.js`).sort(); +let docsLintPosition = testFiles.indexOf('tests/unit/docs-lint-test.js'); +let docsLint = testFiles.splice(docsLintPosition, 1); -testFiles = jshint.concat(testFiles); +testFiles = docsLint.concat(testFiles); if (optionOrFile === 'all') { addFiles(mocha, testFiles); addFiles(mocha, '/**/*-slow.js'); -} else if (process.argv.length > 2) { +} else if (optionOrFile === 'slow') { + addFiles(mocha, '/**/*-slow.js'); +} else if (optionOrFile === 'lint') { + addFiles(mocha, docsLint); +} else if (process.argv.length > 2) { addFiles(mocha, process.argv.slice(2)); } else { addFiles(mocha, testFiles); } function addFiles(mocha, files) { - files = (typeof files === 'string') ? glob.sync(root + files) : files; + files = typeof files === 'string' ? globSync(root + files) : files; files.forEach(mocha.addFile.bind(mocha)); } -function checkOnlyInTests() { - console.log('Verifing `.only` in tests'); - return _checkOnlyInTests().then(function() { - console.log('No `.only` found'); - }); -} - function runMocha() { - mocha.run(function(failures) { - process.on('exit', function() { - process.exit(failures); - }); + let ROOT = process.cwd(); + + /* SilentErrors are used to avoid unhelpful stack traces to users but they can hide the source of test failures in + reporter output */ + process.env.SILENT_ERROR = 'verbose'; + + // ensure that at the end of every test, we are in the correct current + // working directory + mocha.suite.afterEach(function () { + expect(process.cwd()).to.equal(ROOT); }); -} -function ciVerificationStep() { - if (process.env.CI === 'true') { - return checkOnlyInTests(); - } else { - return RSVP.resolve(); - } + console.time('Mocha Tests Running Time'); + mocha.run((failures) => { + console.timeEnd('Mocha Tests Running Time'); + + // eslint-disable-next-line n/no-process-exit + process.exit(failures); + }); } -ciVerificationStep() - .then(function() { - runMocha(); - }) - .catch(function(error) { +Promise.resolve() + .then(() => runMocha()) + .catch((error) => { console.error(error); + console.error(error.stack); + + // eslint-disable-next-line n/no-process-exit process.exit(1); }); diff --git a/tests/unit/analytics-test.js b/tests/unit/analytics-test.js deleted file mode 100644 index 98e3ad6e17..0000000000 --- a/tests/unit/analytics-test.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var Command = require('../../lib/models/command'); -var MockUI = require('../helpers/mock-ui'); -var MockProject = require('../helpers/mock-project'); -var command; -var called = false; - -beforeEach(function() { - var analytics = { - track: function() { - called = true; - } - }; - - var FakeCommand = Command.extend({ - name: 'fake-command', - run: function() {} - }); - - var project = new MockProject(); - project.isEmberCLIProject = function() { return true; }; - - command = new FakeCommand({ - ui: new MockUI(), - analytics: analytics, - project: project - }); -}); - -afterEach(function() { - command = null; -}); - -describe('analytics', function() { - it('track gets invoked on command.validateAndRun()', function() { - return command.validateAndRun([]).then(function() { - expect(called, 'expected analytics.track to be called'); - }); - }); -}); diff --git a/tests/unit/blueprints/addon-test.js b/tests/unit/blueprints/addon-test.js index d9d0727a7b..180b0d95f8 100644 --- a/tests/unit/blueprints/addon-test.js +++ b/tests/unit/blueprints/addon-test.js @@ -1,52 +1,192 @@ 'use strict'; -var Blueprint = require('../../../lib/models/blueprint'); -var MockProject = require('../../helpers/mock-project'); -var expect = require('chai').expect; +const Blueprint = require('@ember-tooling/blueprint-model'); +const MockProject = require('../../helpers/mock-project'); +const { expect } = require('chai'); -describe('blueprint - addon', function(){ - describe('entityName', function(){ - var mockProject; +describe('blueprint - addon', function () { + describe('Blueprint.lookup', function () { + let blueprint; - beforeEach(function() { - mockProject = new MockProject(); - mockProject.isEmberCLIProject = function() { return true; }; + beforeEach(function () { + blueprint = Blueprint.lookup('addon'); }); - afterEach(function() { - mockProject = null; - }); + describe('entityName', function () { + let mockProject; - it('throws error when current project is an existing ember-cli project', function(){ - var blueprint = Blueprint.lookup('addon'); + beforeEach(function () { + mockProject = new MockProject(); + mockProject.isEmberCLIProject = function () { + return true; + }; - blueprint.project = mockProject; + blueprint.project = mockProject; + }); - expect(function() { - blueprint.normalizeEntityName('foo'); - }).to.throw('Generating an addon in an existing ember-cli project is not supported.'); - }); + afterEach(function () { + mockProject = null; + }); + + it('throws error when current project is an existing ember-cli project', function () { + expect(() => blueprint.normalizeEntityName('foo')).to.throw( + 'Generating an addon in an existing ember-cli project is not supported.' + ); + }); - it('works when current project is an existing ember-cli addon', function(){ - mockProject.isEmberCLIAddon = function() { return true; }; - var blueprint = Blueprint.lookup('addon'); + it('works when current project is an existing ember-cli addon', function () { + mockProject.isEmberCLIAddon = function () { + return true; + }; - blueprint.project = mockProject; + expect(() => blueprint.normalizeEntityName('foo')).not.to.throw( + 'Generating an addon in an existing ember-cli project is not supported.' + ); + }); + + it('keeps existing behavior by calling Blueprint.normalizeEntityName', function () { + expect(() => blueprint.normalizeEntityName('foo/')).to.throw(/trailing slash/); + }); + }); + }); - expect(function() { - blueprint.normalizeEntityName('foo'); - }).not.to.throw('Generating an addon in an existing ember-cli project is not supported.'); + describe('direct blueprint require', function () { + let blueprint; + beforeEach(function () { + blueprint = require('@ember-tooling/classic-build-addon-blueprint'); + blueprint.options = { + entity: { name: 'my-cool-addon' }, + }; + blueprint._appBlueprint = { + path: 'test-app-blueprint-path', + }; + blueprint.path = 'test-blueprint-path'; }); - it('keeps existing behavior by calling Blueprint.normalizeEntityName', function(){ - var blueprint = Blueprint.lookup('addon'); + describe('generatePackageJson', function () { + it('removes the `private` property', function () { + let output = blueprint.updatePackageJson(JSON.stringify({})); + + expect(JSON.parse(output).private).to.be.undefined; + }); + + it('overwrites `name`', function () { + let output = blueprint.updatePackageJson(JSON.stringify({ name: 'OMG' })); + expect(JSON.parse(output).name).to.eql('my-cool-addon'); + }); + + it('overwrites `description`', function () { + let output = blueprint.updatePackageJson(JSON.stringify({ description: 'OMG' })); + let json = JSON.parse(output); + + expect(json.description).to.equal('The default blueprint for ember-cli addons.'); + }); + + it('moves `ember-cli-babel` from devDependencies to dependencies', function () { + let output = blueprint.updatePackageJson( + JSON.stringify({ + devDependencies: { + 'ember-cli-babel': '1.0.0', + }, + }) + ); + + let json = JSON.parse(output); + expect(json.dependencies).to.deep.equal({ + 'ember-cli-babel': '1.0.0', + }); + expect(json.devDependencies).to.not.have.property('ember-cli-babel'); + }); + + it('moves `ember-cli-htmlbars` from devDependencies to dependencies', function () { + let output = blueprint.updatePackageJson( + JSON.stringify({ + devDependencies: { + 'ember-cli-htmlbars': '1.0.0', + }, + }) + ); + + let json = JSON.parse(output); + expect(json.dependencies).to.deep.equal({ + 'ember-cli-htmlbars': '1.0.0', + }); + expect(json.devDependencies).to.not.have.property('ember-cli-htmlbars'); + }); + + it('does not push multiple `ember-addon` keywords', function () { + let output = blueprint.updatePackageJson( + JSON.stringify({ + keywords: ['ember-addon'], + }) + ); + let json = JSON.parse(output); + expect(json.keywords).to.deep.equal(['ember-addon']); + }); + + it('adds `scripts.test:all`', function () { + let output = blueprint.updatePackageJson( + JSON.stringify({ + scripts: {}, + }) + ); + + let json = JSON.parse(output); + expect(json.scripts['test:ember-compatibility']).to.equal('ember try:each'); + }); + + it('overwrites `ember-addon.configPath`', function () { + let output = blueprint.updatePackageJson( + JSON.stringify({ + 'ember-addon': { + configPath: 'test-path', + }, + }) + ); + + let json = JSON.parse(output); + expect(json['ember-addon'].configPath).to.equal('tests/dummy/config'); + }); + + it('preserves dependency ordering', function () { + let output = blueprint.updatePackageJson( + JSON.stringify({ + dependencies: { + b: '1', + a: '1', + }, + devDependencies: { + b: '1', + a: '1', + }, + }) + ); + + let json = JSON.parse(output); + delete json.devDependencies['eslint-plugin-n']; + delete json.devDependencies['ember-try']; + delete json.devDependencies['ember-source-channel-url']; + delete json.devDependencies['@embroider/test-setup']; + expect(json.dependencies).to.deep.equal({ a: '1', b: '1' }); + expect(json.devDependencies).to.deep.equal({ a: '1', b: '1' }); + }); + + it('adds `ember-source` to `peerDependencies`', function () { + let output = blueprint.updatePackageJson( + JSON.stringify({ + peerDependencies: { + 'foo-bar': '^1.0.0', + }, + }) + ); - blueprint.project = mockProject; + let json = JSON.parse(output); - expect(function() { - var nonConformantComponentName = 'foo/'; - blueprint.normalizeEntityName(nonConformantComponentName); - }).to.throw(/trailing slash/); + expect(json.peerDependencies).to.deep.equal({ + 'ember-source': '>= 4.0.0', + 'foo-bar': '^1.0.0', + }); + }); }); }); }); diff --git a/tests/unit/blueprints/component-test.js b/tests/unit/blueprints/component-test.js deleted file mode 100644 index cbbdcbc859..0000000000 --- a/tests/unit/blueprints/component-test.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -var Blueprint = require('../../../lib/models/blueprint'); -var expect = require('chai').expect; - -describe('blueprint - component', function(){ - describe('entityName', function(){ - it('throws error when hyphen is not present', function(){ - var blueprint = Blueprint.lookup('component'); - - expect(function() { - var nonConformantComponentName = 'form'; - blueprint.normalizeEntityName(nonConformantComponentName); - }).to.throw(/must include a hyphen in the component name/); - }); - - - it('keeps existing behavior by calling Blueprint.normalizeEntityName', function(){ - var blueprint = Blueprint.lookup('component'); - - expect(function() { - var nonConformantComponentName = 'x-form/'; - blueprint.normalizeEntityName(nonConformantComponentName); - }).to.throw(/trailing slash/); - }); - }); -}); diff --git a/tests/unit/blueprints/in-repo-addon-test.js b/tests/unit/blueprints/in-repo-addon-test.js new file mode 100644 index 0000000000..33fd795769 --- /dev/null +++ b/tests/unit/blueprints/in-repo-addon-test.js @@ -0,0 +1,238 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const { emberDestroy, emberGenerate, emberNew, setupTestHooks } = require('ember-cli-blueprint-test-helpers/helpers'); +const td = require('testdouble'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); + +describe('Acceptance: ember generate and destroy in-repo-addon', function () { + setupTestHooks(this, { + cliPath: path.resolve(`${__dirname}/../../..`), + }); + + it('in-repo-addon fooBar', function () { + let args = ['in-repo-addon', 'fooBar']; + + return emberNew() + .then(function () { + expect(fs.readJsonSync('package.json')['ember-addon']).to.be.undefined; + }) + .then(function () { + return emberGenerate(args); + }) + .then(function () { + expect(file('lib/foo-bar/package.json')).to.exist; + expect(file('lib/foo-bar/index.js')).to.exist; + + expect(fs.readJsonSync('lib/foo-bar/package.json')).to.deep.equal({ + name: 'foo-bar', + keywords: ['ember-addon'], + }); + + expect(fs.readJsonSync('package.json')['ember-addon']).to.deep.equal({ + paths: ['lib/foo-bar'], + }); + }) + .then(function () { + return emberDestroy(args); + }) + .then(function () { + expect(file('lib/foo-bar/package.json')).to.not.exist; + expect(file('lib/foo-bar/index.js')).to.not.exist; + + expect(fs.readJsonSync('package.json')['ember-addon']).to.be.undefined; + }); + }); +}); + +describe('Unit: in-repo-addon blueprint', function () { + let blueprint; + let readJsonSync; + let writeFileSync; + let options; + + beforeEach(function () { + blueprint = require('../../../blueprints/in-repo-addon'); + blueprint.project = { + root: 'test-project-root', + }; + + options = { + entity: { + name: 'test-entity-name', + }, + }; + + readJsonSync = td.replace(blueprint, '_readJsonSync'); + writeFileSync = td.replace(blueprint, '_writeFileSync'); + }); + + afterEach(function () { + td.reset(); + }); + + it('adds to paths', function () { + td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({}); + + blueprint.afterInstall(options); + + let captor = td.matchers.captor(); + + td.verify(readJsonSync(path.normalize('test-project-root/package.json'))); + td.verify(writeFileSync(path.normalize('test-project-root/package.json'), captor.capture())); + + expect(captor.value).to.equal( + '\ +{\n\ + "ember-addon": {\n\ + "paths": [\n\ + "lib/test-entity-name"\n\ + ]\n\ + }\n\ +}\n' + ); + }); + + it('ignores if already exists', function () { + td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({ + 'ember-addon': { + paths: ['lib/test-entity-name'], + }, + }); + + blueprint.afterInstall(options); + + let captor = td.matchers.captor(); + + td.verify(readJsonSync(path.normalize('test-project-root/package.json'))); + td.verify(writeFileSync(path.normalize('test-project-root/package.json'), captor.capture())); + + expect(captor.value).to.equal( + '\ +{\n\ + "ember-addon": {\n\ + "paths": [\n\ + "lib/test-entity-name"\n\ + ]\n\ + }\n\ +}\n' + ); + }); + + it('removes from paths', function () { + td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({ + 'ember-addon': { + paths: ['lib/test-entity-name', 'lib/test-entity-name-2'], + }, + }); + + blueprint.afterUninstall(options); + + let captor = td.matchers.captor(); + + td.verify(readJsonSync(path.normalize('test-project-root/package.json'))); + td.verify(writeFileSync(path.normalize('test-project-root/package.json'), captor.capture())); + + expect(captor.value).to.equal( + '\ +{\n\ + "ember-addon": {\n\ + "paths": [\n\ + "lib/test-entity-name-2"\n\ + ]\n\ + }\n\ +}\n' + ); + }); + + it('removes the `paths` array if the removed path was the last one', function () { + td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({ + 'ember-addon': { + configPath: 'tests/dummy/config', + paths: ['lib/test-entity-name'], + }, + }); + + blueprint.afterUninstall(options); + + let captor = td.matchers.captor(); + + td.verify(readJsonSync(path.normalize('test-project-root/package.json'))); + td.verify(writeFileSync(path.normalize('test-project-root/package.json'), captor.capture())); + + expect(captor.value).to.equal(`{ + "ember-addon": { + "configPath": "tests/dummy/config" + } +} +`); + }); + + it('removes the `paths` array if it was already empty', function () { + td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({ + 'ember-addon': { + configPath: 'tests/dummy/config', + paths: [], + }, + }); + + blueprint.afterUninstall(options); + + let captor = td.matchers.captor(); + + td.verify(writeFileSync(path.normalize('test-project-root/package.json'), captor.capture())); + + expect(captor.value).to.equal(`{ + "ember-addon": { + "configPath": "tests/dummy/config" + } +} +`); + }); + + it('removes the `ember-addon` object if it is empty', function () { + td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({ + 'ember-addon': { + paths: ['lib/test-entity-name'], + }, + }); + + blueprint.afterUninstall(options); + + let captor = td.matchers.captor(); + + td.verify(writeFileSync(path.normalize('test-project-root/package.json'), captor.capture())); + + expect(captor.value).to.equal('{}\n'); + }); + + it('alphabetizes paths', function () { + td.when(readJsonSync(), { ignoreExtraArgs: true }).thenReturn({ + 'ember-addon': { + paths: ['lib/test-entity-name-2'], + }, + }); + + blueprint.afterInstall(options); + + let captor = td.matchers.captor(); + + td.verify(readJsonSync(path.normalize('test-project-root/package.json'))); + td.verify(writeFileSync(path.normalize('test-project-root/package.json'), captor.capture())); + + expect(captor.value).to.equal( + '\ +{\n\ + "ember-addon": {\n\ + "paths": [\n\ + "lib/test-entity-name",\n\ + "lib/test-entity-name-2"\n\ + ]\n\ + }\n\ +}\n' + ); + }); +}); diff --git a/tests/unit/blueprints/lib-test.js b/tests/unit/blueprints/lib-test.js new file mode 100644 index 0000000000..889acf70ab --- /dev/null +++ b/tests/unit/blueprints/lib-test.js @@ -0,0 +1,22 @@ +'use strict'; + +const path = require('path'); +const { emberGenerate, emberNew, setupTestHooks } = require('ember-cli-blueprint-test-helpers/helpers'); + +const { expect } = require('chai'); +const { dir } = require('chai-files'); + +describe('Acceptance: ember generate and destroy lib', function () { + setupTestHooks(this, { + cliPath: path.resolve(`${__dirname}/../../..`), + }); + + it('lib foo', async function () { + let args = ['lib', 'foo']; + + await emberNew(); + await emberGenerate(args); + + expect(dir('lib')).to.exist; + }); +}); diff --git a/tests/unit/blueprints/server-test.js b/tests/unit/blueprints/server-test.js new file mode 100644 index 0000000000..9e5089b0a2 --- /dev/null +++ b/tests/unit/blueprints/server-test.js @@ -0,0 +1,36 @@ +'use strict'; + +const path = require('path'); +const { emberGenerate, emberNew, setupTestHooks } = require('ember-cli-blueprint-test-helpers/helpers'); + +const { expect } = require('chai'); +const { file } = require('chai-files'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +describe('Acceptance: ember generate and destroy server', function () { + setupTestHooks(this, { + cliPath: path.resolve(`${__dirname}/../../..`), + }); + + it('server', async function () { + if (isExperimentEnabled('VITE')) { + this.skip(); + } + + let args = ['server']; + + await emberNew(); + await emberGenerate(args); + expect(file('server/index.js')).to.contain('module.exports = function(app) {'); + // TODO: assert that `morgan` and `glob` dependencies were installed + }); + + it('server throws', async function () { + if (isExperimentEnabled('VITE')) { + let args = ['server']; + + await emberNew(); + await expect(emberGenerate(args)).to.be.rejectedWith('The server blueprint is not supported in Vite projects.'); + } + }); +}); diff --git a/tests/unit/broccoli/addon/linting-test.js b/tests/unit/broccoli/addon/linting-test.js new file mode 100644 index 0000000000..d7142fe4cd --- /dev/null +++ b/tests/unit/broccoli/addon/linting-test.js @@ -0,0 +1,235 @@ +'use strict'; + +const broccoliTestHelper = require('broccoli-test-helper'); +const { expect } = require('chai'); + +const MockCLI = require('../../../helpers/mock-cli'); +const Project = require('../../../../lib/models/project'); +const Addon = require('../../../../lib/models/addon'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Addon - linting', function () { + let input, output, addon, lintTrees; + + beforeEach(async function () { + input = await createTempDir(); + let MockAddon = Addon.extend({ + name: 'first', + root: input.path(), + packageRoot: input.path(), + }); + lintTrees = []; + let cli = new MockCLI(); + let pkg = { name: 'ember-app-test' }; + + let project = new Project(input.path(), pkg, cli.ui, cli); + project.addons = [ + { + name: 'fake-linter-addon', + lintTree(type, tree) { + lintTrees.push(tree); + }, + }, + ]; + + addon = new MockAddon(project, project); + }); + + afterEach(async function () { + await input.dispose(); + await output.dispose(); + }); + + it('calls lintTree on project addons for app directory', async function () { + input.write({ + app: { + 'derp.js': '// slerpy', + }, + }); + + addon.jshintAddonTree(); + expect(lintTrees.length).to.equal(1); + + output = createBuilder(lintTrees[0]); + await output.build(); + + expect(output.read()).to.deep.equal({ + app: { + 'derp.js': '// slerpy', + }, + }); + }); + + it('calls lintTree on project addons for addon directory', async function () { + input.write({ + addon: { + 'derp.js': '// slerpy', + }, + }); + + addon.jshintAddonTree(); + expect(lintTrees.length).to.equal(2); + + output = createBuilder(lintTrees[0]); + await output.build(); + + expect(output.read()).to.deep.equal({ + addon: { + 'derp.js': '// slerpy', + }, + }); + + await output.dispose(); + + output = createBuilder(lintTrees[1]); + await output.build(); + + expect(output.read()).to.deep.equal({ + addon: { + templates: {}, + }, + }); + }); + + it('calls lintTree on project addons for addon directory with only templates', async function () { + input.write({ + addon: { + templates: { + 'foo.hbs': '{{huzzzah}}', + }, + }, + }); + + addon.jshintAddonTree(); + expect(lintTrees.length).to.equal(2); + + output = createBuilder(lintTrees[0]); + await output.build(); + + expect(output.read()).to.deep.equal({}); + + await output.dispose(); + + output = createBuilder(lintTrees[1]); + await output.build(); + + expect(output.read()).to.deep.equal({ + addon: { + templates: { + 'foo.hbs': '{{huzzzah}}', + }, + }, + }); + }); + + it('calls lintTree on project addons for addon directory with templates', async function () { + input.write({ + addon: { + 'derp.js': '// slerpy', + templates: { + 'foo.hbs': '{{huzzzah}}', + }, + }, + }); + + addon.jshintAddonTree(); + expect(lintTrees.length).to.equal(2); + + output = createBuilder(lintTrees[0]); + await output.build(); + + expect(output.read()).to.deep.equal({ + addon: { + 'derp.js': '// slerpy', + }, + }); + + await output.dispose(); + + output = createBuilder(lintTrees[1]); + await output.build(); + + expect(output.read()).to.deep.equal({ + addon: { + templates: { + 'foo.hbs': '{{huzzzah}}', + }, + }, + }); + }); + + it('calls lintTree on project addons for addon-test-support directory', async function () { + input.write({ + 'addon-test-support': { + 'derp.js': '// slerpy', + }, + }); + + addon.jshintAddonTree(); + expect(lintTrees.length).to.equal(1); + + output = createBuilder(lintTrees[0]); + await output.build(); + + expect(output.read()).to.deep.equal({ + 'addon-test-support': { + 'derp.js': '// slerpy', + }, + }); + }); + + it('calls lintTree on project addons for test-support directory', async function () { + input.write({ + 'test-support': { + 'derp.js': '// slerpy', + }, + }); + + addon.jshintAddonTree(); + expect(lintTrees.length).to.equal(1); + + output = createBuilder(lintTrees[0]); + await output.build(); + + expect(output.read()).to.deep.equal({ + 'test-support': { + 'derp.js': '// slerpy', + }, + }); + }); + + it('calls lintTree for trees in an addon', async function () { + addon.project.addons[0].lintTree = function (type, tree) { + return tree; + }; + let addonRootContents = { + app: { + 'app-foo.js': '// hoo-foo', + }, + addon: { + 'addon-bar.js': '// bar-dar', + templates: { + 'addon-templates-quux.hbs': '{{! quux-books }}', + }, + }, + 'addon-test-support': { + 'addon-test-support-baz.js': '// baz-jazz', + }, + 'test-support': { + 'test-support-qux.js': '// qux-bucks', + }, + }; + input.write(addonRootContents); + + output = createBuilder(addon.jshintAddonTree()); + await output.build(); + + expect(output.read()).to.deep.equal({ + first: { + tests: addonRootContents, + }, + }); + }); +}); diff --git a/tests/unit/broccoli/addon/module-name-test.js b/tests/unit/broccoli/addon/module-name-test.js new file mode 100644 index 0000000000..d51dded91e --- /dev/null +++ b/tests/unit/broccoli/addon/module-name-test.js @@ -0,0 +1,71 @@ +'use strict'; + +const path = require('path'); +const broccoliTestHelper = require('broccoli-test-helper'); +const { expect } = require('chai'); + +const MockCLI = require('../../../helpers/mock-cli'); +const Project = require('../../../../lib/models/project'); +const Addon = require('../../../../lib/models/addon'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Addon - moduleName', function () { + let input, output, addon; + + beforeEach(async function () { + input = await createTempDir(); + let MockAddon = Addon.extend({ + root: input.path(), + packageRoot: input.path(), + name: 'fake-addon', + moduleName() { + return 'totes-not-fake-addon'; + }, + }); + let cli = new MockCLI(); + let pkg = { name: 'ember-app-test' }; + let project = new Project(input.path(), pkg, cli.ui, cli); + + addon = new MockAddon(project, project); + + // override the registry so it just returns the input for everything + // and doesn't whine about not finding template preprocessors + addon.registry.load = () => [ + { + toTree(t) { + return t; + }, + }, + ]; + }); + + afterEach(async function () { + await input.dispose(); + await output.dispose(); + }); + + it('uses the module name function', async function () { + input.write({ + addon: { + 'herp.js': '// slerpy', + templates: { + 'derp.hbs': '', + }, + }, + }); + + output = createBuilder(addon.treeForAddon(path.join(addon.root, '/addon'))); + await output.build(); + + expect(output.read()).to.deep.equal({ + 'totes-not-fake-addon': { + 'herp.js': '// slerpy', + templates: { + 'derp.hbs': '', + }, + }, + }); + }); +}); diff --git a/tests/unit/broccoli/config-loader-test.js b/tests/unit/broccoli/config-loader-test.js deleted file mode 100644 index 2f41741dce..0000000000 --- a/tests/unit/broccoli/config-loader-test.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -var fs = require('fs-extra'); -var path = require('path'); -var ConfigLoader = require('../../../lib/broccoli/broccoli-config-loader'); -var Project = require('../../../lib/models/project'); -var Promise = require('../../../lib/ext/promise'); -var existsSync = require('exists-sync'); -var expect = require('chai').expect; -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var remove = Promise.denodeify(fs.remove); - -describe('broccoli/broccoli-config-loader', function() { - var configLoader, tmpDestDir, tmpDestDir2, tmpSrcDir, project, options, config; - - function writeConfig(config) { - var fileContents = 'module.exports = function() { return ' + JSON.stringify(config) + '; };'; - var configDir = path.join(tmpSrcDir, 'config'); - - if (!existsSync(configDir)) { - fs.mkdirSync(configDir); - } - - fs.writeFileSync(path.join(tmpSrcDir, 'config', 'environment.js'), fileContents, { encoding: 'utf8' }); - } - - beforeEach(function() { - tmpDestDir = tmp.in(tmproot); - tmpDestDir2 = tmp.in(tmproot); - tmpSrcDir = tmp.in(tmproot); - - project = new Project(tmpSrcDir, {}); - project.addons = []; - - config = { foo: 'bar', baz: 'qux' }; - writeConfig(config); - - options = { - env: 'development', - tests: true, - project: project - }; - - configLoader = new ConfigLoader('.', options); - }); - - afterEach(function() { - return Promise.all([ - remove(tmpDestDir), - remove(tmpDestDir2) - ]); - }); - - describe('clearConfigGeneratorCache', function() { - it('resets the cache', function() { - configLoader.updateCache(tmpSrcDir, tmpDestDir); - var originalConfig = fs.readFileSync(path.join(tmpDestDir, 'environments', 'development.json'), { encoding: 'utf8' }); - - config.foo = 'blammo'; - writeConfig(config); - - configLoader.updateCache(tmpSrcDir, tmpDestDir2); - var updatedConfig = fs.readFileSync(path.join(tmpDestDir2, 'environments', 'development.json'), { encoding: 'utf8' }); - - expect(originalConfig, 'config/environment.json should have been updated').to.not.equal(updatedConfig); - - expect(true, updatedConfig.match(/blammo/)); - }); - }); - - describe('updateCache', function() { - it('writes the current environments file', function() { - configLoader.updateCache(tmpSrcDir, tmpDestDir); - - expect(true, existsSync(path.join(tmpDestDir, 'environments', 'development.json'))); - expect(true, existsSync(path.join(tmpDestDir, 'environments', 'test.json'))); - }); - - it('does not generate test environment files if testing is disabled', function() { - options.tests = false; - configLoader.updateCache(tmpSrcDir, tmpDestDir); - - expect(true, existsSync(path.join(tmpDestDir, 'environments', 'development.json'))); - expect(true, !existsSync(path.join(tmpDestDir, 'environments', 'test.json'))); - }); - }); -}); diff --git a/tests/unit/broccoli/default-packager/additional-assets-test.js b/tests/unit/broccoli/default-packager/additional-assets-test.js new file mode 100644 index 0000000000..594219be4a --- /dev/null +++ b/tests/unit/broccoli/default-packager/additional-assets-test.js @@ -0,0 +1,168 @@ +'use strict'; + +const { expect } = require('chai'); +const Funnel = require('broccoli-funnel'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); +const defaultPackagerHelpers = require('../../../helpers/default-packager'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; +const setupRegistryFor = defaultPackagerHelpers.setupRegistryFor; + +describe('Default Packager: Additional Assets', function () { + let input, output; + + let MODULES = { + 'addon-tree-output': {}, + 'the-best-app-ever': { + 'router.js': 'router.js', + 'app.js': 'app.js', + components: { + 'x-foo.js': 'export default class {}', + }, + routes: { + 'application.js': 'export default class {}', + }, + config: { + 'environment.js': 'environment.js', + }, + templates: {}, + }, + vendor: { + 'font-awesome': { + fonts: { + 'FontAwesome.otf': '', + 'FontAwesome.woff': '', + }, + }, + }, + }; + let project = { + configPath() { + return `${input.path()}/the-best-app-ever/config/environment`; + }, + + config() { + return { a: 1 }; + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + addons: [], + }; + + before(async function () { + input = await createTempDir(); + + input.write(MODULES); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches packaged javascript tree', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appJsFile: '/assets/the-best-app-ever.js', + vendorJsFile: '/assets/vendor.js', + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + additionalAssetPaths: [ + { + src: 'vendor/font-awesome/fonts', + file: 'FontAwesome.otf', + dest: 'fonts', + }, + { + src: 'vendor/font-awesome/fonts', + file: 'FontAwesome.woff', + dest: 'fonts', + }, + ], + + project, + }); + + expect(defaultPackager._cachedProcessedAdditionalAssets).to.equal(null); + + output = createBuilder(defaultPackager.importAdditionalAssets(input.path())); + await output.build(); + + expect(defaultPackager._cachedProcessedAdditionalAssets).to.not.equal(null); + expect(defaultPackager._cachedProcessedAdditionalAssets._annotation).to.equal( + 'vendor/font-awesome/fonts/{FontAwesome.otf,FontAwesome.woff} => fonts/{FontAwesome.otf,FontAwesome.woff}' + ); + }); + + it('imports additional assets properly', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appJsFile: '/assets/the-best-app-ever.js', + vendorJsFile: '/assets/vendor.js', + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + additionalAssetPaths: [ + { + src: 'vendor/font-awesome/fonts', + file: 'FontAwesome.otf', + dest: 'fonts', + }, + { + src: 'vendor/font-awesome/fonts', + file: 'FontAwesome.woff', + dest: 'fonts', + }, + ], + + project, + }); + + expect(defaultPackager._cachedProcessedAdditionalAssets).to.equal(null); + + output = createBuilder(defaultPackager.importAdditionalAssets(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + fonts: { + 'FontAwesome.otf': '', + 'FontAwesome.woff': '', + }, + }); + }); +}); diff --git a/tests/unit/broccoli/default-packager/config-test.js b/tests/unit/broccoli/default-packager/config-test.js new file mode 100644 index 0000000000..2188755676 --- /dev/null +++ b/tests/unit/broccoli/default-packager/config-test.js @@ -0,0 +1,108 @@ +'use strict'; + +const { expect } = require('chai'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Default Packager: Config', function () { + let input, output; + let name = 'the-best-app-ever'; + let env = 'development'; + + let CONFIG = { + config: { + 'environment.js': '', + }, + }; + let project = { + configPath() { + return `${input.path()}/config/environment`; + }, + + config() { + return { a: 1 }; + }, + }; + + before(async function () { + input = await createTempDir(); + + input.write(CONFIG); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches packaged config tree', async function () { + let defaultPackager = new DefaultPackager({ + name, + project, + env, + }); + + expect(defaultPackager._cachedConfig).to.equal(null); + + output = createBuilder(defaultPackager.packageConfig()); + await output.build(); + + expect(defaultPackager._cachedConfig).to.not.equal(null); + expect(defaultPackager._cachedConfig._annotation).to.equal('Packaged Config'); + }); + + it('packages config files w/ tests disabled', async function () { + let defaultPackager = new DefaultPackager({ + name, + project, + env, + areTestsEnabled: false, + }); + + output = createBuilder(defaultPackager.packageConfig()); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + [name]: { + config: { + environments: { + 'development.json': '{"a":1}', + }, + }, + }, + }); + }); + + it('packages config files w/ tests enabled', async function () { + let defaultPackager = new DefaultPackager({ + name, + project, + env, + areTestsEnabled: true, + }); + + output = createBuilder(defaultPackager.packageConfig()); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + [name]: { + config: { + environments: { + 'development.json': '{"a":1}', + 'test.json': '{"a":1}', + }, + }, + }, + }); + }); +}); diff --git a/tests/unit/broccoli/default-packager/ember-cli-internal-test.js b/tests/unit/broccoli/default-packager/ember-cli-internal-test.js new file mode 100644 index 0000000000..4c586d52dd --- /dev/null +++ b/tests/unit/broccoli/default-packager/ember-cli-internal-test.js @@ -0,0 +1,239 @@ +'use strict'; + +const { expect } = require('chai'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Default Packager: Ember CLI Internal', function () { + let input, output; + + let CONFIG = { + config: { + 'environment.js': '', + }, + }; + let project = { + addons: [], + + configPath() { + return `${input.path()}/config/environment`; + }, + + config() { + return { + modulePrefix: 'the-best-app-ever', + }; + }, + }; + + before(async function () { + input = await createTempDir(); + + input.write(CONFIG); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches packaged ember cli internal tree', async function () { + let defaultPackager = new DefaultPackager({ + env: 'development', + name: 'the-best-app-ever', + project, + autoRun: false, + areTestsEnabled: true, + storeConfigInMeta: false, + }); + + expect(defaultPackager._cachedEmberCliInternalTree).to.equal(null); + + output = createBuilder(defaultPackager.packageEmberCliInternalFiles()); + await output.build(); + + expect(defaultPackager._cachedEmberCliInternalTree).to.not.equal(null); + expect(defaultPackager._cachedEmberCliInternalTree._annotation).to.equal('Packaged Ember CLI Internal Files'); + }); + + it('packages internal files properly', async function () { + let defaultPackager = new DefaultPackager({ + env: 'development', + name: 'the-best-app-ever', + project, + autoRun: false, + areTestsEnabled: true, + storeConfigInMeta: false, + }); + + expect(defaultPackager._cachedEmberCliInternalTree).to.equal(null); + + output = createBuilder(defaultPackager.packageEmberCliInternalFiles()); + await output.build(); + + let outputFiles = output.read(); + + let emberCliFiles = outputFiles.vendor['ember-cli']; + + expect(Object.keys(emberCliFiles)).to.deep.equal([ + 'app-boot.js', + 'app-config.js', + 'app-prefix.js', + 'app-suffix.js', + 'test-support-prefix.js', + 'test-support-suffix.js', + 'tests-prefix.js', + 'tests-suffix.js', + 'vendor-prefix.js', + 'vendor-suffix.js', + ]); + }); + + it('populates the contents of internal files correctly', async function () { + let defaultPackager = new DefaultPackager({ + env: 'development', + name: 'the-best-app-ever', + project, + autoRun: false, + areTestsEnabled: false, + storeConfigInMeta: false, + }); + + expect(defaultPackager._cachedEmberCliInternalTree).to.equal(null); + + output = createBuilder(defaultPackager.packageEmberCliInternalFiles()); + await output.build(); + + let outputFiles = output.read(); + + let emberCliFiles = outputFiles.vendor['ember-cli']; + + let appBootFileContent = emberCliFiles['app-boot.js'].trim(); + let appConfigFileContent = emberCliFiles['app-config.js'].trim(); + let appPrefixFileContent = emberCliFiles['app-prefix.js'].trim(); + let appSuffixFileContent = emberCliFiles['app-suffix.js'].trim(); + + let testPrefixFileContent = emberCliFiles['tests-prefix.js'].trim(); + let testSuffixFileContent = emberCliFiles['tests-suffix.js'].trim(); + let testSupportPrefixFileContent = emberCliFiles['test-support-prefix.js'].trim(); + let testSupportSuffixFileContent = emberCliFiles['test-support-suffix.js'].trim(); + + let vendorPrefixFileContent = emberCliFiles['vendor-prefix.js'].trim(); + let vendorSuffixFileContent = emberCliFiles['vendor-suffix.js'].trim(); + + expect(appBootFileContent).to.equal(''); + expect(appConfigFileContent).to.contain(`'default': {"modulePrefix":"the-best-app-ever"}`); + expect(appPrefixFileContent).to.contain(`'use strict';`); + expect(appSuffixFileContent).to.equal(''); + + expect(testPrefixFileContent).to.contain(`'use strict';`); + expect(testSuffixFileContent).to.contain( + `require('the-best-app-ever/tests/test-helper');\nEmberENV.TESTS_FILE_LOADED = true;` + ); + expect(testSupportPrefixFileContent).to.equal(''); + expect(testSupportSuffixFileContent).to.contain( + `runningTests = true;\n\nif (typeof Testem !== 'undefined' && (typeof QUnit !== 'undefined' || typeof Mocha !== 'undefined')) {\n window.Testem.hookIntoTestFramework();\n}` + ); + + expect(vendorPrefixFileContent).to.contain(`window.EmberENV = (function(EmberENV, extra) { + for (var key in extra) { + EmberENV[key] = extra[key]; + } + + return EmberENV; +})(window.EmberENV || {}, {}); + +// used to determine if the application should be booted immediately when \`app-name.js\` is evaluated +// when \`runningTests\` the \`app-name.js\` file will **not** import the applications \`app/app.js\` and +// call \`Application.create(...)\` on it. Additionally, applications can opt-out of this behavior by +// setting \`autoRun\` to \`false\` in their \`ember-cli-build.js\` +// +// The default \`test-support.js\` file will set this to \`true\` when it runs (so that Application.create() +// is not ran when running tests). +var runningTests = false;`); + + expect(vendorSuffixFileContent).to.equal(''); + }); + + it('populates the contents of internal files correctly when `storeConfigInMeta` is enabled', async function () { + let defaultPackager = new DefaultPackager({ + env: 'development', + name: 'the-best-app-ever', + project, + autoRun: false, + areTestsEnabled: true, + storeConfigInMeta: true, + }); + + expect(defaultPackager._cachedEmberCliInternalTree).to.equal(null); + + output = createBuilder(defaultPackager.packageEmberCliInternalFiles()); + await output.build(); + + let outputFiles = output.read(); + + let emberCliFiles = outputFiles.vendor['ember-cli']; + let appConfigFileContent = emberCliFiles['app-config.js'].trim(); + + expect(appConfigFileContent).to.contain(`var rawConfig = document.querySelector(`); + expect(appConfigFileContent).to.contain(`var config = JSON.parse(decodeURIComponent(rawConfig));`); + }); + + it('populates the contents of internal files correctly, including content from add-ons', async function () { + let defaultPackager = new DefaultPackager({ + env: 'development', + name: 'the-best-app-ever', + project: { + addons: [ + { + contentFor(type) { + if (type === 'test-support-prefix') { + return 'CUSTOM TEST SUPPORT PREFIX CODE'; + } + }, + }, + { + contentFor(type) { + if (type === 'app-boot') { + return 'CUSTOM APP BOOT CODE'; + } + }, + }, + ], + + configPath() { + return `${input.path()}/config/environment`; + }, + + config() { + return { + modulePrefix: 'the-best-app-ever', + }; + }, + }, + autoRun: false, + areTestsEnabled: true, + storeConfigInMeta: false, + }); + + expect(defaultPackager._cachedEmberCliInternalTree).to.equal(null); + + output = createBuilder(defaultPackager.packageEmberCliInternalFiles()); + await output.build(); + + let outputFiles = output.read(); + + let emberCliFiles = outputFiles.vendor['ember-cli']; + let appBootFileContent = emberCliFiles['app-boot.js'].trim(); + let testSupportPrefixFileContent = emberCliFiles['test-support-prefix.js'].trim(); + + expect(appBootFileContent).to.equal('CUSTOM APP BOOT CODE'); + expect(testSupportPrefixFileContent).to.equal('CUSTOM TEST SUPPORT PREFIX CODE'); + }); +}); diff --git a/tests/unit/broccoli/default-packager/external-test.js b/tests/unit/broccoli/default-packager/external-test.js new file mode 100644 index 0000000000..530c7e62a7 --- /dev/null +++ b/tests/unit/broccoli/default-packager/external-test.js @@ -0,0 +1,79 @@ +'use strict'; + +const { expect } = require('chai'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Default Packager: External', function () { + let input, output; + + let EXTERNAL = { + 'addon-tree-output': {}, + vendor: { + 'auth0-js.js': '', + 'auth0-lock.js': '', + 'auth0-lock-passwordless.js': '', + }, + }; + + before(async function () { + input = await createTempDir(); + + input.write(EXTERNAL); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('applies transforms to an external tree', async function () { + let customTransformsMap = new Map(); + + customTransformsMap.set('amd', { + files: ['vendor/auth0-js.js', 'vendor/auth0-lock.js', 'vendor/auth0-lock-passwordless.js'], + callback(tree, options) { + const stew = require('broccoli-stew'); + + return stew.map(tree, (content, relativePath) => { + const name = options[relativePath].as; + + return `${name} was transformed`; + }); + }, + processOptions() {}, + options: { + 'vendor/auth0-js.js': { + as: 'auth0', + }, + 'vendor/auth0-lock-passwordless.js': { + as: 'auth0-lock-passwordless', + }, + 'vendor/auth0-lock.js': { + as: 'auth0-lock', + }, + }, + }); + + let defaultPackager = new DefaultPackager({ + customTransformsMap, + }); + + output = createBuilder(defaultPackager.applyCustomTransforms(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles.vendor).to.deep.equal({ + 'auth0-js.js': 'auth0 was transformed', + 'auth0-lock.js': 'auth0-lock was transformed', + 'auth0-lock-passwordless.js': 'auth0-lock-passwordless was transformed', + }); + }); +}); diff --git a/tests/unit/broccoli/default-packager/index-test.js b/tests/unit/broccoli/default-packager/index-test.js new file mode 100644 index 0000000000..5c46ccd831 --- /dev/null +++ b/tests/unit/broccoli/default-packager/index-test.js @@ -0,0 +1,139 @@ +'use strict'; + +const { expect } = require('chai'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Default Packager: Index', function () { + let input, output; + + let project = { + configPath() { + return `${input.path()}/the-best-app-ever/config/environment`; + }, + + config() { + return { + rootURL: 'best-url-ever', + modulePrefix: 'the-best-app-ever', + }; + }, + + addons: [], + }; + + let META_TAG = + '/best-url-ever/'; + + before(async function () { + input = await createTempDir(); + + let indexContent = ` + {{rootURL}}{{content-for "head"}} + {{content-for "head-footer"}} + {{content-for "body"}} + {{content-for "body-footer"}} + `; + input.write({ + 'addon-tree-output': {}, + 'the-best-app-ever': { + 'router.js': 'router.js', + 'app.js': 'app.js', + 'index.html': indexContent, + config: { + 'environment.js': 'environment.js', + }, + templates: {}, + }, + }); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches processed index tree', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + autoRun: true, + storeConfigInMeta: true, + areTestsEnabled: true, + + distPaths: { + appHtmlFile: 'index.html', + }, + + project, + }); + + expect(defaultPackager._cachedProcessedIndex).to.equal(null); + + output = createBuilder(defaultPackager.processIndex(input.path())); + await output.build(); + + expect(defaultPackager._cachedProcessedIndex).to.not.equal(null); + }); + + it('works with a custom path', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + autoRun: true, + storeConfigInMeta: true, + areTestsEnabled: true, + + distPaths: { + appHtmlFile: 'custom/index.html', + }, + + project, + }); + + expect(defaultPackager._cachedProcessedIndex).to.equal(null); + + output = createBuilder(defaultPackager.processIndex(input.path())); + await output.build(); + + let outputFiles = output.read(); + let indexContent = decodeURIComponent(outputFiles.custom['index.html'].trim()); + + expect(indexContent).to.equal(META_TAG); + }); + + it('populates `index.html` according to settings', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + autoRun: true, + storeConfigInMeta: true, + areTestsEnabled: true, + + distPaths: { + appHtmlFile: 'index.html', + }, + + project, + }); + + expect(defaultPackager._cachedProcessedIndex).to.equal(null); + + output = createBuilder(defaultPackager.processIndex(input.path())); + await output.build(); + + let outputFiles = output.read(); + let indexContent = decodeURIComponent(outputFiles['index.html'].trim()); + + expect(indexContent).to.equal(META_TAG); + }); +}); diff --git a/tests/unit/broccoli/default-packager/javascript-test.js b/tests/unit/broccoli/default-packager/javascript-test.js new file mode 100644 index 0000000000..59a39072cf --- /dev/null +++ b/tests/unit/broccoli/default-packager/javascript-test.js @@ -0,0 +1,300 @@ +'use strict'; + +const { expect } = require('chai'); +const Funnel = require('broccoli-funnel'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); +const defaultPackagerHelpers = require('../../../helpers/default-packager'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; +const setupRegistryFor = defaultPackagerHelpers.setupRegistryFor; + +describe('Default Packager: Javascript', function () { + let input, output; + + let scriptOutputFiles = { + '/assets/vendor.js': [ + 'vendor/ember-cli/vendor-prefix.js', + 'vendor/loader/loader.js', + 'vendor/ember/jquery/jquery.js', + 'vendor/ember-cli-shims/app-shims.js', + 'vendor/ember-resolver/legacy-shims.js', + 'vendor/ember/ember.debug.js', + ], + }; + let MODULES = { + 'addon-tree-output': { + 'ember-ajax': { + 'request.js': '', + }, + 'ember-cli-app-version': { + 'initializer-factory.js': '', + }, + modules: { + 'ember-data': { + 'transform.js': '', + 'store.js': '', + }, + }, + }, + 'the-best-app-ever': { + 'router.js': 'router.js', + 'app.js': 'app.js', + components: { + 'x-foo.js': 'export default class {}', + }, + routes: { + 'application.js': 'export default class {}', + }, + config: { + 'environment.js': 'environment.js', + }, + templates: {}, + }, + vendor: { + loader: { + 'loader.js': '', + }, + ember: { + jquery: { + 'jquery.js': '', + }, + 'ember.debug.js': '', + }, + 'ember-cli': { + 'app-boot.js': 'app-boot.js', + 'app-config.js': 'app-config.js', + 'app-prefix.js': 'app-prefix.js', + 'app-suffix.js': 'app-suffix.js', + 'test-support-prefix.js': 'test-support-prefix.js', + 'test-support-suffix.js': 'test-support-suffix.js', + 'tests-prefix.js': 'tests-prefix.js', + 'tests-suffix.js': 'tests-suffix.js', + 'vendor-prefix.js': 'vendor-prefix.js', + 'vendor-suffix.js': 'vendor-suffix.js', + }, + 'ember-cli-shims': { + 'app-shims.js': '', + }, + 'ember-resolver': { + 'legacy-shims.js': '', + }, + }, + }; + let project = { + configPath() { + return `${input.path()}/the-best-app-ever/config/environment`; + }, + + config() { + return { a: 1 }; + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + addons: [], + }; + + before(async function () { + input = await createTempDir(); + + input.write(MODULES); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches packaged javascript tree', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appJsFile: '/assets/the-best-app-ever.js', + vendorJsFile: '/assets/vendor.js', + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + customTransformsMap: new Map(), + + scriptOutputFiles, + project, + }); + + expect(defaultPackager._cachedJavascript).to.equal(null); + + output = createBuilder(defaultPackager.packageJavascript(input.path())); + await output.build(); + + expect(defaultPackager._cachedJavascript).to.not.equal(null); + expect(defaultPackager._cachedJavascript._annotation).to.equal('Packaged Javascript'); + }); + + it('packages javascript files with sourcemaps on', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appJsFile: '/assets/the-best-app-ever.js', + vendorJsFile: '/assets/vendor.js', + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + customTransformsMap: new Map(), + + scriptOutputFiles, + project, + }); + + output = createBuilder(defaultPackager.packageJavascript(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(Object.keys(outputFiles.assets)).to.deep.equal([ + 'the-best-app-ever.js', + 'the-best-app-ever.map', + 'vendor.js', + 'vendor.map', + ]); + }); + + it('packages javascript files with sourcemaps off', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appJsFile: '/assets/the-best-app-ever.js', + vendorJsFile: '/assets/vendor.js', + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + sourcemaps: { + enabled: false, + }, + + customTransformsMap: new Map(), + + scriptOutputFiles, + project, + }); + + output = createBuilder(defaultPackager.packageJavascript(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(Object.keys(outputFiles.assets)).to.deep.equal(['the-best-app-ever.js', 'vendor.js']); + }); + + it('processes javascript according to the registry', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + + registry: setupRegistryFor('js', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/js/g, 'jsx'); + }, + }); + }), + + project: { addons: [] }, + }); + + expect(defaultPackager._cachedProcessedJavascript).to.equal(null); + + output = createBuilder(defaultPackager.processJavascript(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles['the-best-app-ever']).to.deep.equal({ + 'app.jsx': 'app.js', + components: { + 'x-foo.jsx': 'export default class {}', + }, + routes: { + 'application.jsx': 'export default class {}', + }, + config: { + 'environment.jsx': 'environment.js', + }, + 'router.jsx': 'router.js', + }); + }); + + it('runs pre/post-process add-on hooks', async function () { + let addonPreprocessTreeHookCalled = false; + let addonPostprocessTreeHookCalled = false; + + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + + registry: setupRegistryFor('js', (tree) => tree), + + // avoid using `testdouble.js` here on purpose; it does not have a "proxy" + // option, where a function call would be registered and the original + // would be returned + project: { + addons: [ + { + preprocessTree(type, tree) { + addonPreprocessTreeHookCalled = true; + + return tree; + }, + postprocessTree(type, tree) { + addonPostprocessTreeHookCalled = true; + + return tree; + }, + }, + ], + }, + }); + + expect(defaultPackager._cachedProcessedJavascript).to.equal(null); + + output = createBuilder(defaultPackager.processJavascript(input.path())); + await output.build(); + + expect(addonPreprocessTreeHookCalled).to.equal(true); + expect(addonPostprocessTreeHookCalled).to.equal(true); + }); +}); diff --git a/tests/unit/broccoli/default-packager/process-test.js b/tests/unit/broccoli/default-packager/process-test.js new file mode 100644 index 0000000000..f5d5ef0c80 --- /dev/null +++ b/tests/unit/broccoli/default-packager/process-test.js @@ -0,0 +1,124 @@ +'use strict'; + +const { expect } = require('chai'); +const Funnel = require('broccoli-funnel'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); +const defaultPackagerHelpers = require('../../../helpers/default-packager'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; +const setupRegistryFor = defaultPackagerHelpers.setupRegistryFor; + +describe('Default Packager: Process Javascript', function () { + let input, output; + + let scriptOutputFiles = { + '/assets/vendor.js': [ + 'vendor/ember-cli/vendor-prefix.js', + 'vendor/loader/loader.js', + 'vendor/ember/jquery/jquery.js', + 'vendor/ember-cli-shims/app-shims.js', + 'vendor/ember-resolver/legacy-shims.js', + 'vendor/ember/ember.debug.js', + ], + }; + let MODULES = { + 'addon-tree-output': {}, + 'the-best-app-ever': { + 'router.js': 'router.js', + 'app.js': 'app.js', + components: { + 'x-foo.js': 'export default class {}', + }, + routes: { + 'application.js': 'export default class {}', + }, + config: { + 'environment.js': 'environment.js', + }, + templates: {}, + }, + vendor: {}, + }; + + let project = { + configPath() { + return `${input.path()}/the-best-app-ever/config/environment`; + }, + + config() { + return { a: 1 }; + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + addons: [ + { + treeForAddon(tree) { + const Funnel = require('broccoli-funnel'); + return new Funnel(tree, { + destDir: 'modules/my-addon', + }); + }, + }, + ], + }; + + before(async function () { + input = await createTempDir(); + + input.write(MODULES); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + if (output) { + await output.dispose(); + } + }); + + it('caches packaged application tree', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appJsFile: '/assets/the-best-app-ever.js', + vendorJsFile: '/assets/vendor.js', + }, + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + customTransformsMap: new Map(), + + scriptOutputFiles, + project, + }); + + expect(defaultPackager._cachedProcessedAppAndDependencies).to.equal(null); + + output = createBuilder(defaultPackager.processAppAndDependencies(input.path())); + await output.build(); + + expect(defaultPackager._cachedProcessedAppAndDependencies).to.not.equal(null); + expect(defaultPackager._cachedProcessedAppAndDependencies._annotation).to.equal( + 'Processed Application and Dependencies' + ); + }); +}); diff --git a/tests/unit/broccoli/default-packager/public-test.js b/tests/unit/broccoli/default-packager/public-test.js new file mode 100644 index 0000000000..14a11efe73 --- /dev/null +++ b/tests/unit/broccoli/default-packager/public-test.js @@ -0,0 +1,59 @@ +'use strict'; + +const { expect } = require('chai'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Default Packager: Public', function () { + let input, output; + + let PUBLIC = { + public: { + images: {}, + 'ember-fetch': { + 'fastboot-fetch.js': '', + }, + 'robots.txt': '', + '500.html': '', + }, + }; + + before(async function () { + input = await createTempDir(); + + input.write(PUBLIC); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches packaged public tree', async function () { + let defaultPackager = new DefaultPackager(); + + expect(defaultPackager._cachedPublic).to.equal(null); + + output = createBuilder(defaultPackager.packagePublic(input.path())); + await output.build(); + + expect(defaultPackager._cachedPublic).to.not.equal(null); + }); + + it('packages public files', async function () { + let defaultPackager = new DefaultPackager(); + + output = createBuilder(defaultPackager.packagePublic(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal(PUBLIC.public); + }); +}); diff --git a/tests/unit/broccoli/default-packager/styles-test.js b/tests/unit/broccoli/default-packager/styles-test.js new file mode 100644 index 0000000000..ece3e64ace --- /dev/null +++ b/tests/unit/broccoli/default-packager/styles-test.js @@ -0,0 +1,257 @@ +'use strict'; + +const { expect } = require('chai'); +const Funnel = require('broccoli-funnel'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); +const defaultPackagerHelpers = require('../../../helpers/default-packager'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; +const setupRegistryFor = defaultPackagerHelpers.setupRegistryFor; + +describe('Default Packager: Styles', function () { + let input, output; + + let styleOutputFiles = { + '/assets/vendor.css': [ + 'vendor/font-awesome/css/font-awesome.css', + 'vendor/hint.css/hint.css', + 'vendor/1.css', + 'vendor/2.css', + 'vendor/3.css', + ], + }; + let MODULES = { + 'addon-tree-output': {}, + app: { + styles: { + 'app.css': '@import "extra.css";\nhtml { height: 100%; }', + 'extra.css': 'body{ position: relative; }', + }, + }, + 'the-best-app-ever': { + 'router.js': 'router.js', + 'app.js': 'app.js', + components: { + 'x-foo.js': 'export default class {}', + }, + routes: { + 'application.js': 'export default class {}', + }, + config: { + 'environment.js': 'environment.js', + }, + templates: {}, + }, + vendor: { + '1.css': '.first {}', + '2.css': '.second {}', + '3.css': '.third { position: absolute; }', + 'font-awesome': { + css: { + 'font-awesome.css': 'body { height: 100%; }', + }, + }, + 'hint.css': { + 'hint.css': '', + }, + }, + }; + + before(async function () { + input = await createTempDir(); + + input.write(MODULES); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + if (output) { + await output.dispose(); + } + }); + + it('caches packaged styles tree', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appCssFile: '/assets/the-best-app-ever.css', + vendorCssFile: '/assets/vendor.css', + }, + + registry: setupRegistryFor('css', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/scss$/g, 'css'); + }, + }); + }), + + minifyCSS: { + enabled: true, + options: { processImport: false }, + }, + + styleOutputFiles, + + project: { addons: [] }, + }); + + expect(defaultPackager._cachedProcessedStyles).to.equal(null); + + output = createBuilder(defaultPackager.packageStyles(input.path())); + await output.build(); + + expect(defaultPackager._cachedProcessedStyles).to.not.equal(null); + expect(defaultPackager._cachedProcessedStyles._annotation).to.equal('Packaged Styles'); + }); + + it('does not minify css files when minification is disabled', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appCssFile: { app: '/assets/the-best-app-ever.css' }, + vendorCssFile: '/assets/vendor.css', + }, + + registry: { + load: () => [], + }, + + minifyCSS: { + enabled: false, + options: { + processImport: false, + relativeTo: 'assets', + }, + }, + + styleOutputFiles, + + project: { addons: [] }, + }); + + expect(defaultPackager._cachedProcessedStyles).to.equal(null); + + output = createBuilder(defaultPackager.packageStyles(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(Object.keys(outputFiles.assets)).to.deep.equal(['extra.css', 'the-best-app-ever.css', 'vendor.css']); + expect(outputFiles.assets['vendor.css'].trim()).to.equal( + 'body { height: 100%; }\n\n.first {}\n.second {}\n.third { position: absolute; }' + ); + expect(outputFiles.assets['the-best-app-ever.css'].trim()).to.equal('@import "extra.css";\nhtml { height: 100%; }'); + }); + + it('processes css according to the registry', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appCssFile: { app: '/assets/the-best-app-ever.css' }, + vendorCssFile: '/assets/vendor.css', + }, + + registry: setupRegistryFor('css', function (tree, inputPath, outputPath, options) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + if (relativePath.includes('app.css')) { + return options.outputPaths.app.replace(/css$/g, 'zss'); + } + + return relativePath; + }, + }); + }), + + minifyCSS: { + enabled: true, + options: { + processImport: false, + relativeTo: 'assets', + }, + }, + + styleOutputFiles, + + project: { addons: [] }, + }); + + expect(defaultPackager._cachedProcessedStyles).to.equal(null); + + output = createBuilder(defaultPackager.packageStyles(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(Object.keys(outputFiles.assets)).to.deep.equal(['the-best-app-ever.zss', 'vendor.css']); + }); + + it('runs pre/post-process add-on hooks', async function () { + let addonPreprocessTreeHookCalled = false; + let addonPostprocessTreeHookCalled = false; + + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + env: 'development', + + distPaths: { + appCssFile: { app: '/assets/the-best-app-ever.css' }, + vendorCssFile: '/assets/vendor.css', + }, + + registry: { + load: () => [], + }, + + minifyCSS: { + enabled: true, + options: { + processImport: false, + relativeTo: 'assets', + }, + }, + + styleOutputFiles, + + // avoid using `testdouble.js` here on purpose; it does not have a "proxy" + // option, where a function call would be registered and the original + // would be returned + project: { + addons: [ + { + preprocessTree(type, tree) { + addonPreprocessTreeHookCalled = true; + + return tree; + }, + postprocessTree(type, tree) { + addonPostprocessTreeHookCalled = true; + + return tree; + }, + }, + ], + }, + }); + + expect(defaultPackager._cachedProcessedStyles).to.equal(null); + + output = createBuilder(defaultPackager.packageStyles(input.path())); + await output.build(); + + expect(addonPreprocessTreeHookCalled).to.equal(true); + expect(addonPostprocessTreeHookCalled).to.equal(true); + }); +}); diff --git a/tests/unit/broccoli/default-packager/templates-test.js b/tests/unit/broccoli/default-packager/templates-test.js new file mode 100644 index 0000000000..e191c29830 --- /dev/null +++ b/tests/unit/broccoli/default-packager/templates-test.js @@ -0,0 +1,146 @@ +'use strict'; + +const { expect } = require('chai'); +const Funnel = require('broccoli-funnel'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); +const defaultPackagerHelpers = require('../../../helpers/default-packager'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; +const setupRegistryFor = defaultPackagerHelpers.setupRegistryFor; + +describe('Default Packager: Templates', function () { + let input, output; + + let TEMPLATES = { + 'the-best-app-ever': { + templates: { + 'application.hbs': '', + 'error.hbs': '', + 'index.hbs': '', + 'loading.hbs': '', + }, + }, + }; + + before(async function () { + input = await createTempDir(); + + input.write(TEMPLATES); + }); + + after(async function () { + if (input) { + await input.dispose(); + } + }); + + afterEach(async function () { + if (output) { + await output.dispose(); + } + }); + + it('caches processed templates tree', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + project: { addons: [] }, + }); + + expect(defaultPackager._cachedProcessedTemplates).to.equal(null); + + output = createBuilder(defaultPackager.processTemplates(input.path())); + await output.build(); + + expect(defaultPackager._cachedProcessedTemplates).to.not.equal(null); + }); + + it('processes templates according to the registry', async function () { + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + project: { addons: [] }, + }); + + expect(defaultPackager._cachedProcessedTemplates).to.equal(null); + + output = createBuilder(defaultPackager.processTemplates(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles['the-best-app-ever']).to.deep.equal({ + templates: { + 'application.js': '', + 'error.js': '', + 'index.js': '', + 'loading.js': '', + }, + }); + }); + + it('runs pre/post-process add-on hooks', async function () { + let addonPreprocessTreeHookCalled = false; + let addonPostprocessTreeHookCalled = false; + + let defaultPackager = new DefaultPackager({ + name: 'the-best-app-ever', + + registry: setupRegistryFor('template', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/hbs$/g, 'js'); + }, + }); + }), + + // avoid using `testdouble.js` here on purpose; it does not have a "proxy" + // option, where a function call would be registered and the original + // would be returned + project: { + addons: [ + { + preprocessTree(type, tree) { + expect(type).to.equal('template'); + addonPreprocessTreeHookCalled = true; + + return tree; + }, + postprocessTree(type, tree) { + expect(type).to.equal('template'); + addonPostprocessTreeHookCalled = true; + + return tree; + }, + }, + ], + }, + }); + + expect(defaultPackager._cachedProcessedTemplates).to.equal(null); + + output = createBuilder(defaultPackager.processTemplates(input.path())); + await output.build(); + + expect(addonPreprocessTreeHookCalled).to.equal(true); + expect(addonPostprocessTreeHookCalled).to.equal(true); + }); +}); diff --git a/tests/unit/broccoli/default-packager/tests-test.js b/tests/unit/broccoli/default-packager/tests-test.js new file mode 100644 index 0000000000..09783b1fff --- /dev/null +++ b/tests/unit/broccoli/default-packager/tests-test.js @@ -0,0 +1,565 @@ +'use strict'; + +const stew = require('broccoli-stew'); +const Funnel = require('broccoli-funnel'); +const { expect } = require('chai'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); +const defaultPackagerHelpers = require('../../../helpers/default-packager'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; +const setupRegistryFor = defaultPackagerHelpers.setupRegistryFor; + +describe('Default Packager: Tests', function () { + let input, output; + let name = 'the-best-app-ever'; + let env = 'development'; + + let TESTS = { + 'addon-tree-output': {}, + 'the-best-app-ever': { + 'router.js': 'router.js', + 'app.js': 'app.js', + components: { + 'x-foo.js': 'export default class {}', + }, + routes: { + 'application.js': 'export default class {}', + }, + config: { + 'environment.js': 'environment.js', + }, + templates: {}, + }, + vendor: { + custom: { + 'a.js': 'a.js', + 'a.css': 'a.css', + 'b.js': 'b.js', + 'b.css': 'b.css', + }, + 'ember-cli': { + 'app-boot.js': 'app-boot.js', + 'app-config.js': 'app-config.js', + 'app-prefix.js': 'app-prefix.js', + 'app-suffix.js': 'app-suffix.js', + 'test-support-prefix.js': 'test-support-prefix.js', + 'test-support-suffix.js': 'test-support-suffix.js', + 'tests-prefix.js': 'tests-prefix.js', + 'tests-suffix.js': 'tests-suffix.js', + 'vendor-prefix.js': 'vendor-prefix.js', + 'vendor-suffix.js': 'vendor-suffix.js', + }, + }, + tests: { + 'addon-test-support': { + '@ember': { + 'test-helpers': { + 'global.js': + 'define("@ember/test-helpers/global", ["exports"], function(exports) { Object.defineProperty(exports, "__esModule", { value: true }); });', + }, + }, + }, + acceptance: { + 'login-test.js': ' // login-test.js', + 'logout-test.js': '', + }, + lint: { + 'login-test.lint.js': ' // login-test.lint.js', + 'logout-test.lint.js': '', + }, + helpers: { + 'resolver.js': '', + 'start-app.js': '', + }, + 'index.html': 'index', + integration: { + components: { + 'login-form-test.js': '', + 'user-menu-test.js': '', + }, + }, + 'test-helper.js': '// test-helper.js', + unit: { + services: { + 'session-test.js': '', + }, + }, + }, + }; + + let project = { + configPath() { + return `${input.path()}/the-best-app-ever/config/environment`; + }, + + config() { + return { + a: 1, + modulePrefix: 'the-best-app-ever', + }; + }, + + addons: [ + { + // this lintTree implementation will return the same + // files as the input tree, but the contents will be + // different + lintTree(type, tree) { + return stew.map(tree, (string) => string.toUpperCase()); + }, + }, + ], + }; + + before(async function () { + input = await createTempDir(); + + input.write(TESTS); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches packaged tests tree', async function () { + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: [], + legacyTestFilesToAppend: [], + + registry: setupRegistryFor('js', (tree) => tree), + }); + + expect(defaultPackager._cachedTests).to.equal(null); + + output = createBuilder(defaultPackager.packageTests(input.path())); + await output.build(); + + expect(defaultPackager._cachedTests).to.not.equal(null); + }); + + it('packages test files (with sourcemaps)', async function () { + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: [], + legacyTestFilesToAppend: [], + + registry: setupRegistryFor('js', (tree) => tree), + }); + + output = createBuilder(defaultPackager.packageTests(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(Object.keys(outputFiles.tests)).to.deep.equal(['index.html']); + + expect(Object.keys(outputFiles.assets)).to.deep.equal([ + 'test-support.js', + 'test-support.map', + 'tests.js', + 'tests.map', + ]); + + expect(Object.keys(outputFiles)).to.deep.equal(['assets', 'testem.js', 'tests']); + + expect(outputFiles.assets['tests.js']).to.include('login-test.js'); + expect(outputFiles.assets['tests.js']).to.include('login-test.lint.js'); + expect(outputFiles.assets['tests.js']).to.include('test-helper'); + expect(outputFiles.assets['tests.js']).to.include(`define('the-best-app-ever/config/environment'`); + expect(outputFiles.assets['tests.js']).to.include(`require('the-best-app-ever/tests/test-helper');`); + expect(outputFiles.assets['tests.js']).to.include('EmberENV.TESTS_FILE_LOADED = true;'); + }); + + it('packages test files (without sourcemaps)', async function () { + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + sourcemaps: { enabled: false }, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: [], + legacyTestFilesToAppend: [], + + registry: setupRegistryFor('js', (tree) => tree), + }); + + output = createBuilder(defaultPackager.packageTests(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(Object.keys(outputFiles.tests)).to.deep.equal(['index.html']); + + expect(Object.keys(outputFiles.assets)).to.deep.equal(['test-support.js', 'tests.js']); + + expect(Object.keys(outputFiles)).to.deep.equal(['assets', 'testem.js', 'tests']); + + expect(outputFiles.assets['tests.js']).to.include('login-test.js'); + expect(outputFiles.assets['tests.js']).to.include('login-test.lint.js'); + expect(outputFiles.assets['tests.js']).to.include('test-helper'); + expect(outputFiles.assets['tests.js']).to.include(`define('the-best-app-ever/config/environment'`); + expect(outputFiles.assets['tests.js']).to.include(`require('the-best-app-ever/tests/test-helper');`); + expect(outputFiles.assets['tests.js']).to.include('EmberENV.TESTS_FILE_LOADED = true;'); + }); + + it('does not process `addon-test-support` folder', async function () { + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: [], + legacyTestFilesToAppend: [], + + registry: setupRegistryFor('js', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/js/g, 'js-test'); + }, + }); + }), + }); + + output = createBuilder(defaultPackager.processTests(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + 'addon-test-support': { + '@ember': { + 'test-helpers': { + 'global.js': + 'define("@ember/test-helpers/global", ["exports"], function(exports) { Object.defineProperty(exports, "__esModule", { value: true }); });', + }, + }, + }, + [name]: { + tests: { + acceptance: { + 'login-test.js-test': ' // login-test.js', + 'logout-test.js-test': '', + }, + lint: { + 'login-test.lint.js-test': ' // login-test.lint.js', + 'logout-test.lint.js-test': '', + }, + helpers: { + 'resolver.js-test': '', + 'start-app.js-test': '', + }, + 'index.html': 'index', + integration: { + components: { + 'login-form-test.js-test': '', + 'user-menu-test.js-test': '', + }, + }, + 'test-helper.js-test': '// test-helper.js', + unit: { + services: { + 'session-test.js-test': '', + }, + }, + }, + }, + }); + }); + + it('processes tests files according to the registry', async function () { + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: [], + legacyTestFilesToAppend: [], + + registry: setupRegistryFor('js', function (tree) { + return new Funnel(tree, { + getDestinationPath(relativePath) { + return relativePath.replace(/js/g, 'js-test'); + }, + }); + }), + }); + + output = createBuilder(defaultPackager.processTests(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + 'addon-test-support': { + '@ember': { + 'test-helpers': { + 'global.js': + 'define("@ember/test-helpers/global", ["exports"], function(exports) { Object.defineProperty(exports, "__esModule", { value: true }); });', + }, + }, + }, + [name]: { + tests: { + acceptance: { + 'login-test.js-test': ' // login-test.js', + 'logout-test.js-test': '', + }, + lint: { + 'login-test.lint.js-test': ' // login-test.lint.js', + 'logout-test.lint.js-test': '', + }, + helpers: { + 'resolver.js-test': '', + 'start-app.js-test': '', + }, + 'index.html': 'index', + integration: { + components: { + 'login-form-test.js-test': '', + 'user-menu-test.js-test': '', + }, + }, + 'test-helper.js-test': '// test-helper.js', + unit: { + services: { + 'session-test.js-test': '', + }, + }, + }, + }, + }); + }); + + it('emits dist/assets/tests.js by default', async function () { + let emptyInput = await createTempDir(); + let emptyTestFolder = { + 'addon-tree-output': {}, + 'the-best-app-ever': { + 'router.js': 'router.js', + 'app.js': 'app.js', + components: { + 'x-foo.js': 'export default class {}', + }, + routes: { + 'application.js': 'export default class {}', + }, + config: { + 'environment.js': 'environment.js', + }, + templates: {}, + }, + vendor: { + 'ember-cli': { + 'app-boot.js': 'app-boot.js', + 'app-config.js': 'app-config.js', + 'app-prefix.js': 'app-prefix.js', + 'app-suffix.js': 'app-suffix.js', + 'test-support-prefix.js': 'test-support-prefix.js', + 'test-support-suffix.js': 'test-support-suffix.js', + 'tests-prefix.js': 'tests-prefix.js', + 'tests-suffix.js': 'tests-suffix.js', + 'vendor-prefix.js': 'vendor-prefix.js', + 'vendor-suffix.js': 'vendor-suffix.js', + }, + }, + tests: { + 'test-helper.js': '// test-helper.js', + }, + }; + + emptyInput.write(emptyTestFolder); + + let project = { + configPath() { + return `${emptyInput.path()}/the-best-app-ever/config/environment`; + }, + + config() { + return { a: 1 }; + }, + + addons: [], + }; + + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: [], + legacyTestFilesToAppend: [], + + registry: setupRegistryFor('js', (tree) => tree), + }); + + output = createBuilder(defaultPackager.packageTests(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(Object.keys(outputFiles.tests)).to.deep.equal(['index.html']); + + expect(Object.keys(outputFiles.assets)).to.deep.equal([ + 'test-support.js', + 'test-support.map', + 'tests.js', + 'tests.map', + ]); + + expect(Object.keys(outputFiles)).to.deep.equal(['assets', 'testem.js', 'tests']); + + emptyInput.dispose(); + }); + + it('lintTree results do not "win" over app tests', async function () { + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: [], + legacyTestFilesToAppend: [], + + registry: setupRegistryFor('js', (tree) => tree), + }); + + output = createBuilder(defaultPackager.packageTests(input.path())); + await output.build(); + + let outputFiles = output.read(); + + // confirm this contains the original value + // unmodified by the `lintTree` added above + expect(outputFiles.assets['tests.js']).to.include('// login-test.js'); + }); + + it('maintains the concatenation order', async function () { + let defaultPackager = new DefaultPackager({ + project, + name, + env, + areTestsEnabled: true, + + distPaths: { + testJsFile: '/assets/tests.js', + testSupportJsFile: { + testSupport: '/assets/test-support.js', + testLoader: '/assets/test-loader.js', + }, + testSupportCssFile: '/assets/test-support.css', + }, + + customTransformsMap: new Map(), + + vendorTestStaticStyles: ['vendor/custom/a.css', 'vendor/custom/b.css'], + legacyTestFilesToAppend: ['vendor/custom/a.js', 'vendor/custom/b.js'], + + registry: setupRegistryFor('js', (tree) => tree), + }); + + output = createBuilder(defaultPackager.packageTests(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles.assets['test-support.js']).to.include('a.js\nb.js'); + expect(outputFiles.assets['test-support.css']).to.include('a.css\nb.css'); + }); +}); diff --git a/tests/unit/broccoli/default-packager/vendor-test.js b/tests/unit/broccoli/default-packager/vendor-test.js new file mode 100644 index 0000000000..445d7a53bb --- /dev/null +++ b/tests/unit/broccoli/default-packager/vendor-test.js @@ -0,0 +1,83 @@ +'use strict'; + +const { expect } = require('chai'); +const DefaultPackager = require('../../../../lib/broccoli/default-packager'); +const broccoliTestHelper = require('broccoli-test-helper'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +describe('Default Packager: Vendor', function () { + let input, output; + + let VENDOR_PACKAGES = { + 'babel-polyfill': { + 'polyfill.js': 'polyfill', + 'polyfill.min.js': 'polyfill', + }, + 'ember-fetch.js': 'ember fetch', + 'ember-weakmap-passthrough.js': 'ember weakmap', + 'ember-weakmap-polyfill.js': 'ember weakmap', + 'install-getowner-polyfill.js': 'install getowner', + 'ember-cli-mirage': { + 'prentender-shim.js': 'pretender', + }, + 'ember-cli-shims': { + 'app-shims.js': 'app shims', + 'deprecations.js': 'deprecations', + }, + ember: { + 'ember.min.js': 'ember', + }, + loader: { + 'loader.js': 'loader', + }, + 'ember-resolver': { + 'legacy-shims.js': 'legacy shims', + }, + tether: { + js: { + 'tether.js': 'tether', + }, + }, + }; + + before(async function () { + input = await createTempDir(); + + input.write(VENDOR_PACKAGES); + }); + + after(async function () { + await input.dispose(); + }); + + afterEach(async function () { + await output.dispose(); + }); + + it('caches packaged vendor tree', async function () { + let defaultPackager = new DefaultPackager(); + + expect(defaultPackager._cachedVendor).to.equal(null); + + output = createBuilder(defaultPackager.packageVendor(input.path())); + await output.build(); + + expect(defaultPackager._cachedVendor).to.not.equal(null); + expect(defaultPackager._cachedVendor._annotation).to.equal('Packaged Vendor'); + }); + + it('packages vendor files', async function () { + let defaultPackager = new DefaultPackager(); + + output = createBuilder(defaultPackager.packageVendor(input.path())); + await output.build(); + + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + vendor: VENDOR_PACKAGES, + }); + }); +}); diff --git a/tests/unit/broccoli/ember-addon-test.js b/tests/unit/broccoli/ember-addon-test.js index 635d031e36..cd98595112 100644 --- a/tests/unit/broccoli/ember-addon-test.js +++ b/tests/unit/broccoli/ember-addon-test.js @@ -1,64 +1,89 @@ 'use strict'; -var path = require('path'); -var Project = require('../../../lib/models/project'); -var EmberAddon = require('../../../lib/broccoli/ember-addon'); -var expect = require('chai').expect; +const path = require('path'); +const Project = require('../../../lib/models/project'); +const EmberAddon = require('../../../lib/broccoli/ember-addon'); +const EmberApp = require('../../../lib/broccoli/ember-app'); +const { expect } = require('chai'); +const MockCLI = require('../../helpers/mock-cli'); -describe('EmberAddon', function() { - var project, emberAddon, projectPath; +const EMBER_SOURCE_ADDON = { + name: 'ember-source', + paths: { + debug: 'vendor/ember/ember.js', + prod: 'vendor/ember/ember.js', + testing: 'vendor/ember/ember-testing.js', + }, + pkg: { + name: 'ember-source', + }, +}; + +describe('EmberAddon', function () { + let project, emberAddon, projectPath; function setupProject(rootPath) { - var packageContents = require(path.join(rootPath, 'package.json')); + const packageContents = require(path.join(rootPath, 'package.json')); + let cli = new MockCLI(); - project = new Project(rootPath, packageContents); - project.require = function() { - return function() {}; + project = new Project(rootPath, packageContents, cli.ui, cli); + project.require = function () { + return function () {}; }; - project.initializeAddons = function() { - this.addons = []; + project.initializeAddons = function () { + this.addons = [EMBER_SOURCE_ADDON]; }; return project; } - beforeEach(function() { + beforeEach(function () { projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); project = setupProject(projectPath); }); - it('should merge options with defaults to depth', function() { - emberAddon = new EmberAddon({ - project: project, - foo: { - bar: ['baz'] - }, - fooz: { - bam: { - boo: ['default'] - } - } - }, { - foo: { - bar: ['bizz'] + it('should merge options with defaults to depth', function () { + emberAddon = new EmberAddon( + { + project, + foo: { + bar: ['baz'], + }, + fooz: { + bam: { + boo: ['default'], + }, + }, }, - fizz: 'fizz', - fooz: { - bam: { - boo: ['custom'] - } + { + foo: { + bar: ['bizz'], + }, + fizz: 'fizz', + fooz: { + bam: { + boo: ['custom'], + }, + }, } - }); + ); expect(emberAddon.options.foo).to.deep.eql({ - bar: ['bizz'] + bar: ['bizz'], }); expect(emberAddon.options.fizz).to.eql('fizz'); expect(emberAddon.options.fooz).to.eql({ bam: { - boo: ['custom'] - } + boo: ['custom'], + }, }); }); + it('should contain env', function () { + expect(EmberAddon.env).to.be.a('function'); + }); + + it('should contain return the correct environment', function () { + expect(EmberAddon.env()).to.eql(EmberApp.env()); + }); }); diff --git a/tests/unit/broccoli/ember-app-test.js b/tests/unit/broccoli/ember-app-test.js index 26179c2e7b..7e7e5bd92e 100644 --- a/tests/unit/broccoli/ember-app-test.js +++ b/tests/unit/broccoli/ember-app-test.js @@ -1,680 +1,1360 @@ -/* global escape */ - 'use strict'; -var fs = require('fs'); -var path = require('path'); -var Project = require('../../../lib/models/project'); -var expect = require('chai').expect; -var stub = require('../../helpers/stub').stub; -var proxyquire = require('proxyquire'); - -var mergeTreesStub; -var EmberApp = proxyquire('../../../lib/broccoli/ember-app', { - './merge-trees': function() { - return mergeTreesStub.apply(this, arguments); - } -}); - -describe('broccoli/ember-app', function() { - var project, projectPath, emberApp, addonTreesForStub, addon; +const path = require('path'); +const FixturifyProject = require('../../helpers/fixturify-project'); +const Project = require('../../../lib/models/project'); +const { expect } = require('chai'); +const td = require('testdouble'); +const broccoliTestHelper = require('broccoli-test-helper'); +const { WatchedDir, UnwatchedDir } = require('broccoli-source'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +const MockCLI = require('../../helpers/mock-cli'); +const BroccoliMergeTrees = require('broccoli-merge-trees').MergeTrees; + +let EmberApp = require('../../../lib/broccoli/ember-app'); +const Addon = require('../../../lib/models/addon'); + +function mockTemplateRegistry(app) { + let oldLoad = app.registry.load; + app.registry.load = function (type) { + if (type === 'template') { + return [ + { + toTree: (tree) => tree, + }, + ]; + } + return oldLoad.apply(app.registry, arguments); + }; +} + +const EMBER_SOURCE_ADDON = { + name: 'ember-source', + paths: { + debug: 'vendor/ember/ember.js', + prod: 'vendor/ember/ember.js', + testing: 'vendor/ember/ember-testing.js', + }, + pkg: { + name: 'ember-source', + }, +}; + +describe('EmberApp', function () { + let project, projectPath, app, addon; function setupProject(rootPath) { - var packageContents = require(path.join(rootPath, 'package.json')); + const packageContents = require(path.join(rootPath, 'package.json')); + let cli = new MockCLI(); - project = new Project(rootPath, packageContents); - project.require = function() { - return function() {}; + project = new Project(rootPath, packageContents, cli.ui, cli); + project.require = function () { + return function () {}; }; - project.initializeAddons = function() { - this.addons = []; + project.initializeAddons = function () { + this.addons = [EMBER_SOURCE_ADDON]; }; return project; } - beforeEach(function() { + beforeEach(function () { projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); project = setupProject(projectPath); - - mergeTreesStub = require('../../../lib/broccoli/merge-trees'); }); - describe('constructor', function() { - it('should override project.configPath if configPath option is specified', function() { - project.configPath = function() { return 'original value'; }; + describe('getStyles()', function () { + it('can handle empty styles folders', async function () { + let appStyles = await createTempDir(); + appStyles.write({ + 'app.css': '// css styles', + }); - new EmberApp({ - project: project, - configPath: 'custom config path' + let app = new EmberApp({ + project, + trees: { + styles: appStyles.path(), + }, + }); + + app.addonTreesFor = () => []; + + let output = createBuilder(app.getStyles()); + await output.build(); + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + app: { + styles: { + 'app.css': '// css styles', + }, + }, }); - expect(project.configPath()).to.equal('custom config path'); + await output.dispose(); }); - it('should set bowerDirectory for app', function() { - var app = new EmberApp({ - project: project + it('can handle empty addon styles folders', async function () { + let appOptions = { project }; + + let app = new EmberApp(appOptions); + + let AddonFoo = Addon.extend({ + root: 'foo', + packageRoot: 'foo', + name: 'foo', }); + let addonFoo = new AddonFoo(app, project); + app.project.addons.push(addonFoo); + + let output = createBuilder(app.getStyles()); + await output.build(); + let outputFiles = output.read(); + + let expectedOutput = {}; + expect(outputFiles).to.deep.equal(expectedOutput); - expect(app.bowerDirectory).to.equal(project.bowerDirectory); - expect(app.bowerDirectory).to.equal('bower_components'); + await output.dispose(); }); - it('should merge options with defaults to depth', function() { - var app = new EmberApp({ - project: project, - foo: { - bar: ['baz'] + it('add `app/styles` folder from add-ons', async function () { + let addonFooStyles = await createTempDir(); + + addonFooStyles.write({ + app: { + styles: { + 'foo.css': 'foo', + }, }, - fooz: { - bam: { - boo: ['default'] - } - } - }, { - foo: { - bar: ['bizz'] + }); + + let appOptions = { project }; + + let app = new EmberApp(appOptions); + + let AddonFoo = Addon.extend({ + root: 'foo', + packageRoot: 'foo', + name: 'foo', + treeForStyles() { + return addonFooStyles.path(); }, - fizz: 'fizz', - fooz: { - bam: { - boo: ['custom'] - } - } }); + let addonFoo = new AddonFoo(app, project); + app.project.addons.push(addonFoo); + + let output = createBuilder(app.getStyles()); + await output.build(); + let outputFiles = output.read(); + + let expectedOutput = { + app: { + styles: { + 'foo.css': 'foo', + }, + }, + }; + expect(outputFiles).to.deep.equal(expectedOutput); - expect(app.options.foo).to.deep.eql({ - bar: ['bizz'] + await addonFooStyles.dispose(); + await output.dispose(); + }); + + it('returns add-ons styles files', async function () { + let addonFooStyles = await createTempDir(); + let addonBarStyles = await createTempDir(); + + // `ember-basic-dropdown` + addonFooStyles.write({ + app: { + styles: { + 'foo.css': 'foo', + }, + }, }); - expect(app.options.fizz).to.eql('fizz'); - expect(app.options.fooz).to.eql({ - bam: { - boo: ['custom'] - } + // `ember-bootstrap` + addonBarStyles.write({ + baztrap: { + 'baztrap.css': '// baztrap.css', + }, }); - }); - describe('_notifyAddonIncluded', function() { - beforeEach(function() { - project.initializeAddons = function() { }; - project.addons = [{name: 'custom-addon'}]; + let app = new EmberApp({ + project, }); + app.addonTreesFor = function () { + return [addonFooStyles.path(), addonBarStyles.path()]; + }; - it('should set the app on the addons', function() { - var app = new EmberApp({ - project: project - }); + let output = createBuilder(app.getStyles()); + await output.build(); + let outputFiles = output.read(); - var addon = project.addons[0]; - expect(addon.app).to.deep.equal(app); + expect(outputFiles).to.deep.equal({ + app: { + styles: { + 'foo.css': 'foo', + }, + }, + baztrap: { + 'baztrap.css': '// baztrap.css', + }, }); + + await addonFooStyles.dispose(); + await addonBarStyles.dispose(); + await output.dispose(); + }); + + it('does not fail if add-ons do not export styles', async function () { + let app = new EmberApp({ + project, + }); + app.addonTreesFor = () => []; + + let output = createBuilder(app.getStyles()); + await output.build(); + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({}); + + await output.dispose(); }); }); - describe('contentFor', function() { - var config, defaultMatch; + describe('getPublic()', function () { + it('returns public files for app and add-ons', async function () { + let input = await createTempDir(); + let addonFooPublic = await createTempDir(); + let addonBarPublic = await createTempDir(); - beforeEach(function() { - project._addonsInitialized = true; - project.addons = []; + input.write({ + 'crossdomain.xml': '', + 'robots.txt': '', + }); + addonFooPublic.write({ + foo: 'foo', + }); + addonBarPublic.write({ + bar: 'bar', + }); - emberApp = new EmberApp({ - project: project + app = new EmberApp({ + project, }); - config = { - modulePrefix: 'cool-foo' + app.trees.public = input.path(); + app.addonTreesFor = function () { + return [addonFooPublic.path(), addonBarPublic.path()]; }; - defaultMatch = '{{content-for \'head\'}}'; + let output = createBuilder(app.getPublic()); + await output.build(); + let outputFiles = output.read(); + + expect(outputFiles).to.deep.equal({ + public: { + 'crossdomain.xml': '', + 'robots.txt': '', + foo: 'foo', + bar: 'bar', + }, + }); + + await input.dispose(); + await addonFooPublic.dispose(); + await addonBarPublic.dispose(); + await output.dispose(); }); - describe('contentFor from addons', function() { - it('calls `contentFor` on addon', function() { - var calledConfig, calledType; + it('does not fail if app or add-ons have the same `public` folder structure', async function () { + let input = await createTempDir(); + let addonFooPublic = await createTempDir(); + let addonBarPublic = await createTempDir(); + + input.write({ + 'crossdomain.xml': '', + 'robots.txt': '', + }); + addonFooPublic.write({ + bar: 'bar', + foo: 'foo', + }); + addonBarPublic.write({ + bar: 'bar', + }); - project.addons.push({ - contentFor: function(type, config) { - calledType = type; - calledConfig = config; + app = new EmberApp({ + project, + }); - return 'blammo'; - } - }); + app.trees.public = input.path(); + app.addonTreesFor = function () { + return [addonFooPublic.path(), addonBarPublic.path()]; + }; - var actual = emberApp.contentFor(config, defaultMatch, 'foo'); + let output = createBuilder(app.getPublic()); + await output.build(); + let outputFiles = output.read(); - expect(calledConfig).to.deep.equal(config); - expect(calledType).to.equal('foo'); - expect(actual).to.equal('blammo'); + expect(outputFiles).to.deep.equal({ + public: { + 'crossdomain.xml': '', + 'robots.txt': '', + foo: 'foo', + bar: 'bar', + }, }); - it('calls `contentFor` on each addon', function() { - project.addons.push({ - contentFor: function() { - return 'blammo'; - } - }); + await input.dispose(); + await addonFooPublic.dispose(); + await addonBarPublic.dispose(); + await output.dispose(); + }); + }); - project.addons.push({ - contentFor: function() { - return 'blahzorz'; - } - }); + describe('getAddonTemplates()', function () { + it('returns add-ons template files', async function () { + let input = await createTempDir(); + let addonFooTemplates = await createTempDir(); + let addonBarTemplates = await createTempDir(); + + addonFooTemplates.write({ + 'foo.hbs': 'foo', + }); + addonBarTemplates.write({ + 'bar.hbs': 'bar', + }); + + let app = new EmberApp({ + project, + }); + app.trees.templates = input.path(); + app.addonTreesFor = function () { + return [addonFooTemplates.path(), addonBarTemplates.path()]; + }; - var actual = emberApp.contentFor(config, defaultMatch, 'foo'); + let output = createBuilder(app.getAddonTemplates()); + await output.build(); + let outputFiles = output.read(); - expect(actual).to.equal('blammo\nblahzorz'); + expect(outputFiles['test-project'].templates).to.deep.equal({ + 'foo.hbs': 'foo', + 'bar.hbs': 'bar', }); + + await input.dispose(); + await addonFooTemplates.dispose(); + await addonBarTemplates.dispose(); + await output.dispose(); }); + }); - describe('contentFor("head")', function() { - it('includes the `meta` tag in `head` by default', function() { - var escapedConfig = escape(JSON.stringify(config)); - var metaExpected = ''; - var actual = emberApp.contentFor(config, defaultMatch, 'head'); + describe('getTests()', function () { + it('returns all test files `hinting` is enabled', async function () { + let input = await createTempDir(); + let addonLint = await createTempDir(); + let addonFooTestSupport = await createTempDir(); + let addonBarTestSupport = await createTempDir(); + + input.write({ + acceptance: { + 'login-test.js': '', + 'logout-test.js': '', + }, + }); + addonFooTestSupport.write({ + 'foo-helper.js': 'foo', + }); + addonBarTestSupport.write({ + 'bar-helper.js': 'bar', + }); + addonLint.write({ + 'login-test.lint.js': '', + 'logout-test.lint.js': '', + }); - expect(true, actual.indexOf(metaExpected) > -1); + let app = new EmberApp({ + project, }); + app.trees.tests = input.path(); + app.addonLintTree = (type, tree) => { + if (type === 'tests') { + return addonLint.path(); + } + + return tree; + }; + app.addonTreesFor = function (type) { + if (type === 'test-support') { + return [addonFooTestSupport.path(), addonBarTestSupport.path()]; + } - it('does not include the `meta` tag in `head` if storeConfigInMeta is false', function() { - emberApp.options.storeConfigInMeta = false; + return []; + }; - var escapedConfig = escape(JSON.stringify(config)); - var metaExpected = ''; - var actual = emberApp.contentFor(config, defaultMatch, 'head'); + let output = createBuilder(app.getTests()); + await output.build(); + let outputFiles = output.read(); - expect(true, actual.indexOf(metaExpected) === -1); + expect(outputFiles.tests).to.deep.equal({ + 'addon-test-support': {}, + lint: { + 'login-test.lint.js': '', + 'logout-test.lint.js': '', + }, + acceptance: { + 'login-test.js': '', + 'logout-test.js': '', + }, + 'foo-helper.js': 'foo', + 'bar-helper.js': 'bar', }); - it('includes the `base` tag in `head` if locationType is auto', function() { - config.locationType = 'auto'; - config.baseURL = '/'; - var expected = ''; - var actual = emberApp.contentFor(config, defaultMatch, 'head'); + await input.dispose(); + await addonFooTestSupport.dispose(); + await addonBarTestSupport.dispose(); + await addonLint.dispose(); + await output.dispose(); + }); + + it('returns test files w/o lint tests if `hinting` is disabled', async function () { + let input = await createTempDir(); + let addonFooTestSupport = await createTempDir(); + let addonBarTestSupport = await createTempDir(); + + input.write({ + acceptance: { + 'login-test.js': '', + 'logout-test.js': '', + }, + }); + addonFooTestSupport.write({ + 'foo-helper.js': 'foo', + }); + addonBarTestSupport.write({ + 'bar-helper.js': 'bar', + }); - expect(true, actual.indexOf(expected) > -1); + let app = new EmberApp({ + project, + hinting: false, }); + app.trees.tests = input.path(); + app.addonTreesFor = function (type) { + if (type === 'test-support') { + return [addonFooTestSupport.path(), addonBarTestSupport.path()]; + } - it('includes the `base` tag in `head` if locationType is none (testem requirement)', function() { - config.locationType = 'none'; - config.baseURL = '/'; - var expected = ''; - var actual = emberApp.contentFor(config, defaultMatch, 'head'); + return []; + }; - expect(true, actual.indexOf(expected) > -1); + let output = createBuilder(app.getTests()); + await output.build(); + let outputFiles = output.read(); + + expect(outputFiles.tests).to.deep.equal({ + 'addon-test-support': {}, + acceptance: { + 'login-test.js': '', + 'logout-test.js': '', + }, + 'foo-helper.js': 'foo', + 'bar-helper.js': 'bar', }); - it('does not include the `base` tag in `head` if locationType is hash', function() { - config.locationType = 'hash'; - config.baseURL = '/foo/bar'; - var expected = ''; - var actual = emberApp.contentFor(config, defaultMatch, 'head'); + await input.dispose(); + await addonFooTestSupport.dispose(); + await addonBarTestSupport.dispose(); + await output.dispose(); + }); + }); + + describe('constructor', function () { + it('should override project.configPath if configPath option is specified', function () { + project.configPath = function () { + return 'original value'; + }; + + let expected = 'custom config path'; - expect(true, actual.indexOf(expected) === -1); + new EmberApp({ + project, + configPath: expected, }); + + expect(project.configPath().slice(-expected.length)).to.equal(expected); }); - describe('contentFor("config-module")', function() { - it('includes the meta gathering snippet by default', function() { - var metaSnippetPath = path.join(__dirname, '..','..','..','lib','broccoli','app-config-from-meta.js'); - var expected = fs.readFileSync(metaSnippetPath); + it('should update project.config() if configPath option is specified', function () { + project.require = function (path) { + return () => ({ path }); + }; - var actual = emberApp.contentFor(config, defaultMatch, 'config-module'); + expect(project.config('development')).to.deep.equal({}); - expect(true, actual.indexOf(expected) > -1); + new EmberApp({ + project, + configPath: path.join('..', '..', 'app-import', 'config', 'environment'), }); - it('includes the raw config if storeConfigInMeta is false', function() { - emberApp.options.storeConfigInMeta = false; + expect(project.configPath()).to.contain(path.join('app-import', 'config', 'environment')); + }); - var expected = JSON.stringify(config); - var actual = emberApp.contentFor(config, defaultMatch, 'config-module'); + it('should merge options with defaults to depth', function () { + let app = new EmberApp( + { + project, + foo: { + bar: ['baz'], + }, + fooz: { + bam: { + boo: ['default'], + }, + }, + }, + { + foo: { + bar: ['bizz'], + }, + fizz: 'fizz', + fooz: { + bam: { + boo: ['custom'], + }, + }, + } + ); - expect(true, actual.indexOf(expected) > -1); + expect(app.options.foo).to.deep.eql({ + bar: ['bizz'], + }); + expect(app.options.fizz).to.eql('fizz'); + expect(app.options.fooz).to.eql({ + bam: { + boo: ['custom'], + }, + }); + }); + + it('should do the right thing when merging default object options', function () { + let app = new EmberApp( + { + project, + }, + { + minifyCSS: { + enabled: true, + options: { + processImport: true, + }, + }, + } + ); + + expect(app.options.minifyCSS).to.deep.equal({ + enabled: true, + options: { + processImport: true, + relativeTo: 'assets', + }, }); }); - it('has no default value other than `head`', function() { - expect(emberApp.contentFor(config, defaultMatch, 'foo')).to.equal(''); - expect(emberApp.contentFor(config, defaultMatch, 'body')).to.equal(''); - expect(emberApp.contentFor(config, defaultMatch, 'blah')).to.equal(''); + it('should watch vendor if it exists', function () { + let app = new EmberApp({ + project, + }); + + expect(app.options.trees.vendor.__broccoliGetInfo__()).to.have.property('watched', true); + }); + + describe('Addons included hook', function () { + let includedWasCalled; + let setupPreprocessorRegistryWasCalled; + let addonsAppIncluded, addonsApp; + let addon = { + name: 'custom-addon', + included() { + includedWasCalled++; + expect(setupPreprocessorRegistryWasCalled).to.eql(1); + addonsAppIncluded = this.app; + }, + + setupPreprocessorRegistry() { + expect(includedWasCalled).to.eql(0); + setupPreprocessorRegistryWasCalled++; + addonsApp = this.app; + }, + }; + + beforeEach(function () { + setupPreprocessorRegistryWasCalled = includedWasCalled = 0; + addonsApp = null; + addonsAppIncluded = null; + project.initializeAddons = function () {}; + project.addons = [EMBER_SOURCE_ADDON, addon]; + }); + + it('should set the app on the addons', function () { + expect(includedWasCalled).to.eql(0); + let app = new EmberApp({ + project, + }); + expect(includedWasCalled).to.eql(1); + expect(setupPreprocessorRegistryWasCalled).to.eql(1); + expect(addonsAppIncluded).to.eql(app); + expect(addonsApp).to.eql(app); + + let addon = project.addons[0]; + expect(addon.app).to.deep.equal(app); + }); + }); + + describe('options.babel.sourceMaps', function () { + it('disables babel sourcemaps by default', function () { + let app = new EmberApp({ + project, + }); + + expect(app.options.babel.sourceMaps).to.be.false; + }); + + it('can enable babel sourcemaps with the option', function () { + let app = new EmberApp({ + project, + babel: { + sourceMaps: 'inline', + }, + }); + + expect(app.options.babel.sourceMaps).to.equal('inline'); + }); + }); + + describe('options.fingerprint.exclude', function () { + it('excludeds testem in fingerprint exclude', function () { + let app = new EmberApp({ + project, + fingerprint: { + exclude: [], + }, + }); + + expect(app.options.fingerprint.exclude).to.include('testem'); + }); }); }); - describe('addons', function() { - describe('included hook', function() { - it('included hook is called properly on instantiation', function() { - var called = false; - var passedApp; + describe('addons', function () { + describe('included hook', function () { + it('included hook is called properly on instantiation', function () { + let called = false; + let passedApp; addon = { - included: function(app) { called = true; passedApp = app; }, - treeFor: function() { } + included(app) { + called = true; + passedApp = app; + }, + treeFor() {}, }; - project.initializeAddons = function() { - this.addons = [ addon ]; + project.initializeAddons = function () { + this.addons = [EMBER_SOURCE_ADDON, addon]; }; - emberApp = new EmberApp({ - project: project + let app = new EmberApp({ + project, }); - expect(true, called); - expect(passedApp).to.equal(emberApp); + expect(called).to.be.true; + expect(passedApp).to.equal(app); }); - it('does not throw an error if the addon does not implement `included`', function() { + it('does not throw an error if the addon does not implement `included`', function () { delete addon.included; - project.initializeAddons = function() { - this.addons = [ addon ]; + project.initializeAddons = function () { + this.addons = [EMBER_SOURCE_ADDON, addon]; }; - expect(function() { - emberApp = new EmberApp({ - project: project + expect(() => { + new EmberApp({ + project, }); }).to.not.throw(/addon must implement the `included`/); }); }); - describe('addonTreesFor', function() { - beforeEach(function() { + describe('addonTreesFor', function () { + beforeEach(function () { addon = { - included: function() { }, - treeFor: function() { } + included() {}, + treeFor() {}, }; - project.initializeAddons = function() { - this.addons = [ addon ]; + project.initializeAddons = function () { + this.addons = [EMBER_SOURCE_ADDON, addon]; }; - }); - - it('addonTreesFor returns an empty array if no addons return a tree', function() { - emberApp = new EmberApp({ - project: project + app = new EmberApp({ + project, }); - - expect(emberApp.addonTreesFor('blah')).to.deep.equal([]); }); - it('addonTreesFor calls treesFor on the addon', function() { - emberApp = new EmberApp({ - project: project - }); + it('addonTreesFor returns an empty array if no addons return a tree', function () { + expect(app.addonTreesFor('blah')).to.deep.equal([]); + }); - var sampleAddon = project.addons[0]; - var actualTreeName; + it('addonTreesFor calls treesFor on the addon', function () { + let sampleAddon = project.addons[0]; + let actualTreeName; - sampleAddon.treeFor = function(name) { + sampleAddon.treeFor = function (name) { actualTreeName = name; return 'blazorz'; }; - expect(emberApp.addonTreesFor('blah')).to.deep.equal(['blazorz']); + expect(app.addonTreesFor('blah')).to.deep.equal(['blazorz']); expect(actualTreeName).to.equal('blah'); }); - it('addonTreesFor does not throw an error if treeFor is not defined', function() { + it('addonTreesFor does not throw an error if treeFor is not defined', function () { delete addon.treeFor; - emberApp = new EmberApp({ - project: project + app = new EmberApp({ + project, }); - expect(function() { - emberApp.addonTreesFor('blah'); + expect(() => { + app.addonTreesFor('blah'); }).not.to.throw(/addon must implement the `treeFor`/); }); - describe('addonTreesFor is called properly', function() { - beforeEach(function() { - emberApp = new EmberApp({ - project: project + describe('addonTreesFor is called properly', function () { + beforeEach(function () { + app = new EmberApp({ + project, }); - addonTreesForStub = stub(emberApp, 'addonTreesFor', ['batman']); + app.addonTreesFor = td.function(); + td.when(app.addonTreesFor(), { ignoreExtraArgs: true }).thenReturn(['batman']); }); - it('_processedVendorTree calls addonTreesFor', function() { - emberApp._processedVendorTree(); + it('getAppJavascript calls addonTreesFor', function () { + app.getAppJavascript(); - expect(addonTreesForStub.calledWith[0][0]).to.equal('addon'); - expect(addonTreesForStub.calledWith[1][0]).to.equal('vendor'); - }); - - it('_processedAppTree calls addonTreesFor', function() { - emberApp._processedAppTree(); + let args = td.explain(app.addonTreesFor).calls.map(function (call) { + return call.args[0]; + }); - expect(addonTreesForStub.calledWith[0][0]).to.equal('app'); + expect(args).to.deep.equal(['app']); }); }); }); - describe('postprocessTree is called properly', function() { - var postprocessTreeStub; - beforeEach(function() { - emberApp = new EmberApp({ - project: project - }); - - postprocessTreeStub = stub(emberApp, 'addonPostprocessTree', ['batman']); - }); - - - it('styles calls addonTreesFor', function() { - emberApp.styles(); - expect(postprocessTreeStub.calledWith[0][0]).to.equal('css'); - expect(postprocessTreeStub.calledWith[0][1].description).to.equal('styles', 'should be called with consolidated tree'); + describe('toArray', function () { + it('excludes `tests` tree from resulting array if the tree is not present', function () { + app = new EmberApp({ + project, + trees: { + tests: null, + }, }); + app._defaultPackager.packageJavascript = td.function(); + app._defaultPackager.packageStyles = td.function(); - it('template type is called', function() { - var oldLoad = emberApp.registry.load; - emberApp.registry.load = function(type) { - if (type === 'template'){ - return [ - { - toTree: function() { - return { - description: 'template' - }; - } - }]; - } else { - return oldLoad.call(emberApp.registry, type); - } - }; + td.when(app._defaultPackager.packageJavascript(), { ignoreExtraArgs: true }).thenReturn('batman'); + td.when(app._defaultPackager.packageStyles(), { ignoreExtraArgs: true }).thenReturn('batman'); - emberApp._processedTemplatesTree(); - expect(postprocessTreeStub.calledWith[0][0]).to.equal('template'); - expect(postprocessTreeStub.calledWith[0][1].description).to.equal('template', 'should be called with consolidated tree'); - }); + app.toArray(); // doesn't throw an error + }); }); - describe('toTree', function() { - beforeEach(function() { + describe('toTree', function () { + beforeEach(function () { addon = { - included: function() { }, - treeFor: function() { }, - postprocessTree: function() { } + included() {}, + treeFor() {}, + postprocessTree: td.function(), }; - project.initializeAddons = function() { - this.addons = [ addon ]; + project.initializeAddons = function () { + this.addons = [EMBER_SOURCE_ADDON, addon]; }; - emberApp = new EmberApp({ - project: project + app = new EmberApp({ + project, + tests: true, + trees: { tests: {} }, }); }); - it('calls postProcessTree if defined', function() { - stub(emberApp, 'toArray', []); - stub(addon, 'postprocessTree', 'derp'); + it('calls postProcessTree if defined', function () { + app.toArray = td.function(); + app._legacyPackage = td.function(); + + td.when(app.toArray(), { ignoreExtraArgs: true }).thenReturn([]); + td.when(app._legacyPackage(), { ignoreExtraArgs: true }).thenReturn('bar'); + td.when( + addon.postprocessTree( + 'all', + td.matchers.argThat( + (t) => t.constructor === BroccoliMergeTrees && t._inputNodes.length === 1 && t._inputNodes[0] === 'bar' + ) + ) + ).thenReturn('derp'); + + expect(app.toTree()).to.equal('derp'); + }); - expect(emberApp.toTree()).to.equal('derp'); + it('calls addonPostprocessTree', function () { + app.toArray = td.function(); + app.addonPostprocessTree = td.function(); + app._legacyPackage = td.function(); + + td.when(app._legacyPackage(), { ignoreExtraArgs: true }).thenReturn('bar'); + td.when(app.toArray(), { ignoreExtraArgs: true }).thenReturn([]); + td.when( + app.addonPostprocessTree( + 'all', + td.matchers.argThat( + (t) => t.constructor === BroccoliMergeTrees && t._inputNodes.length === 1 && t._inputNodes[0] === 'bar' + ) + ) + ).thenReturn('blap'); + + expect(app.toTree()).to.equal('blap'); }); - it('calls addonPostprocessTree', function() { - stub(emberApp, 'toArray', []); - stub(emberApp, 'addonPostprocessTree', 'blap'); + it('calls each addon postprocessTree hook', function () { + mockTemplateRegistry(app); - expect(emberApp.toTree()).to.equal('blap'); - }); + app.index = td.function(); + app.getTests = td.function(); + app._defaultPackager.processTemplates = td.function(); - it('calls each addon postprocessTree hook', function() { - stub(emberApp, '_processedTemplatesTree', 'x'); - stub(addon, 'postprocessTree', 'blap'); - expect(emberApp.toTree()).to.equal('blap'); - expect( - addon.postprocessTree.calledWith.map(function(args){ - return args[0]; - }).sort() - ).to.deep.equal(['all', 'css', 'js', 'test']); - }); + td.when(app._defaultPackager.processTemplates(), { ignoreExtraArgs: true }).thenReturn('x'); + td.when(addon.postprocessTree(), { ignoreExtraArgs: true }).thenReturn('blap'); + td.when(app.index(), { ignoreExtraArgs: true }).thenReturn(null); + td.when(app.getTests(), { ignoreExtraArgs: true }).thenReturn(null); + + expect(app.toTree()).to.equal('blap'); + let args = td.explain(addon.postprocessTree).calls.map(function (call) { + return call.args[0]; + }); + + expect(args).to.deep.equal(['js', 'css', 'test', 'all']); + }); }); - describe('isEnabled is called properly', function() { - beforeEach(function() { + describe('addons can be disabled', function () { + beforeEach(function () { projectPath = path.resolve(__dirname, '../../fixtures/addon/env-addons'); - var packageContents = require(path.join(projectPath, 'package.json')); - project = new Project(projectPath, packageContents); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); + project = new (class extends Project { + initializeAddons() { + if (this._addonsInitialized) { + return; + } + + super.initializeAddons(); + + this.addons.push(EMBER_SOURCE_ADDON); + } + })(projectPath, packageContents, cli.ui, cli); }); - afterEach(function() { + afterEach(function () { process.env.EMBER_ENV = undefined; }); - describe('with environment', function() { - it('development', function() { - process.env.EMBER_ENV = 'development'; - emberApp = new EmberApp({ project: project }); + describe('isEnabled is called properly', function () { + describe('with environment', function () { + let emberFooEnvAddonFixture; + + beforeEach(function () { + emberFooEnvAddonFixture = require(path.resolve(projectPath, 'node_modules/ember-foo-env-addon/index.js')); + }); + + it('development', function () { + process.env.EMBER_ENV = 'development'; + let app = new EmberApp({ project }); - expect(emberApp.project.addons.length).to.equal(5); + emberFooEnvAddonFixture.app = app; + expect(app._addonEnabled(emberFooEnvAddonFixture)).to.be.false; + + expect(app.project.addons.length).to.equal(9); + }); + + it('foo', function () { + process.env.EMBER_ENV = 'foo'; + let app = new EmberApp({ project }); + + emberFooEnvAddonFixture.app = app; + expect(app._addonEnabled(emberFooEnvAddonFixture)).to.be.true; + + expect(app.project.addons.length).to.equal(10); + }); }); + }); - it('foo', function() { + describe('exclude', function () { + it('prevents addons to be added to the project', function () { process.env.EMBER_ENV = 'foo'; - emberApp = new EmberApp({ project: project }); - expect(emberApp.project.addons.length).to.equal(6); + let app = new EmberApp({ + project, + addons: { + exclude: ['ember-foo-env-addon'], + }, + }); + + expect(app._addonDisabledByExclude({ name: 'ember-foo-env-addon' })).to.be.true; + expect(app._addonDisabledByExclude({ name: 'Ember Random Addon' })).to.be.false; + expect(app.project.addons.length).to.equal(9); + }); + + it('throws if unavailable addon is specified', function () { + function load() { + process.env.EMBER_ENV = 'foo'; + + new EmberApp({ + project, + addons: { + exclude: ['ember-cli-self-troll'], + }, + }); + } + + expect(load).to.throw('Addon "ember-cli-self-troll" defined in "exclude" is not found'); }); }); - }); + describe('include', function () { + it('prevents non-included addons to be added to the project', function () { + process.env.EMBER_ENV = 'foo'; - describe('addonLintTree', function() { - beforeEach(function() { - addon = { }; + let app = new EmberApp({ + project, + addons: { + include: ['ember-foo-env-addon'], + }, + }); - project.initializeAddons = function() { - this.addons = [ addon ]; - }; + expect(app._addonDisabledByInclude({ name: 'ember-foo-env-addon' })).to.be.false; + expect(app._addonDisabledByInclude({ name: 'Ember Random Addon' })).to.be.true; + expect(app.project.addons.length).to.equal(1); + }); + + it('throws if unavailable addon is specified', function () { + function load() { + process.env.EMBER_ENV = 'foo'; - emberApp = new EmberApp({ - project: project + new EmberApp({ + project, + addons: { + include: ['ember-cli-self-troll'], + }, + }); + } + + expect(load).to.throw('Addon "ember-cli-self-troll" defined in "include" is not found'); }); }); - it('does not throw an error if lintTree is not defined', function() { - emberApp.addonLintTree(); + describe('exclude wins over include', function () { + it('prevents addon to be added to the project', function () { + process.env.EMBER_ENV = 'foo'; + + let app = new EmberApp({ + project, + addons: { + include: ['ember-foo-env-addon'], + exclude: ['ember-foo-env-addon'], + }, + }); + + expect(app.project.addons.length).to.equal(0); + }); }); + }); - it('calls lintTree on the addon', function() { - var actualType, actualTree; + describe('addon instance bundle caching validation (when used within the project)', function () { + let fixturifyProject; - addon.lintTree = function(type, tree) { - actualType = type; - actualTree = tree; + beforeEach(function () { + fixturifyProject = new FixturifyProject('awesome-proj', '1.0.0'); + fixturifyProject.addDevDependency('ember-cli', '*'); + }); - return 'blazorz'; - }; + afterEach(function () { + fixturifyProject.dispose(); + }); + + it('throws an error if an addon `include` is specified', function () { + fixturifyProject.addInRepoAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + fixturifyProject.addInRepoAddon('foo-bar', '1.0.0', { + callback: (inRepoAddon) => { + inRepoAddon.pkg['ember-addon'].paths = ['../foo']; + }, + }); + + fixturifyProject.writeSync(); - var assertionsWereRun; + let projectWithBundleCaching = fixturifyProject.buildProjectModel(); + projectWithBundleCaching.initializeAddons(); + projectWithBundleCaching.addons.push(EMBER_SOURCE_ADDON); - mergeTreesStub = function(inputTree, options) { - expect(inputTree).to.deep.equal(['blazorz']); - expect(options).to.deep.equal({ - overwrite: true, - annotation: 'TreeMerger (lint)' + expect(() => { + new EmberApp({ + project: projectWithBundleCaching, + addons: { + include: ['foo'], + }, }); + }).to.throw( + [ + '[ember-cli] addon bundle caching is disabled for apps that specify an addon "include"', + '', + 'All addons using bundle caching:', + projectWithBundleCaching.addons.find((addon) => addon.name === 'foo').packageRoot, + ].join('\n') + ); + }); - assertionsWereRun = true; - }; + it('throws an error if an addon `exclude` is specified', function () { + fixturifyProject.addInRepoAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + fixturifyProject.addInRepoAddon('foo-bar', '1.0.0', { + callback: (inRepoAddon) => { + inRepoAddon.pkg['ember-addon'].paths = ['../foo']; + }, + }); - emberApp.addonLintTree('blah', 'blam'); + fixturifyProject.writeSync(); - expect(actualType).to.equal('blah'); - expect(actualTree).to.equal('blam'); - expect(assertionsWereRun).to.be.true; + let projectWithBundleCaching = fixturifyProject.buildProjectModel(); + projectWithBundleCaching.initializeAddons(); + projectWithBundleCaching.addons.push(EMBER_SOURCE_ADDON); + + expect(() => { + new EmberApp({ + project: projectWithBundleCaching, + addons: { + exclude: ['foo'], + }, + }); + }).to.throw( + [ + '[ember-cli] addon bundle caching is disabled for apps that specify an addon "exclude"', + '', + 'All addons using bundle caching:', + projectWithBundleCaching.addons.find((addon) => addon.name === 'foo').packageRoot, + ].join('\n') + ); }); + }); - it('filters out tree if lintTree returns falsey', function() { - addon.lintTree = function() { - return false; + describe('addonLintTree', function () { + beforeEach(function () { + addon = { + lintTree: td.function(), }; - var assertionsWereRun; + project.initializeAddons = function () { + this.addons = [EMBER_SOURCE_ADDON, addon]; + }; - mergeTreesStub = function(inputTree) { - expect(inputTree.length).to.equal(0); + app = new EmberApp({ + project, + }); + }); - assertionsWereRun = true; - }; + it('does not throw an error if lintTree is not defined', function () { + app.addonLintTree(); + }); - emberApp.addonLintTree(); + it('calls lintTree on the addon', function () { + app.addonLintTree('blah', 'blam'); - expect(assertionsWereRun).to.be.true; + td.verify(addon.lintTree('blah', 'blam')); }); }); }); - describe('import', function() { - it('appends dependencies', function() { - emberApp = new EmberApp({ + describe('import', function () { + beforeEach(function () { + app = new EmberApp({ + project, }); - emberApp.import('vendor/moment.js', {type: 'vendor'}); - expect(emberApp.legacyFilesToAppend.indexOf('vendor/moment.js')).to.equal(emberApp.legacyFilesToAppend.length - 1); }); - it('prepends dependencies', function() { - emberApp = new EmberApp({ - }); - emberApp.import('vendor/es5-shim.js', {type: 'vendor', prepend: true}); - expect(emberApp.legacyFilesToAppend.indexOf('vendor/es5-shim.js')).to.equal(0); + + afterEach(function () { + process.env.EMBER_ENV = undefined; }); - it('defaults to development if production is not set', function() { + + it('appends dependencies to vendor by default', function () { + app.import('vendor/moment.js'); + let outputFile = app._scriptOutputFiles['/assets/vendor.js']; + + expect(outputFile).to.be.instanceof(Array); + expect(outputFile.indexOf('vendor/moment.js')).to.equal(outputFile.length - 1); + }); + it('appends dependencies', function () { + app.import('vendor/moment.js', { type: 'vendor' }); + + let outputFile = app._scriptOutputFiles['/assets/vendor.js']; + + expect(outputFile).to.be.instanceof(Array); + expect(outputFile.indexOf('vendor/moment.js')).to.equal(outputFile.length - 1); + }); + + it('prepends dependencies', function () { + app.import('vendor/es5-shim.js', { type: 'vendor', prepend: true }); + + let outputFile = app._scriptOutputFiles['/assets/vendor.js']; + + expect(outputFile).to.be.instanceof(Array); + expect(outputFile.indexOf('vendor/es5-shim.js')).to.equal(0); + }); + + it('prepends dependencies to outputFile', function () { + app.import('vendor/moment.js', { outputFile: 'moment.js', prepend: true }); + + let outputFile = app._scriptOutputFiles['moment.js']; + + expect(outputFile).to.be.instanceof(Array); + expect(outputFile.indexOf('vendor/moment.js')).to.equal(0); + }); + + it('appends dependencies to outputFile', function () { + app.import('vendor/moment.js', { outputFile: 'moment.js' }); + + let outputFile = app._scriptOutputFiles['moment.js']; + + expect(outputFile).to.be.instanceof(Array); + expect(outputFile.indexOf('vendor/moment.js')).to.equal(outputFile.length - 1); + }); + + it('defaults to development if production is not set', function () { process.env.EMBER_ENV = 'production'; - emberApp = new EmberApp({ - }); - emberApp.import({ - 'development': 'vendor/jquery.js' + app.import({ + development: 'vendor/jquery.js', }); - expect(emberApp.legacyFilesToAppend.indexOf('vendor/jquery.js')).to.equal(emberApp.legacyFilesToAppend.length -1); - process.env.EMBER_ENV = undefined; + let outputFile = app._scriptOutputFiles['/assets/vendor.js']; + expect(outputFile.indexOf('vendor/jquery.js')).to.equal(outputFile.length - 1); }); - it('honors explicitly set to null in environment', function() { + + it('honors explicitly set to null in environment', function () { process.env.EMBER_ENV = 'production'; - emberApp = new EmberApp({ + // set EMBER_ENV before creating the project + + app = new EmberApp({ + project, }); - emberApp.import({ - 'development': 'vendor/jquery.js', - 'production': null + + app.import({ + development: 'vendor/jquery.js', + production: null, }); - expect(emberApp.legacyFilesToAppend.indexOf('vendor/jquery.js')).to.equal(-1); - process.env.EMBER_ENV = undefined; + + expect(app._scriptOutputFiles['/assets/vendor.js']).to.not.contain('vendor/jquery.js'); }); - }); - describe('vendorFiles', function() { - var defaultVendorFiles = [ - 'loader.js', 'jquery.js', 'ember.js', - 'app-shims.js', 'ember-resolver.js', 'ember-load-initializers.js' - ]; + it('normalizes asset path correctly', function () { + app.import('vendor\\path\\to\\lib.js', { type: 'vendor' }); + app.import('vendor/path/to/lib2.js', { type: 'vendor' }); - describe('handlebars.js', function() { - it('does not app.import handlebars if not present in bower.json', function() { - var app = new EmberApp({ - project: project - }); + expect(app._scriptOutputFiles['/assets/vendor.js']).to.contain('vendor/path/to/lib.js'); + expect(app._scriptOutputFiles['/assets/vendor.js']).to.contain('vendor/path/to/lib2.js'); + }); - expect(app.vendorFiles).not.to.include.keys('handlebars.js'); - }); + it('option.using throws exception given invalid inputs', function () { + // `using` is looped over if given, we should ensure this throws an exception with proper error message + expect(() => { + app.import('vendor/path/to/lib1.js', { using: 1 }); + }).to.throw(/You must pass an array of transformations for `using` option/); - it('includes handlebars if present in bower.json', function() { - projectPath = path.resolve(__dirname, '../../fixtures/project-with-handlebars'); - project = setupProject(projectPath); + expect(() => { + app.import('vendor/path/to/lib2.js', { using: 'foop' }); + }).to.throw(/You must pass an array of transformations for `using` option/); - var app = new EmberApp({ - project: project - }); + expect(() => { + app.import('vendor/path/to/lib3.js', { using: [1] }); + }).to.throw(/list must have a `transformation` name/); - expect(app.vendorFiles).to.include.keys('handlebars.js'); - }); + expect(() => { + app.import('vendor/path/to/lib3.js', { using: [{ foo: 'bar' }] }); + }).to.throw(/list must have a `transformation` name/); + }); + }); - it('includes handlebars if present in provided `vendorFiles`', function() { - var app = new EmberApp({ - project: project, - vendorFiles: { - 'handlebars.js': 'some/path/whatever.js' - } - }); + describe('vendorFiles', function () { + let defaultVendorFiles = ['ember.js', 'ember-testing.js']; - expect(app.vendorFiles).to.include.keys('handlebars.js'); + it('defines vendorFiles by default', function () { + app = new EmberApp({ + project, }); + expect(Object.keys(app.vendorFiles)).to.deep.equal(defaultVendorFiles); }); - it('defines vendorFiles by default', function() { - emberApp = new EmberApp(); - expect(Object.keys(emberApp.vendorFiles)).to.deep.equal(defaultVendorFiles); - }); + it('redefines a location of a vendor asset', function () { + app = new EmberApp({ + project, - it('redefines a location of a vendor asset', function() { - emberApp = new EmberApp({ vendorFiles: { - 'ember.js': 'vendor/ember.js' - } + 'ember.js': 'vendor/ember.js', + }, }); - expect(emberApp.vendorFiles['ember.js']).to.equal('vendor/ember.js'); + expect(app.vendorFiles['ember.js']).to.equal('vendor/ember.js'); }); - it('defines vendorFiles in order even when option for it is passed', function() { - emberApp = new EmberApp({ + it('defines vendorFiles in order even when option for it is passed', function () { + app = new EmberApp({ + project, + vendorFiles: { - 'ember.js': 'vendor/ember.js' - } + 'ember.js': 'vendor/ember.js', + }, }); - expect(Object.keys(emberApp.vendorFiles)).to.deep.equal(defaultVendorFiles); + expect(Object.keys(app.vendorFiles)).to.deep.equal(defaultVendorFiles); }); - it('removes dependency in vendorFiles', function() { - emberApp = new EmberApp({ + it('removes dependency in vendorFiles', function () { + app = new EmberApp({ + project, + vendorFiles: { 'ember.js': null, - 'handlebars.js': null - } + }, }); - var vendorFiles = Object.keys(emberApp.vendorFiles); - expect(vendorFiles.indexOf('ember.js')).to.equal(-1); - expect(vendorFiles.indexOf('handlebars.js')).to.equal(-1); + let vendorFiles = Object.keys(app.vendorFiles); + expect(vendorFiles).to.not.contain('ember.js'); }); - it('defaults to ember.debug.js if exists in bower_components', function () { - var root = path.resolve(__dirname, '../../fixtures/app/with-default-ember-debug'); + it('does not clobber an explicitly configured ember development file', function () { + app = new EmberApp({ + project, - emberApp = new EmberApp({ - project: new Project(root, {}) + vendorFiles: { + 'ember.js': { + development: 'vendor/ember.debug.js', + }, + }, }); + let files = app.vendorFiles['ember.js']; + expect(files.development).to.equal('vendor/ember.debug.js'); + }); + }); - var emberFiles = emberApp.vendorFiles['ember.js']; - expect(emberFiles.development).to.equal('bower_components/ember/ember.debug.js'); + it('fails with invalid type', function () { + let app = new EmberApp({ + project, }); - it('switches the default ember.debug.js to ember.js if it does not exist', function () { - emberApp = new EmberApp(); - var emberFiles = emberApp.vendorFiles['ember.js']; - expect(emberFiles.development).to.equal('bower_components/ember/ember.js'); + expect(() => { + app.import('vendor/b/c/foo.js', { type: 'javascript' }); + }).to.throw( + /You must pass either `vendor` or `test` for options.type in your call to `app.import` for file: foo.js/ + ); + }); + + describe('_initOptions', function () { + it('sets the tests directory as watched when tests are enabled', function () { + let app = new EmberApp({ + project, + }); + + app._initOptions({ + tests: true, + }); + + expect(app.options.trees.tests).to.be.an.instanceOf(WatchedDir); }); + it('sets the tests directory as unwatched when tests are disabled', function () { + let app = new EmberApp({ + project, + }); - it('does not clobber an explicitly configured ember development file', function () { - emberApp = new EmberApp({ - vendorFiles: { - 'ember.js': { - development: 'vendor/ember.debug.js' - } - } + app._initOptions({ + tests: false, + }); + + expect(app.options.trees.tests).to.be.an.instanceOf(UnwatchedDir); + }); + }); + + describe('_resolveLocal', function () { + it('resolves a path relative to the project root', function () { + let app = new EmberApp({ + project, + }); + + let result = app._resolveLocal('foo'); + expect(result).to.equal(path.join(project.root, 'foo')); + }); + }); + + describe('_concatFiles()', function () { + beforeEach(function () { + app = new EmberApp({ project }); + }); + + describe('concat order', function () { + beforeEach(function () { + mockTemplateRegistry(app); + }); + + it('correctly orders concats from app.styles()', function () { + app.import('files/b.css'); + app.import('files/c.css'); + app.import('files/a.css', { prepend: true }); + app.import('files/d.css'); + + expect(app._styleOutputFiles['/assets/vendor.css']).to.deep.equal([ + 'files/a.css', + 'files/b.css', + 'files/c.css', + 'files/d.css', + ]); + }); + + it('correctly orders concats from app.testFiles()', function () { + app.import('files/b.js', { type: 'test' }); + app.import('files/c.js', { type: 'test' }); + app.import('files/a.js', { type: 'test' }); + app.import('files/a.js', { type: 'test', prepend: true }); // Should end up second. + app.import('files/d.js', { type: 'test' }); + app.import('files/d.js', { type: 'test', prepend: true }); // Should end up first. + app.import('files/d.js', { type: 'test' }); + + app.import('files/b.css', { type: 'test' }); + app.import('files/c.css', { type: 'test' }); + app.import('files/a.css', { type: 'test', prepend: true }); + app.import('files/d.css', { type: 'test' }); + app.import('files/d.css', { type: 'test' }); + + expect(app.legacyTestFilesToAppend).to.deep.equal([ + 'files/d.js', + 'files/a.js', + 'vendor/ember/ember-testing.js', + 'files/b.js', + 'files/c.js', + ]); + + expect(app.vendorTestStaticStyles).to.deep.equal(['files/a.css', 'files/b.css', 'files/c.css', 'files/d.css']); }); - var emberFiles = emberApp.vendorFiles['ember.js']; - expect(emberFiles.development).to.equal('vendor/ember.debug.js'); }); }); }); diff --git a/tests/unit/broccoli/ember-app/app-and-dependencies-test.js b/tests/unit/broccoli/ember-app/app-and-dependencies-test.js new file mode 100644 index 0000000000..3877197a12 --- /dev/null +++ b/tests/unit/broccoli/ember-app/app-and-dependencies-test.js @@ -0,0 +1,158 @@ +'use strict'; + +const broccoliTestHelper = require('broccoli-test-helper'); +const { expect } = require('chai'); + +const EmberApp = require('../../../../lib/broccoli/ember-app'); +const MockCLI = require('../../../helpers/mock-cli'); +const Project = require('../../../../lib/models/project'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; +const walkSync = require('walk-sync'); + +const EMBER_SOURCE_ADDON = { + name: 'ember-source', + paths: { + debug: 'vendor/ember/ember.js', + prod: 'vendor/ember/ember.js', + testing: 'vendor/ember/ember-testing.js', + }, + pkg: { + name: 'ember-source', + }, +}; + +describe('EmberApp#appAndDependencies', function () { + let input, output; + + beforeEach(async function () { + process.env.EMBER_ENV = 'development'; + + input = await createTempDir(); + + input.write({ + node_modules: { + 'fake-template-preprocessor': { + 'package.json': JSON.stringify({ + name: 'fake-template-preprocessor', + main: 'index.js', + keywords: ['ember-addon'], + }), + 'index.js': ` + module.exports = { + name: 'fake-template-preprocessor', + setupPreprocessorRegistry(type, registry) { + registry.add('template', { + isDefaultForType: true, + name: 'fake-template-preprocessor', + ext: 'hbs', + toTree(tree) { return tree; } + }); + } + }; + `, + }, + }, + config: { + 'environment.js': `module.exports = function() { return { modulePrefix: 'test-app' }; };`, + }, + }); + }); + + afterEach(async function () { + delete process.env.EMBER_ENV; + await input.dispose(); + + if (output) { + await output.dispose(); + } + }); + + function createApp(options) { + options = options || {}; + + let pkg = { + name: 'ember-app-test', + dependencies: { + 'fake-template-preprocessor': '*', + 'my-addon': '*', + }, + }; + + let cli = new MockCLI(); + let project = new (class extends Project { + initializeAddons() { + if (this._addonsInitialized) { + return; + } + + super.initializeAddons(); + + this.addons.push(EMBER_SOURCE_ADDON); + } + })(input.path(), pkg, cli.ui, cli); + + return new EmberApp( + { + project, + name: pkg.name, + _ignoreMissingLoader: true, + sourcemaps: { enabled: false }, + }, + options + ); + } + + function getFiles(path) { + return walkSync(path, { + ignore: ['vendor/ember-cli/**/*'], + directories: false, + }); + } + + it('moduleNormalizerDisabled', async function () { + input.write({ + node_modules: { + 'my-addon': { + addon: { + 'index.js': `define('amd', function() {});`, + }, + 'package.json': JSON.stringify({ + name: 'my-addon', + main: 'index.js', + keywords: ['ember-addon'], + }), + 'index.js': ` + module.exports = { + name: 'my-addon', + setupPreprocessorRegistry(type, registry) { + registry.add('template', { ext: 'hbs', toTree(tree) { return tree; } }); + registry.add('js', { ext: 'js', toTree(tree) { return tree; } }); + }, + } + `, + }, + }, + }); + + let app = createApp({ + moduleNormalizerDisabled: true, + }); + + let addon = app.project.findAddonByName('my-addon'); + + addon.treeForAddon = (tree) => { + const Funnel = require('broccoli-funnel'); + return new Funnel(tree, { + destDir: 'modules/my-addon', + }); + }; + + output = createBuilder(app.getExternalTree()); + await output.build(); + let actualFiles = getFiles(output.path()); + + expect(actualFiles).to.contain('addon-tree-output/modules/my-addon/index.js'); + }); +}); diff --git a/tests/unit/broccoli/merge-trees-test.js b/tests/unit/broccoli/merge-trees-test.js new file mode 100644 index 0000000000..f0093af8db --- /dev/null +++ b/tests/unit/broccoli/merge-trees-test.js @@ -0,0 +1,87 @@ +'use strict'; + +const { expect } = require('chai'); + +const mergeTrees = require('../../../lib/broccoli/merge-trees'); + +describe('broccoli/merge-trees', function () { + let originalUpstreamMergeTrees; + + beforeEach(function () { + originalUpstreamMergeTrees = mergeTrees._upstreamMergeTrees; + mergeTrees._upstreamMergeTrees = function () { + return {}; + }; + }); + + afterEach(function () { + // reset the shared EMPTY_MERGE_TREE to ensure + // we end up back in a consistent state + mergeTrees._overrideEmptyTree(null); + mergeTrees._upstreamMergeTrees = originalUpstreamMergeTrees; + }); + + it('returns the first item when merging single item array', function () { + let actual = mergeTrees(['foo']); + + expect(actual).to.equal('foo'); + }); + + it('returns a constant "empty tree" when passed an empty array', function () { + let expected = {}; + + mergeTrees._overrideEmptyTree(expected); + + let first = mergeTrees([]); + let second = mergeTrees([]); + + expect(first).to.equal(expected); + expect(second).to.equal(expected); + }); + + it('passes all inputTrees through when non-empty', function () { + let expected = ['foo', 'bar']; + let actual; + + mergeTrees._upstreamMergeTrees = function (inputTrees) { + actual = inputTrees; + return {}; + }; + + mergeTrees(['foo', null, undefined, 'bar']); + expect(actual).to.deep.equal(expected); + }); + + it('filters out empty trees from inputs', function () { + let expected = ['bar', 'baz']; + let actual; + + mergeTrees._overrideEmptyTree('foo'); + + mergeTrees._upstreamMergeTrees = function (inputTrees) { + actual = inputTrees; + return {}; + }; + + mergeTrees(['foo', 'bar', 'baz']); + expect(actual).to.deep.equal(expected); + }); + + it('removes duplicate trees with the last duplicate being the remainder', function () { + let treeA = {}; + let treeB = {}; + let expected = [treeB, treeA]; + let actual; + + mergeTrees._upstreamMergeTrees = function (inputTrees) { + actual = inputTrees; + return {}; + }; + + mergeTrees([treeB, treeA, treeB, treeA, treeA]); + + expect(actual).to.deep.equal(expected); + expect(actual[0]).to.equal(treeB); + expect(actual[1]).to.equal(treeA); + }); +}); diff --git a/tests/unit/broccoli/template-precompilation-test.js b/tests/unit/broccoli/template-precompilation-test.js new file mode 100644 index 0000000000..31e81baa4c --- /dev/null +++ b/tests/unit/broccoli/template-precompilation-test.js @@ -0,0 +1,214 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs-extra'); +const broccoliTestHelper = require('broccoli-test-helper'); +const { expect } = require('chai'); +const BroccoliPlugin = require('broccoli-plugin'); +const walkSync = require('walk-sync'); + +const MockCLI = require('../../helpers/mock-cli'); +const Project = require('../../../lib/models/project'); +const Addon = require('../../../lib/models/addon'); +const EmberApp = require('../../../lib/broccoli/ember-app'); + +const createBuilder = broccoliTestHelper.createBuilder; +const createTempDir = broccoliTestHelper.createTempDir; + +const EMBER_SOURCE_ADDON = { + name: 'ember-source', + paths: { + debug: 'vendor/ember/ember.js', + prod: 'vendor/ember/ember.js', + testing: 'vendor/ember/ember-testing.js', + }, + pkg: { + name: 'ember-source', + }, +}; + +describe('template preprocessors', function () { + let input, output, addon; + + class FakeTemplateColocator extends BroccoliPlugin { + build() { + let [inputPath] = this.inputPaths; + let entries = fs.readdirSync(inputPath); + if (entries.length > 1) { + throw new Error('all input files should be scoped to the addon or project name'); + } + + if (entries.length === 0) { + // nothing to do, no files in input tree + return; + } + let root = entries[0]; + let files = walkSync(path.join(inputPath, root), { directories: false }); + + files.forEach((file) => { + let fullInputPath = path.join(inputPath, root, file); + let fullOutputPath = path.join(this.outputPath, root, file); + + if (file.startsWith(`components/`)) { + let pathParts = path.parse(file); + let templatePath = path.join(inputPath, root, 'templates', pathParts.dir, `${pathParts.name}.hbs`); + let templateContents = fs.readFileSync(templatePath, { encoding: 'utf8' }); + let componentContents = fs.readFileSync(fullInputPath, { encoding: 'utf8' }); + + let contents = `${componentContents}\nexport const template = hbs\`${templateContents}\``; + fs.ensureDirSync(path.dirname(fullOutputPath)); + fs.writeFileSync(fullOutputPath, contents, { encoding: 'utf8' }); + } else if (file.startsWith(`templates/components/`)) { + // do nothing + } else { + fs.copySync(fullInputPath, fullOutputPath); + } + }); + } + } + + describe('Addon', function () { + beforeEach(async function () { + input = await createTempDir(); + let MockAddon = Addon.extend({ + root: input.path(), + packageRoot: input.path(), + name: 'fake-addon', + }); + let cli = new MockCLI(); + let pkg = { name: 'ember-app-test' }; + let project = new Project(input.path(), pkg, cli.ui, cli); + + addon = new MockAddon(project, project); + + addon.registry.add('js', { + name: 'fake-js-compiler', + ext: 'js', + toTree(tree) { + return tree; + }, + }); + }); + + afterEach(async function () { + await input.dispose(); + await output.dispose(); + }); + + it('the template preprocessor receives access to all files', async function () { + addon.registry.add('template', { + name: 'fake-template-compiler', + ext: 'hbs', + toTree(tree) { + return new FakeTemplateColocator([tree]); + }, + }); + + input.write({ + addon: { + components: { + 'awesome-button.js': 'export default class {}', + }, + templates: { + components: { + 'awesome-button.hbs': '', + }, + }, + }, + }); + + output = createBuilder(addon.treeForAddon(path.join(addon.root, '/addon'))); + await output.build(); + + expect(output.read()).to.deep.equal({ + 'fake-addon': { + components: { + 'awesome-button.js': `export default class {}\nexport const template = hbs\`\``, + }, + }, + }); + }); + }); + + describe('EmberApp', function () { + let project; + + beforeEach(async function () { + input = await createTempDir(); + let cli = new MockCLI(); + let pkg = { name: 'fake-app-test', devDependencies: { 'ember-cli': '*' } }; + project = new (class extends Project { + initializeAddons() { + if (this._addonsInitialized) { + return; + } + + super.initializeAddons(); + + this.addons.push(EMBER_SOURCE_ADDON); + } + })(input.path(), pkg, cli.ui, cli); + }); + + afterEach(async function () { + await input.dispose(); + await output.dispose(); + }); + + it('the template preprocessor receives access to all files', async function () { + input.write({ + app: { + components: { + 'awesome-button.js': 'export default class {}', + }, + styles: { + 'app.css': '/* styles */', + }, + templates: { + components: { + 'awesome-button.hbs': '', + }, + }, + 'index.html': '', + }, + config: { + 'environment.js': 'module.exports = function() { return { modulePrefix: "fake-app-test" } };', + }, + }); + + let app = new EmberApp( + { + project, + name: project.pkg.name, + _ignoreMissingLoader: true, + sourcemaps: { enabled: false }, + }, + {} + ); + + app.registry.add('js', { + name: 'fake-js-compiler', + ext: 'js', + toTree(tree) { + return tree; + }, + }); + + app.registry.add('template', { + name: 'fake-template-compiler', + ext: 'hbs', + toTree(tree) { + return new FakeTemplateColocator([tree]); + }, + }); + + output = createBuilder(app.toTree()); + await output.build(); + + let expectedContent = `export default class {}\nexport const template = hbs\`\``; + let actualContent = output.read().assets['fake-app-test.js']; + + expect(actualContent).to.include(expectedContent); + }); + }); +}); diff --git a/tests/unit/cli/cli-test.js b/tests/unit/cli/cli-test.js index 78e44737fb..5523165006 100644 --- a/tests/unit/cli/cli-test.js +++ b/tests/unit/cli/cli-test.js @@ -1,311 +1,465 @@ 'use strict'; -var EOL = require('os').EOL; -var expect = require('chai').expect; -var stub = require('../../helpers/stub').stub; -var MockUI = require('../../helpers/mock-ui'); -var MockAnalytics = require('../../helpers/mock-analytics'); -var CLI = require('../../../lib/cli/cli'); -var ui; -var analytics; -var commands = {}; -var argv; - -var isWithinProject; - -// helper to similate running the CLI +const { expect } = require('chai'); +const MockUI = require('console-ui/mock'); +const td = require('testdouble'); +const Command = require('../../../lib/models/command'); +const CLI = require('../../../lib/cli/cli'); + +let ui; +let commands = {}; +let isWithinProject; +let project; +let willInterruptProcess; + +// helper to simulate running the CLI function ember(args) { - return new CLI({ - ui: ui, - analytics: analytics, - testing: true - }).run({ - tasks: {}, - commands: commands, - cliArgs: args || [], - settings: {}, - project: { - isEmberCLIProject: function() { // similate being inside or outside of a project - return isWithinProject; - }, - hasDependencies: function() { - return true; - }, - blueprintLookupPaths: function() { - return []; - } - } + let cli = new CLI({ + ui, + testing: true, }); + + let startInstr = td.replace(cli.instrumentation, 'start'); + let stopInstr = td.replace(cli.instrumentation, 'stopAndReport'); + + return cli + .run({ + tasks: {}, + commands, + cliArgs: args || [], + settings: {}, + project, + }) + .then(function (value) { + td.verify(stopInstr('init'), { times: 1 }); + td.verify(startInstr('command'), { times: 1 }); + td.verify(stopInstr('command', td.matchers.anything(), td.matchers.isA(Array)), { times: 1 }); + td.verify(startInstr('shutdown'), { times: 1 }); + + return value; + }); } function stubCallHelp() { - return stub(CLI.prototype, 'callHelp'); + return td.replace(CLI.prototype, 'callHelp', td.function()); } function stubValidateAndRunHelp(name) { - commands[name] = require('../../../lib/commands/' + name); - return stub(commands[name].prototype, 'validateAndRun', 'callHelp'); + let stub = stubValidateAndRun(name); + td.when(stub(), { ignoreExtraArgs: true, times: 1 }).thenReturn('callHelp'); + return stub; } function stubValidateAndRun(name) { - commands[name] = require('../../../lib/commands/' + name); - return stub(commands[name].prototype, 'validateAndRun'); + commands[name] = require(`../../../lib/commands/${name}`); + + return td.replace(commands[name].prototype, 'validateAndRun', td.function()); } function stubRun(name) { - commands[name] = require('../../../lib/commands/' + name); - return stub(commands[name].prototype, 'run'); + commands[name] = require(`../../../lib/commands/${name}`); + return td.replace(commands[name].prototype, 'run', td.function()); } -beforeEach(function() { - ui = new MockUI(); - analytics = new MockAnalytics(); - argv = []; - commands = { }; - isWithinProject = true; -}); - -afterEach(function() { - for(var key in commands) { - if (!commands.hasOwnProperty(key)) { continue; } - if (commands[key].prototype.validateAndRun.restore) { - commands[key].prototype.validateAndRun.restore(); - } - if (commands[key].prototype.run.restore) { - commands[key].prototype.run.restore(); - } - } +describe('Unit: CLI', function () { + beforeEach(function () { + willInterruptProcess = require('../../../lib/utilities/will-interrupt-process'); + td.replace(willInterruptProcess, 'addHandler', td.function()); + td.replace(willInterruptProcess, 'removeHandler', td.function()); + + ui = new MockUI(); + commands = {}; + isWithinProject = true; + project = { + isEmberCLIProject() { + // simulate being inside or outside of a project + return isWithinProject; + }, + hasDependencies() { + return true; + }, + blueprintLookupPaths() { + return []; + }, + }; + }); - delete process.env.EMBER_ENV; - commands = argv = ui = undefined; -}); + afterEach(function () { + td.reset(); -function assertVersion(string, message) { - expect(true, /version:\s\d+\.\d+\.\d+/.test(string), message || ('expected version, got: ' + string)); -} + delete process.env.EMBER_ENV; + commands = ui = undefined; + }); -describe('Unit: CLI', function() { this.timeout(10000); - it('exists', function() { - expect(true, CLI); + + it('ember', function () { + let help = stubValidateAndRun('help'); + + return ember().then(function () { + td.verify(help(), { ignoreExtraArgs: true, times: 1 }); + let output = ui.output.trim(); + expect(output).to.equal('', 'expected no extra output'); + }); }); - it('ember', function() { - var help = stubValidateAndRun('help'); + /* + it('logError', function() { + var cli = new CLI({ + ui: ui, + testing: true + }); + var error = new Error('Error message!'); + var expected = {exitCode: 1, ui: ui, error: error}; + expect(cli.logError(error)).to.eql(expected, 'expected error object'); + }); + */ - return ember().then(function() { - expect(help.called).to.equal(1, 'expected help to be called once'); - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); - expect(output.length).to.equal(1, 'expected no extra output'); + it('callHelp', function () { + let cli = new CLI({ + ui, + testing: true, }); + let init = stubValidateAndRun('init'); + let help = stubValidateAndRun('help'); + let helpOptions = { + environment: { + tasks: {}, + commands, + cliArgs: [], + settings: {}, + project: { + isEmberCLIProject() { + // simulate being inside or outside of a project + return isWithinProject; + }, + hasDependencies() { + return true; + }, + blueprintLookupPaths() { + return []; + }, + }, + }, + commandName: 'init', + commandArgs: [], + }; + cli.callHelp(helpOptions); + td.verify(help(), { ignoreExtraArgs: true, times: 1 }); + td.verify(init(), { ignoreExtraArgs: true, times: 0 }); }); - describe('help', function(){ - ['--help', '-h'].forEach(function(command){ - it('ember ' + command, function() { - var help = stubValidateAndRun('help'); + it('errors correctly if the init hook errors', function () { + stubValidateAndRun('help'); - return ember([command]).then(function() { - expect(help.called).to.equal(1, 'expected help to be called once'); - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); - expect(output.length).to.equal(1, 'expected no extra output'); - }); + let cli = new CLI({ + ui, + testing: true, + }); + + let startInstr = td.replace(cli.instrumentation, 'start'); + let stopInstr = td.replace(cli.instrumentation, 'stopAndReport'); + let logError = td.replace(cli, 'logError'); + let err = new Error('init failed'); + + td.when(stopInstr('init')).thenThrow(err); + + return cli + .run({ + tasks: {}, + commands, + cliArgs: [], + settings: {}, + project, + }) + .then(function () { + td.verify(startInstr('command'), { times: 0 }); + td.verify(stopInstr('command'), { times: 0 }); + td.verify(startInstr('shutdown'), { times: 1 }); + td.verify(logError(err)); }); + }); - it('ember new ' + command, function() { - var help = stubCallHelp(); - var newCommand = stubValidateAndRunHelp('new'); + it('"run" method must throw error if no evironment provided', function () { + stubValidateAndRun('help'); - return ember(['new', command]).then(function() { - expect(help.called).to.equal(1, 'expected help to be called once'); - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); - expect(output.length).to.equal(1, 'expected no extra output'); + let cli = new CLI({ + ui, + testing: true, + }); - expect(newCommand.called).to.equal(1, 'expected the new command to be called once'); - }); + let wasResolved = false; + cli + .run() + .then(() => { + wasResolved = true; + }) + .catch((err) => { + expect(err.toString()).to.be.equal('Error: Unable to execute "run" command without environment argument'); + }) + .finally(() => { + expect(wasResolved).to.be.false; }); - }); }); - ['--version', '-v'].forEach(function(command){ - it('ember ' + command, function() { - var version = stubValidateAndRun('version'); + it('errors correctly if "run" method not called before "maybeMakeCommand" execution', function () { + stubValidateAndRun('help'); - return ember([command]).then(function() { - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); - expect(output.length).to.equal(1, 'expected no extra output'); - expect(version.called).to.equal(1, 'expected version to be called once'); - }); + let cli = new CLI({ + ui, + testing: true, }); + + try { + cli.maybeMakeCommand('foo', ['bar']); + } catch (err) { + expect(err.toString()).to.be.equal( + 'Error: Unable to make command without environment, you have to execute "run" method first.' + ); + } }); - describe('server', function() { - ['server','s'].forEach(function(command) { - it('expects version in UI output', function() { - var server = stubRun('serve'); - - return ember([command]).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); - - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); - var options = server.calledWith[0][0]; - if (/win\d+/.test(process.platform) || options.watcher === 'watchman') { - expect(output.length).to.equal(1, 'expected no extra output'); - } else { - expect(output.length).to.equal(3, 'expected no extra output'); - } - }); - }); + describe('custom addon command', function () { + it('beforeRun can return a promise', function () { + let CustomCommand = Command.extend({ + name: 'custom', - it('ember ' + command + ' --port 9999', function() { - var server = stubRun('serve'); + init() { + this._super && this._super.init.apply(this, arguments); - return ember([command, '--port', '9999']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + this._beforeRunFinished = false; + }, - var options = server.calledWith[0][0]; + beforeRun() { + let command = this; - expect(options.port).to.equal(9999, 'expected port 9999, was ' + options.port); - }); + return new Promise(function (resolve) { + setTimeout(function () { + command._beforeRunFinished = true; + resolve(); + }, 5); + }); + }, + + run() { + if (!this._beforeRunFinished) { + throw new Error('beforeRun not completed before run called!'); + } + }, }); - it('ember ' + command + ' --host localhost', function() { - var server = stubRun('serve'); + project.eachAddonCommand = function (callback) { + callback('custom-addon', { + custom: CustomCommand, + }); + }; - return ember(['server', '--host', 'localhost']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember(['custom']); + }); + }); - var options = server.calledWith[0][0]; + describe('command interruption handler', function () { + let onCommandInterrupt; + beforeEach(function () { + onCommandInterrupt = td.matchers.isA(Function); + }); - expect(options.host).to.equal('localhost', 'correct localhost'); - }); - }); + it('sets up handler before command run', function () { + const CustomCommand = Command.extend({ + name: 'custom', - it('ember ' + command + ' --port 9292 --host localhost', function() { - var server = stubRun('serve'); + beforeRun() { + td.verify(willInterruptProcess.addHandler(onCommandInterrupt)); - return ember([command, '--port', '9292', '--host', 'localhost']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return Promise.resolve(); + }, - var options = server.calledWith[0][0]; + run() { + return Promise.resolve(); + }, + }); - expect(options.host).to.equal('localhost', 'correct localhost'); - expect(options.port).to.equal(9292, 'correct port'); + project.eachAddonCommand = function (callback) { + callback('custom-addon', { + CustomCommand, }); - }); + }; - it('ember ' + command + ' --proxy http://localhost:3000/', function() { - var server = stubRun('serve'); + return ember(['custom']); + }); - return ember([command, '--proxy', 'http://localhost:3000/']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + it('cleans up handler after command finished', function () { + stubValidateAndRun('serve'); - var options = server.calledWith[0][0]; + return ember(['serve']).finally(function () { + td.verify(willInterruptProcess.removeHandler(onCommandInterrupt)); + }); + }); + }); + + describe('help', function () { + ['--help', '-h'].forEach(function (command) { + it(`ember ${command}`, function () { + let help = stubValidateAndRun('help'); - expect(options.proxy).to.equal('http://localhost:3000/', 'correct proxy url'); + return ember([command]).then(function () { + td.verify(help(), { ignoreExtraArgs: true, times: 1 }); + let output = ui.output.trim(); + expect(output).to.equal('', 'expected no extra output'); }); }); - it('ember ' + command + ' --proxy https://localhost:3009/ --insecure-proxy', function () { - var server = stubRun('serve'); + it(`ember new ${command}`, function () { + let help = stubCallHelp(); + stubValidateAndRunHelp('new'); - return ember([command, '--insecure-proxy']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember(['new', command]).then(function () { + td.verify(help(), { ignoreExtraArgs: true, times: 1 }); + let output = ui.output.trim(); + expect(output).to.equal('', 'expected no extra output'); + }); + }); + }); + }); - var options = server.calledWith[0][0]; + ['--version', '-v'].forEach(function (command) { + it(`ember ${command}`, function () { + let version = stubValidateAndRun('version'); - expect(options.insecureProxy).to.equal(true, 'correct `secure` option for http-proxy'); - }); + return ember([command]).then(function () { + let output = ui.output.trim(); + expect(output).to.equal('', 'expected no extra output'); + td.verify(version(), { ignoreExtraArgs: true, times: 1 }); }); + }); + }); - it('ember ' + command + ' --proxy https://localhost:3009/ --no-insecure-proxy', function () { - var server = stubRun('serve'); + describe('server', function () { + ['server', 's'].forEach(function (command) { + it(`ember ${command} --port 9999`, function () { + let server = stubRun('serve'); - return ember([command, '--no-insecure-proxy']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember([command, '--port', '9999']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.port, 'port').to.equal(9999); + }); + }); - var options = server.calledWith[0][0]; + it(`ember ${command} --host localhost`, function () { + let server = stubRun('serve'); - expect(options.insecureProxy).to.equal(false, 'correct `secure` option for http-proxy'); + return ember(['server', '--host', 'localhost']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.host, 'host').to.equal('localhost'); }); }); - it('ember ' + command + ' --watcher events', function() { - var server = stubRun('serve'); + it(`ember ${command} --port 9292 --host localhost`, function () { + let server = stubRun('serve'); - return ember([command, '--watcher', 'events']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember([command, '--port', '9292', '--host', 'localhost']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.host, 'host').to.equal('localhost'); + expect(captor.value.port, 'port').to.equal(9292); + }); + }); - var options = server.calledWith[0][0]; + it(`ember ${command} --proxy http://localhost:3000/`, function () { + let server = stubRun('serve'); - expect(true, /node|events|watchman/.test(options.watcher), 'correct watcher type'); + return ember([command, '--proxy', 'http://localhost:3000/']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.proxy, 'proxy').to.equal('http://localhost:3000/'); }); }); - it('ember ' + command + ' --watcher polling', function() { - var server = stubRun('serve'); + it(`ember ${command} --proxy https://localhost:3009/ --insecure-proxy`, function () { + let server = stubRun('serve'); - return ember([command, '--watcher', 'polling']).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember([command, '--insecure-proxy']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.insecureProxy, 'insecureProxy').to.equal(true); + }); + }); - var options = server.calledWith[0][0]; + it(`ember ${command} --proxy https://localhost:3009/ --no-insecure-proxy`, function () { + let server = stubRun('serve'); - expect(options.watcher).to.equal('polling', 'correct watcher type'); + return ember([command, '--no-insecure-proxy']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.insecureProxy, 'insecureProxy').to.equal(false); }); }); - it('ember ' + command, function() { - var server = stubRun('serve'); + it(`ember ${command} --watcher events`, function () { + let server = stubRun('serve'); - return ember([command]).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember([command, '--watcher', 'events']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.watcher, 'watcher').to.match(/node|polling|watchman/); + }); + }); - var options = server.calledWith[0][0]; + it(`ember ${command} --watcher polling`, function () { + let server = stubRun('serve'); - expect(true, /node|events|watchman/.test(options.watcher), 'correct watcher type'); + return ember([command, '--watcher', 'polling']).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.watcher, 'watcher').to.equal('polling'); }); }); - ['production', 'development', 'foo'].forEach(function(env) { - it('ember ' + command + ' --environment ' + env, function() { - var server = stubRun('serve'); + it(`ember ${command}`, function () { + let server = stubRun('serve'); - return ember([command, '--environment', env]).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember([command]).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.watcher, 'watcher').to.match(/node|polling|watchman/); + }); + }); - var options = server.calledWith[0][0]; + ['production', 'development', 'foo'].forEach(function (env) { + it(`ember ${command} --environment ${env}`, function () { + let server = stubRun('serve'); - expect(options.environment).to.equal(env, 'correct environment'); + return ember([command, '--environment', env]).then(function () { + let captor = td.matchers.captor(); + td.verify(server(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + expect(captor.value.environment, 'environment').to.equal(env); }); }); }); - ['development', 'foo'].forEach(function(env) { - it('ember ' + command + ' --environment ' + env, function() { - var server = stubRun('serve'); - process.env.EMBER_ENV='production'; + ['development', 'foo'].forEach(function (env) { + it(`ember ${command} --environment ${env}`, function () { + let server = stubRun('serve'); + process.env.EMBER_ENV = 'production'; - return ember([command, '--environment', env]).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember([command, '--environment', env]).then(function () { + td.verify(server(), { ignoreExtraArgs: true, times: 1 }); expect(process.env.EMBER_ENV).to.equal('production', 'uses EMBER_ENV over environment'); }); }); }); - ['production', 'development', 'foo'].forEach(function(env) { - it('EMBER_ENV=' + env + ' ember ' + command, function() { - var server = stubRun('serve'); + ['production', 'development', 'foo'].forEach(function (env) { + it(`EMBER_ENV=${env} ember ${command}`, function () { + let server = stubRun('serve'); - process.env.EMBER_ENV=env; + process.env.EMBER_ENV = env; - return ember([command]).then(function() { - expect(server.called).to.equal(1, 'expected the server command to be run'); + return ember([command]).then(function () { + td.verify(server(), { ignoreExtraArgs: true, times: 1 }); expect(process.env.EMBER_ENV).to.equal(env, 'correct environment'); }); @@ -314,157 +468,145 @@ describe('Unit: CLI', function() { }); }); - describe('generate', function() { - ['generate', 'g'].forEach(function(command) { - it('ember ' + command + ' foo bar baz', function() { - var generate = stubRun('generate'); - - return ember([command, 'foo', 'bar', 'baz']).then(function() { - expect(generate.called).to.equal(1, 'expected the generate command to be run'); - - var args = generate.calledWith[0][1]; + describe('generate', function () { + ['generate', 'g'].forEach(function (command) { + it(`ember ${command} foo bar baz`, function () { + let generate = stubRun('generate'); - expect(args).to.deep.equal(['foo', 'bar', 'baz']); + return ember([command, 'foo', 'bar', 'baz']).then(function () { + let captor = td.matchers.captor(); + td.verify(generate(captor.capture(), ['foo', 'bar', 'baz']), { times: 1 }); - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); + let output = ui.output.trim(); - var options = generate.calledWith[0][0]; - if (/win\d+/.test(process.platform) || options.watcher === 'watchman') { - expect(output.length).to.equal(1, 'expected no extra output'); - } else { - expect(output.length).to.equal(3, 'expected no extra output'); - } + expect(output).to.equal('', 'expected no extra output'); }); }); }); }); - describe('init', function() { - ['init', 'i'].forEach(function(command) { - it('ember ' + command, function() { - var init = stubValidateAndRun('init'); + describe('init', function () { + ['init'].forEach(function (command) { + it(`ember ${command}`, function () { + let init = stubValidateAndRun('init'); - return ember([command]).then(function() { - expect(init.called).to.equal(1, 'expected the init command to be run'); + return ember([command]).then(function () { + td.verify(init(), { ignoreExtraArgs: true, times: 1 }); }); }); - it('ember ' + command + ' ', function() { - var init = stubRun('init'); - - return ember([command, 'my-blog']).then(function() { - var args = init.calledWith[0][1]; + it(`ember ${command} `, function () { + let init = stubRun('init'); - expect(init.called).to.equal(1, 'expected the init command to be run'); - expect(args).to.deep.equal(['my-blog'], 'expect first arg to be the app name'); + return ember([command, 'my-blog']).then(function () { + let captor = td.matchers.captor(); + td.verify(init(captor.capture(), ['my-blog']), { times: 1 }); - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); + let output = ui.output.trim(); - var options = init.calledWith[0][0]; - if (/win\d+/.test(process.platform) || options.watcher === 'watchman') { - expect(output.length).to.equal(1, 'expected no extra output'); - } else { - expect(output.length).to.equal(3, 'expected no extra output'); - } + expect(output).to.equal('', 'expected no extra output'); }); }); }); }); - describe('new', function() { - it('ember new', function() { + describe('new', function () { + it('ember new', function () { isWithinProject = false; - var newCommand = stubRun('new'); + let newCommand = stubRun('new'); - return ember(['new']).then(function() { - expect(newCommand.called).to.equal(1, 'expected the new command to be run'); + return ember(['new']).then(function () { + td.verify(newCommand(), { ignoreExtraArgs: true, times: 1 }); }); }); - it('ember new MyApp', function() { + it('ember new MyApp', function () { isWithinProject = false; - var newCommand = stubRun('new'); - - return ember(['new', 'MyApp']).then(function() { - expect(newCommand.called).to.equal(1, 'expected the new command to be run'); - var args = newCommand.calledWith[0][1]; + let newCommand = stubRun('new'); - expect(args).to.deep.equal(['MyApp']); + return ember(['new', 'MyApp']).then(function () { + td.verify(newCommand(td.matchers.anything(), ['MyApp']), { times: 1 }); }); }); }); - describe('build', function() { - ['build','b'].forEach(function(command) { - it('ember ' + command, function() { - var build = stubRun('build'); + describe('build', function () { + ['build', 'b'].forEach(function (command) { + it(`ember ${command}`, function () { + let build = stubRun('build'); - return ember([command]).then(function() { - expect(build.called).to.equal(1, 'expected the build command to be run'); + return ember([command]).then(function () { + let captor = td.matchers.captor(); + td.verify(build(captor.capture()), { ignoreExtraArgs: true, times: 1 }); - var options = build.calledWith[0][0]; + let options = captor.value; expect(options.watch).to.equal(false, 'expected the default watch flag to be false'); + expect(options.suppressSizes).to.equal(false, 'expected the default suppress-sizes flag to be false'); }); }); - it('ember ' + command + ' --disable-analytics', function() { - var build = stubRun('build'); + it(`ember ${command} --watch`, function () { + let build = stubRun('build'); - return ember([command, '--disable-analytics']).then(function() { - var options = build.calledWith[0][0]; - expect(options.disableAnalytics).to.equal(true, 'expected the disableAnalytics flag to be true'); + return ember([command, '--watch']).then(function () { + let captor = td.matchers.captor(); + td.verify(build(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + + let options = captor.value; + expect(options.watch).to.equal(true, 'expected the watch flag to be true'); }); }); - it('ember ' + command + ' --watch', function() { - var build = stubRun('build'); + it(`ember ${command} --suppress-sizes`, function () { + let build = stubRun('build'); - return ember([command, '--watch']).then(function() { - var options = build.calledWith[0][0]; - expect(options.watch).to.equal(true, 'expected the watch flag to be true'); + return ember([command, '--suppress-sizes']).then(function () { + let captor = td.matchers.captor(); + td.verify(build(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + + let options = captor.value; + expect(options.suppressSizes).to.equal(true, 'expected the suppressSizes flag to be true'); }); }); - ['production', 'development', 'baz'].forEach(function(env){ - it('ember ' + command + ' --environment ' + env, function() { - var build = stubRun('build'); - - return ember([command, '--environment', env]).then(function() { - expect(build.called).to.equal(1, 'expected the build command to be run'); + ['production', 'development', 'baz'].forEach(function (env) { + it(`ember ${command} --environment ${env}`, function () { + let build = stubRun('build'); - var options = build.calledWith[0][0]; + return ember([command, '--environment', env]).then(function () { + let captor = td.matchers.captor(); + td.verify(build(captor.capture()), { ignoreExtraArgs: true, times: 1 }); + let options = captor.value; expect(options.environment).to.equal(env, 'correct environment'); }); }); }); - ['development', 'baz'].forEach(function(env){ - it('EMBER_ENV=production ember ' + command + ' --environment ' + env, function() { - var build = stubRun('build'); + ['development', 'baz'].forEach(function (env) { + it(`EMBER_ENV=production ember ${command} --environment ${env}`, function () { + let build = stubRun('build'); process.env.EMBER_ENV = 'production'; - return ember([command, '--environment', env]).then(function() { - expect(build.called).to.equal(1, 'expected the build command to be run'); + return ember([command, '--environment', env]).then(function () { + td.verify(build(), { ignoreExtraArgs: true, times: 1 }); expect(process.env.EMBER_ENV).to.equal('production', 'uses EMBER_ENV over environment'); }); }); }); - ['production', 'development', 'baz'].forEach(function(env){ - it('EMBER_ENV=' + env + ' ember ' + command + ' ', function() { - var build = stubRun('build'); + ['production', 'development', 'baz'].forEach(function (env) { + it(`EMBER_ENV=${env} ember ${command} `, function () { + let build = stubRun('build'); - process.env.EMBER_ENV=env; + process.env.EMBER_ENV = env; - return ember([command]).then(function() { - expect(build.called).to.equal(1, 'expected the build command to be run'); + return ember([command]).then(function () { + td.verify(build(), { ignoreExtraArgs: true, times: 1 }); expect(process.env.EMBER_ENV).to.equal(env, 'correct environment'); }); @@ -473,26 +615,25 @@ describe('Unit: CLI', function() { }); }); - it('ember ', function() { - var help = stubValidateAndRun('help'); - var serve = stubValidateAndRun('serve'); + it('ember ', function () { + let help = stubValidateAndRun('help'); + let serve = stubValidateAndRun('serve'); - return ember(['serve']).then(function() { - expect(help.called).to.equal(0, 'expected the help command NOT to be run'); - expect(serve.called).to.equal(1, 'expected the serve command to be run'); + return ember(['serve']).then(function () { + td.verify(help(), { ignoreExtraArgs: true, times: 0 }); + td.verify(serve(), { ignoreExtraArgs: true, times: 1 }); - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); - expect(output.length).to.equal(1, 'expected no extra output'); + let output = ui.output.trim(); + expect(output).to.equal('', 'expected no extra output'); }); }); - it.skip('ember ', function() { - var help = stubValidateAndRun('help'); - var serve = stubValidateAndRun('serve'); + it.skip('ember ', function () { + let help = stubValidateAndRun('help'); + let serve = stubValidateAndRun('serve'); - return ember(['serve', 'lorem', 'ipsum', 'dolor', '--flag1=one']).then(function() { - var args = serve.calledWith[0][0].cliArgs; + return ember(['serve', 'lorem', 'ipsum', 'dolor', '--flag1=one']).then(function () { + let args = serve.calledWith[0][0].cliArgs; expect(help.called).to.equal(0, 'expected the help command NOT to be run'); expect(serve.called).to.equal(1, 'expected the foo command to be run'); @@ -500,67 +641,114 @@ describe('Unit: CLI', function() { expect(serve.calledWith[0].length).to.equal(2, 'expect foo to receive a total of 4 args'); - var output = ui.output.trim().split(EOL); - assertVersion(output[0]); - expect(output.length).to.equal(1, 'expected no extra output'); + let output = ui.output.trim(); + expect(output).to.equal('', 'expected no extra output'); }); }); - it('ember ', function() { - var help = stubValidateAndRun('help'); + it('ember ', function () { + let help = stubValidateAndRun('help'); - return ember(['unknownCommand']).then(function() { - var output = ui.output.trim().split(EOL); - var helpfulMessage = /The specified command .*unknownCommand.* is invalid\. For available options/; - expect(true, helpfulMessage.test(output[1]), 'expected an invalid command message'); - expect(help.called).to.equal(0, 'expected the help command to be run'); + return expect(ember(['unknownCommand'])).to.be.rejected.then((error) => { + expect(help.called, 'help command was executed').to.not.be.ok; + expect(error.name).to.equal('SilentError'); + expect(error.message).to.equal( + 'The specified command unknownCommand is invalid. For available options, see `ember help`.' + ); }); }); - describe.skip('default options config file', function() { - it('reads default options from .ember-cli file', function() { - var defaults = ['--output', process.cwd()]; - var build = stubValidateAndRun('build'); - - return ember(['build'], defaults).then(function() { + describe.skip('default options config file', function () { + it('reads default options from .ember-cli file', function () { + let defaults = ['--output', process.cwd()]; + let build = stubValidateAndRun('build'); - var options = build.calledWith[0][1].cliOptions; + return ember(['build'], defaults).then(function () { + let options = build.calledWith[0][1].cliOptions; expect(options.output).to.equal(process.cwd()); }); }); }); - describe('Global command options', function() { - var verboseCommand = function(args) { + describe('logError', function () { + it('returns error status code in production', function () { + let cli = new CLI({ + ui: new MockUI(), + testing: false, + }); + + expect(cli.logError('foo')).to.equal(1); + }); + + it('does not throw an error in production', function () { + let cli = new CLI({ + ui: new MockUI(), + testing: false, + }); + + let invokeError = cli.logError.bind(cli, new Error('foo')); + + expect(invokeError).to.not.throw(); + }); + + it('throws error in testing', function () { + let cli = new CLI({ + ui: new MockUI(), + testing: true, + }); + + let invokeError = cli.logError.bind(cli, new Error('foo')); + + expect(invokeError).to.throw(Error, 'foo'); + }); + }); + + describe('Global command options', function () { + let verboseCommand = function (args) { return ember(['fake-command', '--verbose'].concat(args)); }; - describe('--verbose', function() { - describe('option parsing', function() { - afterEach(function() { + describe('--verbose', function () { + describe('option parsing', function () { + afterEach(function () { delete process.env.EMBER_VERBOSE_FAKE_OPTION_1; delete process.env.EMBER_VERBOSE_FAKE_OPTION_2; }); - it('sets process.env.EMBER_VERBOSE_${NAME} for each space delimited option', function() { - return verboseCommand(['fake_option_1', 'fake_option_2']).then(function() { - expect(true, process.env.EMBER_VERBOSE_FAKE_OPTION_1, 'expected it to be true'); - expect(true, process.env.EMBER_VERBOSE_FAKE_OPTION_2, 'expected it to be true'); + // eslint-disable-next-line no-template-curly-in-string + it('sets process.env.EMBER_VERBOSE_${NAME} for each space delimited option', function () { + return expect(verboseCommand(['fake_option_1', 'fake_option_2'])).to.be.rejected.then((error) => { + expect(process.env.EMBER_VERBOSE_FAKE_OPTION_1).to.be.ok; + expect(process.env.EMBER_VERBOSE_FAKE_OPTION_2).to.be.ok; + expect(error.name).to.equal('SilentError'); + expect(error.message).to.equal( + 'The specified command fake-command is invalid. For available options, see `ember help`.' + ); }); }); - it('ignores verbose options after --', function() { - return verboseCommand(['fake_option_1', '--fake-option', 'fake_option_2']).then(function() { - expect(true, process.env.EMBER_VERBOSE_FAKE_OPTION_1, 'expected it to be true'); - expect(false, !process.env.EMBER_VERBOSE_FAKE_OPTION_2, 'expected it to be false'); - }); + it('ignores verbose options after --', function () { + return expect(verboseCommand(['fake_option_1', '--fake-option', 'fake_option_2'])).to.be.rejected.then( + (error) => { + expect(process.env.EMBER_VERBOSE_FAKE_OPTION_1).to.be.ok; + expect(process.env.EMBER_VERBOSE_FAKE_OPTION_2).to.not.be.ok; + expect(error.name).to.equal('SilentError'); + expect(error.message).to.equal( + 'The specified command fake-command is invalid. For available options, see `ember help`.' + ); + } + ); }); - it('ignores verbose options after -', function() { - return verboseCommand(['fake_option_1', '-f', 'fake_option_2']).then(function() { - expect(true, process.env.EMBER_VERBOSE_FAKE_OPTION_1, 'expected it to be true'); - expect(false, !process.env.EMBER_VERBOSE_FAKE_OPTION_2, 'expected it to be false'); + it('ignores verbose options after -', function () { + return expect(verboseCommand(['fake_option_1', '-f', 'fake_option_2'])).to.be.rejected.then((error) => { + expect(process.env.EMBER_VERBOSE_FAKE_OPTION_1).to.be.ok; + expect(process.env.EMBER_VERBOSE_FAKE_OPTION_2).to.not.be.ok; + expect(error.name).to.equal('SilentError'); + expect(error.message).to.equal( + 'The specified command fake-command is invalid. For available options, see `ember help`.' + ); }); }); }); diff --git a/tests/unit/cli/lookup-command-test.js b/tests/unit/cli/lookup-command-test.js index 98cf6da798..ea2ac2cca5 100644 --- a/tests/unit/cli/lookup-command-test.js +++ b/tests/unit/cli/lookup-command-test.js @@ -1,170 +1,184 @@ 'use strict'; -/*jshint expr: true*/ - -var expect = require('chai').expect; -var lookupCommand = require('../../../lib/cli/lookup-command'); -var Command = require('../../../lib/models/command'); -var Project = require('../../../lib/models/project'); -var MockUI = require('../../helpers/mock-ui'); -var AddonCommand = require('../../fixtures/addon/commands/addon-command'); -var OtherCommand = require('../../fixtures/addon/commands/other-addon-command'); -var ClassCommand = require('../../fixtures/addon/commands/addon-command-class'); -var OverrideCommand = require('../../fixtures/addon/commands/addon-override-intentional'); - -var commands = { + +const { expect } = require('chai'); +const lookupCommand = require('../../../lib/cli/lookup-command'); +const Command = require('../../../lib/models/command'); +const Project = require('../../../lib/models/project'); +const MockUI = require('console-ui/mock'); +const AddonCommand = require('../../fixtures/addon/commands/addon-command'); +const OtherCommand = require('../../fixtures/addon/commands/other-addon-command'); +const ClassCommand = require('../../fixtures/addon/commands/addon-command-class'); +const OverrideCommand = require('../../fixtures/addon/commands/addon-override-intentional'); + +let commands = { serve: Command.extend({ name: 'serve', aliases: ['s'], works: 'everywhere', - availableOptions: [ - { name: 'port', key: 'port', type: Number, default: 4200, required: true } - ], - run: function() {} - }) + availableOptions: [{ name: 'port', key: 'port', type: Number, default: 4200, required: true }], + run() {}, + }), }; -function AddonServeCommand() { return this; } -AddonServeCommand.prototype.includedCommands = function() { +function AddonServeCommand() { + this.name = 'FakeAddon'; + return this; +} +AddonServeCommand.prototype.includedCommands = function () { return { - 'Serve': { + Serve: { name: 'serve', - description: 'overrides the serve command' - } + description: 'overrides the serve command', + }, }; }; -describe('cli/lookup-command.js', function() { - var ui; - var project = { - isEmberCLIProject: function(){ return true; }, - initializeAddons: function() { +describe('cli/lookup-command.js', function () { + let ui; + let project = { + isEmberCLIProject() { + return true; + }, + initializeAddons() { this.addons = [new AddonCommand(), new OtherCommand(), new ClassCommand()]; }, addonCommands: Project.prototype.addonCommands, - eachAddonCommand: Project.prototype.eachAddonCommand + eachAddonCommand: Project.prototype.eachAddonCommand, }; - before(function(){ + before(function () { ui = new MockUI(); }); - it('lookupCommand() should find commands by name and aliases.', function() { + it('lookupCommand() should find commands by name and aliases.', function () { // Valid commands expect(lookupCommand(commands, 'serve')).to.exist; expect(lookupCommand(commands, 's')).to.exist; }); - it('lookupCommand() should find commands that addons add by name and aliases.', function() { - var command, Command; + it('lookupCommand() should find commands that addons add by name and aliases.', function () { + let command, Command; Command = lookupCommand(commands, 'addon-command', [], { - project: project, - ui: ui + project, + ui, }); command = new Command({ - ui: ui, - project: project + ui, + project, }); expect(command.name).to.equal('addon-command'); Command = lookupCommand(commands, 'ac', [], { - project: project, - ui: ui + project, + ui, }); command = new Command({ - ui: ui, - project: project + ui, + project, }); expect(command.name).to.equal('addon-command'); Command = lookupCommand(commands, 'other-addon-command', [], { - project: project, - ui: ui + project, + ui, }); command = new Command({ - ui: ui, - project: project + ui, + project, }); expect(command.name).to.equal('other-addon-command'); Command = lookupCommand(commands, 'oac', [], { - project: project, - ui: ui + project, + ui, }); command = new Command({ - ui: ui, - project: project + ui, + project, }); expect(command.name).to.equal('other-addon-command'); Command = lookupCommand(commands, 'class-addon-command', [], { - project: project, - ui: ui + project, + ui, }); command = new Command({ - ui: ui, - project: project + ui, + project, }); expect(command.name).to.equal('class-addon-command'); - }); - it('lookupCommand() should write out a warning when overriding a core command', function() { + it('lookupCommand() should write out a warning when overriding a core command', function () { project = { - isEmberCLIProject: function(){ return true; }, - initializeAddons: function() { + isEmberCLIProject() { + return true; + }, + initializeAddons() { this.addons = [new AddonServeCommand()]; }, addonCommands: Project.prototype.addonCommands, - eachAddonCommand: Project.prototype.eachAddonCommand + eachAddonCommand: Project.prototype.eachAddonCommand, }; lookupCommand(commands, 'serve', [], { - project: project, - ui: ui + project, + ui, }); - expect(ui.output).to.match(/warning: An ember-addon has attempted to override the core command "serve"\. The core command will be used.*/); + let re = new RegExp( + `WARNING: An ember-addon \\(FakeAddon\\) has attempted to override the core command "serve"\\. The core command will be used` + ); + expect(ui.output).to.match(re); }); - it('lookupCommand() should write out a warning when overriding a core command and allow it if intentional', function() { + it('lookupCommand() should write out a warning when overriding a core command and allow it if intentional', function () { project = { - isEmberCLIProject: function(){ return true; }, - initializeAddons: function() { + isEmberCLIProject() { + return true; + }, + initializeAddons() { this.addons = [new OverrideCommand()]; }, addonCommands: Project.prototype.addonCommands, - eachAddonCommand: Project.prototype.eachAddonCommand + eachAddonCommand: Project.prototype.eachAddonCommand, }; lookupCommand(commands, 'serve', [], { - project: project, - ui: ui + project, + ui, }); - expect(ui.output).to.match(/warning: An ember-addon has attempted to override the core command "serve"\. The addon command will be used as the overridding was explicit.*/); + let re = new RegExp( + `WARNING: An ember-addon \\(Other Ember CLI Addon Command To Test Intentional Core Command Override\\) has attempted to override the core command "serve"\\. The addon command will be used as the overridding was explicit.*` + ); + expect(ui.output).to.match(re); }); - it('lookupCommand() should return UnknownCommand object when command name is not present.', function() { - var Command = lookupCommand(commands, 'something-else', [], { - project: project, - ui: ui + it('lookupCommand() should return UnknownCommand object when command name is not present.', function () { + let Command = lookupCommand(commands, 'something-else', [], { + project, + ui, }); - var command = new Command({ - ui: ui, - project: project + let command = new Command({ + ui, + project, }); - expect(function() { + + expect(command.name).to.equal('something-else'); + + expect(() => { command.validateAndRun([]); }).to.throw(/command.*something-else.*is invalid/); }); diff --git a/tests/unit/commands/addon-test.js b/tests/unit/commands/addon-test.js index 2d31df82c9..a5073e8b3d 100644 --- a/tests/unit/commands/addon-test.js +++ b/tests/unit/commands/addon-test.js @@ -1,66 +1,97 @@ 'use strict'; -var expect = require('chai').expect ; -var commandOptions = require('../../factories/command-options'); -var AddonCommand = require('../../../lib/commands/addon'); -describe('addon command', function() { - var command, options; +const { expect } = require('chai'); +const commandOptions = require('../../factories/command-options'); +const AddonCommand = require('../../../lib/commands/addon'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const td = require('testdouble'); - beforeEach(function() { - options = commandOptions({ +describe('addon command', function () { + let command; + + beforeEach(function () { + let options = commandOptions({ project: { - isEmberCLIProject: function() { + isEmberCLIProject() { return false; - } + }, + blueprintLookupPaths() { + return []; + }, }, - settings: {} }); command = new AddonCommand(options); }); - it('doesn\'t allow to create an addon named `test`', function() { - return command.validateAndRun(['test']).then(function() { - expect(true, 'should have rejected with an addon name of test').to.be.false(); - }) - .catch(function(error) { + afterEach(function () { + td.reset(); + }); + + it("doesn't allow to create an addon named `test`", function () { + return expect(command.validateAndRun(['test'])).to.be.rejected.then((error) => { expect(error.message).to.equal('We currently do not support a name of `test`.'); }); }); - it('doesn\'t allow to create an addon named `ember`', function() { - return command.validateAndRun(['ember']).then(function() { - expect(true, 'should have rejected with an addon name of test').to.be.false(); - }) - .catch(function(error) { + it("doesn't allow to create an addon named `ember`", function () { + return expect(command.validateAndRun(['ember'])).to.be.rejected.then((error) => { expect(error.message).to.equal('We currently do not support a name of `ember`.'); }); }); - it('doesn\'t allow to create an addon named `vendor`', function() { - return command.validateAndRun(['vendor']).then(function() { - expect(true, 'should have rejected with an addon name of `vendor`').to.be.false(); - }) - .catch(function(error) { + it("doesn't allow to create an addon named `Ember`", function () { + return expect(command.validateAndRun(['Ember'])).to.be.rejected.then((error) => { + expect(error.message).to.equal('We currently do not support a name of `Ember`.'); + }); + }); + + it("doesn't allow to create an addon named `ember-cli`", function () { + return expect(command.validateAndRun(['ember-cli'])).to.be.rejected.then((error) => { + expect(error.message).to.equal('We currently do not support a name of `ember-cli`.'); + }); + }); + + it("doesn't allow to create an addon named `vendor`", function () { + return expect(command.validateAndRun(['vendor'])).to.be.rejected.then((error) => { expect(error.message).to.equal('We currently do not support a name of `vendor`.'); }); }); - it('doesn\'t allow to create an addon with a period in the name', function() { - return command.validateAndRun(['zomg.awesome']).then(function() { - expect(true, 'should have rejected with period in the addon name').to.be.false(); - }) - .catch(function(error) { + it("doesn't allow to create an addon with a period in the name", function () { + return expect(command.validateAndRun(['zomg.awesome'])).to.be.rejected.then((error) => { expect(error.message).to.equal('We currently do not support a name of `zomg.awesome`.'); }); }); - it('doesn\'t allow to create an addon with a name beginning with a number', function() { - return command.validateAndRun(['123-my-bagel']).then(function() { - expect(true, 'should have rejected with a name beginning with a number').to.be.false(); - }) - .catch(function(error) { + it("doesn't allow to create an addon with a name beginning with a number", function () { + return expect(command.validateAndRun(['123-my-bagel'])).to.be.rejected.then((error) => { expect(error.message).to.equal('We currently do not support a name of `123-my-bagel`.'); }); }); + + it("doesn't allow to create an addon when the name is a period", function () { + return expect(command.validateAndRun(['.'])).to.be.rejected.then((error) => { + expect(error.message).to.equal( + `Trying to generate an addon structure in this directory? Use \`ember init\` instead.` + ); + }); + }); + + it('throws when no addon name is provided', async function () { + expect(command.validateAndRun([])).to.be.rejectedWith( + 'The `ember addon` command requires a name to be specified. For more details, run `ember help`.' + ); + }); + + it('registers blueprint options in beforeRun', function () { + td.replace(Blueprint, 'lookup', td.function()); + + td.when(Blueprint.lookup('addon'), { ignoreExtraArgs: true }).thenReturn({ + availableOptions: [{ name: 'custom-blueprint-option', type: String }], + }); + + command.beforeRun(['addon']); + expect(command.availableOptions.map(({ name }) => name)).to.contain('custom-blueprint-option'); + }); }); diff --git a/tests/unit/commands/build-test.js b/tests/unit/commands/build-test.js index 847b5e625f..8846dffdec 100644 --- a/tests/unit/commands/build-test.js +++ b/tests/unit/commands/build-test.js @@ -1,80 +1,124 @@ 'use strict'; -var expect = require('chai').expect; -var stub = require('../../helpers/stub').stub; -var commandOptions = require('../../factories/command-options'); -var Task = require('../../../lib/models/task'); - -describe('build command', function() { - var BuildCommand; - var tasks, buildTaskInstance, buildWatchTaskInstance; - var options; - - before(function() { - BuildCommand = require('../../../lib/commands/build'); - }); +const { expect } = require('chai'); +const commandOptions = require('../../factories/command-options'); +const Task = require('../../../lib/models/task'); +const BuildCommand = require('../../../lib/commands/build'); +const td = require('testdouble'); + +describe('build command', function () { + let tasks, options, command; + let buildTaskInstance, buildWatchTaskInstance; - beforeEach(function() { + beforeEach(function () { tasks = { - Build: Task.extend({ - init: function() { + Build: class extends Task { + init() { + super.init(...arguments); buildTaskInstance = this; } - }), + }, - BuildWatch: Task.extend({ - init: function() { + BuildWatch: class extends Task { + init() { + super.init(...arguments); buildWatchTaskInstance = this; } - }) + }, + + ShowAssetSizes: class extends Task {}, }; options = commandOptions({ - tasks: tasks, - settings: {} + tasks, }); - stub(tasks.Build.prototype, 'run'); - stub(tasks.BuildWatch.prototype, 'run'); + command = new BuildCommand(options); + + td.replace(tasks.Build.prototype, 'run', td.function()); + td.replace(tasks.BuildWatch.prototype, 'run', td.function()); + td.replace(tasks.ShowAssetSizes.prototype, 'run', td.function()); + + td.when(tasks.Build.prototype.run(), { ignoreExtraArgs: true, times: 1 }).thenResolve(); + td.when(tasks.BuildWatch.prototype.run(), { ignoreExtraArgs: true, times: 1 }).thenResolve(); }); - after(function() { - BuildCommand = null; - buildWatchTaskInstance = null; - buildTaskInstance = null; + afterEach(function () { + td.reset(); }); - afterEach(function() { - tasks.Build.prototype.run.restore(); - tasks.BuildWatch.prototype.run.restore(); + it('Build task is provided with the project instance', function () { + return command.validateAndRun([]).then(function () { + expect(buildTaskInstance.project).to.equal(options.project, 'has correct project instance'); + }); }); - it('Build task is provided with the project instance', function() { - return new BuildCommand(options).validateAndRun([ ]).then(function() { - var buildRun = tasks.Build.prototype.run; + it('BuildWatch task is provided with the project instance', function () { + return command.validateAndRun(['--watch']).then(function () { + expect(buildWatchTaskInstance.project).to.equal(options.project, 'has correct project instance'); + }); + }); - expect(buildRun.called).to.equal(1, 'expected run to be called once'); - expect(buildTaskInstance.project).to.equal(options.project, 'has correct project instance'); + it('`--watch` throws in Vite-based projects', async function () { + command.isViteProject = true; + await expect(command.validateAndRun(['--watch'])).to.be.rejectedWith( + 'The `--watch` option to `ember build` is not supported in Vite-based projects. Please use `vite dev` instead.' + ); + }); + + it('`--watch` does not throw when EMBROIDER_PREBUILD is true', function () { + // setup the scenario + command.isViteProject = true; + process.env.EMBROIDER_PREBUILD = 'true'; + + return command + .validateAndRun(['--watch']) + .then(function () { + expect(buildWatchTaskInstance.project).to.equal(options.project, 'has correct project instance'); + }) + .finally(() => { + delete process.env.EMBROIDER_PREBUILD; + }); + }); + + it('`--watcher` throws in Vite-based projects', async function () { + command.isViteProject = true; + await expect(command.validateAndRun(['--watcher'])).to.be.rejectedWith( + 'The `--watcher` option to `ember build` is not supported in Vite-based projects. Please use `vite dev` instead.' + ); + }); + + it('BuildWatch task is provided with a watcher option', function () { + return command.validateAndRun(['--watch', '--watcher poller']).then(function () { + let buildWatchRun = tasks.BuildWatch.prototype.run; + + let captor = td.matchers.captor(); + td.verify(buildWatchRun(captor.capture()), { times: 1 }); + expect(captor.value.watcherPoller).to.equal(true, 'expected run to be called with a poller option'); }); }); - it('BuildWatch task is provided with the project instance', function() { - return new BuildCommand(options).validateAndRun([ '--watch' ]).then(function() { - var buildWatchRun = tasks.BuildWatch.prototype.run; + it('Asset Size Printer task is not run after Build task in non-production environment', function () { + return new BuildCommand(options).validateAndRun([]).then(function () { + let showSizesRun = tasks.ShowAssetSizes.prototype.run; - expect(buildWatchRun.called).to.equal(1, 'expected run to be called once'); - expect(buildWatchTaskInstance.project).to.equal(options.project, 'has correct project instance'); + td.verify(showSizesRun(), { ignoreExtraArgs: true, times: 0 }); }); }); - it('BuildWatch task is provided with a watcher option', function() { - return new BuildCommand(options).validateAndRun([ '--watch', '--watcher poller' ]).then(function() { - var buildWatchRun = tasks.BuildWatch.prototype.run, - calledWith = buildWatchRun.calledWith[0]['0']; + it('Asset Size Printer task is run after Build task in production environment', function () { + return new BuildCommand(options).validateAndRun(['--environment=production']).then(function () { + let showSizesRun = tasks.ShowAssetSizes.prototype.run; + + td.verify(showSizesRun(), { ignoreExtraArgs: true, times: 1 }); + }); + }); - expect(buildWatchRun.called).to.equal(1, 'expected run to be called once'); - expect(calledWith.watcherPoller).to.equal(true, 'expected run to be called with a poller option'); + it('Asset Size Printer task is not run if suppress sizes option is provided', function () { + return new BuildCommand(options).validateAndRun(['--suppress-sizes']).then(function () { + let showSizesRun = tasks.ShowAssetSizes.prototype.run; + td.verify(showSizesRun(), { ignoreExtraArgs: true, times: 0 }); }); }); }); diff --git a/tests/unit/commands/destroy-test.js b/tests/unit/commands/destroy-test.js index b9abc16d81..61397c8714 100644 --- a/tests/unit/commands/destroy-test.js +++ b/tests/unit/commands/destroy-test.js @@ -1,89 +1,102 @@ 'use strict'; -var DestroyCommand = require('../../../lib/commands/destroy'); -var Promise = require('../../../lib/ext/promise'); -var Task = require('../../../lib/models/task'); -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var MockProject = require('../../helpers/mock-project'); +const { expect } = require('chai'); +const EOL = require('os').EOL; +const MockProject = require('../../helpers/mock-project'); +const processHelpString = require('../../helpers/process-help-string'); +const commandOptions = require('../../factories/command-options'); +const Task = require('../../../lib/models/task'); +const DestroyCommand = require('../../../lib/commands/destroy'); -describe('generate command', function() { - var command; +describe('destroy command', function () { + let options, command, project; - beforeEach(function() { - var project = new MockProject(); + beforeEach(function () { + project = new MockProject(); - project.name = function() { - return 'some-random-name'; - }; - - project.isEmberCLIProject = function isEmberCLIProject() { + project.isEmberCLIProject = function () { return true; }; - command = new DestroyCommand(commandOptions({ - settings: {}, - - project: project, + options = commandOptions({ + project, tasks: { - DestroyFromBlueprint: Task.extend({ - project: project, - run: function(options) { + DestroyFromBlueprint: class extends Task { + init() { + super.init(...arguments); + + this.project = project; + } + + run(options) { return Promise.resolve(options); } - }) - } - })); + }, + }, + }); + + command = new DestroyCommand(options); }); - it('runs DestroyFromBlueprint with expected options', function() { - return command.validateAndRun(['controller', 'foo']) - .then(function(options) { - expect(options.dryRun, false); - expect(options.verbose, false); - expect(options.args).to.deep.equal(['controller', 'foo']); - }); + it('throws when the `--dummy` option is passed in a Vite project', async function () { + command.isViteProject = true; + await expect(command.validateAndRun(['controller', 'foo', '--dummy'])).to.be.rejectedWith( + 'The `--dummy` option to `ember destroy` is not supported in Vite-based projects.' + ); }); - it('complains if no entity name is given', function() { - return command.validateAndRun(['controller']) - .then(function() { - expect(false, 'should not have called run'); - }) - .catch(function(error) { - expect(error.message).to.equal( - 'The `ember destroy` command requires an ' + - 'entity name to be specified. ' + - 'For more details, use `ember help`.'); - }); + it('runs DestroyFromBlueprint with expected options', function () { + return command.validateAndRun(['controller', 'foo']).then(function (options) { + expect(options.dryRun).to.be.false; + expect(options.verbose).to.be.false; + expect(options.args).to.deep.equal(['controller', 'foo']); + }); }); - it('complains if no blueprint name is given', function() { - return command.validateAndRun([]) - .then(function() { - expect(false, 'should not have called run'); - }) - .catch(function(error) { - expect(error.message).to.equal( - 'The `ember destroy` command requires a ' + - 'blueprint name to be specified. ' + - 'For more details, use `ember help`.'); - }); + it('complains if no entity name is given', function () { + return expect(command.validateAndRun(['controller'])).to.be.rejected.then((error) => { + expect(error.message).to.equal( + 'The `ember destroy` command requires an ' + + 'entity name to be specified. ' + + 'For more details, use `ember help`.' + ); + }); }); - it('does not throws errors when beforeRun is invoked without the blueprint name', function() { - expect(function () { + it('complains if no blueprint name is given', function () { + return expect(command.validateAndRun([])).to.be.rejected.then((error) => { + expect(error.message).to.equal( + 'The `ember destroy` command requires a ' + + 'blueprint name to be specified. ' + + 'For more details, use `ember help`.' + ); + }); + }); + + it('does not throw errors when beforeRun is invoked without the blueprint name', function () { + expect(() => { command.beforeRun([]); }).to.not.throw(); }); - it('rethrows errors from beforeRun', function() { - return Promise.resolve(function(){ return command.beforeRun(['controller', 'foo']);}) - .then(function() { - expect(false, 'should not have called run'); - }) - .catch(function(error) { - expect(error.message).to.equal('undefined is not a function'); + it('rethrows errors from beforeRun', function () { + project.blueprintLookupPaths = undefined; + + expect(() => { + command.beforeRun(['controller', 'foo']); + }).to.throw(/(is not a function)|(has no method)/); + }); + + describe('help', function () { + it('prints extra info', function () { + command.printDetailedHelp(); + + let output = options.ui.output; + + let testString = processHelpString(`${EOL}\ + Run \`ember help generate\` to view a list of available blueprints.${EOL}`); + + expect(output).to.equal(testString); }); }); }); diff --git a/tests/unit/commands/generate-test.js b/tests/unit/commands/generate-test.js index 65862ac544..65292e5b6e 100644 --- a/tests/unit/commands/generate-test.js +++ b/tests/unit/commands/generate-test.js @@ -1,89 +1,389 @@ 'use strict'; -var GenerateCommand = require('../../../lib/commands/generate'); -var Promise = require('../../../lib/ext/promise'); -var Task = require('../../../lib/models/task'); -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var SilentError = require('silent-error'); -var MockProject = require('../../helpers/mock-project'); - -describe('generate command', function() { - var command; - - beforeEach(function() { - var project = new MockProject(); - project.name = function() { - return 'some-random-name'; - }; +const { expect } = require('chai'); +const EOL = require('os').EOL; +const commandOptions = require('../../factories/command-options'); +const processHelpString = require('../../helpers/process-help-string'); +const MockProject = require('../../helpers/mock-project'); +const Task = require('../../../lib/models/task'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const GenerateCommand = require('../../../lib/commands/generate'); +const td = require('testdouble'); +const ROOT = process.cwd(); +const { createTempDir } = require('broccoli-test-helper'); +const { ERRORS } = GenerateCommand; + +describe('generate command', function () { + let input, options, command; + + beforeEach(async function () { + input = await createTempDir(); + process.chdir(input.path()); + + let project = new MockProject({ root: input.path() }); - project.isEmberCLIProject = function isEmberCLIProject() { + project.isEmberCLIProject = function () { return true; }; - project.blueprintLookupPaths = function() { + project.blueprintLookupPaths = function () { return []; }; - //nodeModulesPath: 'somewhere/over/the/rainbow' - command = new GenerateCommand(commandOptions({ - settings: {}, - - project: project, + options = commandOptions({ + project, tasks: { - GenerateFromBlueprint: Task.extend({ - project: project, - run: function(options) { + GenerateFromBlueprint: class extends Task { + init() { + super.init(...arguments); + + this.project = project; + } + + run(options) { return Promise.resolve(options); } - }) - } - })); + }, + }, + }); + + command = new GenerateCommand(options); }); - it('runs GenerateFromBlueprint but with null nodeModulesPath', function() { - command.project.hasDependencies = function() { return false; }; + afterEach(async function () { + td.reset(); - expect(function() { - command.validateAndRun(['controller', 'foo']); - }).to.throw(SilentError, 'node_modules appears empty, you may need to run `npm install`'); + process.chdir(ROOT); + await input.dispose(); }); - it('runs GenerateFromBlueprint with expected options', function() { - return command.validateAndRun(['controller', 'foo']) - .then(function(options) { - expect(options.pod, false); - expect(options.dryRun, false); - expect(options.verbose, false); - expect(options.args).to.deep.equal(['controller', 'foo']); - }); + it('throws when the `--dummy` option is passed in a Vite project', async function () { + command.isViteProject = true; + await expect(command.validateAndRun(['controller', 'foo', '--dummy'])).to.be.rejectedWith( + 'The `--dummy` option to `ember generate` is not supported in Vite-based projects.' + ); + }); + + it('runs GenerateFromBlueprint but with null nodeModulesPath with npm', function () { + command.project.hasDependencies = function () { + return false; + }; + + return expect(command.validateAndRun(['controller', 'foo'])).to.be.rejected.then((reason) => { + expect(reason.message).to.eql( + 'Required packages are missing, run `npm install` from this directory to install them.' + ); + }); + }); + + it('runs GenerateFromBlueprint but with null nodeModulesPath with yarn', function () { + // force usage of `yarn` by adding yarn.lock file + input.write({ + 'yarn.lock': '', + }); + + command.project.hasDependencies = function () { + return false; + }; + + return expect(command.validateAndRun(['controller', 'foo'])).to.be.rejected.then((reason) => { + expect(reason.message).to.eql( + 'Required packages are missing, run `yarn install` from this directory to install them.' + ); + }); + }); + + it('runs GenerateFromBlueprint with expected options', function () { + return command.validateAndRun(['controller', 'foo']).then(function (options) { + expect(options.pod).to.be.false; + expect(options.dryRun).to.be.false; + expect(options.verbose).to.be.false; + expect(options.args).to.deep.equal(['controller', 'foo']); + }); }); - it('does not throws errors when beforeRun is invoked without the blueprint name', function() { - expect(function () { + it('does not throw errors when beforeRun is invoked without the blueprint name', function () { + expect(() => { command.beforeRun([]); }).to.not.throw(); }); - it('complains if no blueprint name is given', function() { - return command.validateAndRun([]) - .then(function() { - expect(false, 'should not have called run'); - }) - .catch(function(error) { - expect(error.message).to.equal( - 'The `ember generate` command requires a ' + - 'blueprint name to be specified. ' + - 'For more details, use `ember help`.'); - }); + it('complains if no blueprint name is given', function () { + return expect(command.validateAndRun([])).to.be.rejected.then((error) => { + expect(error.message).to.equal(ERRORS.UNKNOWN_BLUEPRINT_ERROR); + }); }); - it('complains if --help is called for non-existent blueprint.', function() { - return Promise.resolve(command.printDetailedHelp({rawArgs:['foo','-h']})) - .then(function() { - expect(command.ui.output).to.include( - 'The \'foo\' blueprint does not exist in this project.'); + describe('help', function () { + beforeEach(function () { + td.replace(Blueprint, 'list', td.function()); + }); + + it('lists available blueprints', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + availableOptions: [], + printBasicHelp() { + return this.name; + }, + }, + { + name: 'other-blueprint', + availableOptions: [], + printBasicHelp() { + return this.name; + }, + }, + ], + }, + ]); + + command.printDetailedHelp({}); + + let output = options.ui.output; + + let testString = processHelpString(`${EOL}\ + Available blueprints:${EOL}\ + my-app:${EOL}\ +my-blueprint${EOL}\ +other-blueprint${EOL}\ +${EOL}`); + + expect(output).to.equal(testString); + }); + + it('lists available blueprints json', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + availableOptions: [], + getJson() { + return { + name: this.name, + }; + }, + }, + { + name: 'other-blueprint', + availableOptions: [], + getJson() { + return { + name: this.name, + }; + }, + }, + ], + }, + ]); + + let json = {}; + + command.addAdditionalJsonForHelp(json, { + json: true, + }); + + expect(json.availableBlueprints).to.deep.equal([ + { + 'my-app': [ + { + name: 'my-blueprint', + }, + { + name: 'other-blueprint', + }, + ], + }, + ]); + }); + + it('works with single blueprint', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + availableOptions: [], + printBasicHelp() { + return this.name; + }, + }, + { + name: 'skipped-blueprint', + }, + ], + }, + ]); + + command.printDetailedHelp({ + rawArgs: ['my-blueprint'], + }); + + let output = options.ui.output; + + let testString = processHelpString(`\ +my-blueprint${EOL}\ +${EOL}`); + + expect(output).to.equal(testString); + }); + + it('works with single blueprint json', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + availableOptions: [], + getJson() { + return { + name: this.name, + }; + }, + }, + { + name: 'skipped-blueprint', + }, + ], + }, + ]); + + let json = {}; + + command.addAdditionalJsonForHelp(json, { + rawArgs: ['my-blueprint'], + json: true, + }); + + expect(json.availableBlueprints).to.deep.equal([ + { + 'my-app': [ + { + name: 'my-blueprint', + }, + ], + }, + ]); + }); + + it('handles missing blueprint', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + }, + ], + }, + ]); + + command.printDetailedHelp({ + rawArgs: ['missing-blueprint'], + }); + + let output = options.ui.output; + + let testString = processHelpString(`\ +\u001b[33mThe 'missing-blueprint' blueprint does not exist in this project.\u001b[39m${EOL}\ +${EOL}`); + + expect(output).to.equal(testString); + }); + + it('handles missing blueprint json', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + }, + ], + }, + ]); + + let json = {}; + + command.addAdditionalJsonForHelp(json, { + rawArgs: ['missing-blueprint'], + json: true, + }); + + expect(json.availableBlueprints).to.deep.equal([ + { + 'my-app': [], + }, + ]); + }); + + it('ignores overridden blueprints when verbose false', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + availableOptions: [], + printBasicHelp() { + return this.name; + }, + overridden: true, + }, + ], + }, + ]); + + command.printDetailedHelp({}); + + let output = options.ui.output; + + let testString = processHelpString(`${EOL}\ + Available blueprints:${EOL}\ +${EOL}`); + + expect(output).to.equal(testString); + }); + + it('shows overridden blueprints when verbose true', function () { + td.when(Blueprint.list(), { ignoreExtraArgs: true }).thenReturn([ + { + source: 'my-app', + blueprints: [ + { + name: 'my-blueprint', + availableOptions: [], + printBasicHelp() { + return this.name; + }, + overridden: true, + }, + ], + }, + ]); + + command.printDetailedHelp({ + verbose: true, }); + + let output = options.ui.output; + + let testString = processHelpString(`${EOL}\ + Available blueprints:${EOL}\ + my-app:${EOL}\ +my-blueprint${EOL}\ +${EOL}`); + + expect(output).to.equal(testString); + }); }); }); diff --git a/tests/unit/commands/help-test.js b/tests/unit/commands/help-test.js index 9726152ccd..6391aa4f07 100644 --- a/tests/unit/commands/help-test.js +++ b/tests/unit/commands/help-test.js @@ -1,173 +1,463 @@ 'use strict'; -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); -var MockAnalytics = require('../../helpers/mock-analytics'); -var Command = require('../../../lib/models/command'); -var Project = require('../../../lib/models/project'); -var AddonCommand = require('../../fixtures/addon/commands/addon-command'); - -describe('help command', function() { - var ui; - var analytics; - - var commands = { - 'TestCommand1': Command.extend({ - name: 'test-command-1', - description: 'command-description', - aliases: ['t1', 'test-1'], - availableOptions: [ - { name: 'option-with-default', type: String, default: 'default-value' }, - { name: 'required-option', type: String, required: 'true', description: 'option-descriptionnnn' } - ], - run: function() {} - }), - 'TestCommand2': Command.extend({ - name: 'test-command-2', - aliases: ['t2', 'test-2'], - run: function() {} - }), - 'TestCommand3': Command.extend({ - name: 'test-command-3', - description: 'This is test command 3 description', - skipHelp: true, - aliases: ['t3', 'test-3'], - run: function() {} - }) - }; - - var HelpCommand = require('../../../lib/commands/help'); - - beforeEach(function() { - ui = new MockUI(); - analytics = new MockAnalytics(); +const { expect } = require('chai'); +const EOL = require('os').EOL; +const processHelpString = require('../../helpers/process-help-string'); +const convertToJson = require('../../helpers/convert-help-output-to-json'); +const commandOptions = require('../../factories/command-options'); +const td = require('testdouble'); + +let HelpCommand = require('../../../lib/commands/help'); + +describe('help command', function () { + let options; + + beforeEach(function () { + options = commandOptions(); }); - it('should generate complete help output, including aliases', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: { isEmberCLIProject: function(){ return true; }}, - settings: {} - }).validateAndRun([]).then(function() { - expect(ui.output).to.include('Usage: ember'); - expect(ui.output).to.include('ember test-command-1'); - expect(ui.output).to.include('command-description'); - expect(ui.output).to.include('option-with-default'); - expect(ui.output).to.include('(Default: default-value)'); - expect(ui.output).to.include('required-option'); - expect(ui.output).to.include('(Required)'); - expect(ui.output).to.include('ember test-command-2'); - expect(ui.output).to.include('aliases:'); - expect(ui.output).to.not.include('ember test-command-3'); + describe('common to both', function () { + it('finds command on disk', function () { + let Command1 = function () {}; + Command1.prototype.printBasicHelp = td.function(); + Command1.prototype.printDetailedHelp = td.function(); + + options.commands = { + Command1, + }; + + let command = new HelpCommand(options); + + let wasCalled; + command._lookupCommand = function () { + expect(arguments[0]).to.equal(options.commands); + expect(arguments[1]).to.equal('command-2'); + wasCalled = true; + return Command1; + }; + + command.run(options, ['command-2']); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + expect(wasCalled).to.be.true; }); - }); - it('should generate specific help output', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: { isEmberCLIProject: function(){ return true; }}, - settings: {} - }).validateAndRun(['test-command-2']).then(function() { - expect(ui.output).to.include('test-command-2'); - expect(ui.output).to.not.include('test-command-1'); - expect(ui.output).to.not.include('test-command-3'); + it('looks up multiple commands', function () { + let Command1 = function () {}; + let Command2 = function () {}; + let Command3 = function () {}; + Command1.prototype.printBasicHelp = td.function(); + Command2.prototype.printBasicHelp = td.function(); + Command3.prototype.printBasicHelp = td.function(); + Command1.prototype.printDetailedHelp = td.function(); + Command2.prototype.printDetailedHelp = td.function(); + Command3.prototype.printDetailedHelp = td.function(); + + options.commands = { + Command1, + Command2, + Command3, + }; + + let command = new HelpCommand(options); + + command.run(options, ['command-1', 'command-2']); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command2.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command3.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 0 }); + td.verify(Command1.prototype.printDetailedHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command2.prototype.printDetailedHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command3.prototype.printDetailedHelp(), { ignoreExtraArgs: true, times: 0 }); }); }); - it('should generate specific help output when given an alias', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: { isEmberCLIProject: function(){ return true; }}, - settings: {} - }).validateAndRun(['t1']).then(function() { - expect(ui.output).to.include('test-command-1'); - expect(ui.output).to.not.include('test-command-2'); - expect(ui.output).to.not.include('test-command-3'); + describe('unique to text printing', function () { + it('lists commands', function () { + let Command1 = function () {}; + let Command2 = function () {}; + Command1.prototype.printBasicHelp = td.function(); + Command2.prototype.printBasicHelp = td.function(); + Command1.prototype.printDetailedHelp = td.function(); + Command2.prototype.printDetailedHelp = td.function(); + + options.commands = { + Command1, + Command2, + }; + + let command = new HelpCommand(options); + + command.run(options, []); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command2.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command1.prototype.printDetailedHelp(), { ignoreExtraArgs: true, times: 0 }); + td.verify(Command2.prototype.printDetailedHelp(), { ignoreExtraArgs: true, times: 0 }); }); - }); - describe('addon commands', function() { - var projectWithAddons = { - isEmberCLIProject: function(){ return true; }, - initializeAddons: function() { - this.addons = [new AddonCommand()]; - }, - addonCommands: Project.prototype.addonCommands, - eachAddonCommand: Project.prototype.eachAddonCommand - }; - - it('should generate complete help output, including aliases', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: projectWithAddons, - settings: {} - }).validateAndRun([]).then(function() { - expect(ui.output).to.include('Available commands in ember-cli'); - expect(ui.output).to.include('test-command-1'); - expect(ui.output).to.include('Available commands from Ember CLI Addon Command Test'); - expect(ui.output).to.include('addon-command'); - expect(ui.output).to.include('aliases:'); - }); - }); - - it('should generate specific help output', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: projectWithAddons, - settings: {} - }).validateAndRun(['addon-command']).then(function() { - expect(ui.output).to.include('addon-command'); - expect(ui.output).to.not.include('No help entry for'); - }); - }); - - it('should generate specific help output when given an alias', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: projectWithAddons, - settings: {} - }).validateAndRun(['ac']).then(function() { - expect(ui.output).to.include('addon-command'); - expect(ui.output).to.not.include('No help entry for'); - }); + it('works with single command', function () { + let Command1 = function () {}; + let Command2 = function () {}; + Command1.prototype.printBasicHelp = td.function(); + Command2.prototype.printBasicHelp = td.function(); + Command1.prototype.printDetailedHelp = td.function(); + Command2.prototype.printDetailedHelp = td.function(); + + options.commands = { + Command1, + Command2, + }; + + let command = new HelpCommand(options); + + command.run(options, ['command-1']); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command2.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 0 }); + td.verify(Command1.prototype.printDetailedHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command2.prototype.printDetailedHelp(), { ignoreExtraArgs: true, times: 0 }); }); - }); - it('should generate "no help entry" message for non-existent commands', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: { isEmberCLIProject: function(){ return true; }}, - settings: {} - }).validateAndRun(['heyyy']).then(function() { - expect(ui.output).to.include('No help entry for'); + it('works with single command alias', function () { + let Command1 = function () {}; + Command1.prototype.aliases = ['my-alias']; + Command1.prototype.printBasicHelp = td.function(); + Command1.prototype.printDetailedHelp = td.function(); + + options.commands = { + Command1, + }; + + let command = new HelpCommand(options); + + command.run(options, ['my-alias']); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + }); + + it('passes extra commands to `generate`', function () { + let Generate = function () {}; + Generate.prototype.printBasicHelp = td.function(); + Generate.prototype.printDetailedHelp = td.function(); + + options.commands = { + Generate, + }; + + let command = new HelpCommand(options); + + command.run(options, ['generate', 'something', 'else']); + + let captor = td.matchers.captor(); + + td.verify(Generate.prototype.printBasicHelp(captor.capture()), { times: 1 }); + expect(captor.value.rawArgs).to.deep.equal(['something', 'else']); + + td.verify(Generate.prototype.printDetailedHelp(captor.capture()), { times: 1 }); + expect(captor.value.rawArgs).to.deep.equal(['something', 'else']); + }); + + it('handles no extra commands to `generate`', function () { + let Generate = function () {}; + Generate.prototype.printBasicHelp = td.function(); + Generate.prototype.printDetailedHelp = td.function(); + + options.commands = { + Generate, + }; + + let command = new HelpCommand(options); + + command.run(options, ['generate']); + + let captor = td.matchers.captor(); + + td.verify(Generate.prototype.printBasicHelp(captor.capture()), { times: 1 }); + expect(captor.value.rawArgs).to.be.undefined; + + td.verify(Generate.prototype.printDetailedHelp(captor.capture()), { times: 1 }); + expect(captor.value.rawArgs).to.be.undefined; + }); + + it('passes extra commands to `generate` alias', function () { + let Generate = function () {}; + Generate.prototype.aliases = ['g']; + Generate.prototype.printBasicHelp = td.function(); + Generate.prototype.printDetailedHelp = td.function(); + + options.commands = { + Generate, + }; + + let command = new HelpCommand(options); + + command.run(options, ['g', 'something', 'else']); + + let captor = td.matchers.captor(); + + td.verify(Generate.prototype.printBasicHelp(captor.capture()), { times: 1 }); + expect(captor.value.rawArgs).to.deep.equal(['something', 'else']); + + td.verify(Generate.prototype.printDetailedHelp(captor.capture()), { times: 1 }); + expect(captor.value.rawArgs).to.deep.equal(['something', 'else']); + }); + + it('handles missing command', function () { + let Command1 = function () {}; + + options.commands = { + Command1, + }; + + let command = new HelpCommand(options); + + command.run(options, ['missing-command']); + + let output = options.ui.output; + + let testString = processHelpString(`\ +Requested ember-cli commands:${EOL}\ +${EOL}\ +\u001b[31mNo help entry for 'missing-command'\u001b[39m${EOL}`); + + expect(output).to.include(testString); + }); + + it('respects skipHelp when listing', function () { + let Command1 = function () { + this.skipHelp = true; + }; + let Command2 = function () {}; + Command1.prototype.printBasicHelp = td.function(); + Command2.prototype.printBasicHelp = td.function(); + + options.commands = { + Command1, + Command2, + }; + + let command = new HelpCommand(options); + + command.run(options, []); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 0 }); + td.verify(Command2.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + }); + + it('ignores skipHelp when single', function () { + let Command1 = function () { + this.skipHelp = true; + }; + Command1.prototype.printBasicHelp = td.function(); + Command1.prototype.printDetailedHelp = td.function(); + + options.commands = { + Command1, + }; + + let command = new HelpCommand(options); + + command.run(options, ['command-1']); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + }); + + it('lists addons', function () { + let Command1 = function () {}; + let Command2 = function () {}; + Command1.prototype.printBasicHelp = td.function(); + Command2.prototype.printBasicHelp = td.function(); + + options.project.eachAddonCommand = function (callback) { + callback('my-addon', { + Command1, + Command2, + }); + }; + + let command = new HelpCommand(options); + + command.run(options, []); + + let output = options.ui.output; + + let testString = processHelpString(`${EOL}\ +Available commands from my-addon:${EOL}`); + + expect(output).to.include(testString); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + td.verify(Command2.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); + }); + + it('finds single addon command', function () { + let Command1 = function () {}; + let Command2 = function () {}; + Command1.prototype.printBasicHelp = td.function(); + Command1.prototype.printDetailedHelp = td.function(); + + options.project.eachAddonCommand = function (callback) { + callback('my-addon', { + Command1, + Command2, + }); + }; + + let command = new HelpCommand(options); + + command.run(options, ['command-1']); + + td.verify(Command1.prototype.printBasicHelp(), { ignoreExtraArgs: true, times: 1 }); }); }); - it('should generate specific help output for commands with skipHelp', function() { - return new HelpCommand({ - ui: ui, - analytics: analytics, - commands: commands, - project: { isEmberCLIProject: function(){ return true; }}, - settings: {} - }).validateAndRun(['test-command-3']).then(function() { - expect(ui.output).to.include('test-command-3'); - expect(ui.output).to.not.include('test-command-1'); - expect(ui.output).to.not.include('test-command-2'); + describe('unique to json printing', function () { + beforeEach(function () { + options.json = true; + }); + + it('lists commands', function () { + let Command1 = function () { + return { + getJson() { + return { + test1: 'bar', + }; + }, + }; + }; + + let Command2 = function () { + return { + getJson() { + return { + test2: 'bar', + }; + }, + }; + }; + + options.commands = { Command1, Command2 }; + + let command = new HelpCommand(options); + + command.run(options, []); + + let json = convertToJson(options.ui.output); + + expect(json.commands).to.deep.equal([ + { + test1: 'bar', + }, + { + test2: 'bar', + }, + ]); + }); + + it('handles special option `Path`', function () { + let Command1 = function () { + return { + getJson() { + return { + test1: 'Path', + }; + }, + }; + }; + + options.commands = { Command1 }; + + let command = new HelpCommand(options); + + command.run(options, ['command-1']); + + let json = convertToJson(options.ui.output); + + expect(json.commands).to.deep.equal([ + { + test1: 'Path', + }, + ]); + }); + + it('respects skipHelp when listing', function () { + let Command1 = function () { + return { + skipHelp: true, + }; + }; + + let Command2 = function () { + return { + getJson() { + return { + test2: 'bar', + }; + }, + }; + }; + + options.commands = { Command1, Command2 }; + + let command = new HelpCommand(options); + + command.run(options, []); + + let json = convertToJson(options.ui.output); + + expect(json.commands).to.deep.equal([ + { + test2: 'bar', + }, + ]); + }); + + it('lists addons', function () { + let Command1 = function () { + return { + getJson() { + return { + test1: 'foo', + }; + }, + }; + }; + + let Command2 = function () { + return { + getJson() { + return { + test2: 'bar', + }; + }, + }; + }; + + options.project.eachAddonCommand = function (callback) { + callback('my-addon', { Command1, Command2 }); + }; + + let command = new HelpCommand(options); + + command.run(options, []); + + let json = convertToJson(options.ui.output); + + expect(json.addons).to.deep.equal([ + { + name: 'my-addon', + commands: [ + { + test1: 'foo', + }, + { + test2: 'bar', + }, + ], + }, + ]); }); }); }); diff --git a/tests/unit/commands/init-test.js b/tests/unit/commands/init-test.js index 30fa9b39fe..24606d2ce4 100644 --- a/tests/unit/commands/init-test.js +++ b/tests/unit/commands/init-test.js @@ -1,257 +1,240 @@ 'use strict'; -var fs = require('fs'); -var os = require('os'); -var path = require('path'); -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); -var MockAnalytics = require('../../helpers/mock-analytics'); -var Promise = require('../../../lib/ext/promise'); -var Project = require('../../../lib/models/project'); -var Task = require('../../../lib/models/task'); - -describe('init command', function() { - var InitCommand; - var ui; - var analytics; - var project; - var tasks; - - beforeEach(function() { +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); +const { expect } = require('chai'); +const MockUI = require('console-ui/mock'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const Project = require('../../../lib/models/project'); +const Task = require('../../../lib/models/task'); +const InitCommand = require('../../../lib/commands/init'); +const MockCLI = require('../../helpers/mock-cli'); +const td = require('testdouble'); + +const { DEPRECATIONS } = require('../../../lib/debug'); +const { isExperimentEnabled } = require('@ember-tooling/blueprint-model/utilities/experiments'); + +describe('init command', function () { + let ui, tasks, command, workingDir; + + beforeEach(function () { ui = new MockUI(); - analytics = new MockAnalytics(); tasks = { - InstallBlueprint: Task.extend({}), - NpmInstall: Task.extend({}), - BowerInstall: Task.extend({}) + GenerateFromBlueprint: class extends Task {}, + InstallBlueprint: class extends Task {}, + NpmInstall: class extends Task {}, }; - project = new Project(process.cwd(), { name: 'some-random-name'}); - InitCommand = require('../../../lib/commands/init'); + let tmpDir = os.tmpdir(); + workingDir = `${tmpDir}/ember-cli-test-project`; + fs.mkdirSync(workingDir); }); - it('doesn\'t allow to create an application named `test`', function() { - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: 'test'}), - tasks: tasks, - settings: {} - }); + afterEach(function () { + fs.removeSync(workingDir); + td.reset(); + }); + + function buildCommand(projectOpts) { + let cli = new MockCLI({ ui }); + let options = { + ui, + project: new Project(process.cwd(), projectOpts || { name: 'some-random-name' }, ui, cli), + tasks, + settings: {}, + }; - return command.validateAndRun([]).then(function() { - expect(false, 'should have rejected with an application name of test'); - }) - .catch(function(error) { + command = new InitCommand(options); + } + + it("doesn't allow to create an application named `test`", function () { + buildCommand({ name: 'test' }); + + return expect(command.validateAndRun([])).to.be.rejected.then((error) => { expect(error.message).to.equal('We currently do not support a name of `test`.'); }); }); - it('doesn\'t allow to create an application without project name', function() { - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: undefined}), - tasks: tasks, - settings: {} - }); + it("doesn't allow to create an application without project name", function () { + buildCommand({ name: undefined }); - return command.validateAndRun([]).then(function() { - expect(false, 'should have rejected with an application without project name'); - }) - .catch(function(error) { - expect(error.message).to.equal('The `ember init` command requires a package.json in current folder with name attribute or a specified name via arguments. For more details, use `ember help`.'); + return expect(command.validateAndRun([])).to.be.rejected.then((error) => { + expect(error.message).to.equal( + 'The `ember init` command requires a package.json in current folder with name attribute or a specified name via arguments. For more details, use `ember help`.' + ); }); }); - it('Uses the name of the closest project to when calling installBlueprint', function() { - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + it('Uses the name of the closest project to when calling installBlueprint', function () { + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.rawName).to.equal('some-random-name'); return Promise.reject('Called run'); } - }); + }; - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: project, - tasks: tasks, - settings: {} - }); + buildCommand(); - return command.validateAndRun([]) - .catch(function(reason) { - expect(reason).to.equal('Called run'); - }); + return command.validateAndRun([]).catch(function (reason) { + expect(reason).to.equal('Called run'); + }); }); - it('Uses the provided app name over the closest found project', function() { - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + it('Uses the provided app name over the closest found project', function () { + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.rawName).to.equal('provided-name'); return Promise.reject('Called run'); } - }); + }; - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: 'some-random-name'}), - tasks: tasks, - settings: {} - }); + buildCommand(); - return command.validateAndRun(['--name=provided-name']) - .catch(function(reason) { - expect(reason).to.equal('Called run'); - }); + return command.validateAndRun(['--name=provided-name']).catch(function (reason) { + expect(reason).to.equal('Called run'); + }); }); - - it('Uses process.cwd if no package is found when calling installBlueprint', function() { - // change the working dir so `process.cwd` can't be a invalid path for base directories + it('Uses process.cwd if no package is found when calling installBlueprint', function () { + // change the working dir so `process.cwd` can't be an invalid path for base directories // named `ember-cli`. - var tmpDir = os.tmpdir(); - var workingDir = tmpDir + '/ember-cli-test-project'; - var currentWorkingDir = process.cwd(); + let currentWorkingDir = process.cwd(); - fs.mkdirSync(workingDir); process.chdir(workingDir); - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.rawName).to.equal(path.basename(process.cwd())); return Promise.reject('Called run'); } - }); + }; - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: path.basename(process.cwd()) }), - tasks: tasks, - settings: {} - }); + buildCommand({ name: path.basename(process.cwd()) }); - return command.validateAndRun([]) - .catch(function(reason) { + return command + .validateAndRun([]) + .catch(function (reason) { expect(reason).to.equal('Called run'); }) - .then(function() { + .then(function () { process.chdir(currentWorkingDir); - fs.rmdirSync(workingDir); }); }); - it('doesn\'t use --dry-run or any other command option as the name', function() { - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + it("doesn't use --dry-run or any other command option as the name", function () { + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.rawName).to.equal('some-random-name'); return Promise.reject('Called run'); } - }); + }; - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: 'some-random-name'}), - tasks: tasks, - settings: {} - }); + buildCommand(); - return command.validateAndRun(['--dry-run']) - .catch(function(reason) { - expect(reason).to.equal('Called run'); - }); + return command.validateAndRun(['--dry-run']).catch(function (reason) { + expect(reason).to.equal('Called run'); + }); }); - it('doesn\'t use . as the name', function() { - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + // FIXME: This test is wrong. + // This behavior only works because "." is passed as a target file + // to the app blueprint, which in turn fails to create files + it("doesn't use . as the name", function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.rawName).to.equal('some-random-name'); + expect(blueprintOpts.rawArgs).to.equal('.'); // <-- this is wrong return Promise.reject('Called run'); } - }); + }; - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: 'some-random-name'}), - tasks: tasks, - settings: {} - }); + buildCommand(); - return command.validateAndRun(['.']) - .catch(function(reason) { - expect(reason).to.equal('Called run'); - }); + return command.validateAndRun(['.']).catch(function (reason) { + expect(reason).to.equal('Called run'); + }); }); - it('Uses the "app" blueprint by default', function() { - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + it('Uses the "app" blueprint by default', function () { + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.blueprint).to.equal('app'); return Promise.reject('Called run'); } - }); + }; - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: 'some-random-name'}), - tasks: tasks, - settings: {} - }); + buildCommand(); - return command.validateAndRun(['--name=provided-name']) - .catch(function(reason) { - expect(reason).to.equal('Called run'); - }); + return command.validateAndRun(['--name=provided-name']).catch(function (reason) { + expect(reason).to.equal('Called run'); + }); }); - it('Uses arguments to select files to init', function() { - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + it('Uses arguments to select files to init', function () { + if (DEPRECATIONS.INIT_TARGET_FILES.isRemoved || isExperimentEnabled('VITE')) { + this.skip(); + } + + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.blueprint).to.equal('app'); return Promise.reject('Called run'); } - }); + }; - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { name: 'some-random-name'}), - tasks: tasks, - settings: {} - }); + buildCommand(); - return command.validateAndRun(['package.json', '--name=provided-name']) - .catch(function(reason) { - expect(reason).to.equal('Called run'); - }); + return command.validateAndRun(['package.json', '--name=provided-name']).catch(function (reason) { + expect(reason).to.equal('Called run'); + }); }); - it('Uses the "addon" blueprint for addons', function() { - tasks.InstallBlueprint = Task.extend({ - run: function(blueprintOpts) { + it('Uses the "addon" blueprint for addons', function () { + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { expect(blueprintOpts.blueprint).to.equal('addon'); return Promise.reject('Called run'); } + }; + + buildCommand({ keywords: ['ember-addon'], name: 'some-random-name' }); + + return command.validateAndRun(['--name=provided-name']).catch(function (reason) { + expect(reason).to.equal('Called run'); }); + }); - var command = new InitCommand({ - ui: ui, - analytics: analytics, - project: new Project(process.cwd(), { keywords: [ 'ember-addon' ], name: 'some-random-name'}), - tasks: tasks, - settings: {} + it('Registers blueprint options in beforeRun', function () { + td.replace(Blueprint, 'lookup', td.function()); + td.when(Blueprint.lookup('app'), { ignoreExtraArgs: true }).thenReturn({ + availableOptions: [{ name: 'custom-blueprint-option', type: String }], }); - return command.validateAndRun(['--name=provided-name']) - .catch(function(reason) { - expect(reason).to.equal('Called run'); - }); + buildCommand(); + + command.beforeRun(['app']); + expect(command.availableOptions.map(({ name }) => name)).to.contain('custom-blueprint-option'); + }); + + it('Passes command options through to the install blueprint task', function () { + tasks.InstallBlueprint = class extends Task { + run(blueprintOpts) { + expect(blueprintOpts).to.contain.keys('customOption'); + expect(blueprintOpts.customOption).to.equal('customValue'); + return Promise.reject('Called run'); + } + }; + + buildCommand(); + + return expect(command.validateAndRun(['--custom-option=customValue'])).to.be.rejected.then((reason) => { + expect(reason).to.equal('Called run'); + }); }); }); diff --git a/tests/unit/commands/install-addon-test.js b/tests/unit/commands/install-addon-test.js deleted file mode 100644 index 0dec325600..0000000000 --- a/tests/unit/commands/install-addon-test.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var InstallAddonCommand = require('../../../lib/commands/install-addon'); -var commandOptions = require('../../factories/command-options'); -var AddonInstall = require('../../../lib/tasks/addon-install'); -var Task = require('../../../lib/models/task'); -var Promise = require('../../../lib/ext/promise'); -var stub = require('../../helpers/stub').stub; -var MockProject = require('../../helpers/mock-project'); - -describe('install:addon command', function() { - var command, options, tasks, npmInstance, generateBlueprintInstance; - - beforeEach(function() { - tasks = { - AddonInstall: AddonInstall, - NpmInstall: Task.extend({ - init: function() { - npmInstance = this; - } - }), - - GenerateFromBlueprint: Task.extend({ - init: function() { - generateBlueprintInstance = this; - } - }) - }; - - var project = new MockProject(); - - project.name = function() { return 'some-random-name'; }; - project.isEmberCLIProject = function() { return true; }; - project.initializeAddons = function() { }; - project.reloadAddons = function() { - this.addons = [{ - pkg: { - name: 'ember-cli-photoswipe', - 'ember-addon': { - defaultBlueprint: 'photoswipe' - } - } - }]; - }; - - options = commandOptions({ - settings: {}, - project: project, - tasks: tasks - }); - - stub(tasks.NpmInstall.prototype, 'run', Promise.resolve()); - stub(tasks.GenerateFromBlueprint.prototype, 'run', Promise.resolve()); - - command = new InstallAddonCommand(options); - - }); - - afterEach(function() { - tasks.NpmInstall.prototype.run.restore(); - tasks.GenerateFromBlueprint.prototype.run.restore(); - }); - - - it('will show a deprecation warning', function() { - return command.validateAndRun(['ember-cli-photoswipe']).then(function() { - var msg = 'This command has been deprecated. Please use `ember install '; - msg += '` instead.'; - - expect(command.ui.output).to.include(msg); - - expect(npmInstance.ui, 'ui was set'); - expect(npmInstance.project, 'project was set'); - expect(npmInstance.analytics, 'analytics was set'); - - expect(generateBlueprintInstance.ui, 'ui was set'); - expect(generateBlueprintInstance.project, 'project was set'); - expect(generateBlueprintInstance.analytics, 'analytics was set'); - - }); - }); -}); diff --git a/tests/unit/commands/install-bower-test.js b/tests/unit/commands/install-bower-test.js deleted file mode 100644 index 00ac275ffc..0000000000 --- a/tests/unit/commands/install-bower-test.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var InstallCommand = require('../../../lib/commands/install-bower'); -var MockProject = require('../../helpers/mock-project'); - -describe('install:bower command', function() { - var command, options, msg; - - beforeEach(function() { - var project = new MockProject(); - project.name = function() { - return 'some-random-name'; - }; - - project.isEmberCLIProject = function() { - return true; - }; - - options = commandOptions({ - settings: {}, - project: project - }); - - command = new InstallCommand(options); - msg = 'This command has been removed. Please use `bower install '; - msg += ' --save-dev --save-exact` instead.'; - }); - - describe('with args', function() { - it('it throws a friendly silent error', function() { - return command.validateAndRun(['moment', 'lodash']).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - expect(err.message).to.equal( - msg, 'expect error to have a helpful message' - ); - }); - }); - }); - - describe('without args', function() { - it('it throws a friendly slient error', function() { - return command.validateAndRun([]).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - expect(err.message).to.equal( - msg, 'expect error to have a helpful message' - ); - }); - }); - }); -}); diff --git a/tests/unit/commands/install-npm-test.js b/tests/unit/commands/install-npm-test.js deleted file mode 100644 index 90e31f4e5a..0000000000 --- a/tests/unit/commands/install-npm-test.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var InstallCommand = require('../../../lib/commands/install-npm'); -var MockProject = require('../../helpers/mock-project'); - -describe('install:npm command', function() { - var command, options, msg; - - beforeEach(function() { - var project = new MockProject(); - - project.name = function() { - return 'some-random-name'; - }; - - project.isEmberCLIProject =function() { - return true; - }; - - options = commandOptions({ - settings: {}, - project: project - }); - - command = new InstallCommand(options); - msg = 'This command has been removed. Please use `npm install '; - msg += ' --save-dev --save-exact` instead.'; - }); - - describe('with args', function() { - it('it throws a friendly silent error', function() { - return command.validateAndRun(['moment', 'lodash']).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - expect(err.message).to.equal( - msg, 'expect error to have a helpful message' - ); - }); - }); - }); - - describe('without args', function() { - it('it throws a friendly slient error', function() { - return command.validateAndRun([]).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - expect(err.message).to.equal( - msg, 'expect error to have a helpful message' - ); - }); - }); - }); -}); diff --git a/tests/unit/commands/install-test.js b/tests/unit/commands/install-test.js index 1d0d84e2e6..ca8dcb4caa 100644 --- a/tests/unit/commands/install-test.js +++ b/tests/unit/commands/install-test.js @@ -1,175 +1,302 @@ 'use strict'; -var expect = require('chai').expect; -var stub = require('../../helpers/stub').stub; -var commandOptions = require('../../factories/command-options'); -var InstallCommand = require('../../../lib/commands/install'); -var Task = require('../../../lib/models/task'); -var Promise = require('../../../lib/ext/promise'); -var AddonInstall = require('../../../lib/tasks/addon-install'); -var MockProject = require('../../helpers/mock-project'); - -describe('install command', function() { - var command, options, tasks, generateBlueprintInstance, npmInstance; - - beforeEach(function() { - var project = new MockProject(); - - project.name = function() { - return 'some-random-name'; - }; +const { expect } = require('chai'); +const MockProject = require('../../helpers/mock-project'); +const commandOptions = require('../../factories/command-options'); +const Task = require('../../../lib/models/task'); +const AddonInstall = require('../../../lib/tasks/addon-install'); +const InstallCommand = require('../../../lib/commands/install'); +const td = require('testdouble'); + +describe('install command', function () { + let generateBlueprintInstance, npmInstance; + let command, tasks; + + beforeEach(function () { + let project = new MockProject(); - project.isEmberCLIProject = function() { + project.isEmberCLIProject = function () { return true; }; - project.initializeAddons = function() { }; - project.reloadAddons = function() { + project.initializeAddons = function () {}; + project.reloadAddons = function () { this.addons = [ { pkg: { name: 'ember-data', - } + }, }, { pkg: { name: 'ember-cli-cordova', 'ember-addon': { - defaultBlueprint: 'cordova-starter-kit' - } - } + defaultBlueprint: 'cordova-starter-kit', + }, + }, }, { pkg: { - name: 'ember-cli-qunit' - } - } + name: 'ember-cli-qunit', + }, + }, + { + pkg: { + name: '@ember-cli/ember-cli-qunit', + }, + }, ]; }; tasks = { - AddonInstall: AddonInstall, - NpmInstall: Task.extend({ - project: project, - init: function() { + AddonInstall, + NpmInstall: class extends Task { + init() { + super.init(...arguments); + + this.project = project; + npmInstance = this; } - }), + }, + + GenerateFromBlueprint: class extends Task { + init() { + super.init(...arguments); + + this.project = project; - GenerateFromBlueprint: Task.extend({ - project: project, - init: function() { generateBlueprintInstance = this; } - }) + }, }; - options = commandOptions({ - settings: {}, - project: project, - tasks: tasks + let options = commandOptions({ + project, + tasks, }); - stub(tasks.NpmInstall.prototype, 'run', Promise.resolve()); - stub(tasks.GenerateFromBlueprint.prototype, 'run', Promise.resolve()); + td.replace(tasks.NpmInstall.prototype, 'run', td.function()); + td.when(tasks.NpmInstall.prototype.run(), { ignoreExtraArgs: true }).thenReturn(Promise.resolve()); + td.replace(tasks.GenerateFromBlueprint.prototype, 'run', td.function()); command = new InstallCommand(options); }); - afterEach(function() { - tasks.NpmInstall.prototype.run.restore(); - tasks.GenerateFromBlueprint.prototype.run.restore(); + afterEach(function () { + td.reset(); }); - it('initializes npm install and generate blueprint task with ui, project and analytics', function() { - return command.validateAndRun(['ember-data']).then(function() { - expect(npmInstance.ui, 'ui was set'); - expect(npmInstance.project, 'project was set'); - expect(npmInstance.analytics, 'analytics was set'); + it('initializes npm install and generate blueprint task with ui and project', function () { + return command.validateAndRun(['ember-data']).then(function () { + expect(npmInstance.ui, 'ui was set').to.be.ok; + expect(npmInstance.project, 'project was set').to.be.ok; - expect(generateBlueprintInstance.ui, 'ui was set'); - expect(generateBlueprintInstance.project, 'project was set'); - expect(generateBlueprintInstance.analytics, 'analytics was set'); + expect(generateBlueprintInstance.ui, 'ui was set').to.be.ok; + expect(generateBlueprintInstance.project, 'project was set').to.be.ok; }); }); - describe('with args', function() { - it('runs the npm install task with given name and save-dev true', function() { - return command.validateAndRun(['ember-data']).then(function() { - var npmRun = tasks.NpmInstall.prototype.run; - expect(npmRun.called).to.equal(1, 'expected npm install run was called once'); - - expect(npmRun.calledWith[0][0]).to.deep.equal({ - packages: ['ember-data'], - 'save-dev': true, - 'save-exact': true - }, 'expected npm install called with given name and save-dev true'); + describe('with args', function () { + it('runs the npm install task with given name and save-dev true', function () { + return command.validateAndRun(['ember-data']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['ember-data'], + save: false, + 'save-dev': true, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); + }); + }); + + it('runs the npm install task with given name and save-dev true in an addon', function () { + command.project.isEmberCLIProject = function () { + return false; + }; + command.project.isEmberCLIAddon = function () { + return true; + }; + return command.validateAndRun(['ember-data']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['ember-data'], + save: false, + 'save-dev': true, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); + }); + }); + + it('runs the npm install task with given name and save true with the --save option', function () { + return command.validateAndRun(['ember-data', '--save']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['ember-data'], + save: true, + 'save-dev': false, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); + }); + }); + + it('runs the npm install task with given name and save true in an addon with the --save option', function () { + command.project.isEmberCLIProject = function () { + return false; + }; + command.project.isEmberCLIAddon = function () { + return true; + }; + return command.validateAndRun(['ember-data', '--save']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['ember-data'], + save: true, + 'save-dev': false, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); }); }); - it('runs the package name blueprint task with given name and args', function() { - return command.validateAndRun(['ember-data']).then(function() { - var generateRun = tasks.GenerateFromBlueprint.prototype.run; - expect(generateRun.calledWith[0][0].ignoreMissingMain, true); - expect(generateRun.calledWith[0][0].args).to.deep.equal([ - 'ember-data' - ], 'expected generate blueprint called with correct args'); + it('runs the package name blueprint task with given name and args', function () { + return command.validateAndRun(['ember-data']).then(function () { + let generateRun = tasks.GenerateFromBlueprint.prototype.run; + let captor = td.matchers.captor(); + td.verify(generateRun(captor.capture())); + expect(captor.value.ignoreMissingMain).to.be.true; + expect(captor.value.args).to.deep.equal(['ember-data'], 'expected generate blueprint called with correct args'); }); }); - it('fails to install second argument for unknown addon', function() { - return command.validateAndRun(['ember-cli-cordova', 'com.ember.test']).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - var generateRun = tasks.GenerateFromBlueprint.prototype.run; - expect(generateRun.calledWith[0][0].ignoreMissingMain, true); - expect(generateRun.calledWith[0][0].args).to.deep.equal([ - 'cordova-starter-kit' - ], 'expected generate blueprint called with correct args'); - expect(err.message).to.equal( + it('fails to install second argument for unknown addon', function () { + return expect(command.validateAndRun(['ember-cli-cordova', 'com.ember.test'])).to.be.rejected.then((error) => { + let generateRun = tasks.GenerateFromBlueprint.prototype.run; + let captor = td.matchers.captor(); + td.verify(generateRun(captor.capture())); + expect(captor.value.ignoreMissingMain).to.be.true; + expect(captor.value.args).to.deep.equal( + ['cordova-starter-kit'], + 'expected generate blueprint called with correct args' + ); + expect(error.message).to.equal( 'Install failed. Could not find addon with name: com.ember.test', 'expected error to have helpful message' ); }); }); - it('runs npmInstall once and installs three addons', function() { - return command.validateAndRun([ - 'ember-data', 'ember-cli-cordova', 'ember-cli-qunit' - ]).then(function() { - var npmRun = tasks.NpmInstall.prototype.run; - - expect(npmRun.called).to.equal(1, 'expect npm install to be called once'); - - expect(npmRun.calledWith[0][0]).to.deep.equal({ - packages: ['ember-data', 'ember-cli-cordova', 'ember-cli-qunit'], - 'save-dev': true, - 'save-exact': true - }, 'expected npm install called with given name and save-dev true'); - - var generateRun = tasks.GenerateFromBlueprint.prototype.run; - expect(generateRun.called).to.equal(3, 'expect blueprint generator to run thrice.'); - expect(generateRun.calledWith[0][0].args[0]).to.equal('ember-data'); - expect(generateRun.calledWith[1][0].args[0]).to.equal('cordova-starter-kit'); - expect(generateRun.calledWith[2][0].args[0]).to.equal('ember-cli-qunit'); + it('runs npmInstall once and installs three addons', function () { + return command.validateAndRun(['ember-data', 'ember-cli-cordova', 'ember-cli-qunit']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['ember-data', 'ember-cli-cordova', 'ember-cli-qunit'], + save: false, + 'save-dev': true, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); + + let generateRun = tasks.GenerateFromBlueprint.prototype.run; + let generateRunArgs = td.explain(generateRun).calls.map(function (call) { + return call.args[0].args[0]; + }); + expect(generateRunArgs).to.deep.equal(['ember-data', 'cordova-starter-kit', 'ember-cli-qunit']); + }); + }); + + it('ember-cli/ember-cli-qunit: runs npmInstall but does not install the addon blueprint', function () { + return command.validateAndRun(['ember-cli/ember-cli-qunit']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['ember-cli/ember-cli-qunit'], + save: false, + 'save-dev': true, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); + + let generateRun = tasks.GenerateFromBlueprint.prototype.run; + td.verify(generateRun(), { ignoreExtraArgs: true, times: 0 }); + }); + }); + + it('ember-cli-qunit@1.2.0: runs npmInstall and installs the addon blueprint', function () { + return command.validateAndRun(['ember-cli-qunit@1.2.0']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['ember-cli-qunit@1.2.0'], + save: false, + 'save-dev': true, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); + + let generateRun = tasks.GenerateFromBlueprint.prototype.run; + let generateRunArgs = td.explain(generateRun).calls.map(function (call) { + return call.args[0].args[0]; + }); + expect(generateRunArgs).to.deep.equal(['ember-cli-qunit']); }); }); - it('runs the package name blueprint task when given github/name and args', function() { - return command.validateAndRun(['ember-cli/ember-cli-qunit']).then(function() { - var generateRun = tasks.GenerateFromBlueprint.prototype.run; - expect(generateRun.calledWith[0][0].ignoreMissingMain, true); - expect(generateRun.calledWith[0][0].args).to.deep.equal([ - 'ember-cli-qunit' - ], 'expected generate blueprint called with correct args'); + it('@ember-cli/ember-cli-qunit: runs npmInstall and installs the addon blueprint', function () { + return command.validateAndRun(['@ember-cli/ember-cli-qunit']).then(function () { + let npmRun = tasks.NpmInstall.prototype.run; + + td.verify( + npmRun({ + packages: ['@ember-cli/ember-cli-qunit'], + save: false, + 'save-dev': true, + 'save-exact': false, + packageManager: undefined, + }), + { times: 1 } + ); + + let generateRun = tasks.GenerateFromBlueprint.prototype.run; + let generateRunArgs = td.explain(generateRun).calls.map(function (call) { + return call.args[0].args[0]; + }); + expect(generateRunArgs).to.deep.equal(['@ember-cli/ember-cli-qunit']); }); }); - it('gives helpful message if it can\'t find the addon', function() { - return command.validateAndRun(['unknown-addon']).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - expect(err.message).to.equal( + it("gives helpful message if it can't find the addon", function () { + return expect(command.validateAndRun(['unknown-addon'])).to.be.rejected.then((error) => { + expect(error.message).to.equal( 'Install failed. Could not find addon with name: unknown-addon', 'expected error to have helpful message' ); @@ -177,16 +304,13 @@ describe('install command', function() { }); }); - describe('without args', function() { - it('gives a helpful message if no arguments are passed', function() { - return command.validateAndRun([]).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - var msg = 'The `install` command must take an argument with the name '; - msg += 'of an ember-cli addon. For installing all npm and bower '; - msg += 'dependencies you can run `npm install && bower install`.'; - expect(err.message).to.equal( - msg, 'expect error to have a helpful message' + describe('without args', function () { + it('gives a helpful message if no arguments are passed', function () { + return expect(command.validateAndRun([])).to.be.rejected.then((error) => { + expect(error.message).to.equal( + // TODO: Make this package-manager agnostic? + "An addon's name is required when running the `install` command. If you want to install all node modules, please run `pnpm install` instead.", + 'expect error to have a helpful message' ); }); }); diff --git a/tests/unit/commands/new-test.js b/tests/unit/commands/new-test.js index f67d42c4dc..dde2473fcf 100644 --- a/tests/unit/commands/new-test.js +++ b/tests/unit/commands/new-test.js @@ -1,96 +1,219 @@ 'use strict'; -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var NewCommand = require('../../../lib/commands/new'); +const { expect } = require('chai'); +const commandOptions = require('../../factories/command-options'); +const NewCommand = require('../../../lib/commands/new'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const Command = require('../../../lib/models/command'); +const Task = require('../../../lib/models/task'); +const InteractiveNewTask = require('../../../lib/tasks/interactive-new'); +const td = require('testdouble'); -describe('new command', function() { - var command, options; - - beforeEach(function() { - options = commandOptions({ - settings: {}, +describe('new command', function () { + let command; + beforeEach(function () { + let options = commandOptions({ project: { - isEmberCLIProject: function() { + isEmberCLIProject() { return false; - } - } + }, + blueprintLookupPaths() { + return []; + }, + }, }); command = new NewCommand(options); + + td.replace(Blueprint, 'lookup', td.function()); }); - it('doesn\'t allow to create an application named `test`', function() { - return command.validateAndRun(['test']).then(function() { - expect(false, 'should have rejected with an application name of test'); - }) - .catch(function(error) { - expect(error.message).to.equal('We currently do not support a name of `test`.'); - }); + afterEach(function () { + td.reset(); }); - it('doesn\'t allow to create an application named `ember`', function() { - return command.validateAndRun(['ember']).then(function() { - expect(false, 'should have rejected with an application name of ember'); - }) - .catch(function(error) { - expect(error.message).to.equal('We currently do not support a name of `ember`.'); - }); + it("doesn't allow to create an application named `test`", async function () { + let { message } = await expect(command.validateAndRun(['test'])).to.be.rejected; + expect(message).to.equal('We currently do not support a name of `test`.'); }); - it('doesn\'t allow to create an application named `Ember`', function() { - return command.validateAndRun(['Ember']).then(function() { - expect(false, 'should have rejected with an application name of Ember'); - }) - .catch(function(error) { - expect(error.message).to.equal('We currently do not support a name of `Ember`.'); - }); + it("doesn't allow to create an application named `ember`", async function () { + let { message } = await expect(command.validateAndRun(['ember'])).to.be.rejected; + expect(message).to.equal('We currently do not support a name of `ember`.'); }); - it('doesn\'t allow to create an application named `ember-cli`', function() { - return command.validateAndRun(['ember-cli']).then(function() { - expect(false, 'should have rejected with an application name of ember-cli'); - }) - .catch(function(error) { - expect(error.message).to.equal('We currently do not support a name of `ember-cli`.'); - }); + it("doesn't allow to create an application named `Ember`", async function () { + let { message } = await expect(command.validateAndRun(['Ember'])).to.be.rejected; + expect(message).to.equal('We currently do not support a name of `Ember`.'); }); - it('doesn\'t allow to create an application named `vendor`', function() { - return command.validateAndRun(['vendor']).then(function() { - expect(false, 'should have rejected with an application name of `vendor`'); - }) - .catch(function(error) { - expect(error.message).to.equal('We currently do not support a name of `vendor`.'); - }); + it("doesn't allow to create an application named `ember-cli`", async function () { + let { message } = await expect(command.validateAndRun(['ember-cli'])).to.be.rejected; + expect(message).to.equal('We currently do not support a name of `ember-cli`.'); }); - it('doesn\'t allow to create an application with a period in the name', function() { - return command.validateAndRun(['zomg.awesome']).then(function() { - expect(false, 'should have rejected with period in the application name'); - }) - .catch(function(error) { - expect(error.message).to.equal('We currently do not support a name of `zomg.awesome`.'); - }); + it("doesn't allow to create an application named `vendor`", async function () { + let { message } = await expect(command.validateAndRun(['vendor'])).to.be.rejected; + expect(message).to.equal('We currently do not support a name of `vendor`.'); + }); + + it("doesn't allow to create an application with a period in the name", async function () { + let { message } = await expect(command.validateAndRun(['zomg.awesome'])).to.be.rejected; + expect(message).to.equal('We currently do not support a name of `zomg.awesome`.'); + }); + + it("doesn't allow to create an application with a name beginning with a number", async function () { + let { message } = await expect(command.validateAndRun(['123-my-bagel'])).to.be.rejected; + expect(message).to.equal('We currently do not support a name of `123-my-bagel`.'); }); - it('doesn\'t allow to create an application with a name beginning with a number', function() { - return command.validateAndRun(['123-my-bagel']).then(function() { - expect(false, 'should have rejected with a name beginning with a number'); - }) - .catch(function(error) { - expect(error.message).to.equal('We currently do not support a name of `123-my-bagel`.'); + it('shows a suggestion messages when the application name is a period', async function () { + let { message } = await expect(command.validateAndRun(['.'])).to.be.rejected; + expect(message).to.equal( + `Trying to generate an application structure in this directory? Use \`ember init\` instead.` + ); + }); + + it('registers blueprint options in beforeRun', function () { + td.when(Blueprint.lookup('app'), { ignoreExtraArgs: true }).thenReturn({ + availableOptions: [{ name: 'custom-blueprint-option', type: String }], }); + + command.beforeRun(['app']); + expect(command.availableOptions.map(({ name }) => name)).to.contain('custom-blueprint-option'); }); - it('shows a suggestion messages when the application name is a period', function() { - return command.validateAndRun(['.']).then(function() { - expect(false, 'should have rejected with a name `.`'); - }) - .catch(function(error) { - expect(error.message).to.equal('Trying to generate an application structure in this directory? Use `ember init` instead.'); + it('passes command options through to init command', async function () { + command.tasks.CreateAndStepIntoDirectory = class extends Task { + run() { + return Promise.resolve(); + } + }; + + command.commands.Init = Command.extend({ + run(commandOptions) { + expect(commandOptions).to.contain.keys('customOption'); + expect(commandOptions.customOption).to.equal('customValue'); + return Promise.resolve('Called run'); + }, + }); + + td.when(Blueprint.lookup('app'), { ignoreExtraArgs: true }).thenReturn({ + availableOptions: [{ name: 'custom-blueprint-option', type: String }], }); + + let reason = await command.validateAndRun(['foo', '--custom-option=customValue']); + expect(reason).to.equal('Called run'); }); + describe('interactive', function () { + it('interactive new is entered when no app/addon name is provided', async function () { + class InteractiveNewTaskMock extends InteractiveNewTask { + run(newCommandOptions) { + return super.run(newCommandOptions, { + blueprint: 'addon', + name: 'foo', + langSelection: 'en-US', + packageManager: 'npm', + ciProvider: 'github', + }); + } + } + + class CreateAndStepIntoDirectoryTask extends Task { + run() {} + } + + class InitCommand extends Command { + run(commandOptions) { + expect(commandOptions).to.deep.include({ + blueprint: 'addon', + name: 'foo', + lang: 'en-US', + packageManager: 'npm', + ciProvider: 'github', + }); + } + } + + command.tasks.InteractiveNew = InteractiveNewTaskMock; + command.tasks.CreateAndStepIntoDirectory = CreateAndStepIntoDirectoryTask; + command.commands.Init = InitCommand; + + expect(command.validateAndRun([])).to.be.fulfilled; + }); + + it('interactive new is entered when the `--interactive` flag is provided', async function () { + class InteractiveNewTaskMock extends InteractiveNewTask { + run(newCommandOptions) { + return super.run(newCommandOptions, { + blueprint: 'app', + name: newCommandOptions.name, + langSelection: 'nl-BE', + packageManager: 'yarn', + ciProvider: 'github', + }); + } + } + + class CreateAndStepIntoDirectoryTask extends Task { + run() {} + } + + class InitCommand extends Command { + run(commandOptions) { + expect(commandOptions).to.deep.include({ + blueprint: 'app', + name: 'bar', + lang: 'nl-BE', + packageManager: 'yarn', + ciProvider: 'github', + }); + } + } + + command.tasks.InteractiveNew = InteractiveNewTaskMock; + command.tasks.CreateAndStepIntoDirectory = CreateAndStepIntoDirectoryTask; + command.commands.Init = InitCommand; + + expect(command.validateAndRun(['bar', '--interactive'])).to.be.fulfilled; + }); + + it('interactive new is entered when the `-i` flag is provided', async function () { + class InteractiveNewTaskMock extends InteractiveNewTask { + run(newCommandOptions) { + return super.run(newCommandOptions, { + blueprint: 'app', + name: newCommandOptions.name, + langSelection: 'fr-BE', + packageManager: null, + ciProvider: null, + }); + } + } + + class CreateAndStepIntoDirectoryTask extends Task { + run() {} + } + + class InitCommand extends Command { + run(commandOptions) { + expect(commandOptions).does.not.have.key('packageManager'); + expect(commandOptions).to.deep.include({ + blueprint: 'app', + name: 'baz', + lang: 'fr-BE', + ciProvider: 'github', + }); + } + } + + command.tasks.InteractiveNew = InteractiveNewTaskMock; + command.tasks.CreateAndStepIntoDirectory = CreateAndStepIntoDirectoryTask; + command.commands.Init = InitCommand; + + expect(command.validateAndRun(['baz', '-i'])).to.be.fulfilled; + }); + }); }); diff --git a/tests/unit/commands/serve-test.js b/tests/unit/commands/serve-test.js new file mode 100644 index 0000000000..2dffc72671 --- /dev/null +++ b/tests/unit/commands/serve-test.js @@ -0,0 +1,256 @@ +'use strict'; + +const { expect } = require('chai'); +const EOL = require('os').EOL; +const commandOptions = require('../../factories/command-options'); +const Task = require('../../../lib/models/task'); +const td = require('testdouble'); +const { getPortPromise } = require('portfinder'); + +const ServeCommand = require('../../../lib/commands/serve'); + +describe('serve command', function () { + let tasks, options, command; + + beforeEach(function () { + tasks = { + Serve: class extends Task {}, + }; + + options = commandOptions({ + tasks, + }); + + td.replace(tasks.Serve.prototype, 'run', td.function()); + + command = new ServeCommand(options); + }); + + afterEach(function () { + td.reset(); + }); + + it('has correct default options', function () { + return command.validateAndRun(['--port', '0']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.port).to.be.gte(4200, 'has correct port'); + expect(captor.value.liveReloadPort).to.be.equal(captor.value.port, 'has correct liveReload port'); + }); + }); + + it('setting --port without --live-reload-port', function () { + return getPortPromise().then(function (port) { + return command.validateAndRun(['--port', `${port}`]).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.port).to.equal(port, 'has correct port'); + expect(captor.value.liveReloadPort).to.be.equal(port, 'has correct liveReload port'); + }); + }); + }); + + it('setting both --port and --live-reload-port', function () { + return getPortPromise().then(function (port) { + return command.validateAndRun(['--port', `${port}`, '--live-reload-port', '8005']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.port).to.equal(port, 'has correct port'); + expect(captor.value.liveReloadPort).to.be.within(8005, 65535, 'has live reload port > port'); + }); + }); + }); + + // Allocate opts.port on opts.host if it is available (which we check through getPortPromise) + let testServer = function (opts, test) { + let server = require('http').createServer(function () {}); + return getPortPromise({ + ...opts, + stopPort: opts.port, + }) + .then(() => + new Promise(function (resolve) { + server.listen(opts.port, opts.host, () => resolve(test())); + }).finally(() => new Promise((resolve) => server.close(() => resolve()))) + ) + .catch(() => test()); + }; + + it('should throw error when -p PORT is taken', function () { + return testServer({ port: '32773' }, function () { + return expect(command.validateAndRun(['--port', '32773'])).to.be.rejected.then((err) => { + td.verify(tasks.Serve.prototype.run(), { ignoreExtraArgs: true, times: 0 }); + expect(err.message).to.contain('is already in use.'); + }); + }); + }); + + it('should throw descriptive error when -p PORT is taken and PORT < 1024', function () { + return testServer({ port: '1000' }, function () { + return expect(command.validateAndRun(['--port', '1000'])).to.be.rejected.then((err) => { + td.verify(tasks.Serve.prototype.run(), { ignoreExtraArgs: true, times: 0 }); + expect(err.message).to.contain('you do not have permission'); + }); + }); + }); + + it('allows OS to choose port by default', function () { + return testServer({ port: '4200' }, function () { + return command.validateAndRun().then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.port).to.be.within(4201, 65535, 'has correct port'); + expect(captor.value.liveReloadPort).to.be.equal(captor.value.port, 'has correct port'); + }); + }); + }); + + it('has correct liveReloadPort', function () { + return command.validateAndRun(['--port', '0', '--live-reload-port', '4001']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.liveReloadPort).to.be.gte(4001, 'has correct liveReload port'); + }); + }); + + it('has correct liveReloadHost', function () { + return command.validateAndRun(['--port', '0', '--live-reload-host', '127.0.0.1']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.liveReloadHost).to.equal('127.0.0.1', 'has correct liveReload host'); + }); + }); + + it('has correct liveReloadBaseUrl', function () { + return command + .validateAndRun(['--port', '0', '--live-reload-base-url', 'http://127.0.0.1:4200/']) + .then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.liveReloadBaseUrl).to.equal('http://127.0.0.1:4200/', 'has correct liveReload baseUrl'); + }); + }); + + it('has correct proxy', function () { + return command.validateAndRun(['--port', '0', '--proxy', 'http://localhost:3000/']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.proxy).to.equal('http://localhost:3000/', 'has correct port'); + }); + }); + + it('has correct secure proxy option', function () { + return command.validateAndRun(['--port', '0', '--secure-proxy', 'false']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.secureProxy).to.equal(false, 'has correct insecure proxy option'); + }); + }); + + it('has correct default value for secure proxy', function () { + return command.validateAndRun(['--port', '0']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.secureProxy).to.equal(true, 'has correct secure proxy option when not set'); + }); + }); + + it('requires proxy URL to include protocol', function () { + return expect(command.validateAndRun(['--port', '0', '--proxy', 'localhost:3000'])).to.be.rejected.then((error) => { + expect(error.message).to.equal( + `You need to include a protocol with the proxy URL.${EOL}Try --proxy http://localhost:3000` + ); + }); + }); + + it('has correct default value for transparent proxy', function () { + return command.validateAndRun(['--port', '0']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.transparentProxy).to.equal(true, 'has correct transparent proxy option when not set'); + }); + }); + + it('host alias does not conflict with help alias', function () { + return command.validateAndRun(['--port', '0', '-H', 'localhost']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.host).to.equal('localhost', 'has correct hostname'); + }); + }); + + it('has correct value for proxy-in-timeout', function () { + return command.validateAndRun(['--port', '0', '--proxy-in-timeout', '300000']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.proxyInTimeout).to.equal(300000, 'has correct incoming proxy timeout option'); + }); + }); + + it('has correct default value for proxy-in-timeout', function () { + return command.validateAndRun(['--port', '0']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.proxyInTimeout).to.equal(120000, 'has correct incoming proxy timeout option when not set'); + }); + }); + + it('has correct value for proxy-out-timeout', function () { + return command.validateAndRun(['--port', '0', '--proxy-out-timeout', '30000']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.proxyOutTimeout).to.equal(30000, 'has correct outgoing proxy timeout option'); + }); + }); + + it('has correct default value for proxy-out-timeout', function () { + return command.validateAndRun(['--port', '0']).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.Serve.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.proxyOutTimeout).to.equal(0, 'has correct outgoing proxy timeout option when not set'); + }); + }); + + it('throws a meaningful error when run outside the project', function () { + command.project.hasDependencies = function () { + return false; + }; + + command.project.isEmberCLIProject = function () { + return false; + }; + + command.isWithinProject = false; + + return expect(command.validateAndRun(['--port', '0'])).to.be.rejected.then((error) => { + expect(error.message).to.match(/You have to be inside an ember-cli project/); + }); + }); + + it('throws when run in a Vite-based project', async function () { + command.isViteProject = true; + + await expect(command.validateAndRun([])).to.be.rejectedWith( + 'The `serve` command is not supported in Vite-based projects. Please use the `start` script from package.json.' + ); + }); + + it('waits on the serve tasks promise', async function () { + let serveTaskResolved = false; + + tasks.Serve = class extends Task { + run() { + return new Promise((resolve) => { + setTimeout(() => { + serveTaskResolved = true; + resolve(); + }, 10); + }); + } + }; + + await command.validateAndRun(['--port', '0']); + + expect(serveTaskResolved).to.be.ok; + }); +}); diff --git a/tests/unit/commands/server-test.js b/tests/unit/commands/server-test.js deleted file mode 100644 index d01f9873cb..0000000000 --- a/tests/unit/commands/server-test.js +++ /dev/null @@ -1,165 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var stub = require('../../helpers/stub').stub; -var commandOptions = require('../../factories/command-options'); -var Task = require('../../../lib/models/task'); -var Promise = require('../../../lib/ext/promise'); - -describe('server command', function() { - var ServeCommand; - var tasks; - var options; - - before(function() { - ServeCommand = require('../../../lib/commands/serve'); - }); - - beforeEach(function() { - tasks = { - Serve: Task.extend() - }; - - options = commandOptions({ - tasks: tasks, - settings: {} - }); - - stub(tasks.Serve.prototype, 'run'); - stub(ServeCommand.prototype, '_getPort', new Promise(function(resolve) { - resolve(49152); - })); - }); - - after(function() { - ServeCommand = null; - }); - - afterEach(function() { - tasks.Serve.prototype.run.restore(); - ServeCommand.prototype._getPort.restore(); - }); - - it('has correct options', function() { - return new ServeCommand(options).validateAndRun([ - '--port', '4000' - ]).then(function() { - var serveRun = tasks.Serve.prototype.run; - var runOps = serveRun.calledWith[0][0]; - var getPortOps = ServeCommand.prototype._getPort.calledWith[0][0]; - - expect(getPortOps.host).to.equal('0.0.0.0', 'a livereload port is found using the default host'); - - expect(serveRun.called).to.equal(1, 'expected run to be called once'); - - expect(runOps.port).to.equal(4000, 'has correct port'); - expect(runOps.liveReloadPort).to.be.within(49152, 65535, 'has correct liveReload port'); - expect(runOps.liveReloadHost).to.equal('0.0.0.0', 'has correct liveReload host'); - }); - }); - - it('has correct liveLoadPort', function() { - return new ServeCommand(options).validateAndRun([ - '--live-reload-port', '4001' - ]).then(function() { - var serveRun = tasks.Serve.prototype.run; - var ops = serveRun.calledWith[0][0]; - - expect(serveRun.called).to.equal(1, 'expected run to be called once'); - - expect(ops.liveReloadPort).to.equal(4001, 'has correct liveReload port'); - }); - }); - - it('has correct liveLoadHost', function() { - return new ServeCommand(options).validateAndRun([ - '--live-reload-host', '127.0.0.1' - ]).then(function() { - var serveRun = tasks.Serve.prototype.run; - var runOps = serveRun.calledWith[0][0]; - var getPortOpts = ServeCommand.prototype._getPort.calledWith[0][0]; - - expect(serveRun.called).to.equal(1, 'expected run to be called once'); - - expect(getPortOpts.host).to.equal('127.0.0.1', 'gets a port based on the liveReload host'); - expect(runOps.liveReloadHost).to.equal('127.0.0.1', 'has correct liveReload host'); - }); - }); - - it('has correct proxy', function() { - return new ServeCommand(options).validateAndRun([ - '--proxy', 'http://localhost:3000/' - ]).then(function() { - var serveRun = tasks.Serve.prototype.run; - var ops = serveRun.calledWith[0][0]; - - expect(serveRun.called).to.equal(1, 'expected run to be called once'); - - expect(ops.proxy).to.equal('http://localhost:3000/', 'has correct port'); - }); - }); - - it('has correct insecure proxy option', function() { - return new ServeCommand(options).validateAndRun([ - '--insecure-proxy' - ]).then(function() { - var serveRun = tasks.Serve.prototype.run; - var ops = serveRun.calledWith[0][0]; - - expect(serveRun.called).to.equal(1, 'expected run to be called once'); - - expect(ops.insecureProxy).to.equal(true, 'has correct insecure proxy option'); - }); - }); - - it('has correct default value for insecure proxy', function() { - return new ServeCommand(options).validateAndRun().then(function() { - var serveRun = tasks.Serve.prototype.run; - var ops = serveRun.calledWith[0][0]; - - expect(serveRun.called).to.equal(1, 'expected run to be called once'); - - expect(ops.insecureProxy).to.equal(false, 'has correct insecure proxy option when not set'); - }); - }); - - it('requires proxy URL to include protocol', function() { - return new ServeCommand(options).validateAndRun([ - '--proxy', 'localhost:3000' - ]).then(function() { - expect(false, 'it rejects when proxy URL doesn\'t include protocol'); - }) - .catch(function(error) { - expect(error.message).to.equal( - 'You need to include a protocol with the proxy URL.\nTry --proxy http://localhost:3000' - ); - }); - }); - - it('uses baseURL of correct environment', function() { - options.project.config = function(env) { - return { baseURL: env }; - }; - - return new ServeCommand(options).validateAndRun([ - '--environment', 'test' - ]).then(function() { - var serveRun = tasks.Serve.prototype.run; - var ops = serveRun.calledWith[0][0]; - - expect(ops.baseURL).to.equal('test', 'Uses the correct environment.'); - }); - }); - - it('host alias does not conflict with help alias', function() { - return new ServeCommand(options).validateAndRun([ - '-H', 'hostname' - ]).then(function() { - var serveRun = tasks.Serve.prototype.run; - var ops = serveRun.calledWith[0][0]; - - expect(serveRun.called).to.equal(1, 'expected run to be called once'); - expect(ops.host).to.equal('hostname', 'has correct hostname'); - }); - }); -}); diff --git a/tests/unit/commands/show-asset-sizes-test.js b/tests/unit/commands/show-asset-sizes-test.js new file mode 100644 index 0000000000..bdb0d047d3 --- /dev/null +++ b/tests/unit/commands/show-asset-sizes-test.js @@ -0,0 +1,54 @@ +'use strict'; + +const { expect } = require('chai'); +const commandOptions = require('../../factories/command-options'); +const Task = require('../../../lib/models/task'); +const path = require('path'); +const td = require('testdouble'); + +describe('asset-sizes command', function () { + let ShowCommand; + let tasks; + let options; + + before(function () { + ShowCommand = require('../../../lib/commands/asset-sizes'); + }); + + beforeEach(function () { + tasks = { + ShowAssetSizes: class extends Task {}, + }; + + options = commandOptions({ + tasks, + settings: {}, + }); + + td.replace(tasks.ShowAssetSizes.prototype, 'run', td.function()); + }); + + after(function () { + ShowCommand = null; + }); + + afterEach(function () { + td.reset(); + }); + + it('has correct default value for output path', function () { + return new ShowCommand(options).validateAndRun().then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.ShowAssetSizes.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.outputPath).to.equal('dist/', 'has correct output path option when not set'); + }); + }); + + it('has correct options', function () { + return new ShowCommand(options).validateAndRun(['--output-path', path.join('some', 'path')]).then(function () { + let captor = td.matchers.captor(); + td.verify(tasks.ShowAssetSizes.prototype.run(captor.capture()), { times: 1 }); + expect(captor.value.outputPath).to.equal(path.join(process.cwd(), 'some', 'path'), 'has correct asset path'); + }); + }); +}); diff --git a/tests/unit/commands/test-test.js b/tests/unit/commands/test-test.js index ddc2c3ba3f..d9cff50d65 100644 --- a/tests/unit/commands/test-test.js +++ b/tests/unit/commands/test-test.js @@ -1,307 +1,458 @@ 'use strict'; -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var stub = require('../../helpers/stub').stub; -var existsSync = require('exists-sync'); -var Promise = require('../../../lib/ext/promise'); -var Task = require('../../../lib/models/task'); -var CoreObject = require('core-object'); -var path = require('path'); -var fs = require('fs'); -var TestCommand = require('../../../lib/commands/test'); -var MockProject = require('../../helpers/mock-project'); - -describe('test command', function() { - var tasks; - var options; - var buildRun; - var testRun; - var testServerRun; - - beforeEach(function(){ +const path = require('path'); +const CoreObject = require('core-object'); +const { expect } = require('chai'); +const MockProject = require('../../helpers/mock-project'); +const commandOptions = require('../../factories/command-options'); +const Task = require('../../../lib/models/task'); +const TestCommand = require('../../../lib/commands/test'); +const td = require('testdouble'); +const http = require('http'); + +describe('test command', function () { + this.timeout(30000); + + let tasks, options, command; + + beforeEach(function () { tasks = { - Build: Task.extend(), - Test: Task.extend(), - TestServer: Task.extend() + Build: class extends Task {}, + Test: class extends Task {}, + TestServer: class extends Task {}, + }; + + let project = new MockProject(); + + project.isEmberCLIProject = function () { + return true; + }; + + project.isViteProject = function () { + return true; }; - var project = new MockProject(); - project.isEmberCLIProject = function() { return true; }; options = commandOptions({ - tasks: tasks, + tasks, testing: true, - settings: {}, - project: project + project, }); - stub(tasks.Test.prototype, 'run', Promise.resolve()); - stub(tasks.Build.prototype, 'run', Promise.resolve()); - - buildRun = tasks.Build.prototype.run; - testRun = tasks.Test.prototype.run; - - stub(tasks.TestServer.prototype, 'run', Promise.resolve()); - testServerRun = tasks.TestServer.prototype.run; + td.replace(tasks.Test.prototype, 'run', td.function()); + td.replace(tasks.Build.prototype, 'run', td.function()); + td.replace(tasks.TestServer.prototype, 'run', td.function()); + td.when(tasks.Test.prototype.run(), { ignoreExtraArgs: true, times: 1 }).thenReturn(Promise.resolve()); + td.when(tasks.Build.prototype.run(), { ignoreExtraArgs: true, times: 1 }).thenReturn(Promise.resolve()); + td.when(tasks.TestServer.prototype.run(), { ignoreExtraArgs: true }).thenReturn(Promise.resolve()); }); - it('builds and runs test', function() { - return new TestCommand(options).validateAndRun([]).then(function() { - expect(buildRun.called).to.equal(1, 'expected build task to be called once'); - expect(testRun.called).to.equal(1, 'expected test task to be called once'); - }); + afterEach(function () { + td.reset(); }); - it('has the correct options', function() { - return new TestCommand(options).validateAndRun([]).then(function() { - var buildOptions = buildRun.calledWith[0][0]; - var testOptions = testRun.calledWith[0][0]; + function buildCommand() { + command = new TestCommand(options); + } - expect(buildOptions.environment).to.equal('test', 'has correct env'); - expect(buildOptions.outputPath, 'has outputPath'); - expect(testOptions.configFile).to.equal('./testem.json', 'has config file'); - expect(testOptions.port).to.equal(7357, 'has config file'); + describe('default', function () { + beforeEach(function () { + buildCommand(); }); - }); - - it('passes through custom configFile option', function() { - return new TestCommand(options).validateAndRun(['--config-file=some-random/path.json']).then(function() { - var testOptions = testRun.calledWith[0][0]; - expect(testOptions.configFile).to.equal('some-random/path.json'); + it('builds and runs test', function () { + return command.validateAndRun([]); }); - }); - it('does not pass any port options', function() { - return new TestCommand(options).validateAndRun([]).then(function() { - var testOptions = testRun.calledWith[0][0]; + it('has the correct options', function () { + return command.validateAndRun([]).then(function () { + let captor = td.matchers.captor(); + + td.verify(tasks.Build.prototype.run(captor.capture())); + expect(captor.value.environment).to.equal('test', 'has correct env'); + expect(captor.value.outputPath, 'has outputPath').to.be.ok; - expect(testOptions.port).to.equal(7357); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.configFile).to.equal(undefined, 'does not supply config file when not specified'); + expect(captor.value.port).to.equal(7357, 'has config file'); + }); }); - }); - it('passes through a custom test port option', function() { - return new TestCommand(options).validateAndRun(['--test-port=5679']).then(function() { - var testOptions = testRun.calledWith[0][0]; + it('passes through custom configFile option', function () { + return command.validateAndRun(['--config-file=some-random/path.json']).then(function () { + let captor = td.matchers.captor(); - expect(testOptions.port).to.equal(5679); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.configFile).to.equal('some-random/path.json'); + }); }); - }); - it('only passes through the port option', function() { - return new TestCommand(options).validateAndRun(['--port=5678']).then(function() { - var testOptions = testRun.calledWith[0][0]; + it('does not pass any port options', function () { + return command.validateAndRun([]).then(function () { + let captor = td.matchers.captor(); - expect(testOptions.port).to.equal(5679); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.port).to.equal(7357); + }); }); - }); - it('passes both the port and the test port options', function() { - return new TestCommand(options).validateAndRun(['--port=5678', '--test-port=5900']).then(function() { - var testOptions = testRun.calledWith[0][0]; + it('default port in use', function () { + let server = http.createServer(); + server.listen(7357); + return command.validateAndRun([]).then(function () { + let captor = td.matchers.captor(); - expect(testOptions.port).to.equal(5900); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.port).to.not.equal(7357); + server.close(); + }); }); - }); - it('passes through custom host option', function() { - return new TestCommand(options).validateAndRun(['--host=greatwebsite.com']).then(function() { - var testOptions = testRun.calledWith[0][0]; + it('passes through a custom test port option', function () { + return command.validateAndRun(['--test-port=5679']).then(function () { + let captor = td.matchers.captor(); - expect(testOptions.host).to.equal('greatwebsite.com'); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.port).to.equal(5679); + }); }); - }); - it('passes through custom reporter option', function() { - return new TestCommand(options).validateAndRun(['--reporter=xunit']).then(function() { - var testOptions = testRun.calledWith[0][0]; + it('passes through a custom test port option of 0 to allow OS to choose open system port', function () { + return command.validateAndRun(['--test-port=0']).then(function () { + let captor = td.matchers.captor(); - expect(testOptions.reporter).to.equal('xunit'); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.port).to.equal(0); + }); }); - }); - describe('--server option', function() { - beforeEach(function() { - options.Builder = CoreObject.extend(); - options.Watcher = CoreObject.extend(); + it('only passes through the port option', function () { + return command.validateAndRun(['--port=5678']).then(function () { + let captor = td.matchers.captor(); + + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.port).to.equal(5679); + }); }); - it('builds a watcher with verbose set to false', function() { - return new TestCommand(options).validateAndRun(['--server']).then(function() { - var testOptions = testServerRun.calledWith[0][0]; + it('passes both the port and the test port options', function () { + return command.validateAndRun(['--port=5678', '--test-port=5900']).then(function () { + let captor = td.matchers.captor(); - expect(testOptions.watcher.verbose, false); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.port).to.equal(5900); }); }); - it('builds a watcher with options.watcher set to value provided', function() { - return new TestCommand(options).validateAndRun(['--server', '--watcher=polling']).then(function() { - var testOptions = testServerRun.calledWith[0][0]; + it('passes through custom host option', function () { + return command.validateAndRun(['--host=greatwebsite.com']).then(function () { + let captor = td.matchers.captor(); - expect(testOptions.watcher.options.watcher).to.equal('polling'); + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.host).to.equal('greatwebsite.com'); }); }); - }); - describe('_generateCustomConfigFile', function() { - var command; - var runOptions; - var fixturePath; - - beforeEach(function() { - fixturePath = path.join(__dirname, '..', '..', 'fixtures', 'tasks', 'testem-config'); - command = new TestCommand(options); - runOptions = { - configFile: path.join(fixturePath, 'testem.json') - }; + it('old option `--output-path` option throws', async function () { + await expect(command.validateAndRun(['--output-path=some/path'])).to.be.rejectedWith( + 'The `--output-path` option to `ember test` is not supported in Vite-based projects.' + ); }); - afterEach(function() { - command.rmTmp(); + it('passes through custom reporter option', function () { + return command.validateAndRun(['--reporter=xunit']).then(function () { + let captor = td.matchers.captor(); + + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.reporter).to.equal('xunit'); + }); }); - it('should return a valid path', function() { - var newPath = command._generateCustomConfigFile(runOptions); + it('has the correct options when called with a build path and does not run a build task', function () { + return command.validateAndRun(['--path=tests']).then(function () { + let captor = td.matchers.captor(); + + td.verify(tasks.Build.prototype.run(td.matchers.anything()), { times: 0 }); + td.verify(tasks.Test.prototype.run(captor.capture())); - expect(existsSync(newPath)); + expect(captor.value.outputPath).to.equal(path.resolve('tests'), 'has outputPath'); + expect(captor.value.configFile).to.equal( + undefined, + 'does not include configFile when not specified in options' + ); + expect(captor.value.port).to.equal(7357, 'has port'); + }); }); - it('should return the original path if options are not present', function() { - var originalPath = runOptions.configFile; - var newPath = command._generateCustomConfigFile(runOptions); + it('throws an error if the build path does not exist', function () { + return expect(command.validateAndRun(['--path=bad/path/to/build'])).to.be.rejected.then((error) => { + let expectedPath = path.resolve('bad/path/to/build'); + expect(error.message).to.equal( + `The path ${expectedPath} does not exist. Please specify a valid build directory to test.` + ); + }); + }); - expect(newPath).to.equal(originalPath); + it('--server option throws', async function () { + await expect(command.validateAndRun(['--server'])).to.be.rejectedWith( + 'The `--server` option to `ember test` is not supported in Vite-based projects. Please use the `start` script from package.json and visit `/tests` in the browser.' + ); }); + }); - it('when options are present the new file path returned exists', function() { - var originalPath = runOptions.configFile; - runOptions.module = 'fooModule'; - runOptions.filter = 'bar'; - runOptions.launch = 'fooLauncher'; - runOptions['test-page'] = 'foo/test.html?foo'; - var newPath = command._generateCustomConfigFile(runOptions); + describe('_generateCustomConfigs', function () { + let runOptions; - expect(newPath).to.not.equal(originalPath); - expect(existsSync(newPath), 'file should exist'); + beforeEach(function () { + buildCommand(); + runOptions = {}; }); - it('when filter option is present the new file path returned exists', function() { - var originalPath = runOptions.configFile; - runOptions.filter = 'foo'; - var newPath = command._generateCustomConfigFile(runOptions); + it('should return an object even if passed param is empty object', function () { + let result = command._generateCustomConfigs(runOptions); + expect(result).to.be.an('object'); + }); + + it('when launch option is present, should be reflected in returned config', function () { + runOptions.launch = 'fooLauncher'; + let result = command._generateCustomConfigs(runOptions); - expect(newPath).to.not.equal(originalPath); - expect(existsSync(newPath), 'file should exist'); + expect(result.launch).to.equal('fooLauncher'); }); - it('when module option is present the new file path returned exists', function() { - var originalPath = runOptions.configFile; - runOptions.module = 'fooModule'; - var newPath = command._generateCustomConfigFile(runOptions); + it('when query option is present, should be reflected in returned config', function () { + runOptions.query = 'someQuery=test'; + let result = command._generateCustomConfigs(runOptions); - expect(newPath).to.not.equal(originalPath); - expect(existsSync(newPath), 'file should exist'); + expect(result.queryString).to.equal(runOptions.query); }); - it('when launch option is present the new file path returned exists', function() { - var originalPath = runOptions.configFile; - runOptions.launch = 'fooLauncher'; - var newPath = command._generateCustomConfigFile(runOptions); + it('when provided test-page the new file returned contains the value in test_page', function () { + runOptions['test-page'] = 'foo/test.html?foo'; + let result = command._generateCustomConfigs(runOptions); - expect(newPath).to.not.equal(originalPath); - expect(existsSync(newPath), 'file should exist'); + expect(result.testPage).to.be.equal('foo/test.html?foo&'); }); - it('when test-page option is present the new file path returned exists', function() { - var originalPath = runOptions.configFile; + it('when provided test-page with filter, module, and query the new file returned contains those values in test_page', function () { + runOptions.module = 'fooModule'; + runOptions.filter = 'bar'; + runOptions.query = 'someQuery=test'; runOptions['test-page'] = 'foo/test.html?foo'; - var newPath = command._generateCustomConfigFile(runOptions); + let contents = command._generateCustomConfigs(runOptions); - expect(newPath).to.not.equal(originalPath); - expect(existsSync(newPath), 'file should exist'); + expect(contents.testPage).to.be.equal('foo/test.html?foo&module=fooModule&filter=bar&someQuery=test'); }); - it('when provided filter and module the new file returned contains the both option values in test_page', function() { + it('when provided test-page with filter and module the new file returned contains both option values in test_page', function () { runOptions.module = 'fooModule'; runOptions.filter = 'bar'; - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + runOptions['test-page'] = 'foo/test.html?foo'; + let contents = command._generateCustomConfigs(runOptions); - expect(contents['test_page']).to.be.equal('tests/index.html?module=fooModule&filter=bar'); + expect(contents.testPage).to.be.equal('foo/test.html?foo&module=fooModule&filter=bar'); }); - it('when provided test-page the new file returned contains the value in test_page', function() { + it('when provided test-page with filter and query the new file returned contains both option values in test_page', function () { + runOptions.query = 'someQuery=test'; + runOptions.filter = 'bar'; runOptions['test-page'] = 'foo/test.html?foo'; - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + let contents = command._generateCustomConfigs(runOptions); - expect(contents['test_page']).to.be.equal('foo/test.html?foo&'); + expect(contents.testPage).to.be.equal('foo/test.html?foo&filter=bar&someQuery=test'); }); - it('when provided test-page with filter and module the new file returned contains those values in test_page', function() { + it('when provided test-page with module and query the new file returned contains both option values in test_page', function () { runOptions.module = 'fooModule'; - runOptions.filter = 'bar'; + runOptions.query = 'someQuery=test'; runOptions['test-page'] = 'foo/test.html?foo'; - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + let contents = command._generateCustomConfigs(runOptions); - expect(contents['test_page']).to.be.equal('foo/test.html?foo&module=fooModule&filter=bar'); + expect(contents.testPage).to.be.equal('foo/test.html?foo&module=fooModule&someQuery=test'); }); - it('when provided launch the new file returned contains the value in launch', function() { + it('when provided launch the new file returned contains the value in launch', function () { runOptions.launch = 'fooLauncher'; - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + let contents = command._generateCustomConfigs(runOptions); expect(contents['launch']).to.be.equal('fooLauncher'); }); - it('when provided filter is all lowercase to match the test name', function() { + it('when provided filter is all lowercase to match the test name', function () { + runOptions['test-page'] = 'tests/index.html'; runOptions.filter = 'BAR'; - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + let contents = command._generateCustomConfigs(runOptions); - expect(contents['test_page']).to.be.equal('tests/index.html?filter=bar'); + expect(contents.testPage).to.be.equal('tests/index.html?filter=bar'); }); - it('when module and filter option is present uses buildTestPageQueryString for test_page queryString', function() { + it('when module and filter option is present uses buildTestPageQueryString for test_page queryString', function () { runOptions.filter = 'bar'; - command.buildTestPageQueryString = function(options) { + runOptions['test-page'] = 'tests/index.html'; + command.buildTestPageQueryString = function (options) { expect(options).to.deep.equal(runOptions); return 'blah=zorz'; }; - var newPath = command._generateCustomConfigFile(runOptions); + let contents = command._generateCustomConfigs(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); - - expect(contents['test_page']).to.be.equal('tests/index.html?blah=zorz'); + expect(contents.testPage).to.be.equal('tests/index.html?blah=zorz'); }); - it('new file returned contains the filter option value in test_page', function() { + it('new file returned contains the filter option value in test_page', function () { runOptions.filter = 'foo'; - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + runOptions['test-page'] = 'tests/index.html'; + let contents = command._generateCustomConfigs(runOptions); - expect(contents['test_page']).to.be.equal('tests/index.html?filter=foo'); + expect(contents.testPage).to.be.equal('tests/index.html?filter=foo'); }); - it('adds with a `&` if query string contains `?` already', function() { + it('adds with a `&` if query string contains `?` already', function () { runOptions.filter = 'foo'; - runOptions.configFile = path.join(fixturePath, 'testem-with-query-string.json'); - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + runOptions['test-page'] = 'tests/index.html?hidepassed'; + let contents = command._generateCustomConfigs(runOptions); - expect(contents['test_page']).to.be.equal('tests/index.html?hidepassed&filter=foo'); + expect(contents.testPage).to.be.equal('tests/index.html?hidepassed&filter=foo'); }); - it('new file returned contains the module option value in test_page', function() { + it('new file returned contains the module option value in test_page', function () { runOptions.module = 'fooModule'; - var newPath = command._generateCustomConfigFile(runOptions); - var contents = JSON.parse(fs.readFileSync(newPath, { encoding: 'utf8' })); + runOptions['test-page'] = 'tests/index.html'; + let contents = command._generateCustomConfigs(runOptions); + + expect(contents.testPage).to.be.equal('tests/index.html?module=fooModule'); + }); + + it('new file returned contains the query option value in test_page', function () { + runOptions.query = 'someQuery=test'; + runOptions['test-page'] = 'tests/index.html'; + let contents = command._generateCustomConfigs(runOptions); + + expect(contents.testPage).to.be.equal('tests/index.html?someQuery=test'); + }); + + it('new file returned contains the query option value with multiple queries in test_page', function () { + runOptions.query = 'someQuery=test&something&else=false'; + runOptions['test-page'] = 'tests/index.html'; + let contents = command._generateCustomConfigs(runOptions); + + expect(contents.testPage).to.be.equal('tests/index.html?someQuery=test&something&else=false'); + }); + }); +}); + +describe('test command in classic', function () { + this.timeout(30000); + + let tasks, options, command; + + beforeEach(function () { + tasks = { + Build: class extends Task {}, + Test: class extends Task {}, + TestServer: class extends Task {}, + }; + + let project = new MockProject(); + + project.isEmberCLIProject = function () { + return true; + }; + + project.isViteProject = function () { + return false; + }; + + options = commandOptions({ + tasks, + testing: true, + project, + }); + + td.replace(tasks.Test.prototype, 'run', td.function()); + td.replace(tasks.Build.prototype, 'run', td.function()); + td.replace(tasks.TestServer.prototype, 'run', td.function()); + td.when(tasks.Test.prototype.run(), { ignoreExtraArgs: true, times: 1 }).thenReturn(Promise.resolve()); + td.when(tasks.Build.prototype.run(), { ignoreExtraArgs: true, times: 1 }).thenReturn(Promise.resolve()); + td.when(tasks.TestServer.prototype.run(), { ignoreExtraArgs: true }).thenReturn(Promise.resolve()); + }); + + afterEach(function () { + td.reset(); + }); + + function buildCommand() { + command = new TestCommand(options); + } + + describe('default', function () { + beforeEach(function () { + buildCommand(); + }); + + it('builds and runs test', function () { + return command.validateAndRun([]); + }); + + it('passes through output path option', function () { + return command.validateAndRun(['--output-path=some/path']).then(function () { + let captor = td.matchers.captor(); + + td.verify(tasks.Test.prototype.run(captor.capture())); + expect(captor.value.outputPath).to.equal(path.resolve('some/path')); + }); + }); + }); + + describe('--server option', function () { + let buildCleanupWasCalled; + beforeEach(function () { + buildCleanupWasCalled = false; + options.Builder = class Builder extends CoreObject { + cleanup() { + buildCleanupWasCalled = true; + } + }; + options.Watcher = class Watcher extends CoreObject { + static build() { + return { watcher: new this(...arguments) }; + } + }; + + buildCommand(); + }); + + it('builds a watcher with verbose set to false', function () { + return command + .validateAndRun(['--server']) + .then(function () { + let captor = td.matchers.captor(); + + td.verify(tasks.TestServer.prototype.run(captor.capture())); + expect(captor.value.watcher.verbose).to.be.false; + }) + .finally(function () { + expect(buildCleanupWasCalled).to.be.true; + }); + }); + + it('builds a watcher with options.watcher set to value provided', function () { + return command + .validateAndRun(['--server', '--watcher=polling']) + .then(function () { + let captor = td.matchers.captor(); + + td.verify(tasks.TestServer.prototype.run(captor.capture())); + expect(captor.value.watcher.options.watcher).to.equal('polling'); + }) + .finally(function () { + expect(buildCleanupWasCalled).to.be.true; + }); + }); - expect(contents['test_page']).to.be.equal('tests/index.html?module=fooModule'); + it('DOES NOT throw an error if using a build path', async function () { + // if this throws then the test will fail + await command.validateAndRun(['--server', '--path=tests']); }); }); }); diff --git a/tests/unit/commands/uninstall-npm-test.js b/tests/unit/commands/uninstall-npm-test.js deleted file mode 100644 index aed2bd242f..0000000000 --- a/tests/unit/commands/uninstall-npm-test.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var UninstallCommand = require('../../../lib/commands/uninstall-npm'); -var MockProject = require('../../helpers/mock-project'); - -describe('uninstall:npm command', function() { - var command, options, msg; - - beforeEach(function() { - var project = new MockProject(); - project.name = function() { return 'some-random-name'; }; - project.isEmberCLIProject = function() { return true; }; - - options = commandOptions({ - settings: {}, - project: project - }); - - command = new UninstallCommand(options); - msg = 'This command has been removed Please use `npm uninstall '; - msg += ' --save-dev` instead.'; - }); - - describe('with no args', function() { - it('throws a friendly silent error', function() { - return command.validateAndRun([]).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - expect(err.message).to.equal( - msg, 'expect error to have a helpful message' - ); - }); - }); - }); - - describe('with args', function() { - it('throws a friendly silent error', function() { - return command.validateAndRun(['moment', 'lodash']).then(function() { - expect(false, 'should reject with error'); - }).catch(function(err) { - expect(err.message).to.equal( - msg, 'expect error to have a helpful message' - ); - }); - }); - }); -}); diff --git a/tests/unit/commands/version-test.js b/tests/unit/commands/version-test.js index c8024b2fef..36afbf40af 100644 --- a/tests/unit/commands/version-test.js +++ b/tests/unit/commands/version-test.js @@ -1,54 +1,46 @@ 'use strict'; -var expect = require('chai').expect; -var commandOptions = require('../../factories/command-options'); -var VersionCommand = require('../../../lib/commands/version'); -var MockUI = require('../../helpers/mock-ui'); -var EOL = require('os').EOL; +const { expect } = require('chai'); +const EOL = require('os').EOL; +const commandOptions = require('../../factories/command-options'); +const VersionCommand = require('../../../lib/commands/version'); -describe('version command', function() { - var ui, command, options; +describe('version command', function () { + let options, command; - beforeEach(function() { - ui = new MockUI(); + beforeEach(function () { options = commandOptions({ - settings: {}, - - ui: ui, - project: { - isEmberCLIProject: function() { + isEmberCLIProject() { return false; - } - } + }, + }, }); command = new VersionCommand(options); }); - it('reports node, npm, and os versions', function() { - return command.validateAndRun().then(function() { - var lines = ui.output.split(EOL); - expect(someLineStartsWith(lines, 'node:'), 'contains the version of node'); - expect(someLineStartsWith(lines, 'npm:'), 'contains the version of npm'); - expect(someLineStartsWith(lines, 'os:'), 'contains the version of os'); + it('reports node, npm, and os versions', function () { + return command.validateAndRun().then(function () { + let lines = options.ui.output.split(EOL); + expect(someLineStartsWith(lines, 'ember-cli:'), 'contains the version of ember-cli').to.be.ok; + expect(someLineStartsWith(lines, 'node:'), 'contains the version of node').to.be.ok; + expect(someLineStartsWith(lines, 'os:'), 'contains the version of os').to.be.ok; }); }); - it('supports a --verbose flag', function() { - return command.validateAndRun(['--verbose']).then(function() { - var lines = ui.output.split(EOL); - expect(someLineStartsWith(lines, 'node:'), 'contains the version of node'); - expect(someLineStartsWith(lines, 'npm:'), 'contains the version of npm'); - expect(someLineStartsWith(lines, 'os:'), 'contains the version of os'); - expect(someLineStartsWith(lines, 'v8:'), 'contains the version of v8'); + it('supports a --verbose flag', function () { + return command.validateAndRun(['--verbose']).then(function () { + let lines = options.ui.output.split(EOL); + expect(someLineStartsWith(lines, 'node:'), 'contains the version of node').to.be.ok; + expect(someLineStartsWith(lines, 'os:'), 'contains the version of os').to.be.ok; + expect(someLineStartsWith(lines, 'v8:'), 'contains the version of v8').to.be.ok; }); }); - }); function someLineStartsWith(lines, search) { - return lines.some(function(line) { + return lines.some(function (line) { return line.indexOf(search) === 0; }); } diff --git a/tests/unit/debug/assert-test.js b/tests/unit/debug/assert-test.js new file mode 100644 index 0000000000..a9680722dd --- /dev/null +++ b/tests/unit/debug/assert-test.js @@ -0,0 +1,44 @@ +'use strict'; + +const { expect } = require('chai'); +const { assert } = require('../../../lib/debug'); + +describe('assert', function () { + it('it throws when the description argument is missing', function () { + expect(() => { + assert(); + }).to.throw('When calling `assert`, you must provide a description as the first argument.'); + + expect(() => { + assert(''); + }).to.throw('When calling `assert`, you must provide a description as the first argument.'); + }); + + it('it does nothing when the condition argument is truthy', function () { + expect(() => { + assert('description', 1); + }).to.not.throw(); + + expect(() => { + assert('description', {}); + }).to.not.throw(); + + expect(() => { + assert('description', true); + }).to.not.throw(); + }); + + it('it throws when the condition argument is falsy', function () { + expect(() => { + assert('description'); + }).to.throw('ASSERTION FAILED: description'); + + expect(() => { + assert('description', null); + }).to.throw('ASSERTION FAILED: description'); + + expect(() => { + assert('description', false); + }).to.throw('ASSERTION FAILED: description'); + }); +}); diff --git a/tests/unit/debug/deprecate-test.js b/tests/unit/debug/deprecate-test.js new file mode 100644 index 0000000000..ef3ab9da79 --- /dev/null +++ b/tests/unit/debug/deprecate-test.js @@ -0,0 +1,259 @@ +'use strict'; + +const { expect } = require('chai'); +const { default: stripAnsi } = require('strip-ansi'); +const { deprecate } = require('../../../lib/debug'); +const { makeDeprecate } = require('semver-deprecate'); + +describe('deprecate', function () { + let consoleWarn; + let deprecations; + + beforeEach(function () { + consoleWarn = console.warn; + deprecations = []; + + // TODO: This should be updated once we can register deprecation handlers: + console.warn = (deprecation) => { + // Remove the stack trace: + deprecations.push(stripAnsi(deprecation).split('at getStackTrace')[0].trimEnd()); + }; + }); + + afterEach(function () { + console.warn = consoleWarn; + }); + + it('it throws when the description argument is missing', function () { + expect(() => { + deprecate(); + }).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a description as the first argument.'); + + expect(() => { + deprecate(''); + }).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a description as the first argument.'); + }); + + it('it throws when the condition argument is missing', function () { + expect(() => { + deprecate('description'); + }).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a condition as the second argument.'); + }); + + it('it throws when the options argument is missing', function () { + expect(() => { + deprecate('description', true); + }).to.throw( + 'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).' + ); + + expect(() => { + deprecate('description', undefined); + }).to.throw( + 'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).' + ); + + expect(() => { + deprecate('description', false, null); + }).to.throw( + 'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).' + ); + }); + + it('it throws when the `for` option is missing', function () { + expect(() => { + deprecate('description', true, {}); + }).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide the `for` option.'); + }); + + it('it throws when the `id` option is missing', function () { + expect(() => { + deprecate('description', true, { + for: 'foo', + }); + }).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide the `id` option.'); + }); + + it('it throws when the `since` option is missing', function () { + expect(() => { + deprecate('description', true, { + for: 'foo', + id: 'foo', + }); + }).to.throw( + 'ASSERTION FAILED: When calling `deprecate`, you must provide the `since` option. `since` must include the `available` and/or the `enabled` option.' + ); + }); + + it('it throws when both the `since.available` and `since.enabled` options are missing', function () { + expect(() => { + deprecate('description', true, { + for: 'foo', + id: 'foo', + since: {}, + }); + }).to.throw( + 'ASSERTION FAILED: When calling `deprecate`, you must provide the `since.available` and/or the `since.enabled` option.' + ); + }); + + it('it throws when the `since.available` option is not a valid SemVer version', function () { + expect(() => { + deprecate('description', true, { + for: 'foo', + id: 'foo', + since: { + available: 'foo', + }, + }); + }).to.throw('ASSERTION FAILED: `since.available` must be a valid SemVer version.'); + }); + + it('it throws when the `since.enabled` option is not a valid SemVer version', function () { + expect(() => { + deprecate('description', true, { + for: 'foo', + id: 'foo', + since: { + enabled: 'foo', + }, + }); + }).to.throw('ASSERTION FAILED: `since.enabled` must be a valid SemVer version.'); + }); + + it('it throws when the `until` option is not a valid SemVer version', function () { + expect(() => { + deprecate('description', true, { + for: 'foo', + id: 'foo', + since: { + available: '4.0.0', + enabled: '4.0.0', + }, + }); + }).to.throw( + 'ASSERTION FAILED: When calling `deprecate`, you must provide a valid SemVer version for the `until` option.' + ); + + expect(() => { + deprecate('description', true, { + for: 'foo', + id: 'foo', + since: { + available: '4.0.0', + enabled: '4.0.0', + }, + until: 'foo', + }); + }).to.throw( + 'ASSERTION FAILED: When calling `deprecate`, you must provide a valid SemVer version for the `until` option.' + ); + }); + + it('it does nothing when the condition argument is truthy', function () { + deprecate('description', true, { + for: 'foo', + id: 'foo', + since: { + available: '4.0.0', + enabled: '4.0.0', + }, + until: '5.0.0', + }); + + expect(deprecations).to.deep.equal([]); + }); + + it('it displays a deprecation message when the condition argument is falsy', function () { + deprecate('description', false, { + for: 'foo', + id: 'foo', + since: { + available: '4.0.0', + enabled: '4.0.0', + }, + until: '5.0.0', + }); + + expect(deprecations).to.deep.equal([ + ` DEPRECATION \n +description + +ID foo +UNTIL 5.0.0`, + ]); + }); + + it('it includes the `url` option in the deprecation message when provided', function () { + deprecate('description', false, { + for: 'foo', + id: 'foo', + since: { + available: '4.0.0', + enabled: '4.0.0', + }, + until: '5.0.0', + url: 'https://example.com', + }); + + expect(deprecations).to.deep.equal([ + ` DEPRECATION \n +description + +ID foo +UNTIL 5.0.0 +URL https://example.com`, + ]); + }); + + it('throws an deprecation if the current ember-cli version is greater than the until version of deprecation', function () { + expect(() => { + deprecate('The `foo` method is deprecated', false, { + for: 'ember-cli', + id: 'ember-cli.foo-method', + since: { + available: '4.1.0', + enabled: '4.2.0', + }, + until: '3.0.0', // This should be less than the current emberCLIVersion to trigger the error + url: 'https://example.com', + }); + }).to.throw( + 'The API deprecated by ember-cli.foo-method was removed in ember-cli@3.0.0. The message was: The `foo` method is deprecated. Please see https://example.com for more details.' + ); + }); + + it('throws an deprecation if the pre-release ember-cli version is greater than the until version of deprecation', function () { + // this shadows the imported deprecate function on purpose because we need to be able to override the version + let deprecate = makeDeprecate('ember-cli', '9.0.0-beta.1'); + expect(() => { + deprecate('The `bar` method is deprecated', false, { + for: 'ember-cli', + id: 'ember-cli.bar-method', + since: { + available: '4.1.0', + enabled: '4.2.0', + }, + until: '9.0.0', // This should be less than the current emberCLIVersion to trigger the error + url: 'https://example.com', + }); + }).to.throw( + 'The API deprecated by ember-cli.bar-method was removed in ember-cli@9.0.0. The message was: The `bar` method is deprecated. Please see https://example.com for more details.' + ); + }); + + it('does not throw a deprecation if "for" is not "ember-cli"', function () { + expect(() => { + deprecate('The `foo` method is deprecated.', false, { + for: 'ember-source', + id: 'not-ember-cli.foo-method', + since: { + available: '4.1.0', + enabled: '4.2.0', + }, + until: '3.0.0', + url: 'https://example.com', + }); + }).to.not.throw(); + }); +}); diff --git a/tests/unit/dev/update-blueprint-dependencies-test.js b/tests/unit/dev/update-blueprint-dependencies-test.js new file mode 100644 index 0000000000..4974befcf9 --- /dev/null +++ b/tests/unit/dev/update-blueprint-dependencies-test.js @@ -0,0 +1,48 @@ +'use strict'; + +const { expect } = require('chai'); +const { readFileSync } = require('node:fs'); +const { join } = require('node:path'); +const { PACKAGE_FILES, updateDependencies } = require('../../../dev/update-blueprint-dependencies'); + +describe('updateDependencies', function () { + it('it works', async function () { + let dependenciesBeforeUpdate = { + // Template expression in key: + '<% if (foo) { %>@glimmer/component': '^1.1.0', + + // Template expression in value: + '@glimmer/component': '^1.1.0<% if (foo) { %>', + 'ember-cli': '~<%= emberCLIVersion %>', + + // Template expression in key and value: + '<% } %>@glimmer/component': '^1.1.0<% if (foo) { %>', + + // Without template expressions: + '@glimmer/tracking': '^1.1.0', + }; + + let dependenciesAfterUpdate = { ...dependenciesBeforeUpdate }; + + await updateDependencies(dependenciesAfterUpdate); + + expect(Object.keys(dependenciesAfterUpdate)).to.deep.equal(Object.keys(dependenciesBeforeUpdate)); + + for (let dependency in dependenciesAfterUpdate) { + if (dependency === 'ember-cli') { + expect(dependenciesAfterUpdate[dependency]).to.equal(dependenciesBeforeUpdate[dependency]); + } else { + expect(dependenciesAfterUpdate[dependency]).to.not.equal(dependenciesBeforeUpdate[dependency]); + } + } + }); + + it('all package files are parseable', function () { + for (let pkgFile of PACKAGE_FILES) { + let pkgPath = join(__dirname, '..', '..', pkgFile); + let pkg = JSON.parse(readFileSync(pkgPath, { encoding: 'utf8' })); + + expect(pkg).to.be.ok; + } + }); +}); diff --git a/tests/unit/docs-lint-test.js b/tests/unit/docs-lint-test.js new file mode 100644 index 0000000000..899251234e --- /dev/null +++ b/tests/unit/docs-lint-test.js @@ -0,0 +1,62 @@ +'use strict'; + +/* + * Turns out, YUIDoc attached a process.on('unhandledRejection' handler which + * incorrectly reports any unhandledRejection as a YUIDoc error. This makes + * debugging of unhandledRejections in the ember-cli test suite quite painful + * + * To address YUIDocs behavior, we run YUIDoc isolated in it's own process; + */ + +if (!process.env['IS_CHILD']) { + const { execa } = require('execa'); + + describe('YUIDoc', function () { + it('parses without warnings', async function () { + await execa('node', [`--unhandled-rejections=strict`, __filename], { + env: { + IS_CHILD: true, + }, + }); + }); + }); + return; +} + +const Y = require('yuidocjs'); +const EOL = require('os').EOL; + +let options = Y.Project.init({ + quiet: true, +}); +let yuiDoc = new Y.YUIDoc(options); + +let json = yuiDoc.run(); + +let warnings = {}; +json.warnings.forEach(function (warning) { + let tmp = warning.line.split(':'); + let file = tmp[0].trim(); + let line = tmp[1]; + + if (!warnings[file]) { + warnings[file] = []; + } + + warnings[file].push({ + line, + message: warning.message, + }); +}); + +let message = ''; +Object.keys(warnings).forEach(function (file) { + message += `\t${file} – YUIDoc issues found:${EOL}${EOL}`; + warnings[file].forEach(function (warning) { + message += `\t\tline ${warning.line}: ${warning.message}${EOL}`; + }); +}); + +if (message.length) { + throw new Error(message); +} diff --git a/tests/unit/errors/silent-test.js b/tests/unit/errors/silent-test.js deleted file mode 100644 index 534ea582da..0000000000 --- a/tests/unit/errors/silent-test.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -var SilentError = require('silent-error'); -var SilentErrorLib = require('../../../lib/errors/silent'); -var expect = require('chai').expect; - -describe('SilentError', function() { - it('return silent-error and print a deprecation', function() { - expect(SilentErrorLib, 'returns silent-error').to.equal(SilentError); - }); -}); \ No newline at end of file diff --git a/tests/unit/experiments-test.js b/tests/unit/experiments-test.js new file mode 100644 index 0000000000..866a6dfc8c --- /dev/null +++ b/tests/unit/experiments-test.js @@ -0,0 +1,72 @@ +'use strict'; + +const { expect } = require('chai'); +const { + isExperimentEnabled, + _deprecatedExperimentsDeprecationsIssued, +} = require('@ember-tooling/blueprint-model/utilities/experiments'); + +function resetProcessEnv(originalProcessEnv) { + for (let key in process.env) { + if (key in originalProcessEnv) { + process.env[key] = originalProcessEnv[key]; + } else { + delete process.env[key]; + } + } + + for (let key in originalProcessEnv) { + if (!(key in process.env)) { + process.env[key] = originalProcessEnv[key]; + } + } +} + +const ORIGINAL_CONSOLE = Object.assign({}, console); + +describe('experiments', function () { + describe('isExperimentEnabled', function () { + let originalProcessEnv, warnings; + + beforeEach(function () { + originalProcessEnv = Object.assign({}, process.env); + + // reset all experiment flags for these tests, they will be restored in + // afterEach + delete process.env.EMBER_CLI_ENABLE_ALL_EXPERIMENTS; + delete process.env.EMBER_CLI_EMBROIDER; + delete process.env.EMBER_CLI_CLASSIC; + + warnings = []; + console.warn = (warning) => warnings.push(warning); + }); + + afterEach(function () { + resetProcessEnv(originalProcessEnv); + Object.assign(console, ORIGINAL_CONSOLE); + _deprecatedExperimentsDeprecationsIssued.length = 0; + }); + + it('should return true for all experiments when `EMBER_CLI_ENABLE_ALL_EXPERIMENTS` is set', function () { + process.env.EMBER_CLI_ENABLE_ALL_EXPERIMENTS = true; + + expect(isExperimentEnabled('EMBROIDER')).to.be.true; + + expect(warnings).to.deep.equal([]); + }); + + it('should return true when an experiment is enabled via environment variable', function () { + process.env.EMBER_CLI_EMBROIDER = 'true'; + process.env.EMBER_CLI_CLASSIC = 'true'; + + // classic experiment will disable embroider + expect(isExperimentEnabled('EMBROIDER')).to.be.false; + + delete process.env.EMBER_CLI_CLASSIC; + + expect(isExperimentEnabled('EMBROIDER')).to.be.true; + + expect(warnings).to.deep.equal([]); + }); + }); +}); diff --git a/tests/unit/jshint-test.js b/tests/unit/jshint-test.js deleted file mode 100644 index ae81ecc5e1..0000000000 --- a/tests/unit/jshint-test.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -// configuration is based on settings found in .jshintrc and .jshintignore -require('mocha-jshint')(); diff --git a/tests/unit/models/addon-discovery-test.js b/tests/unit/models/addon-discovery-test.js deleted file mode 100644 index ac3d59fd48..0000000000 --- a/tests/unit/models/addon-discovery-test.js +++ /dev/null @@ -1,462 +0,0 @@ -'use strict'; - -var path = require('path'); -var expect = require('chai').expect; -var assign = require('lodash/object/assign'); -var Project = require('../../../lib/models/project'); -var AddonDiscovery = require('../../../lib/models/addon-discovery'); -var fixturePath = path.resolve(__dirname, '../../fixtures/addon'); -var MockUI = require('../../helpers/mock-ui'); -var chalk = require('chalk'); - -describe('models/addon-discovery.js', function() { - var project, projectPath, ui; - this.timeout(40000); - - beforeEach(function() { - ui = new MockUI(); - projectPath = path.resolve(fixturePath, 'simple'); - var packageContents = require(path.join(projectPath, 'package.json')); - - project = new Project(projectPath, packageContents, ui); - }); - - describe('dependencies', function() { - var mockPkg, deps, devDeps; - - beforeEach(function() { - deps = { - 'foo-bar': 'latest', - 'blah-blah': '1.0.0' - }; - - devDeps = { - 'dev-foo-bar': 'latest' - }; - - mockPkg = { - dependencies: deps, - devDependencies: devDeps - }; - }); - - it('returns an object containing depenencies from the provided package.json', function() { - var expected = assign({}, deps, devDeps); - var discovery = new AddonDiscovery(ui); - - expect(discovery.dependencies(mockPkg)).to.be.eql(expected); - }); - - it('excludes development dependencies if instructed', function() { - var expected = assign({}, deps); - var discovery = new AddonDiscovery(ui); - - expect(discovery.dependencies(mockPkg, true)).to.be.eql(expected); - }); - }); - - describe('discoverFromInternalProjectAddons', function() { - it('calls discoverAtPath for each path in project.supportedInternalAddonPaths', function() { - var actualPaths = []; - var project = { - supportedInternalAddonPaths: function() { - return [ 'lib/foo/', 'baz/qux/' ]; - } - }; - - var discovery = new AddonDiscovery(ui); - - discovery.discoverAtPath = function(path) { - actualPaths.push(path); - }; - - discovery.discoverFromInternalProjectAddons(project); - - expect(actualPaths).to.be.eql(project.supportedInternalAddonPaths()); - }); - }); - - describe('discoverInRepoAddons', function() { - describe('returns empty array when ember-addon.paths is empty', function() { - var discovery, pkg; - - beforeEach(function() { - discovery = new AddonDiscovery(ui); - }); - - it('returns empty array if `ember-addon` is not present in provided package', function() { - pkg = { }; - - var actual = discovery.discoverInRepoAddons(fixturePath, pkg); - expect(actual).to.be.eql([]); - }); - - it('returns empty array if `ember-addon.paths` is missing in provided package', function() { - pkg = { - 'ember-addon': { } - }; - - var actual = discovery.discoverInRepoAddons(fixturePath, pkg); - expect(actual).to.be.eql([]); - }); - - it('returns empty array if `ember-addon.paths` is empty in provided package', function() { - pkg = { - 'ember-addon': { - paths: [] - } - }; - - var actual = discovery.discoverInRepoAddons(fixturePath, pkg); - expect(actual).to.be.eql([]); - }); - }); - - it('calls discoverAtPath for each path in ember-addon.paths', function() { - var actualPaths = []; - var pkg = { - 'ember-addon': { - paths: [ 'lib/foo', 'baz/qux' ] - } - }; - var discovery = new AddonDiscovery(ui); - - discovery.discoverAtPath = function(providedPath) { - actualPaths.push(providedPath); - - return providedPath; - }; - - discovery.discoverInRepoAddons(fixturePath, pkg); - - var expected = [ - path.join(fixturePath, 'lib', 'foo'), - path.join(fixturePath, 'baz', 'qux') - ]; - - expect(actualPaths).to.be.eql(expected); - }); - - it('falsey results from discoverAtPath are filtered out', function() { - var actualPaths = []; - var pkg = { - 'ember-addon': { - paths: [ 'lib/foo', 'baz/qux' ] - } - }; - var discovery = new AddonDiscovery(ui); - - discovery.discoverAtPath = function(providedPath) { - actualPaths.push(providedPath); - - return null; - }; - - var result = discovery.discoverInRepoAddons(fixturePath, pkg); - - var expectedPaths = [ - path.join(fixturePath, 'lib', 'foo'), - path.join(fixturePath, 'baz', 'qux') - ]; - - expect(actualPaths).to.be.eql(expectedPaths); - expect(result).to.be.eql([]); - }); - }); - - describe('discoverFromDependencies', function() { - var mockPkg, deps, devDeps; - - beforeEach(function() { - deps = { - 'foo-bar': 'latest', - 'blah-blah': '1.0.0' - }; - - devDeps = { - 'dev-foo-bar': 'latest' - }; - - mockPkg = { - dependencies: deps, - devDependencies: devDeps - }; - }); - - it('can find a package without a main entry point [DEPRECATED]', function() { - var root = path.join(fixturePath, 'shared-package', 'base'); - var addonNodeModulesPath = path.join(root, 'node_modules'); - var actualPaths = []; - var discovery = new AddonDiscovery(ui); - - deps['invalid-package'] = 'latest'; - discovery.discoverAtPath = function(providedPath) { - actualPaths.push(providedPath); - - return providedPath; - }; - - discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg, true); - - var expectedPaths = [ - path.join(root, 'node_modules', 'foo-bar'), - path.join(root, 'node_modules', 'blah-blah'), - path.join(root, 'node_modules', 'invalid-package') - ]; - - expect(actualPaths).to.be.eql(expectedPaths); - - var output = ui.output.trim(); - var expectedWarning = chalk.yellow('The package `invalid-package` is not a properly formatted package, we have used a fallback lookup to resolve it at `' + path.join(root, 'node_modules', 'invalid-package') + '`. This is generally caused by an addon not having a `main` entry point (or `index.js`).'); - expect(output).to.equal(expectedWarning); - }); - - it('does not error when dependencies are not found', function() { - var root = path.join(fixturePath, 'shared-package', 'base'); - var addonNodeModulesPath = path.join(root, 'node_modules'); - var actualPaths = []; - var discovery = new AddonDiscovery(ui); - - deps['blah-zorz'] = 'latest'; - discovery.discoverAtPath = function(providedPath) { - actualPaths.push(providedPath); - - return providedPath; - }; - - discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg, true); - - var expectedPaths = [ - path.join(root, 'node_modules', 'foo-bar'), - path.join(root, 'node_modules', 'blah-blah'), - path.join(root, 'node_modules', 'blah-zorz') - ]; - - expect(actualPaths).to.be.eql(expectedPaths); - }); - - it('calls discoverAtPath for each entry in dependencies', function() { - var root = path.join(fixturePath, 'shared-package', 'base'); - var addonNodeModulesPath = path.join(root, 'node_modules'); - var actualPaths = []; - var discovery = new AddonDiscovery(ui); - - discovery.discoverAtPath = function(providedPath) { - actualPaths.push(providedPath); - - return providedPath; - }; - - discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg); - - var expectedPaths = [ - path.join(root, '..', 'node_modules', 'dev-foo-bar'), - path.join(root, 'node_modules', 'foo-bar'), - path.join(root, 'node_modules', 'blah-blah') - ]; - - expect(actualPaths).to.be.eql(expectedPaths); - }); - - it('excludes devDeps if `excludeDevDeps` is true', function() { - var root = path.join(fixturePath, 'shared-package', 'base'); - var addonNodeModulesPath = path.join(root, 'node_modules'); - var actualPaths = []; - var discovery = new AddonDiscovery(ui); - - discovery.discoverAtPath = function(providedPath) { - actualPaths.push(providedPath); - - return providedPath; - }; - - discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg, true); - - var expectedPaths = [ - path.join(root, 'node_modules', 'foo-bar'), - path.join(root, 'node_modules', 'blah-blah') - ]; - - expect(actualPaths).to.be.eql(expectedPaths); - }); - }); - - describe('discoverFromProjectItself', function() { - it('adds the project.root if it is an addon', function() { - var project = { - isEmberCLIAddon: function() { - return false; - } - }; - - var discovery = new AddonDiscovery(ui); - var actual = discovery.discoverFromProjectItself(project); - - expect(actual).to.be.eql([]); - }); - - it('returns the root path if the project is an addon', function() { - var actualPaths = []; - var project = { - root: 'foo/bar/baz', - isEmberCLIAddon: function() { - return true; - } - }; - - var discovery = new AddonDiscovery(ui); - - discovery.discoverAtPath = function(providedPath) { - actualPaths.push(providedPath); - - return providedPath; - }; - - var actual = discovery.discoverFromProjectItself(project); - var expectedPaths = [ 'foo/bar/baz' ]; - - expect(actualPaths).to.be.eql(expectedPaths); - expect(actual).to.be.eql(expectedPaths); - }); - }); - - describe('discoverChildAddons', function() { - var addon, discovery, discoverFromDependenciesCalled, discoverInRepoAddonsCalled; - - beforeEach(function() { - addon = { - name: 'awesome-sauce', - root: fixturePath, - pkg: { - dependencies: { - 'foo-bar': 'latest' - }, - devDependencies: { - 'dev-dep-bar': 'latest' - } - } - }; - - discovery = new AddonDiscovery(ui); - - discovery.discoverFromDependencies = function() { - discoverFromDependenciesCalled = true; - - return []; - }; - - discovery.discoverInRepoAddons = function() { - discoverInRepoAddonsCalled = true; - - return []; - }; - }); - - it('delegates to discoverInRepoAddons and discoverFromDependencies', function() { - discovery.discoverChildAddons(addon); - - expect(discoverInRepoAddonsCalled).to.equal(true); - expect(discoverFromDependenciesCalled).to.equal(true); - }); - - it('concats discoverInRepoAddons and discoverFromDependencies results', function() { - discovery.discoverFromDependencies = function() { - return [ 'discoverFromDependencies' ]; - }; - - discovery.discoverInRepoAddons = function() { - return [ 'discoverInRepoAddons' ]; - }; - - var result = discovery.discoverChildAddons(addon); - - expect(result).to.be.eql([ 'discoverFromDependencies', 'discoverInRepoAddons' ]); - }); - }); - - describe('discoverProjectAddons', function() { - var addon, discovery, discoverFromProjectItselfCalled, discoverFromInternalProjectAddonsCalled, discoverFromDependenciesCalled, discoverInRepoAddonsCalled; - - beforeEach(function() { - addon = { - name: 'awesome-sauce', - root: fixturePath, - pkg: { - dependencies: { - 'foo-bar': 'latest' - }, - devDependencies: { - 'dev-dep-bar': 'latest' - } - }, - hasDependencies: function() { - return true; - } - }; - - discovery = new AddonDiscovery(ui); - - discovery.discoverFromProjectItself = function() { - discoverFromProjectItselfCalled = true; - - return [ 'discoverFromProjectItself' ]; - }; - - discovery.discoverFromInternalProjectAddons = function() { - discoverFromInternalProjectAddonsCalled = true; - - return [ 'discoverFromInternalProjectAddons' ]; - }; - - discovery.discoverFromDependencies = function() { - discoverFromDependenciesCalled = true; - - return [ 'discoverFromDependencies' ]; - }; - - discovery.discoverInRepoAddons = function() { - discoverInRepoAddonsCalled = true; - - return [ 'discoverInRepoAddons' ]; - }; - }); - - it('delegates to internal methods', function() { - discovery.discoverProjectAddons(addon); - - expect(discoverFromProjectItselfCalled).to.equal(true); - expect(discoverFromInternalProjectAddonsCalled).to.equal(true); - expect(discoverInRepoAddonsCalled).to.equal(true); - expect(discoverFromDependenciesCalled).to.equal(true); - }); - - it('concats discoverInRepoAddons and discoverFromDependencies results', function() { - var result = discovery.discoverProjectAddons(addon); - - expect(result).to.be.eql([ 'discoverFromProjectItself', 'discoverFromInternalProjectAddons', 'discoverFromDependencies', 'discoverInRepoAddons' ]); - }); - }); - - describe('discoverAtPath', function() { - it('returns an info object when addon is found', function() { - var addonPath = path.join(fixturePath, 'simple/node_modules/ember-random-addon'); - var addonPkg = require(path.join(addonPath, 'package.json')); - var discovery = new AddonDiscovery(ui); - - var result = discovery.discoverAtPath(addonPath); - - expect(result.name).to.be.equal('ember-random-addon'); - expect(result.path).to.be.equal(addonPath); - expect(result.pkg).to.be.eql(addonPkg); - }); - - it('returns `null` if path is not for an addon', function() { - var addonPath = path.join(fixturePath, 'simple'); - var discovery = new AddonDiscovery(ui); - - var result = discovery.discoverAtPath(addonPath); - - expect(result).to.be.equal(null); - }); - }); -}); diff --git a/tests/unit/models/addon-test.js b/tests/unit/models/addon-test.js index 6c575e7aa3..e4ee3eccfe 100644 --- a/tests/unit/models/addon-test.js +++ b/tests/unit/models/addon-test.js @@ -1,399 +1,341 @@ 'use strict'; -var fs = require('fs-extra'); -var path = require('path'); -var Project = require('../../../lib/models/project'); -var Addon = require('../../../lib/models/addon'); -var Promise = require('../../../lib/ext/promise'); -var expect = require('chai').expect; -var remove = Promise.denodeify(fs.remove); -var tmp = require('tmp-sync'); -var path = require('path'); -var findWhere = require('lodash/collection/find'); -var MockUI = require('../../helpers/mock-ui'); - -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); - -var fixturePath = path.resolve(__dirname, '../../fixtures/addon'); - -describe('models/addon.js', function() { - var addon, project, projectPath; - - describe('root property', function() { - it('is required', function() { - expect(function() { - var TheAddon = Addon.extend({root:undefined}); +const fs = require('fs-extra'); +const path = require('path'); +const Project = require('../../../lib/models/project'); +const Addon = require('../../../lib/models/addon'); +const { expect } = require('chai'); +const MockUI = require('console-ui/mock'); +const MockCLI = require('../../helpers/mock-cli'); + +const broccoli = require('broccoli'); +const walkSync = require('walk-sync'); +const td = require('testdouble'); +const tmp = require('tmp-promise'); + +let fixturePath = path.resolve(__dirname, '../../fixtures/addon'); + +describe('models/addon.js', function () { + let addon, project, projectPath; + + describe('root property', function () { + it('is required', function () { + expect(() => { + let TheAddon = Addon.extend({ root: undefined }); new TheAddon(); }).to.throw(/root/); }); }); - describe('treePaths and treeForMethods', function() { - var FirstAddon, SecondAddon; + describe('old core object compat', function () { + it('treeGenerator throws when `.project` is missing', function () { + let TheAddon = Addon.extend({ + name: 'such name', + root: path.resolve(fixturePath, 'simple'), + packageRoot: path.resolve(fixturePath, 'simple'), + }); + + let addon = new TheAddon(); + + expect(() => { + addon.treeGenerator('foo'); + }).to.throw(/Addon `such name` is missing `addon.project`/); + }); + }); + + describe('treePaths and treeForMethods', function () { + let FirstAddon, SecondAddon; - beforeEach(function() { + beforeEach(function () { projectPath = path.resolve(fixturePath, 'simple'); - var packageContents = require(path.join(projectPath, 'package.json')); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); - project = new Project(projectPath, packageContents); + project = new Project(projectPath, packageContents, cli.ui, cli); FirstAddon = Addon.extend({ name: 'first', root: projectPath, + packageRoot: projectPath, - init: function() { + init() { + this._super.apply(this, arguments); this.treePaths.vendor = 'blazorz'; this.treeForMethods.public = 'huzzah!'; - } + }, }); SecondAddon = Addon.extend({ name: 'first', root: projectPath, + packageRoot: projectPath, - init: function() { + init() { + this._super.apply(this, arguments); this.treePaths.vendor = 'blammo'; this.treeForMethods.public = 'boooo'; - } + }, }); - }); - describe('.jshintAddonTree', function() { - var addon; + describe('.jshintAddonTree', function () { + let addon; - beforeEach(function() { - addon = new FirstAddon(project); + beforeEach(function () { + addon = new FirstAddon(project, project); // TODO: fix config story... addon.app = { - options: { jshintrc: {} }, - addonLintTree: function(type, tree) { return tree; } - }; - - addon.jshintTrees = function(){}; - - }); - - it('uses the fullPath', function() { - var addonPath; - addon.addonJsFiles = function(_path) { - addonPath = _path; + addonLintTree(type, tree) { + return tree; + }, }; - var root = path.join(fixturePath, 'with-styles'); - addon.root = root; - - addon.jshintAddonTree(); - expect(addonPath).to.eql(path.join(root, 'addon')); + addon.jshintTrees = function () {}; }); - it('lints the files before preprocessing', function() { - addon.preprocessJs = function() { - expect(false, 'should not preprocess files').to.eql(true); + it('lints the files before preprocessing', function () { + addon.preprocessJs = function () { + throw new Error('should not preprocess files'); }; - var root = path.join(fixturePath, 'with-styles'); + let root = path.join(fixturePath, 'with-styles'); addon.root = root; addon.jshintAddonTree(); }); - }); - it('modifying a treePath does not affect other addons', function() { - var first = new FirstAddon(project); - var second = new SecondAddon(project); + it('modifying a treePath does not affect other addons', function () { + let first = new FirstAddon(project); + let second = new SecondAddon(project); expect(first.treePaths.vendor).to.equal('blazorz'); expect(second.treePaths.vendor).to.equal('blammo'); }); - it('modifying a treeForMethod does not affect other addons', function() { - var first = new FirstAddon(project); - var second = new SecondAddon(project); + it('modifying a treeForMethod does not affect other addons', function () { + let first = new FirstAddon(project); + let second = new SecondAddon(project); expect(first.treeForMethods.public).to.equal('huzzah!'); expect(second.treeForMethods.public).to.equal('boooo'); }); }); - describe('resolvePath', function() { - beforeEach(function() { - addon = { - pkg: { - 'ember-addon': { - 'main': '' - } - }, - path: '' - }; - }); - - it('adds .js if not present', function() { - addon.pkg['ember-addon']['main'] = 'index'; - var resolvedFile = path.basename(Addon.resolvePath(addon)); - expect(resolvedFile).to.equal('index.js'); - }); - - it('doesn\'t add .js if it is .js', function() { - addon.pkg['ember-addon']['main'] = 'index.js'; - var resolvedFile = path.basename(Addon.resolvePath(addon)); - expect(resolvedFile).to.equal('index.js'); - }); - - it('doesn\'t add .js if it has another extension', function() { - addon.pkg['ember-addon']['main'] = 'index.coffee'; - var resolvedFile = path.basename(Addon.resolvePath(addon)); - expect(resolvedFile).to.equal('index.coffee'); - }); - - it('allows lookup of non-`index.js` `main` entry points', function() { - delete addon.pkg['ember-addon']; - addon.pkg['main'] = 'some/other/path.js'; - - var resolvedFile = Addon.resolvePath(addon); - expect(resolvedFile).to.equal(path.join(process.cwd(), 'some/other/path.js')); - }); - - it('falls back to `index.js` if `main` and `ember-addon` are not found', function() { - delete addon.pkg['ember-addon']; - - var resolvedFile = Addon.resolvePath(addon); - expect(resolvedFile).to.equal(path.join(process.cwd(), 'index.js')); - }); - - it('falls back to `index.js` if `main` and `ember-addon.main` are not found', function() { - delete addon.pkg['ember-addon'].main; - - var resolvedFile = Addon.resolvePath(addon); - expect(resolvedFile).to.equal(path.join(process.cwd(), 'index.js')); - }); - }); - - describe('initialized addon', function() { + describe('initialized addon', function () { this.timeout(40000); - before(function() { + beforeEach(function () { projectPath = path.resolve(fixturePath, 'simple'); - var packageContents = require(path.join(projectPath, 'package.json')); - project = new Project(projectPath, packageContents); + const packageContents = require(path.join(projectPath, 'package.json')); + let ui = new MockUI(); + let cli = new MockCLI({ ui }); + project = new Project(projectPath, packageContents, ui, cli); project.initializeAddons(); }); - describe('generated addon', function() { - beforeEach(function() { - addon = findWhere(project.addons, { name: 'Ember CLI Generated with export' }); + describe('generated addon', function () { + beforeEach(function () { + addon = project.addons.find((addon) => addon.name === 'ember-generated-with-export-addon'); // Clear the caches delete addon._moduleName; }); - it('sets it\'s project', function() { + it('sets its project', function () { expect(addon.project.name).to.equal(project.name); }); - it('sets it\'s parent', function() { + it('sets its parent', function () { expect(addon.parent.name).to.equal(project.name); }); - it('sets the root', function() { + it('sets the root', function () { expect(addon.root).to.not.equal(undefined); }); - it('sets the pkg', function() { + it('sets the pkg', function () { expect(addon.pkg).to.not.equal(undefined); }); - describe('trees for it\'s treePaths', function() { - it('app', function() { - var tree = addon.treeFor('app'); + describe('trees for its treePaths', function () { + it('app', function () { + let tree = addon.treeFor('app'); expect(typeof (tree.read || tree.rebuild)).to.equal('function'); }); - it('styles', function() { - var tree = addon.treeFor('styles'); + it('styles', function () { + let tree = addon.treeFor('styles'); expect(typeof (tree.read || tree.rebuild)).to.equal('function'); }); - it('templates', function() { - var tree = addon.treeFor('templates'); + it('templates', function () { + let tree = addon.treeFor('templates'); expect(typeof (tree.read || tree.rebuild)).to.equal('function'); }); - it('addon-templates', function() { - var tree = addon.treeFor('addon-templates'); + it('addon-templates', function () { + let tree = addon.treeFor('addon-templates'); expect(typeof (tree.read || tree.rebuild)).to.equal('function'); }); - it('vendor', function() { - var tree = addon.treeFor('vendor'); + it('vendor', function () { + let tree = addon.treeFor('vendor'); expect(typeof (tree.read || tree.rebuild)).to.equal('function'); }); - it('addon', function() { - var app = { - importWhitelist: {}, + it('addon', function () { + let app = { options: {}, }; addon.registry = { app: addon, - load: function() { - return [{ - toTree: function(tree) { - return tree; - } - }]; + load() { + return [ + { + toTree(tree) { + return tree; + }, + }, + ]; }, - extensionsForType: function() { + extensionsForType() { return ['js']; - } + }, }; addon.app = app; - var tree = addon.treeFor('addon'); + let tree = addon.treeFor('addon'); expect(typeof (tree.read || tree.rebuild)).to.equal('function'); }); }); - describe('custom treeFor methods', function() { - it('can define treeForApp', function() { - var called = false; - - addon.treeForApp = function() { - called = true; - }; - + describe('custom treeFor methods', function () { + it('can define treeForApp', function () { + addon.treeForApp = td.function(); addon.treeFor('app'); - expect(called).to.equal(true); + td.verify(addon.treeForApp(), { ignoreExtraArgs: true }); }); - it('can define treeForStyles', function() { - var called = false; - - addon.treeForStyles = function() { - called = true; - }; - + it('can define treeForStyles', function () { + addon.treeForStyles = td.function(); addon.treeFor('styles'); - expect(called).to.equal(true); + td.verify(addon.treeForStyles(), { ignoreExtraArgs: true }); }); - it('can define treeForVendor', function() { - var called = false; - - addon.treeForVendor = function() { - called = true; - }; - + it('can define treeForVendor', function () { + addon.treeForVendor = td.function(); addon.treeFor('vendor'); - expect(called).to.equal(true); + td.verify(addon.treeForVendor(), { ignoreExtraArgs: true }); }); - it('can define treeForTemplates', function() { - var called = false; - - addon.treeForTemplates = function() { - called = true; - }; - + it('can define treeForTemplates', function () { + addon.treeForTemplates = td.function(); addon.treeFor('templates'); - expect(called).to.equal(true); + td.verify(addon.treeForTemplates(), { ignoreExtraArgs: true }); }); - it('can define treeForAddonTemplates', function() { - var called = false; - - addon.treeForAddonTemplates = function() { - called = true; - }; - + it('can define treeForAddonTemplates', function () { + addon.treeForAddonTemplates = td.function(); addon.treeFor('addon-templates'); - expect(called).to.equal(true); + td.verify(addon.treeForAddonTemplates(), { ignoreExtraArgs: true }); }); - it('can define treeForPublic', function() { - var called = false; - - addon.treeForPublic = function() { - called = true; - }; - + it('can define treeForPublic', function () { + addon.treeForPublic = td.function(); addon.treeFor('public'); - expect(called).to.equal(true); + td.verify(addon.treeForPublic(), { ignoreExtraArgs: true }); }); }); }); - describe('addon with dependencies', function() { - beforeEach(function() { - addon = findWhere(project.addons, { name: 'Ember Addon With Dependencies' }); + describe('addon with dependencies', function () { + beforeEach(function () { + addon = project.addons.find((addon) => addon.name === 'ember-addon-with-dependencies'); }); - it('returns a listing of all dependencies in the addon\'s package.json', function() { - var expected = { + it("returns a listing of all dependencies in the addon's package.json", function () { + let expected = { 'ember-cli': 'latest', - 'something-else': 'latest' + 'something-else': 'latest', }; expect(addon.dependencies()).to.deep.equal(expected); }); }); - it('must define a `name` property', function() { - var Foo = Addon.extend({ root: 'foo' }); + it('must define a `name` property', function () { + let Foo = Addon.extend({ root: 'foo', packageRoot: 'foo' }); - expect(function() { + expect(() => { new Foo(project); }).to.throw(/An addon must define a `name` property./); }); - describe('isDevelopingAddon', function() { - var originalEnvValue, addon, project; + describe('isDevelopingAddon', function () { + let originalEnvValue, addon, project; - beforeEach(function() { - var MyAddon = Addon.extend({ + beforeEach(function () { + let MyAddon = Addon.extend({ name: 'test-project', - root: 'foo' + root: 'foo', + packageRoot: 'foo', }); - var projectPath = path.resolve(fixturePath, 'simple'); - var packageContents = require(path.join(projectPath, 'package.json')); + let projectPath = path.resolve(fixturePath, 'simple'); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); - project = new Project(projectPath, packageContents); + project = new Project(projectPath, packageContents, cli.ui, cli); - addon = new MyAddon(project); + addon = new MyAddon(project, project); originalEnvValue = process.env.EMBER_ADDON_ENV; }); - afterEach(function() { - if(originalEnvValue === undefined) { + afterEach(function () { + if (originalEnvValue === undefined) { delete process.env.EMBER_ADDON_ENV; } else { process.env.EMBER_ADDON_ENV = originalEnvValue; } }); - it('returns true when `EMBER_ADDON_ENV` is set to development', function() { + it('returns true when `EMBER_ADDON_ENV` is set to development', function () { process.env.EMBER_ADDON_ENV = 'development'; expect(addon.isDevelopingAddon(), 'addon is being developed').to.eql(true); }); - it('returns false when `EMBER_ADDON_ENV` is not set', function() { + it('throws when the addon name is prefixed in package.json and not in index.js', function () { + process.env.EMBER_ADDON_ENV = 'development'; + project.root = 'foo'; + project.name = () => '@foo/my-addon'; + addon.name = 'my-addon'; + expect(() => addon.isDevelopingAddon()).to.throw(/Your names in package.json and index.js must match*/); + }); + + it('throws an error if addon name is different in package.json and index.js ', function () { + process.env.EMBER_ADDON_ENV = 'development'; + project.root = 'foo'; + project.name = () => 'foo-my-addon'; + addon.name = 'my-addon'; + expect(() => addon.isDevelopingAddon()).to.throw(/Your names in package.json and index.js must match*/); + }); + + it('returns false when `EMBER_ADDON_ENV` is not set', function () { delete process.env.EMBER_ADDON_ENV; expect(addon.isDevelopingAddon()).to.eql(false); }); - it('returns false when `EMBER_ADDON_ENV` is something other than `development`', function() { + it('returns false when `EMBER_ADDON_ENV` is something other than `development`', function () { process.env.EMBER_ADDON_ENV = 'production'; expect(addon.isDevelopingAddon()).to.equal(false); }); - it('returns false when the addon is not the one being developed', function() { + it('returns false when the addon is not the one being developed', function () { process.env.EMBER_ADDON_ENV = 'development'; addon.name = 'my-addon'; @@ -401,64 +343,188 @@ describe('models/addon.js', function() { }); }); - describe('treeGenerator', function() { - it('watch tree when developing the addon itself', function() { - addon.isDevelopingAddon = function() { return true; }; + describe('findOwnAddonByName', function () { + let ThisAddon = Addon.extend({ + root: 'foo', + packageRoot: 'foo', + name: 'this-addon', + }); - var tree = addon.treeGenerator('foo/bar'); + it('it has the given addon', function () { + let addon = new ThisAddon(); + let ownAddon = { name: 'my-cool-addon' }; + addon.addons = [ownAddon]; + expect(addon.findOwnAddonByName('my-cool-addon')).to.eql(ownAddon); + }); - expect(tree.__broccoliGetInfo__()).to.have.property('watched', true); + it('it does not have the given addon', function () { + let addon = new ThisAddon(); + let ownAddon = { name: 'my-cool-addon' }; + addon.addons = [ownAddon]; + expect(addon.findOwnAddonByName('my-non-existentcool-addon')).to.eql(undefined); }); + }); - it('uses unwatchedTree when not developing the addon itself', function() { - addon.isDevelopingAddon = function() { return false; }; + describe('hintingEnabled', function () { + /** + Tests the various configuration options that affect the hintingEnabled method. - var tree = addon.treeGenerator('foo/bar'); + | configuration | test1 | test2 | test3 | test4 | test5 | + | ------------- | ----- | ----- | ----- | ----- | ----- | + | hinting | true | true | true | false | unset | + | environment | dev | N/A | prod | N\A | N\A | + | test_command | set | set | unset | set | set | + | RESULT | true | true | false | false | true | - expect(tree.__broccoliGetInfo__()).to.have.property('watched', false); + @method hintingEnabled + */ + + let originalEnvValue, originalEmberEnvValue, originalTestCommand, addon, project; + + beforeEach(function () { + let MyAddon = Addon.extend({ + name: 'test-project', + root: 'foo', + packageRoot: 'foo', + }); + + let projectPath = path.resolve(fixturePath, 'simple'); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); + + project = new Project(projectPath, packageContents, cli.ui, cli); + + addon = new MyAddon(project); + + originalEmberEnvValue = process.env.EMBER_ENV; + originalEnvValue = process.env.EMBER_ADDON_ENV; + originalTestCommand = process.env.EMBER_CLI_TEST_COMMAND; + }); + + afterEach(function () { + addon.app = { + options: {}, + }; + + if (originalEnvValue === undefined) { + delete process.env.EMBER_ADDON_ENV; + } else { + process.env.EMBER_ADDON_ENV = originalEnvValue; + } + + if (originalTestCommand === undefined) { + delete process.env.EMBER_CLI_TEST_COMMAND; + } else { + process.env.EMBER_CLI_TEST_COMMAND = originalTestCommand; + } + + if (originalEmberEnvValue === undefined) { + delete process.env.EMBER_ENV; + } else { + process.env.EMBER_ENV = originalEmberEnvValue; + } + }); + + it('returns true when `EMBER_ENV` is not set to production and options.hinting is true', function () { + process.env.EMBER_ENV = 'development'; + + addon.app = { + options: { hinting: true }, + }; + + expect(addon.hintingEnabled()).to.be.true; + }); + + it('returns true when `EMBER_CLI_TEST_COMMAND` is set and options.hinting is true', function () { + addon.app = { + options: { hinting: true }, + }; + + expect(addon.hintingEnabled()).to.be.true; + }); + + it('returns false when `EMBER_ENV` is set to production, `EMBER_CLI_TEST_COMMAND` is unset and options.hinting is true', function () { + process.env.EMBER_ENV = 'production'; + delete process.env.EMBER_CLI_TEST_COMMAND; + + addon.app = { + options: { hinting: true }, + }; + + expect(addon.hintingEnabled()).to.be.false; + }); + + it('returns false when options.hinting is set to false', function () { + addon.app = { + options: { hinting: false }, + }; + + expect(addon.hintingEnabled()).to.be.false; + }); + + it('returns true when options.hinting is not set', function () { + expect(addon.hintingEnabled()).to.be.ok; }); }); - describe('blueprintsPath', function() { - var tmpdir; + describe('treeGenerator', function () { + it('watch tree when developing the addon itself', function () { + addon.isDevelopingAddon = function () { + return true; + }; - beforeEach(function() { - tmpdir = tmp.in(tmproot); + let tree = addon.treeGenerator('foo/bar'); - addon.root = tmpdir; + expect(tree.__broccoliGetInfo__()).to.have.property('watched', true); }); - afterEach(function() { - return remove(tmproot); + it('uses UnwatchedDir when not developing the addon itself', function () { + addon.isDevelopingAddon = function () { + return false; + }; + + let tree = addon.treeGenerator('foo/bar'); + + expect(tree.__broccoliGetInfo__()).to.have.property('watched', false); }); + }); - it('returns undefined if the `blueprint` folder does not exist', function() { - var returnedPath = addon.blueprintsPath(); + describe('blueprintsPath', function () { + let tmpdir; + + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpdir = path; + addon.root = path; + }); + + it('returns undefined if the `blueprint` folder does not exist', function () { + let returnedPath = addon.blueprintsPath(); expect(returnedPath).to.equal(undefined); }); - it('returns blueprint path if the folder exists', function() { - var blueprintsDir = path.join(tmpdir, 'blueprints'); + it('returns blueprint path if the folder exists', function () { + let blueprintsDir = path.join(tmpdir, 'blueprints'); fs.mkdirSync(blueprintsDir); - var returnedPath = addon.blueprintsPath(); + let returnedPath = addon.blueprintsPath(); expect(returnedPath).to.equal(blueprintsDir); }); }); - describe('config', function() { - it('returns undefined if `config/environment.js` does not exist', function() { + describe('config', function () { + it('returns undefined if `config/environment.js` does not exist', function () { addon.root = path.join(fixturePath, 'no-config'); - var result = addon.config(); + let result = addon.config(); expect(result).to.equal(undefined); }); - it('returns blueprint path if the folder exists', function() { + it('returns blueprint path if the folder exists', function () { addon.root = path.join(fixturePath, 'with-config'); - var appConfig = {}; + let appConfig = {}; addon.config('development', appConfig); @@ -467,92 +533,361 @@ describe('models/addon.js', function() { }); }); - describe('Addon.lookup', function() { - it('should throw an error if an addon could not be found', function() { - var addon = { - path: 'foo/bar-baz/blah/doesnt-exist', - pkg: { - name: 'dummy-addon', - 'ember-addon': { } - } - }; + describe('compileTemplates', function () { + beforeEach(function () { + projectPath = path.resolve(fixturePath, 'simple'); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); + + project = new Project(projectPath, packageContents, cli.ui, cli); + + project.initializeAddons(); - expect(function() { - Addon.lookup(addon); - }).to.throw(/The `dummy-addon` addon could not be found at `foo\/bar-baz\/blah\/doesnt-exist`\./); + addon = project.addons.find((addon) => addon.name === 'ember-generated-with-export-addon'); + }); + + it('should not throw an error if addon/templates is present but empty', function () { + addon.root = path.join(fixturePath, 'with-empty-addon-templates'); + + expect(() => { + addon.compileTemplates(); + }).not.to.throw(); }); }); - describe('compileTemplates', function() { - beforeEach(function() { + describe('_fileSystemInfo', function () { + beforeEach(function () { projectPath = path.resolve(fixturePath, 'simple'); - var packageContents = require(path.join(projectPath, 'package.json')); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); - project = new Project(projectPath, packageContents); + project = new Project(projectPath, packageContents, cli.ui, cli); project.initializeAddons(); - addon = findWhere(project.addons, { name: 'Ember CLI Generated with export' }); + addon = project.addons.find((addon) => addon.name === 'ember-generated-with-export-addon'); }); - it('should throw a useful error if a template compiler is not present -- non-pods', function() { - addon.root = path.join(fixturePath, 'with-addon-templates'); + it('should not call _getAddonTemplatesTreeFiles when default treePath is used', function () { + let wasCalled = false; + addon._getAddonTemplatesTreeFiles = function () { + wasCalled = true; + return []; + }; - expect(function() { - addon.compileTemplates(); - }).to.throw( - 'Addon templates were detected, but there ' + - 'are no template compilers registered for `' + addon.name + '`. ' + - 'Please make sure your template precompiler (commonly `ember-cli-htmlbars`) ' + - 'is listed in `dependencies` (NOT `devDependencies`) in ' + - '`' + addon.name + '`\'s `package.json`.' - ); + addon._fileSystemInfo(); + + expect(wasCalled).to.not.be.ok; }); - it('should throw a useful error if a template compiler is not present -- pods', function() { - addon.root = path.join(fixturePath, 'with-addon-pod-templates'); + it("should call _getAddonTemplatesTreeFiles when custom treePaths['addon-templates'] is used", function () { + addon.treePaths['addon-templates'] = 'foo'; + let wasCalled = false; + addon._getAddonTemplatesTreeFiles = function () { + wasCalled = true; + return []; + }; - expect(function() { - addon.compileTemplates(); - }).to.throw( - 'Addon templates were detected, but there ' + - 'are no template compilers registered for `' + addon.name + '`. ' + - 'Please make sure your template precompiler (commonly `ember-cli-htmlbars`) ' + - 'is listed in `dependencies` (NOT `devDependencies`) in ' + - '`' + addon.name + '`\'s `package.json`.' - ); + addon._fileSystemInfo(); + + expect(wasCalled).to.be.ok; }); - it('should not throw an error if addon/templates is present but empty', function() { - addon.root = path.join(fixturePath, 'with-empty-addon-templates'); + /* The following three are skipped because they fail due to something about the test setup that + appears to not be loading ember-cli-htmlbars. + */ + it.skip('hasPodTemplates when pod templates found', function () { + addon._getAddonTreeFiles = function () { + return ['foo-bar/', 'foo-bar/component.js', 'foo-bar/template.hbs']; + }; - expect(function() { - addon.compileTemplates(); - }).not.to.throw(); + expect(addon._fileSystemInfo()).to.deep.equal({ + hasJSFiles: true, + hasTemplates: true, + hasPodTemplates: true, + }); + }); + + it.skip('does not hasPodTemplates when no pod templates found', function () { + addon._getAddonTreeFiles = function () { + return ['templates/', 'templates/components/', 'templates/components/foo-bar.hbs']; + }; + + expect(addon._fileSystemInfo()).to.deep.equal({ + hasJSFiles: false, + hasTemplates: true, + hasPodTemplates: false, + }); + }); + + it.skip('does not hasPodTemplates when no pod templates found (pod-like structure in `addon/templates/`)', function () { + addon._getAddonTreeFiles = function () { + return [ + 'templates/', + // this doesn't need "pod template handling" because + // it is actually in the addon-templates tree + 'templates/foo-bar/template.hbs', + ]; + }; + + expect(addon._fileSystemInfo()).to.deep.equal({ + hasJSFiles: false, + hasTemplates: true, + hasPodTemplates: false, + }); + }); + + it('does not hasTemplates when no templates found', function () { + addon._getAddonTreeFiles = function () { + return ['components/', 'components/foo-bar.js', 'templates/', 'templates/components/']; + }; + + expect(addon._fileSystemInfo()).to.deep.equal({ + hasJSFiles: true, + hasTemplates: false, + hasPodTemplates: false, + }); + }); + + it('does not hasJSFiles when none found', function () { + addon._getAddonTreeFiles = function () { + return ['components/', 'templates/', 'templates/components/', 'styles/foo.css']; + }; + + expect(addon._fileSystemInfo()).to.deep.equal({ + hasJSFiles: false, + hasTemplates: false, + hasPodTemplates: false, + }); }); }); - describe('addonDiscovery', function() { - var discovery, addon, ui; + describe('packageInfoCache', function () { + let packageInfoCache, addon, ui; - beforeEach(function() { + beforeEach(function () { projectPath = path.resolve(fixturePath, 'simple'); - var packageContents = require(path.join(projectPath, 'package.json')); + const packageContents = require(path.join(projectPath, 'package.json')); ui = new MockUI(); - project = new Project(projectPath, packageContents, ui); + let cli = new MockCLI({ ui }); + project = new Project(projectPath, packageContents, ui, cli); - var AddonTemp = Addon.extend({ + let AddonTemp = Addon.extend({ name: 'temp', - root: 'foo' + root: 'foo', + packageRoot: 'foo', }); addon = new AddonTemp(project, project); - discovery = addon.addonDiscovery; + packageInfoCache = addon.packageInfoCache; }); - it('is provided with the addon\'s `ui` object', function() { - expect(discovery.ui).to.equal(ui); + it("is provided with the parent's `packageInfoCache` object", function () { + expect(packageInfoCache).to.equal(project.packageInfoCache); + }); + }); + + describe('treeForStyles', function () { + let builder, addon; + + beforeEach(function () { + projectPath = path.resolve(fixturePath, 'with-app-styles'); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); + + project = new Project(projectPath, packageContents, cli.ui, cli); + + let BaseAddon = Addon.extend({ + name: 'test-project', + root: projectPath, + packageRoot: projectPath, + }); + + addon = new BaseAddon(project, project); + }); + + afterEach(function () { + if (builder) { + return builder.cleanup(); + } + }); + + it('should move files in the root of the addons app/styles tree into the app/styles path', async function () { + builder = new broccoli.Builder(addon.treeFor('styles')); + + await builder.build(); + + let outputPath = builder.outputPath; + let expected = ['app/', 'app/styles/', 'app/styles/foo-bar.css']; + + expect(walkSync(outputPath)).to.eql(expected); + }); + }); + + describe('._eachProjectAddonInvoke', function () { + beforeEach(function () { + let MyAddon = Addon.extend({ + name: 'test-project', + root: 'foo', + packageRoot: 'foo', + }); + + let projectPath = path.resolve(fixturePath, 'simple'); + const packageContents = require(path.join(projectPath, 'package.json')); + let cli = new MockCLI(); + + project = new Project(projectPath, packageContents, cli.ui, cli); + addon = new MyAddon(project, project); + }); + + it('should invoke the method on each of the project addons', function () { + let counter = 0; + project.addons = [ + { + foo(num) { + counter += num; + }, + }, + { + foo(num) { + counter += num; + }, + }, + ]; + + addon._eachProjectAddonInvoke('foo', [1]); + expect(counter).to.eql(2); + }); + + it('should provide default arguments if none are specified', function () { + let counter = 0; + project.addons = [ + { + foo() { + counter += 1; + }, + }, + { + foo() { + counter += 1; + }, + }, + ]; + + addon._eachProjectAddonInvoke('foo'); + expect(counter).to.eql(2); + }); + }); + + describe('addon tree caching', function () { + let projectPath = path.resolve(fixturePath, 'simple'); + const packageContents = require(path.join(projectPath, 'package.json')); + + function createAddon(Addon) { + let cli = new MockCLI(); + let project = new Project(projectPath, packageContents, cli.ui, cli); + return new Addon(project, project); + } + + describe('cacheKeyForTree', function () { + it('returns null if `treeForApp` methods are implemented for the app tree', function () { + let addon = createAddon( + Addon.extend({ + name: 'test-project', + root: 'foo', + packageRoot: 'foo', + treeForApp() {}, + }) + ); + + expect(addon.cacheKeyForTree('app')).to.equal(null); + }); + + it('returns null if `compileAddon` methods are implemented for the addon tree', function () { + let addon = createAddon( + Addon.extend({ + name: 'test-project', + root: 'foo', + packageRoot: 'foo', + compileAddon() {}, + }) + ); + + expect(addon.cacheKeyForTree('addon')).to.equal(null); + }); + + it('returns null if `treeForMethods` is modified', function () { + let addon = createAddon( + Addon.extend({ + name: 'test-project', + root: 'foo', + packageRoot: 'foo', + init() { + this._super && this._super.init.apply(this, arguments); + + this.treeForMethods['app'] = 'treeForZOMG_WHY!?!'; + }, + }) + ); + + expect(addon.cacheKeyForTree('app')).to.equal(null); + }); + + it('returns stable value for repeated invocations', function () { + let addon = createAddon( + Addon.extend({ + name: 'test-project', + root: 'foo', + packageRoot: 'foo', + }) + ); + + let firstResult = addon.cacheKeyForTree('app'); + let secondResult = addon.cacheKeyForTree('app'); + + expect(firstResult).to.equal(secondResult); + }); + }); + + describe('treeFor caching', function () { + it('defining custom treeForAddon without modifying cacheKeyForTree does not cache', function () { + let addon = createAddon( + Addon.extend({ + name: 'test-project', + root: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'), + packageRoot: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'), + treeForAddon(tree) { + return tree; + }, + }) + ); + + let firstTree = addon.treeFor('addon'); + let secondTree = addon.treeFor('addon'); + + expect(firstTree).not.to.equal(secondTree); + }); + + it('defining custom cacheKeyForTree allows addon control of cache', function () { + let addonProto = { + name: 'test-project', + root: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'), + packageRoot: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'), + treeForAddon(tree) { + return tree; + }, + }; + addonProto.cacheKeyForTree = function (type) { + return type; + }; + + let addon = createAddon(Addon.extend(addonProto)); + let firstTree = addon.treeFor('addon'); + let secondTree = addon.treeFor('addon'); + + expect(firstTree).to.equal(secondTree); + }); }); }); }); diff --git a/tests/unit/models/asset-size-printer-test.js b/tests/unit/models/asset-size-printer-test.js new file mode 100644 index 0000000000..d34385d805 --- /dev/null +++ b/tests/unit/models/asset-size-printer-test.js @@ -0,0 +1,152 @@ +'use strict'; + +const { expect } = require('chai'); +const MockUi = require('console-ui/mock'); +const AssetSizePrinter = require('../../../lib/models/asset-size-printer'); +const path = require('path'); +const fs = require('fs-extra'); +const tmp = require('tmp-promise'); + +describe('models/asset-size-printer', function () { + let storedTmpDir, assetDir, assetChildDir; + + function writeFiles() { + fs.writeFileSync(path.join(assetDir, 'some-project.scss'), 'body { margin: 0 20px; }', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'some-project.css4'), 'body { margin: 0 20px; }', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'some-project.css'), 'body { margin: 0 20px; }', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'some-project.js'), 'module.exports = function () {};', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'some-project.json'), 'module.exports = function () {};', { + encoding: 'utf8', + }); + fs.writeFileSync(path.join(assetDir, 'test-loader.js'), 'module.exports = function () {};', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'test-support.js'), 'module.exports = function () {};', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'testem.js'), 'module.exports = function () {};', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'test.js'), 'module.exports = function () {};', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetDir, 'empty.js'), '', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetChildDir, 'nested-asset.css'), 'body { margin: 0 20px; }', { encoding: 'utf8' }); + fs.writeFileSync(path.join(assetChildDir, 'nested-asset.js'), 'module.exports = function () {};', { + encoding: 'utf8', + }); + } + + beforeEach(async function () { + const { path: tempPath } = await tmp.dir(); + storedTmpDir = tempPath; + + assetDir = path.join(storedTmpDir, 'assets'); + assetChildDir = path.join(assetDir, 'childDir'); + + fs.mkdirsSync(assetDir); + fs.mkdirsSync(assetChildDir); + + writeFiles(); + }); + + afterEach(function () { + return fs.remove(storedTmpDir); + }); + + it('prints human-readable file sizes (including gzipped sizes) of css and js files in the output path', async function () { + let sizePrinter = new AssetSizePrinter({ + ui: new MockUi(), + outputPath: storedTmpDir, + }); + + await sizePrinter.print(); + + expect(sizePrinter.ui.output).to.include('File sizes:'); + expect(sizePrinter.ui.output).to.include('some-project.css: '); + expect(sizePrinter.ui.output).to.include('some-project.js: '); + expect(sizePrinter.ui.output).to.include('24 B'); + expect(sizePrinter.ui.output).to.include('32 B'); + expect(sizePrinter.ui.output).to.include('(44 B gzipped)'); + expect(sizePrinter.ui.output).to.include('(52 B gzipped)'); + }); + + it('does not print gzipped file sizes of empty files', async function () { + let sizePrinter = new AssetSizePrinter({ + ui: new MockUi(), + outputPath: storedTmpDir, + }); + + await sizePrinter.print(); + expect(sizePrinter.ui.output).to.not.include('0 B gzipped)'); + }); + + it('does not print project test helper file sizes', async function () { + let sizePrinter = new AssetSizePrinter({ + ui: new MockUi(), + outputPath: storedTmpDir, + }); + + await sizePrinter.print(); + + expect(sizePrinter.ui.output).to.not.include('test-loader'); + expect(sizePrinter.ui.output).to.not.include('test-support'); + expect(sizePrinter.ui.output).to.not.include('testem'); + expect(sizePrinter.ui.output).to.include('test.js'); + }); + + it('does not print non-css or js file sizes', async function () { + let sizePrinter = new AssetSizePrinter({ + ui: new MockUi(), + outputPath: storedTmpDir, + }); + + await sizePrinter.print(); + + expect(sizePrinter.ui.output).to.not.include('some-project.scss'); + expect(sizePrinter.ui.output).to.not.include('some-project.css4'); + expect(sizePrinter.ui.output).to.not.include('some-project.json'); + }); + + it('can print out to JSON', async function () { + let sizePrinter = new AssetSizePrinter({ + ui: new MockUi(), + outputPath: storedTmpDir, + }); + + await sizePrinter.printJSON(); + + let output = JSON.parse(sizePrinter.ui.output); + + expect(output.files[0].name).to.include('nested-asset.css'); + expect(output.files[1].name).to.include('nested-asset.js'); + expect(output.files[1].size).to.equal(32); + expect(output.files[1].gzipSize).to.equal(52); + expect(output.files[0]).to.not.have.property('showGzipped'); + }); + + it('creates an array of asset objects', async function () { + let assetObjectKeys; + let sizePrinter = new AssetSizePrinter({ + ui: new MockUi(), + outputPath: storedTmpDir, + }); + + let assetObject = await sizePrinter.makeAssetSizesObject(); + + assetObjectKeys = Object.keys(assetObject[0]); + + expect(assetObjectKeys).to.deep.equal(['name', 'size', 'gzipSize', 'showGzipped']); + expect(assetObject[0].name).to.include('nested-asset.css'); + expect(assetObject[1].name).to.include('nested-asset.js'); + expect(assetObject[2].name).to.include('empty.js'); + expect(assetObject[3].name).to.include('some-project.css'); + expect(assetObject[4].name).to.include('some-project.js'); + expect(assetObject[5].name).to.include('test.js'); + }); + + it('prints an error when no files are found', function () { + let outputPath = path.join('path', 'that', 'does', 'not', 'exist'); + let sizePrinter = new AssetSizePrinter({ + ui: new MockUi(), + outputPath, + }); + + return expect(sizePrinter.print()).to.be.rejectedWith( + Error, + `No asset files found in the path provided: ${outputPath}` + ); + }); +}); diff --git a/tests/unit/models/blueprint-test.js b/tests/unit/models/blueprint-test.js index e02e20ab0c..d26b587817 100644 --- a/tests/unit/models/blueprint-test.js +++ b/tests/unit/models/blueprint-test.js @@ -1,1627 +1,376 @@ 'use strict'; -var fs = require('fs-extra'); -var Blueprint = require('../../../lib/models/blueprint'); -var Task = require('../../../lib/models/task'); -var MockProject = require('../../helpers/mock-project'); -var MockUI = require('../../helpers/mock-ui'); -var expect = require('chai').expect; -var path = require('path'); -var glob = require('glob'); -var walkSync = require('walk-sync'); -var Promise = require('../../../lib/ext/promise'); -var remove = Promise.denodeify(fs.remove); -var EOL = require('os').EOL; -var root = process.cwd(); -var tmp = require('tmp-sync'); -var tmproot = path.join(root, 'tmp'); -var SilentError = require('silent-error'); - -var defaultBlueprints = path.resolve(__dirname, '..', '..', '..', 'blueprints'); -var fixtureBlueprints = path.resolve(__dirname, '..', '..', 'fixtures', 'blueprints'); -var basicBlueprint = path.join(fixtureBlueprints, 'basic'); -var basicNewBlueprint = path.join(fixtureBlueprints, 'basic_2'); - -var defaultIgnoredFiles = Blueprint.ignoredFiles; - -var basicBlueprintFiles = [ - '.ember-cli', - '.gitignore', - 'bar', - 'foo.txt', - 'test.txt' -]; - -describe('Blueprint', function() { - beforeEach(function() { - Blueprint.ignoredFiles = defaultIgnoredFiles; +const processHelpString = require('../../helpers/process-help-string'); +const { expect } = require('chai'); +const path = require('path'); +const EOL = require('os').EOL; +const MarkdownColor = require('@ember-tooling/blueprint-model/utilities/markdown-color'); +const td = require('testdouble'); + +let Blueprint = require('@ember-tooling/blueprint-model'); + +describe('Blueprint', function () { + afterEach(function () { + td.reset(); }); - describe('.mapFile', function() { - it('replaces all occurences of __name__ with module name',function(){ - var path = Blueprint.prototype.mapFile('__name__/__name__-controller.js',{dasherizedModuleName: 'my-blueprint'}); - expect(path).to.equal('my-blueprint/my-blueprint-controller.js'); - - path = Blueprint.prototype.mapFile('__name__/controller.js',{dasherizedModuleName: 'my-blueprint'}); - expect(path).to.equal('my-blueprint/controller.js'); - - path = Blueprint.prototype.mapFile('__name__/__name__.js',{dasherizedModuleName: 'my-blueprint'}); - expect(path).to.equal('my-blueprint/my-blueprint.js'); - }); - it('accepts locals.fileMap with multiple mappings',function(){ - var locals = {}; - locals.fileMap= { - __name__: 'user', - __type__: 'controller', - __path__: 'pods/users', - __plural__: '' - }; - - var path = Blueprint.prototype.mapFile('__name__/__type____plural__.js',locals); - expect(path).to.equal('user/controller.js'); - - path = Blueprint.prototype.mapFile('__path__/__name__/__type__.js',locals); - expect(path).to.equal('pods/users/user/controller.js'); - }); - }); - describe('.fileMapTokens', function() { - it('adds additional tokens from fileMapTokens hook', function() { - var blueprint = Blueprint.lookup(basicBlueprint); - blueprint.fileMapTokens = function() { - return { - __foo__: function(){ - return 'foo'; - } - }; - }; - var tokens = blueprint._fileMapTokens(); - expect(tokens.__foo__()).to.equal('foo'); - }); - }); - describe('.generateFileMap', function() { - it('should not have locals in the fileMap', function() { - var blueprint = Blueprint.lookup(basicBlueprint); - - var fileMapVariables = { - pod: true, - podPath: 'pods', - isAddon: false, - blueprintName: 'test', - dasherizedModuleName: 'foo-baz', - locals: { SOME_LOCAL_ARG: 'ARGH' } - }; - - var fileMap = blueprint.generateFileMap(fileMapVariables); - var expected = { - __name__: 'foo-baz', - __path__: 'tests', - __root__: 'app', - __test__: 'foo-baz-test' - }; - - expect(fileMap).to.deep.equal(expected); - }); - }); - describe('.lookup', function() { - it('uses an explicit path if one is given', function() { - var expectedClass = require(basicBlueprint); - var blueprint = Blueprint.lookup(basicBlueprint); - - expect(blueprint.name).to.equal('basic'); - expect(blueprint.path).to.equal(basicBlueprint); - expect(blueprint instanceof expectedClass).to.equal(true); - }); - - it('finds blueprints within given lookup paths', function() { - var expectedClass = require(basicBlueprint); - var blueprint = Blueprint.lookup('basic', { - paths: [fixtureBlueprints] - }); - - expect(blueprint.name).to.equal('basic'); - expect(blueprint.path).to.equal(basicBlueprint); - expect(blueprint instanceof expectedClass).to.equal(true); - }); - - it('finds blueprints in the ember-cli package', function() { - var expectedPath = path.resolve(defaultBlueprints, 'app'); - var expectedClass = Blueprint; - - var blueprint = Blueprint.lookup('app'); - - expect(blueprint.name).to.equal('app'); - expect(blueprint.path).to.equal(expectedPath); - expect(blueprint instanceof expectedClass).to.equal(true); - }); - - it('can instantiate a blueprint that exports an object instead of a constructor', function() { - var blueprint = Blueprint.lookup('exporting-object', { - paths: [fixtureBlueprints] - }); - - expect(blueprint.woot).to.equal('someValueHere'); - expect(blueprint instanceof Blueprint).to.equal(true); - }); - - it('throws an error if no blueprint is found', function() { - expect(function() { - Blueprint.lookup('foo'); - }).to.throw('Unknown blueprint: foo'); - }); - - it('returns undefined if no blueprint is found and ignoredMissing is passed', function() { - var blueprint = Blueprint.lookup('foo', { - ignoreMissing: true - }); - - expect(blueprint).to.equal(undefined); - }); - }); - - describe('.list', function() { - it('returns a list of blueprints grouped by lookup path', function() { - var list = Blueprint.list({ paths: [fixtureBlueprints] }); - var actual = list[0]; - var expected = { - source: 'fixtures', - blueprints: [{ - name: 'basic', - description: 'A basic blueprint', - overridden: false - }, { - name: 'basic_2', - description: 'Another basic blueprint', - overridden: false - }, { - name: 'exporting-object', - description: 'A blueprint that exports an object', - overridden: false - }, { - name: 'with-templating', - description: 'A blueprint with templating', - overridden: false - }] - }; - - expect(actual[0]).to.deep.equal(expected[0]); - }); - }); - - it('exists', function() { - var blueprint = new Blueprint(basicBlueprint); - expect(!!blueprint).to.equal(true); - }); - - it('derives name from path', function() { - var blueprint = new Blueprint(basicBlueprint); - expect(blueprint.name).to.equal('basic'); - }); - - describe('basic blueprint installation', function() { - var BasicBlueprintClass = require(basicBlueprint); - var blueprint; - var ui; - var project; - var options; - var tmpdir; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new BasicBlueprintClass(basicBlueprint); - ui = new MockUI(); - project = new MockProject(); - options = { - ui: ui, - project: project, - target: tmpdir - }; - }); - - afterEach(function() { - return remove(tmproot); - }); - - it('installs basic files', function() { - expect(!!blueprint).to.equal(true); - - return blueprint.install(options) - .then(function() { - var actualFiles = walkSync(tmpdir).sort(); - var output = ui.output.trim().split(EOL); - - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/create.* .ember-cli/); - expect(output.shift()).to.match(/create.* .gitignore/); - expect(output.shift()).to.match(/create.* bar/); - expect(output.shift()).to.match(/create.* foo.txt/); - expect(output.shift()).to.match(/create.* test.txt/); - expect(output.length).to.equal(0); - - expect(actualFiles).to.deep.equal(basicBlueprintFiles); - - expect( function(){ - fs.readFile(path.join(tmpdir , 'test.txt'), 'utf-8', - function(err, content){ - if(err){ - throw 'error'; - } - expect(content).to.match(/I AM TESTY/); - }); - } - ).not.to.throw(); - - }); - }); - - it('re-installing identical files', function() { - return blueprint.install(options) - .then(function() { - var output = ui.output.trim().split(EOL); - ui.output = ''; - - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/create.* .ember-cli/); - expect(output.shift()).to.match(/create.* .gitignore/); - expect(output.shift()).to.match(/create.* bar/); - expect(output.shift()).to.match(/create.* foo.txt/); - expect(output.shift()).to.match(/create.* test.txt/); - expect(output.length).to.equal(0); - - return blueprint.install(options); - }) - .then(function() { - var actualFiles = walkSync(tmpdir).sort(); - var output = ui.output.trim().split(EOL); - - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/identical.* .ember-cli/); - expect(output.shift()).to.match(/identical.* .gitignore/); - expect(output.shift()).to.match(/identical.* bar/); - expect(output.shift()).to.match(/identical.* foo.txt/); - expect(output.shift()).to.match(/identical.* test.txt/); - expect(output.length).to.equal(0); - - expect(actualFiles).to.deep.equal(basicBlueprintFiles); - }); - }); - - it('re-installing conflicting files', function() { - return blueprint.install(options) - .then(function() { - var output = ui.output.trim().split(EOL); - ui.output = ''; - - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/create.* .ember-cli/); - expect(output.shift()).to.match(/create.* .gitignore/); - expect(output.shift()).to.match(/create.* bar/); - expect(output.shift()).to.match(/create.* foo.txt/); - expect(output.shift()).to.match(/create.* test.txt/); - expect(output.length).to.equal(0); - var blueprintNew = new Blueprint(basicNewBlueprint); - - ui.waitForPrompt().then(function(){ - ui.inputStream.write('n' + EOL); - return ui.waitForPrompt(); - }).then(function(){ - ui.inputStream.write('y' + EOL); - }); - - return blueprintNew.install(options); - }) - .then(function() { - var actualFiles = walkSync(tmpdir).sort(); - // Prompts contain \n EOL - // Split output on \n since it will have the same affect as spliting on OS specific EOL - var output = ui.output.trim().split('\n'); - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/Overwrite.*foo.*\?/); // Prompt - expect(output.shift()).to.match(/Overwrite.*foo.*No, skip/); - expect(output.shift()).to.match(/Overwrite.*test.*\?/); // Prompt - expect(output.shift()).to.match(/Overwrite.*test.*Yes, overwrite/); - expect(output.shift()).to.match(/identical.* \.ember-cli/); - expect(output.shift()).to.match(/identical.* \.gitignore/); - expect(output.shift()).to.match(/skip.* foo.txt/); - expect(output.shift()).to.match(/overwrite.* test.txt/); - expect(output.length).to.equal(0); - - expect(actualFiles).to.deep.equal(basicBlueprintFiles); - }); - }); - - it('installs path globPattern file', function() { - options.targetFiles = ['foo.txt']; - return blueprint.install(options) - .then(function() { - var actualFiles = walkSync(tmpdir).sort(); - var globFiles = glob.sync(path.join('**', 'foo.txt'), { - cwd: tmpdir, - dot: true, - mark: true, - strict: true - }).sort(); - var output = ui.output.trim().split(EOL); - - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/create.* foo.txt/); - expect(output.length).to.equal(0); - - expect(actualFiles).to.deep.equal(globFiles); - }); - }); - - it('installs multiple globPattern files', function() { - options.targetFiles = ['foo.txt','test.txt']; - return blueprint.install(options) - .then(function() { - var actualFiles = walkSync(tmpdir).sort(); - var globFiles = glob.sync(path.join('**', '*.txt'), { - cwd: tmpdir, - dot: true, - mark: true, - strict: true - }).sort(); - var output = ui.output.trim().split(EOL); - - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/create.* foo.txt/); - expect(output.shift()).to.match(/create.* test.txt/); - expect(output.length).to.equal(0); - - expect(actualFiles).to.deep.equal(globFiles); - }); - }); - - describe('called on an existing project', function() { - beforeEach(function() { - Blueprint.ignoredUpdateFiles.push('foo.txt'); - }); - - it('ignores files in ignoredUpdateFiles', function() { - return blueprint.install(options) - .then(function() { - var output = ui.output.trim().split(EOL); - ui.output = ''; - - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/create.* .ember-cli/); - expect(output.shift()).to.match(/create.* .gitignore/); - expect(output.shift()).to.match(/create.* bar/); - expect(output.shift()).to.match(/create.* foo.txt/); - expect(output.shift()).to.match(/create.* test.txt/); - expect(output.length).to.equal(0); - - var blueprintNew = new Blueprint(basicNewBlueprint); - - ui.waitForPrompt().then(function(){ - ui.inputStream.write('n' + EOL); - return ui.waitForPrompt(); - }).then(function(){ - ui.inputStream.write('n' + EOL); - }); - - options.project.isEmberCLIProject = function() { return true; }; - - return blueprintNew.install(options); - }) - .then(function() { - var actualFiles = walkSync(tmpdir).sort(); - // Prompts contain \n EOL - // Split output on \n since it will have the same affect as spliting on OS specific EOL - var output = ui.output.trim().split('\n'); - expect(output.shift()).to.match(/^installing/); - expect(output.shift()).to.match(/Overwrite.*test.*\?/); // Prompt - expect(output.shift()).to.match(/Overwrite.*test.*No, skip/); - expect(output.shift()).to.match(/identical.* \.ember-cli/); - expect(output.shift()).to.match(/identical.* \.gitignore/); - expect(output.shift()).to.match(/skip.* test.txt/); - expect(output.length).to.equal(0); - - expect(actualFiles).to.deep.equal(basicBlueprintFiles); - }); - }); - }); - - it('throws error when there is a trailing forward slash in entityName', function(){ - options.entity = { name: 'foo/' }; - expect(function() { - blueprint.install(options); - }).to.throw(/You specified "foo\/", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./); - - options.entity = { name: 'foo\\' }; - expect(function() { - blueprint.install(options); - }).to.throw(/You specified "foo\\", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./); - - options.entity = { name: 'foo' }; - expect(function() { - blueprint.install(options); - }).not.to.throw(); + describe('.removeTypes', function () { + it('returns input when passing javascript', async function () { + const output = await Blueprint.prototype.removeTypes('.js', 'const x = 1;\n'); + expect(output).to.equal('const x = 1;\n'); }); - it('throws error when an entityName is not provided', function(){ - options.entity = { }; - expect(function() { - blueprint.install(options); - }).to.throw(SilentError, /The `ember generate ` command requires an entity name to be specified./); + it('strips types when converting ts', async function () { + const output = await Blueprint.prototype.removeTypes('.ts', 'const x: number = 1;\n'); + expect(output).to.equal('const x = 1;\n'); }); - it('throws error when an action does not exist', function() { - blueprint._actions = {}; - return blueprint.install(options) - .catch(function(err) { - expect(err.message).to.equal('Tried to call action "write" but it does not exist'); - }); + it('stripes types when converting gts', async function () { + const output = await Blueprint.prototype.removeTypes( + '.gts', + 'const x: number = 1;\n\n' + ); + expect(output).to.equal('const x = 1;\n\n'); }); - it('calls normalizeEntityName hook during install', function(done){ - blueprint.normalizeEntityName = function(){ done(); }; - options.entity = { name: 'foo' }; - blueprint.install(options); - }); + it('keeps imports used in templates when converting gts', async function () { + const output = await Blueprint.prototype.removeTypes( + '.gts', + `import { foo } from 'foo'; +import type { Bar } from 'bar'; - it('normalizeEntityName hook can modify the entity name', function(){ - blueprint.normalizeEntityName = function(){ return 'foo'; }; - options.entity = { name: 'bar' }; +const bar: Bar = 'bar'; - return blueprint.install(options) - .then(function() { - var actualFiles = walkSync(tmpdir).sort(); + +` + ); + expect(output).to.equal(`import { foo } from 'foo'; - expect(actualFiles).to.deep.equal(basicBlueprintFiles); - }); - }); +const bar = 'bar'; - it('calls normalizeEntityName before locals hook is called', function(done) { - blueprint.normalizeEntityName = function(){ return 'foo'; }; - blueprint.locals = function(options) { - expect(options.entity.name).to.equal('foo'); - done(); - }; - options.entity = { name: 'bar' }; - blueprint.install(options); + +`); }); - }); - describe('basic blueprint uninstallation', function() { - var BasicBlueprintClass = require(basicBlueprint); - var blueprint; - var ui; - var project; - var options; - var tmpdir; - - function refreshUI() { - ui = new MockUI(); - options.ui = ui; - } - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new BasicBlueprintClass(basicBlueprint); - project = new MockProject(); - options = { - project: project, - target: tmpdir - }; - - refreshUI(); - return blueprint.install(options) - .then(refreshUI); + it('can handle template-only gts', async function () { + const output = await Blueprint.prototype.removeTypes('.gts', '\n'); + expect(output).to.equal('\n'); }); - afterEach(function() { - return remove(tmproot); + it('can handle multi-line template tag', async function () { + const output = await Blueprint.prototype.removeTypes('.gts', '\n'); + expect(output).to.equal('\n'); }); - it('uninstalls basic files', function() { - expect(!!blueprint).to.equal(true); - - return blueprint.uninstall(options) - .then(function() { - var actualFiles = walkSync(tmpdir); - var output = ui.output.trim().split(EOL); - - expect(output.shift()).to.match(/^uninstalling/); - expect(output.shift()).to.match(/remove.* .ember-cli/); - expect(output.shift()).to.match(/remove.* .gitignore/); - expect(output.shift()).to.match(/remove.* bar/); - expect(output.shift()).to.match(/remove.* foo.txt/); - expect(output.shift()).to.match(/remove.* test.txt/); - expect(output.length).to.equal(0); - - expect(actualFiles.length).to.equal(0); - - fs.exists(path.join(tmpdir, 'test.txt'), - function(exists) { - expect(exists).to.be.false; - }); - }); + it('can handle multiple template tags in one file', async function () { + const output = await Blueprint.prototype.removeTypes( + '.gts', + 'const x = \nconst y = \n' + ); + expect(output).to.equal('const x = \nconst y = \n'); }); - }); - describe('addPackageToProject', function() { - var blueprint; - var ui; - var tmpdir; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); + it('works in class body', async function () { + const output = await Blueprint.prototype.removeTypes( + '.gts', + 'const foo: number = 1;\nexport default class Bar extends Component {\n \n}\n' + ); + expect(output).to.equal( + 'const foo = 1;\nexport default class Bar extends Component {\n \n}\n' + ); }); - afterEach(function() { - return remove(tmproot); + it('can handle multi-byte characters', async function () { + const output = await Blueprint.prototype.removeTypes( + '.gts', + "const x: string = '💩';\n\n" + ); + expect(output).to.equal("const x = '💩';\n\n"); }); - it('passes a packages array for addPackagesToProject', function() { - blueprint.addPackagesToProject = function(packages) { - expect(packages).to.deep.equal([{name: 'foo-bar'}]); - }; - - blueprint.addPackageToProject('foo-bar'); + it('can handle template tags as function argument', async function () { + const output = await Blueprint.prototype.removeTypes( + '.gts', + 'await render();\n' + ); + expect(output).to.equal('await render();\n'); }); - it('passes a packages array with target for addPackagesToProject', function() { - blueprint.addPackagesToProject = function(packages) { - expect(packages).to.deep.equal([{name: 'foo-bar', target: '^123.1.12'}]); - }; - - blueprint.addPackageToProject('foo-bar', '^123.1.12'); + it('can handle template tags as function argument, including newlines', async function () { + const output = await Blueprint.prototype.removeTypes( + '.gts', + 'await render(\n \n);\n' + ); + expect(output).to.equal('await render();\n'); }); }); - describe('addPackagesToProject', function() { - var blueprint; - var ui; - var tmpdir; - var NpmInstallTask; - var taskNameLookedUp; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - - blueprint.taskFor = function(name) { - taskNameLookedUp = name; - - return new NpmInstallTask(); - }; - }); - - afterEach(function() { - return remove(tmproot); - }); - - it('looks up the `npm-install` task', function() { - NpmInstallTask = Task.extend({ - run: function() {} - }); - - blueprint.addPackagesToProject([{name: 'foo-bar'}]); - - expect(taskNameLookedUp).to.equal('npm-install'); - }); - - it('calls the task with package names', function() { - var packages; - - NpmInstallTask = Task.extend({ - run: function(options) { - packages = options.packages; - } - }); - - blueprint.addPackagesToProject([ - {name: 'foo-bar'}, - {name: 'bar-foo'} - ]); - - expect(packages).to.deep.equal(['foo-bar', 'bar-foo']); - }); - - it('calls the task with package names and versions', function() { - var packages; - - NpmInstallTask = Task.extend({ - run: function(options) { - packages = options.packages; - } - }); - - blueprint.addPackagesToProject([ - {name: 'foo-bar', target: '^123.1.12'}, - {name: 'bar-foo', target: '0.0.7'} - ]); - - expect(packages).to.deep.equal(['foo-bar@^123.1.12', 'bar-foo@0.0.7']); - }); - - it('writes information to the ui log for a single package', function() { - blueprint.ui = ui; - - blueprint.addPackagesToProject([ - {name: 'foo-bar', target: '^123.1.12'} - ]); - - var output = ui.output.trim(); - - expect(output).to.match(/install package.*foo-bar/); - }); - - it('writes information to the ui log for multiple packages', function() { - blueprint.ui = ui; - - blueprint.addPackagesToProject([ - {name: 'foo-bar', target: '^123.1.12'}, - {name: 'bar-foo', target: '0.0.7'} - ]); - - var output = ui.output.trim(); - - expect(output).to.match(/install packages.*foo-bar, bar-foo/); - }); - - it('does not error if ui is not present', function() { - delete blueprint.ui; - - blueprint.addPackagesToProject([ - {name: 'foo-bar', target: '^123.1.12'} - ]); - - var output = ui.output.trim(); - - expect(output).to.not.match(/install package.*foo-bar/); - }); - - it('runs task with --save-dev', function() { - var saveDev; - - NpmInstallTask = Task.extend({ - run: function(options) { - saveDev = options['save-dev']; - } - }); - - blueprint.addPackagesToProject([ - {name: 'foo-bar', target: '^123.1.12'}, - {name: 'bar-foo', target: '0.0.7'} - ]); - - expect(!!saveDev).to.equal(true); - }); - - it('does not use verbose mode with the task', function() { - var verbose; - - NpmInstallTask = Task.extend({ - run: function(options) { - verbose = options.verbose; - } + describe('.mapFile', function () { + it('replaces all occurrences of __name__ with module name', function () { + let path = Blueprint.prototype.mapFile('__name__/__name__-controller.js', { + dasherizedModuleName: 'my-blueprint', }); + expect(path).to.equal('my-blueprint/my-blueprint-controller.js'); - blueprint.addPackagesToProject([ - {name: 'foo-bar', target: '^123.1.12'}, - {name: 'bar-foo', target: '0.0.7'} - ]); + path = Blueprint.prototype.mapFile('__name__/controller.js', { dasherizedModuleName: 'my-blueprint' }); + expect(path).to.equal('my-blueprint/controller.js'); - expect(verbose).to.equal(false); + path = Blueprint.prototype.mapFile('__name__/__name__.js', { dasherizedModuleName: 'my-blueprint' }); + expect(path).to.equal('my-blueprint/my-blueprint.js'); }); - }); - - describe('removePackageFromProject', function() { - var blueprint; - var ui; - var tmpdir; - var NpmUninstallTask; - var taskNameLookedUp; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - blueprint.taskFor = function(name) { - taskNameLookedUp = name; - - return new NpmUninstallTask(); + it('accepts locals.fileMap with multiple mappings', function () { + let locals = {}; + locals.fileMap = { + __name__: 'user', + __type__: 'controller', + __path__: 'pods/users', + __plural__: '', }; - }); - - afterEach(function() { - return remove(tmproot); - }); - it('looks up the `npm-uninstall` task', function() { - NpmUninstallTask = Task.extend({ - run: function() {} - }); - - blueprint.removePackageFromProject({name: 'foo-bar'}); + let path = Blueprint.prototype.mapFile('__name__/__type____plural__.js', locals); + expect(path).to.equal('user/controller.js'); - expect(taskNameLookedUp).to.equal('npm-uninstall'); + path = Blueprint.prototype.mapFile('__path__/__name__/__type__.js', locals); + expect(path).to.equal('pods/users/user/controller.js'); }); - }); - describe('removePackagesFromProject', function() { - var blueprint; - var ui; - var tmpdir; - var NpmUninstallTask; - var taskNameLookedUp; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - - blueprint.taskFor = function(name) { - taskNameLookedUp = name; - - return new NpmUninstallTask(); - }; - }); - - afterEach(function() { - return remove(tmproot); - }); - - it('looks up the `npm-uninstall` task', function() { - NpmUninstallTask = Task.extend({ - run: function() {} + describe('.list', function () { + beforeEach(function () { + td.replace(Blueprint, '_existsSync', function (path) { + return path.indexOf('package.json') === -1; }); - blueprint.removePackagesFromProject([{name: 'foo-bar'}]); - - expect(taskNameLookedUp).to.equal('npm-uninstall'); - }); - - it('calls the task with package names', function() { - var packages; - - NpmUninstallTask = Task.extend({ - run: function(options) { - packages = options.packages; - } - }); - - blueprint.removePackagesFromProject([ - {name: 'foo-bar'}, - {name: 'bar-foo'} - ]); - - expect(packages).to.deep.equal(['foo-bar', 'bar-foo']); - }); - - it('writes information to the ui log for a single package', function() { - blueprint.ui = ui; - - blueprint.removePackagesFromProject([ - {name: 'foo-bar'} - ]); + td.replace(Blueprint, 'defaultLookupPaths'); + td.when(Blueprint.defaultLookupPaths()).thenReturn([]); - var output = ui.output.trim(); - - expect(output).to.match(/uninstall package.*foo-bar/); - }); - - it('writes information to the ui log for multiple packages', function() { - blueprint.ui = ui; - - blueprint.removePackagesFromProject([ - {name: 'foo-bar'}, - {name: 'bar-foo'} - ]); - - var output = ui.output.trim(); - - expect(output).to.match(/uninstall packages.*foo-bar, bar-foo/); - }); - - it('does not error if ui is not present', function() { - delete blueprint.ui; - - blueprint.removePackagesFromProject([ - {name: 'foo-bar'} - ]); - - var output = ui.output.trim(); - - expect(output).to.not.match(/uninstall package.*foo-bar/); - }); - - it('runs task with --save-dev', function() { - var saveDev; - - NpmUninstallTask = Task.extend({ - run: function(options) { - saveDev = options['save-dev']; - } + td.replace(Blueprint, 'load', function (blueprintPath) { + return { + name: path.basename(blueprintPath), + }; }); - - blueprint.removePackagesFromProject([ - {name: 'foo-bar'}, - {name: 'bar-foo'} - ]); - - expect(!!saveDev).to.equal(true); }); - it('does not use verbose mode with the task', function() { - var verbose; - - NpmUninstallTask = Task.extend({ - run: function(options) { - verbose = options.verbose; - } + it('returns a list of blueprints grouped by lookup path', function () { + td.replace(Blueprint, '_readdirSync', function () { + return ['test1', 'test2']; }); - blueprint.removePackagesFromProject([ - {name: 'foo-bar'}, - {name: 'bar-foo'} - ]); - - expect(verbose).to.equal(false); - }); - }); - - describe('addBowerPackageToProject', function() { - var blueprint; - var ui; - var tmpdir; - var BowerInstallTask; - var taskNameLookedUp; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - blueprint.ui = ui; - blueprint.taskFor = function(name) { - taskNameLookedUp = name; - - return new BowerInstallTask(); - }; - }); - - afterEach(function() { - return remove(tmproot); - }); - - it('passes a packages array for addBowerPackagesToProject', function() { - blueprint.addBowerPackagesToProject = function(packages) { - expect(packages).to.deep.equal([{name: 'foo-bar', source: 'foo-bar', target: '*'}]); - }; - - blueprint.addBowerPackageToProject('foo-bar'); - }); - - it('passes a packages array with target for addBowerPackagesToProject', function() { - blueprint.addBowerPackagesToProject = function(packages) { - expect(packages).to.deep.equal([{name: 'foo-bar', source: 'foo-bar', target: '1.0.0'}]); - }; - - blueprint.addBowerPackageToProject('foo-bar', '1.0.0'); - }); - - it('correctly handles local package naming, with a numbered pkg version', function() { - blueprint.addBowerPackagesToProject = function(packages) { - expect(packages).to.deep.equal([{name: 'foo-bar-local', target: '1.0.0', source: 'foo-bar'}]); - }; - - blueprint.addBowerPackageToProject('foo-bar-local', 'foo-bar#1.0.0'); - }); - - it('correctly handles local package naming, with a non-versioned package', function() { - blueprint.addBowerPackagesToProject = function(packages) { - expect(packages).to.deep.equal([{name: 'foo-bar-local', target: '*', source: 'http://twitter.github.io/bootstrap/assets/bootstrap'}]); - }; - - blueprint.addBowerPackageToProject('foo-bar-local', 'http://twitter.github.io/bootstrap/assets/bootstrap'); - }); - - it('correctly handles a single versioned package descriptor as argument (1) (DEPRECATED)', function() { - blueprint.ui = ui; - blueprint.addBowerPackagesToProject = function(packages) { - expect(packages).to.deep.equal([{name: 'foo-bar', target: '1.11.1', source: 'foo-bar'}]); - }; - - blueprint.addBowerPackageToProject('foo-bar#1.11.1'); - }); - }); - - describe('addBowerPackagesToProject', function() { - var blueprint; - var ui; - var tmpdir; - var BowerInstallTask; - var taskNameLookedUp; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - - blueprint.taskFor = function(name) { - taskNameLookedUp = name; - - return new BowerInstallTask(); - }; - }); - - afterEach(function() { - return remove(tmproot); - }); - - it('looks up the `bower-install` task', function() { - BowerInstallTask = Task.extend({ - run: function() {} + let list = Blueprint.list({ paths: ['test0/blueprints'] }); + + expect(list[0]).to.deep.equal({ + source: 'test0', + blueprints: [ + { + name: 'test1', + overridden: false, + }, + { + name: 'test2', + overridden: false, + }, + ], }); - blueprint.addBowerPackagesToProject([{name: 'foo-bar'}]); - - expect(taskNameLookedUp).to.equal('bower-install'); }); - it('calls the task with the package names', function() { - var packages; - - BowerInstallTask = Task.extend({ - run: function(options) { - packages = options.packages; - } + it('overrides a blueprint of the same name from another package', function () { + td.replace(Blueprint, '_readdirSync', function () { + return ['test2']; }); - blueprint.addBowerPackagesToProject([ - {name: 'foo-bar'}, - {name: 'bar-foo'} - ]); - - expect(packages).to.deep.equal(['foo-bar=foo-bar', 'bar-foo=bar-foo']); - }); - - it('uses the provided target (version, range, sha, etc)', function() { - var packages; - - BowerInstallTask = Task.extend({ - run: function(options) { - packages = options.packages; - } + let list = Blueprint.list({ + paths: ['test0/blueprints', 'test1/blueprints'], }); - blueprint.addBowerPackagesToProject([ - {name: 'foo-bar', target: '~1.0.0'}, - {name: 'bar-foo', target: '0.7.0'} - ]); - - expect(packages).to.deep.equal(['foo-bar=foo-bar#~1.0.0', 'bar-foo=bar-foo#0.7.0']); - }); - - it('properly parses a variety of bower package endpoints', function() { - var packages; - - BowerInstallTask = Task.extend({ - run: function(options) { - packages = options.packages; - } + expect(list[0]).to.deep.equal({ + source: 'test0', + blueprints: [ + { + name: 'test2', + overridden: false, + }, + ], }); - - blueprint.addBowerPackagesToProject([ - {name: '', source: 'jquery', target: '~2.0.0'}, - {name: 'backbone', source: 'backbone-amd', target: '~1.0.0'}, - {name: 'bootstrap', source: 'http://twitter.github.io/bootstrap/assets/bootstrap', target: '*'} - ]); - - expect(packages).to.deep.equal([ - // standard local name, versioned bower pkg - 'jquery#~2.0.0', - // custom local name, versioned bower pkg - 'backbone=backbone-amd#~1.0.0', - // no numbered version, custom local name - 'bootstrap=http://twitter.github.io/bootstrap/assets/bootstrap' - ]); - }); - - it('uses uses verbose mode with the task', function() { - var verbose; - - BowerInstallTask = Task.extend({ - run: function(options) { - verbose = options.verbose; - } + expect(list[1]).to.deep.equal({ + source: 'test1', + blueprints: [ + { + name: 'test2', + overridden: true, + }, + ], }); - - blueprint.addBowerPackagesToProject([ - {name: 'foo-bar', target: '~1.0.0'}, - {name: 'bar-foo', target: '0.7.0'} - ]); - - expect(verbose).to.equal(true); }); }); - describe('addAddonToProject', function() { - var blueprint; - var ui; - var tmpdir; - var AddonInstallTask; - var taskNameLookedUp; + describe('help', function () { + let blueprint; - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - - blueprint.taskFor = function(name) { - taskNameLookedUp = name; - - return new AddonInstallTask(); - }; + beforeEach(function () { + blueprint = new Blueprint('path/to/my-blueprint'); }); - afterEach(function() { - return remove(tmproot); - }); - - it('looks up the `addon-install` task', function() { - AddonInstallTask = Task.extend({ - run: function() {} + describe('printBasicHelp', function () { + beforeEach(function () { + td.replace(blueprint, '_printCommand', td.function()); + td.replace(blueprint, 'printDetailedHelp', td.function()); + td.when(blueprint.printDetailedHelp(), { ignoreExtraArgs: true }).thenReturn('help in detail'); }); - blueprint.addAddonToProject('foo-bar'); - - expect(taskNameLookedUp).to.equal('addon-install'); - }); - - it('calls the task with package name', function() { - var pkg; - - AddonInstallTask = Task.extend({ - run: function(options) { - pkg = options['packages']; - } - }); + it('handles overridden', function () { + Object.assign(blueprint, { + overridden: true, + }); - blueprint.addAddonToProject('foo-bar'); + let output = blueprint.printBasicHelp(); - expect(pkg).to.deep.equal(['foo-bar']); - }); + let testString = processHelpString( + '\ + \u001b[90m(overridden) my-blueprint\u001b[39m' + ); - it('calls the task with correctly parsed options', function() { - var pkg, args; + expect(output).to.equal(testString); - AddonInstallTask = Task.extend({ - run: function(options) { - pkg = options['packages']; - args = options['extraArgs']; - } + td.verify(blueprint._printCommand(), { times: 0 }); }); - blueprint.addAddonToProject({ - name: 'foo-bar', - target: '1.0.0', - extraArgs: ['baz'] - }); + it('calls printCommand', function () { + td.when(blueprint._printCommand(), { ignoreExtraArgs: true }).thenReturn(' command printed'); - expect(pkg).to.deep.equal(['foo-bar@1.0.0']); - expect(args).to.deep.equal(['baz']); - }); + let output = blueprint.printBasicHelp(); - it('writes information to the ui log for a single package', function() { - blueprint.ui = ui; + let testString = processHelpString( + '\ + my-blueprint command printed' + ); - blueprint.addAddonToProject({ - name: 'foo-bar', - target: '^123.1.12' + expect(output).to.equal(testString); }); - var output = ui.output.trim(); - - expect(output).to.match(/install addon.*foo-bar/); - }); - - it('does not error if ui is not present', function() { - delete blueprint.ui; - - blueprint.addAddonToProject({ - name: 'foo-bar', target: '^123.1.12'} - ); - - var output = ui.output.trim(); - - expect(output).to.not.match(/install addon.*foo-bar/); - }); - }); - - describe('load', function(){ - var blueprint; - it('loads and returns a blueprint object', function() { - blueprint = Blueprint.load(basicBlueprint); - expect(blueprint).to.be.an('object'); - expect(blueprint.name).to.equal('basic'); - }); - it('loads only blueprints with an index.js', function() { - expect(Blueprint.load(path.join(fixtureBlueprints, '.notablueprint'))).to.be.empty; - }); - }); - - describe('insertIntoFile', function() { - var blueprint; - var ui; - var tmpdir; - var project; - var filename; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - project = new MockProject(); - - // normally provided by `install`, but mocked here for testing - project.root = tmpdir; - blueprint.project = project; + it('prints detailed help if verbose', function () { + td.when(blueprint._printCommand(), { ignoreExtraArgs: true }).thenReturn(' command printed'); - filename = 'foo-bar-baz.txt'; - }); - - afterEach(function() { - return remove(tmproot); - }); - - it('will create the file if not already existing', function() { - var toInsert = 'blahzorz blammo'; - - return blueprint.insertIntoFile(filename, toInsert) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); - - expect(contents.indexOf(toInsert) > -1).to.equal(true, 'contents were inserted'); - expect(result.originalContents).to.equal('', 'returned object should contain original contents'); - expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); - expect(contents).to.equal(result.contents, 'returned object should contain contents'); + let availableOptions = []; + Object.assign(blueprint, { + availableOptions, }); - }); - it('will insert into the file if it already exists', function() { - var toInsert = 'blahzorz blammo'; - var originalContent = 'some original content\n'; - var filePath = path.join(project.root, filename); + let output = blueprint.printBasicHelp(true); - fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + let testString = processHelpString(`\ + my-blueprint command printed${EOL}\ +help in detail`); - return blueprint.insertIntoFile(filename, toInsert) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); - - expect(contents).to.equal(originalContent + toInsert, 'inserted contents should be appended to original'); - expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); - expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); - }); + expect(output).to.equal(testString); + }); }); - it('will not insert into the file if it already contains the content', function() { - var toInsert = 'blahzorz blammo'; - var filePath = path.join(project.root, filename); - - fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' }); - - return blueprint.insertIntoFile(filename, toInsert) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); - - expect(contents).to.equal(toInsert, 'contents should be unchanged'); - expect(result.inserted).to.equal(false, 'inserted should indicate that the file was not modified'); + describe('printDetailedHelp', function () { + it('did not find the file', function () { + td.replace(Blueprint, '_existsSync', function () { + return false; }); - }); - it('will insert into the file if it already contains the content if force option is passed', function() { - var toInsert = 'blahzorz blammo'; - var filePath = path.join(project.root, filename); + td.replace(MarkdownColor.prototype, 'renderFile'); - fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' }); + let help = blueprint.printDetailedHelp(); + expect(help).to.equal(''); - return blueprint.insertIntoFile(filename, toInsert, { force: true }) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); + td.verify(MarkdownColor.prototype.renderFile(), { ignoreExtraArgs: true, times: 0 }); + }); - expect(contents).to.equal(toInsert + toInsert, 'contents should be unchanged'); - expect(result.inserted).to.equal(true, 'inserted should indicate that the file was not modified'); + it('found the file', function () { + td.replace(Blueprint, '_existsSync', function () { + return true; }); - }); - - it('will insert into the file after a specified string if options.after is specified', function(){ - var toInsert = 'blahzorz blammo'; - var line1 = 'line1 is here'; - var line2 = 'line2 here'; - var line3 = 'line3'; - var originalContent = [line1, line2, line3].join(EOL); - var filePath = path.join(project.root, filename); - - fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); - return blueprint.insertIntoFile(filename, toInsert, {after: line2 + EOL}) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); - - expect(contents).to.equal([line1, line2, toInsert, line3].join(EOL), - 'inserted contents should be inserted after the `after` value'); - expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); - expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + td.replace(MarkdownColor.prototype, 'renderFile', function () { + expect(arguments[1].indent).to.equal(' '); + return 'test-file'; }); - }); - - it('will insert into the file after the first instance of options.after only', function(){ - var toInsert = 'blahzorz blammo'; - var line1 = 'line1 is here'; - var line2 = 'line2 here'; - var line3 = 'line3'; - var originalContent = [line1, line2, line2, line3].join(EOL); - var filePath = path.join(project.root, filename); - fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + let help = blueprint.printDetailedHelp(); - return blueprint.insertIntoFile(filename, toInsert, {after: line2 + EOL}) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); - - expect(contents).to.equal([line1, line2, toInsert, line2, line3].join(EOL), - 'inserted contents should be inserted after the `after` value'); - expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); - expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); - }); + expect(help).to.equal('test-file'); + }); }); - it('will insert into the file before a specified string if options.before is specified', function(){ - var toInsert = 'blahzorz blammo'; - var line1 = 'line1 is here'; - var line2 = 'line2 here'; - var line3 = 'line3'; - var originalContent = [line1, line2, line3].join(EOL); - var filePath = path.join(project.root, filename); - - fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); - - return blueprint.insertIntoFile(filename, toInsert, {before: line2 + EOL}) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); + describe('getJson', function () { + beforeEach(function () { + blueprint._printableProperties = ['test1', 'availableOptions']; + }); - expect(contents).to.equal([line1, toInsert, line2, line3].join(EOL), - 'inserted contents should be inserted before the `before` value'); - expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); - expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + it('iterates options', function () { + let availableOptions = [ + { + type: 'my-string-type', + showAnything: true, + }, + { + type: function myFunctionType() {}, + }, + ]; + + Object.assign(blueprint, { + test1: 'a test', + availableOptions, }); - }); - it('will insert into the file before the first instance of options.before only', function(){ - var toInsert = 'blahzorz blammo'; - var line1 = 'line1 is here'; - var line2 = 'line2 here'; - var line3 = 'line3'; - var originalContent = [line1, line2, line2, line3].join(EOL); - var filePath = path.join(project.root, filename); - - fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); - - return blueprint.insertIntoFile(filename, toInsert, {before: line2 + EOL}) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); - - expect(contents).to.equal([line1, toInsert, line2, line2, line3].join(EOL), - 'inserted contents should be inserted after the `after` value'); - expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); - expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + let json = blueprint.getJson(); + + expect(json).to.deep.equal({ + test1: 'a test', + availableOptions: [ + { + type: 'my-string-type', + showAnything: true, + }, + { + type: 'myFunctionType', + }, + ], }); - }); - - - it('it will make no change if options.after is not found in the original', function(){ - var toInsert = 'blahzorz blammo'; - var originalContent = 'the original content'; - var filePath = path.join(project.root, filename); - - fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + }); - return blueprint.insertIntoFile(filename, toInsert, {after: 'not found' + EOL}) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); + it('does not print detailed if not verbose', function () { + td.replace(blueprint, 'printDetailedHelp', td.function()); - expect(contents).to.equal(originalContent, 'original content is unchanged'); - expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); - expect(result.inserted).to.equal(false, 'inserted should indicate that the file was not modified'); - }); - }); + blueprint.getJson(); - it('it will make no change if options.before is not found in the original', function(){ - var toInsert = 'blahzorz blammo'; - var originalContent = 'the original content'; - var filePath = path.join(project.root, filename); - - fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + td.verify(blueprint.printDetailedHelp(), { ignoreExtraArgs: true, times: 0 }); + }); - return blueprint.insertIntoFile(filename, toInsert, {before: 'not found' + EOL}) - .then(function(result) { - var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' }); + it('is calling printDetailedHelp with availableOptions', function () { + td.replace(blueprint, 'printDetailedHelp', td.function()); - expect(contents).to.equal(originalContent, 'original content is unchanged'); - expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); - expect(result.inserted).to.equal(false, 'inserted should indicate that the file was not modified'); + let availableOptions = []; + Object.assign(blueprint, { + availableOptions, }); - }); - }); - - describe('lookupBlueprint', function() { - var blueprint; - var ui; - var tmpdir; - var project; - var filename; - - beforeEach(function() { - tmpdir = tmp.in(tmproot); - blueprint = new Blueprint(basicBlueprint); - ui = new MockUI(); - project = new MockProject(); - - // normally provided by `install`, but mocked here for testing - project.root = tmpdir; - blueprint.project = project; - project.blueprintLookupPaths = function() { - return [fixtureBlueprints]; - }; - - filename = 'foo-bar-baz.txt'; - }); - - afterEach(function() { - return remove(tmproot); - }); - it('can lookup other Blueprints from the project blueprintLookupPaths', function() { - var result = blueprint.lookupBlueprint('basic_2'); - - expect(result.description).to.equal('Another basic blueprint'); - }); - - it('can find internal blueprints', function() { - var result = blueprint.lookupBlueprint('controller'); - - expect(result.description).to.equal('Generates a controller.'); - }); - }); - - describe('._generateFileMapVariables', function() { - var blueprint; - var project; - var moduleName; - var locals; - var options; - var result; - var expectation; - - beforeEach(function() { - blueprint = new Blueprint(basicBlueprint); - project = new MockProject(); - moduleName = project.name(); - locals = {}; - - blueprint.project = project; - - options = { - project: project - }; - - expectation = { - blueprintName: 'basic', - dasherizedModuleName: 'mock-project', - hasPathToken: undefined, - inAddon: false, - inDummy: false, - inRepoAddon: undefined, - locals: {}, - originBlueprintName: 'basic', - pod: undefined, - podPath: '' - }; - }); - - it('should create the correct default fileMapVariables', function() { - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); - - it('should use the moduleName method argument for moduleName', function() { - moduleName = 'foo'; - expectation.dasherizedModuleName = 'foo'; - - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); - - it('should use the locals method argument for its locals value', function() { - locals = { foo: 'bar' }; - expectation.locals = locals; - - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); - - it('should use the option.originBlueprintName value as its originBlueprintName if included in the options hash', function() { - options.originBlueprintName = 'foo'; - expectation.originBlueprintName = 'foo'; - - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); - - it('should include a podPath if the project\'s podModulePrefix is defined', function() { - blueprint.project.config = function() { - return { - podModulePrefix: 'foo/bar' - }; - }; - - expectation.podPath = 'bar'; - - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); - - it('should include an inAddon and inDummy flag of true if the project is an addon', function () { - options.dummy = true; - - blueprint.project.isEmberCLIAddon = function() { - return true; - }; - - expectation.inAddon = true; - expectation.inDummy = true; - - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); + blueprint.getJson(true); - it('should include an inAddon and inRepoAddon flag of true if options.inRepoAddon is true', function() { - options.inRepoAddon = true; - - expectation.inRepoAddon = true; - expectation.inAddon = true; - - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); - - it('should have a hasPathToken flag of true if the blueprint hasPathToken is true', function() { - blueprint.hasPathToken = true; - - expectation.hasPathToken = true; - - result = blueprint._generateFileMapVariables(moduleName, locals, options); - - expect(result).to.eql(expectation); - }); - }); - - describe('._locals', function() { - var blueprint; - var project; - var options; - var result; - var expectation; - - beforeEach(function() { - blueprint = new Blueprint(basicBlueprint); - project = new MockProject(); - - blueprint._generateFileMapVariables = function() { - return {}; - }; - - blueprint.generateFileMap = function() { - return {}; - }; - - options = { - project: project - }; - - expectation = { - 'camelizedModuleName': 'mockProject', - 'classifiedModuleName': 'MockProject', - 'classifiedPackageName': 'MockProject', - 'dasherizedModuleName': 'mock-project', - 'dasherizedPackageName': 'mock-project', - 'decamelizedModuleName': 'mock-project', - 'fileMap': {} - }; - }); - - it('should return a default object if no custom options are passed', function() { - result = blueprint._locals(options); - - expect(result).to.eql(expectation); - }); - - it('it should call the locals method with the correct arguments', function() { - blueprint.locals = function (opts) { - expect(opts).to.equal(options); - }; - - blueprint._locals(options); - }); - - it('should call _generateFileMapVariables with the correct arguments', function() { - blueprint.locals = function() { - return { foo: 'bar' }; - }; - - blueprint._generateFileMapVariables = function(modName, lcls, opts) { - expect(modName).to.equal('mock-project'); - expect(lcls).to.eql({ foo: 'bar' }); - expect(opts).to.eql(opts); - }; - - blueprint._locals(options); - }); - - it('should call generateFileMap with the correct arguments', function() { - blueprint._generateFileMapVariables = function() { - return { bar: 'baz' }; - }; - - blueprint.generateFileMap = function(fileMapVariables) { - expect(fileMapVariables).to.eql({ bar: 'baz' }); - }; - - blueprint._locals(options); - }); - - it('should use the options.entity.name as its moduleName if its value is defined', function() { - options.entity = { - name: 'foo' - }; - - expectation.camelizedModuleName = 'foo'; - expectation.classifiedModuleName = 'Foo'; - expectation.dasherizedModuleName = 'foo'; - expectation.decamelizedModuleName = 'foo'; - - result = blueprint._locals(options); - - expect(result).to.eql(expectation); - }); - - it('should update its fileMap values to match the generateFileMap result', function() { - blueprint.generateFileMap = function() { - return { foo: 'bar' }; - }; - - expectation.fileMap = { foo: 'bar' }; + td.verify(blueprint.printDetailedHelp(availableOptions)); + }); - result = blueprint._locals(options); + it("if printDetailedHelp returns falsy, don't attach property detailedHelp", function () { + td.replace(blueprint, 'printDetailedHelp', td.function()); - expect(result).to.eql(expectation); - }); + let json = blueprint.getJson(true); - it('should return an object containing custom local values', function() { - blueprint.locals = function() { - return { foo: 'bar' }; - }; + td.verify(blueprint.printDetailedHelp(), { ignoreExtraArgs: true, times: 1 }); + expect(json).to.not.have.property('detailedHelp'); + }); - expectation.foo = 'bar'; + it('sets detailedHelp properly', function () { + td.replace(blueprint, 'printDetailedHelp', td.function()); + td.when(blueprint.printDetailedHelp(), { ignoreExtraArgs: true }).thenReturn('some details'); - result = blueprint._locals(options); + let json = blueprint.getJson(true); - expect(result).to.eql(expectation); + expect(json.detailedHelp).to.equal('some details'); + }); }); }); }); diff --git a/tests/unit/models/builder-test.js b/tests/unit/models/builder-test.js index b563afbf25..b6117b7561 100644 --- a/tests/unit/models/builder-test.js +++ b/tests/unit/models/builder-test.js @@ -1,272 +1,496 @@ 'use strict'; -var fs = require('fs-extra'); -var path = require('path'); -var Builder = require('../../../lib/models/builder'); -var BuildCommand = require('../../../lib/commands/build'); -var commandOptions = require('../../factories/command-options'); -var touch = require('../../helpers/file-utils').touch; -var existsSync = require('exists-sync'); -var expect = require('chai').expect; -var Promise = require('../../../lib/ext/promise'); -var stub = require('../../helpers/stub').stub; -var MockProject = require('../../helpers/mock-project'); -var remove = Promise.denodeify(fs.remove); -var tmp = require('tmp-sync'); - -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); - -describe('models/builder.js', function() { - var addon, builder, buildResults, outputPath, tmpdir; - - describe('copyToOutputPath', function() { - beforeEach(function() { - tmpdir = tmp.in(tmproot); +const fs = require('fs-extra'); +const path = require('path'); +const BuildCommand = require('../../../lib/commands/build'); +const commandOptions = require('../../factories/command-options'); +const fixturify = require('fixturify'); +const MockProject = require('../../helpers/mock-project'); +const td = require('testdouble'); +const { expect } = require('chai'); +const { file } = require('chai-files'); +const tmp = require('tmp-promise'); + +let Builder; + +describe('models/builder.js', function () { + let addon, builder, buildResults, tmpdir; + + async function setupBroccoliBuilder() { + this.builder = { + outputPath: 'build results', + outputNodeWrapper: { + __heimdall__: {}, + }, + build() { + return Promise.resolve({ + outputPath: 'build results', + outputNodeWrapper: { + __heimdall__: {}, + }, + }); + }, + cleanup() {}, + }; + } + + before(function () { + let willInterruptProcess = require('../../../lib/utilities/will-interrupt-process'); + td.replace(willInterruptProcess, 'addHandler', td.function()); + td.replace(willInterruptProcess, 'removeHandler', td.function()); + + Builder = require('../../../lib/models/builder'); + }); + afterEach(function () { + if (builder) { + return builder.cleanup(); + } + }); + + describe('copyToOutputPath', function () { + beforeEach(async function () { + const { path } = await tmp.dir(); + tmpdir = path; + let project = new MockProject(); builder = new Builder({ - setupBroccoliBuilder: function() { }, - trapSignals: function() { }, - cleanupOnExit: function() { }, - project: new MockProject() + project, + ui: project.ui, + setupBroccoliBuilder, }); }); - afterEach(function() { - return remove(tmproot); + it('allows for non-existent output-paths at arbitrary depth', function () { + builder.outputPath = path.join(tmpdir, 'some', 'path', 'that', 'does', 'not', 'exist'); + + builder.copyToOutputPath('tests/fixtures/blueprints/basic_2'); + expect(file(path.join(builder.outputPath, 'files', 'foo.txt'))).to.exist; }); - it('allows for non-existent output-paths at arbitrary depth', function() { - builder.outputPath = path.join(tmpdir, 'some', 'path', 'that', 'does', 'not', 'exist'); + describe('build command', function () { + let command; + let parentPath = `..${path.sep}..${path.sep}`; + + beforeEach(function () { + command = new BuildCommand(commandOptions()); - return builder.copyToOutputPath('tests/fixtures/blueprints/basic_2') - .then(function() { - expect(existsSync(path.join(builder.outputPath, 'files', 'foo.txt'))).to.equal(true); + let project = new MockProject(); + builder = new Builder({ + project, + ui: project.ui, + setupBroccoliBuilder, }); + }); + + it('when outputPath is root directory ie., `--output-path=/` or `--output-path=C:`', function () { + let outputPathArg = '--output-path=.'; + let outputPath = command.parseArgs([outputPathArg]).options.outputPath; + outputPath = outputPath.split(path.sep)[0] + path.sep; + builder.outputPath = outputPath; + + expect(builder.canDeleteOutputPath(outputPath)).to.equal(false); + }); + + it('when outputPath is project root ie., `--output-path=.`', function () { + let outputPathArg = '--output-path=.'; + let outputPath = command.parseArgs([outputPathArg]).options.outputPath; + builder.outputPath = outputPath; + + expect(builder.canDeleteOutputPath(outputPath)).to.equal(false); + }); + + it(`when outputPath is a parent directory ie., \`--output-path=${parentPath}\``, function () { + let outputPathArg = `--output-path=${parentPath}`; + let outputPath = command.parseArgs([outputPathArg]).options.outputPath; + builder.outputPath = outputPath; + + expect(builder.canDeleteOutputPath(outputPath)).to.equal(false); + }); + + it('allow outputPath to contain the root path as a substring, as long as it is not a parent', function () { + let outputPathArg = '--output-path=.'; + let outputPath = command.parseArgs([outputPathArg]).options.outputPath; + outputPath = outputPath.substr(0, outputPath.length - 1); + builder.outputPath = outputPath; + + expect(builder.canDeleteOutputPath(outputPath)).to.equal(true); + }); }); }); - it('clears the outputPath when multiple files are present', function() { - outputPath = 'tmp/builder-fixture/'; - var firstFile = outputPath + '/assets/foo-bar.js'; - var secondFile = outputPath + '/assets/baz-bif.js'; + describe('build', function () { + let instrumentationStart; + let instrumentationStop; + let cwd, project; - fs.mkdirsSync(outputPath + '/assets/'); - touch(firstFile); - touch(secondFile); + beforeEach(function () { + // Cache cwd to reset after test + cwd = process.cwd(); + project = new MockProject(); + builder = new Builder({ + project, + ui: project.ui, + setupBroccoliBuilder, + copyToOutputPath() { + return []; + }, + }); - builder = new Builder({ - setupBroccoliBuilder: function() { }, - trapSignals: function() { }, - cleanupOnExit: function() { }, + instrumentationStart = td.replace(builder.project._instrumentation, 'start'); + instrumentationStop = td.replace(builder.project._instrumentation, 'stopAndReport'); + }); - outputPath: outputPath, - project: new MockProject() + afterEach(function () { + process.chdir(cwd); + delete process._heimdall; + delete process.env.BROCCOLI_VIZ; + builder.project.ui.output = ''; + if (fs.existsSync(`${builder.project.root}/tmp`)) { + fs.removeSync(`${builder.project.root}/tmp`); + } }); - return builder.clearOutputPath() - .then(function() { - expect(existsSync(firstFile)).to.equal(false); - expect(existsSync(secondFile)).to.equal(false); - }); - }); + it('calls instrumentation.start', async function () { + let mockAnnotation = 'MockAnnotation'; + await builder.build(null, mockAnnotation); + td.verify(instrumentationStart('build'), { times: 1 }); + }); + + it('calls instrumentation.stop(build, result, resultAnnotation)', async function () { + let mockAnnotation = 'MockAnnotation'; - describe('Prevent deletion of files for improper outputPath', function() { - var command; - var parentPath = '..' + path.sep + '..' + path.sep; + await builder.build(null, mockAnnotation); - before(function() { - command = new BuildCommand(commandOptions({ - settings: {} - })); + td.verify( + instrumentationStop('build', { directory: 'build results', graph: { __heimdall__: {} } }, mockAnnotation), + { times: 1 } + ); + }); + it('writes temp files to Broccoli temp dir', async function () { + const project = new MockProject(); + project.root += '/tests/fixtures/build/simple'; + expect(fs.existsSync(`${builder.project.root}/tmp`)).to.be.false; builder = new Builder({ - setupBroccoliBuilder: function() { }, - trapSignals: function() { }, - cleanupOnExit: function() { }, - project: new MockProject() + project, + ui: project.ui, + copyToOutputPath() { + return []; + }, }); + + expect(fs.existsSync(`${builder.project.root}/tmp`)).to.be.false; + + let result = await builder.build(); + expect(fs.existsSync(result.directory)).to.be.true; + expect(fs.existsSync(`${builder.project.root}/tmp`)).to.be.false; + fs.removeSync(result.directory); }); - it('when outputPath is root directory ie., `--output-path=/` or `--output-path=C:`', function() { - var outputPathArg = '--output-path=.'; - var outputPath = command.parseArgs([outputPathArg]).options.outputPath; - outputPath = outputPath.split(path.sep)[0] + path.sep; - builder.outputPath = outputPath; + it('produces the correct output', async function () { + const project = new MockProject(); + project.root += '/tests/fixtures/build/simple'; + const setup = () => + new Builder({ + project, + ui: project.ui, + copyToOutputPath() { + return []; + }, + }); + + let result = await setup().build(); - expect(builder.canDeleteOutputPath(outputPath)).to.equal(false); + expect(fixturify.readSync(result.directory)).to.deep.equal(fixturify.readSync(`${project.root}/dist`)); }); - it('when outputPath is project root ie., `--output-path=.`', function() { - var outputPathArg = '--output-path=.'; - var outputPath = command.parseArgs([outputPathArg]).options.outputPath; - builder.outputPath = outputPath; + // packages using node's module support (via type=module) need to have + // ember-cli-build.cjs rather than ember-cli.js in order for require to + // work correctly + it('builds packages using ESM', async function () { + const project = new MockProject(); + project.root += '/tests/fixtures/build/node-esm'; + const setupBuilder = () => + new Builder({ + project, + ui: project.ui, + copyToOutputPath() { + return []; + }, + }); + + let result = await setupBuilder().build(); - expect(builder.canDeleteOutputPath(outputPath)).to.equal(false); + expect(fixturify.readSync(result.directory)).to.deep.equal(fixturify.readSync(`${project.root}/dist`)); }); - it('when outputPath is a parent directory ie., `--output-path=' + parentPath + '`', function() { - var outputPathArg = '--output-path=' + parentPath; - var outputPath = command.parseArgs([outputPathArg]).options.outputPath; - builder.outputPath = outputPath; + it('returns {directory, graph} as the result object', async function () { + const project = new MockProject(); + project.root += '/tests/fixtures/build/simple'; + + builder = new Builder({ + project, + ui: project.ui, + copyToOutputPath() { + return []; + }, + }); + + let result = await builder.build(); + + expect(Object.keys(result)).to.eql(['directory', 'graph']); + expect(result.graph.__heimdall__).to.not.be.undefined; + expect(fs.existsSync(result.directory)).to.be.true; + }); + }); - expect(builder.canDeleteOutputPath(outputPath)).to.equal(false); + describe('cleanup', function () { + beforeEach(function () { + let project = new MockProject(); + builder = new Builder({ + project, + ui: project.ui, + setupBroccoliBuilder, + copyToOutputPath() { + return []; + }, + }); }); - it('allow outputPath to contain the root path as a substring, as long as it is not a parent', function() { - var outputPathArg = '--output-path=.'; - var outputPath = command.parseArgs([outputPathArg]).options.outputPath; - outputPath = outputPath.substr(0, outputPath.length - 1); - builder.outputPath = outputPath; + it('is idempotent', async function () { + await builder.build(); + + let cleanupCount = 0; + builder.builder.cleanup = function () { + cleanupCount++; + }; + + let cleanupPromises = [builder.cleanup(), builder.cleanup(), builder.cleanup(), builder.cleanup()]; - expect(builder.canDeleteOutputPath(outputPath)).to.equal(true); + await Promise.all(cleanupPromises); + + expect(cleanupCount).to.equal(1); }); }); - describe('addons', function() { - var hooksCalled; + describe('addons', function () { + let hooksCalled; - beforeEach(function() { + beforeEach(function () { hooksCalled = []; addon = { name: 'TestAddon', - preBuild: function() { + preBuild() { hooksCalled.push('preBuild'); + expect(this).to.equal(addon); return Promise.resolve(); }, - postBuild: function() { + postBuild() { hooksCalled.push('postBuild'); return Promise.resolve(); }, - buildError: function() { + outputReady() { + hooksCalled.push('outputReady'); + }, + + buildError() { hooksCalled.push('buildError'); }, }; + let project = new MockProject(); + project.addons = [addon]; + builder = new Builder({ - setupBroccoliBuilder: function() { }, - trapSignals: function() { }, - cleanupOnExit: function() { }, - builder: { - build: function() { + async setupBroccoliBuilder() { + await setupBroccoliBuilder.call(this); + let originalBuild = this.builder.build; + this.builder.build = () => { hooksCalled.push('build'); - - return Promise.resolve(buildResults); - } + return originalBuild.call(this); + }; + }, + copyToOutputPath() { + return []; }, - processBuildResult: function(buildResults) { return Promise.resolve(buildResults); }, - project: { - addons: [addon] - } + project, + ui: project.ui, }); - buildResults = 'build results'; + buildResults = { + directory: 'build results', + graph: { + __heimdall__: {}, + }, + }; }); - it('allows addons to add promises preBuild', function() { - var preBuild = stub(addon, 'preBuild', Promise.resolve()); + afterEach(function () { + delete process.env.BROCCOLI_VIZ; + delete process.env.EMBER_CLI_INSTRUMENTATION; + }); - return builder.build().then(function() { - expect(preBuild.called).to.equal(1, 'expected preBuild to be called'); - }); + it('allows addons to add promises preBuild', function () { + let preBuild = td.replace(addon, 'preBuild', td.function()); + td.when(preBuild(), { ignoreExtraArgs: true, times: 1 }).thenReturn(Promise.resolve()); + + return builder.build(); }); - it('allows addons to add promises postBuild', function() { - var postBuild = stub(addon, 'postBuild'); + it('allows addons to add promises postBuild', async function () { + let postBuild = td.replace(addon, 'postBuild', td.function()); - return builder.build().then(function() { - expect(postBuild.called).to.equal(1, 'expected postBuild to be called'); - expect(postBuild.calledWith[0][0]).to.equal(buildResults, 'expected postBuild to be called with the results'); - }); + await builder.build(); + td.verify(postBuild(buildResults), { times: 1 }); + }); + + it('allows addons to add promises outputReady', async function () { + let outputReady = td.replace(addon, 'outputReady', td.function()); + + builder.outputPath = 'dist/'; + await builder.build(); + + let expected = Object.assign({}, buildResults, { outputChanges: [], directory: 'dist/' }); + td.verify(outputReady(expected), { times: 1 }); }); - it('hooks are called in the right order', function() { - return builder.build().then(function() { - expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'postBuild']); + describe('instrumentation hooks', function () { + beforeEach(function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + }); + + it('invokes the instrumentation hook if it is preset', async function () { + addon.instrumentation = function () { + hooksCalled.push('instrumentation'); + }; + + await builder.build(null, {}); + expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'postBuild', 'outputReady', 'instrumentation']); }); }); - it('should call postBuild before processBuildResult', function() { - var called = []; + it('hooks are called in the right order without visualization', async function () { + await builder.build(); + expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'postBuild', 'outputReady']); + }); + + it('should call postBuild before copying to dist', async function () { + let called = []; - addon.postBuild = function() { + addon.postBuild = function () { called.push('postBuild'); }; - builder.processBuildResult = function() { - called.push('processBuildResult'); + builder.copyToOutputPath = function () { + called.push('copyToOutputPath'); }; - return builder.build().then(function() { - expect(called).to.deep.equal(['postBuild', 'processBuildResult']); - }); + await builder.build(); + expect(called).to.deep.equal(['postBuild', 'copyToOutputPath']); + }); + + it('should call outputReady after copying to output path', async function () { + let called = []; + + builder.copyToOutputPath = function (directory) { + called.push(['copyToOutputPath', directory]); + return []; + }; + + addon.outputReady = function (result) { + called.push(['outputReady', result]); + }; + + builder.outputPath = 'dist/'; + + await builder.build(); + + expect(called).to.deep.equal([ + ['copyToOutputPath', buildResults.directory], + ['outputReady', Object.assign({}, buildResults, { outputChanges: [], directory: 'dist/' })], + ]); }); - it('buildError receives the error object from the errored step', function() { - var thrownBuildError = new Error('buildError'); - var receivedBuildError; + it('buildError receives the error object from the errored step', async function () { + let thrownBuildError = new Error('buildError'); + let receivedBuildError; - addon.buildError = function(errorThrown) { + addon.buildError = function (errorThrown) { receivedBuildError = errorThrown; }; - builder.builder.build = function() { + await builder.setupBroccoliBuilder(); + builder.builder.build = function () { hooksCalled.push('build'); return Promise.reject(thrownBuildError); }; - return builder.build().then(function() { - expect(false, 'should not succeed'); - }).catch(function() { - expect(receivedBuildError).to.equal(thrownBuildError); - }); + await expect(builder.build()).to.be.rejected; + expect(receivedBuildError).to.equal(thrownBuildError); }); - it('calls buildError and does not call build or postBuild when preBuild fails', function() { - addon.preBuild = function() { + it('calls buildError and does not call build, postBuild or outputReady when preBuild fails', async function () { + addon.preBuild = function () { hooksCalled.push('preBuild'); return Promise.reject(new Error('preBuild Error')); }; - return builder.build().then(function() { - expect(false, 'should not succeed'); - }).catch(function() { - expect(hooksCalled).to.deep.equal(['preBuild', 'buildError']); - }); + await expect(builder.build()).to.be.rejected; + expect(hooksCalled).to.deep.equal(['preBuild', 'buildError']); }); - it('calls buildError and does not call postBuild when build fails', function() { - builder.builder.build = function() { + it('calls buildError and does not call postBuild or outputReady when build fails', async function () { + await builder.setupBroccoliBuilder(); + builder.builder.build = function () { hooksCalled.push('build'); return Promise.reject(new Error('build Error')); }; - return builder.build().then(function() { - expect(false, 'should not succeed'); - }).catch(function() { - expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'buildError']); - }); + await expect(builder.build()).to.be.rejected; + expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'buildError']); }); - it('calls buildError when postBuild fails', function() { - addon.postBuild = function() { + it('calls buildError when postBuild fails', async function () { + addon.postBuild = function () { hooksCalled.push('postBuild'); return Promise.reject(new Error('preBuild Error')); }; - return builder.build().then(function() { - expect(false, 'should not succeed'); - }).catch(function() { - expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'postBuild', 'buildError']); - }); + await expect(builder.build()).to.be.rejected; + expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'postBuild', 'buildError']); + }); + + it('calls buildError when outputReady fails', async function () { + addon.outputReady = function () { + hooksCalled.push('outputReady'); + + return Promise.reject(new Error('outputReady Error')); + }; + + await expect(builder.build()).to.be.rejected; + expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'postBuild', 'outputReady', 'buildError']); + }); + + it('sets `isBuilderError` on handled addon errors', async function () { + addon.postBuild = function () { + return Promise.reject(new Error('preBuild Error')); + }; + + let error; + try { + await builder.build(); + } catch (e) { + error = e; + } + expect(error).to.haveOwnProperty('isBuilderError', true); }); }); }); diff --git a/tests/unit/models/command-test.js b/tests/unit/models/command-test.js index 91fbd88079..e070b7bf0c 100644 --- a/tests/unit/models/command-test.js +++ b/tests/unit/models/command-test.js @@ -1,137 +1,140 @@ 'use strict'; -/*jshint expr: true*/ -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); -var MockAnalytics = require('../../helpers/mock-analytics'); -var Command = require('../../../lib/models/command'); -var Yam = require('yam'); +const { expect } = require('chai'); +const commandOptions = require('../../factories/command-options'); +const processHelpString = require('../../helpers/process-help-string'); +const Yam = require('yam'); +const EOL = require('os').EOL; +const td = require('testdouble'); -var ServeCommand = Command.extend({ +let Task = require('../../../lib/models/task'); +let Command = require('../../../lib/models/command'); + +let ServeCommand = Command.extend({ name: 'serve', aliases: ['server', 's'], availableOptions: [ { name: 'port', type: Number, default: 4200 }, { name: 'host', type: String, default: '0.0.0.0' }, - { name: 'proxy', type: String }, - { name: 'live-reload', type: Boolean, default: true, aliases: ['lr']}, - { name: 'live-reload-port', type: Number, description: '(Defaults to port number + 31529)'}, - { name: 'environment', type: String, default: 'development' } + { name: 'proxy', type: String }, + { name: 'live-reload', type: Boolean, default: true, aliases: ['lr'] }, + { name: 'live-reload-port', type: Number, description: '(Defaults to port number + 31529)' }, + { name: 'environment', type: String, default: 'development' }, ], - run: function() {} + run(options) { + return options; + }, }); -var DevelopEmberCLICommand = Command.extend({ +let DevelopEmberCLICommand = Command.extend({ name: 'develop-ember-cli', works: 'everywhere', - availableOptions: [ - { name: 'package-name', key: 'packageName', type: String, required: true } - ], - run: function() {} + availableOptions: [{ name: 'package-name', key: 'packageName', type: String, required: true }], + run(options) { + return options; + }, }); -var InsideProjectCommand = Command.extend({ +let InsideProjectCommand = Command.extend({ name: 'inside-project', works: 'insideProject', - run: function() {} + run(options) { + return options; + }, }); -var OutsideProjectCommand = Command.extend({ +let OutsideProjectCommand = Command.extend({ name: 'outside-project', works: 'outsideProject', - run: function() {} + run(options) { + return options; + }, }); -var OptionsAliasCommand = Command.extend({ +let OptionsAliasCommand = Command.extend({ name: 'options-alias', - availableOptions: [{ - name: 'taco', - type: String, - default: 'traditional', - aliases:[ - {'hard-shell' : 'hard-shell'}, - {'soft-shell' : 'soft-shell'} - ] - }, - { - name: 'spicy', - type: Boolean, - default: true, - aliases:[ - {'mild' : false} - ] + availableOptions: [ + { + name: 'taco', + type: String, + default: 'traditional', + aliases: [{ 'hard-shell': 'hard-shell' }, { 'soft-shell': 'soft-shell' }], + }, + { + name: 'spicy', + type: Boolean, + default: true, + aliases: [{ mild: false }], + }, + { + name: 'display-message', + type: String, + aliases: ['dm', { hw: 'Hello world' }], + }, + ], + run(options) { + return options; }, - { - name: 'display-message', - type: String, - aliases:[ - 'dm', - { 'hw': 'Hello world' } - ] - }], - run: function() {} }); -describe('models/command.js', function() { - var ui; - var analytics; - var project; - var config; +describe('models/command.js', function () { + let ui; + let config; + let options; - before(function(){ - ui = new MockUI(); - analytics = new MockAnalytics(); - project = { isEmberCLIProject: function(){ return true; }}; + before(function () { config = new Yam('ember-cli', { - secondary: process.cwd() + '/tests/fixtures/home', - primary: process.cwd() + '/tests/fixtures/project' + secondary: `${process.cwd()}/tests/fixtures/home`, + primary: `${process.cwd()}/tests/fixtures/project`, }); }); - it('parseArgs() should parse the command options.', function() { - expect(new ServeCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['--port', '80'])).to.have.deep.property('options.port', 80); + beforeEach(function () { + options = commandOptions(); + ui = options.ui; + delete process.env.EMBROIDER_PREBUILD; + }); + + afterEach(function () { + td.reset(); }); - it('parseArgs() should get command options from the config file and command line', function() { - expect(new ServeCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: config.getAll() - }).parseArgs(['--port', '789'])).to.deep.equal({ + it('parseArgs() should parse the command options.', function () { + expect(new ServeCommand(options).parseArgs(['--port', '80'])).to.have.nested.property('options.port', 80); + }); + + it('parseArgs() should get command options from the config file and command line', function () { + expect( + new ServeCommand( + Object.assign(options, { + settings: config.getAll(), + }) + ).parseArgs(['--port', '789']) + ).to.deep.equal({ options: { port: 789, environment: 'mock-development', host: '0.1.0.1', proxy: 'http://iamstef.net/ember-cli', liveReload: false, - checkForUpdates: true + checkForUpdates: true, }, - args: [] + args: [], }); }); - it('parseArgs() should set default option values.', function() { - expect(new ServeCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs([])).to.have.deep.property('options.port', 4200); + it('parseArgs() should set default option values.', function () { + expect(new ServeCommand(options).parseArgs([])).to.have.nested.property('options.port', 4200); }); - it('parseArgs() should return args too.', function() { - expect(new ServeCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: config.getAll() - }).parseArgs(['foo', '--port', '80'])).to.deep.equal({ + it('parseArgs() should return args too.', function () { + expect( + new ServeCommand( + Object.assign(options, { + settings: config.getAll(), + }) + ).parseArgs(['foo', '--port', '80']) + ).to.deep.equal({ args: ['foo'], options: { environment: 'mock-development', @@ -139,516 +142,762 @@ describe('models/command.js', function() { proxy: 'http://iamstef.net/ember-cli', liveReload: false, port: 80, - checkForUpdates: true - } + checkForUpdates: true, + }, }); }); - it('parseArgs() should warn if an option is invalid.', function() { - ui.clear(); - new ServeCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: config.getAll() - }).parseArgs(['foo', '--envirmont', 'production']); - expect(ui.output).to.match(/The option '--envirmont' is not registered with the serve command. Run `ember serve --help` for a list of supported options./); + it('parseArgs() should warn if an option is invalid.', function () { + new ServeCommand( + Object.assign(options, { + settings: config.getAll(), + }) + ).parseArgs(['foo', '--envirmont', 'production']); + expect(ui.output).to.match( + /The option '--envirmont' is not registered with the 'serve' command. Run `ember serve --help` for a list of supported options./ + ); }); - it('parseArgs() should parse shorthand options.', function() { - expect(new ServeCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['-e', 'tacotown'])).to.have.deep.property('options.environment', 'tacotown'); + it('parseArgs() should parse shorthand options.', function () { + expect(new ServeCommand(options).parseArgs(['-e', 'tacotown'])).to.have.nested.property( + 'options.environment', + 'tacotown' + ); }); - it('parseArgs() should parse shorthand dasherized options.', function() { - expect(new ServeCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['-lr', 'false'])).to.have.deep.property('options.liveReload', false); + it('parseArgs() should parse shorthand dasherized options.', function () { + expect(new ServeCommand(options).parseArgs(['-lr', 'false'])).to.have.nested.property('options.liveReload', false); }); - it('validateAndRun() should print a message if a required option is missing.', function() { - ui.clear(); - return new DevelopEmberCLICommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).validateAndRun([]).then(function() { - expect(ui.output).to.match(/requires the option.*package-name/); + it('parseArgs() should parse string options.', function () { + let CustomAliasCommand = Command.extend({ + name: 'custom-alias', + availableOptions: [ + { + name: 'options', + type: String, + }, + ], + run(options) { + return options; + }, }); + const command = new CustomAliasCommand(options).parseArgs(['1', '--options', '--split 2 --random']); + expect(command).to.have.nested.property('options.options', '--split 2 --random'); }); - it('validateAndRun() should print a message if outside a project and command is not valid there.', function() { - return new InsideProjectCommand({ - ui: ui, - analytics: analytics, - project: { isEmberCLIProject: function(){ return false; }}, - settings: {} - }).validateAndRun([]).catch(function(reason) { - expect(reason.message).to.match(/You have to be inside an ember-cli project/); + describe('#validateAndRun', function () { + it('should reject and print a message if a required option is missing.', function () { + return new DevelopEmberCLICommand(options).validateAndRun([]).catch(function () { + expect(ui.output).to.match(/requires the option.*package-name/); + }); + }); + + it('should print a message if outside a project and command is not valid there.', function () { + return new InsideProjectCommand( + Object.assign(options, { + project: { + hasDependencies() { + return true; + }, + isEmberCLIProject() { + return false; + }, + }, + }) + ) + .validateAndRun([]) + .catch(function (reason) { + expect(reason.message).to.match(/You have to be inside an ember-cli project/); + }); + }); + + it('selects watcher if an option', function () { + return new InsideProjectCommand( + Object.assign(options, { + availableOptions: [{ type: 'string', name: 'watcher' }], + project: { + hasDependencies() { + return true; + }, + isEmberCLIProject() { + return true; + }, + }, + }) + ) + .validateAndRun([]) + .then(function (options) { + expect(options).to.have.property('watcher'); + }); + }); + it('selects watcher (when EMBROIDER_PREBUILD is present)', function () { + process.env.EMBROIDER_PREBUILD = 'true'; + + return new InsideProjectCommand( + Object.assign(options, { + availableOptions: [], + project: { + hasDependencies() { + return true; + }, + isEmberCLIProject() { + return true; + }, + }, + }) + ) + .validateAndRun([]) + .then(function (options) { + expect(options).to.have.property('watcher'); + }); + }); + + it('selects NO watcher if NOT an option', function () { + return new InsideProjectCommand( + Object.assign(options, { + availableOptions: [{ type: 'string', name: 'foo' }], + project: { + hasDependencies() { + return true; + }, + isEmberCLIProject() { + return true; + }, + }, + }) + ) + .validateAndRun([]) + .then(function (options) { + expect(options).to.not.have.property('watcher'); + }); + }); + + it('should print a message if inside a project and command is not valid there.', function () { + return new OutsideProjectCommand(options).validateAndRun([]).catch(function (reason) { + expect(reason.message).to.match(/You cannot use.*inside an ember-cli project/); + }); }); }); - it('validateAndRun() should print a message if inside a project and command is not valid there.', function() { - return new OutsideProjectCommand({ - ui: ui, - analytics: analytics, - project: { isEmberCLIProject: function(){ return true; }}, - settings: {} - }).validateAndRun([]).catch(function(reason) { - expect(reason.message).to.match(/You cannot use.*inside an ember-cli project/); + it('should be able to set availableOptions within init', function () { + let AvailableOptionsInitCommand = Command.extend({ + name: 'available-options-init-command', + init() { + this._super.apply(this, arguments); + + this.availableOptions = [ + { + name: 'spicy', + type: String, + default: true, + }, + ]; + }, + run(options) { + return options; + }, }); + + return new AvailableOptionsInitCommand( + Object.assign(options, { + project: { + hasDependencies() { + return true; + }, + isEmberCLIProject() { + return false; + }, + }, + }) + ) + .validateAndRun([]) + .then(function (commandOptions) { + expect(commandOptions).to.deep.equal({ spicy: true }); + }); }); - it('availableOptions with aliases should work.', function() { - expect(new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['-soft-shell'])).to.deep.equal({ + it('should be able to set availableOptions within beforeRun', function () { + let AvailableOptionsInitCommand = Command.extend({ + name: 'available-options-init-command', + + availableOptions: [ + { + name: 'spicy', + type: Boolean, + default: true, + }, + ], + + beforeRun() { + return new Promise((resolve) => { + resolve( + this.availableOptions.push({ + name: 'foobar', + type: String, + default: 'bazbaz', + }) + ); + }); + }, + run(options) { + return options; + }, + }); + + const command = new AvailableOptionsInitCommand( + Object.assign(options, { + project: { + hasDependencies() { + return true; + }, + isEmberCLIProject() { + return false; + }, + }, + }) + ); + + return command.beforeRun().then(() => + command.validateAndRun([]).then((commandOptions) => { + expect(commandOptions).to.deep.equal({ spicy: true, foobar: 'bazbaz' }); + }) + ); + }); + + it('availableOptions with aliases should work.', function () { + expect(new OptionsAliasCommand(options).parseArgs(['-soft-shell'])).to.deep.equal({ options: { taco: 'soft-shell', - spicy: true + spicy: true, }, - args: [] + args: [], }); }); - it('availableOptions with aliases should work with minimum characters.', function() { - expect(new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['-so'])).to.deep.equal({ + it('availableOptions with aliases should work with minimum characters.', function () { + expect(new OptionsAliasCommand(options).parseArgs(['-so'])).to.deep.equal({ options: { taco: 'soft-shell', - spicy: true + spicy: true, }, - args: [] + args: [], }); }); - it('availableOptions with aliases should work with hyphenated options', function() { - expect(new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['-dm', 'hi'])).to.deep.equal({ + it('availableOptions with aliases should work with hyphenated options', function () { + expect(new OptionsAliasCommand(options).parseArgs(['-dm', 'hi'])).to.deep.equal({ options: { taco: 'traditional', spicy: true, - displayMessage: 'hi' + displayMessage: 'hi', }, - args: [] + args: [], }); - expect(new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['-hw'])).to.deep.equal({ + expect(new OptionsAliasCommand(options).parseArgs(['-hw'])).to.deep.equal({ options: { taco: 'traditional', spicy: true, - displayMessage: 'Hello world' + displayMessage: 'Hello world', }, - args: [] + args: [], }); }); - it('registerOptions() should allow adding availableOptions.', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var extendedAvailableOptions = [{ - name: 'filling', - type: String, - default: 'adobada', - aliases:[ - {'carne-asada' : 'carne-asada'}, - {'carnitas' : 'carnitas' }, - {'fish' : 'fish'} - ] - }]; - - optionsAlias.registerOptions( { availableOptions : extendedAvailableOptions } ); + it('registerOptions() should allow adding availableOptions.', function () { + let optionsAlias = new OptionsAliasCommand(options); + let extendedAvailableOptions = [ + { + name: 'filling', + type: String, + default: 'adobada', + aliases: [{ 'carne-asada': 'carne-asada' }, { carnitas: 'carnitas' }, { fish: 'fish' }], + }, + ]; + + optionsAlias.registerOptions({ availableOptions: extendedAvailableOptions }); // defaults expect(optionsAlias.parseArgs([])).to.deep.equal({ options: { taco: 'traditional', spicy: true, - filling: 'adobada' + filling: 'adobada', }, - args: [] + args: [], }); // shorthand expect(optionsAlias.parseArgs(['-carne'])).to.deep.equal({ options: { taco: 'traditional', spicy: true, - filling: 'carne-asada' + filling: 'carne-asada', }, - args: [] + args: [], }); // last argument wins - expect(optionsAlias.parseArgs(['-carne','-fish'])).to.deep.equal({ + expect(optionsAlias.parseArgs(['-carne', '-fish'])).to.deep.equal({ options: { taco: 'traditional', spicy: true, - filling: 'fish' + filling: 'fish', }, - args: [] + args: [], }); - }); - it('registerOptions() should allow overriding availableOptions.', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var extendedAvailableOptions = [{ - name: 'filling', - type: String, - default: 'adobada', - aliases:[ - {'carne-asada' : 'carne-asada'}, - {'carnitas' : 'carnitas' }, - {'fish' : 'fish'} - ] - }]; - var duplicateExtendedAvailableOptions = [{ - name: 'filling', - type: String, - default: 'carnitas', - aliases:[ - {'pollo-asado' : 'pollo-asado'}, - {'carne-asada' : 'carne-asada'} - ] - }]; - - optionsAlias.registerOptions( { availableOptions : extendedAvailableOptions } ); + it('registerOptions() should allow overriding availableOptions.', function () { + let optionsAlias = new OptionsAliasCommand(options); + let extendedAvailableOptions = [ + { + name: 'filling', + type: String, + default: 'adobada', + aliases: [{ 'carne-asada': 'carne-asada' }, { carnitas: 'carnitas' }, { fish: 'fish' }], + }, + ]; + let duplicateExtendedAvailableOptions = [ + { + name: 'filling', + type: String, + default: 'carnitas', + aliases: [{ 'pollo-asado': 'pollo-asado' }, { 'carne-asada': 'carne-asada' }], + }, + ]; + + optionsAlias.registerOptions({ availableOptions: extendedAvailableOptions }); // default expect(optionsAlias.parseArgs([])).to.deep.equal({ - options: { - taco: 'traditional', - spicy: true, - filling: 'adobada' - }, - args: [] - }); + options: { + taco: 'traditional', + spicy: true, + filling: 'adobada', + }, + args: [], + }); // shorthand expect(optionsAlias.parseArgs(['-carne'])).to.deep.equal({ - options: { - taco: 'traditional', - spicy: true, - filling: 'carne-asada' - }, - args: [] - }); - - optionsAlias.registerOptions( { availableOptions : duplicateExtendedAvailableOptions } ); + options: { + taco: 'traditional', + spicy: true, + filling: 'carne-asada', + }, + args: [], + }); + optionsAlias.registerOptions({ availableOptions: duplicateExtendedAvailableOptions }); // override default expect(optionsAlias.parseArgs([])).to.deep.equal({ options: { taco: 'traditional', spicy: true, - filling: 'carnitas' + filling: 'carnitas', }, - args: [] + args: [], }); // last argument wins expect(optionsAlias.parseArgs(['-fish', '-pollo'])).to.deep.equal({ - options: { - taco: 'traditional', - spicy: true, - filling: 'pollo-asado' - }, - args: [] - }); - + options: { + taco: 'traditional', + spicy: true, + filling: 'pollo-asado', + }, + args: [], + }); }); - it('registerOptions() should not allow aliases with the same name.', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var extendedAvailableOptions = [{ + it('registerOptions() should not allow aliases with the same name.', function () { + let optionsAlias = new OptionsAliasCommand(options); + let extendedAvailableOptions = [ + { name: 'filling', type: String, default: 'adobada', - aliases:[ - {'carne-asada' : 'carne-asada'}, - {'carnitas' : 'carnitas' }, - {'fish' : 'fish'} - ] + aliases: [{ 'carne-asada': 'carne-asada' }, { carnitas: 'carnitas' }, { fish: 'fish' }], }, { name: 'favorite', type: String, default: 'adobada', - aliases:[ - {'carne-asada' : 'carne-asada'}, - {'carnitas' : 'carnitas' }, - {'fish' : 'fish'} - ] - } + aliases: [{ 'carne-asada': 'carne-asada' }, { carnitas: 'carnitas' }, { fish: 'fish' }], + }, ]; - var register = optionsAlias.registerOptions.bind(optionsAlias); + let register = optionsAlias.registerOptions.bind(optionsAlias); optionsAlias.availableOptions = extendedAvailableOptions; - expect(register).to.throw('The "carne-asada" alias is already in use by the "--filling" option and ' + - 'cannot be used by the "--favorite" option. Please use a different alias.'); - + expect(register).to.throw( + 'The "carne-asada" alias is already in use by the "--filling" option and ' + + 'cannot be used by the "--favorite" option. Please use a different alias.' + ); }); - it('registerOptions() should warn on options override attempts.', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var extendedAvailableOptions = [ + it('registerOptions() should warn on options override attempts.', function () { + let optionsAlias = new OptionsAliasCommand(options); + let extendedAvailableOptions = [ { name: 'spicy', type: Boolean, default: true, - aliases:[ - {'mild' : true} - ] - } + aliases: [{ mild: true }], + }, ]; - optionsAlias.registerOptions( { availableOptions : extendedAvailableOptions }); + optionsAlias.registerOptions({ availableOptions: extendedAvailableOptions }); expect(ui.output).to.match(/The ".*" alias cannot be overridden. Please use a different alias./); - }); - it('registerOptions() should handle invalid alias definitions.', function() { + it('registerOptions() should handle invalid alias definitions.', function () { //check for different types, validate proper errors are thrown - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var badArrayAvailableOptions = [{ name: 'filling', type: String, default: 'adobada', aliases:[ - 'meat',[{'carne-asada' : 'carne-asada'}], {'carnitas' : 'carnitas' }, {'fish' : 'fish'} - ] - }]; - var badObjectAvailableOptions = [{ name: 'filling', type: String, default: 'adobada', aliases:[ - 'meat',{'carne-asada':['steak','grilled']}, {'carnitas' : 'carnitas' }, {'fish' : 'fish'} - ] - }]; - var register = optionsAlias.registerOptions.bind(optionsAlias); + let optionsAlias = new OptionsAliasCommand(options); + let badArrayAvailableOptions = [ + { + name: 'filling', + type: String, + default: 'adobada', + aliases: ['meat', [{ 'carne-asada': 'carne-asada' }], { carnitas: 'carnitas' }, { fish: 'fish' }], + }, + ]; + let badObjectAvailableOptions = [ + { + name: 'filling', + type: String, + default: 'adobada', + aliases: ['meat', { 'carne-asada': ['steak', 'grilled'] }, { carnitas: 'carnitas' }, { fish: 'fish' }], + }, + ]; + let register = optionsAlias.registerOptions.bind(optionsAlias); optionsAlias.availableOptions = badArrayAvailableOptions; - expect(register).to.throw('The "[object Object]" [type:array] alias is not an acceptable value. ' + - 'It must be a string or single key object with a string value (for example, "value" or { "key" : "value" }).'); + expect(register).to.throw( + 'The "[object Object]" [type:array] alias is not an acceptable value. ' + + 'It must be a string or single key object with a string value (for example, "value" or { "key" : "value" }).' + ); optionsAlias.availableOptions = badObjectAvailableOptions; - expect(register).to.throw('The "[object Object]" [type:object] alias is not an acceptable value. ' + - 'It must be a string or single key object with a string value (for example, "value" or { "key" : "value" }).'); - + expect(register).to.throw( + 'The "[object Object]" [type:object] alias is not an acceptable value. ' + + 'It must be a string or single key object with a string value (for example, "value" or { "key" : "value" }).' + ); }); - it('parseAlias() should parse aliases and return an object', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var option = { + it('parseAlias() should parse aliases and return an object', function () { + let optionsAlias = new OptionsAliasCommand(options); + let option = { name: 'filling', type: String, key: 'filling', default: 'adobada', - aliases:[ - {'carne-asada' : 'carne-asada'}, - {'carnitas' : 'carnitas' }, - {'fish' : 'fish'} - ] + aliases: [{ 'carne-asada': 'carne-asada' }, { carnitas: 'carnitas' }, { fish: 'fish' }], }; - var alias = {'carnitas' : 'carnitas' }; + let alias = { carnitas: 'carnitas' }; expect(optionsAlias.parseAlias(option, alias)).to.deep.equal({ key: 'carnitas', - value: ['--filling','carnitas'], - original: {'carnitas' : 'carnitas'} + value: ['--filling', 'carnitas'], + original: { carnitas: 'carnitas' }, }); }); - it('validateOption() should validate options', function() { - var option = { + it('validateOption() should validate options', function () { + let option = { name: 'filling', type: String, default: 'adobada', - aliases:[ - {'carne-asada' : 'carne-asada'}, - {'carnitas' : 'carnitas' }, - {'fish' : 'fish'} - ] + aliases: [{ 'carne-asada': 'carne-asada' }, { carnitas: 'carnitas' }, { fish: 'fish' }], }; - var dupe = { name: 'spicy', type: Boolean, default: true, aliases:[{'mild' : false}] }; - var noAlias = { name: 'reload', type: Boolean, default: false}; - expect(new OptionsAliasCommand({ui: ui, - analytics: analytics, - project: project, - settings: {} - }).validateOption(option)).to.be.ok; - - expect(new ServeCommand({ui: ui, - analytics: analytics, - project: project, - settings: {} - }).validateOption(noAlias)).to.be.false; - - expect(new OptionsAliasCommand({ui: ui, - analytics: analytics, - project: project, - settings: {} - }).validateOption(dupe)).to.be.false; + let dupe = { name: 'spicy', type: Boolean, default: true, aliases: [{ mild: false }] }; + let noAlias = { name: 'reload', type: Boolean, default: false }; + const aliasCommand = new OptionsAliasCommand(options); + aliasCommand.registerOptions(); + expect(aliasCommand.validateOption(option)).to.be.ok; + + const serveCommand = new ServeCommand(options); + serveCommand.registerOptions(); + expect(serveCommand.validateOption(noAlias)).to.be.false; + + const optionsAliasCommand = new OptionsAliasCommand(options); + optionsAliasCommand.registerOptions(); + expect(optionsAliasCommand.validateOption(dupe)).to.be.false; }); - it('validateOption() should throw an error when option is missing name or type', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var notype = { name: 'taco' }; - var noname = { type: Boolean }; - - expect(optionsAlias.validateOption.bind(optionsAlias, notype)).to.throw('The command "options-alias" has an ' + - 'option without the required type and name fields.'); - expect(optionsAlias.validateOption.bind(optionsAlias, noname)).to.throw('The command "options-alias" has an ' + - 'option without the required type and name fields.'); + it('validateOption() should throw an error when option is missing name or type', function () { + let optionsAlias = new OptionsAliasCommand(options); + let notype = { name: 'taco' }; + let noname = { type: Boolean }; + + expect(optionsAlias.validateOption.bind(optionsAlias, notype)).to.throw( + 'The command "options-alias" has an ' + 'option without the required type and name fields.' + ); + expect(optionsAlias.validateOption.bind(optionsAlias, noname)).to.throw( + 'The command "options-alias" has an ' + 'option without the required type and name fields.' + ); }); - it('validateOption() should throw an error when option name is camelCase or capitalized', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var capital = { + it('validateOption() should throw an error when option name is camelCase or capitalized', function () { + let optionsAlias = new OptionsAliasCommand(options); + let capital = { name: 'Taco', - type: Boolean + type: Boolean, }; - var camel = { + let camel = { name: 'tacoTown', - type: Boolean + type: Boolean, }; - expect(optionsAlias.validateOption.bind(optionsAlias, capital)).to.throw('The "Taco" option\'s name of the "options-alias"' + - ' command contains a capital letter.'); - expect(optionsAlias.validateOption.bind(optionsAlias, camel)).to.throw('The "tacoTown" option\'s name of the "options-alias"' + - ' command contains a capital letter.'); + expect(optionsAlias.validateOption.bind(optionsAlias, capital)).to.throw( + 'The "Taco" option\'s name of the "options-alias"' + ' command contains a capital letter.' + ); + expect(optionsAlias.validateOption.bind(optionsAlias, camel)).to.throw( + 'The "tacoTown" option\'s name of the "options-alias"' + ' command contains a capital letter.' + ); }); - it('mergeDuplicateOption() should merge duplicate options together', function() { - var optionsAlias = new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }); - var garbageAvailableOptions = [ - { name: 'spicy', type: Boolean, default: true, aliases:[{'mild' : true}] } - ]; - optionsAlias.registerOptions( { availableOptions : garbageAvailableOptions }); - var extendedAvailableOptions = [{ name: 'filling', type: String, default: 'adobada', aliases:[ - {'carne-asada' : 'carne-asada'}, {'carnitas' : 'carnitas' }, {'fish' : 'fish'} - ] - }]; - var duplicateExtendedAvailableOptions = [{ name: 'filling', type: String, default: 'carnitas', aliases:[ - {'pollo-asado' : 'pollo-asado'}, {'carne-asada' : 'carne-asada'} - ] - }]; - optionsAlias.registerOptions( { availableOptions : extendedAvailableOptions }); + it('mergeDuplicateOption() should merge duplicate options together', function () { + let optionsAlias = new OptionsAliasCommand(options); + let garbageAvailableOptions = [{ name: 'spicy', type: Boolean, default: true, aliases: [{ mild: true }] }]; + optionsAlias.registerOptions({ availableOptions: garbageAvailableOptions }); + let extendedAvailableOptions = [ + { + name: 'filling', + type: String, + default: 'adobada', + aliases: [{ 'carne-asada': 'carne-asada' }, { carnitas: 'carnitas' }, { fish: 'fish' }], + }, + ]; + let duplicateExtendedAvailableOptions = [ + { + name: 'filling', + type: String, + default: 'carnitas', + aliases: [{ 'pollo-asado': 'pollo-asado' }, { 'carne-asada': 'carne-asada' }], + }, + ]; + optionsAlias.registerOptions({ availableOptions: extendedAvailableOptions }); optionsAlias.availableOptions.push(duplicateExtendedAvailableOptions[0]); - expect(optionsAlias.mergeDuplicateOption( 'filling' )).to.deep.equal([ + expect(optionsAlias.mergeDuplicateOption('filling')).to.deep.equal([ { name: 'taco', type: String, default: 'traditional', - aliases: [ - {'hard-shell' : 'hard-shell'}, - {'soft-shell' : 'soft-shell'} - ], + aliases: [{ 'hard-shell': 'hard-shell' }, { 'soft-shell': 'soft-shell' }], key: 'taco', - required: false + required: false, }, { name: 'display-message', type: String, - aliases:[ - 'dm', - { 'hw': 'Hello world' } - ], + aliases: ['dm', { hw: 'Hello world' }], key: 'displayMessage', - required: false + required: false, }, { name: 'spicy', type: Boolean, default: true, - aliases: [ - {'mild' : false} - ], + aliases: [{ mild: false }], key: 'spicy', - required: false + required: false, }, { name: 'filling', type: String, default: 'carnitas', aliases: [ - {'carne-asada' : 'carne-asada'}, - {'carnitas' : 'carnitas' }, - {'fish' : 'fish'}, - {'pollo-asado' : 'pollo-asado'} + { 'carne-asada': 'carne-asada' }, + { carnitas: 'carnitas' }, + { fish: 'fish' }, + { 'pollo-asado': 'pollo-asado' }, ], key: 'filling', - required: false - } + required: false, + }, ]); }); - it('implicit shorthands work with values.', function() { - expect(new OptionsAliasCommand({ - ui: ui, - analytics: analytics, - project: project, - settings: {} - }).parseArgs(['-s', 'false', '-t', 'hard-shell'])).to.deep.equal({ + it('implicit shorthands work with values.', function () { + expect(new OptionsAliasCommand(options).parseArgs(['-s', 'false', '-t', 'hard-shell'])).to.deep.equal({ options: { taco: 'hard-shell', - spicy: false + spicy: false, }, - args: [] + args: [], + }); + }); + + describe('runTask', function () { + let command; + + class AsyncTask extends Task { + run(options) { + return new Promise(function (resolve) { + setTimeout(() => resolve(options), 50); + }); + } + } + + class SyncTask extends Task { + run(options) { + return options; + } + } + + class FailingTask extends Task { + run(/* options */) { + throw new Error('I was born to fail'); + } + } + + beforeEach(function () { + // this should be changed to new Command(), but needs more mocking + command = new ServeCommand( + Object.assign({}, options, { + tasks: { + Async: AsyncTask, + Sync: SyncTask, + Failing: FailingTask, + }, + }) + ); + }); + + it('always handles task as a promise', function () { + return command.runTask('Sync', { param: 'value' }).then((result) => { + expect(result).to.eql({ + param: 'value', + }); + }); + }); + + it('command environment should be shared with a task', function () { + let taskRun = command.runTask('Async', { param: 'value' }); + + expect(command._currentTask.ui).to.eql(command.ui); + expect(command._currentTask.project).to.eql(command.project); + + return taskRun; + }); + + it('_currentTask should store a reference to the current task', function () { + expect(command._currentTask).to.be.undefined; + let taskRun = command.runTask('Sync', { param: 'value' }).then(() => { + expect(command._currentTask).to.be.undefined; + }); + expect(command._currentTask).to.be.an.instanceof(SyncTask); + + return taskRun; + }); + + it('_currentTask should cleanup current task on fail', function () { + return expect(command.runTask('Failing', { param: 'value' })).to.be.rejected.then(() => { + expect(command._currentTask).to.be.undefined; + }); + }); + + it('throws on attempt to launch concurrent tasks', function () { + let asyncTaskRun, syncTaskRun; + + expect(() => { + asyncTaskRun = command.runTask('Async'); + syncTaskRun = command.runTask('Sync'); + }).to.throw(`Concurrent tasks are not supported`); + + return Promise.all([asyncTaskRun, syncTaskRun]); + }); + + it('throws if the task is not found', function () { + try { + let taskRun = command.runTask('notfound'); + + expect(false, 'task should not be launched').to.equal(true); + + return taskRun; + } catch (e) { + expect(e.message).to.equal(`Unknown task "notfound"`); + } + }); + }); + + describe('help', function () { + let command; + + beforeEach(function () { + // this should be changed to new Command(), but needs more mocking + command = new ServeCommand(options); + }); + + describe('printBasicHelp', function () { + beforeEach(function () { + td.replace(command, '_printCommand', td.function()); + td.when(command._printCommand(), { ignoreExtraArgs: true }).thenReturn(' command printed'); + }); + + afterEach(function () { + td.reset(); + }); + + it('calls printCommand', function () { + let output = command.printBasicHelp(); + + let testString = processHelpString(`ember serve command printed${EOL}`); + + expect(output).to.equal(testString); + }); + + it('is root', function () { + command.isRoot = true; + + let output = command.printBasicHelp(); + + let testString = processHelpString(`Usage: serve command printed${EOL}`); + + expect(output).to.equal(testString); + }); + }); + + describe('printDetailedHelp', function () { + it('has no-op function', function () { + let output = command.printDetailedHelp(); + + expect(output).to.be.undefined; + }); + }); + + describe('hasOption', function () { + it('reports false if no option with that name is present', function () { + expect(command.hasOption('no-option-by-this-name')).to.be.false; + }); + + it('reports true if option with that name is present', function () { + expect(command.hasOption('port')).to.be.true; + }); + }); + + describe('getJson', function () { + beforeEach(function () { + command._printableProperties = ['test1', 'test2']; + }); + + it('iterates options', function () { + Object.assign(command, { + test1: 'a test', + test2: 'another test', + }); + + let json = command.getJson(); + + expect(json).to.deep.equal({ + test1: 'a test', + test2: 'another test', + }); + }); + + it('calls detailed json', function () { + td.replace(command, 'addAdditionalJsonForHelp', td.function()); + + let options = {}; + + let json = command.getJson(options); + + td.verify(command.addAdditionalJsonForHelp(json, options)); + }); }); }); }); diff --git a/tests/unit/models/file-info-test.js b/tests/unit/models/file-info-test.js index 5b837f9164..acccbd3631 100644 --- a/tests/unit/models/file-info-test.js +++ b/tests/unit/models/file-info-test.js @@ -1,154 +1,200 @@ 'use strict'; -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); -var FileInfo = require('../../../lib/models/file-info'); -var path = require('path'); -var fs = require('fs-extra'); -var EOL = require('os').EOL; -var Promise = require('../../../lib/ext/promise'); -var writeFile = Promise.denodeify(fs.writeFile); -var root = process.cwd(); -var tmproot = path.join(root, 'tmp'); -var tmp = require('tmp-sync'); -var assign = require('lodash/object/assign'); -var tmpdir; -var testOutputPath; - -describe('Unit - FileInfo', function(){ - - var validOptions, ui; - - beforeEach(function(){ - tmpdir = tmp.in(tmproot); +const { expect } = require('chai'); +const MockUI = require('console-ui/mock'); +const FileInfo = require('@ember-tooling/blueprint-model/utilities/file-info'); +const path = require('path'); +const fs = require('fs-extra'); +const EOL = require('os').EOL; +const tmp = require('tmp-promise'); +const td = require('testdouble'); + +describe('Unit - FileInfo', function () { + let validOptions, ui, testOutputPath; + + beforeEach(async function () { + const { path: tmpdir } = await tmp.dir(); + testOutputPath = path.join(tmpdir, 'outputfile'); ui = new MockUI(); + td.replace(ui, 'prompt'); + validOptions = { action: 'write', outputPath: testOutputPath, displayPath: '/pretty-output-path', - inputPath: path.resolve(__dirname, - '../../fixtures/blueprints/with-templating/files/foo.txt'), + inputPath: path.resolve(__dirname, '../../fixtures/blueprints/with-templating/files/foo.txt'), templateVariables: {}, - ui: ui + ui, }; }); - afterEach(function(done){ - fs.remove(tmproot, done); + afterEach(function () { + td.reset(); }); - it('can instantiate with options', function(){ + it('can instantiate with options', function () { new FileInfo(validOptions); }); + // eslint-disable-next-line no-template-curly-in-string it('does not interpolate {{ }} or ${ }', function () { - var options = {}; - assign(options, validOptions, {inputPath: path.resolve(__dirname, - '../../fixtures/file-info/interpolate.txt'), templateVariables: { name: 'tacocat' }}); - var fileInfo = new FileInfo(options); - return fileInfo.render().then(function(output) { + let options = {}; + Object.assign(options, validOptions, { + inputPath: path.resolve(__dirname, '../../fixtures/file-info/interpolate.txt'), + templateVariables: { name: 'tacocat' }, + }); + let fileInfo = new FileInfo(options); + return fileInfo.render().then(function (output) { + // eslint-disable-next-line no-template-curly-in-string expect(output.trim()).to.equal('{{ name }} ${ name } tacocat tacocat'); }); }); - it('renders an input file', function(){ + it('renders an input file', function () { validOptions.templateVariables.friend = 'Billy'; - var fileInfo = new FileInfo(validOptions); + let fileInfo = new FileInfo(validOptions); - return fileInfo.render().then(function(output){ - expect(output.trim()).to.equal('Howdy Billy', - 'expects the template to have been run'); + return fileInfo.render().then(function (output) { + expect(output.trim()).to.equal('Howdy Billy', 'expects the template to have been run'); }); }); - it('rejects if templating throws', function(){ - var templateWithUndefinedVariable = path.resolve(__dirname, - '../../fixtures/blueprints/with-templating/files/with-undefined-variable.txt'); - var options = {}; - assign(options, validOptions, { inputPath: templateWithUndefinedVariable }); - var fileInfo = new FileInfo(options); - - return fileInfo.render().then(function() { - throw new Error('FileInfo.render should reject if templating throws'); - }).catch(function(e) { - if (!e.toString().match(/ReferenceError/)) { - throw e; - } + it('allows mutation to the rendered file', function () { + validOptions.templateVariables.friend = 'Billy'; + let fileInfo; + + validOptions.replacer = function (content, theFileInfo) { + expect(theFileInfo).to.eql(fileInfo); + expect(content).to.eql('Howdy Billy\n'); + + return content.toUpperCase(); + }; + + fileInfo = new FileInfo(validOptions); + + return fileInfo.render().then(function (output) { + expect(output.trim()).to.equal('HOWDY BILLY', 'expects the template to have been run'); }); }); - it('does not explode when trying to template binary files', function() { - var binary = path.resolve(__dirname, '../../fixtures/problem-binary.png'); + it('rejects if templating throws', function () { + let templateWithUndefinedVariable = path.resolve( + __dirname, + '../../fixtures/blueprints/with-templating/files/with-undefined-variable.txt' + ); + let options = {}; + Object.assign(options, validOptions, { inputPath: templateWithUndefinedVariable }); + let fileInfo = new FileInfo(options); + + return fileInfo + .render() + .then(function () { + throw new Error('FileInfo.render should reject if templating throws'); + }) + .catch(function (e) { + if (!e.toString().match(/ReferenceError/)) { + throw e; + } + }); + }); + + it('does not explode when trying to template binary files', function () { + let binary = path.resolve(__dirname, '../../fixtures/problem-binary.png'); validOptions.inputPath = binary; - var fileInfo = new FileInfo(validOptions); + let fileInfo = new FileInfo(validOptions); - return fileInfo.render().then(function(output){ + return fileInfo.render().then(function (output) { expect(!!output, 'expects the file to be processed without error').to.equal(true); }); }); - it('renders a diff to the UI', function(){ + it('renders a diff to the UI', function () { validOptions.templateVariables.friend = 'Billy'; - var fileInfo = new FileInfo(validOptions); - - return writeFile(testOutputPath, 'Something Old' + EOL).then(function(){ - return fileInfo.displayDiff(); - }).then(function(){ - var output = ui.output.trim().split(EOL); - expect(output.shift()).to.equal('Index: ' + testOutputPath); - expect(output.shift()).to.match(/=+/); - expect(output.shift()).to.match(/---/); - expect(output.shift()).to.match(/\+{3}/); - expect(output.shift()).to.match(/.*/); - expect(output.shift()).to.match(/-Something Old/); - expect(output.shift()).to.match(/\+Howdy Billy/); - }); + let fileInfo = new FileInfo(validOptions); + + return fs + .writeFile(testOutputPath, `Something Old${EOL}`) + .then(function () { + return fileInfo.displayDiff(); + }) + .then(function () { + let output = ui.output.trim().split(EOL); + expect(output.shift()).to.equal(`Index: ${testOutputPath}`); + expect(output.shift()).to.match(/=+/); + expect(output.shift()).to.match(/---/); + expect(output.shift()).to.match(/\+{3}/); + expect(output.shift()).to.match(/.*/); + expect(output.shift()).to.match(/-Something Old/); + expect(output.shift()).to.match(/\+Howdy Billy/); + }); }); - it('renders a menu with an overwrite option', function(){ - var fileInfo = new FileInfo(validOptions); + it('renders a menu with an overwrite option', function () { + td.when(ui.prompt(td.matchers.anything())).thenReturn(Promise.resolve({ answer: 'overwrite' })); - ui.waitForPrompt().then(function(){ - ui.inputStream.write('Y' + EOL); - }); + let fileInfo = new FileInfo(validOptions); - return fileInfo.confirmOverwrite().then(function(action){ - var output = ui.output.trim().split(EOL); - expect(output.shift()).to.match(/Overwrite.*\?/); + return fileInfo.confirmOverwrite('test.js').then(function (action) { + td.verify(ui.prompt(td.matchers.anything()), { times: 1 }); expect(action).to.equal('overwrite'); }); }); - it('renders a menu with an skip option', function(){ - var fileInfo = new FileInfo(validOptions); + it('renders a menu with a skip option', function () { + td.when(ui.prompt(td.matchers.anything())).thenReturn(Promise.resolve({ answer: 'skip' })); - ui.waitForPrompt().then(function(){ - ui.inputStream.write('n' + EOL); - }); + let fileInfo = new FileInfo(validOptions); - return fileInfo.confirmOverwrite().then(function(action){ - var output = ui.output.trim().split(EOL); - expect(output.shift()).to.match(/Overwrite.*\?/); + return fileInfo.confirmOverwrite('test.js').then(function (action) { + td.verify(ui.prompt(td.matchers.anything()), { times: 1 }); expect(action).to.equal('skip'); }); }); - it('renders a menu with an diff option', function(){ - var fileInfo = new FileInfo(validOptions); + it('renders a menu with a diff option', function () { + td.when(ui.prompt(td.matchers.anything())).thenReturn(Promise.resolve({ answer: 'diff' })); - ui.waitForPrompt().then(function(){ - ui.inputStream.write('d' + EOL); - }); + let fileInfo = new FileInfo(validOptions); - return fileInfo.confirmOverwrite().then(function(action){ - var output = ui.output.trim().split(EOL); - expect(output.shift()).to.match(/Overwrite.*\?/); + return fileInfo.confirmOverwrite('test.js').then(function (action) { + td.verify(ui.prompt(td.matchers.anything()), { times: 1 }); expect(action).to.equal('diff'); }); }); + it('renders a menu without diff and edit options when dealing with binary files', function () { + td.when(ui.prompt(td.matchers.anything())).thenReturn(Promise.resolve({ answer: 'skip' })); + + let binary = path.resolve(__dirname, '../../fixtures/problem-binary.png'); + validOptions.inputPath = binary; + let fileInfo = new FileInfo(validOptions); + + return fileInfo.confirmOverwrite('test.png').then(function (/* action */) { + td.verify( + ui.prompt( + td.matchers.argThat(function (options) { + return options.choices.length === 2 && options.choices[0].key === 'y' && options.choices[1].key === 'n'; + }) + ) + ); + }); + }); + + it('normalizes line endings before comparing files', function () { + if (EOL === '\n') { + return; + } + + validOptions.inputPath = path.resolve(__dirname, '../../fixtures/file-info/test_crlf.js'); + validOptions.outputPath = path.resolve(__dirname, '../../fixtures/file-info/test_lf.js'); + let fileInfo = new FileInfo(validOptions); + + return fileInfo.checkForConflict().then(function (type) { + expect(type).to.equal('identical'); + }); + }); }); diff --git a/tests/unit/models/hardware-info-test.js b/tests/unit/models/hardware-info-test.js new file mode 100644 index 0000000000..6be7c6da6b --- /dev/null +++ b/tests/unit/models/hardware-info-test.js @@ -0,0 +1,509 @@ +'use strict'; + +const { expect } = require('chai'); +const { execa, hwinfo } = require('../../../lib/models/hardware-info'); +const fs = require('fs'); +const os = require('os'); +const td = require('testdouble'); + +const UPOWER_AC_OFF = ` native-path: /sys/devices/LNXSYSTM:00/device:00/PNP0C0A:00/power_supply/BAT0 + vendor: NOTEBOOK + model: BAT + serial: 0001 + power supply: yes + updated: Thu Feb 9 18:42:15 2012 (1 seconds ago) + has history: yes + has statistics: yes + battery + present: yes + rechargeable: yes + state: discharging + energy: 22.3998 Wh + energy-empty: 0 Wh + energy-full: 52.6473 Wh + energy-full-design: 62.16 Wh + energy-rate: 31.6905 W + voltage: 12.191 V + time to full: 57.3 minutes + percentage: 42.5469% + capacity: 84.6964% + technology: lithium-ion + History (charge): + 1328809335 42.547 charging + 1328809305 42.020 charging + 1328809275 41.472 charging + 1328809245 41.008 charging + History (rate): + 1328809335 31.691 charging + 1328809305 32.323 charging + 1328809275 33.133 charging +`; + +const UPOWER_AC_ON = ` native-path: /sys/devices/LNXSYSTM:00/device:00/PNP0C0A:00/power_supply/BAT0 + vendor: NOTEBOOK + model: BAT + serial: 0001 + power supply: yes + updated: Thu Feb 9 18:42:15 2012 (1 seconds ago) + has history: yes + has statistics: yes + battery + present: yes + rechargeable: yes + state: charging + energy: 22.3998 Wh + energy-empty: 0 Wh + energy-full: 52.6473 Wh + energy-full-design: 62.16 Wh + energy-rate: 31.6905 W + voltage: 12.191 V + time to full: 57.3 minutes + percentage: 42.5469% + capacity: 84.6964% + technology: lithium-ion + History (charge): + 1328809335 42.547 charging + 1328809305 42.020 charging + 1328809275 41.472 charging + 1328809245 41.008 charging + History (rate): + 1328809335 31.691 charging + 1328809305 32.323 charging + 1328809275 33.133 charging +`; + +// helper function for creating test doubles of execa.sync +function stdout(value) { + return () => ({ + stdout: value, + }); +} + +describe('models/hardware-info.js', function () { + afterEach(function () { + td.reset(); + }); + + describe('.isUsingBattery', function () { + it('returns null for unsupported platforms', function () { + expect(hwinfo.isUsingBattery('not-a-real-platform')).to.be.null; + }); + + describe('on FreeBSD', function () { + it('returns false via apm when not on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenReturn({ stdout: '1\n' }); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('freebsd')).to.be.false; + }); + + it('returns true via apm when on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenReturn({ stdout: '0\n' }); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('freebsd')).to.be.true; + }); + + it('returns false via upower when not on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenReturn({ stdout: UPOWER_AC_ON }); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('freebsd')).to.be.false; + }); + + it('returns true via upower when on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenReturn({ stdout: UPOWER_AC_OFF }); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('freebsd')).to.be.true; + }); + + it('returns null when battery status cannot be determined', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('freebsd')).to.be.null; + }); + }); + + describe('on Linux', function () { + it('returns false via /sys/class/power_supply when not on battery', function () { + const execaStub = td.function(execa.sync); + + td.when(execaStub(), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', execaStub); + + const readFileStub = td.function(fs.readFileSync); + + td.when(readFileStub(), { ignoreExtraArgs: true }).thenReturn('1\n'); + td.replace(fs, 'readFileSync', readFileStub); + + expect(hwinfo.isUsingBattery('linux')).to.be.false; + }); + + it('returns true via /sys/class/power_supply when on battery', function () { + const execaStub = td.function(execa.sync); + + td.when(execaStub(), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', execaStub); + + const fsStub = td.function(fs.readFileSync); + + td.when(fsStub(), { ignoreExtraArgs: true }).thenReturn('0\n'); + td.replace(fs, 'readFileSync', fsStub); + + expect(hwinfo.isUsingBattery('linux')).to.be.true; + }); + + it('returns false via acpi when not on battery', function () { + const execaStub = td.function(execa.sync); + + td.when(execaStub('acpi'), { ignoreExtraArgs: true }).thenReturn({ stdout: 'Adapter 0: on-line\n' }); + td.when(execaStub('upower'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', execaStub); + + const fsStub = td.function(fs.readFileSync); + + td.when(fsStub(), { ignoreExtraArgs: true }).thenThrow(new Error('file not found')); + td.replace(fs, 'readFileSync', fsStub); + + expect(hwinfo.isUsingBattery('linux')).to.be.false; + }); + + it('returns true via acpi when on battery', function () { + const execaStub = td.function(execa.sync); + + td.when(execaStub('acpi'), { ignoreExtraArgs: true }).thenReturn({ stdout: 'Adapter 0: off-line\n' }); + td.when(execaStub('upower'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', execaStub); + + const fsStub = td.function(fs.readFileSync); + + td.when(fsStub(), { ignoreExtraArgs: true }).thenThrow(new Error('file not found')); + td.replace(fs, 'readFileSync', fsStub); + + expect(hwinfo.isUsingBattery('linux')).to.be.true; + }); + + it('returns false via upower when not on battery', function () { + const execaStub = td.function(execa.sync); + + td.when(execaStub('acpi'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.when(execaStub('upower', ['--enumerate'])).thenReturn({ stdout: 'foo\n' }); + td.when(execaStub('upower', ['--show-info', 'foo'])).thenReturn({ stdout: UPOWER_AC_ON }); + td.replace(execa, 'sync', execaStub); + + const fsStub = td.function(fs.readFileSync); + + td.when(fsStub(), { ignoreExtraArgs: true }).thenThrow(new Error('file not found')); + td.replace(fs, 'readFileSync', fsStub); + + expect(hwinfo.isUsingBattery('linux')).to.be.false; + }); + + it('returns true via upower when on battery', function () { + const execaStub = td.function(execa.sync); + + td.when(execaStub('acpi'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.when(execaStub('upower', ['--enumerate'])).thenReturn({ stdout: 'foo\n' }); + td.when(execaStub('upower', ['--show-info', 'foo'])).thenReturn({ stdout: UPOWER_AC_OFF }); + td.replace(execa, 'sync', execaStub); + + const fsStub = td.function(fs.readFileSync); + + td.when(fsStub(), { ignoreExtraArgs: true }).thenThrow(new Error('file not found')); + td.replace(fs, 'readFileSync', fsStub); + + expect(hwinfo.isUsingBattery('linux')).to.be.true; + }); + + it('returns null when battery status cannot be determined', function () { + const execaStub = td.function(execa.sync); + + td.when(execaStub(), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', execaStub); + + const fsStub = td.function(fs.readFileSync); + + td.when(fsStub(), { ignoreExtraArgs: true }).thenThrow(new Error('file not found')); + td.replace(fs, 'readFileSync', fsStub); + + expect(hwinfo.isUsingBattery('linux')).to.be.null; + }); + }); + + describe('on macOS', function () { + it('returns false when not on battery', function () { + td.replace( + execa, + 'sync', + stdout(`Now drawing from 'AC Power' +-InternalBattery-0 (id=5636195) 100%; charged; 0:00 remaining present: true +`) + ); + + expect(hwinfo.isUsingBattery('darwin')).to.be.false; + }); + + it('returns true when on battery', function () { + td.replace( + execa, + 'sync', + stdout(`Now drawing from 'Battery Power' +-InternalBattery-0 (id=5636195) 100%; discharging; (no estimate) present: true +`) + ); + + expect(hwinfo.isUsingBattery('darwin')).to.be.true; + }); + + it('returns null when an error occurs', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('whoops!')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('darwin')).to.be.null; + }); + }); + + describe('on OpenBSD', function () { + it('returns false via apm when not on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenReturn({ stdout: '1\n' }); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('openbsd')).to.be.false; + }); + + it('returns true via apm when on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenReturn({ stdout: '0\n' }); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('openbsd')).to.be.true; + }); + + it('returns false via upower when not on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenReturn({ stdout: UPOWER_AC_ON }); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('openbsd')).to.be.false; + }); + + it('returns true via upower when on battery', function () { + const stub = td.function(execa.sync); + + td.when(stub('apm'), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.when(stub('upower'), { ignoreExtraArgs: true }).thenReturn({ stdout: UPOWER_AC_OFF }); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('openbsd')).to.be.true; + }); + + it('returns null when battery status cannot be determined', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('command not found')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('openbsd')).to.be.null; + }); + }); + + describe('on Windows', function () { + it('returns false when not on battery', function () { + td.replace( + execa, + 'sync', + stdout(` + +PowerOnline=TRUE + + + +`) + ); + + expect(hwinfo.isUsingBattery('win32')).to.be.false; + }); + + it('returns true when on battery', function () { + td.replace( + execa, + 'sync', + stdout(` + +PowerOnline=FALSE + + + +`) + ); + + expect(hwinfo.isUsingBattery('win32')).to.be.true; + }); + + it('returns null when an error occurs', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('whoops!')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.isUsingBattery('win32')).to.be.null; + }); + }); + }); + + describe('.memorySwapUsed', function () { + it('returns null for unsupported platforms', function () { + expect(hwinfo.memorySwapUsed('not-a-real-platform')).to.be.null; + }); + + it('returns the expected value on FreeBSD', function () { + td.replace( + execa, + 'sync', + stdout(`Device 1K-blocks Used Avail Capacity +/dev/gpt/swapfs1 1048576 837 1047739 0% +/dev/gpt/swapfs2 1048576 985 1047591 0% +`) + ); + + expect(hwinfo.memorySwapUsed('freebsd')).to.equal(1865728); + }); + + it('returns null on FreeBSD when an error occurs', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('whoops!')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.memorySwapUsed('freebsd')).to.be.null; + }); + + it('returns the expected value on Linux', function () { + td.replace( + execa, + 'sync', + stdout(` total used free shared buff/cache available +Mem: 67275370496 2743869440 41266409472 3447558144 23265091584 60482338816 +Swap: 67448598528 121593856 67327004672 +`) + ); + + expect(hwinfo.memorySwapUsed('linux')).to.equal(121593856); + }); + + it('returns null on Linux when an error occurs', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('whoops!')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.memorySwapUsed('linux')).to.be.null; + }); + + it('returns the expected value on macOS', function () { + td.replace( + execa, + 'sync', + stdout(`vm.swapusage: total = 6144.00M used = 4987.75M free = 1156.25M (encrypted) +`) + ); + + expect(hwinfo.memorySwapUsed('darwin')).to.equal(5230034944); + }); + + it('returns null on macOS when an error occurs', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('whoops!')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.memorySwapUsed('darwin')).to.be.null; + }); + + it('returns the expected value on OpenBSD', function () { + td.replace( + execa, + 'sync', + stdout(`Device 512-blocks Used Avail Capacity +/dev/gpt/swapfs1 1048576 837 1047739 0% +/dev/gpt/swapfs2 1048576 985 1047591 0% +`) + ); + + expect(hwinfo.memorySwapUsed('openbsd')).to.equal(932864); + }); + + it('returns null on OpenBSD when an error occurs', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('whoops!')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.memorySwapUsed('openbsd')).to.be.null; + }); + + it('returns the expected value on Windows', function () { + td.replace( + execa, + 'sync', + stdout(` + +CurrentUsage=325 + + + +`) + ); + + expect(hwinfo.memorySwapUsed('win32')).to.equal(340787200); + }); + + it('returns null on Windows when an error occurs', function () { + const stub = td.function(execa.sync); + + td.when(stub(), { ignoreExtraArgs: true }).thenThrow(new Error('whoops!')); + td.replace(execa, 'sync', stub); + + expect(hwinfo.memorySwapUsed('win32')).to.be.null; + }); + }); + + describe('.processorLoad', function () { + it('returns null on Windows', function () { + expect(hwinfo.processorLoad('win32')).to.be.null; + }); + }); + + describe('.processorSpeed', function () { + it("averages the processors' speeds", function () { + td.replace(os, 'cpus', () => [{ speed: 1 }, { speed: 2 }, { speed: 3 }, { speed: 4 }, { speed: 5 }]); + + expect(hwinfo.processorSpeed()).to.equal(3); + }); + }); +}); diff --git a/tests/unit/models/host-info-cache-test.js b/tests/unit/models/host-info-cache-test.js new file mode 100644 index 0000000000..0bf08b22a8 --- /dev/null +++ b/tests/unit/models/host-info-cache-test.js @@ -0,0 +1,187 @@ +'use strict'; + +/** + * Tests for the various proxies and instances once the project has initialized + * its addons + */ +const { expect } = require('chai'); +const FixturifyProject = require('../../helpers/fixturify-project'); + +describe('Unit | host-addons-utils', function () { + let fixturifyProject; + + beforeEach(function () { + fixturifyProject = new FixturifyProject('awesome-proj', '1.0.0'); + fixturifyProject.addDevDependency('ember-cli', '*'); + }); + + afterEach(function () { + fixturifyProject.dispose(); + }); + + it('multiple lazy engines in project, including nested lazy engines', function () { + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { enableLazyLoading: true }); + + fixturifyProject.addAddon('addon-a', '1.0.0', { + enableLazyLoading: true, + callback: (addon) => { + addon.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('lazy-engine-a'); + engine.addEngine('lazy-engine-c', '1.0.0', { enableLazyLoading: true }); + }, + }); + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + const app = {}; + + const lazyEngineA = project.addons.find((addon) => addon.name === 'lazy-engine-a'); + lazyEngineA.app = app; + const pkgInfoLazyEngineA = lazyEngineA._packageInfo; + + const addonA = project.addons.find((addon) => addon.name === 'addon-a'); + addonA.app = app; + const pkgInfoAddonA = addonA._packageInfo; + + let { hostPackageInfo, hostAndAncestorBundledPackageInfos } = + project.hostInfoCache.getHostAddonInfo(pkgInfoLazyEngineA); + + expect(hostPackageInfo).to.equal(project._packageInfo, 'host package-info for lazy-engine A is the project'); + expect(project.hostInfoCache.findLCAHost(lazyEngineA)).to.equal(lazyEngineA.app, 'LCA host is the app'); + + expect(hostAndAncestorBundledPackageInfos).to.deep.equal( + new Set([pkgInfoAddonA]), + 'host packge-infos for lazy-engine A includes only addon-a' + ); + + const lazyEngineB = project.addons + .find((addon) => addon.name === 'addon-a') + .addons.find((addon) => addon.name === 'lazy-engine-b'); + + const pkgInfoLazyEngineB = lazyEngineB._packageInfo; + + ({ hostPackageInfo, hostAndAncestorBundledPackageInfos } = + project.hostInfoCache.getHostAddonInfo(pkgInfoLazyEngineB)); + + expect(hostPackageInfo).to.equal(project._packageInfo, 'host package-info for lazy-engine B is the project'); + expect(project.hostInfoCache.findLCAHost(lazyEngineB)).to.equal(lazyEngineA.app, 'LCA host is the app'); + expect(hostAndAncestorBundledPackageInfos).to.deep.equal( + new Set([pkgInfoAddonA]), + 'host packge-infos for lazy-engine B includes only addon-a' + ); + + const lazyEngineC = project.addons + .find((addon) => addon.name === 'addon-a') + .addons.find((addon) => addon.name === 'lazy-engine-b') + .addons.find((addon) => addon.name === 'lazy-engine-c'); + + const pkgInfoLazyEngineC = lazyEngineC._packageInfo; + + ({ hostPackageInfo, hostAndAncestorBundledPackageInfos } = + project.hostInfoCache.getHostAddonInfo(pkgInfoLazyEngineC)); + + expect(hostPackageInfo).to.equal(pkgInfoLazyEngineB, 'host package-info for lazy-engine C is lazy engine B'); + + expect(project.hostInfoCache.findLCAHost(lazyEngineC)).to.equal( + lazyEngineB, + 'LCA host for lazy engine C is lazy engine B' + ); + + expect(hostAndAncestorBundledPackageInfos).to.deep.equal( + new Set([pkgInfoAddonA]), + 'host packge-infos for lazy-engine C includes addon-a' + ); + }); + + it('multiple lazy engines in project, including nested lazy engines; some nested lazy engines have non-lazy deps', function () { + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { enableLazyLoading: true }); + + fixturifyProject.addAddon('addon-a', '1.0.0', { + enableLazyLoading: true, + callback: (addon) => { + addon.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('lazy-engine-a'); + engine.addAddon('addon-b', '1.0.0'); + engine.addEngine('lazy-engine-c', '1.0.0', { enableLazyLoading: true }); + }, + }); + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + const pkgInfoAddonA = project.addons.find((addon) => addon.name === 'addon-a')._packageInfo; + + const pkgInfoLazyEngineB = project.addons + .find((addon) => addon.name === 'addon-a') + .addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + + const pkgInfoAddonB = project.addons + .find((addon) => addon.name === 'addon-a') + .addons.find((addon) => addon.name === 'lazy-engine-b') + .addons.find((addon) => addon.name === 'addon-b')._packageInfo; + + const pkgInfoLazyEngineC = project.addons + .find((addon) => addon.name === 'addon-a') + .addons.find((addon) => addon.name === 'lazy-engine-b') + .addons.find((addon) => addon.name === 'lazy-engine-c')._packageInfo; + + let { hostPackageInfo, hostAndAncestorBundledPackageInfos } = + project.hostInfoCache.getHostAddonInfo(pkgInfoLazyEngineC); + + expect(hostPackageInfo).to.equal(pkgInfoLazyEngineB, 'host package-info for lazy-engine C is lazy engine B'); + expect(hostAndAncestorBundledPackageInfos).to.deep.equal( + new Set([pkgInfoAddonA, pkgInfoAddonB]), + 'host packge-infos for lazy-engine C includes addon-a, addon-b' + ); + }); + + it('multiple lazy engines at same level with a common ancestor host', function () { + fixturifyProject.addInRepoEngine('lazy-engine-a', '1.0.0', { enableLazyLoading: true }); + fixturifyProject.pkg['ember-addon'].paths = []; + + fixturifyProject.addInRepoEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../lazy-engine-a']; + }, + }); + + fixturifyProject.addInRepoEngine('lazy-engine-c', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../lazy-engine-a']; + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + const pkgInfoLazyEngineA = project.addons + .find((addon) => addon.name === 'lazy-engine-b') + .addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + + let { hostPackageInfo, hostAndAncestorBundledPackageInfos } = + project.hostInfoCache.getHostAddonInfo(pkgInfoLazyEngineA); + + expect(hostPackageInfo).to.equal(project._packageInfo, 'host package-info for lazy-engine A is the project'); + expect(hostAndAncestorBundledPackageInfos).to.deep.equal( + new Set([]), + 'host packge-infos for lazy-engine A has no non-lazy deps' + ); + }); +}); diff --git a/tests/unit/models/installation-checker-test.js b/tests/unit/models/installation-checker-test.js deleted file mode 100644 index ffca6df280..0000000000 --- a/tests/unit/models/installation-checker-test.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var InstallationChecker = require('../../../lib/models/installation-checker'); -var path = require('path'); - -describe('Installation Checker', function() { - var installationChecker; - - function fixturePath(pathToFile) { - return path.resolve(path.join(__dirname, '..', '..', 'fixtures'), pathToFile); - } - - function checkInstallations() { - installationChecker.checkInstallations(); - } - - describe('bower', function() { - - it('works when installation directory exist', function() { - var project = { - root: fixturePath('installation-checker/valid-bower-installation'), - bowerDirectory: fixturePath('installation-checker/valid-bower-installation/bower_components') - }; - installationChecker = new InstallationChecker({ project: project }); - - expect(checkInstallations).to.not.throw(/No dependencies installed/); - }); - - it('fails when installation directory doesn\'t exist', function() { - var project = { - root: fixturePath('installation-checker/invalid-bower-installation'), - bowerDirectory: fixturePath('installation-checker/invalid-bower-installation/bower_components') - }; - installationChecker = new InstallationChecker({ project: project }); - - expect(checkInstallations).to.throw(/^InstallationChecker: Unable to parse: .*bower.json/); - }); - - }); - - describe('npm', function() { - - it('works when installation directory exist', function() { - var project = { - root: fixturePath('installation-checker/valid-npm-installation'), - nodeModulesPath: fixturePath('installation-checker/valid-npm-installation/node_modules') - }; - installationChecker = new InstallationChecker({ project: project }); - - expect(checkInstallations).to.not.throw(/No dependencies installed/); - }); - - it('fails when installation directory doesn\'t exist', function() { - var project = { - root: fixturePath('installation-checker/invalid-npm-installation'), - nodeModulesPath: fixturePath('installation-checker/invalid-npm-installation/node_modules') - }; - installationChecker = new InstallationChecker({ project: project }); - - expect(checkInstallations).to.throw(/^InstallationChecker: Unable to parse: .*package.json/); - }); - - }); - - describe('npm and bower', function() { - - it('fails reporting both dependencies', function() { - var project = { - root: fixturePath('installation-checker/invalid-bower-and-npm'), - bowerDirectory: fixturePath('installation-checker/invalid-bower-and-npm/bower_components'), - nodeModulesPath: fixturePath('installation-checker/invalid-bower-and-npm/node_modules') - }; - installationChecker = new InstallationChecker({ project: project }); - - expect(checkInstallations).to.throw(/^InstallationChecker: Unable to parse: .*package.json/); - }); - - it('ignores directories without bower.js and package.json files', function() { - var project = { - root: fixturePath('installation-checker/empty'), - bowerDirectory: fixturePath('installation-checker/empty/bower_components'), - nodeModulesPath: fixturePath('installation-checker/empty/node_modules') - }; - installationChecker = new InstallationChecker({ project: project }); - - expect(checkInstallations).to.not.throw('/No dependencies installed/'); - }); - - }); -}); diff --git a/tests/unit/models/instantiate-addons-test.js b/tests/unit/models/instantiate-addons-test.js new file mode 100644 index 0000000000..0d579b2dba --- /dev/null +++ b/tests/unit/models/instantiate-addons-test.js @@ -0,0 +1,104 @@ +'use strict'; + +const FixturifyProject = require('../../helpers/fixturify-project'); +const { expect } = require('chai'); + +describe('models/instatiate-addons.js', function () { + let fixturifyProject; + + beforeEach(function () { + fixturifyProject = new FixturifyProject('awesome-proj', '0.0.0'); + fixturifyProject.addDevDependency('ember-cli', '*'); + }); + + afterEach(function () { + fixturifyProject.dispose(); + }); + + it('ordering without before/after', function () { + // this tests ordering is very important to maintain, it tests some naunced + // details which must be maintained + fixturifyProject.addAddon('foo', '1.0.0'); + fixturifyProject.addAddon('bar', '1.0.0'); + fixturifyProject.addAddon('qux', '1.0.0'); + + // duplicates + fixturifyProject.addDevAddon('foo', '2.0.0'); + fixturifyProject.addDevAddon('bar', '2.0.0'); + fixturifyProject.addDevAddon('qux', '2.0.0'); + + // unique devDependencies + fixturifyProject.addDevAddon('a', '2.0.0'); + fixturifyProject.addDevAddon('b', '2.0.0'); + fixturifyProject.addDevAddon('c', '2.0.0'); + + fixturifyProject.writeSync(); + + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(project.addons.map((a) => ({ name: a.pkg.name, version: a.pkg.version }))).to.deep.eql([ + { name: 'a', version: '2.0.0' }, + { name: 'b', version: '2.0.0' }, + { name: 'c', version: '2.0.0' }, + + { name: 'bar', version: '1.0.0' }, + { name: 'foo', version: '1.0.0' }, + { name: 'qux', version: '1.0.0' }, + ]); + }); + + it('ordering with before specified', function () { + fixturifyProject.addAddon('foo', '1.0.0'); + fixturifyProject.addAddon('bar', '1.0.0'); + fixturifyProject.addAddon('qux', '1.0.0', (a) => (a.pkg['ember-addon'].before = 'foo')); + + fixturifyProject.writeSync(); + + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(project.addons.map((a) => a.name)).to.deep.eql(['bar', 'qux', 'foo']); + }); + + it('ordering with after specified', function () { + fixturifyProject.addAddon('foo', '1.0.0'); + fixturifyProject.addAddon('bar', '1.0.0'); + fixturifyProject.addAddon('qux', '1.0.0', (a) => (a.pkg['ember-addon'].after = 'foo')); + + fixturifyProject.writeSync(); + + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(project.addons.map((a) => a.name)).to.deep.eql(['bar', 'foo', 'qux']); + }); + + it('ordering always matches package.json name (index.js name is ignored)', function () { + let foo = fixturifyProject.addAddon('lol', '1.0.0'); + foo.files['index.js'] = 'module.exports = { name: "foo" };'; + fixturifyProject.addAddon('qux', '1.0.0', (a) => (a.pkg['ember-addon'].before = 'foo')); + + fixturifyProject.writeSync(); + + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(project.addons.map((a) => a.name)).to.deep.eql(['foo', 'qux']); + }); + + it('errors when there is a cycle detected', function () { + fixturifyProject.addAddon('foo', '1.0.0', (a) => (a.pkg['ember-addon'].after = 'qux')); + fixturifyProject.addAddon('qux', '1.0.0', (a) => (a.pkg['ember-addon'].after = 'foo')); + + fixturifyProject.writeSync(); + + let project = fixturifyProject.buildProjectModel(); + + expect(() => project.initializeAddons()).to.throw(/cycle detected/); + }); +}); diff --git a/tests/unit/models/instrumentation-test.js b/tests/unit/models/instrumentation-test.js new file mode 100644 index 0000000000..158b7db794 --- /dev/null +++ b/tests/unit/models/instrumentation-test.js @@ -0,0 +1,871 @@ +'use strict'; + +const Heimdall = require('heimdalljs/heimdall'); +const heimdallGraph = require('heimdalljs-graph'); +const { expect } = require('chai'); +const td = require('testdouble'); +const fs = require('fs'); +const tmp = require('tmp-promise'); +const fse = require('fs-extra'); +const MockUI = require('console-ui/mock'); +const Yam = require('yam'); + +const MockProject = require('../../helpers/mock-project'); +const { hwinfo } = require('../../../lib/models/hardware-info'); +const Instrumentation = require('../../../lib/models/instrumentation'); + +const any = td.matchers.anything; +const contains = td.matchers.contains; + +const root = process.cwd(); + +let instrumentation; +let originalStatSync = fs.statSync; + +describe('models/instrumentation.js', function () { + afterEach(async function () { + delete process.env.BROCCOLI_VIZ; + delete process.env.EMBER_CLI_INSTRUMENTATION; + + process.chdir(root); + }); + + describe('._enableFSMonitorIfInstrumentationEnabled', function () { + beforeEach(function () { + expect(!!process.env.BROCCOLI_VIZ).to.eql(false); + expect(!!process.env.EMBER_CLI_INSTRUMENTATION).to.eql(false); + expect(fs.statSync).to.equal(originalStatSync); + }); + + afterEach(function () { + td.reset(); + }); + + it('if VIZ is NOT enabled, do not monitor', function () { + let monitor = Instrumentation._enableFSMonitorIfInstrumentationEnabled(); + try { + expect(fs.statSync).to.equal(originalStatSync); + expect(monitor).to.eql(undefined); + } finally { + if (monitor) { + monitor.stop(); + } + } + }); + + it('if VIZ is enabled, monitor', function () { + process.env.BROCCOLI_VIZ = '1'; + let monitor = Instrumentation._enableFSMonitorIfInstrumentationEnabled(); + try { + expect(fs.statSync).to.not.equal(originalStatSync); + } finally { + if (monitor) { + monitor.stop(); + } + } + }); + + it('if instrumentation is enabled, monitor', function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + let monitor = Instrumentation._enableFSMonitorIfInstrumentationEnabled(); + try { + expect(fs.statSync).to.not.equal(originalStatSync); + } finally { + if (monitor) { + monitor.stop(); + } + } + }); + + it('if enableInstrumentation is NOT enabled in .ember-cli, do not monitor', function () { + let mockedYam = new Yam('ember-cli', { + primary: `${process.cwd()}/tests/fixtures/instrumentation-disabled-config`, + }); + let monitor = Instrumentation._enableFSMonitorIfInstrumentationEnabled(mockedYam); + try { + expect(fs.statSync).to.equal(originalStatSync); + expect(monitor).to.eql(undefined); + } finally { + if (monitor) { + monitor.stop(); + } + } + }); + + it('if enableInstrumentation is enabled in .ember-cli, monitor', function () { + let mockedYam = new Yam('ember-cli', { + primary: `${process.cwd()}/tests/fixtures/instrumentation-enabled-config`, + }); + let monitor = Instrumentation._enableFSMonitorIfInstrumentationEnabled(mockedYam); + try { + expect(fs.statSync, 'fs.statSync').to.not.equal(originalStatSync, '[original] fs.statSync'); + } finally { + if (monitor) { + monitor.stop(); + } + } + }); + }); + + describe('constructor', function () { + const heimdall = require('heimdalljs'); + let heimdallStart; + + beforeEach(function () { + heimdallStart = td.replace(heimdall, 'start'); + }); + + afterEach(function () { + delete process.env.EMBER_CLI_INSTRUMENTATION; + td.reset(); + }); + + describe('when instrumentation is enabled', function () { + beforeEach(function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + }); + + it('starts an init node if init instrumentation is missing', function () { + let mockToken = {}; + + td.when( + heimdallStart( + contains({ + name: 'init', + emberCLI: true, + }) + ) + ).thenReturn(mockToken); + + let instrumentation = new Instrumentation({ + ui: new MockUI(), + }); + + expect(instrumentation.instrumentations.init).to.not.equal(undefined); + expect(instrumentation.instrumentations.init.token).to.equal(mockToken); + expect(instrumentation.instrumentations.init.node).to.not.equal(undefined); + }); + + it('does not create an init node if init instrumentation is included', function () { + let mockToken = {}; + let mockInstrumentation = {}; + + td.when(heimdallStart('init')).thenReturn(mockToken); + + let instrumentation = new Instrumentation({ + initInstrumentation: mockInstrumentation, + }); + + expect(instrumentation.instrumentations.init).to.eql(mockInstrumentation); + td.verify(heimdallStart(), { times: 0, ignoreExtraArgs: true }); + }); + + it('does not warn if init instrumentation is included', function () { + td.when(heimdallStart('init')); + + let mockInstrumentation = {}; + + let ui = new MockUI(); + + new Instrumentation({ + ui, + initInstrumentation: mockInstrumentation, + }); + + expect(ui.output.trim()).to.eql(''); + }); + }); + + describe('when instrumentation is not enabled', function () { + beforeEach(function () { + expect(process.env.EMBER_CLI_INSTRUMENTATION).to.eql(undefined); + }); + + it('does not create an init node if init instrumentation is missing', function () { + let mockToken = {}; + + td.when(heimdallStart('init')).thenReturn(mockToken); + + let instrumentation = new Instrumentation({}); + + expect(instrumentation.instrumentations.init).to.eql(undefined); + td.verify(heimdallStart(), { times: 0, ignoreExtraArgs: true }); + }); + + it('does not warn when init instrumentation is missing', function () { + td.when(heimdallStart('init')); + + let ui = new MockUI(); + + new Instrumentation({ + ui, + }); + + expect(ui.output.trim()).to.eql(''); + }); + }); + }); + + describe('.isVizEnabled', function () { + let originalWarn = console.warn; + let warnInvocations; + + beforeEach(function () { + instrumentation = new Instrumentation({ + ui: new MockUI(), + }); + + delete process.env.BROCCOLI_VIZ; + delete process.env.EMBER_CLI_INSTRUMENTATION; + warnInvocations = []; + console.warn = function () { + warnInvocations.push.apply(warnInvocations, Array.prototype.slice.call(arguments)); + }; + }); + + afterEach(function () { + console.warn = originalWarn; + }); + + it('is true and does not warn if BROCCOLI_VIZ=1', function () { + process.env.BROCCOLI_VIZ = '1'; + expect(instrumentation.isVizEnabled()).to.eql(true); + expect(warnInvocations).to.eql([]); + }); + + it('is false if BROCCOLI_VIZ is unset', function () { + expect('BROCCOLI_VIZ' in process.env).to.eql(false); + expect(instrumentation.isVizEnabled()).to.eql(false); + expect(warnInvocations).to.eql([]); + }); + }); + + describe('.isEnabled', function () { + beforeEach(function () { + instrumentation = new Instrumentation({ + ui: new MockUI(), + }); + delete process.env.BROCCOLI_VIZ; + delete process.env.EMBER_CLI_INSTRUMENTATION; + }); + + it('is true if BROCCOLI_VIZ=1', function () { + process.env.BROCCOLI_VIZ = '1'; + expect(instrumentation.isEnabled()).to.eql(true); + }); + + it('is true if EMBER_CLI_INSTRUMENTATION=1', function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + expect(instrumentation.isEnabled()).to.eql(true); + }); + + it('is false if EMBER_CLI_INSTRUMENTATION != 1', function () { + process.env.EMBER_CLI_INSTRUMENTATION = 'on'; + expect(instrumentation.isEnabled()).to.eql(false); + }); + + it('is false if both BROCCOLI_VIZ and EMBER_CLI_INSTRUMENTATION are unset', function () { + expect('BROCCOLI_VIZ' in process.env).to.eql(false); + expect('EMBER_CLI_INSTRUMENTATION' in process.env).to.eql(false); + expect(instrumentation.isEnabled()).to.eql(false); + }); + }); + + describe('.start', function () { + let project; + let instrumentation; + let heimdall; + + beforeEach(function () { + project = new MockProject(); + instrumentation = project._instrumentation; + instrumentation._heimdall = heimdall = new Heimdall(); + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + }); + + it('starts a new subtree for name', function () { + let heimdallStart = td.replace(heimdall, 'start'); + + instrumentation.start('init'); + + td.verify( + heimdallStart( + td.matchers.contains({ + name: 'init', + emberCLI: true, + }) + ) + ); + + instrumentation.start('build'); + + td.verify( + heimdallStart( + td.matchers.contains({ + name: 'build', + emberCLI: true, + }) + ) + ); + + instrumentation.start('command'); + + td.verify( + heimdallStart( + td.matchers.contains({ + name: 'command', + emberCLI: true, + }) + ) + ); + + instrumentation.start('shutdown'); + + td.verify( + heimdallStart( + td.matchers.contains({ + name: 'shutdown', + emberCLI: true, + }) + ) + ); + }); + + it('does not start a subtree if instrumentation is disabled', function () { + process.env.EMBER_CLI_INSTRUMENTATION = 'no thanks'; + + let heimdallStart = td.replace(heimdall, 'start'); + + instrumentation.start('init'); + + td.verify(heimdallStart(), { times: 0, ignoreExtraArgs: true }); + }); + + it('throws if name is unexpected', function () { + expect(() => { + instrumentation.start('a party!'); + }).to.throw('No such instrumentation "a party!"'); + }); + + it('removes any prior instrumentation information to avoid leaks', function () { + function build() { + instrumentation.start('build'); + let a = heimdall.start('a'); + let b = heimdall.start('b'); + b.stop(); + a.stop(); + instrumentation.stopAndReport('build'); + } + + function countNodes() { + let graph = heimdallGraph.loadFromNode(heimdall.root); + let count = 0; + + // eslint-disable-next-line no-unused-vars + for (let n of graph.dfsIterator()) { + ++count; + } + + return count; + } + + td.replace(instrumentation, '_buildSummary'); + td.replace(instrumentation, '_invokeAddonHook'); + + build(); + let count = countNodes(); + build(); + expect(countNodes()).to.equal(count); + }); + }); + + describe('.stopAndReport', function () { + let project; + let instrumentation; + let heimdall; + let addon; + + beforeEach(function () { + project = new MockProject(); + instrumentation = project._instrumentation; + heimdall = instrumentation._heimdall = new Heimdall(); + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + + addon = { + name: 'Test Addon', + }; + project.addons = [ + { + name: 'Some Other Addon', + }, + addon, + ]; + }); + + it('throws if name is unexpected', function () { + expect(() => instrumentation.stopAndReport('the weather')).to.throw('No such instrumentation "the weather"'); + }); + + it('throws if name has not yet started', function () { + expect(() => instrumentation.stopAndReport('init')).to.throw( + 'Cannot stop instrumentation "init". It has not started.' + ); + }); + + it('warns if heimdall stop throws (eg when unbalanced)', function () { + instrumentation.start('init'); + heimdall.start('a ruckus'); + + expect(() => instrumentation.stopAndReport('init')).to.not.throw(); + + instrumentation.start('init'); + heimdall.start('trouble'); + + expect(() => instrumentation.stopAndReport('init')).to.not.throw(); + }); + + it('computes summary for name', function () { + let buildSummary = td.replace(instrumentation, '_buildSummary'); + let initSummary = td.replace(instrumentation, '_initSummary'); + let treeFor = td.replace(instrumentation, '_instrumentationTreeFor'); + + let invokeAddonHook = td.replace(instrumentation, '_invokeAddonHook'); + let writeInstrumentation = td.replace(instrumentation, '_writeInstrumentation'); + + let mockInitSummary = 'init summary'; + let mockBuildSummary = 'build summary'; + let mockInitTree = 'init tree'; + let mockBuildTree = 'build tree'; + + td.when(initSummary(any(), 'a', 'b')).thenReturn(mockInitSummary); + td.when(buildSummary(any(), 'a', 'b')).thenReturn(mockBuildSummary); + td.when(treeFor('init')).thenReturn(mockInitTree); + td.when(treeFor('build')).thenReturn(mockBuildTree); + + td.verify(invokeAddonHook(), { ignoreExtraArgs: true, times: 0 }); + td.verify(writeInstrumentation(), { ignoreExtraArgs: true, times: 0 }); + + instrumentation.start('init'); + instrumentation.stopAndReport('init', 'a', 'b'); + + td.verify( + invokeAddonHook('init', { + summary: mockInitSummary, + tree: mockInitTree, + }), + { ignoreExtraArgs: true, times: 1 } + ); + + td.verify( + writeInstrumentation('init', { + summary: mockInitSummary, + tree: mockInitTree, + }), + { ignoreExtraArgs: true, times: 1 } + ); + + td.verify(invokeAddonHook(), { ignoreExtraArgs: true, times: 1 }); + td.verify(writeInstrumentation(), { ignoreExtraArgs: true, times: 1 }); + + instrumentation.start('build'); + instrumentation.stopAndReport('build', 'a', 'b'); + + td.verify( + invokeAddonHook('build', { + summary: mockBuildSummary, + tree: mockBuildTree, + }), + { ignoreExtraArgs: true, times: 1 } + ); + + td.verify( + writeInstrumentation('build', { + summary: mockBuildSummary, + tree: mockBuildTree, + }), + { ignoreExtraArgs: true, times: 1 } + ); + }); + + describe('writes to disk', function () { + beforeEach(function () { + let buildSummary = td.replace(instrumentation, '_buildSummary'); + let initSummary = td.replace(instrumentation, '_initSummary'); + let treeFor = td.replace(instrumentation, '_instrumentationTreeFor'); + + let mockInitSummary = { ok: 'init dokie' }; + let mockInitTree = { + toJSON() { + return { nodes: [{ i: 'can init json' }] }; + }, + }; + let mockBuildSummary = { ok: 'build dokie' }; + let mockBuildTree = { + toJSON() { + return { nodes: [{ i: 'can build json' }] }; + }, + }; + + td.when(initSummary(any(), 'a', 'b')).thenReturn(mockInitSummary); + td.when(buildSummary(any(), 'a', 'b')).thenReturn(mockBuildSummary); + td.when(treeFor('init')).thenReturn(mockInitTree); + td.when(treeFor('build')).thenReturn(mockBuildTree); + + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + }); + + it('writes instrumentation info if viz is enabled', async function () { + process.env.BROCCOLI_VIZ = '1'; + + const { path } = await tmp.dir(); + + process.chdir(path); + + instrumentation.start('init'); + instrumentation.stopAndReport('init', 'a', 'b'); + + expect(fs.existsSync('instrumentation.init.json')).to.equal(true); + expect(fse.readJsonSync('instrumentation.init.json')).to.eql({ + summary: { ok: 'init dokie' }, + nodes: [{ i: 'can init json' }], + }); + + instrumentation.start('build'); + instrumentation.stopAndReport('build', 'a', 'b'); + + expect(fs.existsSync('instrumentation.build.0.json')).to.equal(true); + expect(fse.readJsonSync('instrumentation.build.0.json')).to.eql({ + summary: { ok: 'build dokie' }, + nodes: [{ i: 'can build json' }], + }); + + instrumentation.start('build'); + instrumentation.stopAndReport('build', 'a', 'b'); + + expect(fs.existsSync('instrumentation.build.1.json')).to.equal(true); + expect(fse.readJsonSync('instrumentation.build.1.json')).to.eql({ + summary: { ok: 'build dokie' }, + nodes: [{ i: 'can build json' }], + }); + }); + + it('does not write instrumentation info if viz is disabled', async function () { + delete process.env.BROCCOLI_VIZ; + + const { path } = await tmp.dir(); + process.chdir(path); + + instrumentation.start('init'); + instrumentation.stopAndReport('init'); + + expect(fs.existsSync('instrumentation.init.json')).to.equal(false); + + instrumentation.start('build'); + instrumentation.stopAndReport('build'); + + expect(fs.existsSync('instrumentation.build.0.json')).to.equal(false); + + instrumentation.start('build'); + instrumentation.stopAndReport('build'); + + expect(fs.existsSync('instrumentation.build.1.json')).to.equal(false); + }); + }); + + describe('addons', function () { + let mockInitSummary; + let mockInitTree; + let mockBuildSummary; + let mockBuildTree; + + beforeEach(function () { + let buildSummary = td.replace(instrumentation, '_buildSummary'); + let initSummary = td.replace(instrumentation, '_initSummary'); + let treeFor = td.replace(instrumentation, '_instrumentationTreeFor'); + + mockInitSummary = 'init summary'; + mockInitTree = 'init tree'; + mockBuildSummary = 'build summary'; + mockBuildTree = 'build tree'; + + td.when(initSummary(any(), 'a', 'b')).thenReturn(mockInitSummary); + td.when(buildSummary(any(), 'a', 'b')).thenReturn(mockBuildSummary); + td.when(treeFor('init')).thenReturn(mockInitTree); + td.when(treeFor('build')).thenReturn(mockBuildTree); + }); + + it('invokes addons that have [INSTRUMENTATION] for init', function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + + let hook = td.function(); + addon.instrumentation = hook; + + instrumentation.start('init'); + instrumentation.stopAndReport('init', 'a', 'b'); + + td.verify(hook('init', { summary: mockInitSummary, tree: mockInitTree })); + }); + + it('invokes addons that have [INSTRUMENTATION] for build', function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + + let hook = td.function(); + addon.instrumentation = hook; + + instrumentation.start('build'); + instrumentation.stopAndReport('build', 'a', 'b'); + + td.verify(hook('build', { summary: mockBuildSummary, tree: mockBuildTree })); + }); + + it('does not invoke addons if instrumentation is disabled', function () { + process.env.EMBER_CLI_INSTRUMENTATION = 'not right now thanks'; + + let hook = td.function(); + addon.instrumentation = hook; + + instrumentation.start('build'); + instrumentation.stopAndReport('build', 'a', 'b'); + + td.verify(hook(), { ignoreExtraArgs: true, times: 0 }); + }); + }); + }); + + describe('._instrumenationTreeFor', function () { + function StatsSchema() { + this.x = 0; + this.y = 0; + } + + function makeTree(name) { + instrumentation = new Instrumentation({ + ui: new MockUI(), + initInstrumentation: { + node: null, + token: null, + }, + }); + let heimdall = (instrumentation._heimdall = new Heimdall()); + + // {init,build,command,shutdown} + // └── a + // ├── b1 + // │ └── c1 + // └── b2 + // ├── c2 + // │ └── d1 + // └── c3 + heimdall.registerMonitor('mystats', StatsSchema); + + instrumentation.start(name); + let a = heimdall.start('a'); + let b1 = heimdall.start({ name: 'b1', broccoliNode: true, broccoliCachedNode: false }); + let c1 = heimdall.start('c1'); + heimdall.statsFor('mystats').x = 3; + heimdall.statsFor('mystats').y = 4; + c1.stop(); + b1.stop(); + let b2 = heimdall.start('b2'); + let c2 = heimdall.start({ name: 'c2', broccoliNode: true, broccoliCachedNode: false }); + let d1 = heimdall.start({ name: 'd1', broccoliNode: true, broccoliCachedNode: true }); + d1.stop(); + c2.stop(); + let c3 = heimdall.start('c3'); + c3.stop(); + b2.stop(); + a.stop(); + } + + function assertTreeValidJSON(name, tree) { + let json = tree.toJSON(); + + expect(Object.keys(json)).to.eql(['nodes']); + expect(json.nodes.length).to.eql(8); + + expect(json.nodes.map((x) => x.id)).to.eql([1, 2, 3, 4, 5, 6, 7, 8]); + + expect(json.nodes.map((x) => x.label)).to.eql([ + { name, emberCLI: true }, + { name: 'a' }, + { name: 'b1', broccoliNode: true, broccoliCachedNode: false }, + { name: 'c1' }, + { name: 'b2' }, + { name: 'c2', broccoliNode: true, broccoliCachedNode: false }, + { name: 'd1', broccoliNode: true, broccoliCachedNode: true }, + { name: 'c3' }, + ]); + + expect(json.nodes.map((x) => x.children)).to.eql([[2], [3, 5], [4], [], [6, 8], [7], [], []]); + + let stats = json.nodes.map((x) => x.stats); + stats.forEach((nodeStats) => { + expect('own' in nodeStats).to.eql(true); + expect('time' in nodeStats).to.eql(true); + expect(nodeStats.time.self).to.be.within(0, 2000000); //2ms in nanoseconds + }); + + let c1Stats = stats[3]; + expect(c1Stats.mystats).to.eql({ + x: 3, + y: 4, + }); + } + + function assertTreeValidAPI(name, tree) { + let depthFirstNames = Array.from(tree.dfsIterator()).map((x) => x.label.name); + expect(depthFirstNames, 'depth first name order').to.eql([name, 'a', 'b1', 'c1', 'b2', 'c2', 'd1', 'c3']); + + let breadthFirstNames = Array.from(tree.bfsIterator()).map((x) => x.label.name); + expect(breadthFirstNames, 'breadth first name order').to.eql([name, 'a', 'b1', 'b2', 'c1', 'c2', 'c3', 'd1']); + + let c2 = Array.from(tree.dfsIterator()).filter((x) => x.label.name === 'c2')[0]; + + let ancestorNames = Array.from(c2.ancestorsIterator()).map((x) => x.label.name); + expect(ancestorNames).to.eql(['b2', 'a', name]); + } + + function assertTreeValid(name, tree) { + assertTreeValidJSON(name, tree); + assertTreeValidAPI(name, tree); + } + + it('produces a valid tree for init', function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + makeTree('init'); + assertTreeValid('init', instrumentation._instrumentationTreeFor('init')); + }); + + it('produces a valid tree for build', function () { + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + makeTree('build'); + assertTreeValid('build', instrumentation._instrumentationTreeFor('build')); + }); + }); + + describe('summaries', function () { + let instrTree; + let instrumentation; + + beforeEach(function () { + instrumentation = new Instrumentation({ ui: new MockUI() }); + + let heimdall = new Heimdall(); + let root; + let a1 = heimdall.start({ name: 'a1', broccoliNode: true, broccoliCachedNode: false }); + root = heimdall.current; + let b1 = heimdall.start({ name: 'b1' }); + let c1 = heimdall.start({ name: 'c1', broccoliNode: true, broccoliCachedNode: true }); + c1.stop(); + let c2 = heimdall.start({ name: 'c2', broccoliNode: true, broccoliCachedNode: false }); + c2.stop(); + b1.stop(); + a1.stop(); + + instrTree = heimdallGraph.loadFromNode(root); + process.env.EMBER_CLI_INSTRUMENTATION = '1'; + }); + + describe('._buildSummary', function () { + it('computes initial build sumamries', function () { + let result = { + directory: 'tmp/someplace', + outputChanges: ['assets/foo.js', 'assets/foo.css'], + }; + let annotation = { + type: 'initial', + }; + + let summary = instrumentation._buildSummary(instrTree, result, annotation); + + expect(Object.keys(summary)).to.eql(['build', 'platform', 'output', 'totalTime', 'buildSteps']); + + expect(summary.build).to.eql({ + type: 'initial', + count: 0, + outputChangedFiles: ['assets/foo.js', 'assets/foo.css'], + }); + + expect(summary.output).to.eql('tmp/someplace'); + expect(summary.buildSteps).to.eql(2); // 2 uncached broccli nodes + expect(summary.totalTime).to.be.within(0, 2000000); //2ms (in nanoseconds) + expect(summary).to.have.nested.property('platform.name', process.platform); + expect(Object.keys(summary.platform)).to.eql(['name', ...Object.keys(hwinfo), 'collectionTime']); + }); + + it('computes rebuild summaries', function () { + let result = { + directory: 'tmp/someplace', + outputChanges: ['assets/foo.js', 'assets/foo.css'], + }; + let annotation = { + type: 'rebuild', + primaryFile: 'a', + changedFiles: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'], + }; + + instrumentation.start('build'); + instrumentation.stopAndReport('build', result, annotation); + instrumentation.start('build'); + instrumentation.stopAndReport('build', result, annotation); + + let summary = instrumentation._buildSummary(instrTree, result, annotation); + + expect(Object.keys(summary)).to.eql(['build', 'platform', 'output', 'totalTime', 'buildSteps']); + + expect(summary.build).to.eql({ + type: 'rebuild', + count: 2, + outputChangedFiles: ['assets/foo.js', 'assets/foo.css'], + primaryFile: 'a', + changedFileCount: 11, + changedFiles: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], + }); + + expect(summary.output).to.eql('tmp/someplace'); + expect(summary.buildSteps).to.eql(2); // 2 uncached broccli nodes + expect(summary.totalTime).to.be.within(0, 2000000); //2ms (in nanoseconds) + expect(summary).to.have.nested.property('platform.name', process.platform); + expect(Object.keys(summary.platform)).to.eql(['name', ...Object.keys(hwinfo), 'collectionTime']); + }); + }); + + describe('._initSummary', function () { + it('computes an init summary', function () { + let summary = instrumentation._initSummary(instrTree); + + expect(Object.keys(summary)).to.eql(['totalTime', 'platform']); + + expect(summary.totalTime).to.be.within(0, 2000000); //2ms (in nanoseconds) + expect(summary).to.have.nested.property('platform.name', process.platform); + expect(Object.keys(summary.platform)).to.eql(['name', ...Object.keys(hwinfo), 'collectionTime']); + }); + }); + + describe('._commandSummary', function () { + it('computes a command summary', function () { + let summary = instrumentation._commandSummary(instrTree, 'build', ['--like', '--whatever']); + + expect(Object.keys(summary)).to.eql(['name', 'args', 'totalTime', 'platform']); + + expect(summary.name).to.equal('build'); + expect(summary.args).to.eql(['--like', '--whatever']); + expect(summary.totalTime).to.be.within(0, 2000000); //2ms (in nanoseconds) + expect(summary).to.have.nested.property('platform.name', process.platform); + expect(Object.keys(summary.platform)).to.eql(['name', ...Object.keys(hwinfo), 'collectionTime']); + }); + }); + + describe('._shutdownSummary', function () { + it('computes a shutdown summary', function () { + let summary = instrumentation._shutdownSummary(instrTree); + + expect(Object.keys(summary)).to.eql(['totalTime', 'platform']); + + expect(summary.totalTime).to.be.within(0, 2000000); //2ms (in nanoseconds) + expect(summary).to.have.nested.property('platform.name', process.platform); + expect(Object.keys(summary.platform)).to.eql(['name', ...Object.keys(hwinfo), 'collectionTime']); + }); + }); + }); +}); diff --git a/tests/unit/models/package-info-cache/node-module-list-test.js b/tests/unit/models/package-info-cache/node-module-list-test.js new file mode 100644 index 0000000000..0afe5f184d --- /dev/null +++ b/tests/unit/models/package-info-cache/node-module-list-test.js @@ -0,0 +1,45 @@ +'use strict'; + +const { expect } = require('chai'); +const NodeModulesList = require('../../../../lib/models/package-info-cache/node-modules-list'); + +describe('models/package-info-cache/node-modules-list-test', function () { + it('correctly constructs', function () { + expect(new NodeModulesList()).to.be.ok; + expect(new NodeModulesList('/some/path')).to.be.ok; + }); + + describe('.NULL', function () { + it('returns a singleton, deeply frozen NodeMoudlesList', function () { + expect(NodeModulesList.NULL).to.equal(NodeModulesList.NULL); + expect(NodeModulesList.NULL).to.be.frozen; + expect(NodeModulesList.NULL.entries).to.be.frozen; + expect(NodeModulesList.NULL.errors).to.be.frozen; + expect(NodeModulesList.NULL.errors.errors).to.be.frozen; + }); + }); + + describe('findPackage', function () { + it('works with no entries', function () { + let list = new NodeModulesList(); + expect(list.findPackage('omg')).to.eql(null); + }); + + it('supports basic entries (missing, present, scoped)', function () { + let list = new NodeModulesList(); + let scoped = new NodeModulesList(); + let omg = { name: 'omg' }; + let scopedOmg = { name: 'omg' }; + scoped.addEntry('omg', scopedOmg); + + list.addEntry('omg', omg); + list.addEntry('@thescope', scoped); + + expect(list.findPackage('omg')).to.eql(omg); + expect(list.findPackage('nope')).to.eql(null); + expect(list.findPackage('@thescope/omg')).to.eql(scopedOmg); + expect(list.findPackage('@thescope/nope')).to.eql(null); + expect(list.findPackage('@nope/nope')).to.eql(null); + }); + }); +}); diff --git a/tests/unit/models/package-info-cache/package-info-cache-test.js b/tests/unit/models/package-info-cache/package-info-cache-test.js new file mode 100644 index 0000000000..3b144b6532 --- /dev/null +++ b/tests/unit/models/package-info-cache/package-info-cache-test.js @@ -0,0 +1,645 @@ +'use strict'; + +const path = require('path'); +const { expect } = require('chai'); +const PackageInfo = require('../../../../lib/models/package-info-cache/package-info'); +const Project = require('../../../../lib/models/project'); +const addonFixturePath = path.resolve(__dirname, '../../../fixtures/addon'); +const MockUI = require('console-ui/mock'); +const MockCLI = require('../../../helpers/mock-cli'); +const FixturifyProject = require('../../../helpers/fixturify-project'); + +describe('models/package-info-cache/package-info-cache-test.js', function () { + let project, projectPath, packageJsonPath, packageContents, projectPackageInfo, resolvedFile, ui, cli, pic; + this.timeout(20000); + + beforeEach(function () { + ui = new MockUI(); + cli = new MockCLI({ ui }); + }); + + describe('lexicographically', function () { + it('works', function () { + expect( + [ + { name: 'c' }, + { foo: 2 }, + { name: 'z/b/z' }, + { name: 'z/b/d' }, + { foo: 1 }, + { name: 'z/a/d' }, + { name: 'z/a/c' }, + { name: 'b' }, + { name: 'z/a/d' }, + { name: 'a' }, + { foo: 3 }, + ].sort(PackageInfo.lexicographically) + ).to.eql([ + { name: 'a' }, + { name: 'b' }, + { name: 'c' }, + { name: 'z/a/c' }, + { name: 'z/a/d' }, + { name: 'z/a/d' }, + { name: 'z/b/d' }, + { name: 'z/b/z' }, + { foo: 2 }, + { foo: 1 }, + { foo: 3 }, + ]); + }); + }); + + describe('pushUnique', function () { + it('works (and does last write win)', function () { + let a = { name: 'a' }; + let b = { name: 'b' }; + let c = { name: 'c' }; + + let result = []; + [a, a, a, b, a, c, a, c].forEach((entry) => PackageInfo.pushUnique(result, entry)); + + expect(result).to.eql([b, a, c]); + }); + }); + + describe('packageInfo contents tests on valid project', function () { + let project, projectPath, packageJsonPath, packageContents, projectPackageInfo; + + beforeEach(function () { + projectPath = path.resolve(addonFixturePath, 'simple'); + packageJsonPath = path.join(projectPath, 'package.json'); + packageContents = require(packageJsonPath); + project = new Project(projectPath, packageContents, ui, cli); + let pic = project.packageInfoCache; + + projectPackageInfo = pic.getEntry(projectPath); + }); + + it('finds project PackageInfo entry for project root', function () { + expect(projectPackageInfo).to.exist; + }); + + it('projectPackageInfo has a "pkg" field', function () { + expect(projectPackageInfo.pkg).to.exist; + }); + + it('shows projectPackageInfo is considered valid', function () { + expect(projectPackageInfo.valid).to.be.true; + }); + + it('is a project, so it may have addons', function () { + expect(projectPackageInfo.mayHaveAddons).to.eql(true); + }); + + it('shows projectPackageInfo has cliInfo at ember-cli root dir', function () { + expect(projectPackageInfo.cliInfo).to.exist; + + let cliRealPath = projectPackageInfo.cliInfo.realPath; + let emberCliRealPath = path.resolve(`${projectPackageInfo.realPath}/../../../../`); + expect(cliRealPath).to.equal(emberCliRealPath); + }); + + it('shows projectPackageInfo has 1 error', function () { + expect(projectPackageInfo.hasErrors()).to.be.true; + + let errorArray = projectPackageInfo.errors.getErrors(); + + expect(errorArray.length).to.equal(1); + }); + + it('shows projectPackageInfo error is "3 dependencies missing"', function () { + let errorArray = projectPackageInfo.errors.getErrors(); + let error = errorArray[0]; + expect(error.type).to.equal('dependenciesMissing'); + expect(error.data.length).to.equal(3); + }); + + it('shows projectPackageInfo has 1 dependencyPackage', function () { + let dependencyPackages = projectPackageInfo.dependencyPackages; + + expect(dependencyPackages).to.exist; + expect(Object.keys(dependencyPackages).length).to.equal(1); + expect(dependencyPackages['something-else']).to.exist; + }); + + it('shows projectPackageInfo has 8 devDependencyPackages', function () { + let devDependencyPackages = projectPackageInfo.devDependencyPackages; + expect(devDependencyPackages).to.exist; + expect(Object.keys(devDependencyPackages).length).to.equal(8); + }); + + it('shows projectPackageInfo.devDependencyPackages + missing dependencies = project.devDependencies', function () { + let devDependencyPackages = projectPackageInfo.devDependencyPackages; + expect(devDependencyPackages).to.exist; + let devDependencyPackageNames = Object.keys(devDependencyPackages); + + let devDependencies = projectPackageInfo.pkg.devDependencies; + expect(devDependencies).to.exist; + let devDependencyNames = Object.keys(devDependencies).sort(); + + let errorArray = projectPackageInfo.errors.getErrors(); + let error = errorArray[0]; + expect(error.type).to.equal('dependenciesMissing'); + + let missingDependencies = error.data; + + let packageAndErrorNames = devDependencyPackageNames.concat(missingDependencies).sort(); + + expect(packageAndErrorNames).to.deep.equal(devDependencyNames); + }); + + it('shows projectPackageInfo has 2 in-repo addons', function () { + let inRepoAddons = projectPackageInfo.inRepoAddons; + + expect(inRepoAddons).to.exist; + expect(inRepoAddons.length).to.equal(2); + + expect(inRepoAddons[0].realPath.indexOf(`simple${path.sep}lib${path.sep}ember-super-button`)).to.be.above(0); + expect(inRepoAddons[0].pkg.name).to.equal('ember-super-button'); + + expect( + inRepoAddons[1].realPath.indexOf( + `simple${path.sep}lib${path.sep}ember-super-button${path.sep}lib${path.sep}ember-with-addon-main` + ) + ).to.be.above(0); + expect(inRepoAddons[1].pkg.name).to.equal('ember-with-addon-main'); + }); + + it('shows projectPackageInfo has 7 internal addon packages', function () { + let internalAddons = projectPackageInfo.internalAddons; + expect(internalAddons).to.exist; + expect(internalAddons.length).to.equal(7); + }); + + it('shows projectPackageInfo has 9 node-module entries', function () { + let nodeModules = projectPackageInfo.nodeModules; + expect(nodeModules).to.exist; + expect(nodeModules.entries).to.exist; + expect(Object.keys(nodeModules.entries).length).to.equal(9); + }); + + it('returns stable package infos for a package info representing the same addon', function () { + project.initializeAddons(); + + const findAddonsByName = (projectOrAddon, addonToFind, _foundAddons = []) => { + if (!projectOrAddon) { + return _foundAddons; + } + + projectOrAddon.addons.forEach((addon) => { + if (addon.name === addonToFind) { + _foundAddons.push(addon); + } + + findAddonsByName(addon, addonToFind, _foundAddons); + }); + + return _foundAddons; + }; + + const findAllInRepoPackageInfosByPredicate = (packageInfo, predicate, _foundPackageInfos = []) => { + if (predicate(packageInfo)) { + _foundPackageInfos.push(packageInfo); + } + + (packageInfo.inRepoAddons || []).forEach((addonPackageInfo) => + findAllInRepoPackageInfosByPredicate(addonPackageInfo, predicate, _foundPackageInfos) + ); + + return _foundPackageInfos; + }; + + let allAddonsWithAddonMain = findAddonsByName(project, 'ember-with-addon-main'); + + let projectAddonWithMainPackageInfo = findAllInRepoPackageInfosByPredicate( + project._packageInfo, + (packageInfo) => + typeof packageInfo.addonMainPath === 'string' && + packageInfo.addonMainPath.endsWith(path.join('ember-with-addon-main', 'lib', 'main.js')) + ); + + let allPackageInfosForAddonWithMain = [ + ...allAddonsWithAddonMain.map((addon) => addon._packageInfo), + ...projectAddonWithMainPackageInfo, + ]; + + let areAllPackageInfosEqual = allPackageInfosForAddonWithMain.every( + (packageInfo) => packageInfo === allPackageInfosForAddonWithMain[0] + ); + + expect(allAddonsWithAddonMain.length).to.equal(2); + expect(allPackageInfosForAddonWithMain.length).to.equal(4); + expect(areAllPackageInfosEqual).to.equal(true); + }); + }); + + describe('packageInfo', function () { + describe('project with invalid paths', function () { + let project, fixturifyProject; + beforeEach(function () { + // create a new ember-app + fixturifyProject = new FixturifyProject('simple-ember-app', '0.0.0', (project) => { + project.addAddon('ember-resolver', '^5.0.1'); + project.addAddon('ember-random-addon', 'latest'); + project.addAddon('loader.js', 'latest'); + project.addAddon('something-else', 'latest'); + project.addInRepoAddon('ember-super-button', 'latest', function (project) { + project.pkg['ember-addon'].paths = ['lib/herp-not-here']; + }); + project.addDevDependency('ember-cli', 'latest'); + project.addDevDependency('non-ember-thingy', 'latest'); + project.pkg['ember-addon'].paths.push('lib/no-such-path'); + }); + + fixturifyProject.writeSync(); + + project = fixturifyProject.buildProjectModel(Project); + }); + + afterEach(function () { + fixturifyProject.dispose(); + }); + + it('throws an error', function () { + expect(() => project.discoverAddons()).to.throw( + /specifies an invalid, malformed or missing addon at relative path 'lib[\\/]no-such-path'/ + ); + }); + }); + describe('valid project', function () { + let project, fixturifyProject; + before(function () { + // create a new ember-app + fixturifyProject = new FixturifyProject('simple-ember-app', '0.0.0', (project) => { + project.addAddon('ember-resolver', '^5.0.1'); + project.addAddon('ember-random-addon', 'latest', (addon) => { + addon.addAddon('other-nested-addon', 'latest', (addon) => { + addon.addAddon('ember-resolver', '*'); + addon.toJSON = function () { + const json = Object.getPrototypeOf(this).toJSON.call(this); + // here we introduce an empty folder in our node_modules. + json[this.name].node_modules['ember-resolver'] = {}; + return json; + }; + }); + }); + + project.addAddon('loader.js', 'latest'); + project.addAddon('something-else', 'latest'); + + project.addInRepoAddon('ember-super-button', 'latest'); + project.addDevDependency('ember-cli', 'latest'); + project.addDevDependency('non-ember-thingy', 'latest'); + }); + + fixturifyProject.writeSync(); + + project = fixturifyProject.buildProjectModel(Project); + project.discoverAddons(); + pic = project.packageInfoCache; + projectPackageInfo = pic.getEntry(path.join(fixturifyProject.root, 'simple-ember-app')); + }); + + after(function () { + fixturifyProject.dispose(); + }); + + it('was able to find ember-resolver even if an empty directory was left', function () { + const emberResolver = project.findAddonByName('ember-resolver'); + const nestedEmberResolver = project.findAddonByName('ember-random-addon').addons[0].addons[0]; + expect(emberResolver.name).to.eql('ember-resolver'); + expect(nestedEmberResolver.name).to.eql('ember-resolver'); + expect(emberResolver.root).to.eql(nestedEmberResolver.root); + }); + + it('has dependencies who have their mayHaveAddons correctly set', function () { + expect(projectPackageInfo.devDependencyPackages['non-ember-thingy']).to.have.property('mayHaveAddons', false); + expect(projectPackageInfo.devDependencyPackages['ember-cli']).to.have.property('mayHaveAddons', false); + expect(projectPackageInfo.dependencyPackages['loader.js']).to.have.property('mayHaveAddons', true); + expect(projectPackageInfo.dependencyPackages['ember-resolver']).to.have.property('mayHaveAddons', true); + expect(projectPackageInfo.dependencyPackages['ember-random-addon']).to.have.property('mayHaveAddons', true); + expect(projectPackageInfo.dependencyPackages['something-else']).to.have.property('mayHaveAddons', true); + }); + + it('validates projectPackageInfo', function () { + expect(projectPackageInfo).to.exist; + expect(projectPackageInfo.pkg).to.exist; + expect(projectPackageInfo.valid).to.be.true; + }); + + it('shows projectPackageInfo has 0 errors', function () { + expect(projectPackageInfo.hasErrors()).to.be.false; + expect(projectPackageInfo.errors.getErrors()).to.have.property('length', 0); + }); + + it('shows projectPackageInfo has 1 dependencyPackage', function () { + let dependencyPackages = projectPackageInfo.dependencyPackages; + + expect(dependencyPackages).to.exist; + expect(Object.keys(dependencyPackages).length).to.equal(4); + expect(dependencyPackages['something-else']).to.exist; + }); + + it('shows projectPackageInfo has 82devDependencyPackages', function () { + let devDependencyPackages = projectPackageInfo.devDependencyPackages; + + expect(devDependencyPackages).to.exist; + expect(Object.keys(devDependencyPackages).length).to.equal(2); + }); + + it('shows projectPackageInfo has 1 in-repo addon named "ember-super-button"', function () { + let inRepoAddons = projectPackageInfo.inRepoAddons; + + expect(inRepoAddons).to.exist; + expect(inRepoAddons.length).to.equal(1); + expect(inRepoAddons[0].realPath).to.contain(path.join('simple-ember-app', 'lib', 'ember-super-button')); + expect(inRepoAddons[0].pkg.name).to.equal('ember-super-button'); + }); + + it('shows projectPackageInfo has 7 internal addon packages', function () { + let internalAddons = projectPackageInfo.internalAddons; + + expect(internalAddons).to.exist; + expect(internalAddons.length).to.equal(7); + }); + + it('shows projectPackageInfo has 7 node-module entries', function () { + let nodeModules = projectPackageInfo.nodeModules; + + expect(nodeModules).to.exist; + expect(nodeModules.entries).to.exist; + expect(Object.keys(nodeModules.entries).length).to.equal(6); + }); + }); + }); + + describe('packageInfo contents tests on missing project', function () { + beforeEach(function () { + projectPath = path.resolve(addonFixturePath, 'fakepackage'); + + let deps = { + 'foo-bar': 'latest', + 'blah-blah': '1.0.0', + }; + + let devDeps = { + 'dev-foo-bar': 'latest', + }; + + packageContents = { + dependencies: deps, + devDependencies: devDeps, + }; + + project = new Project(projectPath, packageContents, ui, cli); + + pic = project.packageInfoCache; + projectPackageInfo = pic.getEntry(projectPath); + }); + + it('creates a packageInfo object for the missing path', function () { + expect(projectPackageInfo).to.exist; + }); + + it('has 3 errors', function () { + let errors = projectPackageInfo.errors; + expect(errors).to.exist; + expect(errors.hasErrors()).to.be.true; + expect(errors.getErrors().length).to.equal(3); + }); + + it('has a "packageDirectoryMissing" error', function () { + let errorArray = projectPackageInfo.errors.getErrors(); + let pkgDirMissingErr = errorArray.find(function (err) { + return err.type === 'packageDirectoryMissing'; + }); + expect(pkgDirMissingErr).to.exist; + expect(pkgDirMissingErr.data).to.equal(projectPath); + }); + + it('has empty "dependencyPackages" and "devDependencyPackages" objects', function () { + expect(projectPackageInfo.dependencyPackages).to.exist; + expect(projectPackageInfo.devDependencyPackages).to.exist; + expect(Object.keys(projectPackageInfo.dependencyPackages).length).to.equal(0); + expect(Object.keys(projectPackageInfo.devDependencyPackages).length).to.equal(0); + }); + }); + + describe('packageInfo contents tests on with-nested-addons project', function () { + beforeEach(function () { + projectPath = path.resolve(addonFixturePath, 'with-nested-addons'); + packageJsonPath = path.join(projectPath, 'package.json'); + packageContents = null; // there is no actual package.json + project = new Project(projectPath, packageContents, ui, cli); + + pic = project.packageInfoCache; + projectPackageInfo = pic.getEntry(projectPath); + }); + + it('shows projectPackageInfo has a "packageJsonMissing" error', function () { + let errorArray = projectPackageInfo.errors.getErrors(); + let pkgJsonMissingErr = errorArray.find(function (err) { + return err.type === 'packageJsonMissing'; + }); + expect(pkgJsonMissingErr).to.exist; + expect(pkgJsonMissingErr.data).to.equal(packageJsonPath); + }); + }); + + describe('packageInfo contents tests on external-dependency project', function () { + beforeEach(function () { + projectPath = path.resolve(addonFixturePath, 'external-dependency'); + packageJsonPath = path.join(projectPath, 'package.json'); + packageContents = require(packageJsonPath); + project = new Project(projectPath, packageContents, ui, cli); + + pic = project.packageInfoCache; + projectPackageInfo = pic.getEntry(projectPath); + }); + + it('shows projectPackageInfo finds a dependency above project root', function () { + expect(projectPackageInfo.dependencyPackages).to.exist; + + let emberCliStringUtilsPkgInfo = projectPackageInfo.dependencyPackages['ember-cli-string-utils']; + expect(emberCliStringUtilsPkgInfo).to.exist; + + let emberCliRealPath = path.resolve(`${projectPackageInfo.realPath}/../../../../`); + expect(emberCliStringUtilsPkgInfo.realPath).to.equal( + path.join( + emberCliRealPath, + // TODO: Make this package-manager agnostic? + 'node_modules/.pnpm/ember-cli-string-utils@1.1.0/node_modules/ember-cli-string-utils' + ) + ); + }); + + it('shows projectPackageInfo finds an external dependency involving a scope', function () { + expect(projectPackageInfo.dependencyPackages).to.exist; + + let restPkgInfo = projectPackageInfo.dependencyPackages['@pnpm/find-workspace-dir']; + expect(restPkgInfo).to.exist; + + let emberCliRealPath = path.resolve(`${projectPackageInfo.realPath}/../../../../`); + expect(restPkgInfo.realPath).to.equal( + // TODO: Make this package-manager agnostic? + path.join( + emberCliRealPath, + 'node_modules/.pnpm/@pnpm+find-workspace-dir@1000.1.5/node_modules/@pnpm/find-workspace-dir' + ) + ); + }); + }); + + describe('discoverProjectAddons', function () { + let fixturifyProject; + + afterEach(function () { + if (fixturifyProject) { + fixturifyProject.dispose(); + } + }); + + describe('within an addon', function () { + beforeEach(function () { + fixturifyProject = new FixturifyProject('external-dependency', '0.0.0', (project) => { + project.addDevDependency('ember-cli-string-utils', 'latest'); + project.addDevDependency('@octokit/rest', 'latest'); + project.addAddon('ember-cli-blueprint-test-helpers', 'latest'); + project.addAddon('c', 'latest'); + project.addAddon('a', 'latest'); + project.addAddon('b', 'latest'); + + project.addDevAddon('y', 'latest'); + project.addDevAddon('z', 'latest'); + project.addDevAddon('x', 'latest'); + + project.addInRepoAddon('t', 'latest'); + project.addInRepoAddon('s', 'latest'); + + project.pkg.keywords.push('ember-addon'); + project.pkg.keywords.push('ember-addon'); + }); + }); + + it('lock down dependency orderings', function () { + let project = fixturifyProject.buildProjectModel(); + + project.discoverAddons(); + + expect(Object.keys(project.addonPackages)).to.deep.equal([ + // itself + 'external-dependency', + + // dev dependencies + 'x', + 'y', + 'z', + + // dependencies + 'a', + 'b', + 'c', + 'ember-cli-blueprint-test-helpers', + + // in repo addons + 's', + 't', + ]); + }); + }); + }); + + describe('tests for projectPackageInfo.addonMainPath', function () { + let origPackageContents; + + beforeEach(function () { + projectPath = path.resolve(addonFixturePath, 'external-dependency'); + packageJsonPath = path.join(projectPath, 'package.json'); + // Because we allow the tests to modify packageContents, and the original + // 'require' of package contents will always return the same structure + // once it has been required, we must deep-copy that structure before letting + // the tests modify it, so they modify only the copy. + if (!origPackageContents) { + origPackageContents = require(packageJsonPath); + origPackageContents['ember-addon'] = Object.create(null); + } + + packageContents = JSON.parse(JSON.stringify(origPackageContents)); + }); + + it('adds .js if not present', function () { + packageContents['ember-addon']['main'] = 'index'; + + project = new Project(projectPath, packageContents, ui, cli); + projectPackageInfo = project.packageInfoCache.getEntry(projectPath); + + resolvedFile = path.basename(projectPackageInfo.addonMainPath); + expect(resolvedFile).to.equal('index.js'); + }); + + it("doesn't add .js if it is .js", function () { + packageContents['ember-addon']['main'] = 'index.js'; + + project = new Project(projectPath, packageContents, ui, cli); + projectPackageInfo = project.packageInfoCache.getEntry(projectPath); + + resolvedFile = path.basename(projectPackageInfo.addonMainPath); + expect(resolvedFile).to.equal('index.js'); + }); + + it("doesn't add .js if it has another extension", function () { + packageContents['ember-addon']['main'] = 'index.coffee'; + + project = new Project(projectPath, packageContents, ui, cli); + projectPackageInfo = project.packageInfoCache.getEntry(projectPath); + + resolvedFile = path.basename(projectPackageInfo.addonMainPath); + expect(resolvedFile).to.equal('index.coffee'); + }); + + it('allows lookup of existing non-`index.js` `main` entry points', function () { + delete packageContents['ember-addon']; + packageContents['main'] = 'some/other/path.js'; + + project = new Project(projectPath, packageContents, ui, cli); + projectPackageInfo = project.packageInfoCache.getEntry(projectPath); + + resolvedFile = projectPackageInfo.addonMainPath; + expect(resolvedFile).to.equal(path.join(projectPath, 'some/other/path.js')); + }); + + it('fails invalid other `main` entry points', function () { + delete packageContents['ember-addon']; + packageContents['main'] = 'some/other/non-existent-file.js'; + + project = new Project(projectPath, packageContents, ui, cli); + projectPackageInfo = project.packageInfoCache.getEntry(projectPath); + + expect(projectPackageInfo.hasErrors()).to.be.true; + expect(projectPackageInfo.errors.getErrors().length).to.equal(1); + let error = projectPackageInfo.errors.getErrors()[0]; + expect(error.type).to.equal('emberAddonMainMissing'); + }); + + it('falls back to `index.js` if `main` and `ember-addon` are not found', function () { + delete packageContents['ember-addon']; + + project = new Project(projectPath, packageContents, ui, cli); + projectPackageInfo = project.packageInfoCache.getEntry(projectPath); + + resolvedFile = projectPackageInfo.addonMainPath; + expect(resolvedFile).to.equal(path.join(projectPath, 'index.js')); + }); + + it('falls back to `index.js` if `main` and `ember-addon.main` are not found', function () { + delete packageContents['ember-addon'].main; + + project = new Project(projectPath, packageContents, ui, cli); + projectPackageInfo = project.packageInfoCache.getEntry(projectPath); + + resolvedFile = projectPackageInfo.addonMainPath; + expect(resolvedFile).to.equal(path.join(projectPath, 'index.js')); + }); + }); +}); diff --git a/tests/unit/models/per-bundle-addon-cache/addon-proxy-test.js b/tests/unit/models/per-bundle-addon-cache/addon-proxy-test.js new file mode 100644 index 0000000000..6d79af9347 --- /dev/null +++ b/tests/unit/models/per-bundle-addon-cache/addon-proxy-test.js @@ -0,0 +1,48 @@ +'use strict'; + +const { expect } = require('chai'); + +const { getAddonProxy } = require('../../../../lib/models/per-bundle-addon-cache/addon-proxy'); +const { TARGET_INSTANCE } = require('../../../../lib/models/per-bundle-addon-cache/target-instance'); + +describe('Unit | addon-proxy-test', function () { + it('it allows a patched `preprocessJs` and is never set on the original addon instance', function () { + const realAddon = { + addons: ['foo'], + preprocessJs() {}, + }; + + const proxy1 = getAddonProxy({ [TARGET_INSTANCE]: realAddon }, {}); + const proxy2 = getAddonProxy({ [TARGET_INSTANCE]: realAddon }, {}); + + const originalPreprocessJs1 = proxy1.preprocessJs; + const originalPreprocessJs2 = proxy2.preprocessJs; + + proxy1.preprocessJs = () => {}; + + expect(proxy1[TARGET_INSTANCE].preprocessJs).to.equal( + realAddon.preprocessJs, + "original addon's `preprocessJs` has not been modified" + ); + + proxy2.preprocessJs = () => {}; + + expect(proxy2[TARGET_INSTANCE].preprocessJs).to.equal( + realAddon.preprocessJs, + "original addon's `preprocessJs` has not been modified" + ); + + proxy1.preprocessJs(); + + // restore original + proxy1.preprocessJs = originalPreprocessJs1; + + proxy2.preprocessJs(); + + // restore original + proxy2.preprocessJs = originalPreprocessJs2; + + proxy1.preprocessJs(); + proxy2.preprocessJs(); + }); +}); diff --git a/tests/unit/models/per-bundle-addon-cache/cache-bundle-hosts-test.js b/tests/unit/models/per-bundle-addon-cache/cache-bundle-hosts-test.js new file mode 100644 index 0000000000..add1665887 --- /dev/null +++ b/tests/unit/models/per-bundle-addon-cache/cache-bundle-hosts-test.js @@ -0,0 +1,44 @@ +'use strict'; + +/** + * Tests for checking that the list of 'bundle hosts' in the cache is correct. + * A 'bundle host' is either the project or a lazy engine. + */ +const { expect } = require('chai'); +const Project = require('../../../../lib/models/project'); + +const { createStandardCacheFixture } = require('../../../../tests/helpers/per-bundle-addon-cache'); + +describe('Unit | per-bundle-addon-cache bundle host', function () { + let project; + + before('setup fixture', function setup() { + let fixture = createStandardCacheFixture(); + project = fixture.buildProjectModel(Project); + project.initializeAddons(); + }); + + it('Should have 4 inRepo addons in project', function () { + // project should contain 4 children, 3 of which are engines and 1 is a regular addon. + expect(project._packageInfo.inRepoAddons.length).to.equal(4); + }); + + it('project.perBundleAddonCache should exist', function () { + expect(project.perBundleAddonCache).to.exist; + }); + + it("Should have 0 bundle hosts since they're constructed lazily", function () { + const bundleHostCache = project.perBundleAddonCache.bundleHostCache; + expect(bundleHostCache.size).to.equal(0); + }); + + it('Should not have any addonInstanceCache entries', function () { + const bundleHostCache = project.perBundleAddonCache.bundleHostCache; + + for (const [key] of bundleHostCache) { + let value = bundleHostCache.get(key); + expect(value.addonInstanceCache && value.addonInstanceCache.size).to.equal(0); + expect(value.realPath).to.exist; + } + }); +}); diff --git a/tests/unit/models/per-bundle-addon-cache/enable-cache-test.js b/tests/unit/models/per-bundle-addon-cache/enable-cache-test.js new file mode 100644 index 0000000000..7a41c582c0 --- /dev/null +++ b/tests/unit/models/per-bundle-addon-cache/enable-cache-test.js @@ -0,0 +1,49 @@ +'use strict'; + +/** + * Tests for enabling and disabling per-bundle-addon-cache support + */ +const { expect } = require('chai'); +const { createStandardCacheFixture } = require('../../../../tests/helpers/per-bundle-addon-cache'); +const Project = require('../../../../lib/models/project'); + +function enablePerBundleAddonCache(explicitValue) { + // default is opt-out + if (explicitValue !== null && explicitValue !== undefined) { + process.env.EMBER_CLI_ADDON_INSTANCE_CACHING = explicitValue; + } else { + delete process.env.EMBER_CLI_ADDON_INSTANCE_CACHING; + } +} + +function disablePerBundleAddonCache() { + process.env.EMBER_CLI_ADDON_INSTANCE_CACHING = false; +} + +function createProject() { + let fixture = createStandardCacheFixture(); + let project = fixture.buildProjectModel(Project); + return project; +} + +describe('Unit | per-bundle-addon-cache enable caching', function () { + afterEach(function () { + enablePerBundleAddonCache(); + }); + + it('perBundleAddonCache should be set in Project if EMBER_CLI_ADDON_INSTANCE_CACHING is not false', function () { + enablePerBundleAddonCache('foo'); + let project = createProject(); + expect(project.perBundleAddonCache).to.exist; + + enablePerBundleAddonCache(); + project = createProject(); + expect(project.perBundleAddonCache).to.exist; + }); + + it('perBundleAddonCache should not be set in Project if EMBER_CLI_ADDON_INSTANCE_CACHING is false', function () { + disablePerBundleAddonCache(); + let project = createProject(); + expect(project.perBundleAddonCache).not.to.exist; + }); +}); diff --git a/tests/unit/models/per-bundle-addon-cache/proxy-test.js b/tests/unit/models/per-bundle-addon-cache/proxy-test.js new file mode 100644 index 0000000000..6f77cc9e74 --- /dev/null +++ b/tests/unit/models/per-bundle-addon-cache/proxy-test.js @@ -0,0 +1,1159 @@ +'use strict'; + +/** + * Tests for the various proxies and instances once the project has initialized + * its addons + */ +const { expect } = require('chai'); +const path = require('path'); +const FixturifyProject = require('../../../helpers/fixturify-project'); + +const { + findAddonCacheEntriesByName, + createStandardCacheFixture, + getAllAddonsByNameWithinHost, + areAllInstancesEqualWithinHost, + countAddons, +} = require('../../../../tests/helpers/per-bundle-addon-cache'); + +const Project = require('../../../../lib/models/project'); +const { TARGET_INSTANCE } = require('../../../../lib/models/per-bundle-addon-cache/target-instance'); + +describe('models/per-bundle-addon-cache', function () { + let fixturifyProject; + + beforeEach(function () { + fixturifyProject = new FixturifyProject('awesome-proj', '1.0.0'); + fixturifyProject.addDevDependency('ember-cli', '*'); + }); + + afterEach(function () { + fixturifyProject.dispose(); + }); + + it('simple case: bundle addon caching within a single project host', function () { + fixturifyProject.addInRepoAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + fixturifyProject.addInRepoAddon('foo-bar', '1.0.0', { + callback: (inRepoAddon) => { + inRepoAddon.pkg['ember-addon'].paths = ['../foo']; + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(areAllInstancesEqualWithinHost(project, 'foo')).to.be.true; + }); + + it('it should create multiple proxies within a project host', function () { + fixturifyProject.addInRepoAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + + for (let i = 0; i < 10; i++) { + fixturifyProject.addInRepoAddon(`foo-bar-${i}`, '1.0.0', { + callback: (inRepoAddon) => { + inRepoAddon.pkg['ember-addon'].paths = ['../foo']; + }, + }); + } + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(areAllInstancesEqualWithinHost(project, 'foo')).to.be.true; + }); + + it('it should create a proxy for a regular addon when added as a dependency to an in-repo addon', function () { + fixturifyProject.addAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + + for (let i = 0; i < 10; i++) { + fixturifyProject.addInRepoAddon(`foo-bar-${i}`, '1.0.0', { + callback: (inRepoAddon) => { + inRepoAddon.addReferenceDependency('foo', '1.0.0'); + }, + }); + } + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(areAllInstancesEqualWithinHost(project, 'foo')).to.be.true; + }); + + it('it should create a proxy for a regular addon when added as a dependency to a regular addon', function () { + fixturifyProject.addAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + + for (let i = 0; i < 10; i++) { + fixturifyProject.addAddon(`foo-bar-${i}`, '1.0.0', { + callback: (addon) => { + addon.addReferenceDependency('foo', '1.0.0'); + }, + }); + } + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(areAllInstancesEqualWithinHost(project, 'foo')).to.be.true; + }); + + it('it should create a proxy to a target "real addon" per host', function () { + fixturifyProject.addAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + + fixturifyProject.addInRepoEngine('in-repo-lazy-engine', '1.0.0', { + enableLazyLoading: true, + callback: (lazyEngine) => { + lazyEngine.addReferenceDependency('foo', '1.0.0'); + lazyEngine.addReferenceDependency('foo-bar', '1.0.0'); + }, + }); + + fixturifyProject.addAddon('foo-bar', '1.0.0', { + callback: (addon) => { + addon.addReferenceDependency('foo', '1.0.0'); + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(areAllInstancesEqualWithinHost(project, 'foo')).to.be.true; + + // addons within lazy engine host are cached + expect( + areAllInstancesEqualWithinHost( + project.addons.find((addon) => addon.name === 'in-repo-lazy-engine'), + 'foo' + ) + ).to.be.true; + }); + + describe('when `EMBER_ENGINES_ADDON_DEDUPE` is enabled', function () { + beforeEach(function () { + process.env.EMBER_ENGINES_ADDON_DEDUPE = true; + }); + + afterEach(function () { + delete process.env.EMBER_ENGINES_ADDON_DEDUPE; + }); + + it('it should create a proxy to a target "real addon" using the project host', function () { + fixturifyProject.addAddon('foo', '1.0.0', { allowCachingPerBundle: true }); + + fixturifyProject.addInRepoEngine('in-repo-lazy-engine', '1.0.0', { + enableLazyLoading: true, + callback: (lazyEngine) => { + lazyEngine.addReferenceDependency('foo', '1.0.0'); + lazyEngine.addReferenceDependency('foo-bar', '1.0.0'); + }, + }); + + fixturifyProject.addAddon('foo-bar', '1.0.0', { + callback: (addon) => { + addon.addReferenceDependency('foo', '1.0.0'); + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + // we use project addon instance as the "real addon" + expect(areAllInstancesEqualWithinHost(project, 'foo')).to.be.true; + + const { realAddon: realAddonForProject, proxies: proxiesForProject } = getAllAddonsByNameWithinHost( + project, + 'foo' + ); + const { proxies: proxiesForEngine } = getAllAddonsByNameWithinHost( + project.addons.find((addon) => addon.name === 'in-repo-lazy-engine'), + 'foo' + ); + + expect( + [...proxiesForProject, ...proxiesForEngine].every((proxy) => proxy[TARGET_INSTANCE] === realAddonForProject) + ).to.be.true; + }); + + it('addon with `allowCachingPerBundle`, 1 in each of 2 lazy engines; project also depends on this addon', function () { + fixturifyProject.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + fixturifyProject.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let counts = countAddons(project); + + expect(counts.byName['lazy-engine-a'].addons.length).to.equal(1); + expect(counts.byName['lazy-engine-b'].addons.length).to.equal(1); + + expect(counts.proxyCount).to.equal(2); + expect(project.perBundleAddonCache.numProxies).to.equal(2); + + expect(counts.byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(counts.byName['test-addon-a'].proxyCount).to.equal(2); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + + // project cache should be used + expect(cacheEntries).to.not.exist; + + const lazyEngineBPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + + // project cache should be used + expect(cacheEntries).to.not.exist; + }); + + it('2 lazy engines; each depend on two addons; project also depends on these addons, ensure project cache is used', function () { + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0', { + allowCachingPerBundle: true, + callback: (addon) => { + addon.pkg['ember-addon'].paths = ['../test-addon-b']; + }, + }); + + fixturifyProject.addInRepoAddon('test-addon-b', '1.0.0', { allowCachingPerBundle: true }); + + fixturifyProject.addInRepoEngine('lazy-engine-a', '1.0.0', { + allowCachingPerBundle: true, + enableLazyLoading: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a', '../test-addon-b']; + }, + }); + + fixturifyProject.addInRepoEngine('lazy-engine-b', '1.0.0', { + allowCachingPerBundle: true, + enableLazyLoading: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a', '../test-addon-b']; + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['lazy-engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-a'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-b'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-b'].proxyCount).to.equal(0); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(2); + + expect(byName['test-addon-b'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-b'].proxyCount).to.equal(3); + + const projectPkgInfo = project._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, projectPkgInfo, 'test-addon-a'); + expect(cacheEntries.length).to.equal(1, 'project cache should have an entry for `test-addon-a`'); + + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, projectPkgInfo, 'test-addon-b'); + expect(cacheEntries.length).to.equal(1, 'project cache should have an entry for `test-addon-b`'); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.deep.equal( + [], + 'should exist; lazy-engine A has opted-in to bundle caching, but should have no entries for `test-addon-a`' + ); + + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-b'); + expect(cacheEntries).to.deep.equal( + [], + 'should exist; lazy-engine A has opted-in to bundle caching, but should have no entries for `test-addon-b`' + ); + + const lazyEngineBPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.deep.equal( + [], + 'should exist; lazy engine B has opted into bundle caching, but should have no entries for `test-addon-a`' + ); + + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-b'); + expect(cacheEntries).to.deep.equal( + [], + 'should exist; lazy engine B has opted into bundle caching, but should have no entries for `test-addon-b`' + ); + }); + + it('should work for a common ancestor host that is not the project; i.e., another a lazy engine', function () { + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.addAddon('test-addon-a', '1.0.0', { + allowCachingPerBundle: true, + }); + + engine.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + engine.addEngine('lazy-engine-c', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['lazy-engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-a'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-b'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-b'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-c'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-c'].proxyCount).to.equal(0); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(2); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + + expect(cacheEntries.length).to.equal(1, 'should exist; lazy-engine A depends on a non-project addon'); + + const lazyEngineBPkgInfo = project.addons + .find((addon) => addon.name === 'lazy-engine-a') + .addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + + expect(cacheEntries).to.deep.equal( + [], + "lazy engine B's should exist, but be unusued; ultimately lazy engine A is responsible for bundling" + ); + + const lazyEngineCPkgInfo = project.addons + .find((addon) => addon.name === 'lazy-engine-a') + .addons.find((addon) => addon.name === 'lazy-engine-c')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineCPkgInfo, 'test-addon-a'); + + expect(cacheEntries).to.be.equal(null, 'should not exist; lazy-engine C has not opted-in to bundle caching'); + }); + + it('should work for a common ancestor host that is not the project where multiple hosts bundle the same addon', function () { + fixturifyProject.addInRepoEngine('test-addon-a', '1.0.0', { + allowCachingPerBundle: true, + }); + + fixturifyProject.pkg['ember-addon'].paths = []; + + fixturifyProject.addInRepoEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a']; + + engine.addInRepoEngine('lazy-engine-a-lazy-engine-dep', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../../../test-addon-a']; + }, + }); + }, + }); + + fixturifyProject.addInRepoEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a']; + + engine.addInRepoEngine('lazy-engine-b-lazy-engine-dep', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../../../test-addon-a']; + }, + }); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['lazy-engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-a'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-b'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-b'].proxyCount).to.equal(0); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(2); + expect(byName['test-addon-a'].proxyCount).to.equal(2); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + + expect(cacheEntries.length).to.equal(1, 'should exist; lazy-engine A depends on a non-project addon'); + + const lazyEngineBPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + + expect(cacheEntries.length).to.equal(1, 'should exist; lazy-engine B depends on a non-project addon'); + }); + + it('should work for a common ancestor host that is not the project with an addon bundled by a different host', function () { + fixturifyProject.addAddon('test-addon-a', '1.0.0', { + allowCachingPerBundle: true, + }); + + fixturifyProject.addAddon('test-addon-b', '1.0.0', (addon) => { + addon.addReferenceDependency('test-addon-a', '1.0.0'); + }); + + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + + engine.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + engine.addEngine('lazy-engine-c', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['lazy-engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-a'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-b'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-b'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-c'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-c'].proxyCount).to.equal(0); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(4); + + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, project._packageInfo, 'test-addon-a'); + + expect(cacheEntries.length).to.equal(1, 'project cache should be used'); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + + expect(cacheEntries.length).to.equal(0, 'lazy engine A cache should not be used'); + + const lazyEngineBPkgInfo = project.addons + .find((addon) => addon.name === 'lazy-engine-a') + .addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + + expect(cacheEntries).to.deep.equal([], 'should exist; lazy-engine B has opted-in to bundle caching'); + + expect(cacheEntries.length).to.equal(0, 'lazy engine B should have no entries; the project has bundled this'); + + const lazyEngineCPkgInfo = project.addons + .find((addon) => addon.name === 'lazy-engine-a') + .addons.find((addon) => addon.name === 'lazy-engine-c')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineCPkgInfo, 'test-addon-a'); + + expect(cacheEntries).to.be.equal(null, 'should not exist; lazy-engine C has not opted-in to bundle caching'); + }); + + it('should work for a common ancestor host that is not the project with an addon bundled by a different host when building a lazy engine independently', function () { + fixturifyProject.addAddon('test-addon-a', '1.0.0', { + allowCachingPerBundle: true, + }); + + fixturifyProject.addInRepoEngine('lazy-engine-a-project', '1.0.0', { + enableLazyLoading: true, + callback: (projectLazyEngine) => { + projectLazyEngine.addReferenceDependency('test-addon-a', '1.0.0'); + + projectLazyEngine.addAddon('test-addon-b', '1.0.0', { + allowCachingPerBundle: true, + callback: (addon) => { + addon.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + projectLazyEngine.addEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + + engine.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + allowCachingPerBundle: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + engine.addEngine('lazy-engine-c', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + }, + }); + }, + }); + + let project = fixturifyProject.buildProjectModelForInRepoAddon('lazy-engine-a-project'); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['lazy-engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-a'].proxyCount).to.equal( + 1, + 'engine should be added as a dependency to `ember-cli`, so we should have 1 proxy' + ); + + expect(byName['lazy-engine-b'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-b'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-c'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-c'].proxyCount).to.equal(0); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(5); + + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, project._packageInfo, 'test-addon-a'); + + expect(cacheEntries.length).to.equal(1, 'project cache should not be used'); + + const lazyEngineAPkgInfo = project.addons + .find((addon) => addon.name === 'lazy-engine-a-project') + .addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + + expect(cacheEntries.length).to.equal(0, 'lazy engine A cache should not be used'); + + const lazyEngineBPkgInfo = project.addons + .find((addon) => addon.name === 'lazy-engine-a-project') + .addons.find((addon) => addon.name === 'lazy-engine-a') + .addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + + expect(cacheEntries).to.deep.equal([], 'should exist; lazy-engine B has opted-in to bundle caching'); + + expect(cacheEntries.length).to.equal(0, 'lazy engine B should have no entries; the project has bundled this'); + + const lazyEngineCPkgInfo = project.addons + .find((addon) => addon.name === 'lazy-engine-a-project') + .addons.find((addon) => addon.name === 'lazy-engine-a') + .addons.find((addon) => addon.name === 'lazy-engine-c')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineCPkgInfo, 'test-addon-a'); + + expect(cacheEntries).to.be.equal(null, 'should not exist; lazy-engine C has not opted-in to bundle caching'); + }); + }); + + describe('proxy checks with addon counts', function () { + it('no `allowCachingPerBundle` set, no proxies, verify instance counts', function () { + let fixture = createStandardCacheFixture(); + let project = fixture.buildProjectModel(Project); + project.initializeAddons(); + + let counts = countAddons(project); + + expect(counts.proxyCount).to.equal(0); + expect(counts.byName['test-addon-a'].addons.length).to.equal(1); + expect(counts.byName['test-addon-dep'].addons.length).to.equal(2); + expect(counts.byName['test-engine-dep'].addons.length).to.equal(3); + expect(counts.byName['lazy-engine-a'].addons.length).to.equal(1); + expect(counts.byName['lazy-engine-b'].addons.length).to.equal(1); + expect(counts.byName['regular-engine-c'].addons.length).to.equal(1); + + // addon cache should also have 0 proxies. test-addon-b was the only addon marked as cacheable, + // so it will end up in the count of addon instances for the addon cache, but have no proxies. + expect(project.perBundleAddonCache.numProxies).to.equal(0); + }); + + it('addon with allowCachingPerBundle, 1 instance, the rest proxies', function () { + // PROJ to TAA, TAB, TAC and TAD. TAB, TAC and TAD have TAA underneath. + fixturifyProject.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + fixturifyProject.addAddon('test-addon-b', '1.0.0', { + callback: (addon) => { + addon.addReferenceDependency('test-addon-a', '*'); + }, + }); + + fixturifyProject.addAddon('test-addon-c', '1.0.0', { + callback: (addon) => { + addon.addReferenceDependency('test-addon-a', '*'); + }, + }); + + fixturifyProject.addAddon('test-addon-d', '1.0.0', { + callback: (addon) => { + addon.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let counts = countAddons(project); + expect(counts.proxyCount).to.equal(3); + expect(project.perBundleAddonCache.numProxies).to.equal(3); + expect(counts.byName['test-addon-a'].addons.length).to.equal(4); + + expect(counts.byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(counts.byName['test-addon-a'].proxyCount).to.equal(3); + + expect(counts.byName['test-addon-b'].addons.length).to.equal(1); + expect(counts.byName['test-addon-c'].addons.length).to.equal(1); + }); + + it('addon with `allowCachingPerBundle`, 1 in lazy engine, one in regular', function () { + // PROJ to LEA, REB, LEA and REB both depend on TAA + // Neither instance of test-addon-a is declared in the project, but the one in engine B + // will be 'owned' by Project as far as PerBundleAddonCache is concerned. + // Should end with 2 instances of test-addon-a, one in PROJECT, one in lazy-engine-a, + // and no proxies. + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + }, + }); + + fixturifyProject.addEngine('regular-engine-b', '1.0.0', { + callback: (engine) => { + engine.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let counts = countAddons(project); + + expect(counts.byName['lazy-engine-a'].addons.length).to.equal(1); + expect(counts.byName['regular-engine-b'].addons.length).to.equal(1); + + expect(counts.proxyCount).to.equal(0); + expect(project.perBundleAddonCache.numProxies).to.equal(0); + + expect(counts.byName['test-addon-a'].realAddonInstanceCount).to.equal(2); + expect(counts.byName['test-addon-a'].proxyCount).to.equal(0); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, project._packageInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + }); + + it('addon with allowCachingPerBundle, 1 in each of 2 lazy engines', function () { + // Same as above, but regular-engine-b is now lazy-engine-b + // Should have 2 instances, 1 in LEA, 1 in LEB, separate paths. + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + }, + }); + + fixturifyProject.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let counts = countAddons(project); + + expect(counts.byName['lazy-engine-a'].addons.length).to.equal(1); + expect(counts.byName['lazy-engine-b'].addons.length).to.equal(1); + + expect(counts.proxyCount).to.equal(0); + expect(project.perBundleAddonCache.numProxies).to.equal(0); + + expect(counts.byName['test-addon-a'].realAddonInstanceCount).to.equal(2); + expect(counts.byName['test-addon-a'].proxyCount).to.equal(0); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + + const lazyEngineBPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + }); + + it('addon with `allowCachingPerBundle`, 1 in each of 2 lazy engines; project also depends on this addon', function () { + fixturifyProject.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + + fixturifyProject.addEngine('lazy-engine-a', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a'); + }, + }); + + fixturifyProject.addEngine('lazy-engine-b', '1.0.0', { + enableLazyLoading: true, + callback: (engine) => { + engine.addReferenceDependency('test-addon-a'); + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let counts = countAddons(project); + + expect(counts.byName['lazy-engine-a'].addons.length).to.equal(1); + expect(counts.byName['lazy-engine-b'].addons.length).to.equal(1); + + expect(counts.proxyCount).to.equal(0); + expect(project.perBundleAddonCache.numProxies).to.equal(0); + + expect(counts.byName['test-addon-a'].realAddonInstanceCount).to.equal(3); + expect(counts.byName['test-addon-a'].proxyCount).to.equal(0); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + + const lazyEngineBPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + }); + + it('addon with allowCachingPerBundle, 2 regular engines - cache entries in project but not declared there', function () { + // Project declares an in-repo addon TAA. Then remove the ember-addon.paths entry so the project + // "doesn't know" about it but it's available for engines. Declare 2 non-lazy in-repo engines. + // Then have them add a shared in-repo dependency to TAA, with the path pointing to the one in + // PROJ/lib (i.e. '../test-addon-a') + // Should have 1 instance, 1 proxy, both in project. + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + fixturifyProject.pkg['ember-addon'].paths = []; // remove the 'dependency' (file still exists) + + fixturifyProject.addInRepoEngine('regular-engine-a', '1.0.0', { + enableLazyLoading: false, + shouldShareDependencies: true, + callback: (inRepoEngine) => { + inRepoEngine.pkg['ember-addon'].paths = ['../test-addon-a']; + }, + }); + + fixturifyProject.addInRepoEngine('regular-engine-b', '1.0.0', { + enableLazyLoading: false, + shouldShareDependencies: true, + callback: (inRepoEngine) => { + inRepoEngine.pkg['ember-addon'].paths = ['../test-addon-a']; + }, + }); + + fixturifyProject.writeSync(); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { proxyCount, byName } = countAddons(project); + + expect(byName['regular-engine-a'].addons.length).to.equal(1); + expect(byName['regular-engine-b'].addons.length).to.equal(1); + + expect(proxyCount).to.equal(1); + expect(project.perBundleAddonCache.numProxies).to.equal(1); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(1); + + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, project._packageInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + }); + + it('addon with allowCachingPerBundle, 2 regular engines - cache entries in project (also declared there)', function () { + // Same as above, now both are regular engines. + // Should have 1 instance, 2 proxies, both in project. + fixturifyProject.addAddon('test-addon-a', '1.0.0', { allowCachingPerBundle: true }); + + fixturifyProject.addEngine('regular-engine-a', '1.0.0', { + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + fixturifyProject.addEngine('regular-engine-b', '1.0.0', { + callback: (engine) => { + engine.addReferenceDependency('test-addon-a', '1.0.0'); + }, + }); + + fixturifyProject.writeSync(); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { proxyCount, byName } = countAddons(project); + + expect(byName['regular-engine-a'].addons.length).to.equal(1); + expect(byName['regular-engine-b'].addons.length).to.equal(1); + + expect(proxyCount).to.equal(2); + expect(project.perBundleAddonCache.numProxies).to.equal(2); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(2); + + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, project._packageInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + }); + + it('2 lazy engines; each depend on two addons; ensure that each lazy engine has proxy for subsequent instantiations of duplicate addons', function () { + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0', { + allowCachingPerBundle: true, + callback: (addon) => { + addon.pkg['ember-addon'].paths = ['../test-addon-b']; + }, + }); + + fixturifyProject.addInRepoAddon('test-addon-b', '1.0.0', { allowCachingPerBundle: true }); + fixturifyProject.pkg['ember-addon'].paths = []; // project now 'doesn't know' about test-addon-b + + fixturifyProject.addInRepoEngine('lazy-engine-a', '1.0.0', { + allowCachingPerBundle: true, + enableLazyLoading: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a', '../test-addon-b']; + }, + }); + + fixturifyProject.addInRepoEngine('lazy-engine-b', '1.0.0', { + allowCachingPerBundle: true, + enableLazyLoading: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a', '../test-addon-b']; + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['lazy-engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-a'].proxyCount).to.equal(0); + + expect(byName['lazy-engine-b'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-b'].proxyCount).to.equal(0); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(2); + expect(byName['test-addon-a'].proxyCount).to.equal(0); + + expect(byName['test-addon-b'].realAddonInstanceCount).to.equal(2); + expect(byName['test-addon-b'].proxyCount).to.equal(2); + + const lazyEngineAPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-a')._packageInfo; + let cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineAPkgInfo, 'test-addon-b'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + + const lazyEngineBPkgInfo = project.addons.find((addon) => addon.name === 'lazy-engine-b')._packageInfo; + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-a'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + + cacheEntries = findAddonCacheEntriesByName(project.perBundleAddonCache, lazyEngineBPkgInfo, 'test-addon-b'); + expect(cacheEntries).to.exist; + expect(cacheEntries.length).to.equal(1); + }); + + it('2 regular engines; each depend on two addons; ensure that project cache is used', function () { + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0', { + allowCachingPerBundle: true, + callback: (addon) => { + addon.pkg['ember-addon'].paths = ['../test-addon-b']; + }, + }); + + fixturifyProject.addInRepoAddon('test-addon-b', '1.0.0', { allowCachingPerBundle: true }); + fixturifyProject.pkg['ember-addon'].paths = []; + + fixturifyProject.addInRepoEngine('engine-a', '1.0.0', { + allowCachingPerBundle: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a', '../test-addon-b']; + }, + }); + + fixturifyProject.addInRepoEngine('engine-b', '1.0.0', { + allowCachingPerBundle: true, + callback: (engine) => { + engine.pkg['ember-addon'].paths = ['../test-addon-a', '../test-addon-b']; + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['engine-a'].proxyCount).to.equal(0); + + expect(byName['engine-b'].realAddonInstanceCount).to.equal(1); + expect(byName['engine-b'].proxyCount).to.equal(0); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(1); + + expect(byName['test-addon-b'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-b'].proxyCount).to.equal(2); + }); + + it('multiple references to a single lazy engine that has opted-in to `allowCachingPerBundle`', function () { + fixturifyProject.addInRepoEngine('lazy-engine-a', '1.0.0', { + allowCachingPerBundle: true, + enableLazyLoading: true, + }); + + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0', { + callback: (addon) => { + addon.pkg['ember-addon'].paths = ['../lazy-engine-a']; + }, + }); + + fixturifyProject.addInRepoAddon('test-addon-b', '1.0.0', { + callback: (addon) => { + addon.pkg['ember-addon'].paths = ['../lazy-engine-a']; + }, + }); + + fixturifyProject.addInRepoAddon('test-addon-c', '1.0.0', { + callback: (addon) => { + addon.pkg['ember-addon'].paths = ['../lazy-engine-a']; + }, + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['lazy-engine-a'].realAddonInstanceCount).to.equal(1); + expect(byName['lazy-engine-a'].proxyCount).to.equal(3); + + expect(areAllInstancesEqualWithinHost(project, 'lazy-engine-a')).to.be.true; + }); + + it('uses the custom `per-bundle-addon-cache.js` util if it exists', function () { + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0'); + + fixturifyProject.addInRepoAddon('test-addon-b', '1.0.0', (addon) => { + addon.pkg['ember-addon'].paths = ['../test-addon-a']; + }); + + fixturifyProject.pkg['ember-addon'].perBundleAddonCacheUtil = './per-bundle-addon-cache.js'; + + fixturifyProject.files['per-bundle-addon-cache.js'] = ` +'use strict'; + +function allowCachingPerBundle({ addonEntryPointModule }) { + if (addonEntryPointModule.name === 'test-addon-a') { + return true; + } + + return false; +} + +module.exports = { + allowCachingPerBundle, +}; + `; + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(1); + }); + + it('throws an error if the provided `perBundleAddonCacheUtil` does not exist', function () { + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0'); + + fixturifyProject.addInRepoAddon('test-addon-b', '1.0.0', (addon) => { + addon.pkg['ember-addon'].paths = ['../test-addon-a']; + }); + + fixturifyProject.pkg['ember-addon'].perBundleAddonCacheUtil = './per-bundle-addon-cache.js'; + + expect(() => { + fixturifyProject.buildProjectModel(); + }).to.throw( + '[ember-cli] the provided `./per-bundle-addon-cache.js` for `ember-addon.perBundleAddonCacheUtil` does not exist' + ); + }); + + it('bundle caching works for addons that opt-in via `prototype.allowCachingPerBundle`', function () { + fixturifyProject.addInRepoAddon('test-addon-a', '1.0.0', { + addonEntryPoint: ` + function Addon() {} + Addon.prototype.allowCachingPerBundle = true; + Addon.prototype.pkg = require('./package.json'); + Addon.prototype.name = 'test-addon-a'; + module.exports = Addon; + `, + }); + + fixturifyProject.addInRepoAddon('test-addon-b', '1.0.0', (addon) => { + addon.pkg['ember-addon'].paths = ['../test-addon-a']; + }); + + let project = fixturifyProject.buildProjectModel(); + project.initializeAddons(); + + let { byName } = countAddons(project); + + expect(byName['test-addon-a'].realAddonInstanceCount).to.equal(1); + expect(byName['test-addon-a'].proxyCount).to.equal(1); + }); + }); + + describe('validation', function () { + function findAllAddonsByName(projectOrAddon, addonToFind, _foundAddons = []) { + projectOrAddon.addons.forEach((addon) => { + if (addon.name === addonToFind) { + _foundAddons.push(addon); + } + + findAllAddonsByName(addon, addonToFind, _foundAddons); + }); + + return _foundAddons; + } + + it('does not throw if the addon returns a stable cache key', function () { + fixturifyProject.addInRepoAddon('foo', '1.0.0', { + allowCachingPerBundle: true, + }); + + fixturifyProject.addInRepoAddon('foo-bar', '1.0.0', { + callback: (inRepoAddon) => { + inRepoAddon.pkg['ember-addon'].paths = ['../foo']; + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + findAllAddonsByName(project, 'foo').forEach((addon) => { + addon.cacheKeyForTree(); + addon.cacheKeyForTree(); + + addon.cacheKeyForTree('addon'); + addon.cacheKeyForTree('addon'); + }); + }); + + it('throws an error for addons that do not have stable cache keys', function () { + fixturifyProject.addInRepoAddon('foo', '1.0.0', { + allowCachingPerBundle: true, + additionalContent: `init() {this._super.init.apply(this, arguments); this.current = 0;}, cacheKeyForTree() {return this.current++;},`, + }); + + fixturifyProject.addInRepoAddon('foo-bar', '1.0.0', { + callback: (inRepoAddon) => { + inRepoAddon.pkg['ember-addon'].paths = ['../foo']; + }, + }); + + fixturifyProject.writeSync(); + let project = fixturifyProject.buildProjectModel(); + + project.initializeAddons(); + + expect(() => { + findAllAddonsByName(project, 'foo').forEach((addon) => { + addon.cacheKeyForTree(); + addon.cacheKeyForTree(); + }); + }).to.throw( + `[ember-cli] addon bundle caching can only be used on addons that have stable cache keys (previously \`2\`, now \`3\`; for addon \`foo\` located at \`${path.join( + fixturifyProject.baseDir, + 'lib/foo' + )}\`)` + ); + }); + }); +}); diff --git a/tests/unit/models/project-test.js b/tests/unit/models/project-test.js index e309698168..bcf9bcbd04 100644 --- a/tests/unit/models/project-test.js +++ b/tests/unit/models/project-test.js @@ -1,70 +1,161 @@ 'use strict'; -var path = require('path'); -var Project = require('../../../lib/models/project'); -var Addon = require('../../../lib/models/addon'); -var stub = require('../../helpers/stub').stub; -var tmp = require('../../helpers/tmp'); -var touch = require('../../helpers/file-utils').touch; -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); - -var versionUtils = require('../../../lib/utilities/version-utils'); -var emberCLIVersion = versionUtils.emberCLIVersion; - -describe('models/project.js', function() { - var project, projectPath; - - afterEach(function() { - if (project) { project = null; } +const path = require('path'); +const fs = require('fs-extra'); +const Project = require('../../../lib/models/project'); +const Addon = require('../../../lib/models/addon'); +const tmp = require('../../helpers/tmp'); +const touch = require('../../helpers/file-utils').touch; +const { expect } = require('chai'); +const emberCLIVersion = require('../../../lib/utilities/version-utils').emberCLIVersion; +const td = require('testdouble'); +const MockCLI = require('../../helpers/mock-cli'); + +describe('models/project.js', function () { + let project, projectPath, packageContents; + + function makeProject() { + let cli = new MockCLI(); + project = new Project(projectPath, packageContents, cli.ui, cli); + } + + beforeEach(function () { + packageContents = {}; }); - describe('Project.prototype.config', function() { - var called; + afterEach(function () { + if (project) { + project = null; + } + }); + + describe('constructor', function () { + it('sets up bidirectional instrumentation', function () { + let cli = new MockCLI(); + + expect(cli.instrumentation.project).to.equal(null); + + projectPath = 'tmp/test-app'; + return tmp.setup(projectPath).then(function () { + touch(`${projectPath}/config/environment.js`, { + rootURL: '/foo/bar', + }); + + project = new Project(projectPath, {}, cli.ui, cli); + expect(cli.instrumentation.project).to.equal(project); + expect(project._instrumentation).to.equal(cli.instrumentation); + }); + }); + }); - beforeEach(function() { - projectPath = process.cwd() + '/tmp/test-app'; + describe('Project.prototype.config', function () { + let called; + projectPath = 'tmp/test-app'; + + beforeEach(function () { called = false; - return tmp.setup(projectPath) - .then(function() { - touch(projectPath + '/config/environment.js', { - baseURL: '/foo/bar' - }); + return tmp.setup(projectPath).then(function () { + touch(`${projectPath}/config/environment.js`, { + rootURL: '/foo/bar', + }); - project = new Project(projectPath, { }, new MockUI()); - project.require = function() { - called = true; - return function() {}; - }; + touch(`${projectPath}/config/a.js`, { + rootURL: '/a', + }); + + touch(`${projectPath}/config/b.js`, { + rootURL: '/b', }); + + makeProject(); + + project.require = function () { + called = true; + return function () {}; + }; + }); }); - afterEach(function() { + afterEach(function () { called = null; return tmp.teardown(projectPath); }); - it('config() finds and requires config/environment', function() { + it('config() finds and requires config/environment', function () { project.config('development'); expect(called).to.equal(true); }); - it('configPath() returns tests/dummy/config/environment', function() { + it('config() with no args returns the config corresponding to the current EMBER_ENV', function () { + process.env.EMBER_ENV = 'testing'; + + project.config(); + let configCacheKey; + for (const key of project.configCache.keys()) { + if (key.match('|')) { + configCacheKey = key; + } + } + expect(configCacheKey).to.match(/testing/i); + }); + + describe('memoizes', function () { + it('memoizes', function () { + project.config('development'); + expect(called).to.equal(true); + called = false; + project.config('development'); + expect(called).to.equal(false); + }); + + it('considers configPath when memoizing', function () { + project.configPath = function () { + return `${projectPath}/config/a`; + }; + project.config('development'); + + expect(called).to.equal(true); + called = false; + + project.configPath = function () { + return `${projectPath}/config/a`; + }; + project.config('development'); + + expect(called).to.equal(false); + called = false; + + project.configPath = function () { + return `${projectPath}/config/b`; + }; + project.config('development'); + + expect(called).to.equal(true); + called = false; + + project.config('development'); + + expect(called).to.equal(false); + called = false; + }); + }); + + it('configPath() returns tests/dummy/config/environment', function () { project.pkg = { 'ember-addon': { - 'configPath': 'tests/dummy/config' - } + configPath: 'tests/dummy/config', + }, }; - var expected = path.normalize('tests/dummy/config/environment'); + let expected = path.normalize('tests/dummy/config/environment'); - expect(project.configPath()).to.equal(expected); + expect(project.configPath().slice(-expected.length)).to.equal(expected); }); - it('calls getAddonsConfig', function() { - var addonConfigCalled = false; + it('calls getAddonsConfig', function () { + let addonConfigCalled = false; - project.getAddonsConfig = function() { + project.getAddonsConfig = function () { addonConfigCalled = true; return {}; @@ -74,89 +165,142 @@ describe('models/project.js', function() { expect(addonConfigCalled).to.equal(true); }); - it('returns getAddonsConfig result when configPath is not present', function() { - var expected = { - foo: 'bar' + it('returns getAddonsConfig result when configPath is not present', function () { + let expected = { + foo: 'bar', }; - return tmp.setup(projectPath) // ensure no config/environment.js is present - .then(function() { - project.getAddonsConfig = function() { - return expected; - }; + project.getAddonsConfig = function () { + return expected; + }; - var actual = project.config('development'); - expect(actual).to.deep.equal(expected); - }); + let actual = project.config('development'); + expect(actual).to.deep.equal(expected); }); - describe('merges getAddonsConfig result with app config', function() { - var projectConfig, addon1Config, addon2Config; + describe('merges getAddonsConfig result with app config', function () { + let projectConfig, addon1Config, addon2Config; - beforeEach(function() { - addon1Config = { addon: { derp: 'herp' } }; - addon2Config = { addon: { blammo: 'blahzorz' } }; + beforeEach(function () { + addon1Config = { addon: { derp: 'herp' } }; + addon2Config = { addon: { blammo: 'blahzorz' } }; projectConfig = { foo: 'bar', baz: 'qux' }; project.addons = [ - { config: function() { return addon1Config; }}, - { config: function() { return addon2Config; }} + { + config() { + return addon1Config; + }, + }, + { + config() { + return addon2Config; + }, + }, ]; project._addonsInitialized = true; - project.require = function() { - return function() { + project.require = function () { + return function () { return projectConfig; }; }; }); - it('merges getAddonsConfig result with app config', function() { - var expected = { + it('merges getAddonsConfig result with app config', function () { + let expected = { foo: 'bar', baz: 'qux', addon: { derp: 'herp', - blammo: 'blahzorz' - } + blammo: 'blahzorz', + }, }; - var actual = project.config('development'); + let actual = project.config('development'); expect(actual).to.deep.equal(expected); }); - it('getAddonsConfig does NOT override project config', function() { - var expected = { + it('getAddonsConfig does NOT override project config', function () { + let expected = { foo: 'bar', baz: 'qux', addon: { derp: 'herp', - blammo: 'blahzorz' - } + blammo: 'blahzorz', + }, }; addon1Config.foo = 'NO!!!!!!'; - var actual = project.config('development'); + let actual = project.config('development'); expect(actual).to.deep.equal(expected); }); }); }); - describe('addons', function() { - beforeEach(function() { + describe('Project.prototype.targets', function () { + beforeEach(function () { + projectPath = 'tmp/test-app'; + }); + + afterEach(function () { + return tmp.teardown(projectPath); + }); + + describe('when the is a `/config/targets.js` file', function () { + beforeEach(function () { + return tmp.setup(projectPath).then(function () { + let targetsPath = path.join(projectPath, 'config', 'targets.js'); + fs.createFileSync(targetsPath); + fs.writeFileSync(targetsPath, 'module.exports = { browsers: ["last 2 versions", "safari >= 7"] };', { + encoding: 'utf8', + }); + + makeProject(); + + project.require = function () { + return { browsers: ['last 2 versions', 'safari >= 7'] }; + }; + }); + }); + + it('returns the object defined in `/config/targets` if present', function () { + expect(project.targets).to.deep.equal({ + browsers: ['last 2 versions', 'safari >= 7'], + }); + }); + }); + + describe("when there isn't a `/config/targets.js` file", function () { + beforeEach(function () { + return tmp.setup(projectPath).then(function () { + makeProject(); + }); + }); + + it('returns the default targets', function () { + expect(project.targets).to.deep.equal({ + browsers: ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'], + }); + }); + }); + }); + + describe('addons', function () { + beforeEach(function () { projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); - var packageContents = require(path.join(projectPath, 'package.json')); + packageContents = require(path.join(projectPath, 'package.json')); - project = new Project(projectPath, packageContents, new MockUI()); - project.initializeAddons(); + makeProject(); }); - it('returns a listing of all dependencies in the projects package.json', function() { - var expected = { + it("returns a listing of all dependencies in the project's package.json", function () { + let expected = { 'ember-cli': 'latest', 'ember-random-addon': 'latest', + 'ember-resolver': '^13.2.0', 'ember-non-root-addon': 'latest', 'ember-generated-with-export-addon': 'latest', 'non-ember-thingy': 'latest', @@ -164,367 +308,383 @@ describe('models/project.js', function() { 'ember-after-blueprint-addon': 'latest', 'something-else': 'latest', 'ember-devDeps-addon': 'latest', - 'ember-addon-with-dependencies': 'latest' + 'ember-addon-with-dependencies': 'latest', + 'loader.js': 'latest', }; expect(project.dependencies()).to.deep.equal(expected); }); - it('returns a listing of all dependencies in the projects bower.json', function() { - var expected = { - 'jquery': '^1.11.1', - 'ember': '1.7.0', - 'ember-data': '1.0.0-beta.10', - 'ember-resolver': '~0.1.7', - 'loader.js': 'ember-cli/loader.js#1.0.1', - 'ember-cli-shims': 'ember-cli/ember-cli-shims#0.0.3', - 'ember-cli-test-loader': 'rwjblue/ember-cli-test-loader#0.0.4', - 'ember-load-initializers': 'ember-cli/ember-load-initializers#0.0.2', - 'ember-qunit': '0.1.8', - 'ember-qunit-notifications': '0.0.4', - 'qunit': '~1.15.0' - }; - - expect(project.bowerDependencies()).to.deep.equal(expected); - }); + it('returns a listing of all ember-cli-addons directly depended on by the project', function () { + project.initializeAddons(); - it('returns a listing of all ember-cli-addons directly depended on by the project', function() { - var expected = [ + let expected = [ + 'amd-transform', + 'broccoli-serve-files', + 'broccoli-watcher', + 'history-support-middleware', + 'proxy-server-middleware', + 'testem-url-rewriter-middleware', 'tests-server-middleware', - 'history-support-middleware', 'serve-files-middleware', - 'proxy-server-middleware', 'ember-random-addon', 'ember-non-root-addon', + 'ember-addon-with-dependencies', + 'ember-after-blueprint-addon', + 'ember-before-blueprint-addon', + 'ember-devDeps-addon', 'ember-generated-with-export-addon', - 'ember-before-blueprint-addon', 'ember-after-blueprint-addon', - 'ember-devDeps-addon', 'ember-addon-with-dependencies', 'ember-super-button' + 'ember-non-root-addon', + 'ember-random-addon', + 'ember-super-button', + 'ember-with-addon-main', ]; + expect(Object.keys(project.addonPackages)).to.deep.equal(expected); }); - it('returns instances of the addons', function() { - var addons = project.addons; + it('returns instances of the addons', function () { + project.initializeAddons(); + + let addons = project.addons; - expect(addons[5].name).to.equal('Ember Non Root Addon'); - expect(addons[11].name).to.equal('Ember Super Button'); - expect(addons[11].addons[0].name).to.equal('Ember Yagni'); - expect(addons[11].addons[1].name).to.equal('Ember Ng'); + expect(addons[8].name).to.equal('ember-before-blueprint-addon'); + expect(addons[14].name).to.equal('ember-super-button'); + expect(addons[14].addons[0].name).to.equal('ember-yagni'); + expect(addons[14].addons[1].name).to.equal('ember-ng'); }); - it('addons get passed the project instance', function() { - var addons = project.addons; + it('addons get passed the project instance', function () { + project.initializeAddons(); + + let addons = project.addons; expect(addons[1].project).to.equal(project); }); - it('returns an instance of an addon that uses `ember-addon-main`', function() { - var addons = project.addons; + it('returns an instance of an addon that uses `ember-addon-main`', function () { + project.initializeAddons(); + + let addons = project.addons; - expect(addons[7].name).to.equal('Ember Random Addon'); + expect(addons[10].name).to.equal('ember-random-addon'); }); - it('returns the default blueprints path', function() { - var expected = project.root + path.normalize('/blueprints'); + it('returns the default blueprints path', function () { + let expected = project.root + path.normalize('/blueprints'); expect(project.localBlueprintLookupPath()).to.equal(expected); }); - it('returns a listing of all addon blueprints paths ordered by last loaded when called once', function() { - var loadedBlueprintPaths = [ + it('returns a listing of all addon blueprints paths ordered by last loaded when called once', function () { + project.initializeAddons(); + + let loadedBlueprintPaths = [ project.root + path.normalize('/node_modules/ember-before-blueprint-addon/blueprints'), project.root + path.normalize('/node_modules/ember-random-addon/blueprints'), - project.root + path.normalize('/node_modules/ember-after-blueprint-addon/blueprints') + project.root + path.normalize('/node_modules/ember-after-blueprint-addon/blueprints'), ]; // the first found addon blueprint should be the last one defined - var expected = loadedBlueprintPaths.reverse(); - var first = project.addonBlueprintLookupPaths(); + let expected = loadedBlueprintPaths.reverse(); + let first = project.addonBlueprintLookupPaths(); expect(first).to.deep.equal(expected); }); - it('returns a listing of all addon blueprints paths ordered by last loaded when called twice', function() { - var loadedBlueprintPaths = [ + it('returns a listing of all addon blueprints paths ordered by last loaded when called twice', function () { + project.initializeAddons(); + + let loadedBlueprintPaths = [ project.root + path.normalize('/node_modules/ember-before-blueprint-addon/blueprints'), project.root + path.normalize('/node_modules/ember-random-addon/blueprints'), - project.root + path.normalize('/node_modules/ember-after-blueprint-addon/blueprints') + project.root + path.normalize('/node_modules/ember-after-blueprint-addon/blueprints'), ]; // the first found addon blueprint should be the last one defined - var expected = loadedBlueprintPaths.reverse(); - /*var first = */project.addonBlueprintLookupPaths(); - var second = project.addonBlueprintLookupPaths(); + let expected = loadedBlueprintPaths.reverse(); + /*var first = */ project.addonBlueprintLookupPaths(); + let second = project.addonBlueprintLookupPaths(); expect(second).to.deep.equal(expected); }); - it('returns a listing of all blueprints paths', function() { - var expected = [ + it('returns a listing of all blueprints paths', function () { + project.initializeAddons(); + + let expected = [ project.root + path.normalize('/blueprints'), project.root + path.normalize('/node_modules/ember-after-blueprint-addon/blueprints'), project.root + path.normalize('/node_modules/ember-random-addon/blueprints'), - project.root + path.normalize('/node_modules/ember-before-blueprint-addon/blueprints') + project.root + path.normalize('/node_modules/ember-before-blueprint-addon/blueprints'), ]; expect(project.blueprintLookupPaths()).to.deep.equal(expected); }); - it('does not include blueprint path relative to root if outside a project', function() { - project.isEmberCLIProject = function() { + it('does not include blueprint path relative to root if outside a project', function () { + project.isEmberCLIProject = function () { return false; }; expect(project.blueprintLookupPaths()).to.deep.equal(project.addonBlueprintLookupPaths()); }); - it('returns an instance of an addon with an object export', function() { - var addons = project.addons; + it('returns an instance of an addon with an object export', function () { + project.initializeAddons(); + + let addons = project.addons; - expect(addons[4] instanceof Addon).to.equal(true); - expect(addons[4].name).to.equal('Ember CLI Generated with export'); + expect(addons[13] instanceof Addon).to.equal(true); + expect(addons[13].name).to.equal('ember-generated-with-export-addon'); }); - it('adds the project itself if it is an addon', function() { - var added = false; + it('adds the project itself if it is an addon', function () { project.addonPackages = {}; - project.isEmberCLIAddon = function() { return true; }; - - project.addonDiscovery.discoverAtPath = function(path) { - if (path === project.root) { - added = true; - } + project.isEmberCLIAddon = function () { + return true; }; project.discoverAddons(); - expect(added); + expect(project.addonPackages[project.name()]).to.exist; + }); + + it('should catch addon constructor errors', function () { + projectPath = path.resolve(__dirname, '../../fixtures/addon/invalid-constructor'); + packageContents = require(path.join(projectPath, 'package.json')); + + makeProject(); + + const invalidAddonName = 'ember-invalid-addon'; + const expectedPath = path.join(projectPath, '/lib/', invalidAddonName); + const expectedError = `An error occurred in the constructor for ${invalidAddonName} at ${expectedPath}`; + + expect(() => { + project.initializeAddons(); + }).to.throw(expectedError); }); }); - describe('reloadAddon', function() { - beforeEach(function() { - projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); - var packageContents = require(path.join(projectPath, 'package.json')); + describe('reloadAddon', function () { + beforeEach(function () { + projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); + packageContents = require(path.join(projectPath, 'package.json')); + + makeProject(); - project = new Project(projectPath, packageContents, new MockUI()); project.initializeAddons(); - stub(Project.prototype, 'initializeAddons'); - stub(Project.prototype, 'reloadPkg'); + td.replace(Project.prototype, 'initializeAddons', td.function()); + td.replace(Project.prototype, 'reloadPkg', td.function()); project.reloadAddons(); }); - afterEach(function() { - Project.prototype.initializeAddons.restore(); - Project.prototype.reloadPkg.restore(); + afterEach(function () { + td.reset(); }); - it('sets _addonsInitialized to false', function() { + it('sets _addonsInitialized to false', function () { expect(project._addonsInitialized).to.equal(false); }); - it('reloads the package', function() { - expect(Project.prototype.reloadPkg.called, 'reloadPkg was called'); + it('reloads the package', function () { + td.verify(Project.prototype.reloadPkg(), { ignoreExtraArgs: true }); }); - it('initializes the addons', function() { - expect(Project.prototype.initializeAddons.called, 'initialize addons was called'); + it('initializes the addons', function () { + td.verify(Project.prototype.initializeAddons(), { ignoreExtraArgs: true }); }); }); - describe('reloadPkg', function() { - var newProjectPath, oldPkg; - beforeEach(function() { - projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); - var packageContents = require(path.join(projectPath, 'package.json')); + describe('reloadPkg', function () { + let newProjectPath, oldPkg; + + beforeEach(function () { + projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); + packageContents = require(path.join(projectPath, 'package.json')); + + makeProject(); - project = new Project(projectPath, packageContents, new MockUI()); project.initializeAddons(); newProjectPath = path.resolve(__dirname, '../../fixtures/addon/env-addons'); - oldPkg = project.pkg; + oldPkg = project.pkg; project.root = newProjectPath; }); - it('reloads the package from disk', function() { + it('reloads the package from disk', function () { project.reloadPkg(); expect(oldPkg).to.not.deep.equal(project.pkg); }); + + it('reloads the pkginfo', function () { + let pkgInfo = project._packageInfo; + + project.reloadPkg(); + + expect(pkgInfo).to.not.equal(project._packageInfo); + }); }); - describe('emberCLIVersion', function() { - beforeEach(function() { - projectPath = process.cwd() + '/tmp/test-app'; - project = new Project(projectPath, {}, new MockUI()); + describe('emberCLIVersion', function () { + beforeEach(function () { + projectPath = `${process.cwd()}/tmp/test-app`; + makeProject(); }); - it('should return the same value as the utlity function', function() { + it('should return the same value as the utility function', function () { expect(project.emberCLIVersion()).to.equal(emberCLIVersion()); }); }); - describe('isEmberCLIProject', function() { - beforeEach(function() { - projectPath = process.cwd() + '/tmp/test-app'; - project = new Project(projectPath, {}, new MockUI()); + describe('isEmberCLIProject', function () { + beforeEach(function () { + projectPath = `${process.cwd()}/tmp/test-app`; + + makeProject(); }); - it('returns false when `ember-cli` is not a dependency', function(){ + it('returns false when `ember-cli` is not a dependency', function () { expect(project.isEmberCLIProject()).to.equal(false); }); - it('returns true when `ember-cli` is a devDependency', function(){ - project.pkg.devDependencies = {'ember-cli': '*'}; + it('returns true when `ember-cli` is a devDependency', function () { + project.pkg.devDependencies = { 'ember-cli': '*' }; expect(project.isEmberCLIProject()).to.equal(true); }); - it('returns true when `ember-cli` is a dependency', function(){ - project.pkg.dependencies = {'ember-cli': '*'}; + it('returns true when `ember-cli` is a dependency', function () { + project.pkg.dependencies = { 'ember-cli': '*' }; expect(project.isEmberCLIProject()).to.equal(true); }); }); - describe('isEmberCLIAddon', function() { - beforeEach(function() { - projectPath = process.cwd() + '/tmp/test-app'; + describe('isViteProject', function () { + beforeEach(function () { + projectPath = `${process.cwd()}/tmp/test-app`; + + makeProject(); + }); + + it('returns false when `@embroider/vite` is not a dependency', function () { + expect(project.isViteProject()).to.equal(false); + }); + + it('returns true when `@embroider/vite` is a devDependency', function () { + project.pkg.devDependencies = { '@embroider/vite': '*' }; + + expect(project.isViteProject()).to.equal(true); + }); + + it('returns true when `@embroider/vite` is a dependency', function () { + project.pkg.dependencies = { '@embroider/vite': '*' }; + + expect(project.isViteProject()).to.equal(true); + }); + }); + + describe('isEmberCLIAddon', function () { + beforeEach(function () { + projectPath = `${process.cwd()}/tmp/test-app`; - project = new Project(projectPath, {}, new MockUI()); + makeProject(); project.initializeAddons(); }); - it('should return true if `ember-addon` is included in keywords', function() { + it('should return true if `ember-addon` is included in keywords', function () { project.pkg = { - keywords: [ 'ember-addon' ] + keywords: ['ember-addon'], }; expect(project.isEmberCLIAddon()).to.equal(true); }); - it('should return false if `ember-addon` is not included in keywords', function() { + it('should return false if `ember-addon` is not included in keywords', function () { project.pkg = { - keywords: [ ] + keywords: [], }; expect(project.isEmberCLIAddon()).to.equal(false); }); }); - describe('findAddonByName', function() { - beforeEach(function() { - projectPath = process.cwd() + '/tmp/test-app'; + describe('findAddonByName', function () { + beforeEach(function () { + projectPath = `${process.cwd()}/tmp/test-app`; - project = new Project(projectPath, {}, new MockUI()); - - stub(Project.prototype, 'initializeAddons'); + makeProject(); - project.addons = [{ - name: 'foo', - pkg: { name: 'foo' } - }, { - pkg: { name: 'bar-pkg' } - }, { - name: 'foo-bar', - pkg: { name: 'foo-bar' } - }]; + td.replace(Project.prototype, 'initializeAddons', td.function()); + + project.addons = [ + { + name: 'foo', + pkg: { name: 'foo' }, + }, + { + pkg: { name: 'bar-pkg' }, + }, + { + name: 'foo-bar', + pkg: { name: 'foo-bar' }, + }, + ]; }); - afterEach(function() { - Project.prototype.initializeAddons.restore(); + afterEach(function () { + td.reset(); }); - it('should call initialize addons', function() { + it('should call initialize addons', function () { project.findAddonByName('foo'); - expect(project.initializeAddons.called, 'should have called initializeAddons'); + td.verify(project.initializeAddons(), { ignoreExtraArgs: true }); }); - it('should return the foo addon from name', function() { - var addon = project.findAddonByName('foo'); + it('finds addons by their `package.json` name', function () { + let addon = project.findAddonByName('foo'); expect(addon.name).to.equal('foo', 'should have found the foo addon'); - }); - it('should return the foo-bar addon from name when a foo also exists', function() { - var addon = project.findAddonByName('foo-bar'); - expect(addon.name).to.equal('foo-bar', 'should have found the foo-bar addon'); - }); - - it('should return the bar-pkg addon from package name', function() { - var addon = project.findAddonByName('bar-pkg'); + addon = project.findAddonByName('bar-pkg'); expect(addon.pkg.name).to.equal('bar-pkg', 'should have found the bar-pkg addon'); }); - - it('should return undefined if adddon doesn\'t exist', function() { - var addon = project.findAddonByName('not-an-addon'); - expect(addon).to.equal(undefined, 'not found addon should be undefined'); - }); }); - describe('bowerDirectory', function() { - beforeEach(function() { - projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); - project = new Project(projectPath, {}, new MockUI()); - }); - - it('should be initialized in constructor', function() { - expect(project.bowerDirectory).to.equal('bower_components'); - }); - - it('should be set to directory property in .bowerrc', function() { - projectPath = path.resolve(__dirname, '../../fixtures/bower-directory-tests/bowerrc-with-directory'); - project = new Project(projectPath, {}, new MockUI()); - expect(project.bowerDirectory).to.equal('vendor'); - }); - - it('should default to ‘bower_components’ unless directory property is set in .bowerrc', function() { - projectPath = path.resolve(__dirname, '../../fixtures/bower-directory-tests/bowerrc-without-directory'); - project = new Project(projectPath, {}, new MockUI()); - expect(project.bowerDirectory).to.equal('bower_components'); - }); - - it('should default to ‘bower_components’ if .bowerrc is not present', function() { - projectPath = path.resolve(__dirname, '../../fixtures/bower-directory-tests/no-bowerrc'); - project = new Project(projectPath, {}, new MockUI()); - expect(project.bowerDirectory).to.equal('bower_components'); - }); - - it('should default to ‘bower_components’ if .bowerrc json is invalid', function() { - projectPath = path.resolve(__dirname, '../../fixtures/bower-directory-tests/invalid-bowerrc'); - project = new Project(projectPath, {}, new MockUI()); - expect(project.bowerDirectory).to.equal('bower_components'); + describe('.nullProject', function () { + it('is a singleton', function () { + expect(Project.nullProject()).to.equal(Project.nullProject()); }); }); - describe('nodeModulesPath', function() { - function makeProject() { - projectPath = path.resolve(__dirname, '../../fixtures/addon/simple'); - project = new Project(projectPath, {}, new MockUI()); - } - - afterEach(function() { - delete process.env.EMBER_NODE_PATH; - }); - - it('should equal env.EMBER_NODE_PATH when it is set', function() { - var nodePath = '/my/path/node_modules'; - process.env.EMBER_NODE_PATH = nodePath; - + describe('generateTestFile()', function () { + it('returns empty file and shows warning', function () { + projectPath = path.resolve(__dirname, '../../fixtures/project'); makeProject(); - expect(project.nodeModulesPath).to.equal(path.resolve(nodePath)); + expect(project.generateTestFile()).to.equal(''); + expect(project.ui.output).to.contain( + 'Please install an Ember.js test framework addon or update your dependencies.' + ); }); + }); - it('should equal project.root joined with "node_modules" when EMBER_NODE_PATH is not set', function() { - makeProject(); - - expect(project.nodeModulesPath).to.equal(path.join(projectPath, 'node_modules')); + describe('Project.closestSync', function () { + it('should use the `actual-project` specified by `ember-addon.projectRoot` in the top-level `package.json`', function () { + let cli = new MockCLI(); + projectPath = path.resolve(__dirname, '../../fixtures/app/nested-project'); + project = Project.closestSync(projectPath, cli.ui, cli); + expect(project.root).to.equal(path.resolve(__dirname, '../../fixtures/app/nested-project/actual-project')); }); - }); - describe('.nullProject', function (){ - it('is a singleton', function() { - expect(Project.nullProject()).to.equal(Project.nullProject()); + it('should throw if both `ember-addon.projectRoot` and `ember-cli-build.js` exist', function () { + let cli = new MockCLI(); + projectPath = path.resolve(__dirname, '../../fixtures/app/project-root-with-ember-cli-build'); + + expect(() => Project.closestSync(projectPath, cli.ui, cli)).to.throw( + `Both \`ember-addon.projectRoot\` and \`ember-cli-build.js\` exist as part of \`${projectPath}\`` + ); }); }); }); diff --git a/tests/unit/models/server-watcher-test.js b/tests/unit/models/server-watcher-test.js index 552bf1d62f..6c82a3ddc3 100644 --- a/tests/unit/models/server-watcher-test.js +++ b/tests/unit/models/server-watcher-test.js @@ -1,95 +1,59 @@ 'use strict'; -var expect = require('chai').expect; -var EOL = require('os').EOL; -var MockUI = require('../../helpers/mock-ui'); -var MockAnalytics = require('../../helpers/mock-analytics'); -var MockServerWatcher = require('../../helpers/mock-watcher'); -var ServerWatcher = require('../../../lib/models/server-watcher'); +const { expect } = require('chai'); +const EOL = require('os').EOL; +const MockUI = require('console-ui/mock'); +const MockServerWatcher = require('../../helpers/mock-watcher'); +const ServerWatcher = require('../../../lib/models/server-watcher'); -describe('Server Watcher', function() { - var ui; - var subject; - var analytics; - var watcher; +describe('Server Watcher', function () { + let ui; + let watcher; - beforeEach(function() { - ui = new MockUI(); - analytics = new MockAnalytics(); - watcher = new MockServerWatcher(); + beforeEach(async function () { + ui = new MockUI(); + watcher = new MockServerWatcher(); - subject = new ServerWatcher({ - ui: ui, - analytics: analytics, - watcher: watcher - }); - }); - - describe('watcher strategy selection', function() { - it('selects the events-based watcher by default', function () { - subject.options = null; - expect(!!subject.polling()).to.equal(false); - }); + class ServerWatcherMock extends ServerWatcher { + setupBroccoliWatcher() { + this.watcher = watcher; - it('selects the events-based watcher when given events watcher option', function () { - subject.options = { watcher: 'events' }; - expect(!!subject.polling()).to.equal(false); - }); + return super.setupBroccoliWatcher(...arguments); + } + } - it('selects the polling watcher when given polling watcher option', function () { - subject.options = { watcher: 'polling' }; - expect(!!subject.polling()); + await ServerWatcherMock.build({ + ui, }); }); - describe('watcher:change', function() { + describe('watcher:change', function () { beforeEach(function () { watcher.emit('change', 'foo.txt'); }); - it('logs that the file was changed', function() { - expect(ui.output).to.equal('Server file changed: foo.txt' + EOL); - }); - - it('tracks changes', function() { - expect(analytics.tracks).to.deep.equal([{ - name: 'server file change', - description: 'File changed: "foo.txt"' - }]); + it('logs that the file was changed', function () { + expect(ui.output).to.equal(`File changed: "foo.txt"${EOL}`); }); }); - describe('watcher:add', function() { + describe('watcher:add', function () { beforeEach(function () { watcher.emit('add', 'foo.txt'); }); - it('logs that the file was added', function() { - expect(ui.output).to.equal('Server file added: foo.txt' + EOL); - }); - - it('tracks additions', function() { - expect(analytics.tracks).to.deep.equal([{ - name: 'server file addition', - description: 'File added: "foo.txt"' - }]); + it('logs that the file was added', function () { + expect(ui.output).to.equal(`File added: "foo.txt"${EOL}`); }); }); - describe('watcher:delete', function() { + describe('watcher:delete', function () { beforeEach(function () { watcher.emit('delete', 'foo.txt'); }); - it('logs that the file was deleted', function() { - expect(ui.output).to.equal('Server file deleted: foo.txt' + EOL); - }); - - it('tracks deletions', function() { - expect(analytics.tracks).to.deep.equal([{ - name: 'server file deletion', - description: 'File deleted: "foo.txt"' - }]); + it('logs that the file was deleted', function () { + expect(ui.output).to.equal(`File deleted: "foo.txt"${EOL}`); }); }); }); diff --git a/tests/unit/models/update-checker-test.js b/tests/unit/models/update-checker-test.js deleted file mode 100644 index 6972a5f5ee..0000000000 --- a/tests/unit/models/update-checker-test.js +++ /dev/null @@ -1,138 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); -var UpdateChecker = require('../../../lib/models/update-checker'); -var Promise = require('../../../lib/ext/promise'); - -describe('Update Checker', function() { - var ui; - var versionConfig; - - beforeEach(function() { - ui = new MockUI(); - versionConfig = { - store: {}, - get: function(key) { - return this.store[key]; - }, - set: function(key, val) { - this.store[key] = val; - } - }; - }); - - it('returns { updatedNeeded: false } if local version is a dev build', function() { - var updateChecker = new UpdateChecker(ui, { - checkForUpdates: true - }, '0.0.5-master-237cc6024d'); - - // overwrite doCheck so it ignores any existing configstore - updateChecker.doCheck = (function() { - var doCheck = updateChecker.doCheck; - - return function() { - updateChecker.versionConfig = versionConfig; - return doCheck.apply(this); - }; - }()); - - updateChecker.checkNPM = function() { - return Promise.resolve('0.0.5'); - }; - - return updateChecker.checkForUpdates().then(function(updateInfo) { - expect(updateInfo.updateNeeded).to.equal(false, 'updateNeeded should be false'); - }); - }); - - it('returns { updatedNeeded: false } if no update is needed', function() { - var updateChecker = new UpdateChecker(ui, { - checkForUpdates: true - }, '0.0.5'); - - // overwrite doCheck so it ignores any existing configstore - updateChecker.doCheck = (function() { - var doCheck = updateChecker.doCheck; - - return function() { - updateChecker.versionConfig = versionConfig; - return doCheck.apply(this); - }; - }()); - - updateChecker.checkNPM = function() { - return Promise.resolve('0.0.1'); - }; - - return updateChecker.checkForUpdates().then(function(updateInfo) { - expect(updateInfo.updateNeeded).to.equal(false, 'updateNeeded should be false'); - }); - }); - - it('says \'A new version of ember-cli is available\' if an update is needed', function() { - var updateChecker = new UpdateChecker(ui, { - checkForUpdates: true - }, '0.0.1'); - - // overwrite doCheck so it ignores any existing configstore - updateChecker.doCheck = (function() { - var doCheck = updateChecker.doCheck; - - return function() { - updateChecker.versionConfig = versionConfig; - return doCheck.apply(this, arguments); - }; - }()); - - updateChecker.checkNPM = function() { - return Promise.resolve('1000.0.0'); - }; - - return updateChecker.checkForUpdates().then(function() { - expect(ui.output).to.include('A new version of ember-cli is available'); - }); - }); - - it('should not check if last check was less than a day ago', function() { - var updateChecker = new UpdateChecker(ui, { - checkForUpdates: true - }, '0.0.1'); - - var now = new Date().getTime(); - var npmCalled = false; - - updateChecker.doCheck = (function() { - var doCheck = updateChecker.doCheck; - - return function() { - updateChecker.versionConfig = versionConfig; - versionConfig.set('lastVersionCheckAt', now - 86400); - return doCheck.apply(this); - }; - }()); - - updateChecker.checkNPM = function() { - npmCalled = true; - }; - - return updateChecker.checkForUpdates().then(function() { - expect(npmCalled).to.equal(false, 'NPM should not be called if the last check was less than a day ago'); - }); - }); - - it('should save version information in configstore if checking with npm', function() { - var updateChecker = new UpdateChecker(ui, { - checkForUpdates: true - }, '0.0.1'); - - updateChecker.versionConfig = versionConfig; - updateChecker.saveVersionInformation('1000.0.0'); - - var now = new Date().getTime(); - - expect(updateChecker.versionConfig.store.newestVersion).to.equal('1000.0.0', 'should store newest version in configstore'); - expect(updateChecker.versionConfig.store.lastVersionCheckAt).to.be.closeTo(now, 100, 'should store lastVersionCheckAt in configstore'); - }); - -}); diff --git a/tests/unit/models/watcher-test.js b/tests/unit/models/watcher-test.js index aaed65566d..18e967d24a 100644 --- a/tests/unit/models/watcher-test.js +++ b/tests/unit/models/watcher-test.js @@ -1,36 +1,53 @@ 'use strict'; -var expect = require('chai').expect; - -var MockUI = require('../../helpers/mock-ui'); -var MockAnalytics = require('../../helpers/mock-analytics'); -var MockWatcher = require('../../helpers/mock-watcher'); -var Watcher = require('../../../lib/models/watcher'); -var EOL = require('os').EOL; -var chalk = require('chalk'); -var BuildError = require('../../helpers/build-error'); - -describe('Watcher', function() { - var ui; - var subject; - var builder; - var analytics; - var watcher; - - beforeEach(function() { - ui = new MockUI(); - analytics = new MockAnalytics(); - watcher = new MockWatcher(); - - subject = new Watcher({ - ui: ui, - analytics: analytics, - builder: builder, - watcher: watcher - }); +const { expect } = require('chai'); + +const MockUI = require('console-ui/mock'); +const MockBroccoliWatcher = require('../../helpers/mock-broccoli-watcher'); +const Watcher = require('../../../lib/models/watcher'); +const EOL = require('os').EOL; +const { default: chalk } = require('chalk'); +const BuildError = require('../../helpers/build-error'); + +describe('Watcher', function () { + let ui; + let subject; + let builder; + let watcher; + + let mockResult = { + totalTime: 12344000000, + graph: { + __heimdall__: { + visitPreOrder(cb) { + return cb({ + stats: { + time: { + self: 12344000000, + }, + }, + }); + }, + visitPostOrder() {}, + }, + }, + }; + + beforeEach(async function () { + ui = new MockUI(); + + watcher = new MockBroccoliWatcher(); + + subject = ( + await Watcher.build({ + ui, + builder, + watcher, + }) + ).watcher; }); - describe('watcher strategy selection', function() { + describe('watcher strategy selection', function () { it('selects the events-based watcher by default', function () { subject.options = null; @@ -38,150 +55,301 @@ describe('Watcher', function() { verbose: true, poll: false, watchman: false, - node: false + node: false, }); }); it('selects the events-based watcher when given events watcher option', function () { subject.options = { - watcher: 'events' + watcher: 'events', }; expect(subject.buildOptions()).to.deep.equal({ verbose: true, poll: false, watchman: true, - node: false + node: false, }); }); it('selects the polling watcher when given polling watcher option', function () { subject.options = { - watcher: 'polling' + watcher: 'polling', }; expect(subject.buildOptions()).to.deep.equal({ verbose: true, poll: true, watchman: false, - node: false + node: false, }); }); }); - describe('watcher:change', function() { - beforeEach(function() { - watcher.emit('change', { - totalTime: 12344000000 - }); + describe('underlining watcher properly logs change events', function () { + it('logs that the file was added', function () { + watcher.emit('change', 'add', 'foo.txt'); + expect(ui.output).to.equal(`file added foo.txt${EOL}`); + }); + it('logs that the file was changed', function () { + watcher.emit('change', 'change', 'foo.txt'); + expect(ui.output).to.equal(`file changed foo.txt${EOL}`); + }); + it('logs that the file was deleted', function () { + watcher.emit('change', 'delete', 'foo.txt'); + expect(ui.output).to.equal(`file deleted foo.txt${EOL}`); + }); + }); + + describe(`watcher:buildSuccess`, function () { + beforeEach(function () { + watcher.emit(`buildSuccess`, mockResult); }); - it('tracks events', function() { - expect(analytics.tracks).to.deep.equal([{ - name: 'ember rebuild', - message: 'broccoli rebuild time: 12344ms' - }]); + it('logs that the build was successful', function () { + expect(ui.output).to.equal(EOL + chalk.green('Build successful (12344ms)') + EOL); }); + }); - it('tracks timings', function() { - expect(analytics.trackTimings).to.deep.equal([{ - category: 'rebuild', - variable: 'rebuild time', - label: 'broccoli rebuild time', - value: 12344 - }]); + describe('output', function () { + this.timeout(40000); + + it('with ssl', async function () { + let subject = ( + await Watcher.build({ + ui, + builder, + watcher, + serving: true, + options: { + host: undefined, + port: '1337', + ssl: true, + sslCert: 'tests/fixtures/ssl/server.crt', + sslKey: 'tests/fixtures/ssl/server.key', + environment: 'development', + project: { + config() { + return { + rootURL: '/', + }; + }, + }, + }, + }) + ).watcher; + + subject.didChange(mockResult); + + let output = ui.output.trim().split(EOL); + expect(output[0]).to.equal(`${chalk.green('Build successful (12344ms)')} – Serving on https://localhost:1337/`); }); - it('logs that the build was successful', function() { - expect(ui.output).to.equal(EOL + chalk.green('Build successful - 12344ms.') + EOL); + it('with rootURL', async function () { + let subject = ( + await Watcher.build({ + ui, + builder, + watcher, + serving: true, + options: { + host: undefined, + port: '1337', + environment: 'development', + project: { + config() { + return { + rootURL: '/foo', + }; + }, + }, + }, + }) + ).watcher; + + subject.didChange(mockResult); + + let output = ui.output.trim().split(EOL); + + expect(output[0]).to.equal( + `${chalk.green('Build successful (12344ms)')} – Serving on http://localhost:1337/foo/` + ); + expect(output.length).to.equal(1, 'expected only one line of output'); + }); + + it('with empty rootURL', async function () { + let subject = ( + await Watcher.build({ + ui, + builder, + watcher, + serving: true, + options: { + host: undefined, + port: '1337', + rootURL: '', + environment: 'development', + project: { + config() { + return { + rootURL: '', + }; + }, + }, + }, + }) + ).watcher; + + subject.didChange(mockResult); + + let output = ui.output.trim().split(EOL); + expect(output[0]).to.equal(`${chalk.green('Build successful (12344ms)')} – Serving on http://localhost:1337/`); + expect(output.length).to.equal(1, 'expected only one line of output'); + }); + + it('with customURL', async function () { + let subject = ( + await Watcher.build({ + ui, + builder, + watcher, + serving: true, + options: { + host: undefined, + port: '1337', + rootURL: '', + environment: 'development', + project: { + config() { + return { + rootURL: '', + }; + }, + }, + }, + }) + ).watcher; + subject.serveURL = function () { + return `http://customurl.com/`; + }; + subject.didChange(mockResult); + + let output = ui.output.trim().split(EOL); + expect(output[0]).to.equal(`${chalk.green('Build successful (12344ms)')} – Serving on http://customurl.com/`); }); }); - describe('watcher:error', function() { - it('tracks errors', function() { + describe('watcher:error', function () { + it('watcher error', function () { watcher.emit('error', { message: 'foo', - stack: new Error().stack + stack: new Error().stack, + }); + + expect(ui.output).to.equal(''); + + let outs = ui.errors.split(EOL); + + expect(outs[0]).to.equal(chalk.red('foo')); + }); + + it('watcher buildFailure', function () { + watcher.emit('buildFailure', { + isBuilderError: true, + message: 'I am a build error', + file: 'the-file.txt', + stack: new Error().stack, }); - expect(analytics.trackErrors).to.deep.equal([{ - description: 'foo' - }]); + expect(ui.output).to.equal(''); + + let outs = ui.errors.split(EOL); + + expect(outs[0]).to.equal(chalk.red('File: the-file.txt')); + expect(outs[2]).to.equal(chalk.red('I am a build error')); }); - it('emits without error.file', function() { - subject.didError(new BuildError({ - file: 'someFile', - message: 'buildFailed' - })); + it('emits without error.file', function () { + subject.didError( + new BuildError({ + file: 'someFile', + message: 'buildFailed', + }) + ); + + expect(ui.output).to.equal(''); - var outs = ui.output.split(EOL); + let outs = ui.errors.split(EOL); expect(outs[0]).to.equal(chalk.red('File: someFile')); - expect(outs[1]).to.equal(chalk.red('buildFailed')); + expect(outs[2]).to.equal(chalk.red('buildFailed')); }); - it('emits with error.file with error.line without err.col', function() { - subject.didError(new BuildError({ - file: 'someFile', - line: 24, - message: 'buildFailed' - })); + it('emits with error.file with error.line without err.col', function () { + subject.didError( + new BuildError({ + file: 'someFile', + line: 24, + message: 'buildFailed', + }) + ); - var outs = ui.output.split(EOL); + expect(ui.output).to.eql(''); - expect(outs[0]).to.equal(chalk.red('File: someFile (24)')); - expect(outs[1]).to.equal(chalk.red('buildFailed')); + let outs = ui.errors.split(EOL); + + expect(outs[0]).to.equal(chalk.red('File: someFile:24')); + expect(outs[2]).to.equal(chalk.red('buildFailed')); }); - it('emits with error.file without error.line with err.col', function() { - subject.didError(new BuildError({ - file: 'someFile', - col: 80, - message: 'buildFailed' - })); - var outs = ui.output.split(EOL); + it('emits with error.file without error.line with err.col', function () { + subject.didError( + new BuildError({ + file: 'someFile', + col: 80, + message: 'buildFailed', + }) + ); + + expect(ui.output).to.eql(''); + + let outs = ui.errors.split(EOL); expect(outs[0]).to.equal(chalk.red('File: someFile')); - expect(outs[1]).to.equal(chalk.red('buildFailed')); + expect(outs[2]).to.equal(chalk.red('buildFailed')); }); - it('emits with error.file with error.line with err.col', function() { - subject.didError(new BuildError({ - file: 'someFile', - line: 24, - col: 80, - message: 'buildFailed' - })); + it('emits with error.file with error.line with err.col', function () { + subject.didError( + new BuildError({ + file: 'someFile', + line: 24, + col: 80, + message: 'buildFailed', + }) + ); + + expect(ui.output).to.eql(''); - var outs = ui.output.split(EOL); + let outs = ui.errors.split(EOL); - expect(outs[0]).to.equal(chalk.red('File: someFile (24:80)')); - expect(outs[1]).to.equal(chalk.red('buildFailed')); + expect(outs[0]).to.equal(chalk.red('File: someFile:24:80')); + expect(outs[2]).to.equal(chalk.red('buildFailed')); }); }); - describe('watcher:change afterError', function() { - beforeEach(function() { + describe('watcher:change afterError', function () { + beforeEach(function () { watcher.emit('error', { message: 'foo', - stack: new Error().stack + stack: new Error().stack, }); - watcher.emit('change', { - totalTime: 12344000000 - }); + watcher.emit(`buildSuccess`, mockResult); }); - it('log that the build was green', function() { + it('log that the build was green', function () { expect(ui.output).to.match(/Build successful./, 'has successful build output'); }); - - it('keep tracking analytics', function() { - expect(analytics.tracks).to.deep.equal([{ - name: 'ember rebuild', - message: 'broccoli rebuild time: 12344ms' - }]); - }); }); }); diff --git a/tests/unit/package/dependency-version-test.js b/tests/unit/package/dependency-version-test.js deleted file mode 100644 index 5b6d8ebba7..0000000000 --- a/tests/unit/package/dependency-version-test.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var semver = require('semver'); - -function assertVersionLock(_deps) { - var deps = _deps || {}; - - Object.keys(deps).forEach(function(name) { - if (name !== 'ember-cli' && - semver.valid(deps[name]) && - semver.gtr('1.0.0', deps[name])) { - // only valid if the version is fixed - expect(semver.valid(deps[name]), '"' + name + '" has a valid version'); - } - }); -} - -describe('dependencies', function() { - var pkg; - - describe('in package.json', function() { - before(function() { - pkg = require('../../../package.json'); - }); - - it('are locked down for pre-1.0 versions', function() { - assertVersionLock(pkg.dependencies); - assertVersionLock(pkg.devDependencies); - }); - }); - - describe('in blueprints/app/files/package.json', function() { - before(function() { - pkg = require('../../../blueprints/app/files/package.json'); - }); - - it('are locked down for pre-1.0 versions', function() { - assertVersionLock(pkg.dependencies); - assertVersionLock(pkg.devDependencies); - }); - }); -}); diff --git a/tests/unit/settings-file/leek-options-test.js b/tests/unit/settings-file/leek-options-test.js deleted file mode 100644 index f170d2b721..0000000000 --- a/tests/unit/settings-file/leek-options-test.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); -var Yam = require('yam'); -var cliEntry = require('../../../lib/cli'); - -describe('.ember-cli leek options', function() { - var cli; - var settings; - var passedOptions; - - before(function() { - settings = new Yam('ember-cli', { - primary: process.cwd() + '/tests/fixtures/leek-config' - }); - - cli = cliEntry({ - UI: MockUI, - Leek: function (options) { - passedOptions = options; - }, - Yam: function () { - return settings; - } - }); - - }); - - it('should contain the leek options from .ember-cli file', function() { - expect(passedOptions.adapterUrls).to.contain.keys(['event', 'exception', 'timing', 'appview']); - }); -}); \ No newline at end of file diff --git a/tests/unit/settings-file/precedence-settings-test.js b/tests/unit/settings-file/precedence-settings-test.js index de496f0143..83b7221317 100644 --- a/tests/unit/settings-file/precedence-settings-test.js +++ b/tests/unit/settings-file/precedence-settings-test.js @@ -1,53 +1,51 @@ 'use strict'; -var expect = require('chai').expect; -var merge = require('lodash/object/merge'); -var MockUI = require('../../helpers/mock-ui'); -var MockAnalytics = require('../../helpers/mock-analytics'); -var Command = require('../../../lib/models/command'); -var Yam = require('yam'); - -describe('.ember-cli', function() { - var ui; - var analytics; - var project; - var settings; - var homeSettings; - - before(function() { - ui = new MockUI(); - analytics = new MockAnalytics(); - project = { isEmberCLIProject: function() { return true; }}; +const { expect } = require('chai'); +const MockUI = require('console-ui/mock'); +const Command = require('../../../lib/models/command'); +const Yam = require('yam'); + +describe('.ember-cli', function () { + let ui; + let project; + let settings; + let homeSettings; + + before(function () { + ui = new MockUI(); + project = { + isEmberCLIProject() { + return true; + }, + }; homeSettings = { - proxy: 'http://iamstef.net/ember-cli', - liveReload: false, + proxy: 'http://iamstef.net/ember-cli', + liveReload: false, environment: 'mock-development', - host: '0.1.0.1' + host: '0.1.0.1', }; settings = new Yam('ember-cli', { - secondary: process.cwd() + '/tests/fixtures/home', - primary: process.cwd() + '/tests/fixtures/project' + secondary: `${process.cwd()}/tests/fixtures/home`, + primary: `${process.cwd()}/tests/fixtures/project`, }).getAll(); }); - it('local settings take precendence over global settings', function() { - var command = new Command({ - ui: ui, - analytics: analytics, - project: project, - settings: settings + it('local settings take precedence over global settings', function () { + let command = new Command({ + ui, + project, + settings, }); - var args = command.parseArgs(); + let args = command.parseArgs(); expect(args.options).to.include( - merge(homeSettings, { - port: 999, - liveReload: false + Object.assign(homeSettings, { + port: 999, + liveReload: false, }) ); }); }); - diff --git a/tests/unit/tasks/addon-install-test.js b/tests/unit/tasks/addon-install-test.js new file mode 100644 index 0000000000..be7e3cb407 --- /dev/null +++ b/tests/unit/tasks/addon-install-test.js @@ -0,0 +1,121 @@ +'use strict'; + +const Task = require('../../../lib/models/task'); +const AddonInstallTask = require('../../../lib/tasks/addon-install'); +const { expect } = require('chai'); +const MockUi = require('console-ui/mock'); + +describe('addon install task', function () { + let ui; + let project; + + beforeEach(function () { + ui = new MockUi(); + project = { + reloadAddons() {}, + }; + }); + + afterEach(function () { + // ui.stopProgress(); + ui = undefined; + project = undefined; + }); + + describe('when no save flag specified in blueprintOptions', function () { + it('calls npm install with --save-dev as a default', function (done) { + let MockNpmInstallTask = class extends Task { + run(options) { + expect(options.save).to.not.be.true; + expect(options['save-dev']).to.be.true; + done(); + return Promise.resolve(); + } + }; + + let addonInstallTask = new AddonInstallTask({ + ui, + project, + NpmInstallTask: MockNpmInstallTask, + }); + + addonInstallTask.run({}); + }); + }); + + describe('when save flag specified in blueprintOptions', function () { + it('calls npm install with --save', function (done) { + let MockNpmInstallTask = class extends Task { + run(options) { + expect(options.save).to.be.true; + expect(options['save-dev']).to.not.be.true; + done(); + return Promise.resolve(); + } + }; + + let addonInstallTask = new AddonInstallTask({ + ui, + project, + NpmInstallTask: MockNpmInstallTask, + }); + + addonInstallTask.run({ + blueprintOptions: { + save: true, + }, + }); + }); + }); + + describe('when saveDev flag specified in blueprintOptions', function () { + it('calls npm install with --save-dev', function (done) { + let MockNpmInstallTask = class extends Task { + run(options) { + expect(options.save).to.not.be.true; + expect(options['save-dev']).to.be.true; + done(); + return Promise.resolve(); + } + }; + + let addonInstallTask = new AddonInstallTask({ + ui, + project, + NpmInstallTask: MockNpmInstallTask, + }); + + addonInstallTask.run({ + blueprintOptions: { + saveDev: true, + }, + }); + }); + }); + + describe('when both save and saveDev flag specified in blueprintOptions', function () { + it('calls npm install with --save', function (done) { + let MockNpmInstallTask = class extends Task { + run(options) { + expect(options.save).to.be.true; + expect(options['save-dev']).to.not.be.true; + done(); + return Promise.resolve(); + } + }; + + let addonInstallTask = new AddonInstallTask({ + ui, + project, + NpmInstallTask: MockNpmInstallTask, + }); + + addonInstallTask.run({ + blueprintOptions: { + save: true, + saveDev: true, + }, + }); + }); + }); +}); diff --git a/tests/unit/tasks/build-watch-test.js b/tests/unit/tasks/build-watch-test.js new file mode 100644 index 0000000000..432b7befa4 --- /dev/null +++ b/tests/unit/tasks/build-watch-test.js @@ -0,0 +1,72 @@ +'use strict'; + +const BuildWatchTask = require('../../../lib/tasks/build-watch'); +const Builder = require('../../../lib/models/builder'); +const MockProject = require('../../helpers/mock-project'); +const { expect } = require('chai'); + +describe('build-watch task', function () { + let task, ui; + + function setupBroccoliBuilder() { + this.builder = { + build() { + return Promise.resolve('build results'); + }, + + cleanup() { + return Promise.resolve('cleanup result'); + }, + + processBuildResult(results) { + return Promise.resolve(results); + }, + }; + } + + function runBuildWatchTask() { + let project = new MockProject(); + + ui = project.ui; + let _builder = new Builder({ + ui, + project, + setupBroccoliBuilder, + onProcessInterrupt: { + addHandler() {}, + removeHandler() {}, + }, + }); + + let _watcher = { + then(fulfillmentHandler, rejectionHandler) { + return Promise.resolve().then(fulfillmentHandler, rejectionHandler); + }, + }; + + task = new BuildWatchTask({ + ui, + project, + }); + + let options = { + outputPath: 'tmp/build-watch-task-test', + environment: 'test', + _builder, + _watcher, + }; + return task.run(options); + } + + describe('onInterrupt', function () { + it('fulfills the run promise and cleans up the builder', async function () { + let runPromise = runBuildWatchTask(); + + Promise.resolve().then(() => task.onInterrupt()); + + await runPromise; + expect(ui.output).to.include('cleaning up...'); + expect(ui.output).to.include('Environment: test'); + }); + }); +}); diff --git a/tests/unit/tasks/git-init-test.js b/tests/unit/tasks/git-init-test.js new file mode 100644 index 0000000000..d921b510c9 --- /dev/null +++ b/tests/unit/tasks/git-init-test.js @@ -0,0 +1,74 @@ +'use strict'; + +const { expect } = require('chai'); +const MockUI = require('console-ui/mock'); +const GitInitTask = require('../../../lib/tasks/git-init'); +const MockProject = require('../../helpers/mock-project'); +let root = process.cwd(); +const td = require('testdouble'); +const tmp = require('tmp-promise'); + +describe('git-init', function () { + let task; + + beforeEach(async function () { + task = new GitInitTask({ + ui: new MockUI(), + project: new MockProject(), + _gitVersion: td.function(), + _gitInit: td.function(), + _gitAdd: td.function(), + _gitCommit: td.function(), + }); + + const { path } = await tmp.dir(); + process.chdir(path); + }); + + afterEach(async function () { + process.chdir(root); + }); + + describe('skipGit: true', function () { + it('does not initialize git', async function () { + await task.run({ + skipGit: true, + }); + expect(task.ui.output).to.not.include('Git:.'); + td.verify(task._gitVersion(), { times: 0 }); + }); + }); + + it('correctly initializes git if git is around, and more or less works', async function () { + td.when(task._gitVersion()).thenResolve(); + td.when(task._gitInit()).thenResolve(); + td.when(task._gitAdd()).thenResolve(); + td.when(task._gitCommit()).thenResolve(); + + await task.run(); + td.verify(task._gitVersion()); + td.verify(task._gitInit()); + td.verify(task._gitAdd()); + td.verify(task._gitCommit()); + + expect(task.ui.output).to.contain('Git: successfully initialized.'); + expect(task.ui.errors).to.equal(''); + }); + + it('skips initializing git, if `git --version` fails', async function () { + td.when(task._gitVersion()).thenReject(); + + await task.run(); + td.verify(task._gitInit(), { times: 0 }); + td.verify(task._gitAdd(), { times: 0 }); + td.verify(task._gitCommit(td.matchers.anything()), { times: 0 }); + + expect(task.ui.output).to.contain(''); + expect(task.ui.errors).to.equal(''); + }); + + it('includes the HOME environment variable in the environment passed to git', function () { + let env = task.buildGitEnvironment(); + expect(env.HOME).to.equal(process.env.HOME); + }); +}); diff --git a/tests/unit/tasks/interactive-new-test.js b/tests/unit/tasks/interactive-new-test.js new file mode 100644 index 0000000000..49de96eac8 --- /dev/null +++ b/tests/unit/tasks/interactive-new-test.js @@ -0,0 +1,190 @@ +'use strict'; + +const { expect } = require('chai'); +const InteractiveNewTask = require('../../../lib/tasks/interactive-new'); + +describe('interactive new task', function () { + let interactiveNewTask; + + beforeEach(function () { + interactiveNewTask = new InteractiveNewTask(); + }); + + afterEach(function () { + interactiveNewTask = null; + }); + + it('it runs', async function () { + const newCommandOptions = {}; + const answers = await interactiveNewTask.run(newCommandOptions, { + blueprint: 'app', + name: 'foo', + langSelection: 'en-US', + packageManager: 'yarn', + ciProvider: 'github', + emberData: true, + }); + + expect(answers).to.deep.equal({ + blueprint: 'app', + name: 'foo', + lang: 'en-US', + packageManager: 'yarn', + ciProvider: 'github', + emberData: true, + }); + }); + + it('it can opt out of ember-data', async function () { + const newCommandOptions = {}; + const answers = await interactiveNewTask.run(newCommandOptions, { + blueprint: 'app', + name: 'foo', + langSelection: 'en-US', + packageManager: 'pnpm', + ciProvider: 'github', + emberData: false, + }); + + expect(answers).to.deep.equal({ + blueprint: 'app', + name: 'foo', + lang: 'en-US', + packageManager: 'pnpm', + ciProvider: 'github', + emberData: false, + }); + }); + + it('it only displays the `name` question when no app/addon name is provided', async function () { + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('name', questions); + + expect(question.when).to.be.true; + + questions = await interactiveNewTask.getQuestions({ name: 'foo' }); + question = getQuestion('name', questions); + + expect(question.when).to.be.false; + }); + + it('it validates the provided app/addon name', async function () { + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('name', questions); + + expect(question.validate('')).to.equal('Please provide a name.'); + expect(question.validate('app')).to.equal(`We currently do not support \`app\` as a name.`); + expect(question.validate('foo')).to.be.true; + }); + + it('it only displays the `langSelection` question when no language is provided', async function () { + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('langSelection', questions); + + expect(question.when).to.be.true; + + questions = await interactiveNewTask.getQuestions({ lang: 'nl-BE' }); + question = getQuestion('langSelection', questions); + + expect(question.when).to.be.false; + }); + + it('it only displays the `langDifferent` question when no language is provided and when the user wants to provide a different language', async function () { + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('langDifferent', questions); + + expect(question.when()).to.be.true; + + questions = await interactiveNewTask.getQuestions({ lang: 'nl-BE' }); + question = getQuestion('langDifferent', questions); + + expect(question.when()).to.be.false; + + questions = await interactiveNewTask.getQuestions(); + question = getQuestion('langDifferent', questions); + + expect(question.when({ langSelection: 'nl-BE' })).to.be.false; + }); + + it('it validates the provided different language', async function () { + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('langDifferent', questions); + + expect(question.validate('')).to.equal('Please provide a valid locale code.'); + expect(question.validate('foo')).to.equal('Please provide a valid locale code.'); + expect(question.validate('nl-BE')).to.be.true; + }); + + it('it only displays the `packageManager` question when no package manager is provided', async function () { + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('packageManager', questions); + + expect(question.when).to.be.true; + + questions = await interactiveNewTask.getQuestions({ packageManager: 'pnpm' }); + question = getQuestion('packageManager', questions); + + expect(question.when).to.be.false; + }); + + it('it displays the correct language choices', async function () { + let userLocale = InteractiveNewTask.DEFAULT_LOCALE; + + class InteractiveNewTaskMock extends InteractiveNewTask { + getUserLocale() { + return userLocale; + } + } + + interactiveNewTask = new InteractiveNewTaskMock(); + + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('langSelection', questions); + + expect(question.choices).to.deep.equal([ + { + name: InteractiveNewTask.DEFAULT_LOCALE, + value: InteractiveNewTask.DEFAULT_LOCALE, + }, + { + name: 'I want to manually provide a different language', + value: null, + }, + ]); + + userLocale = 'nl-BE'; + questions = await interactiveNewTask.getQuestions(); + question = getQuestion('langSelection', questions); + + expect(question.choices).to.deep.equal([ + { + name: InteractiveNewTask.DEFAULT_LOCALE, + value: InteractiveNewTask.DEFAULT_LOCALE, + }, + { + name: 'nl-BE', + value: 'nl-BE', + }, + { + name: 'I want to manually provide a different language', + value: null, + }, + ]); + }); + + it('it only displays the `ciProvider` question when no CI provider is provided', async function () { + let questions = await interactiveNewTask.getQuestions(); + let question = getQuestion('ciProvider', questions); + + expect(question.when).to.be.true; + + questions = await interactiveNewTask.getQuestions({ ciProvider: 'github' }); + question = getQuestion('ciProvider', questions); + + expect(question.when).to.be.false; + }); +}); + +function getQuestion(name, questions) { + return questions.find((question) => question.name === name); +} diff --git a/tests/unit/tasks/npm-install-test.js b/tests/unit/tasks/npm-install-test.js new file mode 100644 index 0000000000..7d0ce65804 --- /dev/null +++ b/tests/unit/tasks/npm-install-test.js @@ -0,0 +1,33 @@ +'use strict'; + +const NpmInstallTask = require('../../../lib/tasks/npm-install'); +const MockUI = require('console-ui/mock'); +const { expect } = require('chai'); + +describe('npm install task', function () { + let npmInstallTask; + let ui; + + beforeEach(function () { + let project = { + root: __dirname, + }; + ui = new MockUI(); + + npmInstallTask = new NpmInstallTask({ + ui, + project, + }); + }); + + afterEach(function () { + ui = undefined; + npmInstallTask = undefined; + }); + + it('skips npm installs if there is no package.json', function () { + return npmInstallTask.run({}).then(() => { + expect(ui.output).to.include('Skipping install: `package.json` not found.'); + }); + }); +}); diff --git a/tests/unit/tasks/npm-task-test.js b/tests/unit/tasks/npm-task-test.js new file mode 100644 index 0000000000..30a28addef --- /dev/null +++ b/tests/unit/tasks/npm-task-test.js @@ -0,0 +1,285 @@ +'use strict'; + +const NpmTask = require('../../../lib/tasks/npm-task'); +const MockUI = require('console-ui/mock'); +const { expect } = require('chai'); +const td = require('testdouble'); +const SilentError = require('silent-error'); + +describe('NpmTask', function () { + describe('checkNpmVersion', function () { + let task, ui; + + beforeEach(function () { + ui = new MockUI(); + task = new NpmTask({ ui }); + task.npm = td.function(); + }); + + it('resolves when a compatible version is found', async function () { + td.when(task.npm(['--version'])).thenResolve({ stdout: '5.2.1' }); + + await task.checkNpmVersion(); + expect(ui.output).to.be.empty; + expect(ui.errors).to.be.empty; + }); + + it('rejects when npm is not found', async function () { + let error = new Error('npm not found'); + error.code = 'ENOENT'; + + td.when(task.npm(['--version'])).thenReject(error); + + await expect(task.checkNpmVersion()).to.be.rejectedWith( + SilentError, + /instructions at https:\/\/github.com\/npm\/npm/ + ); + expect(ui.output).to.be.empty; + expect(ui.errors).to.be.empty; + }); + + it('rejects when an unknown error is thrown', async function () { + td.when(task.npm(['--version'])).thenReject(new Error('foobar?')); + + await expect(task.checkNpmVersion()).to.be.rejectedWith('foobar?'); + expect(ui.output).to.be.empty; + expect(ui.errors).to.be.empty; + }); + }); + + describe('checkYarn', function () { + let task, ui; + + beforeEach(function () { + ui = new MockUI(); + task = new NpmTask({ ui }); + task.yarn = td.function(); + }); + + it('resolves when yarn <2.0.0 is found', async function () { + td.when(task.yarn(['--version'])).thenResolve({ stdout: '1.22.0' }); + + await task.checkYarn(); + expect(ui.output).to.be.empty; + expect(ui.errors).to.be.empty; + }); + + it('resolves when yarn >=2.0.0 is found and using node_modules', async function () { + td.when(task.yarn(['--version'])).thenResolve({ stdout: '3.2.2' }); + td.when(task.yarn(['config', 'get', 'nodeLinker'])).thenResolve({ stdout: 'node-modules' }); + + await task.checkYarn(); + expect(ui.output).to.be.empty; + expect(ui.errors).to.be.empty; + }); + + it('resolves with warning when yarn >=2.0.0 is found and not using node_modules', async function () { + td.when(task.yarn(['--version'])).thenResolve({ stdout: '2.0.0' }); + td.when(task.yarn(['config', 'get', 'nodeLinker'])).thenResolve({ stdout: 'pnp' }); + await task.checkYarn(); + expect(ui.output).to.contain('WARNING'); + expect(ui.errors).to.be.empty; + }); + + it('rejects when yarn is not found', async function () { + let error = new Error('yarn not found'); + error.code = 'ENOENT'; + + td.when(task.yarn(['--version'])).thenReject(error); + + await expect(task.checkYarn()).to.be.rejectedWith( + SilentError, + /instructions at https:\/\/classic.yarnpkg.com\/en\/docs\/install/ + ); + }); + + it('rejects when an unknown error is thrown', async function () { + td.when(task.yarn(['--version'])).thenReject(new Error('foobar?')); + + await expect(task.checkYarn()).to.be.rejectedWith('foobar?'); + }); + }); + + describe('findPackageManager', function () { + let task; + + beforeEach(function () { + task = new NpmTask(); + task.hasYarnLock = td.function(); + task.hasPNPMLock = td.function(); + task.checkYarn = td.function(); + task.checkNpmVersion = td.function(); + }); + + it('resolves when no yarn.lock file was found and npm is compatible', async function () { + td.when(task.hasYarnLock()).thenReturn(false); + td.when(task.hasPNPMLock()).thenReturn(false); + td.when(task.checkNpmVersion()).thenResolve(); + + await task.findPackageManager(); + }); + + it('resolves when no yarn.lock file was found and npm is incompatible', async function () { + td.when(task.hasYarnLock()).thenReturn(false); + td.when(task.hasPNPMLock()).thenReturn(false); + td.when(task.checkNpmVersion()).thenReject(); + + await expect(task.findPackageManager()).to.be.rejected; + }); + + it('resolves when yarn.lock file and yarn were found', async function () { + td.when(task.hasYarnLock()).thenReturn(true); + td.when(task.hasPNPMLock()).thenReturn(false); + td.when(task.checkYarn()).thenResolve({ name: 'yarn', version: '1.22.0' }); + + expect(task.useYarn).to.be.undefined; + let packageManager = await task.findPackageManager(); + expect(packageManager).to.have.property('name', 'yarn'); + expect(packageManager).to.have.property('version', '1.22.0'); + }); + + it('resolves when yarn.lock file was found, yarn was not found and npm is compatible', async function () { + td.when(task.hasYarnLock()).thenReturn(true); + td.when(task.hasPNPMLock()).thenReturn(false); + td.when(task.checkYarn()).thenReject(); + td.when(task.checkNpmVersion()).thenResolve(); + + expect(task.useYarn).to.be.undefined; + await task.findPackageManager(); + expect(task.useYarn).to.not.be.true; + }); + + it('rejects when yarn.lock file was found, yarn was not found and npm is incompatible', async function () { + td.when(task.hasYarnLock()).thenReturn(true); + td.when(task.hasPNPMLock()).thenReturn(false); + td.when(task.checkYarn()).thenReject(); + td.when(task.checkNpmVersion()).thenReject(); + + await expect(task.findPackageManager()).to.be.rejected; + }); + + it('resolves when yarn is requested and found', async function () { + td.when(task.checkYarn()).thenResolve({ name: 'yarn', version: '1.22.0' }); + + await task.findPackageManager('yarn'); + }); + + it('rejects with SilentError when yarn is requested but not found', async function () { + let error = new SilentError( + 'Ember CLI is now using yarn, but was not able to find it.\n' + + 'Please install yarn using the instructions at https://classic.yarnpkg.com/en/docs/install' + ); + + td.when(task.checkYarn()).thenReject(error); + + await expect(task.findPackageManager('yarn')).to.be.rejectedWith( + SilentError, + /instructions at https:\/\/classic.yarnpkg.com\/en\/docs\/install/ + ); + }); + + it('rejects when yarn is requested and yarn check errors', async function () { + td.when(task.checkYarn()).thenReject(new Error('foobar')); + + await expect(task.findPackageManager('yarn')).to.be.rejectedWith('foobar'); + }); + + it('resolves when npm is requested and compatible', async function () { + td.when(task.checkNpmVersion()).thenResolve(); + + await task.findPackageManager('npm'); + }); + + it('rejects when npm is requested but incompatible', async function () { + td.when(task.checkNpmVersion()).thenReject(); + + await expect(task.findPackageManager('npm')).to.be.rejected; + }); + }); + + describe('toYarnArgs', function () { + let task; + + beforeEach(function () { + task = new NpmTask(); + task.packageManager = { name: 'yarn', version: '1.22.0' }; + }); + + it('correctly adds "--non-interactive" for yarn versions <2.0.0', function () { + let args = task.toYarnArgs('install', {}); + expect(args).to.deep.equal(['install', '--non-interactive']); + }); + + it('skips "--non-interactive" for yarn versions >=2.0.0', function () { + task.packageManager.version = '2.0.1'; + let args = task.toYarnArgs('install', {}); + expect(args).to.deep.equal(['install']); + }); + + it('converts "npm install --no-optional" to "yarn install --ignore-optional"', function () { + let args = task.toYarnArgs('install', { optional: false }); + + expect(args).to.deep.equal(['install', '--ignore-optional', '--non-interactive']); + }); + + it('converts "npm install --save foobar" to "yarn add foobar"', function () { + let args = task.toYarnArgs('install', { save: true, packages: ['foobar'] }); + + expect(args).to.deep.equal(['add', 'foobar', '--non-interactive']); + }); + + it('converts "npm install --save-dev --save-exact foo" to "yarn add --dev --exact foo"', function () { + let args = task.toYarnArgs('install', { + 'save-dev': true, + 'save-exact': true, + packages: ['foo'], + }); + + expect(args).to.deep.equal(['add', '--dev', '--exact', 'foo', '--non-interactive']); + }); + + it('converts "npm uninstall bar" to "yarn remove bar"', function () { + let args = task.toYarnArgs('uninstall', { packages: ['bar'] }); + + expect(args).to.deep.equal(['remove', 'bar', '--non-interactive']); + }); + + it('throws when "yarn install" is called with packages', function () { + expect(() => task.toYarnArgs('install', { packages: ['foo'] })).to.throw(Error, /install foo/); + }); + }); + + describe('toPNPMArgs', function () { + let task; + + beforeEach(function () { + task = new NpmTask(); + }); + + it('converts "npm install --save foobar" to "pnpm add foobar"', function () { + let args = task.toPNPMArgs('install', { save: true, packages: ['foobar'] }); + + expect(args).to.deep.equal(['add', 'foobar']); + }); + + it('converts "npm install --save-dev --save-exact foo" to "pnpm add --save-dev --save-exact foo"', function () { + let args = task.toPNPMArgs('install', { + 'save-dev': true, + 'save-exact': true, + packages: ['foo'], + }); + + expect(args).to.deep.equal(['add', '--save-dev', '--save-exact', 'foo']); + }); + + it('converts "npm uninstall bar" to "pnpm remove bar"', function () { + let args = task.toPNPMArgs('uninstall', { packages: ['bar'] }); + + expect(args).to.deep.equal(['remove', 'bar']); + }); + + it('throws when "pnpm install" is called with packages', function () { + expect(() => task.toPNPMArgs('install', { packages: ['foo'] })).to.throw(Error, /install foo/); + }); + }); +}); diff --git a/tests/unit/tasks/serve-test.js b/tests/unit/tasks/serve-test.js new file mode 100644 index 0000000000..4eeb723631 --- /dev/null +++ b/tests/unit/tasks/serve-test.js @@ -0,0 +1,92 @@ +'use strict'; + +const ServeTask = require('../../../lib/tasks/serve'); +const Builder = require('../../../lib/models/builder'); +const MockProject = require('../../helpers/mock-project'); +const { expect } = require('chai'); + +describe('serve task', function () { + let task, ui; + + function setupBroccoliBuilder() { + this.builder = { + build() { + return Promise.resolve('build results'); + }, + + cleanup() { + return Promise.resolve('cleanup result'); + }, + + processBuildResult(results) { + return Promise.resolve(results); + }, + }; + } + + function runServeTask(path) { + let project = new MockProject(); + + ui = project.ui; + let _builder = new Builder({ + ui, + project, + setupBroccoliBuilder, + onProcessInterrupt: { + addHandler() {}, + removeHandler() {}, + }, + }); + + let _watcher = {}; + let _expressServer = { + start() { + return Promise.resolve(); + }, + }; + let _liveReloadServer = _expressServer; + + task = new ServeTask({ + ui, + project, + }); + + let options = { + outputPath: 'tmp/serve-task-test', + environment: 'test', + _builder, + _watcher, + _expressServer, + _liveReloadServer, + path, + }; + return task.run(options); + } + + describe('run with path', function () { + it(`Throws error if path doesn't exist`, function () { + expect(runServeTask('xyz')).to.be.rejectedWith( + 'The path xyz does not exist. Please specify a valid build directory to serve.' + ); + }); + + it(`Serves ember app from given path`, async function () { + runServeTask('docs'); + + await Promise.resolve(); + expect(ui.output).to.be.contains('– Serving on'); + }); + }); + + describe('onInterrupt', function () { + it('fulfills the run promise and cleans up the builder', async function () { + let servePromise = runServeTask(); + + await Promise.resolve(); + task.onInterrupt(); + + await servePromise; + expect(ui.output).to.include('cleaning up...'); + }); + }); +}); diff --git a/tests/unit/tasks/server/express-server-test.js b/tests/unit/tasks/server/express-server-test.js index 870f6a3347..3cda4c696d 100644 --- a/tests/unit/tasks/server/express-server-test.js +++ b/tests/unit/tasks/server/express-server-test.js @@ -1,211 +1,259 @@ 'use strict'; -var expect = require('chai').expect; -var ExpressServer = require('../../../../lib/tasks/server/express-server'); -var Promise = require('../../../../lib/ext/promise'); -var MockUI = require('../../../helpers/mock-ui'); -var MockProject = require('../../../helpers/mock-project'); -var MockWatcher = require('../../../helpers/mock-watcher'); -var MockServerWatcher = require('../../../helpers/mock-server-watcher'); -var ProxyServer = require('../../../helpers/proxy-server'); -var chalk = require('chalk'); -var request = require('supertest'); -var net = require('net'); -var EOL = require('os').EOL; -var nock = require('nock'); -var express = require('express'); - -describe('express-server', function() { - var subject, ui, project, proxy, nockProxy; +const { expect } = require('chai'); +const ExpressServer = require('../../../../lib/tasks/server/express-server'); +const MockUI = require('console-ui/mock'); +const MockProject = require('../../../helpers/mock-project'); +const MockWatcher = require('../../../helpers/mock-watcher'); +const MockServerWatcher = require('../../../helpers/mock-server-watcher'); +const ProxyServer = require('../../../helpers/proxy-server'); +const { default: chalk } = require('chalk'); +const request = require('supertest'); +const net = require('net'); +const EOL = require('os').EOL; +const nock = require('nock'); +const express = require('express'); +const WebSocket = require('websocket').w3cwebsocket; +const FixturifyProject = require('../../../helpers/fixturify-project'); + +function checkMiddlewareOptions(options) { + expect(options).to.satisfy((option) => option.rootURL); +} + +function sleep(timeout) { + return new Promise((resolve) => setTimeout(resolve, timeout)); +} + +describe('express-server: processAppMiddlewares', function () { + let subject, fixturifyProject; + nock.enableNetConnect(); - beforeEach(function() { - this.timeout(10000); - ui = new MockUI(); - project = new MockProject(); - proxy = new ProxyServer(); + function makeSubject() { subject = new ExpressServer({ - ui: ui, - project: project, + ui: new MockUI(), + project: fixturifyProject.buildProjectModel(), watcher: new MockWatcher(), serverWatcher: new MockServerWatcher(), serverRestartDelayTime: 100, serverRoot: './server', - proxyMiddleware: function() { - return proxy.handler.bind(proxy); - }, - environment: 'development' + environment: 'development', }); + return subject; + } + + beforeEach(function () { + this.timeout(1000); + fixturifyProject = new FixturifyProject('awesome-proj', '0.0.0'); + fixturifyProject.addDevDependency('ember-cli', '*'); }); - afterEach(function() { - try { - subject.httpServer.close(); - } catch(err) { } - try { - proxy.httpServer.close(); - } catch(err) { } + afterEach(async function () { + fixturifyProject.dispose(); + await subject.stopHttpServer().catch(() => {}); }); - describe('displayHost', function() { - it('should use the specified host if not 0.0.0.0', function() { - expect(subject.displayHost('1.2.3.4')).to.equal('1.2.3.4'); + it('has a good error message if a file "server.js" exists, but does not export a function', function () { + fixturifyProject.addFiles({ + 'server.js': 'module.exports = { name: "foo" }', }); + let subject = makeSubject(); + + expect(() => { + subject.processAppMiddlewares(); + }).to.throw(TypeError, 'ember-cli expected ./server/index.js to be the entry for your mock or proxy server'); + }); - it('should use the use localhost if specified host is 0.0.0.0', function() { - expect(subject.displayHost('0.0.0.0')).to.equal('localhost'); + it('has a good error message if a file "server/index.js" exists, but does not export a function', function () { + fixturifyProject.addFiles({ + server: { + 'index.js': 'module.exports = { name: "foo" }', + }, }); + + let subject = makeSubject(); + expect(() => { + subject.processAppMiddlewares(); + }).to.throw(TypeError, 'ember-cli expected ./server/index.js to be the entry for your mock or proxy server'); }); - describe('processAppMiddlewares', function() { - it('has a good error message if a file exists, but does not export a function', function() { - subject.project = { - has: function() { return true; }, - require: function() { return {}; } - }; + it('returns values returned by server/index.js', function () { + fixturifyProject.addFiles({ + server: { + 'index.js': 'module.exports = function() {return "foo"}', + }, + }); + let subject = makeSubject(); + expect(subject.processAppMiddlewares()).to.equal('foo'); + }); - expect(function() { - subject.processAppMiddlewares(); - }).to.throw(TypeError, 'ember-cli expected ./server/index.js to be the entry for your mock or proxy server'); + it('returns values returned by server.js', function () { + fixturifyProject.addFiles({ + 'server.js': 'module.exports = function() {return "foo"}', }); + let subject = makeSubject(); + expect(subject.processAppMiddlewares()).to.equal('foo'); + }); - it('returns values returned by server/index', function(){ - subject.project = { - has: function() { return true; }, - require: function() { - return function(){ return 'foo'; }; - } - }; + it('returns undefined if middleware files does not exists', function () { + let subject = makeSubject(); + expect(subject.processAppMiddlewares()).to.equal(undefined); + }); - expect(subject.processAppMiddlewares()).to.equal('foo'); + it('allow non MODULE_NOT_FOUND errors bubbling if issue happens during module initialization', function () { + fixturifyProject.addFiles({ + server: { + 'index.js': 'throw new Error("OOPS")', + }, }); + let subject = makeSubject(); + expect(() => subject.processAppMiddlewares()).to.throw(Error, 'OOPS'); }); +}); - describe('output', function() { - this.timeout(40000); +describe('express-server', function () { + let subject, ui, project, proxy, nockProxy; + nock.enableNetConnect(); - it('with ssl', function() { - return subject.start({ - host: '0.0.0.0', - port: '1337', - ssl: true, - sslCert: 'tests/fixtures/ssl/server.crt', - sslKey: 'tests/fixtures/ssl/server.key', - baseURL: '/' - }).then(function() { - var output = ui.output.trim().split(EOL); - expect(output[0]).to.equal('Serving on https://localhost:1337/'); - }); + beforeEach(function () { + this.timeout(10000); + ui = new MockUI(); + project = new MockProject(); + proxy = new ProxyServer(); + subject = new ExpressServer({ + ui, + project, + watcher: new MockWatcher(), + serverWatcher: new MockServerWatcher(), + serverRestartDelayTime: 100, + serverRoot: './server', + environment: 'development', }); + }); - it('with proxy', function() { - return subject.start({ - proxy: 'http://localhost:3001/', - host: '0.0.0.0', - port: '1337', - baseURL: '/' - }).then(function() { - var output = ui.output.trim().split(EOL); - expect(output[1]).to.equal('Serving on http://localhost:1337/'); - expect(output[0]).to.equal('Proxying to http://localhost:3001/'); - expect(output.length).to.equal(2, 'expected only two lines of output'); + afterEach(async function () { + await subject + .stopHttpServer() + .catch(() => {}) + .then(() => { + try { + proxy.httpServer.close(); + } catch (err) { + /* ignore */ + } }); - }); + }); + + it('address in use', function () { + let preexistingServer = net.createServer(); + preexistingServer.listen(1337); - it('without proxy', function() { - return subject.start({ - host: '0.0.0.0', + return subject + .start({ + host: undefined, port: '1337', - baseURL: '/' - }).then(function() { - var output = ui.output.trim().split(EOL); - expect(output[0]).to.equal('Serving on http://localhost:1337/'); - expect(output.length).to.equal(1, 'expected only one line of output'); + }) + .then(function () { + expect(false, 'should have rejected').to.be.ok; + }) + .catch(function (reason) { + expect(reason.message).to.equal( + 'Could not serve on http://localhost:1337. It is either in use or you do not have permission.' + ); + }) + .finally(function () { + preexistingServer.close(); }); + }); + + describe('displayHost', function () { + it('should use the specified host if specified', function () { + expect(subject.displayHost('1.2.3.4')).to.equal('1.2.3.4'); }); - it('with baseURL', function() { - return subject.start({ - host: '0.0.0.0', - port: '1337', - baseURL: '/foo' - }).then(function() { - var output = ui.output.trim().split(EOL); - expect(output[0]).to.equal('Serving on http://localhost:1337/foo/'); - expect(output.length).to.equal(1, 'expected only one line of output'); - }); + it('should use the use localhost if host is not specified', function () { + expect(subject.displayHost(undefined)).to.equal('localhost'); }); + }); + + describe('output', function () { + this.timeout(40000); - it('address in use', function() { - var preexistingServer = net.createServer(); + it('address in use', function () { + let preexistingServer = net.createServer(); preexistingServer.listen(1337); - return subject.start({ - host: '0.0.0.0', - port: '1337' - }) - .then(function() { - expect(false, 'should have rejected'); + return expect( + subject.start({ + host: undefined, + port: '1337', }) - .catch(function(reason) { - expect(reason.message).to.equal('Could not serve on http://localhost:1337. It is either in use or you do not have permission.'); + ) + .to.be.rejected.then((reason) => { + expect(reason.message).to.equal( + 'Could not serve on http://localhost:1337. It is either in use or you do not have permission.' + ); }) - .finally(function() { + .finally(function () { preexistingServer.close(); }); }); }); - describe('behaviour', function() { - it('starts with ssl if ssl option is passed', function() { - - return subject.start({ - host: 'localhost', - port: '1337', - ssl: true, - sslCert: 'tests/fixtures/ssl/server.crt', - sslKey: 'tests/fixtures/ssl/server.key', - baseURL: '/' - }) - .then(function() { - return new Promise(function(resolve, reject) { + describe('behaviour', function () { + it('starts with ssl if ssl option is passed', function () { + return subject + .start({ + host: 'localhost', + port: '1337', + ssl: true, + sslCert: 'tests/fixtures/ssl/server.crt', + sslKey: 'tests/fixtures/ssl/server.key', + rootURL: '/', + }) + .then(function () { + return new Promise(function (resolve, reject) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - request('https://localhost:1337', {strictSSL: false}). - get('/').expect(200, function(err, value) { + request('https://localhost:1337', { strictSSL: false }) + .get('/') + .expect(200, function (err, value) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'; - if(err) { reject(err); } - else { resolve(value); } + if (err) { + reject(err); + } else { + resolve(value); + } }); }); }); - }), - + }); - it('app middlewares are processed before the proxy', function(done) { - var expected = '/foo was hit'; + it('app middlewares are processed before the proxy', function (done) { + let expected = '/foo was hit'; - project.require = function() { - return function(app) { - app.use('/foo', function(req,res) { + project.require = function () { + return function (app) { + app.use('/foo', function (req, res) { res.send(expected); }); }; }; - subject.start({ - proxy: 'http://localhost:3001/', - host: '0.0.0.0', - port: '1337', - baseURL: '/' - }) - .then(function() { + subject + .start({ + proxy: 'http://localhost:3001/', + host: undefined, + port: '1337', + rootURL: '/', + }) + .then(function () { request(subject.app) .get('/foo') .set('accept', 'application/json, */*') - .expect(function(res) { + .expect((res) => { expect(res.text).to.equal(expected); }) - .end(function(err) { + .end(function (err) { if (err) { return done(err); } @@ -214,31 +262,33 @@ describe('express-server', function() { }); }); }); - it('works with a regular express app', function(done) { - var expected = '/foo was hit'; - project.require = function() { - var app = express(); - app.use('/foo', function(req,res) { + it('works with a regular express app', function (done) { + let expected = '/foo was hit'; + + project.require = function () { + let app = express(); + app.use('/foo', function (req, res) { res.send(expected); }); return app; }; - subject.start({ - proxy: 'http://localhost:3001/', - host: '0.0.0.0', - port: '1337', - baseURL: '/' - }) - .then(function() { + subject + .start({ + proxy: 'http://localhost:3001/', + host: undefined, + port: '1337', + rootURL: '/', + }) + .then(function () { request(subject.app) .get('/foo') .set('accept', 'application/json, */*') - .expect(function(res) { + .expect((res) => { expect(res.text).to.equal(expected); }) - .end(function(err) { + .end(function (err) { if (err) { return done(err); } @@ -247,13 +297,120 @@ describe('express-server', function() { }); }); }); - describe('with proxy', function() { - beforeEach(function() { + + describe('compression', function () { + let longText = ''; + for (let i = 0; i < 10000; ++i) { + longText += 'x'; + } + longText += ''; + it('uses compression by default for long texts', function (done) { + project.require = function () { + let app = express(); + app.use('/foo', function (req, res) { + res.send(longText); + }); + return app; + }; + + subject + .start({ + proxy: 'http://localhost:3001/', + host: undefined, + port: '1337', + rootURL: '/', + }) + .then(function () { + request(subject.app) + .get('/foo') + .expect(function (res) { + expect(res.text).to.equal(longText); + expect(res.header['content-encoding']).to.equal('gzip'); + }) + .end(function (err) { + if (err) { + return done(err); + } + expect(proxy.called).to.equal(false); + done(); + }); + }); + }); + + it('does not use compression even for long texts when the x-no-compression header is sent in the response', function (done) { + project.require = function () { + let app = express(); + app.use('/foo', function (req, res) { + (res.set('x-no-compression', 'true'), res.send(longText)); + }); + return app; + }; + + subject + .start({ + proxy: 'http://localhost:3001/', + host: undefined, + port: '1337', + rootURL: '/', + }) + .then(function () { + request(subject.app) + .get('/foo') + .set('accept', 'application/json, */*') + .expect(function (res) { + expect(res.text).to.equal(longText); + expect(res.header['content-encoding']).to.not.exist; + expect(parseInt(res.header['content-length'], 10)).to.equal(longText.length); + }) + .end(function (err) { + if (err) { + return done(err); + } + expect(proxy.called).to.equal(false); + done(); + }); + }); + }); + + it('does not use compression for server sent events', async function () { + project.require = function () { + let app = express(); + app.use('/foo', function (req, res) { + res.set('Content-Type', 'text/event-stream'); + res.send(longText); + }); + return app; + }; + + await subject.start({ + proxy: 'http://localhost:3001/', + host: undefined, + port: '1337', + rootURL: '/', + compression: true, + }); + + await request(subject.app) + .get('/foo') + .set('accept', 'application/json, */*') + .expect(function (res) { + expect(res.text).to.equal(longText); + expect(res.header['content-encoding']).to.not.exist; + expect(parseInt(res.header['content-length'], 10)).to.equal(longText.length); + }); + + expect(proxy.called).to.equal(false); + }); + }); + + describe('with proxy', function () { + beforeEach(function () { return subject.start({ proxy: 'http://localhost:3001/', - host: '0.0.0.0', + host: undefined, port: '1337', - baseURL: '/' + rootURL: '/', + liveReload: true, }); }); @@ -261,106 +418,163 @@ describe('express-server', function() { request(app) .get(url) .set('accept', 'text/html') - .end(function(err, response) { + .end(function (err, response) { if (err) { return done(err); } expect(proxy.called).to.equal(false); - if (responseCallback) { responseCallback(response); } + if (responseCallback) { + responseCallback(response); + } done(); }); } - it('bypasses proxy for /', function(done) { + it('bypasses proxy for /', function (done) { bypassTest(subject.app, '/', done); }); - it('bypasses proxy for files that exist', function(done) { - bypassTest(subject.app, '/test-file.txt', done, function(response) { + it('bypasses proxy for files that exist', function (done) { + bypassTest(subject.app, '/test-file.txt', done, function (response) { expect(response.text.trim()).to.equal('some contents'); }); }); function apiTest(app, method, url, done) { - var req = request(app); - return req[method].call(req, url) + let req = request(app); + return req[method] + .call(req, url) + .set('content-length', 0) .set('accept', 'text/json') - .end(function(err) { + .end(function (err) { if (err) { return done(err); } - expect(proxy.called, 'proxy receives the request'); + expect(proxy.called, 'proxy receives the request').to.equal(true); expect(proxy.lastReq.method).to.equal(method.toUpperCase()); expect(proxy.lastReq.url).to.equal(url); done(); }); } - it('proxies GET', function(done) { + + it('proxies GET', function (done) { apiTest(subject.app, 'get', '/api/get', done); }); - it('proxies PUT', function(done) { + + it('proxies PUT', function (done) { apiTest(subject.app, 'put', '/api/put', done); }); - it('proxies POST', function(done) { + + it('proxies POST', function (done) { apiTest(subject.app, 'post', '/api/post', done); }); - it('proxies DELETE', function(done) { + + it('proxies DELETE', function (done) { apiTest(subject.app, 'delete', '/api/delete', done); }); + + it('proxies websockets', function (done) { + let number = Math.round(Math.random() * 0xffffff); + let client = new WebSocket('ws://localhost:1337/foo'); + + client.onerror = (error) => { + done(error); // fail the test + }; + + client.onopen = () => { + client.send(number.toString()); + + setTimeout(() => { + client.close(); + + setTimeout(() => { + expect(proxy.websocketEvents).to.deep.eql(['connect', `message: ${number}`, 'close']); + done(); + }, 10); + }, 10); + }; + }); + // test for #1263 - it('proxies when accept contains */*', function(done) { + it('proxies when accept contains */*', function (done) { request(subject.app) .get('/api/get') .set('accept', 'application/json, */*') - .end(function(err) { + .end(function (err) { if (err) { return done(err); } - expect(proxy.called, 'proxy receives the request'); + expect(proxy.called, 'proxy receives the request').to.equal(true); done(); }); }); + + it('handles errors gracefully when proxy target is down', function (done) { + proxy.httpServer.close(); + + request(subject.httpServer) + .get('/api/get') + .end(function (err, res) { + if (err) { + return done(err); + } + expect(res.status, 'proxied request fails gracefully').to.equal(502); + done(); + }); + }); + + it('handles websocket errors gracefully when proxy target is down', function (done) { + proxy.httpServer.close(); + + let client = new WebSocket('ws://localhost:1337/foo'); + client.onerror = (error) => { + expect(error).to.be.ok; + done(); + }; + }); }); - describe('proxy with subdomain', function() { - beforeEach(function() { + describe('proxy with subdomain', function () { + beforeEach(function () { nockProxy = { called: null, method: null, - url: null + url: null, }; return subject.start({ proxy: 'http://api.lvh.me', - host: '0.0.0.0', + host: undefined, port: '1337', - baseURL: '/' + rootURL: '/', }); }); function apiTest(app, method, url, done) { - var req = request(app); - return req[method].call(req, url) + let req = request(app); + return req[method] + .call(req, url) .set('accept', 'text/json') - .end(function(err) { + .end(function (err) { if (err) { return done(err); } - expect(nockProxy.called, 'proxy receives the request'); + expect(nockProxy.called, 'proxy receives the request').to.equal(true); expect(nockProxy.method).to.equal(method.toUpperCase()); expect(nockProxy.url).to.equal(url); done(); }); } - it('proxies GET', function(done) { + it('proxies GET', function (done) { nock('http://api.lvh.me', { reqheaders: { - 'host': 'api.lvh.me' - } - }).get('/api/get') - .reply(200, function() { + host: 'api.lvh.me', + }, + }) + .get('/api/get') + .reply(200, function () { nockProxy.called = true; nockProxy.method = 'GET'; nockProxy.url = '/api/get'; @@ -370,13 +584,15 @@ describe('express-server', function() { apiTest(subject.app, 'get', '/api/get', done); }); - it('proxies PUT', function(done) { + + it('proxies PUT', function (done) { nock('http://api.lvh.me', { reqheaders: { - 'host': 'api.lvh.me' - } - }).put('/api/put') - .reply(204, function() { + host: 'api.lvh.me', + }, + }) + .put('/api/put') + .reply(204, function () { nockProxy.called = true; nockProxy.method = 'PUT'; nockProxy.url = '/api/put'; @@ -386,13 +602,15 @@ describe('express-server', function() { apiTest(subject.app, 'put', '/api/put', done); }); - it('proxies POST', function(done) { + + it('proxies POST', function (done) { nock('http://api.lvh.me', { reqheaders: { - 'host': 'api.lvh.me' - } - }).post('/api/post') - .reply(201, function() { + host: 'api.lvh.me', + }, + }) + .post('/api/post') + .reply(201, function () { nockProxy.called = true; nockProxy.method = 'POST'; nockProxy.url = '/api/post'; @@ -402,13 +620,15 @@ describe('express-server', function() { apiTest(subject.app, 'post', '/api/post', done); }); - it('proxies DELETE', function(done) { + + it('proxies DELETE', function (done) { nock('http://api.lvh.me', { reqheaders: { - 'host': 'api.lvh.me' - } - }).delete('/api/delete') - .reply(204, function() { + host: 'api.lvh.me', + }, + }) + .delete('/api/delete') + .reply(204, function () { nockProxy.called = true; nockProxy.method = 'DELETE'; nockProxy.url = '/api/delete'; @@ -418,11 +638,12 @@ describe('express-server', function() { apiTest(subject.app, 'delete', '/api/delete', done); }); + // test for #1263 - it('proxies when accept contains */*', function(done) { + it('proxies when accept contains */*', function (done) { nock('http://api.lvh.me') .get('/api/get') - .reply(200, function() { + .reply(200, function () { nockProxy.called = true; nockProxy.method = 'GET'; nockProxy.url = '/api/get'; @@ -433,527 +654,599 @@ describe('express-server', function() { request(subject.app) .get('/api/get') .set('accept', 'application/json, */*') - .end(function(err) { + .end(function (err) { if (err) { return done(err); } - expect(nockProxy.called, 'proxy receives the request'); + expect(nockProxy.called, 'proxy receives the request').to.equal(true); done(); }); }); }); - describe('without proxy', function() { - function startServer(baseURL) { + describe('without proxy', function () { + function startServer(rootURL) { return subject.start({ - host: '0.0.0.0', + environment: 'development', + host: undefined, port: '1337', - baseURL: baseURL || '/' + rootURL: rootURL || '/', }); } - it('serves index.html when file not found with auto/history location', function(done) { - return startServer() - .then(function() { - request(subject.app) - .get('/someurl.withperiod') - .set('accept', 'text/html') - .expect(200) - .expect('Content-Type', /html/) - .end(function(err) { - if (err) { - return done(err); - } - done(); - }); - }); + it('serves index.html when file not found with auto/history location', function (done) { + startServer().then(function () { + request(subject.app) + .get('/someurl.withperiod') + .set('accept', 'text/html') + .expect(200) + .expect('Content-Type', /html/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); }); - it('GET /tests serves tests/index.html for mime of */* (hash location)', function(done) { + it('GET /tests serves tests/index.html for mime of */* (hash location)', function (done) { project._config = { - baseURL: '/', - locationType: 'hash' + rootURL: '/', + locationType: 'hash', }; - return startServer() - .then(function() { - request(subject.app) - .get('/tests') - .set('accept', '*/*') - .expect(200) - .expect('Content-Type', /html/) - .end(function(err) { - if (err) { - return done(err); - } - done(); - }); - }); + startServer().then(function () { + request(subject.app) + .get('/tests') + .set('accept', '*/*') + .expect(200) + .expect('Content-Type', /html/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); }); - it('GET /tests serves tests/index.html for mime of */* (auto location)', function(done) { - return startServer() - .then(function() { - request(subject.app) - .get('/tests') - .set('accept', '*/*') - .expect(200) - .expect('Content-Type', /html/) - .end(function(err) { - if (err) { - return done(err); - } - done(); - }); - }); + it('GET /tests serves tests/index.html for mime of */* (auto location)', function (done) { + startServer().then(function () { + request(subject.app) + .get('/tests') + .set('accept', '*/*') + .expect(200) + .expect('Content-Type', /html/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); }); - it('GET /tests/whatever serves tests/index.html when file not found', function(done) { - return startServer() - .then(function() { - request(subject.app) - .get('/tests/whatever') - .set('accept', 'text/html') - .expect(200) - .expect('Content-Type', /html/) - .end(function(err) { - if (err) { - return done(err); - } - done(); - }); - }); + it('GET /tests/whatever serves tests/index.html when file not found', function (done) { + startServer().then(function () { + request(subject.app) + .get('/tests/whatever') + .set('accept', 'text/html') + .expect(200) + .expect('Content-Type', /html/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); }); - it('GET /tests/an-existing-file.tla serves tests/an-existing-file.tla if it is found', function(done) { - return startServer() - .then(function() { - request(subject.app) - .get('/tests/test-file.txt') - .set('accept', 'text/html') - .expect(200) - .expect('some contents') - .expect('Content-Type', /text/) - .end(function(err) { - if (err) { - return done(err); - } - done(); - }); - }); + it('GET /tests/an-existing-file.tla serves tests/an-existing-file.tla if it is found', function (done) { + startServer().then(function () { + request(subject.app) + .get('/tests/test-file.txt') + .set('accept', 'text/html') + .expect(200) + .expect(/some contents/) + .expect('Content-Type', /text/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); }); - it('serves index.html when file not found (with baseURL) with auto/history location', function(done) { - return startServer('/foo') - .then(function() { - request(subject.app) - .get('/foo/someurl') - .set('accept', 'text/html') - .expect(200) - .expect('Content-Type', /html/) - .end(function(err) { - if (err) { - return done(err); - } - done(); - }); - }); + it('serves index.html when file not found (with rootURL) with auto/history location', function (done) { + startServer('/foo').then(function () { + request(subject.app) + .get('/foo/someurl') + .set('accept', 'text/html') + .expect(200) + .expect('Content-Type', /html/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); }); - it('serves index.html when file not found (with baseURL) with custom history location', function(done) { + it('serves index.html when file not found (with rootURL) with auto/history location on root url without trailing slash', function (done) { project._config = { - baseURL: '/', + rootURL: '/foo', + locationType: 'history', + }; + + startServer('/foo').then(function () { + request(subject.app) + .get('/foo') + .set('accept', 'text/html') + .expect(200) + .expect('Content-Type', /html/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); + }); + + it('serves index.html when file not found (with rootURL) with custom history location', function (done) { + project._config = { + rootURL: '/', locationType: 'blahr', - historySupportMiddleware: true + historySupportMiddleware: true, }; - return startServer('/foo') - .then(function() { - request(subject.app) - .get('/foo/someurl') - .set('accept', 'text/html') - .expect(200) - .expect('Content-Type', /html/) - .end(function(err) { - if (err) { - return done(err); - } - done(); - }); - }); + startServer('/foo').then(function () { + request(subject.app) + .get('/foo/someurl') + .set('accept', 'text/html') + .expect(200) + .expect('Content-Type', /html/) + .end(function (err) { + if (err) { + return done(err); + } + done(); + }); + }); }); - it('returns a 404 when file not found with hash location', function(done) { + it('returns a 404 when file not found with hash location', function (done) { project._config = { - baseURL: '/', - locationType: 'hash' + rootURL: '/', + locationType: 'hash', }; - return startServer() - .then(function() { - request(subject.app) - .get('/someurl.withperiod') - .set('accept', 'text/html') - .expect(404) - .end(done); - }); + startServer().then(function () { + request(subject.app).get('/someurl.withperiod').set('accept', 'text/html').expect(404).end(done); + }); }); - it('files that exist in broccoli directory are served up', function(done) { - return startServer() - .then(function() { - request(subject.app) + it('files that exist in broccoli directory are served up', function (done) { + startServer().then(function () { + request(subject.app) .get('/test-file.txt') - .end(function(err, response) { + .end(function (err, response) { expect(response.text.trim()).to.equal('some contents'); done(); }); - }); + }); }); - it('serves static asset up from build output without a period in name', function(done) { - return startServer() - .then(function() { - request(subject.app) - .get('/someurl-without-period') - .expect(200) - .end(function(err, response) { - if (err) { - return done(err); - } + it('serves static asset up from build output without a period in name', function (done) { + startServer().then(function () { + request(subject.app) + .get('/someurl-without-period') + .expect(200) + .end(function (err, response) { + if (err) { + return done(err); + } - expect(response.text.trim()).to.equal('some other content'); + expect(response.body.toString().trim()).to.equal('some other content'); - done(); - }); - }); + done(); + }); + }); }); - it('serves static asset up from build output without a period in name (with baseURL)', function(done) { - return startServer('/foo') - .then(function() { - request(subject.app) - .get('/foo/someurl-without-period') - .expect(200) - .end(function(err, response) { - if (err) { - return done(err); - } + it('serves a static wasm file up from build output with correct Content-Type header', function (done) { + startServer().then(function () { + request(subject.app) + .get('/vendor/foo.wasm') + .expect(200) + .end(function (err, response) { + if (err) { + return done(err); + } - expect(response.text.trim()).to.equal('some other content'); + expect(response.headers['content-type']).to.equal('application/wasm'); - done(); - }); - }); + done(); + }); + }); + }); + + it('serves static asset up from build output without a period in name (with rootURL)', function (done) { + project._config = { + rootURL: '/foo', + }; + + startServer('/foo').then(function () { + request(subject.app) + .get('/foo/someurl-without-period') + .expect(200) + .end(function (err, response) { + if (err) { + return done(err); + } + + expect(response.body.toString().trim()).to.equal('some other content'); + + done(); + }); + }); }); }); - describe('addons', function() { - var calls; - beforeEach(function() { + describe('addons', function () { + let calls; + beforeEach(function () { calls = 0; - subject.processAddonMiddlewares = function() { + subject.processAddonMiddlewares = function (options) { + checkMiddlewareOptions(options); calls++; }; }); - it('calls processAddonMiddlewares upon start', function() { - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - expect(calls).to.equal(1); - }); + it('calls processAddonMiddlewares upon start', function () { + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + expect(calls).to.equal(1); + }); }); }); - describe('addon middleware', function() { - var firstCalls; - var secondCalls; - beforeEach(function() { + describe('addon middleware', function () { + let firstCalls; + let secondCalls; + beforeEach(function () { firstCalls = 0; secondCalls = 0; - project.initializeAddons = function() { }; - project.addons = [{ - serverMiddleware: function() { + project.initializeAddons = function () {}; + project.addons = [ + { + serverMiddleware({ options }) { + checkMiddlewareOptions(options); firstCalls++; - } - }, { - serverMiddleware: function() { + }, + }, + { + serverMiddleware({ options }) { + checkMiddlewareOptions(options); secondCalls++; - } - }, { - doesntGoBoom: null - }]; - + }, + }, + { + doesntGoBoom: null, + }, + ]; }); - it('calls serverMiddleware on the addons on start', function() { - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - expect(firstCalls).to.equal(1); - expect(secondCalls).to.equal(1); - }); + it('calls serverMiddleware on the addons on start', function () { + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + expect(firstCalls).to.equal(1); + expect(secondCalls).to.equal(1); + }); }); - it('calls serverMiddleware on the addons on restart', function() { - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - subject.changedFiles = ['bar.js']; - return subject.restartHttpServer(); - }).then(function() { - expect(firstCalls).to.equal(2); - expect(secondCalls).to.equal(2); - }); + it('calls serverMiddleware on the addons on restart', function () { + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + subject.changedFiles = ['bar.js']; + return subject.restartHttpServer(); + }) + .then(function () { + expect(firstCalls).to.equal(2); + expect(secondCalls).to.equal(2); + }); }); }); - describe('addon middleware is async', function(){ - var order = []; - beforeEach(function() { - project.initializeAddons = function() { }; + describe('addon middleware is async', function () { + let order = []; + beforeEach(function () { + project.initializeAddons = function () {}; project.addons = [ { - serverMiddleware: function () { + serverMiddleware() { order.push('first'); - } + }, }, { - serverMiddleware: function() { - return new Promise(function(resolve) { - setTimeout(function(){ + serverMiddleware() { + return new Promise(function (resolve) { + setTimeout(function () { order.push('second'); resolve(); }, 50); }); - } - }, { - serverMiddleware: function() { + }, + }, + { + serverMiddleware() { order.push('third'); - } - } + }, + }, ]; }); - it('waits for async middleware to complete before the next middleware', function(){ - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - expect(order[0]).to.equal('first'); - expect(order[1]).to.equal('second'); - expect(order[2]).to.equal('third'); - }); + it('waits for async middleware to complete before the next middleware', function () { + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + expect(order[0]).to.equal('first'); + expect(order[1]).to.equal('second'); + expect(order[2]).to.equal('third'); + }); }); }); - describe('addon middleware bubble errors', function(){ - beforeEach(function() { - project.initializeAddons = function() { }; - project.addons = [{ - serverMiddleware: function() { - return Promise.reject('addon middleware fail'); - } - } + describe('addon middleware bubble errors', function () { + beforeEach(function () { + project.initializeAddons = function () {}; + project.addons = [ + { + serverMiddleware() { + return Promise.reject('addon middleware fail'); + }, + }, ]; }); - it('up to server start', function(){ - return subject.start({ - host: '0.0.0.0', - port: '1337' - }) - .catch(function(reason){ + it('up to server start', function () { + return subject + .start({ + host: undefined, + port: '1337', + }) + .catch(function (reason) { expect(reason).to.equal('addon middleware fail'); }); }); }); - describe('app middleware', function() { - var passedOptions; - var calls; + describe('app middleware', function () { + let passedOptions; + let calls; - beforeEach(function() { + beforeEach(function () { passedOptions = null; calls = 0; - subject.processAppMiddlewares = function(options) { + subject.processAppMiddlewares = function (options) { passedOptions = options; calls++; }; }); - it('calls processAppMiddlewares upon start', function() { - var realOptions = { - host: '0.0.0.0', - port: '1337' + it('calls processAppMiddlewares upon start', function () { + let realOptions = { + rootURL: '/', + host: undefined, + port: '1337', }; - return subject.start(realOptions).then(function() { - expect(passedOptions === realOptions).to.equal(true); + return subject.start(realOptions).then(function () { + expect(passedOptions).to.deep.equal(realOptions); expect(calls).to.equal(1); }); }); - it('calls processAppMiddlewares upon restart', function() { - var realOptions = { - host: '0.0.0.0', - port: '1337' + it('calls processAppMiddlewares upon restart', function () { + let realOptions = { + rootURL: '/', + host: undefined, + port: '1337', }; - var originalApp; + let originalApp; - return subject.start(realOptions) - .then(function() { + return subject + .start(realOptions) + .then(function () { originalApp = subject.app; subject.changedFiles = ['bar.js']; return subject.restartHttpServer(); }) - .then(function() { - expect(subject.app); + .then(function () { + expect(subject.app).to.be.ok; expect(originalApp).to.not.equal(subject.app); - expect(passedOptions === realOptions).to.equal(true); + expect(passedOptions).to.deep.equal(realOptions); expect(calls).to.equal(2); }); }); - it('includes httpServer instance in options', function() { - var passedOptions; + it('includes httpServer instance in options', function () { + let passedOptions; - subject.processAppMiddlewares = function(options) { + subject.processAppMiddlewares = function (options) { passedOptions = options; }; - var realOptions = { - host: '0.0.0.0', - port: '1337' + let realOptions = { + host: undefined, + port: '1337', }; - return subject.start(realOptions).then(function() { - expect(!!passedOptions.httpServer.listen); + return subject.start(realOptions).then(function () { + expect(!!passedOptions.httpServer.listen).to.be.ok; }); }); }); - describe('serverWatcherDidChange', function() { - it('is called on file change', function() { - var calls = 0; - subject.serverWatcherDidChange = function() { + describe('serverWatcherDidChange', function () { + it('is called on file change', function () { + let calls = 0; + subject.serverWatcherDidChange = function () { calls++; }; - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - subject.serverWatcher.emit('change', 'foo.txt'); - expect(calls).to.equal(1); - }); + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + subject.serverWatcher.emit('change', 'foo.txt'); + expect(calls).to.equal(1); + }); }); - it('schedules a server restart', function() { - var calls = 0; - subject.scheduleServerRestart = function() { + it('schedules a server restart', function () { + let calls = 0; + subject.scheduleServerRestart = function () { calls++; }; - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - subject.serverWatcher.emit('change', 'foo.txt'); - subject.serverWatcher.emit('change', 'bar.txt'); - expect(calls).to.equal(2); - }); + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + subject.serverWatcher.emit('change', 'foo.txt'); + subject.serverWatcher.emit('change', 'bar.txt'); + expect(calls).to.equal(2); + }); }); }); - describe('scheduleServerRestart', function() { - it('schedules exactly one call of restartHttpServer', function(done) { - var calls = 0; - subject.restartHttpServer = function() { + describe('scheduleServerRestart', function () { + it('schedules exactly one call of restartHttpServer', async function () { + let calls = 0; + + subject.restartHttpServer = function () { calls++; }; subject.scheduleServerRestart(); + // scheduleServerRestart is debounced and only ran after 100ms, + // restartHttpServer shouldn't be called yet expect(calls).to.equal(0); - setTimeout(function() { - expect(calls).to.equal(0); - subject.scheduleServerRestart(); - }, 50); - setTimeout(function() { - expect(calls).to.equal(1); - done(); - }, 175); + + await sleep(50); + + // after a 50ms wait, we still haven't called restartHttpServer since + // we are still within our 100ms debounce time. + expect(calls).to.equal(0); + subject.scheduleServerRestart(); + + await sleep(175); + + // finally, after 175ms we have finally called restartHttpServer, but + // importantly only called it once (all of the other + // `subject.scheduleServerRestart()` calls were within the debounce + // window) + expect(calls).to.equal(1); }); }); - describe('restartHttpServer', function() { - it('restarts the server', function() { - var originalHttpServer; - var originalApp; - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - ui.output = ''; - originalHttpServer = subject.httpServer; - originalApp = subject.app; - subject.changedFiles = ['bar.js']; - return subject.restartHttpServer(); - }).then(function() { - expect(ui.output).to.equal(EOL + chalk.green('Server restarted.') + EOL + EOL); - expect(subject.httpServer, 'HTTP server exists'); - expect(subject.httpServer).to.not.equal(originalHttpServer, 'HTTP server has changed'); - expect(!!subject.app).to.equal(true, 'App exists'); - expect(subject.app).to.not.equal(originalApp, 'App has changed'); - }); + describe('restartHttpServer', function () { + it('restarts the server', function () { + let originalHttpServer; + let originalApp; + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + ui.output = ''; + originalHttpServer = subject.httpServer; + originalApp = subject.app; + subject.changedFiles = ['bar.js']; + return subject.restartHttpServer(); + }) + .then(function () { + expect(ui.output).to.contains(EOL + chalk.green('Server restarted.') + EOL + EOL); + expect(subject.httpServer, 'HTTP server exists').to.be.ok; + expect(subject.httpServer).to.not.equal(originalHttpServer, 'HTTP server has changed'); + expect(!!subject.app).to.equal(true, 'App exists'); + expect(subject.app).to.not.equal(originalApp, 'App has changed'); + }); }); - it('restarts the server again if one or more files change during a previous restart', function() { - var originalHttpServer; - var originalApp; - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - originalHttpServer = subject.httpServer; - originalApp = subject.app; - subject.serverRestartPromise = new Promise(function(resolve) { - setTimeout(function () { - subject.serverRestartPromise = null; - resolve(); - }, 20); + it('restarts the server again if one or more files change during a previous restart', function () { + let originalHttpServer; + let originalApp; + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + originalHttpServer = subject.httpServer; + originalApp = subject.app; + subject.serverRestartPromise = new Promise(function (resolve) { + setTimeout(function () { + subject.serverRestartPromise = null; + resolve(); + }, 20); + }); + subject.changedFiles = ['bar.js']; + return subject.restartHttpServer(); + }) + .then(function () { + expect(!!subject.httpServer).to.equal(true, 'HTTP server exists'); + expect(subject.httpServer).to.not.equal(originalHttpServer, 'HTTP server has changed'); + expect(!!subject.app).to.equal(true, 'App exists'); + expect(subject.app).to.not.equal(originalApp, 'App has changed'); }); - subject.changedFiles = ['bar.js']; - return subject.restartHttpServer(); - }).then(function() { - expect(!!subject.httpServer).to.equal(true, 'HTTP server exists'); - expect(subject.httpServer).to.not.equal(originalHttpServer, 'HTTP server has changed'); - expect(!!subject.app).to.equal(true, 'App exists'); - expect(subject.app).to.not.equal(originalApp, 'App has changed'); - }); }); - it('emits the restart event', function() { - var calls = 0; - subject.on('restart', function() { + it('emits the restart event', function () { + let calls = 0; + subject.on('restart', function () { calls++; }); - return subject.start({ - host: '0.0.0.0', - port: '1337' - }).then(function() { - subject.changedFiles = ['bar.js']; - return subject.restartHttpServer(); - }).then(function() { - expect(calls).to.equal(1); - }); + return subject + .start({ + host: undefined, + port: '1337', + }) + .then(function () { + subject.changedFiles = ['bar.js']; + return subject.restartHttpServer(); + }) + .then(function () { + expect(calls).to.equal(1); + }); }); }); }); diff --git a/tests/unit/tasks/server/livereload-server-test.js b/tests/unit/tasks/server/livereload-server-test.js index b3eed9d00e..ed1e7a2fc0 100644 --- a/tests/unit/tasks/server/livereload-server-test.js +++ b/tests/unit/tasks/server/livereload-server-test.js @@ -1,187 +1,254 @@ 'use strict'; -var expect = require('chai').expect; -var LiveReloadServer = require('../../../../lib/tasks/server/livereload-server'); -var MockUI = require('../../../helpers/mock-ui'); -var MockExpressServer = require('../../../helpers/mock-express-server'); -var net = require('net'); -var EOL = require('os').EOL; -var path = require('path'); -var MockWatcher = require('../../../helpers/mock-watcher'); - -describe('livereload-server', function() { - var subject; - var ui; - var watcher; - var expressServer; - - beforeEach(function() { +const { expect } = require('chai'); +const LiveReloadServer = require('../../../../lib/tasks/server/livereload-server'); +const MockUI = require('console-ui/mock'); +const MockExpressServer = require('../../../helpers/mock-express-server'); +const net = require('net'); +const EOL = require('os').EOL; +const path = require('path'); +const MockWatcher = require('../../../helpers/mock-watcher'); +const express = require('express'); +const FSTree = require('fs-tree-diff'); +const http = require('http'); + +describe('livereload-server', function () { + let subject; + let ui; + let watcher; + let httpServer; + let app; + + beforeEach(function () { ui = new MockUI(); watcher = new MockWatcher(); - expressServer = new MockExpressServer(); - + httpServer = new MockExpressServer(); + app = express(); subject = new LiveReloadServer({ - ui: ui, - watcher: watcher, - expressServer: expressServer, - analytics: { trackError: function() { } }, + app, + ui, + watcher, + httpServer, project: { liveReloadFilterPatterns: [], - root: '/home/user/my-project' - } + root: '/home/user/my-project', + }, }); }); - afterEach(function() { + afterEach(function () { try { - if (subject._liveReloadServer) { - subject._liveReloadServer.close(); + if (subject.liveReloadServer) { + subject.liveReloadServer.close(); } - } catch (err) { } + } catch (err) { + /* ignore */ + } }); - describe('start', function() { - it('does not start the server if `liveReload` option is not true', function() { - return subject.start({ - liveReloadPort: 1337, - liveReload: false, - }).then(function(output) { - expect(output).to.equal('Livereload server manually disabled.'); - expect(!!subject._liveReloadServer).to.equal(false); - }); - }); - - it('correctly indicates which port livereload is present on', function() { - return subject.start({ - liveReloadPort: 1337, - liveReloadHost: 'localhost', - liveReload: true - }).then(function() { - expect(ui.output).to.equal('Livereload server on http://localhost:1337' + EOL); - }); + describe('start', function () { + it('does not start the server if `liveReload` option is not true', function () { + return subject + .setupMiddleware({ + liveReload: false, + liveReloadPort: 4200, + liveReloadPrefix: '/', + }) + .then(function () { + expect(ui.output).to.contains('WARNING: Livereload server manually disabled.'); + expect(!!subject.liveReloadServer).to.equal(false); + }); }); - - it('informs of error during startup', function(done) { - var preexistingServer = net.createServer(); + it('informs of error during startup with custom port', async function () { + let preexistingServer = net.createServer(); preexistingServer.listen(1337); - return subject.start({ + try { + await subject.setupMiddleware({ + liveReload: true, liveReloadPort: 1337, - liveReload: true - }) - .catch(function(reason) { - expect(reason).to.equal('Livereload failed on http://localhost:1337. It is either in use or you do not have permission.' + EOL); + liveReloadPrefix: '/', + port: 4200, + }); + } catch (e) { + expect(e.message).to.include(`Livereload failed on 'http://localhost:1337', It may be in use.`); + } finally { + await new Promise((resolve) => preexistingServer.close(resolve)); + } + }); + it('starts with custom host, custom port', function () { + return subject + .setupMiddleware({ + liveReloadHost: '127.0.0.1', + liveReload: true, + liveReloadPort: 1377, + liveReloadPrefix: '/', + port: 4200, }) - .finally(function() { - preexistingServer.close(done); + .then(function () { + expect(subject.liveReloadServer.options.port).to.equal(1377); + expect(subject.liveReloadServer.options.host).to.equal('127.0.0.1'); }); }); + it('Livereload responds to livereload requests and returns livereload file', function (done) { + let server = app.listen(4200); + subject + .setupMiddleware({ + liveReload: true, + liveReloadPrefix: '_lr', + port: 4200, + }) + .then(function () { + let httpGetOptions = { headers: { Connection: 'close' } }; - it('starts with custom host', function() { - return subject.start({ - liveReloadHost: '127.0.0.1', - liveReloadPort: 1337, - liveReload: true - }).then(function() { - expect(ui.output).to.equal('Livereload server on http://127.0.0.1:1337' + EOL); - }); + http.get('http://localhost:4200/_lr/livereload.js', httpGetOptions, function (response) { + expect(response.statusCode).to.equal(200); + server.close(done); + }); + }); }); }); - - describe('start with https', function() { - it('correctly indicates which port livereload is present on and running in https mode', function() { - return subject.start({ - liveReloadPort: 1337, - liveReloadHost: 'localhost', - liveReload: true, - ssl: true, - sslKey: 'tests/fixtures/ssl/server.key', - sslCert: 'tests/fixtures/ssl/server.crt' - }).then(function() { - expect(ui.output).to.equal('Livereload server on https://localhost:1337' + EOL); - }); + describe('start with https', function () { + it('correctly runs in https mode', function () { + return subject + .setupMiddleware({ + liveReload: true, + liveReloadPort: 4200, + liveReloadPrefix: '/', + ssl: true, + sslKey: 'tests/fixtures/ssl/server.key', + sslCert: 'tests/fixtures/ssl/server.crt', + port: 4200, + }) + .then(function () { + expect(subject.liveReloadServer.options.key).to.be.an.instanceof(Buffer); + expect(subject.liveReloadServer.options.cert).to.be.an.instanceof(Buffer); + }); }); - it('informs of error during startup', function(done) { - var preexistingServer = net.createServer(); + it('informs of error during startup', async function () { + let preexistingServer = net.createServer(); preexistingServer.listen(1337); - return subject.start({ + try { + await subject.setupMiddleware({ liveReloadPort: 1337, liveReload: true, ssl: true, sslKey: 'tests/fixtures/ssl/server.key', - sslCert: 'tests/fixtures/ssl/server.crt' - }) - .catch(function(reason) { - expect(reason).to.equal('Livereload failed on https://localhost:1337. It is either in use or you do not have permission.' + EOL); + sslCert: 'tests/fixtures/ssl/server.crt', + port: 4200, + }); + + expect.fail('expected rejection'); + } catch (e) { + expect(e.message).to.include(`Livereload failed on 'https://localhost:1337', It may be in use.`); + } finally { + await new Promise((resolve) => preexistingServer.close(resolve)); + } + }); + + it('correctly runs in https mode with custom port', function () { + return subject + .setupMiddleware({ + liveReload: true, + liveReloadPort: 1337, + liveReloadPrefix: '/', + ssl: true, + sslKey: 'tests/fixtures/ssl/server.key', + sslCert: 'tests/fixtures/ssl/server.crt', + port: 4200, }) - .finally(function() { - preexistingServer.close(done); + .then(function () { + expect(subject.liveReloadServer.options.key).to.be.an.instanceof(Buffer); + expect(subject.liveReloadServer.options.cert).to.be.an.instanceof(Buffer); }); }); }); - describe('express server restart', function() { - it('triggers when the express server restarts', function() { - var calls = 0; + describe('express server restart', function () { + it('triggers when the express server restarts', function () { + let calls = 0; subject.didRestart = function () { calls++; }; - - return subject.start({ + return subject + .setupMiddleware({ + liveReload: true, + liveReloadPrefix: '/', + port: 4200, + }) + .then(function () { + subject.app.emit('restart'); + expect(calls).to.equal(1); + }); + }); + it('triggers when the express server restarts with custom port', function () { + let calls = 0; + subject.didRestart = function () { + calls++; + }; + return subject + .setupMiddleware({ + liveReload: true, + liveReloadPrefix: '/', liveReloadPort: 1337, - liveReload: true - }).then(function () { - expressServer.emit('restart'); + port: 4200, + }) + .then(function () { + subject.app.emit('restart'); expect(calls).to.equal(1); }); }); }); describe('livereload changes', function () { - var liveReloadServer; - var changedCount; - var oldChanged; - var stubbedChanged = function() { + let liveReloadServer; + let changedCount; + let oldChanged; + let stubbedChanged = function () { changedCount += 1; }; - var trackCount; - var oldTrack; - var stubbedTrack = function() { - trackCount += 1; + let createStubbedGetDirectoryEntries = function (files) { + return function () { + return files.map(function (file) { + return { + relativePath: file, + isDirectory() { + return false; + }, + }; + }); + }; }; - beforeEach(function() { - liveReloadServer = subject.liveReloadServer(); + beforeEach(function () { + subject.setupMiddleware({ + liveReload: true, + liveReloadPort: 4200, + liveReloadPrefix: '/', + port: 4200, + }); + liveReloadServer = subject.liveReloadServer; changedCount = 0; oldChanged = liveReloadServer.changed; liveReloadServer.changed = stubbedChanged; - - trackCount = 0; - oldTrack = subject.analytics.track; - subject.analytics.track = stubbedTrack; + subject.tree = FSTree.fromEntries([]); }); - afterEach(function() { + afterEach(function () { liveReloadServer.changed = oldChanged; - subject.analytics.track = oldTrack; subject.project.liveReloadFilterPatterns = []; }); describe('watcher events', function () { function watcherEventTest(eventName, expectedCount) { + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(['test/fixtures/proxy/file-a.js']); subject.project.liveReloadFilterPatterns = []; - return subject.start({ - liveReloadPort: 1337, - liveReload: true, - }).then(function () { - watcher.emit(eventName, { - filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js' - }); - }).finally(function () { - expect(changedCount).to.equal(expectedCount); - }); + watcher.emit(eventName, { + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + return expect(changedCount).to.equal(expectedCount); } it('triggers a livereload change on a watcher change event', function () { @@ -195,28 +262,392 @@ describe('livereload-server', function() { it('does not trigger a livereload change on other watcher events', function () { return watcherEventTest('not-an-event', 0); }); + + it('recovers from error when file is already cached in previous cache step', function () { + let compileError = function () { + try { + throw new Error('Compile time error'); + } catch (error) { + return error; + } + }.apply(); + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(['test/fixtures/proxy/file-a.js']); + + watcher.emit('error', compileError); + expect(subject._hasCompileError).to.be.true; + expect(changedCount).to.equal(1); + watcher.emit('change', { + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + expect(subject._hasCompileError).to.be.false; + expect(changedCount).to.equal(2); + }); + + describe('filter pattern', function () { + it('shouldTriggerReload must be true if there are no liveReloadFilterPatterns', function () { + subject.project.liveReloadFilterPatterns = []; + let result = subject.shouldTriggerReload({ + filePath: '/home/user/my-project/app/styles/app.css', + }); + expect(result).to.be.true; + }); + + it('shouldTriggerReload is true when no liveReloadFilterPatterns matches the filePath', function () { + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + let result = subject.shouldTriggerReload({ + filePath: '/home/user/my-project/app/styles/app.css', + }); + expect(result).to.be.true; + }); + + it('shouldTriggerReload is false when one or more of the liveReloadFilterPatterns matches filePath', function () { + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + let result = subject.shouldTriggerReload({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + expect(result).to.be.false; + }); + + it('shouldTriggerReload writes a banner after skipping reload for a file', function () { + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + subject.shouldTriggerReload({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + expect(ui.output).to.equal( + `Skipping livereload for: ${path.join('test', 'fixtures', 'proxy', 'file-a.js')}${EOL}` + ); + }); + + it('triggers the livereload server of a change when no pattern matches', function () { + subject.getDirectoryEntries = createStubbedGetDirectoryEntries([]); + subject.didChange({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + expect(changedCount).to.equal(1); + }); + + it('does not trigger livereload server of a change when there is a pattern match', function () { + // normalize test regex for windows + // path.normalize with change forward slashes to back slashes if test is running on windows + // we then replace backslashes with double backslahes to escape the backslash in the regex + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries([]); + subject.didChange({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + + expect(changedCount).to.equal(0); + }); + }); }); - describe('filter pattern', function() { - it('triggers the livereload server of a change when no pattern matches', function() { - subject.didChange({filePath: ''}); + describe('specific files', function () { + let reloadedFiles; + + let stubbedChanged = function (options) { + reloadedFiles = options.body.files; + }; + + beforeEach(function () { + liveReloadServer.changed = stubbedChanged; + }); + + afterEach(function () { + reloadedFiles = undefined; + }); + + it('triggers livereload with modified files', function () { + let changedFiles = ['assets/my-project.css', 'assets/my-project.js']; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(changedFiles); + subject.didChange({ + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + + expect(reloadedFiles).to.deep.equal(changedFiles); + }); + + it('triggers livereload with deleted directories', function () { + let changedFiles = ['assets/my-project.css', 'assets/my-project.js']; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(changedFiles); + subject.didChange({ + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + expect(reloadedFiles).to.deep.equal(changedFiles); + + // Pretend every files were removed from the tree. + subject.getDirectoryEntries = createStubbedGetDirectoryEntries([]); + subject.didChange({ + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + expect(reloadedFiles).to.deep.equal([]); + }); + + it('triggers livereload ignoring source map files', function () { + let changedFiles = ['assets/my-project.css', 'assets/my-project.css.map']; + + let expectedResult = ['assets/my-project.css']; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(changedFiles); + subject.didChange({ + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + + expect(reloadedFiles).to.deep.equal(expectedResult); + }); + + it('triggers livereload with "LiveReload files" if no results.directory was provided', function () { + let changedOptions; + subject.liveReloadServer = { + changed(options) { + changedOptions = options; + }, + }; + + subject.didChange({}); + + expect(changedOptions).to.deep.equal({ + body: { + files: ['LiveReload files'], + }, + }); + }); + }); + }); + describe('livereload changes with custom port', function () { + let liveReloadServer; + let changedCount; + let oldChanged; + let stubbedChanged = function () { + changedCount += 1; + }; + let createStubbedGetDirectoryEntries = function (files) { + return function () { + return files.map(function (file) { + return { + relativePath: file, + isDirectory() { + return false; + }, + }; + }); + }; + }; + + beforeEach(function () { + subject.setupMiddleware({ + liveReload: true, + liveReloadPort: 1337, + liveReloadPrefix: '/', + port: 4200, + }); + liveReloadServer = subject.liveReloadServer; + changedCount = 0; + oldChanged = liveReloadServer.changed; + liveReloadServer.changed = stubbedChanged; + subject.tree = FSTree.fromEntries([]); + }); + + afterEach(function () { + liveReloadServer.changed = oldChanged; + subject.project.liveReloadFilterPatterns = []; + }); + + describe('watcher events', function () { + function watcherEventTest(eventName, expectedCount) { + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(['test/fixtures/proxy/file-a.js']); + subject.project.liveReloadFilterPatterns = []; + watcher.emit(eventName, { + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + return expect(changedCount).to.equal(expectedCount); + } + + it('triggers a livereload change on a watcher change event', function () { + return watcherEventTest('change', 1); + }); + + it('triggers a livereload change on a watcher error event', function () { + return watcherEventTest('error', 1); + }); + + it('does not trigger a livereload change on other watcher events', function () { + return watcherEventTest('not-an-event', 0); + }); + + it('recovers from error when file is already cached in previous cache step', function () { + let compileError = function () { + try { + throw new Error('Compile time error'); + } catch (error) { + return error; + } + }.apply(); + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(['test/fixtures/proxy/file-a.js']); + + watcher.emit('error', compileError); + expect(subject._hasCompileError).to.be.true; expect(changedCount).to.equal(1); - expect(trackCount).to.equal(1); + watcher.emit('change', { + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + expect(subject._hasCompileError).to.be.false; + expect(changedCount).to.equal(2); + }); + + describe('filter pattern', function () { + it('shouldTriggerReload must be true if there are no liveReloadFilterPatterns', function () { + subject.project.liveReloadFilterPatterns = []; + let result = subject.shouldTriggerReload({ + filePath: '/home/user/my-project/app/styles/app.css', + }); + expect(result).to.be.true; + }); + + it('shouldTriggerReload is true when no liveReloadFilterPatterns matches the filePath', function () { + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + let result = subject.shouldTriggerReload({ + filePath: '/home/user/my-project/app/styles/app.css', + }); + expect(result).to.be.true; + }); + + it('shouldTriggerReload is false when one or more of the liveReloadFilterPatterns matches filePath', function () { + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + let result = subject.shouldTriggerReload({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + expect(result).to.be.false; + }); + + it('shouldTriggerReload writes a banner after skipping reload for a file', function () { + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + subject.shouldTriggerReload({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + expect(ui.output).to.equal( + `Skipping livereload for: ${path.join('test', 'fixtures', 'proxy', 'file-a.js')}${EOL}` + ); + }); + + it('triggers the livereload server of a change when no pattern matches', function () { + subject.getDirectoryEntries = createStubbedGetDirectoryEntries([]); + subject.didChange({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + expect(changedCount).to.equal(1); + }); + + it('does not trigger livereload server of a change when there is a pattern match', function () { + // normalize test regex for windows + // path.normalize with change forward slashes to back slashes if test is running on windows + // we then replace backslashes with double backslahes to escape the backslash in the regex + let basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); + let filter = new RegExp(`^${basePath}`); + + subject.project.liveReloadFilterPatterns = [filter]; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries([]); + subject.didChange({ + filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js', + }); + + expect(changedCount).to.equal(0); + }); }); + }); - it('does not trigger livereload server of a change when there is a pattern match', function() { - // normalize test regex for windows - // path.normalize with change forward slashes to back slashes if test is running on windows - // we then replace backslashes with double backslahes to escape the backslash in the regex - var basePath = path.normalize('test/fixtures/proxy').replace(/\\/g, '\\\\'); - var filter = new RegExp('^' + basePath); - subject.project.liveReloadFilterPatterns = [filter]; + describe('specific files', function () { + let reloadedFiles; + let changedOptions; + let stubbedChanged = function (options) { + reloadedFiles = options.body.files; + changedOptions = options; + }; + + beforeEach(function () { + liveReloadServer.changed = stubbedChanged; + }); + + afterEach(function () { + reloadedFiles = undefined; + }); + + it('triggers livereload with modified files', function () { + let changedFiles = ['assets/my-project.css', 'assets/my-project.js']; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(changedFiles); subject.didChange({ - filePath: '/home/user/my-project/test/fixtures/proxy/file-a.js' + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + + expect(reloadedFiles).to.deep.equal(changedFiles); + }); + + it('triggers livereload with deleted directories', function () { + let changedFiles = ['assets/my-project.css', 'assets/my-project.js']; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(changedFiles); + subject.didChange({ + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + expect(reloadedFiles).to.deep.equal(changedFiles); + + // Pretend every files were removed from the tree. + subject.getDirectoryEntries = createStubbedGetDirectoryEntries([]); + subject.didChange({ + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + expect(reloadedFiles).to.deep.equal([]); + }); + + it('triggers livereload ignoring source map files', function () { + let changedFiles = ['assets/my-project.css', 'assets/my-project.css.map']; + + let expectedResult = ['assets/my-project.css']; + + subject.getDirectoryEntries = createStubbedGetDirectoryEntries(changedFiles); + subject.didChange({ + directory: '/home/user/projects/my-project/tmp/something.tmp', + }); + + expect(reloadedFiles).to.deep.equal(expectedResult); + }); + + it('triggers livereload with "LiveReload files" if no results.directory was provided', function () { + subject.didChange({}); + + expect(changedOptions).to.deep.equal({ + body: { + files: ['LiveReload files'], + }, }); - expect(changedCount).to.equal(0); - expect(trackCount).to.equal(0); }); }); }); diff --git a/tests/unit/tasks/server/middleware/history-support-test.js b/tests/unit/tasks/server/middleware/history-support-test.js new file mode 100644 index 0000000000..285c76368a --- /dev/null +++ b/tests/unit/tasks/server/middleware/history-support-test.js @@ -0,0 +1,70 @@ +'use strict'; + +const { expect } = require('chai'); +const HistorySupportAddon = require('../../../../../lib/tasks/server/middleware/history-support'); + +describe('HistorySupportAddon', function () { + describe('.serverMiddleware', function () { + it('add middleware when locationType is auto', function () { + let addon = new HistorySupportAddon({ + config() { + return { + locationType: 'auto', + }; + }, + }); + + expect(addon.shouldAddMiddleware()).to.true; + }); + + it('add middleware when locationType is history', function () { + let addon = new HistorySupportAddon({ + config() { + return { + locationType: 'history', + }; + }, + }); + + expect(addon.shouldAddMiddleware()).to.true; + }); + + it('add middleware when locationType is an unknown type', function () { + let addon = new HistorySupportAddon({ + config() { + return { + locationType: 'foo-bar', + historySupportMiddleware: true, + }; + }, + }); + + expect(addon.shouldAddMiddleware()).to.true; + }); + + it('add middleware when historySupportMiddleware is true', function () { + let addon = new HistorySupportAddon({ + config() { + return { + historySupportMiddleware: true, + }; + }, + }); + + expect(addon.shouldAddMiddleware()).to.true; + }); + + it('do not add middleware when historySupportMiddleware is false and locationType is history', function () { + let addon = new HistorySupportAddon({ + config() { + return { + locationType: 'history', + historySupportMiddleware: false, + }; + }, + }); + + expect(addon.shouldAddMiddleware()).to.false; + }); + }); +}); diff --git a/tests/unit/tasks/server/middleware/proxy-server-test.js b/tests/unit/tasks/server/middleware/proxy-server-test.js new file mode 100644 index 0000000000..d48b275342 --- /dev/null +++ b/tests/unit/tasks/server/middleware/proxy-server-test.js @@ -0,0 +1,24 @@ +'use strict'; + +const MockProject = require('../../../../helpers/mock-project'); +const ProxyServerAddon = require('../../../../../lib/tasks/server/middleware/proxy-server'); +const { expect } = require('chai'); + +describe('proxy-server', function () { + let project, proxyServer; + + beforeEach(function () { + project = new MockProject(); + proxyServer = new ProxyServerAddon(project); + }); + + it(`bypass livereload request`, function () { + let options = { + liveReloadPrefix: 'test/', + }; + let req = { + url: '/test/livereload', + }; + expect(proxyServer.handleProxiedRequest({ req, options })).to.undefined; + }); +}); diff --git a/tests/unit/tasks/server/middleware/tests-server-test.js b/tests/unit/tasks/server/middleware/tests-server-test.js index 32d0dab567..d4e6411daf 100644 --- a/tests/unit/tasks/server/middleware/tests-server-test.js +++ b/tests/unit/tasks/server/middleware/tests-server-test.js @@ -1,50 +1,85 @@ 'use strict'; -var expect = require('chai').expect; -var TestsServerAddon = require('../../../../../lib/tasks/server/middleware/tests-server'); -var Promise = require('../../../../../lib/ext/promise'); +const { expect } = require('chai'); +const TestsServerAddon = require('../../../../../lib/tasks/server/middleware/tests-server'); describe('TestServerAddon', function () { describe('.serverMiddleware', function () { - var addon = new TestsServerAddon(); - var nextWasCalled = false; - var mockRequest = { - method: 'GET', - path: '', - url: 'http://example.com', - headers: {} - }; - var app = { - use: function (callback) { - return callback(mockRequest, null, function () { nextWasCalled = true; }); - } - }; + let addon, nextWasCalled, mockRequest, app; - it('invokes next when the watcher succeeds', function(done) { + beforeEach(function () { + addon = new TestsServerAddon(); + nextWasCalled = false; + mockRequest = { + method: 'GET', + path: '', + url: 'http://example.com', + headers: {}, + }; + app = { + use(callback) { + return callback(mockRequest, null, function () { + nextWasCalled = true; + }); + }, + }; + }); + + it('invokes next when the watcher succeeds', function (done) { addon.serverMiddleware({ - app: app, + app, options: { - watcher: Promise.resolve() + watcher: Promise.resolve({ + directory: 'some-output-directory', + }), + }, + finally() { + try { + expect(nextWasCalled).to.true; + done(); + } catch (e) { + done(e); + } }, - finally: function() { - expect(nextWasCalled).to.true; - done(); - } }); }); it('invokes next when the watcher fails', function (done) { - var mockError = 'bad things are bad'; + let mockError = 'bad things are bad'; + + addon.serverMiddleware({ + app, + options: { + watcher: Promise.reject(mockError), + }, + finally() { + try { + expect(nextWasCalled).to.true; + done(); + } catch (e) { + done(e); + } + }, + }); + }); + it('allows rootURL containing `+` character', function (done) { + mockRequest.path = '/grayson/+/tests/any-old-file'; + mockRequest.headers.accept = ['text/html']; addon.serverMiddleware({ - app: app, + app, options: { - watcher: Promise.reject(mockError) + watcher: Promise.resolve({ directory: 'nothing' }), + rootURL: '/grayson/+', + }, + finally() { + try { + expect(mockRequest.url).to.equal('/grayson/+/tests/index.html'); + done(); + } catch (e) { + done(e); + } }, - finally: function() { - expect(nextWasCalled).to.true; - done(); - } }); }); }); diff --git a/tests/unit/tasks/test-server-test.js b/tests/unit/tasks/test-server-test.js index 69390c7864..441ef7cf21 100644 --- a/tests/unit/tasks/test-server-test.js +++ b/tests/unit/tasks/test-server-test.js @@ -1,46 +1,174 @@ 'use strict'; -var expect = require('chai').expect; -var TestServerTask = require('../../../lib/tasks/test-server'); -var MockProject = require('../../helpers/mock-project'); -var MockUI = require('../../helpers/mock-ui'); -var MockWatcher = require('../../helpers/mock-watcher'); +const { expect } = require('chai'); +const SilentError = require('silent-error'); +const TestServerTask = require('../../../lib/tasks/test-server'); +const MockProject = require('../../helpers/mock-project'); +const MockUI = require('console-ui/mock'); +const MockWatcher = require('../../helpers/mock-watcher'); -describe('test server', function() { - var subject; +describe('test server', function () { + let subject; - it('transforms the options and invokes testem properly', function(done) { - var ui = new MockUI(); - var watcher = new MockWatcher(); + it('transforms and sets defaultOptions in testem and invokes testem properly', function () { + let ui = new MockUI(); + let watcher = new MockWatcher(); subject = new TestServerTask({ project: new MockProject(), - ui: ui, - addonMiddlewares: function() { + ui, + addonMiddlewares() { return ['middleware1', 'middleware2']; }, testem: { - startDev: function(options) { - expect(options.file).to.equal('blahzorz.conf'); + setDefaultOptions(options) { + this.defaultOptions = options; + }, + startDev(options, finalizer) { expect(options.host).to.equal('greatwebsite.com'); expect(options.port).to.equal(123324); - expect(options.cwd).to.equal('blerpy-derpy'); expect(options.reporter).to.equal('xunit'); expect(options.middleware).to.deep.equal(['middleware1', 'middleware2']); - done(); - } - } + expect(options.test_page).to.equal('http://my/test/page'); + expect(options.cwd).to.be.undefined; + expect(options.config_dir).to.be.undefined; + expect(this.defaultOptions.cwd).to.equal('blerpy-derpy'); + expect(this.defaultOptions.config_dir).to.be.an('string'); + finalizer(0); + }, + }, }); - subject.run({ - configFile: 'blahzorz.conf', - host: 'greatwebsite.com', - port: 123324, - reporter: 'xunit', - outputPath: 'blerpy-derpy', - watcher: watcher - }); + let runResult = subject + .run({ + host: 'greatwebsite.com', + port: 123324, + reporter: 'xunit', + outputPath: 'blerpy-derpy', + watcher, + testPage: 'http://my/test/page', + }) + .then(function (value) { + expect(value, 'expected exist status of 0').to.eql(0); + }); watcher.emit('change'); + return runResult; }); -}); + describe('completion', function () { + let ui, watcher, subject, runOptions; + + before(function () { + ui = new MockUI(); + watcher = new MockWatcher(); + + runOptions = { + reporter: 'xunit', + outputPath: 'blerpy-derpy', + watcher, + testPage: 'http://my/test/page', + }; + + subject = new TestServerTask({ + project: new MockProject(), + ui, + addonMiddlewares() { + return ['middleware1', 'middleware2']; + }, + testem: { + startDev(/* options, finalizer */) { + throw new TypeError('startDev not implemented'); + }, + }, + }); + }); + + describe('firstRun', function () { + it('rejects with testem exceptions', function () { + let error = new Error('OMG'); + subject.testem.setDefaultOptions = function (options) { + this.defaultOptions = options; + }; + + subject.testem.startDev = function (options, finalizer) { + finalizer(1, error); + }; + + let runResult = expect(subject.run(runOptions)).to.be.rejected.then((reason) => { + expect(reason).to.eql(error); + }); + + watcher.emit('change'); + + return runResult; + }); + + it('rejects with exit status (1)', function () { + let error = new SilentError('Testem finished with non-zero exit code. Tests failed.'); + subject.testem.setDefaultOptions = function (options) { + this.defaultOptions = options; + }; + + subject.testem.startDev = function (options, finalizer) { + finalizer(1); + }; + + let runResult = expect(subject.run(runOptions)).to.be.rejected.then((reason) => { + expect(reason.message).to.eql(error.message); + }); + + watcher.emit('change'); + return runResult; + }); + + it('resolves with exit status (0)', function () { + subject.testem.setDefaultOptions = function (options) { + this.defaultOptions = options; + }; + + subject.testem.startDev = function (options, finalizer) { + finalizer(0); + }; + + let runResult = subject.run(runOptions).then(function (value) { + expect(value, 'expected exist status of 0').to.eql(0); + }); + + watcher.emit('change'); + + return runResult; + }); + }); + + describe('restart', function () { + it('rejects with testem exceptions', function () { + let error = new Error('OMG'); + subject.testem.setDefaultOptions = function (options) { + this.defaultOptions = options; + }; + + subject.testem.startDev = function (options, finalizer) { + finalizer(0); + }; + + let runResult = subject.run(runOptions); + + watcher.emit('change'); + + return runResult.then(function () { + subject.testem.startDev = function (options, finalizer) { + finalizer(0, error); + }; + + runResult = expect(subject.run(runOptions)).to.be.rejected.then((reason) => { + expect(reason).to.eql(error); + }); + + watcher.emit('change'); + + return runResult; + }); + }); + }); + }); +}); diff --git a/tests/unit/tasks/test-test.js b/tests/unit/tasks/test-test.js index da0585397d..a1129bd7b1 100644 --- a/tests/unit/tasks/test-test.js +++ b/tests/unit/tasks/test-test.js @@ -1,38 +1,110 @@ 'use strict'; -var expect = require('chai').expect; -var TestTask = require('../../../lib/tasks/test'); -var MockProject = require('../../helpers/mock-project'); +const { expect } = require('chai'); +const TestTask = require('../../../lib/tasks/test'); +const MockProject = require('../../helpers/mock-project'); -describe('test', function() { - var subject; +describe('test task test', function () { + let subject; - it('transforms the options and invokes testem properly', function() { + it('call testem middleware with options', async function () { + let testemMiddlewareOptions; + let project = new MockProject(); + + project.initializeAddons = function () {}; + project.addons = [ + { + testemMiddleware(_, options) { + testemMiddlewareOptions = options; + }, + }, + ]; + + let options = { + reporter: 'xunit', + configFile: 'tests/fixtures/tasks/testem-config/testem-dummy.json', + path: 'dist', + ssl: false, + }; + + subject = new TestTask({ project }); + await subject.run(options); + expect(testemMiddlewareOptions).to.deep.equal(options); + expect(testemMiddlewareOptions.path).to.equal('dist'); + }); + + it('transforms options for testem configuration', function () { subject = new TestTask({ project: new MockProject(), - addonMiddlewares: function() { + addonMiddlewares() { return ['middleware1', 'middleware2']; }, - testem: { - startCI: function(options, cb) { - expect(options.file).to.equal('blahzorz.conf'); - expect(options.host).to.equal('greatwebsite.com'); - expect(options.port).to.equal(123324); - expect(options.cwd).to.equal('blerpy-derpy'); - expect(options.reporter).to.equal('xunit'); - expect(options.middleware).to.deep.equal(['middleware1', 'middleware2']); - cb(0); - }, - app: { reporter: { total: 1 } } - } + + invokeTestem(options) { + expect(options.ssl).to.equal(false); + + // cwd and config_dir are not passed to testem progOptions + let testemOptions = this.transformOptions(options); + expect(testemOptions.host).to.equal('greatwebsite.com'); + expect(testemOptions.port).to.equal(123324); + expect(testemOptions.cwd).to.be.undefined; + expect(testemOptions.reporter).to.equal('xunit'); + expect(testemOptions.middleware).to.deep.equal(['middleware1', 'middleware2']); + expect(testemOptions.test_page).to.equal('http://my/test/page'); + expect(testemOptions.config_dir).to.be.undefined; + expect(testemOptions.key).to.be.undefined; + expect(testemOptions.cert).to.be.undefined; + + // cwd and config_dir are present as part of default options. + let defaultOptions = this.defaultOptions(options); + expect(defaultOptions.host).to.equal('greatwebsite.com'); + expect(defaultOptions.port).to.equal(123324); + expect(defaultOptions.cwd).to.equal('blerpy-derpy'); + expect(defaultOptions.reporter).to.equal('xunit'); + expect(defaultOptions.middleware).to.deep.equal(['middleware1', 'middleware2']); + expect(defaultOptions.test_page).to.equal('http://my/test/page'); + expect(defaultOptions.config_dir).to.be.an('string'); + }, + }); + + subject.run({ + host: 'greatwebsite.com', + port: 123324, + reporter: 'xunit', + outputPath: 'blerpy-derpy', + testemDebug: 'testem.log', + testPage: 'http://my/test/page', + configFile: 'custom-testem-config.json', + ssl: false, + sslKey: 'ssl/server.key', + sslCert: 'ssl/server.cert', + }); + }); + + it('supports conditionally passing SSL configuration forward', function () { + subject = new TestTask({ + project: new MockProject(), + + invokeTestem(options) { + expect(options.ssl).to.equal(true); + + let testemOptions = this.transformOptions(options); + expect(testemOptions.key).to.equal('ssl/server.key'); + expect(testemOptions.cert).to.equal('ssl/server.cert'); + }, }); subject.run({ - configFile: 'blahzorz.conf', host: 'greatwebsite.com', port: 123324, reporter: 'xunit', - outputPath: 'blerpy-derpy' + outputPath: 'blerpy-derpy', + testemDebug: 'testem.log', + testPage: 'http://my/test/page', + configFile: 'custom-testem-config.json', + ssl: true, + sslKey: 'ssl/server.key', + sslCert: 'ssl/server.cert', }); }); }); diff --git a/tests/unit/tasks/update-test.js b/tests/unit/tasks/update-test.js deleted file mode 100644 index c328b8ad52..0000000000 --- a/tests/unit/tasks/update-test.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var path = require('path'); -var expect = require('chai').expect; -var MockUI = require('../../helpers/mock-ui'); -var Promise = require('../../../lib/ext/promise'); -var UpdateTask = require('../../../lib/tasks/update'); - -describe('update task', function() { - var updateTask; - var ui; - - var dummyPkgPath = '../../fixtures/dummy-project-outdated/package.json'; - - var loadCalledWith; - var installCalledWith; - var initCommandWasRun; - - var npm = { - load: function(options, callback) { - setTimeout(function() { - callback(undefined, npm); - }, 0); - loadCalledWith = options; - }, - commands: { - install: function(packages, callback) { - setTimeout(callback, 0); - installCalledWith = packages; - } - } - }; - - beforeEach(function() { - installCalledWith = loadCalledWith = initCommandWasRun = undefined; - }); - - describe('don\'t update', function() { - beforeEach(function() { - ui = new MockUI(); - - ui.prompt = function(messageObject) { - return new Promise(function(resolve) { - ui.write(messageObject.message); - resolve({ - answer: false - }); - }); - }; - updateTask = new UpdateTask({ - ui: ui, - npm: npm - }); - }); - - it('says \'a new version is available\' and asks you to confirm you want to update', function() { - return updateTask.run({ - environment: 'development' - }, { - newestVersion: '100.0.0' - }).then(function() { - expect(ui.output).to.include('A new version of ember-cli is available'); - expect(ui.output).to.include('Are you sure you want to update ember-cli?'); - }); - }); - }); - - describe('do update', function() { - var pkg; - - beforeEach(function() { - ui = new MockUI(); - - ui.pleasantProgress = { - start: function() { }, - stop: function() { } - }; - - ui.prompt = function(messageObject) { - return new Promise(function(resolve) { - ui.write(messageObject.message); - resolve({ - answer: true - }); - }); - }; - - function Init() { - - } - - Init.prototype.run = function() { - initCommandWasRun = true; - }; - - updateTask = new UpdateTask({ - commands: { - Init: Init - }, - ui: ui, - npm: npm, - project: { - root: 'tests/fixtures/dummy-project-outdated', - pkg: require(dummyPkgPath) - } - }); - pkg = updateTask.project.pkg; - }); - - afterEach(function() { - pkg.devDependencies['ember-cli'] = '0.0.1'; - fs.writeFileSync(path.join(__dirname, dummyPkgPath), JSON.stringify(pkg, null, 2)); - }); - - it('says \'a new version is available\' and asks you to confirm you want to update', function() { - this.timeout(1000000); - return updateTask.run({ - environment: 'development' - }, { - newestVersion: '100.0.0' - }).then(function() { - expect(ui.output).to.include('A new version of ember-cli is available'); - expect(ui.output).to.include('Are you sure you want to update ember-cli?'); - expect(installCalledWith).to.deep.equal([ 'ember-cli' ], ''); - expect(loadCalledWith).to.deep.equal({ - 'global': true, - 'loglevel': 'silent' - }, ''); - expect(initCommandWasRun); - }); - }); - - it('updates package.json file with newly updated version number', function() { - return updateTask.run({ - environment: 'development' - }, { - newestVersion: '100.0.0' - }).then(function() { - expect(pkg.devDependencies['ember-cli']).to.equal('100.0.0'); - }); - }); - }); -}); diff --git a/tests/unit/ui/write-error-test.js b/tests/unit/ui/write-error-test.js deleted file mode 100644 index 399f2fcf94..0000000000 --- a/tests/unit/ui/write-error-test.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -var expect = require('chai').expect; -var writeError = require('../../../lib/ui/write-error'); -var MockUI = require('../../helpers/mock-ui'); -var BuildError = require('../../helpers/build-error'); -var EOL = require('os').EOL; -var chalk = require('chalk'); - -describe('writeError', function() { - var ui; - - beforeEach(function() { - ui = new MockUI(); - }); - - it('no error', function() { - writeError(ui); - }); - - it('error with message', function() { - writeError(ui, new BuildError({ - message: 'build error' - })); - - expect(ui.output).to.equal(chalk.red('build error') + EOL); - }); - - it('error with stack', function() { - writeError(ui, new BuildError({ - stack: 'the stack' - })); - - expect(ui.output).to.equal(chalk.red('Error') + EOL + 'the stack' + EOL); - }); - - it('error with file', function() { - writeError(ui, new BuildError({ - file: 'the file' - })); - - expect(ui.output).to.equal(chalk.red('File: the file') + EOL + chalk.red('Error') + EOL); - }); - - it('error with filename (as from Uglify)', function() { - writeError(ui, new BuildError({ - filename: 'the file' - })); - - expect(ui.output).to.equal(chalk.red('File: the file') + EOL + chalk.red('Error') + EOL); - }); - - it('error with file + line', function() { - writeError(ui, new BuildError({ - file: 'the file', - line: 'the line' - })); - - expect(ui.output).to.equal(chalk.red('File: the file (the line)') + EOL + chalk.red('Error') + EOL); - }); - - it('error with file + col', function() { - writeError(ui, new BuildError({ - file: 'the file', - col: 'the col' - })); - - expect(ui.output).to.equal(chalk.red('File: the file') + EOL + chalk.red('Error') + EOL); - }); - - it('error with file + line + col', function() { - writeError(ui, new BuildError({ - file: 'the file', - line: 'the line', - col: 'the col' - })); - - expect(ui.output).to.equal(chalk.red('File: the file (the line:the col)') + EOL + chalk.red('Error') + EOL); - }); -}); diff --git a/tests/unit/utilities/attempt-never-index-test.js b/tests/unit/utilities/attempt-never-index-test.js deleted file mode 100644 index d6c8e2f712..0000000000 --- a/tests/unit/utilities/attempt-never-index-test.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -var attemptNeverIndex = require('../../../lib/utilities/attempt-never-index'); -var existsSync = require('exists-sync'); -var quickTemp = require('quick-temp'); -var expect = require('chai').expect; -var isDarwin = /darwin/i.test(require('os').type()); - -describe('attempt-never-index', function() { - var context = {}; - var tmpPath; - before(function() { - tmpPath = quickTemp.makeOrRemake(context, 'attempt-never-index'); - }); - - after(function() { - quickTemp.remove(context, 'attempt-never-index'); - }); - - it('sets the hint to spotlight if possible', function() { - expect(existsSync(tmpPath + '/.metadata_never_index')).to.false; - - attemptNeverIndex(tmpPath); - - if (isDarwin) { - expect(existsSync(tmpPath + '/.metadata_never_index')).to.true; - } else { - expect(existsSync(tmpPath + '/.metadata_never_index')).to.false; - } - }); -}); diff --git a/tests/unit/utilities/command-generator-test.js b/tests/unit/utilities/command-generator-test.js new file mode 100644 index 0000000000..542788b356 --- /dev/null +++ b/tests/unit/utilities/command-generator-test.js @@ -0,0 +1,63 @@ +'use strict'; + +const td = require('testdouble'); +const Command = require('../../../tests/helpers/command-generator'); + +describe('command-generator', function () { + let yarn, _invoke; + + beforeEach(function () { + yarn = new Command('yarn'); + _invoke = yarn._invoke = td.function('invoke'); + }); + + afterEach(function () { + td.reset(); + }); + + it('invoke passes options', function () { + // Works with subcommand or argument. + yarn.invoke('install'); + td.verify(_invoke(td.matchers.isA(Array), {})); + + yarn.invoke('install', {}); + td.verify(_invoke(td.matchers.isA(Array), {})); + + yarn.invoke('install', { cwd: 'foo' }); + td.verify(_invoke(td.matchers.isA(Array), { cwd: 'foo' })); + + yarn.invoke('install', { stdio: ['default', 'default', 'default'] }); + td.verify(_invoke(td.matchers.isA(Array), { stdio: ['default', 'default', 'default'] })); + + // Works with no subcommand or argument. + yarn.invoke(); + td.verify(_invoke(td.matchers.isA(Array), {})); + + yarn.invoke({}); + td.verify(_invoke(td.matchers.isA(Array), {})); + + yarn.invoke({ cwd: 'foo' }); + td.verify(_invoke(td.matchers.isA(Array), { cwd: 'foo' })); + + yarn.invoke({ stdio: ['default', 'default', 'default'] }); + td.verify(_invoke(td.matchers.isA(Array), { stdio: ['default', 'default', 'default'] })); + + td.verify(_invoke(), { times: 8, ignoreExtraArgs: true }); + }); + + it('builds the proper invocation', function () { + yarn.invoke(); + td.verify(_invoke([]), { ignoreExtraArgs: true }); + + yarn.invoke('install'); + td.verify(_invoke(['install']), { ignoreExtraArgs: true }); + + yarn.invoke('install', 'the', 'thing'); + td.verify(_invoke(['install', 'the', 'thing']), { ignoreExtraArgs: true }); + + yarn.invoke('install', 'the', 'thing', {}); + td.verify(_invoke(['install', 'the', 'thing']), { ignoreExtraArgs: true }); + + td.verify(_invoke(), { times: 4, ignoreExtraArgs: true }); + }); +}); diff --git a/tests/unit/utilities/doc-generator-test.js b/tests/unit/utilities/doc-generator-test.js deleted file mode 100644 index ec38522c62..0000000000 --- a/tests/unit/utilities/doc-generator-test.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -var DocGenerator = require('../../../lib/utilities/doc-generator.js'); -var versionUtils = require('../../../lib/utilities/version-utils'); -var calculateVersion = versionUtils.emberCLIVersion; -var expect = require('chai').expect; -var path = require('path'); -var escapeRegExp = require('escape-string-regexp'); - -describe('generateDocs', function(){ - it('calls the the appropriate command', function(){ - function execFunc() { - var commandPath; - - if (process.platform === 'win32') { - commandPath = escapeRegExp(path.normalize('/node_modules/.bin/yuidoc')); - } else { - commandPath = '/node_modules/yuidocjs/lib/cli.js'; - } - - var version = escapeRegExp(calculateVersion()); - var pattern = 'cd docs && .+' + commandPath + ' -q --project-version ' + version; - - expect(arguments[0], 'yudoc command').to.match(new RegExp(pattern)); - } - - new DocGenerator({ - exec: execFunc - }).generate(); - }); -}); diff --git a/tests/unit/utilities/ember-app-utils-test.js b/tests/unit/utilities/ember-app-utils-test.js new file mode 100644 index 0000000000..b011d8175d --- /dev/null +++ b/tests/unit/utilities/ember-app-utils-test.js @@ -0,0 +1,319 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { expect } = require('chai'); + +const { DEPRECATIONS } = require('../../../lib/debug'); +const emberAppUtils = require('../../../lib/utilities/ember-app-utils'); + +const contentFor = emberAppUtils.contentFor; +const configReplacePatterns = emberAppUtils.configReplacePatterns; +const normalizeUrl = emberAppUtils.normalizeUrl; +const convertObjectToString = emberAppUtils.convertObjectToString; + +describe('ember-app-utils', function () { + describe(`rootURL`, function () { + it('`rootURL` regex accepts space-padded padded variation', function () { + const regex = configReplacePatterns()[0].match; + const variations = ['{{rootURL}}', '{{ rootURL }}', 'foo']; + const results = []; + + variations.forEach((variation) => { + const match = variation.match(regex); + + if (match !== null) { + results.push(match[0]); + } + }); + + variations.pop(); + expect(results).to.deep.equal(variations); + }); + }); + + describe(`EMBER_ENV`, function () { + it('`EMBER_ENV` regex accepts space-padded padded variation', function () { + const regex = configReplacePatterns()[1].match; + const variations = ['{{EMBER_ENV}}', '{{ EMBER_ENV }}', 'foo']; + const results = []; + + variations.forEach((variation) => { + const match = variation.match(regex); + + if (match !== null) { + results.push(match[0]); + } + }); + + variations.pop(); + expect(results).to.deep.equal(variations); + }); + }); + + describe(`MODULE_PREFIX`, function () { + it('`MODULE_PREFIX` regex accepts space-padded padded variation', function () { + const regex = configReplacePatterns()[3].match; + const variations = ['{{MODULE_PREFIX}}', '{{ MODULE_PREFIX }}', 'foo']; + const results = []; + + variations.forEach((variation) => { + const match = variation.match(regex); + + if (match !== null) { + results.push(match[0]); + } + }); + + variations.pop(); + expect(results).to.deep.equal(variations); + }); + }); + + describe(`contentFor`, function () { + let config = { + modulePrefix: 'cool-foo', + }; + + let defaultMatch = "{{content-for 'head'}}"; + let escapedConfig = encodeURIComponent(JSON.stringify(config)); + let defaultOptions = { + storeConfigInMeta: true, + autoRun: true, + addons: [], + }; + + it('`content-for` regex returns all matches presents in a same line', function () { + const contentForRegex = configReplacePatterns(defaultOptions)[2].match; + const content = "{{content-for 'foo'}} {{content-for 'bar'}}"; + const results = []; + let match; + + while ((match = contentForRegex.exec(content)) !== null) { + results.push(match); + } + + expect(results).to.deep.equal([ + ["{{content-for 'foo'}}", 'foo'], + ["{{content-for 'bar'}}", 'bar'], + ]); + }); + + it('returns an empty string if invalid type is specified', function () { + expect(contentFor(config, defaultMatch, 'foo', defaultOptions)).to.equal(''); + expect(contentFor(config, defaultMatch, 'body', defaultOptions)).to.equal(''); + expect(contentFor(config, defaultMatch, 'blah', defaultOptions)).to.equal(''); + }); + + describe('"head"', function () { + it('returns `` tag by default', function () { + let actual = contentFor(config, defaultMatch, 'head', defaultOptions); + let expected = ``; + + expect(actual, '`` tag was included by default').to.contain(expected); + }); + + it('handles multibyte characters in `` tag', function () { + let configWithMultibyteChars = { modulePrefix: 'cool-å' }; + let actual = contentFor(configWithMultibyteChars, defaultMatch, 'head', defaultOptions); + let escapedConfig = encodeURIComponent(JSON.stringify(configWithMultibyteChars)); + let expected = ``; + + expect(actual, '`` tag was included with multibyte characters').to.contain(expected); + }); + + it('omits `` tag if `storeConfigInMeta` is false', function () { + let options = Object.assign({}, defaultOptions, { storeConfigInMeta: false }); + + let output = contentFor(config, defaultMatch, 'head', options); + let expected = ``; + + expect(output, '`` tag was not included').not.to.contain(expected); + }); + }); + + describe('"config-module"', function () { + it('returns `` tag gathering snippet by default', function () { + let metaSnippetPath = path.join(__dirname, '..', '..', '..', 'lib', 'broccoli', 'app-config-from-meta.js'); + let expected = fs.readFileSync(metaSnippetPath, { encoding: 'utf8' }); + + let output = contentFor(config, defaultMatch, 'config-module', defaultOptions); + + expect(output, 'includes `` tag snippet').to.contain(expected); + }); + + it('returns "raw" config if `storeConfigInMeta` is false', function () { + let options = Object.assign({}, defaultOptions, { storeConfigInMeta: false }); + let expected = JSON.stringify(config); + let output = contentFor(config, defaultMatch, 'config-module', options); + + expect(output, 'includes "raw" config').to.contain(expected); + }); + }); + + describe('"app-boot"', function () { + it('returns application bootstrap snippet by default', function () { + let output = contentFor(config, defaultMatch, 'app-boot', defaultOptions); + + expect(output, 'includes application bootstrap snippet').to.contain( + 'require("cool-foo/app")["default"].create({});' + ); + }); + + it('omits application bootstrap snippet if `autoRun` is false', function () { + let options = Object.assign({}, defaultOptions, { autoRun: false }); + let output = contentFor(config, defaultMatch, 'app-boot', options); + + expect(output, 'includes application bootstrap snippet').to.equal(''); + }); + }); + + describe('"test-body-footer"', function () { + it('returns `` + ); + }); + }); + + describe(`for addons`, function () { + it('allows later addons to inspect previous content', function () { + let calledContent; + let addons = [ + { + contentFor() { + return 'zero'; + }, + }, + { + contentFor() { + return 'one'; + }, + }, + { + contentFor(type, config, content) { + calledContent = content.slice(); + content.pop(); + + return 'two'; + }, + }, + ]; + + let options = Object.assign({}, defaultOptions, { addons }); + let output = contentFor(config, defaultMatch, 'foo', options); + + expect(calledContent).to.deep.equal(['zero', 'one']); + expect(output).to.equal('zero\ntwo'); + }); + + it('calls `contentFor` on addons', function () { + let addons = [ + { + contentFor() { + return 'blammo'; + }, + }, + { + contentFor() { + return 'blahzorz'; + }, + }, + ]; + + let options = Object.assign({}, defaultOptions, { addons }); + let output = contentFor(config, defaultMatch, 'foo', options); + + expect(output).to.equal('blammo\nblahzorz'); + }); + + for (const deprecatedType of DEPRECATIONS.V1_ADDON_CONTENT_FOR_TYPES.options.meta.types) { + it(`shows a deprecation warning when using the deprecated \`${deprecatedType}\` \`contentFor\` type`, function () { + if (DEPRECATIONS.V1_ADDON_CONTENT_FOR_TYPES.isRemoved) { + this.skip(); + } + + const consoleWarn = console.warn; + const deprecations = []; + + // TODO: This should be updated once we can register deprecation handlers: + console.warn = (deprecation) => deprecations.push(deprecation); + + const addons = [ + { + name: 'foo', + contentFor(type) { + if (type === deprecatedType) { + return `${deprecatedType}-content`; + } + }, + }, + { + contentFor(type) { + if (type === 'foo') { + return 'foo-content'; + } + }, + }, + ]; + + const options = { ...defaultOptions, addons }; + + let content = ''; + content += contentFor(config, defaultMatch, deprecatedType, options); + content += contentFor(config, defaultMatch, 'foo', options); + + expect(content).to.equal(`${deprecatedType}-contentfoo-content`); + expect(deprecations.length).to.equal(1); + expect(deprecations[0]).to.include( + `Addon \`foo\` is using the deprecated \`${deprecatedType}\` type in its \`contentFor\` method.` + ); + + console.warn = consoleWarn; + }); + } + }); + }); + + describe(`convertObjectToString`, function () { + it('transforms config object into a string', function () { + expect(convertObjectToString({ foobar: 'baz' }), `config was transformed correctly`).to.equal('{"foobar":"baz"}'); + }); + + it('returns empty object string for "falsy" values', function () { + let invalidValues = [null, undefined, 0]; + + invalidValues.forEach((value) => { + expect(convertObjectToString(value), `${value} was transformed correctly`).to.equal('{}'); + }); + }); + }); + + describe(`normalizeUrl`, function () { + it('transforms input values to valid urls', function () { + expect(normalizeUrl('local/people'), '`local/people` was transformed correctly').to.equal('/local/people/'); + + expect(normalizeUrl('people'), '`people` was transformed correctly').to.equal('/people/'); + + expect(normalizeUrl('/people'), '`/people` was transformed correctly').to.equal('/people/'); + + expect(normalizeUrl('/'), '`/` is transformed correctly').to.equal('/'); + }); + + it('returns an empty string for `null`, `undefined` and empty string', function () { + let invalidUrls = [null, undefined, '']; + + invalidUrls.forEach((url) => { + expect(normalizeUrl(url), `${url} was transformed correctly`).to.equal(''); + }); + }); + }); +}); diff --git a/tests/unit/utilities/find-build-file-test.js b/tests/unit/utilities/find-build-file-test.js new file mode 100644 index 0000000000..3ca68b35c8 --- /dev/null +++ b/tests/unit/utilities/find-build-file-test.js @@ -0,0 +1,60 @@ +'use strict'; + +const { expect } = require('chai'); +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); +const findBuildFile = require('../../../lib/utilities/find-build-file'); + +describe('find-build-file', function () { + let tmpPath; + let tmpFilename = 'ember-cli-build.js'; + let ROOT = process.cwd(); + + beforeEach(function () { + tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), 'find-build-file-test')); + process.chdir(tmpPath); + }); + + afterEach(function () { + process.chdir(ROOT); + fs.removeSync(tmpPath); + }); + + it('does not throw an error when the file is valid commonjs syntax', async function () { + fs.writeFileSync(tmpFilename, "module.exports = function() {return {'a': 'A', 'b': 'B'};}", { encoding: 'utf8' }); + + let result = await findBuildFile(); + expect(result).to.be.a('function'); + expect(result()).to.deep.equal({ a: 'A', b: 'B' }); + }); + + it('does not throw an error when the file is valid ES module syntax', async function () { + fs.writeFileSync('package.json', JSON.stringify({ type: 'module' }), { encoding: 'utf8' }); + fs.writeFileSync(tmpFilename, "export default function() {return {'a': 'A', 'b': 'B'};}", { encoding: 'utf8' }); + + let result = await findBuildFile(); + expect(result).to.be.a('function'); + expect(result()).to.deep.equal({ a: 'A', b: 'B' }); + }); + + it('throws a SyntaxError if the file contains a syntax mistake', async function () { + fs.writeFileSync(tmpFilename, 'module.exports = ', { encoding: 'utf8' }); + + let error = null; + try { + await findBuildFile(); + } catch (e) { + error = e; + } + + expect(error).to.not.equal(null); + expect(error.constructor).to.equal(SyntaxError); + expect(error.message).to.match(/Could not `import\('.*ember-cli-build.*'\)`/); + }); + + it('does not throw an error when the file is missing', async function () { + let result = await findBuildFile(tmpPath); + expect(result).to.be.null; + }); +}); diff --git a/tests/unit/utilities/format-package-list-test.js b/tests/unit/utilities/format-package-list-test.js new file mode 100644 index 0000000000..e2138f752c --- /dev/null +++ b/tests/unit/utilities/format-package-list-test.js @@ -0,0 +1,51 @@ +'use strict'; + +const { expect } = require('chai'); +const formatPackageList = require('../../../lib/utilities/format-package-list'); + +describe('format-package-list', function () { + it('correctly formats package list with 1 item', function () { + let result = formatPackageList(['ember-foo']); + expect(result).to.equal('ember-foo'); + }); + + it('correctly formats package list with 2 items', function () { + let result = formatPackageList(['ember-foo', 'bar']); + expect(result).to.equal('ember-foo and bar'); + }); + + it('correctly formats package list with 3 items', function () { + let result = formatPackageList(['ember-foo', 'bar', 'baaaaz']); + expect(result).to.equal('ember-foo, bar and baaaaz'); + }); + + it('correctly formats package list with 4 items', function () { + let result = formatPackageList(['ember-foo', 'bar', 'baaaaz', 'ember-blabla']); + expect(result).to.equal('ember-foo, bar and 2 other packages'); + }); + + it('correctly formats package list with 100 items', function () { + let list = []; + for (let i = 1; i <= 100; i++) { + list.push(`package-${i}`); + } + + let result = formatPackageList(list); + expect(result).to.equal('package-1, package-2 and 98 other packages'); + }); + + it('returns generic "dependencies" without input', function () { + let result = formatPackageList(); + expect(result).to.equal('dependencies'); + }); + + it('returns generic "dependencies" for invalid input', function () { + let result = formatPackageList('foo'); + expect(result).to.equal('dependencies'); + }); + + it('returns generic "dependencies" for empty input', function () { + let result = formatPackageList([]); + expect(result).to.equal('dependencies'); + }); +}); diff --git a/tests/unit/utilities/get-lang-arg-test.js b/tests/unit/utilities/get-lang-arg-test.js new file mode 100644 index 0000000000..c71796683f --- /dev/null +++ b/tests/unit/utilities/get-lang-arg-test.js @@ -0,0 +1,229 @@ +'use strict'; + +const getLangArg = require('../../../lib/utilities/get-lang-arg'); +const { expect } = require('chai'); +const MockUI = require('console-ui/mock'); + +describe('lib/utilities/get-lang-arg', function () { + // Reference object with snippets of case-dependent messages for comparison + let msgRef = { + severity: 'WARNING', + head: 'An issue with the `--lang` flag returned the following message:', + help: { + edit: 'If this was not your intention, you may edit the `` element', + info: { + head: 'Information about using the `--lang` flag:', + desc: 'The `--lang` flag sets the base human language of an app or test app:', + usage: 'If used, the lang option must specfify a valid language code.', + default: 'For default behavior, remove the flag', + more: 'See `ember help` for more information.', + }, + }, + body: { + valid: '', + validAndProg: 'which is BOTH a valid language code AND an abbreviation for a programming language', + prog: 'Trying to set the app programming language to ', + cliOpt: 'Detected a `--lang` specification starting with command flag `-`', + }, + status: { + willSet: 'The human language of the application will be set to', + willNotSet: 'The human language of the application will NOT be set', + }, + }; + + let ui; + + beforeEach(function () { + ui = new MockUI(); + }); + + describe('Valid language codes', function () { + ['en', 'en-gb', 'en-GB', 'EN', 'EN-gb', 'EN-GB'].forEach((langArg) => { + it(`'${langArg}' is a valid language code`, function () { + expect(() => { + getLangArg(langArg, ui); + }).not.to.throw(); + expect(getLangArg(langArg, ui)).to.equal(langArg); + expect(ui.output).to.equal(msgRef.body.valid); + }); + }); + }); + + describe('Edge Cases: valid language codes + programming languages', function () { + [ + 'ts', // Tsonga + 'TS', // Tsonga (case insensitivity check) + 'xml', // Malaysian Sign Language + 'xht', // Hattic + 'css', // Costanoan + ].forEach((langArg) => { + it(`'${langArg}' is a valid language code and programming language`, function () { + expect(() => { + getLangArg(langArg, ui); + }).not.to.throw(); + expect(getLangArg(langArg, ui)).to.equal(langArg); + expect(ui.output).to.contain(msgRef.severity); + expect(ui.output).to.contain(msgRef.head); + expect(ui.output).to.contain(msgRef.help.edit); + expect(ui.output).to.contain(msgRef.help.info.head); + expect(ui.output).to.contain(msgRef.help.info.desc); + expect(ui.output).to.contain(msgRef.help.info.usage); + expect(ui.output).to.contain(msgRef.help.info.default); + expect(ui.output).to.contain(msgRef.help.info.more); + expect(ui.output).to.contain(msgRef.status.willSet); + expect(ui.output).to.contain(msgRef.body.validAndProg); + expect(ui.output).to.contain(msgRef.body.prog); + expect(ui.output).to.not.contain(msgRef.body.cliOpt); + }); + }); + }); + + describe('Invalid lang Flags: Misc.', function () { + ['', '..-..', '12-34', ' en', 'en ', 'en-uk', 'en-UK', 'EN-uk', 'EN-UK', 'en-cockney'].forEach((langArg) => { + it(`'${langArg}' is an invalid language argument; not related misuse cases`, function () { + expect(() => { + getLangArg(langArg, ui); + }).not.to.throw(); + expect(getLangArg(langArg, ui)).to.equal(undefined); + expect(ui.output).to.contain(msgRef.severity); + expect(ui.output).to.contain(msgRef.head); + expect(ui.output).to.contain(msgRef.help.edit); + expect(ui.output).to.contain(msgRef.help.info.head); + expect(ui.output).to.contain(msgRef.help.info.desc); + expect(ui.output).to.contain(msgRef.help.info.usage); + expect(ui.output).to.contain(msgRef.help.info.default); + expect(ui.output).to.contain(msgRef.help.info.more); + expect(ui.output).to.contain(msgRef.status.willNotSet); + expect(ui.output).to.not.contain(msgRef.body.validAndProg); + expect(ui.output).to.not.contain(msgRef.body.prog); + expect(ui.output).to.not.contain(msgRef.body.cliOpt); + }); + }); + }); + + describe('Invalid Language Flags, Misuse case: Programming Languages', function () { + [ + 'javascript', + '.js', + 'js', + 'emcascript2015', + 'emcascript6', + 'es6', + 'emcascript2016', + 'emcascript7', + 'es7', + 'emcascript2017', + 'emcascript8', + 'es8', + 'emcascript2018', + 'emcascript9', + 'es9', + 'emcascript2019', + 'emcascript10', + 'es10', + 'typescript', + '.ts', + 'node.js', + 'node', + 'handlebars', + '.hbs', + 'hbs', + 'glimmer', + 'glimmer.js', + 'glimmer-vm', + 'markdown', + 'markup', + 'html5', + 'html4', + '.md', + '.html', + '.htm', + '.xhtml', + '.xml', + '.xht', + 'md', + 'html', + 'htm', + 'xhtml', + '.sass', + '.scss', + '.css', + 'sass', + 'scss', + + // + case-insensitivity + 'JavaScript', + 'JAVASCRIPT', + 'JS', + '.JS', + 'EMCAScript2015', + 'EMCAScript6', + 'ES6', + 'TypeScript', + 'TYPESCRIPT', + '.TS', + ].forEach((langArg) => { + it(`'${langArg}' is an invalid lang argument; possibly an attempt to set app programming language`, function () { + expect(() => { + getLangArg(langArg, ui); + }).not.to.throw(); + expect(getLangArg(langArg, ui)).to.equal(undefined); + expect(ui.output).to.contain(msgRef.severity); + expect(ui.output).to.contain(msgRef.head); + expect(ui.output).to.contain(msgRef.help.edit); + expect(ui.output).to.contain(msgRef.help.info.head); + expect(ui.output).to.contain(msgRef.help.info.desc); + expect(ui.output).to.contain(msgRef.help.info.usage); + expect(ui.output).to.contain(msgRef.help.info.default); + expect(ui.output).to.contain(msgRef.help.info.more); + expect(ui.output).to.contain(msgRef.status.willNotSet); + expect(ui.output).to.not.contain(msgRef.body.validAndProg); + expect(ui.output).to.contain(msgRef.body.prog); + expect(ui.output).to.not.contain(msgRef.body.cliOpt); + }); + }); + }); + + describe('Invalid Language Flags, Misuse case: ember-cli `new` and `init` options / aliases', function () { + [ + '--watcher=node', + '--dry-run', + '-d', + '--verbose', + '-v', + '--blueprint', + '-b', + '--skip-npm', + '-sn', + '--welcome', + '--no-welcome', + '--yarn', + '--pnpm', + '--name', + '--skip-git', + '-sg', + '--directory', + '-dir', + ].forEach((langArg) => { + it(`'${langArg}' is an invalid language argument; possibly an absorbed ember-cli command option`, function () { + expect(() => { + getLangArg(langArg, ui); + }).not.to.throw(); + expect(getLangArg(langArg, ui)).to.equal(undefined); + expect(getLangArg(langArg, ui)).to.equal(undefined); + expect(ui.output).to.contain(msgRef.severity); + expect(ui.output).to.contain(msgRef.head); + expect(ui.output).to.contain(msgRef.help.edit); + expect(ui.output).to.contain(msgRef.help.info.head); + expect(ui.output).to.contain(msgRef.help.info.desc); + expect(ui.output).to.contain(msgRef.help.info.usage); + expect(ui.output).to.contain(msgRef.help.info.default); + expect(ui.output).to.contain(msgRef.help.info.more); + expect(ui.output).to.contain(msgRef.status.willNotSet); + expect(ui.output).to.not.contain(msgRef.body.validAndProg); + expect(ui.output).to.not.contain(msgRef.body.prog); + expect(ui.output).to.contain(msgRef.body.cliOpt); + }); + }); + }); +}); diff --git a/tests/unit/utilities/get-package-base-name-test.js b/tests/unit/utilities/get-package-base-name-test.js index 702c4a9098..f1d44d1dc6 100644 --- a/tests/unit/utilities/get-package-base-name-test.js +++ b/tests/unit/utilities/get-package-base-name-test.js @@ -1,14 +1,18 @@ 'use strict'; -var expect = require('chai').expect; -var getPackageBaseName = require('../../../lib/utilities/get-package-base-name'); +const { expect } = require('chai'); +const getPackageBaseName = require('../../../lib/utilities/get-package-base-name'); -describe('getPackageBaseName', function() { - it('should return the full package name if it is unscoped', function() { +describe('getPackageBaseName', function () { + it('should return the full package name if it is unscoped', function () { expect(getPackageBaseName('my-addon')).to.equal('my-addon'); }); - it('should return the package name without its scope', function() { - expect(getPackageBaseName('@scope/my-addon')).to.equal('my-addon'); + it('should return the full name when scoped', function () { + expect(getPackageBaseName('@scope/my-addon')).to.equal('@scope/my-addon'); + }); + + it('should strip away version numbers', function () { + expect(getPackageBaseName('my-addon@~1.2.0')).to.equal('my-addon'); }); }); diff --git a/tests/unit/utilities/git-repo-test.js b/tests/unit/utilities/git-repo-test.js index 35a23a1e98..d48560d07a 100644 --- a/tests/unit/utilities/git-repo-test.js +++ b/tests/unit/utilities/git-repo-test.js @@ -1,13 +1,20 @@ 'use strict'; -var isGitRepo = require('is-git-url'); -var expect = require('chai').expect; +const isGitRepo = require('is-git-url'); +const { expect } = require('chai'); -describe('cleanBaseURL()', function() { - it('recognizes git-style urls in various formats', function() { - expect(isGitRepo('https://github.com/trek/app-blueprint-test.git')); - expect(isGitRepo('git@github.com:trek/app-blueprint-test.git')); - expect(isGitRepo('git+ssh://user@server/project.git')); - expect(isGitRepo('git+https://user@server/project.git')); +describe('is-git-url', function () { + it('recognizes git-style urls in various formats', function () { + // without ref + expect(isGitRepo('https://github.com/trek/app-blueprint-test.git')).to.be.ok; + expect(isGitRepo('git@github.com:trek/app-blueprint-test.git')).to.be.ok; + expect(isGitRepo('git+ssh://user@server/project.git')).to.be.ok; + expect(isGitRepo('git+https://user@server/project.git')).to.be.ok; + + // with ref + expect(isGitRepo('https://github.com/trek/app-blueprint-test.git#named-ref')).to.be.ok; + expect(isGitRepo('git@github.com:trek/app-blueprint-test.git#named-ref')).to.be.ok; + expect(isGitRepo('git+ssh://user@server/project.git#named-ref')).to.be.ok; + expect(isGitRepo('git+https://user@server/project.git#named-ref')).to.be.ok; }); }); diff --git a/tests/unit/utilities/heimdall-progres-test.js b/tests/unit/utilities/heimdall-progres-test.js new file mode 100644 index 0000000000..e6ce4a9f16 --- /dev/null +++ b/tests/unit/utilities/heimdall-progres-test.js @@ -0,0 +1,55 @@ +'use strict'; +const progress = require('../../../lib/utilities/heimdall-progress'); +const { expect } = require('chai'); +const { default: chalk } = require('chalk'); + +describe('heimdall-progress', function () { + it('supports the root node', function () { + // fake the heimdall graph (public heimdall API); + const heimdall = { + current: { + id: { + name: 'heimdall', + }, + }, + }; + + expect(progress(heimdall)).to.eql(''); + }); + + it('complex example', function () { + // fake the heimdall graph (public heimdall API); + const heimdall = { + current: { + id: { + name: 'applyPatches', + }, + + parent: { + id: { + name: 'babel', + }, + parent: { + id: { + name: 'broccoli', + }, + + parent: { + id: { + name: 'heimdall', + }, + }, + }, + }, + }, + }; + + expect(progress(heimdall)).to.eql('broccoli > babel > applyPatches'); + }); + + describe('.format', function () { + it('works', function () { + expect(progress.format('test content')).to.eql(`${chalk.green('building... ')}[test content]`); + }); + }); +}); diff --git a/tests/unit/utilities/insert-into-file-test.js b/tests/unit/utilities/insert-into-file-test.js new file mode 100644 index 0000000000..f6bc910b96 --- /dev/null +++ b/tests/unit/utilities/insert-into-file-test.js @@ -0,0 +1,265 @@ +'use strict'; + +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); +const EOL = os.EOL; +const insertIntoFile = require('@ember-tooling/blueprint-model/utilities/insert-into-file'); + +const { expect } = require('chai'); + +describe('insertIntoFile()', function () { + let tempDir, filePath; + + beforeEach(function () { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'insert-into-file-test-')); + filePath = path.join(tempDir, 'foo-bar-baz.txt'); + }); + + afterEach(function () { + fs.removeSync(tempDir); + }); + + it('will create the file if not already existing', function () { + let toInsert = 'blahzorz blammo'; + + return insertIntoFile(filePath, toInsert).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.contain(toInsert); + expect(result.originalContents).to.equal('', 'returned object should contain original contents'); + expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + expect(contents).to.equal(result.contents, 'returned object should contain contents'); + }); + }); + + it('will not create the file if not already existing and creation disabled', function () { + let toInsert = 'blahzorz blammo'; + + return insertIntoFile(filePath, toInsert, { create: false }).then(function (result) { + expect(fs.existsSync(filePath)).to.equal(false, 'file should not exist'); + expect(result.originalContents).to.equal('', 'returned object should contain original contents'); + expect(result.inserted).to.equal(false, 'inserted should indicate that the file was not modified'); + expect(result.contents).to.equal('', 'returned object should contain original contents'); + }); + }); + + it('will insert into the file if it already exists', function () { + let toInsert = 'blahzorz blammo'; + let originalContent = 'some original content\n'; + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal(originalContent + toInsert, 'inserted contents should be appended to original'); + expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); + expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + }); + }); + + it('will not insert into the file if it already contains the content', function () { + let toInsert = 'blahzorz blammo'; + + fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal(toInsert, 'contents should be unchanged'); + expect(result.inserted).to.equal(false, 'inserted should indicate that the file was not modified'); + }); + }); + + it('will insert into the file if it already contains the content if force option is passed', function () { + let toInsert = 'blahzorz blammo'; + + fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { force: true }).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal(toInsert + toInsert, 'contents should be unchanged'); + expect(result.inserted).to.equal(true, 'inserted should indicate that the file was not modified'); + }); + }); + + it('will insert into the file after a specified string if options.after is specified', function () { + let toInsert = 'blahzorz blammo'; + let line1 = 'line1 is here'; + let line2 = 'line2 here'; + let line3 = 'line3'; + let originalContent = [line1, line2, line3].join(EOL); + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { after: line2 + EOL }).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal( + [line1, line2, toInsert, line3].join(EOL), + 'inserted contents should be inserted after the `after` value' + ); + expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); + expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + }); + }); + + it('will insert into the file after the first instance of options.after only', function () { + let toInsert = 'blahzorz blammo'; + let line1 = 'line1 is here'; + let line2 = 'line2 here'; + let line3 = 'line3'; + let originalContent = [line1, line2, line2, line3].join(EOL); + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { after: line2 + EOL }).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal( + [line1, line2, toInsert, line2, line3].join(EOL), + 'inserted contents should be inserted after the `after` value' + ); + expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); + expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + }); + }); + + it('will insert into the file before a specified string if options.before is specified', function () { + let toInsert = 'blahzorz blammo'; + let line1 = 'line1 is here'; + let line2 = 'line2 here'; + let line3 = 'line3'; + let originalContent = [line1, line2, line3].join(EOL); + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { before: line2 + EOL }).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal( + [line1, toInsert, line2, line3].join(EOL), + 'inserted contents should be inserted before the `before` value' + ); + expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); + expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + }); + }); + + it('will insert into the file before the first instance of options.before only', function () { + let toInsert = 'blahzorz blammo'; + let line1 = 'line1 is here'; + let line2 = 'line2 here'; + let line3 = 'line3'; + let originalContent = [line1, line2, line2, line3].join(EOL); + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { before: line2 + EOL }).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal( + [line1, toInsert, line2, line2, line3].join(EOL), + 'inserted contents should be inserted after the `after` value' + ); + expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); + expect(result.inserted).to.equal(true, 'inserted should indicate that the file was modified'); + }); + }); + + it('it will make no change if options.after is not found in the original', function () { + let toInsert = 'blahzorz blammo'; + let originalContent = 'the original content'; + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { after: `not found${EOL}` }).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal(originalContent, 'original content is unchanged'); + expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); + expect(result.inserted).to.equal(false, 'inserted should indicate that the file was not modified'); + }); + }); + + it('it will make no change if options.before is not found in the original', function () { + let toInsert = 'blahzorz blammo'; + let originalContent = 'the original content'; + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { before: `not found${EOL}` }).then(function (result) { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal(originalContent, 'original content is unchanged'); + expect(result.originalContents).to.equal(originalContent, 'returned object should contain original contents'); + expect(result.inserted).to.equal(false, 'inserted should indicate that the file was not modified'); + }); + }); + + describe('regex', function () { + it('options.after supports regex', function () { + let toInsert = 'blahzorz blammo'; + let line1 = 'line1 is here'; + let line2 = 'line2 here'; + let line3 = 'line3'; + let originalContent = [line1, line2, line3].join(EOL); + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { after: /line2 here(\r?\n)/ }).then(function () { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal( + [line1, line2, toInsert, line3].join(EOL), + 'inserted contents should be inserted after the `after` value' + ); + }); + }); + + it('options.before supports regex', function () { + let toInsert = 'blahzorz blammo'; + let line1 = 'line1 is here'; + let line2 = 'line2 here'; + let line3 = 'line3'; + let originalContent = [line1, line2, line3].join(EOL); + + fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' }); + + return insertIntoFile(filePath, toInsert, { before: /line2 here(\r?\n)/ }).then(function () { + let contents = fs.readFileSync(filePath, { encoding: 'utf8' }); + + expect(contents).to.equal( + [line1, toInsert, line2, line3].join(EOL), + 'inserted contents should be inserted before the `before` value' + ); + }); + }); + + it("options.after doesn't treat strings as regex", function () { + let toInsert = 'blahzorz blammo'; + + fs.writeFileSync(filePath, '', { encoding: 'utf8' }); + + expect(() => insertIntoFile(filePath, toInsert, { after: '"predef": [\n' })).to.not.throw(); + }); + + it("options.before doesn't treat strings as regex", function () { + let toInsert = 'blahzorz blammo'; + + fs.writeFileSync(filePath, '', { encoding: 'utf8' }); + + expect(() => insertIntoFile(filePath, toInsert, { before: '"predef": [\n' })).to.not.throw(); + }); + }); + + it('will return the file path', function () { + let toInsert = 'blahzorz blammo'; + + return insertIntoFile(filePath, toInsert).then(function (result) { + expect(result.path).to.equal(filePath, 'path should always match'); + }); + }); +}); diff --git a/tests/unit/utilities/is-engine-test.js b/tests/unit/utilities/is-engine-test.js new file mode 100644 index 0000000000..206df72eba --- /dev/null +++ b/tests/unit/utilities/is-engine-test.js @@ -0,0 +1,19 @@ +'use strict'; + +const { expect } = require('chai'); +const isEngine = require(`../../../lib/utilities/is-engine`); + +describe('Unit | is-engine', function () { + it('it identifies an engine correctly', function () { + expect(isEngine([null, 'ember-engine', 'foo', 'bar'])).to.equal(true); + expect(isEngine(['ember-engine'])).to.equal(true); + }); + + it("it returns false if it's not an engine", function () { + expect(isEngine({ 'ember-engine': true })).to.equal(false); + expect(isEngine('ember-engine')).to.equal(false); + expect(isEngine(['foo', 'bar'])).to.equal(false); + expect(isEngine({})).to.equal(false); + expect(isEngine(undefined)).to.equal(false); + }); +}); diff --git a/tests/unit/utilities/is-lazy-engine-test.js b/tests/unit/utilities/is-lazy-engine-test.js new file mode 100644 index 0000000000..81256f50af --- /dev/null +++ b/tests/unit/utilities/is-lazy-engine-test.js @@ -0,0 +1,18 @@ +'use strict'; + +const { expect } = require('chai'); +const isLazyEngine = require(`../../../lib/utilities/is-lazy-engine`); + +describe('Unit | is-lazy-engine', function () { + it('it identifies a lazy engine correctly', function () { + expect(isLazyEngine({ options: { lazyLoading: { enabled: true } } })).to.equal(true); + }); + + it("it returns false if it's not a lazy engine", function () { + expect(isLazyEngine({ options: { lazyLoading: { enabled: false } } })).to.equal(false); + expect(isLazyEngine({ options: { lazyLoading: {} } })).to.equal(false); + expect(isLazyEngine({ options: {} })).equal(false); + expect(isLazyEngine({})).to.equal(false); + expect(isLazyEngine()).to.equal(false); + }); +}); diff --git a/tests/unit/utilities/is-live-reload-request-test.js b/tests/unit/utilities/is-live-reload-request-test.js new file mode 100644 index 0000000000..fd222b7f02 --- /dev/null +++ b/tests/unit/utilities/is-live-reload-request-test.js @@ -0,0 +1,31 @@ +'use strict'; + +const { expect } = require('chai'); +const isLiveReloadRequest = require('../../../lib/utilities/is-live-reload-request'); + +describe('isLiveReloadRequest()', function () { + it('/livereload', function () { + expect(isLiveReloadRequest('/livereload', '/')).to.be.true; + }); + it('path/livereload', function () { + expect(isLiveReloadRequest('/path/livereload', 'path/')).to.be.true; + }); + it('path/path/livereload', function () { + expect(isLiveReloadRequest('/path/path/livereload', 'path/path/')).to.be.true; + }); + it('livereload', function () { + expect(isLiveReloadRequest('livereload', '/')).to.be.false; + }); + it('livereload/path', function () { + expect(isLiveReloadRequest('livereload/path', '/')).to.be.false; + }); + it('/livereload.js', function () { + expect(isLiveReloadRequest('/livereload.js', '/')).to.be.false; + }); + it('path/livereload.js', function () { + expect(isLiveReloadRequest('path/livereload.js', 'path/')).to.be.false; + }); + it('path/livereload/path', function () { + expect(isLiveReloadRequest('path/livereload/path', 'path/')).to.be.false; + }); +}); diff --git a/tests/unit/utilities/lint-addons-by-type-test.js b/tests/unit/utilities/lint-addons-by-type-test.js new file mode 100644 index 0000000000..604c5d9cf0 --- /dev/null +++ b/tests/unit/utilities/lint-addons-by-type-test.js @@ -0,0 +1,31 @@ +'use strict'; + +const td = require('testdouble'); +const { expect } = require('chai'); +const lintAddonsByType = require('../../../lib/utilities/lint-addons-by-type'); + +describe('lintAddonsByType', function () { + let addons; + + beforeEach(function () { + addons = [ + { + name: 'foo', + pkg: { name: 'foo' }, + lintTree: td.function(), + }, + ]; + }); + + it('calls lintTree on the addon', function () { + lintAddonsByType(addons, 'app', { foo: 'bar' }); + + td.verify(addons[0].lintTree('app', { foo: 'bar' })); + }); + + it('filters out tree if `lintTree` returns false-y', function () { + td.when(addons[0].lintTree).thenReturn({}); + + expect(lintAddonsByType(addons, 'app')).to.deep.equal([]); + }); +}); diff --git a/tests/unit/utilities/markdown-color-test.js b/tests/unit/utilities/markdown-color-test.js index 832d766ed8..61a99423c0 100644 --- a/tests/unit/utilities/markdown-color-test.js +++ b/tests/unit/utilities/markdown-color-test.js @@ -1,40 +1,33 @@ 'use strict'; -var MarkdownColor = require('../../../lib/utilities/markdown-color'); -var expect = require('chai').expect; -var path = require('path'); -var chalk = require('chalk'); +const MarkdownColor = require('@ember-tooling/blueprint-model/utilities/markdown-color'); +const { expect } = require('chai'); +const path = require('path'); +const { default: chalk, supportsColor } = require('chalk'); -function isAnsiSupported() { - // when ansi is supported this should be '\u001b[0m\u001b[31ma\u001b[39m\u001b[0m\n\n' - return chalk.red('a') !== 'a'; -} +describe('MarkdownColor', function () { + let mc; -(isAnsiSupported() ? describe : describe.skip)('MarkdownColor', function() { - var mc; - - beforeEach(function() { - /* - // check to make sure ansi is supported - // can use this.skip() after Mocha 2.1.1 - // see https://github.com/mochajs/mocha/pull/946 - if (isAnsiSupported()) { + before(function () { + if (!supportsColor) { this.skip(); } - */ + }); + + beforeEach(function () { mc = new MarkdownColor(); }); - it('parses default markdown', function() { - + it('parses default markdown', function () { // console.log(mc.render('# foo\n__bold__ **words**\n* un\n* ordered\n* list')) expect(mc.render('# foo\n__bold__ words\n* un\n* ordered\n* list')).to.equal( - '\u001b[35m\u001b[4m\u001b[1m\nfoo\u001b[22m\u001b[24m\u001b[39m\n\u001b[0m'+ - '\u001b[1mbold\u001b[22m words\u001b[0m\n\n \u001b[0m* un\u001b[0m\n '+ - '\u001b[0m* ordered\u001b[0m\n \u001b[0m* list\u001b[0m\n\n'); + '\u001b[35m\u001b[4m\u001b[1m\nfoo\u001b[22m\u001b[24m\u001b[39m\n\u001b[0m' + + '\u001b[1mbold\u001b[22m words\u001b[0m\n\n \u001b[0m* un\u001b[0m\n ' + + '\u001b[0m* ordered\u001b[0m\n \u001b[0m* list\u001b[0m\n\n' + ); }); - it('parses color tokens', function() { + it('parses color tokens', function () { expect(mc.render('red')).to.equal('\u001b[0m\u001b[31mred\u001b[39m\u001b[0m\n\n'); expect(mc.render('green')).to.equal('\u001b[0m\u001b[32mgreen\u001b[39m\u001b[0m\n\n'); expect(mc.render('blue')).to.equal('\u001b[0m\u001b[34mblue\u001b[39m\u001b[0m\n\n'); @@ -49,41 +42,46 @@ function isAnsiSupported() { expect(mc.render('bgGreen')).to.equal('\u001b[0m\u001b[42mbgGreen\u001b[49m\u001b[0m\n\n'); expect(mc.render('bgBlue')).to.equal('\u001b[0m\u001b[44mbgBlue\u001b[49m\u001b[0m\n\n'); expect(mc.render('bgCyan')).to.equal('\u001b[0m\u001b[46mbgCyan\u001b[49m\u001b[0m\n\n'); - expect(mc.render('bgMagenta')).to.equal('\u001b[0m\u001b[45mbgMagenta\u001b[49m\u001b[0m\n\n'); + expect(mc.render('bgMagenta')).to.equal( + '\u001b[0m\u001b[45mbgMagenta\u001b[49m\u001b[0m\n\n' + ); expect(mc.render('bgYellow')).to.equal('\u001b[0m\u001b[43mbgYellow\u001b[49m\u001b[0m\n\n'); expect(mc.render('bgBlack')).to.equal('\u001b[0m\u001b[40mbgBlack\u001b[49m\u001b[0m\n\n'); }); - it('parses custom tokens', function() { + it('parses custom tokens', function () { expect(mc.render('--option')).to.equal('\u001b[0m\u001b[36m--option\u001b[39m\u001b[0m\n\n'); expect(mc.render('(Default: value)')).to.equal('\u001b[0m\u001b[36m(Default: value)\u001b[39m\u001b[0m\n\n'); expect(mc.render('(Required)')).to.equal('\u001b[0m\u001b[36m(Required)\u001b[39m\u001b[0m\n\n'); }); - it('accepts tokens on instantiation', function() { - var mctemp = new MarkdownColor({ + it('accepts tokens on instantiation', function () { + let mctemp = new MarkdownColor({ tokens: { foo: { token: '^foo^', pattern: /(?:\^foo\^)(.*?)(?:\^foo\^)/g, - render: MarkdownColor.prototype.renderStylesFactory(chalk, ['blue','bgWhite']) - } - } + render: MarkdownColor.prototype.renderStylesFactory(chalk, ['blue', 'bgWhite']), + }, + }, }); - expect(mctemp.render('^foo^foo^foo^')).to.equal('\u001b[0m\u001b[34m\u001b[47mfoo\u001b[49m\u001b[39m\u001b[0m\n\n'); + expect(mctemp.render('^foo^foo^foo^')).to.equal( + '\u001b[0m\u001b[34m\u001b[47mfoo\u001b[49m\u001b[39m\u001b[0m\n\n' + ); }); - it('parses markdown files', function() { + it('parses markdown files', function () { // console.log(mc.renderFile(path.join(__dirname,'../../../tests/fixtures/markdown/foo.md'))) - expect(mc.renderFile(path.join(__dirname,'../../../tests/fixtures/markdown/foo.md'))).to - .equal('\u001b[0m\u001b[36mtacos are \u001b[33mdelicious\u001b[36m \u001b[34mand I\u001b[39m enjoy eating them\u001b[39m\u001b[0m\n\n'); + expect(mc.renderFile(path.join(__dirname, '../../../tests/fixtures/markdown/foo.md'))).to.equal( + '\u001b[0m\u001b[36mtacos are \u001b[33mdelicious\u001b[39m\u001b[36m \u001b[34mand I\u001b[39m enjoy eating them\u001b[39m\u001b[0m\n\n' + ); }); - it('allows tokens inside other token bounds', function() { + it('allows tokens inside other token bounds', function () { // console.log(mc.render('tacos are delicious and I enjoy eating them')) - expect(mc.render('tacos are delicious and I enjoy eating them')) - .to.equal('\u001b[0m\u001b[36mtacos are \u001b[33mdelicious\u001b[36m and I enjoy eating'+ - ' them\u001b[39m\u001b[0m\n\n'); + expect(mc.render('tacos are delicious and I enjoy eating them')).to.equal( + '\u001b[0m\u001b[36mtacos are \u001b[33mdelicious\u001b[39m\u001b[36m and I enjoy eating them\u001b[39m\u001b[0m\n\n' + ); }); }); /* Chalk supported styles - diff --git a/tests/unit/utilities/merge-blueprint-options-test-disabled.js b/tests/unit/utilities/merge-blueprint-options-test-disabled.js new file mode 100644 index 0000000000..93d338a378 --- /dev/null +++ b/tests/unit/utilities/merge-blueprint-options-test-disabled.js @@ -0,0 +1,47 @@ +'use strict'; + +const { expect } = require('chai'); +const Blueprint = require('@ember-tooling/blueprint-model'); +const Project = require('../../../lib/models/project'); +const Command = require('../../../lib/models/command'); +const mergeBlueprintOptions = require('../../../lib/utilities/merge-blueprint-options'); +const td = require('testdouble'); + +describe('merge-blueprint-options', function () { + let TestCommand = Command.extend({ + name: 'test-command', + description: 'Runs a test command.', + aliases: ['t'], + works: 'everywhere', + + availableOptions: [{ name: 'verbose', type: Boolean, default: false, aliases: ['v'] }], + + beforeRun: mergeBlueprintOptions, + }); + + afterEach(function () { + td.reset(); + }); + + function buildCommand() { + return new TestCommand({ + project: new Project(process.cwd(), { name: 'some-random-name' }), + }); + } + + it("it works as a command's beforeRun()", function () { + let command, availableOptions; + + td.replace(Blueprint, 'lookup', td.function()); + td.when(Blueprint.lookup('test-blueprint'), { ignoreExtraArgs: true }).thenReturn({ + availableOptions: [{ name: 'custom-blueprint-option', type: String }], + }); + + command = buildCommand(); + command.beforeRun(['test-blueprint']); + + availableOptions = command.availableOptions.map(({ name }) => name); + expect(availableOptions).to.contain('verbose'); + expect(availableOptions).to.contain('custom-blueprint-option'); + }); +}); diff --git a/tests/unit/utilities/open-editor-test.js b/tests/unit/utilities/open-editor-test.js new file mode 100644 index 0000000000..11398b2228 --- /dev/null +++ b/tests/unit/utilities/open-editor-test.js @@ -0,0 +1,79 @@ +'use strict'; + +const openEditor = require('@ember-tooling/blueprint-model/utilities/open-editor'); + +const { expect } = require('chai'); +const td = require('testdouble'); + +describe('open-editor', function () { + beforeEach(function () { + td.replace(openEditor, '_env'); + td.replace(openEditor, '_spawn'); + }); + + afterEach(function () { + td.reset(); + }); + + it('throws if EDITOR is not set', async function () { + td.when(openEditor._env()).thenReturn({}); + try { + await openEditor('test'); + expect.fail('expected rejection'); + } catch (e) { + expect(e.message).to.eql('EDITOR environment variable is not set'); + } + }); + + it('spawns EDITOR with passed file', async function () { + td.when(openEditor._env()).thenReturn({ EDITOR: 'vi' }); + td.when(openEditor._spawn(td.matchers.anything(), td.matchers.anything(), td.matchers.anything())).thenReturn({ + on(event, cb) { + cb(0); + }, + }); + + await openEditor('test'); + td.verify(openEditor._spawn('vi', ['test'], { stdio: 'inherit' })); + }); + + it('spawns EDITOR with passed file (rejection scenario)', async function () { + td.when(openEditor._env()).thenReturn({ EDITOR: 'vi' }); + td.when(openEditor._spawn(td.matchers.anything(), td.matchers.anything(), td.matchers.anything())).thenReturn({ + on(event, cb) { + cb(-1); + }, + }); + + try { + await openEditor('test'); + expect.fail('expected rejection'); + } catch (e) { + expect(e.message).to.include(`exited with a non zero error status code: '-1'`); + } + + td.verify(openEditor._spawn('vi', ['test'], { stdio: 'inherit' })); + }); + it('throws if no file option is provided', async function () { + td.when(openEditor._env()).thenReturn({ EDITOR: 'vi' }); + + try { + await openEditor(); + expect.fail('expected rejection'); + } catch (e) { + expect(e.message).to.eql('No `file` option provided'); + } + }); + + describe('.canEdit()', function () { + it('returns false if EDITOR is not set', function () { + td.when(openEditor._env()).thenReturn({}); + expect(openEditor.canEdit()).to.be.false; + }); + + it('returns true if EDITOR is set', function () { + td.when(openEditor._env()).thenReturn({ EDITOR: 'vi' }); + expect(openEditor.canEdit()).to.be.true; + }); + }); +}); diff --git a/tests/unit/utilities/package-cache-test.js b/tests/unit/utilities/package-cache-test.js new file mode 100644 index 0000000000..0fc2ac24ef --- /dev/null +++ b/tests/unit/utilities/package-cache-test.js @@ -0,0 +1,589 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const { default: Configstore } = require('configstore'); + +const PackageCache = require('../../../tests/helpers/package-cache'); +const symlinkOrCopySync = require('symlink-or-copy').sync; + +const td = require('testdouble'); +const { expect } = require('chai'); +const { dir, file } = require('chai-files'); + +describe('PackageCache', function () { + let testPackageCache; + + let npm = td.function('npm'); + let yarn = td.function('yarn'); + + beforeEach(function () { + testPackageCache = new PackageCache(); + testPackageCache._conf = new Configstore('package-cache-test'); + + testPackageCache.__setupForTesting({ + commands: { + npm: { invoke: npm }, + yarn: { invoke: yarn }, + }, + }); + }); + + afterEach(function () { + td.reset(); + testPackageCache.__resetForTesting(); + fs.unlinkSync(testPackageCache._conf.path); + }); + + it('defaults rootPath', function () { + testPackageCache = new PackageCache(); + expect(testPackageCache.rootPath).to.equal(process.cwd()); + + testPackageCache = new PackageCache('./'); + expect(testPackageCache.rootPath).to.equal('./'); + }); + + it('successfully uses getter/setter', function () { + testPackageCache._conf.set('foo', true); + expect(testPackageCache.dirs['foo']).to.be.true; + testPackageCache._conf.delete('foo'); + expect(testPackageCache.dirs['foo']).to.be.undefined; + + expect(() => { + testPackageCache.dirs = { foo: 'asdf' }; + }).to.throw(Error); + }); + + it('_cleanDirs', function () { + testPackageCache._conf.set('existing', __dirname); + testPackageCache._conf.set('nonexisting', path.join(__dirname, 'nonexisting')); + + testPackageCache._cleanDirs(); + + expect(testPackageCache.dirs['existing']).to.exist; + expect(testPackageCache.dirs['nonexisting']).to.not.exist; + }); + + it('_readManifest', function () { + let emberCLIPath = path.resolve(__dirname, '../../..'); + testPackageCache._conf.set('self', emberCLIPath); + testPackageCache._conf.set('boom', __dirname); + + let manifest; + manifest = JSON.parse(testPackageCache._readManifest('self', 'yarn')); + expect(manifest.name).to.equal('ember-cli'); + + manifest = testPackageCache._readManifest('nonexistent', 'yarn'); + expect(manifest).to.be.null; + + testPackageCache._readManifest('boom', 'yarn'); + expect(manifest).to.be.null; + }); + + it('_writeManifest', function () { + let manifest = JSON.stringify({ + name: 'foo', + dependencies: { + ember: '2.9.0', + 'ember-cli-shims': '0.1.3', + }, + }); + + // Confirm that it removes a yarn.lock file if present and type is yarn. + testPackageCache._writeManifest('yarn', 'yarn', manifest); + let yarn = testPackageCache.dirs['yarn']; + let lockFileLocation = path.join(yarn, 'yarn.lock'); + + // Make sure it doesn't throw if it doesn't exist. + expect(() => { + testPackageCache._writeManifest('yarn', 'yarn', manifest); + }).to.not.throw(Error); + + // Add a "lockfile". + fs.writeFileSync(lockFileLocation, 'Hello, world!'); + expect(file(lockFileLocation)).to.exist; // Sanity check. + + // Make sure it gets removed. + testPackageCache._writeManifest('yarn', 'yarn', manifest); + expect(file(lockFileLocation)).to.not.exist; + + testPackageCache.destroy('yarn'); + }); + + it('_checkManifest', function () { + let manifest = JSON.stringify({ + name: 'foo', + dependencies: { + ember: '2.9.0', + 'ember-cli-shims': '0.1.3', + }, + }); + + let manifestShuffled = JSON.stringify({ + name: 'foo', + dependencies: { + 'ember-cli-shims': '0.1.3', + ember: '2.9.0', + }, + }); + + let manifestEmptyKey = JSON.stringify({ + name: 'foo', + dependencies: { + ember: '2.9.0', + 'ember-cli-shims': '0.1.3', + }, + devDependencies: {}, + }); + + testPackageCache._writeManifest('yarn', 'yarn', manifest); + + expect(testPackageCache._checkManifest('yarn', 'yarn', manifest)).to.be.true; + expect(testPackageCache._checkManifest('yarn', 'yarn', manifestShuffled)).to.be.true; + expect(testPackageCache._checkManifest('yarn', 'yarn', manifestEmptyKey)).to.be.true; + expect(testPackageCache._checkManifest('yarn', 'yarn', '{ "dependencies": "different" }')).to.be.false; + + testPackageCache.destroy('yarn'); + }); + + it('_removeLinks', function () { + // This is our package that is linked in. + let srcDir = path.join(process.cwd(), 'tmp', 'beta'); + expect(dir(srcDir)).to.not.exist; + fs.outputFileSync(path.join(srcDir, 'package.json'), 'beta'); + expect(file(path.join(srcDir, 'package.json'))).to.contain('beta'); + + // This is the directory we got "back" from `PackageCache.create`. + let targetDir = path.join(process.cwd(), 'tmp', 'target'); + expect(dir(targetDir)).to.not.exist; + testPackageCache._conf.set('label', targetDir); + fs.mkdirsSync(targetDir); + expect(dir(targetDir)).to.exist; + + // This is the directory which would be created as a link. + let eventualDir = path.join(targetDir, 'node_modules', 'beta'); + fs.mkdirsSync(path.dirname(eventualDir)); + expect(dir(path.dirname(eventualDir))).to.exist; + expect(dir(eventualDir)).to.not.exist; + + // Link or copy in the package. + symlinkOrCopySync(srcDir, eventualDir); + expect(file(path.join(eventualDir, 'package.json'))).to.contain('beta'); + + let manifest = { + _packageCache: { + links: ['one', 'two', 'three', 'alpha', { name: 'beta', path: srcDir }], + }, + dependencies: { + beta: '1.0.0', + one: '1.0.0', + two: '2.0.0', + three: '3.0.0', + four: '4.0.0', // Doesn't remove non-linked items. + }, + devDependencies: { + one: '1.0.0', // Handles duplicates correctly. + }, + }; + + let result = { + _packageCache: { + links: [ + 'one', + 'two', + 'three', + 'alpha', // Will blindly unlink alpha. + { name: 'beta', path: srcDir }, + ], + originals: { + dependencies: { + beta: '1.0.0', + one: '1.0.0', + two: '2.0.0', + three: '3.0.0', + four: '4.0.0', + }, + devDependencies: { + one: '1.0.0', + }, + }, + }, + dependencies: { + four: '4.0.0', + }, + devDependencies: {}, + }; + + let readManifest = td.function('_readManifest'); + td.when(readManifest('label', 'npm')).thenReturn(JSON.stringify(manifest)); + testPackageCache._readManifest = readManifest; + + let writeManifest = td.function('_writeManifest'); + testPackageCache._writeManifest = writeManifest; + + testPackageCache._removeLinks('label', 'npm'); + td.verify(writeManifest('label', 'npm', JSON.stringify(result)), { times: 1, ignoreExtraArgs: true }); + + td.verify(npm('unlink', 'one'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('unlink', 'two'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('unlink', 'three'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('unlink', 'alpha'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('unlink'), { times: 4, ignoreExtraArgs: true }); + + // Confirms manual unlink behavior. + expect(dir(eventualDir)).to.not.exist; + + // Ensures we don't delete the other side of the symlink. + expect(dir(srcDir)).to.exist; + + // Clean up. + fs.removeSync(srcDir); + fs.removeSync(targetDir); + }); + + it('_restoreLinks', function () { + // This is our package that is linked in. + let srcDir = path.join(process.cwd(), 'tmp', 'beta'); + expect(dir(srcDir)).to.not.exist; + fs.outputFileSync(path.join(srcDir, 'package.json'), 'beta'); + expect(file(path.join(srcDir, 'package.json'))).to.contain('beta'); + + // This is the directory we got "back" from `PackageCache.create`. + let targetDir = path.join(process.cwd(), 'tmp', 'target'); + expect(dir(targetDir)).to.not.exist; + testPackageCache._conf.set('label', targetDir); + fs.mkdirsSync(targetDir); + expect(dir(targetDir)).to.exist; + + // This is the directory which will be created as a link. + let eventualDir = path.join(targetDir, 'node_modules', 'beta'); + expect(dir(eventualDir)).to.not.exist; + + let manifest = { + _packageCache: { + links: ['one', 'two', 'three', 'alpha', { name: 'beta', path: srcDir }], + originals: { + dependencies: { + beta: '1.0.0', + one: '1.0.0', + two: '2.0.0', + three: '3.0.0', + four: '4.0.0', + }, + devDependencies: { + one: '1.0.0', + }, + }, + }, + dependencies: { + four: '4.0.0', + }, + devDependencies: {}, + }; + + let result = { + _packageCache: { + links: [ + 'one', + 'two', + 'three', + 'alpha', // Will blindly link alpha. + { name: 'beta', path: srcDir }, + ], + }, + dependencies: { + beta: '1.0.0', + one: '1.0.0', + two: '2.0.0', + three: '3.0.0', + four: '4.0.0', // Order matters! + }, + devDependencies: { + one: '1.0.0', // Restores duplicates. + }, + }; + + let readManifest = td.function('_readManifest'); + td.when(readManifest('label', 'npm')).thenReturn(JSON.stringify(manifest)); + testPackageCache._readManifest = readManifest; + + let writeManifest = td.function('_writeManifest'); + testPackageCache._writeManifest = writeManifest; + + testPackageCache._restoreLinks('label', 'npm'); + td.verify(writeManifest('label', 'npm', JSON.stringify(result)), { times: 1, ignoreExtraArgs: true }); + + td.verify(npm('link', 'one'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('link', 'two'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('link', 'three'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('link', 'alpha'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('link'), { times: 4, ignoreExtraArgs: true }); + + // Confirms manual link behavior. + expect(dir(srcDir)).to.exist; + expect(file(path.join(eventualDir, 'package.json'))).to.contain('beta'); + + // Clean up. + fs.removeSync(srcDir); + fs.removeSync(targetDir); + }); + + describe('_install', function () { + // We're only going to test the invocation pattern boundary. + // Don't want to wait for the install to execute. + + beforeEach(function () { + // Fake in the dir label. + testPackageCache._conf.set('label', 'hello'); + }); + + afterEach(function () { + td.reset(); + testPackageCache.destroy('label'); + }); + + it('Triggers install.', function () { + testPackageCache._install('label', 'npm'); + td.verify(npm('install', { cwd: 'hello' }), { times: 1 }); + td.verify(npm(), { times: 1, ignoreExtraArgs: true }); + }); + + it('Attempts to link when it is supposed to.', function () { + // Add a link. + testPackageCache._writeManifest( + 'label', + 'npm', + JSON.stringify({ + _packageCache: { + links: ['ember-cli'], + }, + }) + ); + testPackageCache._install('label', 'npm'); + + td.verify(npm('unlink', 'ember-cli', { cwd: 'hello' }), { times: 1 }); + td.verify(npm('install', { cwd: 'hello' }), { times: 1 }); + td.verify(npm('link', 'ember-cli', { cwd: 'hello' }), { times: 1 }); + td.verify(npm(), { times: 3, ignoreExtraArgs: true }); + }); + }); + + describe('_upgrade (yarn)', function () { + // We're only going to test the invocation pattern boundary. + // Don't want to wait for the install to execute. + let testCounter = 0; + let label; + + beforeEach(function () { + label = `yarn-upgrade-test-${testCounter++}`; + testPackageCache._conf.set(label, 'hello'); + }); + + afterEach(function () { + td.reset(); + testPackageCache.destroy(label); + }); + + describe('without yarn.lock', function () { + beforeEach(function () { + testPackageCache._canUpgrade = () => false; + }); + + it('Trigger upgrade.', function () { + testPackageCache._upgrade(label, 'yarn'); + td.verify(yarn('install', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn(), { times: 1, ignoreExtraArgs: true }); + }); + + it('Make sure it unlinks, upgrades, re-links.', function () { + // Add a link. + testPackageCache._writeManifest( + label, + 'yarn', + JSON.stringify({ + _packageCache: { + links: ['ember-cli'], + }, + }) + ); + testPackageCache._upgrade(label, 'yarn'); + td.verify(yarn('unlink', 'ember-cli', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn('install', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn('link', 'ember-cli', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn(), { times: 3, ignoreExtraArgs: true }); + }); + + it('Make sure multiple invocations lock out.', function () { + testPackageCache._upgrade(label, 'yarn'); + testPackageCache._upgrade(label, 'yarn'); + td.verify(yarn('install', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn(), { times: 1, ignoreExtraArgs: true }); + }); + }); + + describe('with yarn.lock', function () { + beforeEach(function () { + testPackageCache._canUpgrade = () => true; + }); + + it('Trigger upgrade.', function () { + testPackageCache._upgrade(label, 'yarn'); + td.verify(yarn('upgrade', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn(), { times: 1, ignoreExtraArgs: true }); + }); + + it('Make sure it unlinks, upgrades, re-links.', function () { + // Add a link. + testPackageCache._writeManifest( + label, + 'yarn', + JSON.stringify({ + _packageCache: { + links: ['ember-cli'], + }, + }) + ); + testPackageCache._upgrade(label, 'yarn'); + td.verify(yarn('unlink', 'ember-cli', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn('upgrade', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn('link', 'ember-cli', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn(), { times: 3, ignoreExtraArgs: true }); + }); + + it('Make sure multiple invocations lock out.', function () { + testPackageCache._upgrade(label, 'yarn'); + testPackageCache._upgrade(label, 'yarn'); + td.verify(yarn('upgrade', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn(), { times: 1, ignoreExtraArgs: true }); + }); + + it('locks out _upgrade after _install', function () { + testPackageCache._install(label, 'yarn'); + testPackageCache._upgrade(label, 'yarn'); + td.verify(yarn('install', { cwd: 'hello' }), { times: 1 }); + td.verify(yarn(), { times: 1, ignoreExtraArgs: true }); + }); + }); + }); + + it('create', function () { + td.when(npm('--version')).thenReturn({ stdout: '1.0.0' }); + let dir = testPackageCache.create('npm', 'npm', '{}'); + let manifestFilePath = path.join(dir, 'package.json'); + + td.verify(npm('--version'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('install'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm(), { times: 2, ignoreExtraArgs: true }); + + expect(file(manifestFilePath)).to.exist; // Sanity check. + expect(file(manifestFilePath)).to.contain('_packageCache'); + td.reset(); + + td.when(npm('--version')).thenReturn({ stdout: '1.0.0' }); + testPackageCache.create('npm', 'npm', '{}'); + td.verify(npm('--version'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm(), { times: 1, ignoreExtraArgs: true }); + td.reset(); + + td.when(npm('--version')).thenReturn({ stdout: '1.0.0' }); + testPackageCache.create('npm', 'npm', '{ "dependencies": "different" }'); + td.verify(npm('--version'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('install'), { ignoreExtraArgs: true }); + td.verify(npm(), { times: 2, ignoreExtraArgs: true }); + td.reset(); + + td.when(npm('--version')).thenReturn({ stdout: '1.0.0' }); + testPackageCache.create('npm', 'npm', '{ "dependencies": "different" }'); + td.verify(npm('--version'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm(), { times: 1, ignoreExtraArgs: true }); + td.reset(); + + td.when(npm('--version')).thenReturn({ stdout: '1.0.0' }); + testPackageCache.create('npm', 'npm', '{ "dependencies": "different" }', ['ember-cli']); + td.verify(npm('--version'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('unlink'), { ignoreExtraArgs: true }); + td.verify(npm('install'), { ignoreExtraArgs: true }); + td.verify(npm('link'), { ignoreExtraArgs: true }); + td.verify(npm(), { times: 4, ignoreExtraArgs: true }); + td.reset(); + + // Correctly catches linked versions. + td.when(npm('--version')).thenReturn({ stdout: '1.0.0' }); + testPackageCache.create('npm', 'npm', '{ "dependencies": "different" }', ['ember-cli']); + td.verify(npm('--version'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm(), { times: 1, ignoreExtraArgs: true }); + td.reset(); + + td.when(npm('--version')).thenReturn({ stdout: '1.0.0' }); + testPackageCache.create('npm', 'npm', '{ "dependencies": "changed again" }', ['ember-cli']); + td.verify(npm('--version'), { times: 1, ignoreExtraArgs: true }); + td.verify(npm('unlink'), { ignoreExtraArgs: true }); + td.verify(npm('install'), { ignoreExtraArgs: true }); + td.verify(npm('link'), { ignoreExtraArgs: true }); + td.verify(npm(), { times: 4, ignoreExtraArgs: true }); + + // Clean up. + testPackageCache.destroy('npm'); + }); + + it('get', function () { + testPackageCache._conf.set('label', 'foo'); + expect(testPackageCache.get('label')).to.equal('foo'); + }); + + it('destroy', function () { + testPackageCache._writeManifest('label', 'yarn', '{}'); + + let dir = testPackageCache.get('label'); + let manifestFilePath = path.join(dir, 'package.json'); + expect(file(manifestFilePath)).to.exist; // Sanity check. + + testPackageCache.destroy('label'); + expect(file(manifestFilePath)).to.not.exist; + expect(testPackageCache.dirs['label']).to.be.undefined; + }); + + it('clone', function () { + testPackageCache._writeManifest('from', 'yarn', '{}'); + + let fromDir = testPackageCache.dirs['from']; + let toDir = testPackageCache.clone('from', 'to'); + + expect(fromDir).to.not.equal(toDir); + + let fromManifest = testPackageCache._readManifest('from', 'yarn'); + let toManifest = testPackageCache._readManifest('to', 'yarn'); + + expect(fromManifest).to.equal(toManifest); + + // Clean up. + testPackageCache.destroy('from'); + testPackageCache.destroy('to'); + }); + + it('succeeds at a clean install', function () { + this.timeout(15000); + + // Intentionally turning off testing mode. + testPackageCache.__resetForTesting(); + + let manifest = JSON.stringify({ + name: 'foo', + dependencies: { + 'left-pad': 'latest', + }, + }); + + let dir = testPackageCache.create('npm', 'npm', manifest); + let manifestFilePath = path.join(dir, 'package.json'); + let assetPath = path.join(dir, 'node_modules', 'left-pad', 'package.json'); + + // the manifest was written + expect(file(manifestFilePath)).to.exist; + + // the dependencies were installed + expect(file(assetPath)).to.exist; + + testPackageCache.destroy('npm'); + }); +}); diff --git a/tests/unit/utilities/print-command-test.js b/tests/unit/utilities/print-command-test.js new file mode 100644 index 0000000000..2a6af38748 --- /dev/null +++ b/tests/unit/utilities/print-command-test.js @@ -0,0 +1,108 @@ +'use strict'; + +const printCommand = require('@ember-tooling/blueprint-model/utilities/print-command'); +const processHelpString = require('../../helpers/process-help-string'); +const { expect } = require('chai'); +const EOL = require('os').EOL; + +describe('printCommand', function () { + it('handles all possible options', function () { + let availableOptions = [ + { + name: 'test-option', + values: ['x', 'y'], + default: 'my-def-val', + required: true, + aliases: ['a', 'long-a', { b: 'c', unused: '' }, { 'long-b': 'c' }], + description: 'option desc', + }, + { + name: 'test-type', + type: Boolean, + aliases: ['a'], + }, + { + name: 'test-type-array', + type: ['a-type', Number], + }, + ]; + + let obj = { + description: 'a paragraph', + availableOptions, + anonymousOptions: ['anon-test', ''], + aliases: ['ab', 'cd', '', null, undefined], + }; + + let output = printCommand.call(obj, ' ', true); + + let testString = processHelpString(`\ + \u001b[33m \u001b[39m \u001b[36m\u001b[39m${EOL}\ + \u001b[90ma paragraph\u001b[39m${EOL}\ + \u001b[90maliases: ab, cd\u001b[39m${EOL}\ + \u001b[36m--test-option\u001b[39m\u001b[36m=x|y\u001b[39m \u001b[36m(Required)\u001b[39m \u001b[36m(Default: my-def-val)\u001b[39m option desc${EOL}\ + \u001b[90maliases: -a , --long-a , -b (--test-option=c), --long-b (--test-option=c)\u001b[39m${EOL}\ + \u001b[36m--test-type\u001b[39m \u001b[36m(Boolean)\u001b[39m${EOL}\ + \u001b[90maliases: -a\u001b[39m${EOL}\ + \u001b[36m--test-type-array\u001b[39m \u001b[36m(a-type, Number)\u001b[39m`); + + expect(output).to.equal(testString); + }); + + it('can have no margin or no options', function () { + let output = printCommand.call({ + availableOptions: [], + anonymousOptions: [], + }); + + let testString = processHelpString(''); + + expect(output).to.equal(testString); + }); + + it('can have an uncolored description', function () { + let output = printCommand.call({ + description: 'a paragraph', + availableOptions: [], + anonymousOptions: [], + }); + + let testString = processHelpString(`${EOL}\ + a paragraph`); + + expect(output).to.equal(testString); + }); + + it('does not print with empty aliases', function () { + let output = printCommand.call({ + availableOptions: [], + anonymousOptions: [], + aliases: [], + }); + + let testString = processHelpString(''); + + expect(output).to.equal(testString); + }); + + it('quotes empty strings', function () { + let output = printCommand.call({ + availableOptions: [ + { + name: 'type', + type: ['', 'foo', 'bar'], // setting to empty string is allowed + default: '', + aliases: [{ 'no-type': '' }], + }, + ], + anonymousOptions: [], + }); + + let testString = processHelpString(`\ + \u001b[36m\u001b[39m${EOL}\ + \u001b[36m--type\u001b[39m \u001b[36m("", foo, bar)\u001b[39m \u001b[36m(Default: "")\u001b[39m${EOL}\ + \u001b[90maliases: --no-type (--type="")\u001b[39m`); + + expect(output).to.equal(testString); + }); +}); diff --git a/tests/unit/utilities/process-template-test.js b/tests/unit/utilities/process-template-test.js new file mode 100644 index 0000000000..1cdc753c19 --- /dev/null +++ b/tests/unit/utilities/process-template-test.js @@ -0,0 +1,16 @@ +'use strict'; + +const { expect } = require('chai'); +const processTemplate = require('@ember-tooling/blueprint-model/utilities/process-template'); + +describe('process-template', function () { + it('successfully transforms a template', function () { + expect(processTemplate('hello <%= user %>!', { user: 'fred' })).to.be.equal('hello fred!'); + expect(processTemplate('<%- value %>', { value: '