Repository: apache/cordova-lib Branch: master Commit: 1df24469dac9 Files: 278 Total size: 745.1 KB Directory structure: gitextract_tgy_emxc/ ├── .asf.yaml ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── BUG_REPORT.md │ │ ├── FEATURE_REQUEST.md │ │ └── SUPPORT_QUESTION.md │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── ci.yml │ └── release-audit.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .ratignore ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── RELEASENOTES.md ├── cordova-lib.js ├── eslint.config.js ├── integration-tests/ │ ├── HooksRunner.spec.js │ ├── fetch.spec.js │ ├── pkgJson.spec.js │ ├── platform.spec.js │ ├── plugin.spec.js │ ├── plugman_fetch.spec.js │ └── plugman_uninstall.spec.js ├── licence_checker.yml ├── package.json ├── spec/ │ ├── common.js │ ├── cordova/ │ │ ├── build.spec.js │ │ ├── compile.spec.js │ │ ├── cordova-lib.spec.js │ │ ├── emulate.spec.js │ │ ├── fixtures/ │ │ │ ├── basePkgJson/ │ │ │ │ ├── config.xml │ │ │ │ ├── package.json │ │ │ │ ├── plugins/ │ │ │ │ │ └── .svn │ │ │ │ └── www/ │ │ │ │ ├── css/ │ │ │ │ │ └── index.css │ │ │ │ ├── index.html │ │ │ │ ├── js/ │ │ │ │ │ └── index.js │ │ │ │ └── spec.html │ │ │ ├── plugins/ │ │ │ │ ├── @cordova/ │ │ │ │ │ └── plugin-test-dummy/ │ │ │ │ │ ├── package.json │ │ │ │ │ └── plugin.xml │ │ │ │ ├── com.plugin.withhooks/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── plugin.xml │ │ │ │ │ └── scripts/ │ │ │ │ │ ├── android/ │ │ │ │ │ │ └── androidBeforeBuild.js │ │ │ │ │ ├── beforeBuild.bat │ │ │ │ │ ├── beforeBuild.js │ │ │ │ │ ├── beforeBuild.sh │ │ │ │ │ └── windows/ │ │ │ │ │ └── windowsBeforeBuild.js │ │ │ │ ├── cordova-lib-test-plugin/ │ │ │ │ │ ├── LICENSE │ │ │ │ │ ├── NOTICE │ │ │ │ │ ├── package.json │ │ │ │ │ └── plugin.xml │ │ │ │ ├── fake1/ │ │ │ │ │ ├── package.json │ │ │ │ │ └── plugin.xml │ │ │ │ ├── org.test.defaultvariables/ │ │ │ │ │ ├── package.json │ │ │ │ │ └── plugin.xml │ │ │ │ └── test/ │ │ │ │ ├── package.json │ │ │ │ ├── plugin.xml │ │ │ │ └── www/ │ │ │ │ └── test.js │ │ │ ├── projectHooks/ │ │ │ │ ├── android/ │ │ │ │ │ ├── appAndroidBeforeBuild.bat │ │ │ │ │ ├── appAndroidBeforeBuild.js │ │ │ │ │ └── appAndroidBeforeBuild.sh │ │ │ │ ├── appBeforeBuild02.js │ │ │ │ ├── appBeforeBuild1.bat │ │ │ │ ├── appBeforeBuild1.sh │ │ │ │ ├── fail.js │ │ │ │ ├── orderLogger.js │ │ │ │ └── windows/ │ │ │ │ ├── appWindowsBeforeBuild.bat │ │ │ │ ├── appWindowsBeforeBuild.js │ │ │ │ └── appWindowsBeforeBuild.sh │ │ │ └── projects/ │ │ │ ├── ProjectMetadata/ │ │ │ │ └── config.xml │ │ │ └── platformApi/ │ │ │ └── platforms/ │ │ │ └── windows/ │ │ │ └── cordova/ │ │ │ └── Api.js │ │ ├── platform/ │ │ │ ├── addHelper.spec.js │ │ │ ├── getPlatformDetailsFromDir.spec.js │ │ │ ├── index.spec.js │ │ │ ├── list.spec.js │ │ │ ├── listDeprecated.spec.js │ │ │ └── remove.spec.js │ │ ├── platforms/ │ │ │ └── platforms.spec.js │ │ ├── plugin/ │ │ │ ├── add.getFetchVersion.spec.js │ │ │ ├── add.spec.js │ │ │ ├── index.spec.js │ │ │ ├── list.spec.js │ │ │ ├── plugin_spec_parser.spec.js │ │ │ ├── remove.spec.js │ │ │ └── util.spec.js │ │ ├── prepare/ │ │ │ └── platforms.spec.js │ │ ├── prepare.spec.js │ │ ├── project-metadata-apis.spec.js │ │ ├── requirements.spec.js │ │ ├── restore-util.spec.js │ │ ├── run.spec.js │ │ └── util.spec.js │ ├── fixture-helper.js │ ├── helper.js │ ├── helpers.js │ ├── hooks/ │ │ └── Context.spec.js │ ├── plugman/ │ │ ├── install.spec.js │ │ ├── plugins/ │ │ │ ├── com.adobe.vars/ │ │ │ │ └── plugin.xml │ │ │ ├── com.cordova.engine/ │ │ │ │ ├── megaBoringVersion │ │ │ │ ├── megaFunVersion │ │ │ │ └── plugin.xml │ │ │ ├── com.cordova.engine-android/ │ │ │ │ └── plugin.xml │ │ │ ├── dependencies/ │ │ │ │ ├── A/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── A.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── APluginCommand.h │ │ │ │ │ │ └── APluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-a.js │ │ │ │ ├── B/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── B.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── BPluginCommand.h │ │ │ │ │ │ └── BPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-b.js │ │ │ │ ├── C/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── C.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── CPluginCommand.h │ │ │ │ │ │ └── CPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-c.js │ │ │ │ ├── C@1.0.0/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── C.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── CPluginCommand.h │ │ │ │ │ │ └── CPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-c.js │ │ │ │ ├── D/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── D.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── DPluginCommand.h │ │ │ │ │ │ └── DPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-d.js │ │ │ │ ├── E/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── E.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── EPluginCommand.h │ │ │ │ │ │ └── EPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-d.js │ │ │ │ ├── F/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── F.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── FPluginCommand.h │ │ │ │ │ │ └── FPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-f.js │ │ │ │ ├── G/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── G.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── EPluginCommand.m │ │ │ │ │ │ └── GPluginCommand.h │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-g.js │ │ │ │ ├── H/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── H.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── HPluginCommand.h │ │ │ │ │ │ └── HPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-h.js │ │ │ │ ├── I/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── I.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── IPluginCommand.h │ │ │ │ │ │ └── IPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-i.js │ │ │ │ ├── README.md │ │ │ │ ├── Test1/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ └── android/ │ │ │ │ │ │ └── Test1.java │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-test.js │ │ │ │ ├── Test2/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ └── android/ │ │ │ │ │ │ └── Test2.java │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-test.js │ │ │ │ ├── Test3/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ └── android/ │ │ │ │ │ │ └── Test3.java │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-test.js │ │ │ │ ├── Test4/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ └── android/ │ │ │ │ │ │ └── Test4.java │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-test.js │ │ │ │ ├── meta/ │ │ │ │ │ ├── D/ │ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ │ ├── src/ │ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ │ └── D.java │ │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ │ ├── DPluginCommand.h │ │ │ │ │ │ │ └── DPluginCommand.m │ │ │ │ │ │ └── www/ │ │ │ │ │ │ └── plugin-d.js │ │ │ │ │ └── subdir/ │ │ │ │ │ └── E/ │ │ │ │ │ ├── plugin.xml │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── android/ │ │ │ │ │ │ │ └── E.java │ │ │ │ │ │ └── ios/ │ │ │ │ │ │ ├── EPluginCommand.h │ │ │ │ │ │ └── EPluginCommand.m │ │ │ │ │ └── www/ │ │ │ │ │ └── plugin-e.js │ │ │ │ └── subdir/ │ │ │ │ └── E/ │ │ │ │ ├── plugin.xml │ │ │ │ ├── src/ │ │ │ │ │ ├── android/ │ │ │ │ │ │ └── E.java │ │ │ │ │ └── ios/ │ │ │ │ │ ├── EPluginCommand.h │ │ │ │ │ └── EPluginCommand.m │ │ │ │ └── www/ │ │ │ │ └── plugin-e.js │ │ │ ├── org.test.androidonly/ │ │ │ │ ├── plugin.xml │ │ │ │ └── www/ │ │ │ │ └── android.js │ │ │ ├── org.test.defaultvariables/ │ │ │ │ └── plugin.xml │ │ │ ├── org.test.invalid.engine.no.platform/ │ │ │ │ └── plugin.xml │ │ │ ├── org.test.invalid.engine.no.scriptSrc/ │ │ │ │ └── plugin.xml │ │ │ ├── org.test.invalid.engine.script/ │ │ │ │ └── plugin.xml │ │ │ ├── org.test.plugins.childbrowser/ │ │ │ │ ├── package.json │ │ │ │ ├── plugin.xml │ │ │ │ ├── src/ │ │ │ │ │ ├── android/ │ │ │ │ │ │ └── ChildBrowser.java │ │ │ │ │ └── ios/ │ │ │ │ │ ├── ChildBrowserCommand.h │ │ │ │ │ ├── ChildBrowserCommand.m │ │ │ │ │ ├── ChildBrowserViewController.h │ │ │ │ │ ├── ChildBrowserViewController.m │ │ │ │ │ ├── ChildBrowserViewController.xib │ │ │ │ │ ├── TargetDirTest.h │ │ │ │ │ ├── TargetDirTest.m │ │ │ │ │ └── preserveDirs/ │ │ │ │ │ ├── PreserveDirsTest.h │ │ │ │ │ └── PreserveDirsTest.m │ │ │ │ └── www/ │ │ │ │ ├── childbrowser.js │ │ │ │ └── childbrowser_file.html │ │ │ ├── org.test.plugins.dummyplugin/ │ │ │ │ ├── android-resource.xml │ │ │ │ ├── extra.gradle │ │ │ │ ├── plugin-lib/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ ├── libFile │ │ │ │ │ └── project.properties │ │ │ │ ├── plugin-lib2/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ ├── libFile │ │ │ │ │ └── project.properties │ │ │ │ ├── plugin.xml │ │ │ │ ├── src/ │ │ │ │ │ ├── android/ │ │ │ │ │ │ └── DummyPlugin.java │ │ │ │ │ └── ios/ │ │ │ │ │ ├── Custom.framework/ │ │ │ │ │ │ ├── someFheader.h │ │ │ │ │ │ └── somebinlib │ │ │ │ │ ├── DummyPlugin.bundle │ │ │ │ │ ├── DummyPluginCommand.h │ │ │ │ │ ├── DummyPluginCommand.m │ │ │ │ │ ├── SourceWithFramework.m │ │ │ │ │ ├── TargetDirTest.h │ │ │ │ │ └── TargetDirTest.m │ │ │ │ └── www/ │ │ │ │ └── dummyplugin.js │ │ │ ├── pkgjson-test-plugin/ │ │ │ │ ├── package.json │ │ │ │ └── plugin.xml │ │ │ └── recursivePlug/ │ │ │ ├── asset.txt │ │ │ ├── demo/ │ │ │ │ └── config.xml │ │ │ ├── package.json │ │ │ └── plugin.xml │ │ ├── util/ │ │ │ ├── dependencies.spec.js │ │ │ └── metadata.spec.js │ │ └── variable-merge.spec.js │ ├── project-test-helpers.js │ └── support/ │ └── jasmine.json └── src/ ├── cordova/ │ ├── build.js │ ├── clean.js │ ├── compile.js │ ├── cordova.js │ ├── emulate.js │ ├── platform/ │ │ ├── addHelper.js │ │ ├── getPlatformDetailsFromDir.js │ │ ├── index.js │ │ ├── list.js │ │ └── remove.js │ ├── plugin/ │ │ ├── add.js │ │ ├── index.js │ │ ├── list.js │ │ ├── plugin_spec_parser.js │ │ ├── remove.js │ │ └── util.js │ ├── prepare/ │ │ └── platforms.js │ ├── prepare.js │ ├── project_metadata.js │ ├── requirements.js │ ├── restore-util.js │ ├── run.js │ ├── targets.js │ └── util.js ├── hooks/ │ ├── Context.js │ ├── HooksRunner.js │ └── scriptsFinder.js ├── platforms/ │ ├── index.js │ ├── platforms.js │ └── platformsConfig.json ├── plugman/ │ ├── fetch.js │ ├── install.js │ ├── plugman.js │ ├── uninstall.js │ ├── util/ │ │ ├── default-engines.js │ │ ├── dep-graph.js │ │ ├── dependencies.js │ │ └── metadata.js │ └── variable-merge.js └── util/ └── promise-util.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .asf.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. github: description: Apache Cordova Tooling Library homepage: https://cordova.apache.org/ labels: - cordova - mobile - javascript - nodejs - hacktoberfest features: wiki: false issues: true projects: true enabled_merge_buttons: squash: true merge: false rebase: false notifications: commits: commits@cordova.apache.org issues: issues@cordova.apache.org pullrequests_status: issues@cordova.apache.org pullrequests_comment: issues@cordova.apache.org ================================================ FILE: .gitattributes ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. * text=auto eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/BUG_REPORT.md ================================================ --- name: 🐛 Bug Report about: If something isn't working as expected. --- # Bug Report ## Problem ### What is expected to happen? ### What does actually happen? ## Information ### Command or Code ### Environment, Platform, Device ### Version information ## Checklist - [ ] I searched for existing GitHub issues - [ ] I updated all Cordova tooling to most recent version - [ ] I included all the necessary information above ================================================ FILE: .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md ================================================ --- name: 🚀 Feature Request about: A suggestion for a new functionality --- # Feature Request ## Motivation Behind Feature ## Feature Description ## Alternatives or Workarounds ================================================ FILE: .github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md ================================================ --- name: 💬 Support Question about: If you have a question, please check out our Slack or StackOverflow! --- Apache Cordova uses GitHub Issues as a feature request and bug tracker _only_. For usage and support questions, please check out the resources below. Thanks! --- You can get answers to your usage and support questions about **Apache Cordova** on: * Slack Community Chat: https://cordova.slack.com (you can sign-up at http://slack.cordova.io/) * StackOverflow: https://stackoverflow.com/questions/tagged/cordova using the tag `cordova` --- If you are using a tool that uses Cordova internally, like e.g. Ionic, check their support channels: * **Ionic Framework** * [Ionic Community Forum](https://forum.ionicframework.com/) * [Ionic Worldwide Slack](https://ionicworldwide.herokuapp.com/) * **PhoneGap** * [PhoneGap Developer Community](https://forums.adobe.com/community/phonegap) ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Issue Type - [ ] Bug Report - [ ] Feature Request - [ ] Support Question ## Description ## Information ### Command or Code ### Environment, Platform, Device ### Version information ## Checklist - [ ] I searched for already existing GitHub issues about this - [ ] I updated all Cordova tooling to their most recent version - [ ] I included all the necessary information above ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Platforms affected ### Motivation and Context ### Description ### Testing ### Checklist - [ ] I've run the tests to see all new and existing tests pass - [ ] I added automated test coverage as appropriate for this change - [ ] Commit is prefixed with `(platform)` if this change only applies to one platform (e.g. `(android)`) - [ ] If this Pull Request resolves an issue, I linked to the issue in the text above (and used the correct [keyword to close issues using keywords](https://help.github.com/articles/closing-issues-using-keywords/)) - [ ] I've updated the documentation if necessary ================================================ FILE: .github/workflows/ci.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Node CI on: push: branches-ignore: - 'dependabot/**' pull_request: branches: - '*' permissions: contents: read security-events: write jobs: test: name: NodeJS ${{ matrix.node-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: node-version: [20.x, 22.x, 24.x] os: [ubuntu-latest, windows-latest, macos-15] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Environment Information run: | node --version npm --version - uses: github/codeql-action/init@v3 with: languages: javascript queries: security-and-quality config: | paths-ignore: - coverage - node_modules - spec/plugman/plugins/recursivePlug/demo - name: npm install and test run: npm cit env: CI: true - uses: github/codeql-action/analyze@v3 # v4.6.0 - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 if: success() with: name: ${{ runner.os }} node.js ${{ matrix.node-version }} token: ${{ secrets.CORDOVA_CODECOV_TOKEN }} fail_ci_if_error: false ================================================ FILE: .github/workflows/release-audit.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Release Auditing on: push: branches-ignore: - 'dependabot/**' pull_request: branches: - '*' permissions: contents: read jobs: test: name: Audit Licenses runs-on: ubuntu-latest steps: # Checkout project - uses: actions/checkout@v6 # Check license headers (v2.0.0) - uses: erisu/apache-rat-action@46fb01ce7d8f76bdcd7ab10e7af46e1ea95ca01c # Setup environment with node - uses: actions/setup-node@v6 with: node-version: 24 # Install node packages - name: npm install packages run: npm ci # Check node package licenses (v2.0.1) - uses: erisu/license-checker-action@99cffa11264fe545fd0baa6c13bca5a00ae608f2 with: license-config: 'licence_checker.yml' include-asf-category-a: true ================================================ FILE: .gitignore ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Coverage coverage/ lcov.info lib-cov # Dependency directories node_modules/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # System Files .DS_Store # Temp files and folders temp # Editor related files and folders .idea .vscode # Other files **/*.swp *.jar # Test related files and folders spec/plugman/plugins/recursivePlug/demo/fetch.json spec/plugman/plugins/recursivePlug/demo/test-recursive ================================================ FILE: .npmignore ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. .* spec integration-tests coverage temp eslint.config.js licence_checker.yml # Output of 'npm pack' *.tgz ================================================ FILE: .npmrc ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. registry=https://registry.npmjs.org ================================================ FILE: .ratignore ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. .git/ coverage/ node_modules/ spec/cordova/fixtures/ spec/plugman/plugins/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Apache Cordova Anyone can contribute to Cordova. And we need your contributions. There are multiple ways to contribute: report bugs, improve the docs, and contribute code. For instructions on this, start with the [contribution overview](http://cordova.apache.org/contribute/). The details are explained there, but the important items are: - Check for Github issues that corresponds to your contribution and link or create them if necessary. - Run the tests so your patch doesn't break existing functionality. We look forward to your contributions! ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: NOTICE ================================================ Apache Cordova Copyright 2012 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). ================================================ FILE: README.md ================================================ [![Build status](https://ci.appveyor.com/api/projects/status/hovrl5rwj03co6oa/branch/master?svg=true)](https://ci.appveyor.com/project/ApacheSoftwareFoundation/cordova-lib/branch/master) [![Build Status](https://travis-ci.org/apache/cordova-lib.svg?branch=master)](https://travis-ci.org/apache/cordova-lib) [![Code coverage](https://codecov.io/github/apache/cordova-lib/coverage.svg?branch=master)](https://codecov.io/github/apache/cordova-lib?branch=master) [![NPM](https://nodei.co/npm/cordova.png)](https://nodei.co/npm/cordova/) # cordova-lib Contains npm modules used primarily by [cordova](https://github.com/apache/cordova-cli/) and [plugman](https://github.com/apache/cordova-plugman/). ## Setup from a cloned repo * Clone this repository onto your local machine. `git clone https://github.com/apache/cordova-lib.git` * Install dependencies and npm-link `npm install && npm link` ## Setup from npm * `npm install cordova-lib` > Note: you will likely also want to get github.com/apache/cordova-common, github.com/apache/cordova-create, github.com/apache/cordova-serve which previously lived in this repo but have since been moved. ## npm commands This package exposes the following commands; * `npm run lint` - runs a linter (eslint) on relevant source and test code * `npm run unit-tests` - runs the unit tests (via jasmine) from the `spec/` directory * `npm run cover` - runs istanbul code coverage tool to measure unit test code coverage * `npm run e2e-tests` - runs heavy integration tests from the `integration-tests/` directory (WARNING: these take a long time to run and rely on file and network I/O) * `npm test` - shortcut for running the linter, the unit tests and the integration tests ================================================ FILE: RELEASENOTES.md ================================================ # Cordova-lib Release Notes ### 13.0.0 (Oct 29, 2025) **Breaking Changes:** * [GH-964](https://github.com/apache/cordova-lib/pull/964) chore(npm)!: bump `write-file-atomic@7.0.0` * [GH-961](https://github.com/apache/cordova-lib/pull/961) chore!: update dependencies & node requirement * [GH-957](https://github.com/apache/cordova-lib/pull/957) feat!: remove `cordova serve` command * [GH-955](https://github.com/apache/cordova-lib/pull/955) chore(npm)!: bump various packages * [GH-953](https://github.com/apache/cordova-lib/pull/953) feat(plugman)!: remove `create plugin` support * [GH-949](https://github.com/apache/cordova-lib/pull/949) chore!: bump node requirement & npm dependencies * [GH-958](https://github.com/apache/cordova-lib/pull/958) chore!: remove deprecated platform data * [GH-959](https://github.com/apache/cordova-lib/pull/959) chore!: remove unused templates **Features:** * [GH-952](https://github.com/apache/cordova-lib/pull/952) feat: replace `dep-graph` dependency w/ custom class **Fixes:** * [GH-944](https://github.com/apache/cordova-lib/pull/944) fix(emulator): Support listing emulators with `--list` **Chores:** * [GH-963](https://github.com/apache/cordova-lib/pull/963) chore: update `package-lock.json` * [GH-960](https://github.com/apache/cordova-lib/pull/960) chore: various updates to the root files * [GH-956](https://github.com/apache/cordova-lib/pull/956) chore(npm): bump `@cordova/eslint-config@6.0.0` * [GH-946](https://github.com/apache/cordova-lib/pull/946) chore(deps-dev): bump `brace-expansion` from 1.1.11 to 1.1.12 * [GH-939](https://github.com/apache/cordova-lib/pull/939) chore(deps): bump `path-to-regexp` and express * [GH-937](https://github.com/apache/cordova-lib/pull/937) chore(deps): bump `cross-spawn` from 7.0.3 to 7.0.6 * [GH-938](https://github.com/apache/cordova-lib/pull/938) chore(ci): Fix failing dependabot PRs **CI & Others:** * [GH-954](https://github.com/apache/cordova-lib/pull/954) ci: various workflow improvements * [GH-950](https://github.com/apache/cordova-lib/pull/950) ci(workflow): update release-audit & license config * [GH-945](https://github.com/apache/cordova-lib/pull/945) ci: Add node 22 and 24 to CI testing matrix * [GH-941](https://github.com/apache/cordova-lib/pull/941) Keep final new line in `package.json` * [GH-942](https://github.com/apache/cordova-lib/pull/942) Persist relative paths on disk ### 12.0.2 (Oct 29, 2024) **Fixes:** * [GH-935](https://github.com/apache/cordova-lib/pull/935) fix: platform & plugin prerelease package support * [GH-933](https://github.com/apache/cordova-lib/pull/933) fix(ios): Prevent mix build phases * [GH-913](https://github.com/apache/cordova-lib/pull/913) fix: uninstalling plugin with platform separated required variables **Chores:** * [GH-934](https://github.com/apache/cordova-lib/pull/934) chore(deps): bump cookie and express * [GH-928](https://github.com/apache/cordova-lib/pull/928) chore(deps): bump micromatch from 4.0.5 to 4.0.8 * [GH-925](https://github.com/apache/cordova-lib/pull/925) chore(deps): bump braces from 3.0.2 to 3.0.3 * [GH-924](https://github.com/apache/cordova-lib/pull/924) chore(deps): Update some dependencies, add Node 20 to CI **CI:** * [GH-926](https://github.com/apache/cordova-lib/pull/926) ci(release-audit): add license header and dependency checker * [GH-923](https://github.com/apache/cordova-lib/pull/923) ci: update codecov@v4 w/ token ### 12.0.1 (May 19, 2023) * [GH-918](https://github.com/apache/cordova-lib/pull/918) fix: platform add with tarball & directory ### 12.0.0 (May 13, 2023) **Features:** * [GH-917](https://github.com/apache/cordova-lib/pull/917) feat!(`run`): call platform api to list targets * [GH-894](https://github.com/apache/cordova-lib/pull/894) feat!: remove platform pinning * [GH-896](https://github.com/apache/cordova-lib/pull/896) feat!: remove **OSX** & **Windows** platform **Dependencies:** * [GH-915](https://github.com/apache/cordova-lib/pull/915) dep!: bump `@cordova/eslint-config@latest@5.0.0` w/ automatic fix * [GH-914](https://github.com/apache/cordova-lib/pull/914) dep!: packages upgrade & requirements * Bumped Packages * `cordova-common@5.0.0` * `cordova-fetch@4.0.0` * `cordova-serve@4.0.1` * `init-package-json@5.0.0` * `jasmine@4.6.0` * `semver@7.5.0` * Rebuilt `package-lock.json` * Bumped `node` engine requirement `>=16.13.0` * [GH-910](https://github.com/apache/cordova-lib/pull/910) dep(npm): bump all dependencies to next major * `fs-extra@^11.1.0` * `write-file-atomic@^5.0.0` * `cordova-android@^11.0.0` * `jasmine@^4.5.0` * `jasmine-spec-reporter@^7.0.0` * `rewire@^6.0.0` * `init-package-json@^4.0.1` **Others:** * [GH-916](https://github.com/apache/cordova-lib/pull/916) fix(node-18): hook tests * [GH-905](https://github.com/apache/cordova-lib/pull/905) ci(workflow): update codecov action usage * [GH-903](https://github.com/apache/cordova-lib/pull/903) ci(workflow): update node support & action dependencies * [GH-911](https://github.com/apache/cordova-lib/pull/911) test: temporary disable broken spec#012 ### 11.1.0 (Dec 26, 2022) * [GH-904](https://github.com/apache/cordova-lib/pull/904) dep(npm): bump `cordova-fetch@3.1.0` w/ package-lock rebuild * [GH-901](https://github.com/apache/cordova-lib/pull/901) feat: bump dependencies to latest minor & patch revision * `fs-extra@^10.1.0` * `globby@^11.1.0` * `semver@^7.3.8` * `cordova-android@10.1.2` * `jasmine@^3.99.0` * [GH-899](https://github.com/apache/cordova-lib/pull/899) feat: bump `cordova-common@4.1.0` * [GH-852](https://github.com/apache/cordova-lib/pull/852) feat(windows): deprecate platform * [GH-851](https://github.com/apache/cordova-lib/pull/851) feat(osx): deprecate platform ### 11.0.0 (Dec 14, 2021) * [GH-889](https://github.com/apache/cordova-lib/pull/889) bump(platform): bump **Electron** & **Android** to latest release * [GH-890](https://github.com/apache/cordova-lib/pull/890) chore: rebuild `package-lock` * [GH-888](https://github.com/apache/cordova-lib/pull/888) dep(dev)!: bump `@cordova/eslint-config@^4.0.0` w/ fix * [GH-887](https://github.com/apache/cordova-lib/pull/887) dep!: update dependencies * [GH-873](https://github.com/apache/cordova-lib/pull/873) ci: switch to GitHub Actions * [GH-884](https://github.com/apache/cordova-lib/pull/884) chore!: drop support for Node.js 10 ### 10.1.0 (Sep 29, 2021) **Features:** * [GH-885](https://github.com/apache/cordova-lib/pull/885) feat: bump platform pinning * [GH-860](https://github.com/apache/cordova-lib/pull/860) feat(`cordova/util`): support loading platform API from `node_modules` **Fixes:** * [GH-880](https://github.com/apache/cordova-lib/pull/880) fix(`restore-util`): properly support long and short platform names * [GH-874](https://github.com/apache/cordova-lib/pull/874) fix: Platforms restored from both dev and normal dependencies. * [GH-871](https://github.com/apache/cordova-lib/pull/871) fix: remove undeclared dependency on `underscore` * [GH-856](https://github.com/apache/cordova-lib/pull/856) fix(`cordova/util`): version detection for legacy platforms **Chores & Refactor Changes:** * [GH-886](https://github.com/apache/cordova-lib/pull/886) chore: update dependencies w/ `package-lock` rebuild & test update * [GH-881](https://github.com/apache/cordova-lib/pull/881) chore: `npmrc` * [GH-879](https://github.com/apache/cordova-lib/pull/879) chore: `package-lock` update * [GH-858](https://github.com/apache/cordova-lib/pull/858) chore: clean up `package.json` * [GH-882](https://github.com/apache/cordova-lib/pull/882) refactor(`addHelper`): more concise `package.json` spec lookup **CI, Test & Doc Changes:** * [GH-862](https://github.com/apache/cordova-lib/pull/862) ci: add node-14.x to workflow * [GH-870](https://github.com/apache/cordova-lib/pull/870) test(`plugin.spec`): fix version change in test fixture * [GH-865](https://github.com/apache/cordova-lib/pull/865) test(`pkgJson`): make expectations work for npm 5 to 7 * [GH-864](https://github.com/apache/cordova-lib/pull/864) test(`pkgJson`): fix test after release of geolocation plugin v4.1.0 * [GH-855](https://github.com/apache/cordova-lib/pull/855) test: fix missing stack traces in jasmine output * [GH-853](https://github.com/apache/cordova-lib/pull/853) test: unit test deprecated platforms * [GH-877](https://github.com/apache/cordova-lib/pull/877) docs: correct linter command in `README` ### 10.0.0 (Jul 22, 2020) * [GH-846](https://github.com/apache/cordova-lib/pull/846) breaking: remove `cordova info` logic from `lib` * [GH-849](https://github.com/apache/cordova-lib/pull/849) breaking: bump dependencies * [GH-847](https://github.com/apache/cordova-lib/pull/847) chore: update dev dependencies * [GH-848](https://github.com/apache/cordova-lib/pull/848) chore: bump `cordova-eslint` w/ automatic fixes * [GH-843](https://github.com/apache/cordova-lib/pull/843) test(`fixture-helper`): install **Android** platform by name again * [GH-844](https://github.com/apache/cordova-lib/pull/844) fix: remove unused dependency on `cordova-create` * [GH-841](https://github.com/apache/cordova-lib/pull/841) chore: add `package-lock.json` * [GH-840](https://github.com/apache/cordova-lib/pull/840) chore: use short notation in `package.json` * [GH-839](https://github.com/apache/cordova-lib/pull/839) chore: stop testing with nightly * [GH-838](https://github.com/apache/cordova-lib/pull/838) chore: update **Android** platform pinning to 9.0.0 * [GH-837](https://github.com/apache/cordova-lib/pull/837) chore: update **OSX** platform pinning to 6.0.0 * [GH-836](https://github.com/apache/cordova-lib/pull/836) chore: update **iOS** platform pinning to 6.1.0 * [GH-835](https://github.com/apache/cordova-lib/pull/835) GH-832: Look at devDeps for restoring platforms * [GH-833](https://github.com/apache/cordova-lib/pull/833) breaking: upgrade cordova dependencies for next major * [GH-831](https://github.com/apache/cordova-lib/pull/831) test: use `expectAsync` for rejections * [GH-825](https://github.com/apache/cordova-lib/pull/825) test(e2e): improve `HooksRunner.spec` * [GH-828](https://github.com/apache/cordova-lib/pull/828) chore: consolidate eslint configs * [GH-803](https://github.com/apache/cordova-lib/pull/803) test: move `cordova/platform/{platform => addHelper}` * [GH-827](https://github.com/apache/cordova-lib/pull/827) fix: plugin installation from `git` url w/ `semver` * [GH-826](https://github.com/apache/cordova-lib/pull/826) test: use `fs.copySync` for increased performance * [GH-823](https://github.com/apache/cordova-lib/pull/823) test(e2e): re-enable HooksRunner#12 and move it to plugin#14 * [GH-824](https://github.com/apache/cordova-lib/pull/824) style: fix linting violations * [GH-821](https://github.com/apache/cordova-lib/pull/821) feat: proper support for scoped plugins * [GH-822](https://github.com/apache/cordova-lib/pull/822) refactor: `eslint` setup * [GH-820](https://github.com/apache/cordova-lib/pull/820) refactor: remove stub interface to `cordova-create` * [GH-819](https://github.com/apache/cordova-lib/pull/819) refactor: use `execa`'s cross-platform shebang support in `HooksRunner` * [GH-812](https://github.com/apache/cordova-lib/pull/812) chore: replace `superspawn` with `execa` * [GH-781](https://github.com/apache/cordova-lib/pull/781) chore: remove `plugin save` * [GH-780](https://github.com/apache/cordova-lib/pull/780) chore: deprecate `plugin save` command * [GH-818](https://github.com/apache/cordova-lib/pull/818) Extend and improve plugin tests in preparation of supporting scoped plugins * [GH-810](https://github.com/apache/cordova-lib/pull/810) chore: bump production dependencies * [GH-816](https://github.com/apache/cordova-lib/pull/816) Simplify `jasmine` configuration * [GH-817](https://github.com/apache/cordova-lib/pull/817) Remove dead code in `integration-tests/plugin.spec` * [GH-815](https://github.com/apache/cordova-lib/pull/815) Do not spawn child process to get platform version * [GH-813](https://github.com/apache/cordova-lib/pull/813) `plugman_fetch.spec` cleanup * [GH-814](https://github.com/apache/cordova-lib/pull/814) Remove obsolete and duplicate ignore entries * [GH-809](https://github.com/apache/cordova-lib/pull/809) chore: improve npm ignore list * [GH-811](https://github.com/apache/cordova-lib/pull/811) chore: update `jasmine` dependencies * [GH-808](https://github.com/apache/cordova-lib/pull/808) Remove unused module plugman/platforms/common * [GH-807](https://github.com/apache/cordova-lib/pull/807) Break dependency cycles * [GH-804](https://github.com/apache/cordova-lib/pull/804) Fix `cordova/emulate.spec` * [GH-806](https://github.com/apache/cordova-lib/pull/806) Remove unused exports from `cordova/util` * [GH-805](https://github.com/apache/cordova-lib/pull/805) Remove support for obsolete `/.cordova/config.json` * [GH-802](https://github.com/apache/cordova-lib/pull/802) Minor code cleanup * [GH-797](https://github.com/apache/cordova-lib/pull/797) Do not run legacy hooks from dirs anymore * [GH-800](https://github.com/apache/cordova-lib/pull/800) Remove `platform.check` * [GH-765](https://github.com/apache/cordova-lib/pull/765) Remove code to handle plugins that were added by cordova@<5.4.0 * [GH-766](https://github.com/apache/cordova-lib/pull/766) Remove parts of plugman that have been moved to the plugman repo * [GH-772](https://github.com/apache/cordova-lib/pull/772) Replace underscore with modern JS * [GH-799](https://github.com/apache/cordova-lib/pull/799) chore: drop node 6 and 8 support * [GH-798](https://github.com/apache/cordova-lib/pull/798) chore: bump version to 10.0.0-dev * [GH-770](https://github.com/apache/cordova-lib/pull/770) Use up-to-date fixtures in tests * [GH-796](https://github.com/apache/cordova-lib/pull/796) HooksRunner code & spec cleanup * [GH-791](https://github.com/apache/cordova-lib/pull/791) fix: error message during plugin installation w/ missing engine * [GH-777](https://github.com/apache/cordova-lib/pull/777) chore: add Node.js 12 to CI services * [GH-786](https://github.com/apache/cordova-lib/pull/786) Quick workaround for e2e failure on AppVeyor CI * [GH-783](https://github.com/apache/cordova-lib/pull/783) `nyc@14` update in devDependencies * [GH-775](https://github.com/apache/cordova-lib/pull/775) chore: cleanup `plugman.createPackageJson` * [GH-767](https://github.com/apache/cordova-lib/pull/767) Simpler and better `cordova/util.getPlatformApiFunction` * [GH-774](https://github.com/apache/cordova-lib/pull/774) Make `src/plugman/init-defaults.js` lintable * [GH-773](https://github.com/apache/cordova-lib/pull/773) Have `plugman.createPackageJson` create file in plugin dir, not in cwd * [GH-771](https://github.com/apache/cordova-lib/pull/771) Prevent masking of errors during testing * [GH-768](https://github.com/apache/cordova-lib/pull/768) Proper async code in `src/plugman/createpackagejson.js` * [GH-764](https://github.com/apache/cordova-lib/pull/764) chore: expressive `pkgJson.spec` * [GH-763](https://github.com/apache/cordova-lib/pull/763) Remove unnecessary spy * [GH-762](https://github.com/apache/cordova-lib/pull/762) Remove unused fixtures * [GH-761](https://github.com/apache/cordova-lib/pull/761) chore: various test improvements * Fix some test descriptions in `cordova/util.spec` * Stop `cordova/util.spec` from messing with the user's home directory! * Proper temp folder usage in `cordova/util.spec` * Remove outdated test from `cordova/util.spec` * Remove `rewire`/`revert` anti-pattern * Remove `superspawn` faking from `HooksRunner.spec` * [GH-760](https://github.com/apache/cordova-lib/pull/760) Minor cleanup of CI configs ### 9.0.1 (Mar 31, 2019) * [GH-759](https://github.com/apache/cordova-lib/pull/759) Fix faulty Promise handling in plugman.uninstall * [GH-752](https://github.com/apache/cordova-lib/pull/752) Fix restoring plugins from `package.json` * [GH-754](https://github.com/apache/cordova-lib/issues/754) [GH-755](https://github.com/apache/cordova-lib/issues/755) Do not wrap engine script path in quotes ### 9.0.0 (Mar 15, 2019) * [GH-750](https://github.com/apache/cordova-lib/pull/750) Remove saving platforms/plugins to `config.xml` * [GH-751](https://github.com/apache/cordova-lib/pull/751) Pass project `config.xml` path to platform's prepare * [GH-749](https://github.com/apache/cordova-lib/pull/749) Cordova Lib Release Preparation (Cordova 9) * Remove unused property `apiCompatibleSince` from `platformsConfig.json` * Fix plugin dependency tests when using `npm >= 5` * Bumped Platform Pinning and Support Minor SemVer * `cordova-android@^8.0.0` * `cordova-browser@^6.0.0` * `cordova-electron@^1.0.0` * `cordova-ios@^5.0.0` * `cordova-osx@^5.0.0` * `cordova-windows@^7.0.0` * Bumped dependencies * `jasmine@^3.3.1` * `globby@^9.1.0` * `underscore@^1.9.1` * `semver@^5.6.0` * `read-chunk@^3.1.0` * `init-package-json@^1.10.3` * `fs-extra@^7.0.1` * Dev Dependencies * Updated `nyc` Code Coverage * Updated ESlint with lint corrections * Added missing module `shelljs` to fix test failures * Updated Package Cordova Dependencies * `cordova-common@^3.1.0` * `cordova-create@^2.0.0` * `cordova-fetch@^2.0.0` * `cordova-serve@^3.0.0` * [GH-748](https://github.com/apache/cordova-lib/pull/748) Remove handling of legacy `.fetch.json` files * [GH-709](https://github.com/apache/cordova-lib/pull/709) `hooks/Context` Improvements * [GH-622](https://github.com/apache/cordova-lib/pull/622) [CB-14166](https://issues.apache.org/jira/browse/CB-14166) (cli) Fixed issue when install plugins on **Windows** * [GH-744](https://github.com/apache/cordova-lib/pull/744) Add **Electron** Platform * [GH-743](https://github.com/apache/cordova-lib/pull/743) Updated Platform Config Git URL Paths * [GH-742](https://github.com/apache/cordova-lib/pull/742) Cleanup indentation spacing in `*/jasmine.json` * [GH-741](https://github.com/apache/cordova-lib/pull/741) Fix crash in `cordova requirements` due to an unbound function * [GH-711](https://github.com/apache/cordova-lib/pull/711) Fix 2 integration tests failures on **macOS** * [GH-710](https://github.com/apache/cordova-lib/pull/710) Drop `Q` Dependency and Use Native Promises * [GH-687](https://github.com/apache/cordova-lib/pull/687) Test, Fix and Cleanup `cordova serve` * [GH-707](https://github.com/apache/cordova-lib/pull/707) Deprecate `requireCordovaModule` for non-Cordova modules * [GH-705](https://github.com/apache/cordova-lib/pull/705) Dereference possible symlinks when copying plugin * [GH-699](https://github.com/apache/cordova-lib/pull/699) Increase timeout for `cordova.platform` e2e tests * [GH-698](https://github.com/apache/cordova-lib/pull/698) Increase plugman install test timeout * [GH-686](https://github.com/apache/cordova-lib/pull/686) Remove support for old option format * [GH-685](https://github.com/apache/cordova-lib/pull/685) Remove unused dependency `properties-parser` * [GH-677](https://github.com/apache/cordova-lib/pull/677) Fix `cordova/platform/addHelper` tests * [GH-679](https://github.com/apache/cordova-lib/pull/679) `platform.spec` Cleanup * [GH-684](https://github.com/apache/cordova-lib/pull/684) Code Cleanup and Refactor (Bits and pieces) * [GH-683](https://github.com/apache/cordova-lib/pull/683) Remove unused npm utility functions * [GH-682](https://github.com/apache/cordova-lib/pull/682) GH-676 Remove Browserify * [GH-652](https://github.com/apache/cordova-lib/pull/652) Make `plugin.remove` more easily understandable * [GH-650](https://github.com/apache/cordova-lib/pull/650) Make `cordova/platform/check` more approachable * [GH-678](https://github.com/apache/cordova-lib/pull/678) Fix tests that are failing with npm config 'save-exact' set * [GH-675](https://github.com/apache/cordova-lib/pull/675) One root `describe` per suite * [GH-674](https://github.com/apache/cordova-lib/pull/674) `plugman/install.spec` Cleanup * [GH-672](https://github.com/apache/cordova-lib/pull/672) `plugman_uninstall.spec` Cleanup * [GH-613](https://github.com/apache/cordova-lib/pull/613) Switch to using `fs-extra` in favour of `shelljs` * [GH-665](https://github.com/apache/cordova-lib/pull/665) Radically focused `restore.spec` finally fast and reliable * [GH-671](https://github.com/apache/cordova-lib/pull/671) Remove `cordova plugin search` command * [GH-670](https://github.com/apache/cordova-lib/pull/670) Various `spec` fixes and cleanup * [GH-666](https://github.com/apache/cordova-lib/pull/666) Remove deprecated and unused content * [GH-669](https://github.com/apache/cordova-lib/pull/669) Linting Improvements * [GH-668](https://github.com/apache/cordova-lib/pull/668) Only upload coverage when tests succeed * [GH-667](https://github.com/apache/cordova-lib/pull/667) Report Code Coverage to `codecov` on Travis CI * [GH-664](https://github.com/apache/cordova-lib/pull/664) Move some cordova test fixtures into fixtures folder * [GH-651](https://github.com/apache/cordova-lib/pull/651) Remove all usage of Q-specific methods on Promise instances * [GH-662](https://github.com/apache/cordova-lib/pull/662) Remove unused content * [GH-663](https://github.com/apache/cordova-lib/pull/663) Update `read-chunk` to properly close file descriptors on failure * [GH-658](https://github.com/apache/cordova-lib/pull/658) Remove deprecated platform support files * [GH-616](https://github.com/apache/cordova-lib/pull/616) Extend and improve `cordova info` output ### 8.0.0 (Dec 14, 2017) * [CB-13057](https://issues.apache.org/jira/browse/CB-13057): removed `cordova save` command * [CB-13056](https://issues.apache.org/jira/browse/CB-13056): removed support for **WebOS**, **BlackBerry10**, and **Ubuntu** * [CB-13674](https://issues.apache.org/jira/browse/CB-13674): updated cordova dependencies * [CB-13055](https://issues.apache.org/jira/browse/CB-13055): updated integration tests, removed `lazy_load.js`, removed `gitclone.js` and `--nofetch` flag. This removes the need for us to include an npm dependency. * [CB-13532](https://issues.apache.org/jira/browse/CB-13532): updated to include a check for `package.json` `devDependencies` * [CB-12361](https://issues.apache.org/jira/browse/CB-12361): added unit tests for `check.js` * [CB-13501](https://issues.apache.org/jira/browse/CB-13501): added support for node 8 to tests * [CB-13463](https://issues.apache.org/jira/browse/CB-13463): prevent `package.json` updating plugins with `--nosave` ### 7.1.0 (Oct 04, 2017) * [CB-13303](https://issues.apache.org/jira/browse/CB-13303) added `--save_exact`, `--production` flags * [CB-13288](https://issues.apache.org/jira/browse/CB-13288) updated `index.js` and test to fix `cordova plugin search` * [CB-13206](https://issues.apache.org/jira/browse/CB-13206) fixed incorrect target being passed in to `plugin add` from `restore-util.js` * [CB-13145](https://issues.apache.org/jira/browse/CB-13145) added `variable-merge.js` to deal with `plugin.xml` variables for uninstall * [CB-12870](https://issues.apache.org/jira/browse/CB-12870) catch all use cases for `getPlatformApiFunction` and update tests accordingly * [CB-12944](https://issues.apache.org/jira/browse/CB-12944) Platform's spec is ignored in `config.xml` if `package.json` doesn't contain dependency for platform * [CB-12361](https://issues.apache.org/jira/browse/CB-12361) added new unit tests for plugin tests * [CB-13020](https://issues.apache.org/jira/browse/CB-13020) (plugman) install filters out `nohooks` * [CB-13056](https://issues.apache.org/jira/browse/CB-13056) added deprecation notice for **WebOS** * [CB-13057](https://issues.apache.org/jira/browse/CB-13057) added deprecation warning for `cordova platform save` * [CB-12361](https://issues.apache.org/jira/browse/CB-12361) added tests for `save.js` and rebased * [CB-12895](https://issues.apache.org/jira/browse/CB-12895) switched from `jshint` to `eslint` * [CB-12361](https://issues.apache.org/jira/browse/CB-12361) updated `addHelper` tests * [CB-11980](https://issues.apache.org/jira/browse/CB-11980) Update `README` to reflect new repos * [CB-6143](https://issues.apache.org/jira/browse/CB-6143) Change `plugman.emit()` to `events.emit()` * Reorganized unit test directory. Changes include: - consolidate `spec-cordova/` and `spec-plugman/` into a single `spec/` dir. - put `jasmine config` and helper modules in top-level spec dir. - changed `package.json` npm run scripts to reflect purposes of tasks. remove `npm run ci`. Updated `README` to reflect `package.json` npm run script changes. * [CB-12361](https://issues.apache.org/jira/browse/CB-12361) added unit tests for `prepare.spec.js` * Update cordova-lib api. Deprecate `raw` from api calls. * [CB-11980](https://issues.apache.org/jira/browse/CB-11980) moved `fetch`, `common` and `serve` into their own repos * [CB-12786](https://issues.apache.org/jira/browse/CB-12786) Improve logic for searching plugin id in case of module already exists in `node_modules` * [CB-12250](https://issues.apache.org/jira/browse/CB-12250) [CB-12409](https://issues.apache.org/jira/browse/CB-12409) **iOS**: Fix bug with escaping properties from plist file * [CB-12762](https://issues.apache.org/jira/browse/CB-12762) point `package.json` repo items to github mirrors instead of apache repos site * [CB-12777](https://issues.apache.org/jira/browse/CB-12777) removed **Android**, **iOS**, and **Windows** projects fixtures * [CB-12787](https://issues.apache.org/jira/browse/CB-12787) Fix plugin installation with `--link` option * [CB-12738](https://issues.apache.org/jira/browse/CB-12738) Cordova ignores plugin dependency version on **Windows** platform * [CB-12766](https://issues.apache.org/jira/browse/CB-12766) Consistently write JSON with 2 spaces indentation ### 7.0.1 (May 08, 2017) * [CB-12773](https://issues.apache.org/jira/browse/CB-12773): fixed incorrect plugin version fetching issue * [CB-12769](https://issues.apache.org/jira/browse/CB-12769): updated `cordova-create` dependency to 1.1.1 * [CB-12757](https://issues.apache.org/jira/browse/CB-12757): if there's a plugin dependency in `package.json`, use that one for `config.xml` ### 7.0.0 (May 02, 2017) * [CB-12747](https://issues.apache.org/jira/browse/CB-12747): updated pinned platforms * [CB-12705](https://issues.apache.org/jira/browse/CB-12705): Modified `(before|after)_plugin_(uninstall|install)` to always expect existence of plugin field * [CB-12705](https://issues.apache.org/jira/browse/CB-12705): Pass plugin info to project `*_plugin_install` hooks * [CB-11242](https://issues.apache.org/jira/browse/CB-11242): removed support for platforms that don't have a `package.json` * [CB-11242](https://issues.apache.org/jira/browse/CB-11242): updated tests and fixtures * [CB-11242](https://issues.apache.org/jira/browse/CB-11242): refactored out `getPlatformApiFunction` * [CB-11242](https://issues.apache.org/jira/browse/CB-11242): removed `parser` and `handler` files for deprecated versions of platforms * [CB-12683](https://issues.apache.org/jira/browse/CB-12683): improved error messaging for when a plugin doesn't have `package.json` * [CB-12674](https://issues.apache.org/jira/browse/CB-12674): Added deprecation notice for **blackberry10** and **ubuntu** * [CB-11777](https://issues.apache.org/jira/browse/CB-11777): Restore plugins before preparing * [CB-12643](https://issues.apache.org/jira/browse/CB-12643): removed references to **wp8** * [CB-12645](https://issues.apache.org/jira/browse/CB-12645): removed references to **firefoxos** * [CB-12665](https://issues.apache.org/jira/browse/CB-12665): removed `engineStrict` as it is no longer supported * [CB-12612](https://issues.apache.org/jira/browse/CB-12612): removing old `amazon-fireos` code * [CB-12425](https://issues.apache.org/jira/browse/CB-12425): autocreate a `package.json` if it doesn't exist during `cordova prepare` * [CB-12517](https://issues.apache.org/jira/browse/CB-12517): `package.json` `name` feild is `config.xml` `id` feild and `package.json` `displayName` feild is `config.xml` `name` feild * [CB-12592](https://issues.apache.org/jira/browse/CB-12592): added `requireNoCache` function and replaced instances of `delete.require cache` * [CB-12606](https://issues.apache.org/jira/browse/CB-12606): Fix plugin dependency installation. Now it respects the `spec` specified for dependencies of plugins in `plugin.xml` * [CB-12016](https://issues.apache.org/jira/browse/CB-12016): removed `pluginMapper` code from uninstall * [CB-12337](https://issues.apache.org/jira/browse/CB-12337): Resolve symbolic links in project root * [CB-11346](https://issues.apache.org/jira/browse/CB-11346): Remove known platforms check * [CB-11977](https://issues.apache.org/jira/browse/CB-11977): removed support for `node 0.x` * [CB-12021](https://issues.apache.org/jira/browse/CB-12021): Added local path support to `--fetch` and fixed failing tests for adding a relative path * [CB-11960](https://issues.apache.org/jira/browse/CB-11960): Added support to `package.json` for platform/plugin add/rm * [CB-12001](https://issues.apache.org/jira/browse/CB-12001): Added support for platform/plugin & `spec` restore to sync `config.xml` and `package.json` ### 6.5.0 (Jan 17, 2017) * [CB-12018](https://issues.apache.org/jira/browse/CB-12018): updated `jshint` and updated `jasmine` tests to work with `jasmine` instead of `jasmine-node` * [CB-12314](https://issues.apache.org/jira/browse/CB-12314) updated pinned android to 6.1.1 * [CB-12261](https://issues.apache.org/jira/browse/CB-12261) fix subdirectories deprecated warning always shows and stop fetch caused by [CB-11979](https://issues.apache.org/jira/browse/CB-11979) * [CB-12284](https://issues.apache.org/jira/browse/CB-12284) Include project root as additional root for static router * [CB-12088](https://issues.apache.org/jira/browse/CB-12088) Fix misleading warning when adding platform without `Api.js` ### 6.4.0 (Oct 21, 2016) * [CB-12039](https://issues.apache.org/jira/browse/CB-12039) updated pinned `Android` to 6.0.0 and `iOS` to 4.3.0 * [CB-11979](https://issues.apache.org/jira/browse/CB-11979) added deprecation warning for installing plugins via subdirectories * [CB-11730](https://issues.apache.org/jira/browse/CB-11730) Modify condition of if clause to avoid similar project name with plugin name * [CB-11985](https://issues.apache.org/jira/browse/CB-11985) Check if cached platform/plugin exists before `npm cache` * [CB-11951](https://issues.apache.org/jira/browse/CB-11951) [CB-11967](https://issues.apache.org/jira/browse/CB-11967) Respect preference default values when installling plugins * [CB-11771](https://issues.apache.org/jira/browse/CB-11771) Deep symlink directories to target project instead of linking the directory itself * [CB-11908](https://issues.apache.org/jira/browse/CB-11908) Handle `edit-config` in `config.xml` on prepare * Add github pull request template * [CB-8320](https://issues.apache.org/jira/browse/CB-8320) We look for a `build.gradle` to make sure it's **Android**, not an `AndroidManifest`, because it moved * [CB-11811](https://issues.apache.org/jira/browse/CB-11811) Moved **iOS** platform specific tests to `platform.spec.ios.js`, added `test-ios` npm run script. * [CB-11811](https://issues.apache.org/jira/browse/CB-11811) disable `CocoaPods` e2e test temporarily since it is platform specific and requires cocoapods to be installed. * updated `save.spec.js` to use latest **android** and newer fb plugin * [CB-11607](https://issues.apache.org/jira/browse/CB-11607) breakout `cordova-create` from `cordova-lib` * [CB-9825](https://issues.apache.org/jira/browse/CB-9825) framework tag spec parsing * [CB-11698](https://issues.apache.org/jira/browse/CB-11698) Fix plugin installation when restoring platform * [CB-11679](https://issues.apache.org/jira/browse/CB-11679) Speed up save/restore tests * [CB-11205](https://issues.apache.org/jira/browse/CB-11205) Respect saved variables when installing plugin * [CB-11589](https://issues.apache.org/jira/browse/CB-11589) Fix missing plugin files after restore ### 6.3.1 (Aug 08, 2016) * [CB-11652](https://issues.apache.org/jira/browse/CB-11652) Update run and emulate to skip build * [CB-11194](https://issues.apache.org/jira/browse/CB-11194) Defer creating of libDir folder until something actually requests it * [CB-11493](https://issues.apache.org/jira/browse/CB-11493) Add cordova emulate option to skip prepare * [CB-11205](https://issues.apache.org/jira/browse/CB-11205) Respect saved variables when installing plugin * [CB-11589](https://issues.apache.org/jira/browse/CB-11589) Fix missing plugin files after restore ### 6.3.0 (Jul 12, 2016) * [CB-11491](https://issues.apache.org/jira/browse/CB-11491) Introduce before_deploy hook * [CB-11412](https://issues.apache.org/jira/browse/CB-11412) template support for www folders * Fix config.xml path in PlatformApi.prepare * [CB-11412](https://issues.apache.org/jira/browse/CB-11412) improve template implementation * [CB-11164](https://issues.apache.org/jira/browse/CB-11164) Allow forced dependent plugin removal * [CB-11339](https://issues.apache.org/jira/browse/CB-11339) Add a warning about prerelease platform usage * [CB-11349](https://issues.apache.org/jira/browse/CB-11349) added --fetch and cordova fetch to create --template * [CB-11337](https://issues.apache.org/jira/browse/CB-11337) Use latest released platform version in e2e tests * [CB-11274](https://issues.apache.org/jira/browse/CB-11274) Platform browser: wrong path for config.xml * [CB-11274](https://issues.apache.org/jira/browse/CB-11274) Make serve dashboard take config.xml -> content.src entry point into account * [CB-11261](https://issues.apache.org/jira/browse/CB-11261) Cut out '-nightly' prerelease tag when checking plugin engines * [CB-9858](https://issues.apache.org/jira/browse/CB-9858) added fetch tests to travis * [CB-9858](https://issues.apache.org/jira/browse/CB-9858) fixed failing travis and appveyor tests ### 6.2.0 (May 12, 2016) * [CB-11259](https://issues.apache.org/jira/browse/CB-11259) Improving prepare and build logging * Resolve npm run jshint failure due to npm/npm#10343 * [CB-11200](https://issues.apache.org/jira/browse/CB-11200) Bump `node-xcode` dependency and update tests to pass * [CB-11240](https://issues.apache.org/jira/browse/CB-11240) added `--fetch` support to `cordova prepare` * [CB-9858](https://issues.apache.org/jira/browse/CB-9858) merging initial `--fetch` work for plugin and platform fetching * [CB-11194](https://issues.apache.org/jira/browse/CB-11194) Improve cordova load time * [CB-11174](https://issues.apache.org/jira/browse/CB-11174) Resolve `symlinked` path before getting `PlatformApi` instance * [CB-11036](https://issues.apache.org/jira/browse/CB-11036) `args.slice is not a function` when building **Windows** with other platform * [CB-10761](https://issues.apache.org/jira/browse/CB-10761) Resore plugins saved without spec attribute * [CB-10981](https://issues.apache.org/jira/browse/CB-10981) Remove `cordova-common` from bundled dependencies * [CB-11042](https://issues.apache.org/jira/browse/CB-11042) Add cordova run option to skip prepare * [CB-11022](https://issues.apache.org/jira/browse/CB-11022) Respect result returned by plugin installation and skip prepare if it is truthy * [CB-10975](https://issues.apache.org/jira/browse/CB-10975) Allow plugin path to be relative to current directory * [CB-10986](https://issues.apache.org/jira/browse/CB-10986) Adding support for scoped npm package plugins * [CB-10770](https://issues.apache.org/jira/browse/CB-10770) Remove `cache-min` when adding platforms * [CB-10921](https://issues.apache.org/jira/browse/CB-10921) Emit warning in case of plugin restoration failure ### 6.1.1 (Mar 29, 2016) * [CB-10961](https://issues.apache.org/jira/browse/CB-10961) Error no such file or directory adding ios platform when plugins present or required * [CB-10908](https://issues.apache.org/jira/browse/CB-10908) Reload the config.xml before writing the saved plugin ### 6.1.0 (Mar 17, 2016) * [CB-10902](https://issues.apache.org/jira/browse/CB-10902) updated pinned platforms * [CB-10808](https://issues.apache.org/jira/browse/CB-10808) revert npm install for templates * [CB-10808](https://issues.apache.org/jira/browse/CB-10808) CLI Support templates with subdirectory * [CB-10880](https://issues.apache.org/jira/browse/CB-10880) Removed plugin pinning * [CB-10679](https://issues.apache.org/jira/browse/CB-10679) Improving version choosing logic test coverage * [CB-10673](https://issues.apache.org/jira/browse/CB-10673) add plugin `--force` option. * [CB-10679](https://issues.apache.org/jira/browse/CB-10679) New version choosing logic for plugin add * [CB-10328](https://issues.apache.org/jira/browse/CB-10328) set top-level property when adding new platforms * [CB-10314](https://issues.apache.org/jira/browse/CB-10314) avoid fetching plugins when oldId is already fetched * [CB-10708](https://issues.apache.org/jira/browse/CB-10708) Install/uninstall plugins correctly into CLI project using plugman * [CB-10462](https://issues.apache.org/jira/browse/CB-10462) Get rid of npmconf in favor of npm. * [CB-10662](https://issues.apache.org/jira/browse/CB-10662) Use project's `config.xml` as a fallback for package name * [CB-10644](https://issues.apache.org/jira/browse/CB-10644) Adds deprecation message about old platforms support removal. * [CB-10519](https://issues.apache.org/jira/browse/CB-10519) Wrap all sync calls inside of `cordova.raw` methods into promises * [CB-10641](https://issues.apache.org/jira/browse/CB-10641) Adds tests for order of operations in platform add * [CB-10641](https://issues.apache.org/jira/browse/CB-10641) Run prepare `_after_` plugins were installed * [CB-10618](https://issues.apache.org/jira/browse/CB-10618) Do not call `prepBuildFiles` for `cordova-android@>=5.2.0`. * [CB-10518](https://issues.apache.org/jira/browse/CB-10518) Correct log level and error messages for some cordova errors * [CB-10550](https://issues.apache.org/jira/browse/CB-10550) Fix plugin id mapper not enforced when a version is specified * [CB-10611](https://issues.apache.org/jira/browse/CB-10611) fix `before_plugin_install` hook not disabled with `--nohooks` * [CB-10235](https://issues.apache.org/jira/browse/CB-10235) Added clearer error message for info command. * [CB-10584](https://issues.apache.org/jira/browse/CB-10584) Splashscreen plugin crashes the app on windows 10 when built with browserify * [CB-10592](https://issues.apache.org/jira/browse/CB-10592) Don't quote platform specific args values * [CB-10482](https://issues.apache.org/jira/browse/CB-10482) Remove references to **windows8** from cordova-lib/cli * [CB-10567](https://issues.apache.org/jira/browse/CB-10567) Bubble up `cordova.raw.run()` error to the caller * [CB-10553](https://issues.apache.org/jira/browse/CB-10553) Fix framework tag handler for **Android** * [CB-10461](https://issues.apache.org/jira/browse/CB-10461) `cordova platform ls` should list the versions of platforms pinned * [CB-10531](https://issues.apache.org/jira/browse/CB-10531) Enable coverage reports for cordova-lib * [CB-10465](https://issues.apache.org/jira/browse/CB-10465) Pass correct options to prepare from compile * [CB-10459](https://issues.apache.org/jira/browse/CB-10459) cordova platform list should mark amazon-fireos and wp8 as deprecated * [CB-10499](https://issues.apache.org/jira/browse/CB-10499) `--template` should pull the latest template from npm when version isn't specified * [CB-10432](https://issues.apache.org/jira/browse/CB-10432) Adds e2e test to protect against future regressions. * Added node versions matrix to `.travis.yml`. ### 6.0.0 (Jan 25, 2016) * [CB-10432](https://issues.apache.org/jira/browse/CB-10432) Fix plugin installation for newly added platform * [CB-10423](https://issues.apache.org/jira/browse/CB-10423) allow recursive folder copy skipping whatever .. was * [CB-10394](https://issues.apache.org/jira/browse/CB-10394) updated pinned **Android** version to `~5.1.0` * [CB-10299](https://issues.apache.org/jira/browse/CB-10299) updated pinned **windows** version to `~4.3.0` * [CB-10274](https://issues.apache.org/jira/browse/CB-10274) Make www directory the default for plugman * [CB-10121](https://issues.apache.org/jira/browse/CB-10121) added deprecation notice for **amazon-fireos** and **wp8** * [CB-7183](https://issues.apache.org/jira/browse/CB-7183) prevent read/write/modify files outside project from plugins * [CB-8455](https://issues.apache.org/jira/browse/CB-8455) Added `--nohooks` option. * [CB-10193](https://issues.apache.org/jira/browse/CB-10193) Add deprecation notice about `pre_package` removal * [CB-10147](https://issues.apache.org/jira/browse/CB-10147) updated pinned **iOS** to `~4.0.0` * [CB-10125](https://issues.apache.org/jira/browse/CB-10125): Android build fails on read-only files. * [CB-6698](https://issues.apache.org/jira/browse/CB-6698) Fix directory resolution of framework with parent. * [CB-9653](https://issues.apache.org/jira/browse/CB-9653) Adds copying of **blackberry10** splashscreens * **Ubuntu** support for the new plugin naming convention * [CB-9957](https://issues.apache.org/jira/browse/CB-9957) removed support for fetching from Cordova Plugins Registry. Only fetch plugins from **npm** now. * [CB-10108](https://issues.apache.org/jira/browse/CB-10108) Fixes **android** frameworks installation/removal * [CB-9964](https://issues.apache.org/jira/browse/CB-9964) Added `--template` support to `cordova create` * Removing the `--usegit` flag from `cordova platform`. Recommended method is to use `cordova platform add git_url#branch` * [CB-10081](https://issues.apache.org/jira/browse/CB-10081) pinned plugin versions. These are default versions fetched when adding a plugin. * add missing `package_suffix` function on **amazon-fireos** platform for plugman installations. * [CB-10057](https://issues.apache.org/jira/browse/CB-10057) - removing `` tag does not remove `ATS` entry * [CB-10048](https://issues.apache.org/jira/browse/CB-10048) clobbering of `` tags to `ATS` directives ### 5.4.1 (Nov 19, 2015) * [CB-9976](https://issues.apache.org/jira/browse/CB-9976) Reinstall plugins for platform if they were installed with `cordova@<5.4.0`. * [CB-9981](https://issues.apache.org/jira/browse/CB-9981) `path.parse` only available on `node 0.12+`. * [CB-9987](https://issues.apache.org/jira/browse/CB-9987) Adds compatibility layer for `cordova.raw.*` methods * [CB-9975](https://issues.apache.org/jira/browse/CB-9975) Fix issue with using `all" as orientation for **iOS** * [CB-9984](https://issues.apache.org/jira/browse/CB-9984) Bumps `plist` version and fixes failing `cordova-common` test ### 5.4.0 (Oct 30, 2015) * [CB-9935](https://issues.apache.org/jira/browse/CB-9935) Cordova CLI silently fails on node.js v5 * [CB-9834](https://issues.apache.org/jira/browse/CB-9834) Introduce compat map for hook requires * [CB-9902](https://issues.apache.org/jira/browse/CB-9902) Fix broken `cordova run --list` * [CB-9872](https://issues.apache.org/jira/browse/CB-9872) Fixed save.spec.11 failure * [CB-9800](https://issues.apache.org/jira/browse/CB-9800) Fixing contribute link. * [CB-9736](https://issues.apache.org/jira/browse/CB-9736) Extra main activity generated when an android package name is specified * [CB-9675](https://issues.apache.org/jira/browse/CB-9675) OSX App Icons are not properly copied. * [CB-9758](https://issues.apache.org/jira/browse/CB-9758) Mobilespec crashes adding plugins on OS X * [CB-9782](https://issues.apache.org/jira/browse/CB-9782) Update create/update signatures for PlatformApi polyfill * [CB-9815](https://issues.apache.org/jira/browse/CB-9815) Engine name="cordova" should check tools version, not platforms. * [CB-9824](https://issues.apache.org/jira/browse/CB-9824) removed plugin download counter code from lib * [CB-9821](https://issues.apache.org/jira/browse/CB-9821) Fix EventEmitter incorrect trace level usages * [CB-9813](https://issues.apache.org/jira/browse/CB-9813) Keep module-to-plugin mapping at hand. * [CB-9598](https://issues.apache.org/jira/browse/CB-9598) Fixes broken `require` for FFOS plugin handler * Update 'serve' to use 'express' implementation of cordova-serve. * [CB-9712](https://issues.apache.org/jira/browse/CB-9712) CLI 5.3 breaks with node 3.3.3 * [CB-9598](https://issues.apache.org/jira/browse/CB-9598) Fixies broken require calls that aren't covered by tests * [CB-9589](https://issues.apache.org/jira/browse/CB-9589) added more warnings and added conversion step to fetch.js * [CB-9589](https://issues.apache.org/jira/browse/CB-9589) auto convert old plugin ids to new npm ids using [registry-mapper](https://github.com/stevengill/cordova-registry-mapper) * Pick ConfigParser changes from apache@0c3614e * [CB-9743](https://issues.apache.org/jira/browse/CB-9743) Removes system frameworks handling from ConfigChanges * [CB-9598](https://issues.apache.org/jira/browse/CB-9598) Cleans out code which has been moved to `cordova-common` * [CB-9598](https://issues.apache.org/jira/browse/CB-9598) Switches LIB to use `cordova-common` * [CB-9569](https://issues.apache.org/jira/browse/CB-9569) Support and tag translation to Application Transport Security (ATS) Info.plist directives. * [CB-9737](https://issues.apache.org/jira/browse/CB-9737) (save flag) unit test failures for spec.14 * [CB-8914](https://issues.apache.org/jira/browse/CB-8914) when project is renamed, remove userdata otherwise project is un-usable in xcode * [CB-9665](https://issues.apache.org/jira/browse/CB-9665) Support .xcassets for icons and splashscreens in the CLI * [CB-9407](https://issues.apache.org/jira/browse/CB-9407) Fixes incorrect applying of plugin-provided config changes. * [CB-8198](https://issues.apache.org/jira/browse/CB-8198) Unified console output logic for core platforms * [CB-9408](https://issues.apache.org/jira/browse/CB-9408) Added support for `windows-packageVersion` on `` * [CB-9588](https://issues.apache.org/jira/browse/CB-9588) Plugman. Add support for on Windows * [CB-8615](https://issues.apache.org/jira/browse/CB-8615) Improves plugman tests for Windows * [CB-8615](https://issues.apache.org/jira/browse/CB-8615) **Windows** .winmd files with the same names are not added properly when using framework tag with target attribute * [CB-9297](https://issues.apache.org/jira/browse/CB-9297) Parse xcode project syncronously to avoid issues with node v4 * [CB-9617](https://issues.apache.org/jira/browse/CB-9617) Do not restore plugins after plugin removal. * [CB-9631](https://issues.apache.org/jira/browse/CB-9631) Save plugin to config.xml only if installation succeeds * [CB-9601](https://issues.apache.org/jira/browse/CB-9601) Fix .versions support on Windows after semver update * [CB-9617](https://issues.apache.org/jira/browse/CB-9617) Fixes incorrect project state after adding/removing plugins * [CB-9560](https://issues.apache.org/jira/browse/CB-9560) Issue using plugin restore for plugins with common dependencies * [CB-8993](https://issues.apache.org/jira/browse/CB-8993) Plugin restore ignores search path * [CB-9587](https://issues.apache.org/jira/browse/CB-9587) Check if browser platform added properly before creating parser. * [CB-9604](https://issues.apache.org/jira/browse/CB-9604) Fix error adding browser platform with PlatformApi polyfill. * [CB-9597](https://issues.apache.org/jira/browse/CB-9597) Initial Implementation of PlatformApiPoly * [CB-9354](https://issues.apache.org/jira/browse/CB-9354) Fix array merging with complex items * [CB-9556](https://issues.apache.org/jira/browse/CB-9556) Don't uninstall dependent plugin if it was installed as a top-level after ### 5.3.2 (Sep 17, 2015) * [CB-9297](https://issues.apache.org/jira/browse/CB-9297) Parse xcode project syncronously to avoid issues with node v4 ### 5.3.1 (Aug 28, 2015) * pinned blackberry@3.8.0 in prepartion for its release * pinned browser@4.0.0 and windows@4.1.0 * [CB-9559](https://issues.apache.org/jira/browse/CB-9559) Adding a plugin with caret in version results in an error * Update cordova-serve required version to 0.1.3. * [CB-6506](https://issues.apache.org/jira/browse/CB-6506) RTC: Add support for OSX (closes #278) * [CB-9517](https://issues.apache.org/jira/browse/CB-9517) Adding a plugin on iOS/OSX that uses a private framework does not work (closes #281) * [CB-9549](https://issues.apache.org/jira/browse/CB-9549) Removes excess JS files from browserified app * [CB-9505](https://issues.apache.org/jira/browse/CB-9505) Correct plugin modules loading within browserify flow * [CB-8532](https://issues.apache.org/jira/browse/CB-8532) Adding Windows Plugin Failed with "Cannot read property 'text' of null" Updated elementtree API according 0.1.6 release. This closes #277 ### 5.2.0 (Aug 06, 2015) * [CB-9436](https://issues.apache.org/jira/browse/CB-9436) Removes `require-tr` bundle transformation * updated pinned ios version to ~3.9.0 * [CB-9278](https://issues.apache.org/jira/browse/CB-9278): Restoring multiple platforms fails. This closes #266 * updated pinned android to ~4.1.0 * [CB-9421](https://issues.apache.org/jira/browse/CB-9421) Added a test for plugin fetch with searchpath parameter * [CB-9421](https://issues.apache.org/jira/browse/CB-9421) Fixed searchpath parameter being ignored. This closes #269 * Update xcode dependency to latest stable version. This closes #272 * [CB-9420](https://issues.apache.org/jira/browse/CB-9420) Fixes malformed require calls in browserify bundle. This closes #270 * [CB-9405](https://issues.apache.org/jira/browse/CB-9405) limit author/description to 256 char per WMAppManifest schema * [CB-9414](https://issues.apache.org/jira/browse/CB-9414) plugin fetching now defaults to npm, CPR fallback * [CB-9384](https://issues.apache.org/jira/browse/CB-9384) Added tests that test plugin fetch from github branch|tag|sha * added comment outlining the types of things git_ref can be : commit SHA | branch | tag * actually checkout git_ref because it may be a branch OR a commit SHA * [CB-9332](https://issues.apache.org/jira/browse/CB-9332) Upgrade npm and semver to actual versions * [CB-9330](https://issues.apache.org/jira/browse/CB-9330) updated wording for warning messages for removal of publish/unpublish commands * Adds stubs for `publish`/`unpublish` commands. This closes #254 * [CB-9330](https://issues.apache.org/jira/browse/CB-9330) Removes 'plugman publish' related functionality * [CB-9335](https://issues.apache.org/jira/browse/CB-9335): Windows quality-of-life improvements. To align with the change in Cordova-Windows which removes the Windows 8 project from the solution file used by Windows 8.1 and Windows 10, the same is done in the spec. * Fix prepare to wait the promise from plugman prepare. * [CB-9362](https://issues.apache.org/jira/browse/CB-9362) Don't fail if superspawn can't chmod a file * [CB-9122](https://issues.apache.org/jira/browse/CB-9122) Added tests for platform/plugin add/rm/update with --save flag. This closes #246 * Fixed ios node-xcode related tests failing on Windows according to version update * Added webOS parsers for project creation/manipulation * [CB-8965](https://issues.apache.org/jira/browse/CB-8965) Prevent cli from copying cordova.js and cordova-js-src/ multiple times * [CB-9114](https://issues.apache.org/jira/browse/CB-9114): Log deprecation message when --usegit flag is used. This closes #234 * [CB-9126](https://issues.apache.org/jira/browse/CB-9126) Fix ios pbxproj' resources paths when adding ios platform on non-OSX environment. This closes #237 * [CB-9221](https://issues.apache.org/jira/browse/CB-9221) Updates `cordova serve` command to use cordova-serve module. * [CB-9225](https://issues.apache.org/jira/browse/CB-9225) Add windows platform support to `plugman platform add` * [CB-9163](https://issues.apache.org/jira/browse/CB-9163) when engine check isn't satisfied, skip that plugin install * [CB-9162](https://issues.apache.org/jira/browse/CB-9162) Adds support for default values for plugin variables. * [CB-9188](https://issues.apache.org/jira/browse/CB-9188) Confusing error after delete plugin folder then prepare. * [CB-9145](https://issues.apache.org/jira/browse/CB-9145) prepare can lose data during config munge * [CB-9177](https://issues.apache.org/jira/browse/CB-9177) Use tilde instead of caret when save to config.xml. * [CB-9147](https://issues.apache.org/jira/browse/CB-9147) Adding a platform via caret version adds latest rather than the latest matching. * [CB-5578](https://issues.apache.org/jira/browse/CB-5578) Adds `clean` module to cordova. This closes #241 * [CB-9124](https://issues.apache.org/jira/browse/CB-9124) Makes network-related errors' messages more descriptive. * [CB-9067](https://issues.apache.org/jira/browse/CB-9067) fixed plugman config set registry and adduser * [CB-8993](https://issues.apache.org/jira/browse/CB-8993) Plugin restore ignores search path. This closes #224 * [CB-9087](https://issues.apache.org/jira/browse/CB-9087) updated pinned windows platform to 4.0.0 * [CB-9108](https://issues.apache.org/jira/browse/CB-9108) Handle version ranges when add platform with --usegit. * [CB-8898](https://issues.apache.org/jira/browse/CB-8898) Makes error message descriptive when `requirements` is called outside of cordova project. * [CB-8007](https://issues.apache.org/jira/browse/CB-8007) Two cordova plugins modifying “*-Info.plist” CFBundleURLTypes * [CB-9065](https://issues.apache.org/jira/browse/CB-9065) Allow removing plugins by short name. * [CB-9001](https://issues.apache.org/jira/browse/CB-9001) Set WMAppManifest.xml Author, Description and Publisher attributes based on config.xml * [CB-9073](https://issues.apache.org/jira/browse/CB-9073) Allow to add platform if project path contains `&` symbol ### 5.1.1 (June 4, 2015) * [CB-9087](https://issues.apache.org/jira/browse/CB-9087) Updated pinned version of cordova-windows to 4.0.0 * [CB-9108](https://issues.apache.org/jira/browse/CB-9108) Handle version ranges when add platform with --usegit. * [CB-8898](https://issues.apache.org/jira/browse/CB-8898) Makes error message descriptive when `requirements` is called outside of cordova project. * Fix four failing tests on Windows. * [CB-8007](https://issues.apache.org/jira/browse/CB-8007) Two cordova plugins modifying “*-Info.plist” CFBundleURLTypes * [CB-9065](https://issues.apache.org/jira/browse/CB-9065) Allow removing plugins by short name. * [CB-9001](https://issues.apache.org/jira/browse/CB-9001) Set WMAppManifest.xml Author, Description and Publisher attributes based on config.xml * [CB-9073](https://issues.apache.org/jira/browse/CB-9073) Allow to add platform if project path contains `&` symbol * [CB-8783](https://issues.apache.org/jira/browse/CB-8783) - Revert 'all' as a global preference value for Orientation (specific to iOS for now) * [CB-8783](https://issues.apache.org/jira/browse/CB-8783) - 'default' value for Orientation does not support both landscape and portrait orientations. (new 'all' value) * [CB-9075](https://issues.apache.org/jira/browse/CB-9075) pinned platforms will include patch updates without new tools release * [CB-9051](https://issues.apache.org/jira/browse/CB-9051) Plugins don't get re-added if platforms folder deleted. * [CB-9025](https://issues.apache.org/jira/browse/CB-9025) Call windows `prepare` logic on as part of cordova-lib `prepare`. This closes #217 * [CB-9048](https://issues.apache.org/jira/browse/CB-9048) Clean up git cloned directories (close #222) * [CB-8965](https://issues.apache.org/jira/browse/CB-8965) readded browserify transform * [CB-8965](https://issues.apache.org/jira/browse/CB-8965) copy platform specific js into platform_www when adding new platforms for browserify workflow * [CB-8965](https://issues.apache.org/jira/browse/CB-8965) passing platform as argument when getting symbolList * [CB-8965](https://issues.apache.org/jira/browse/CB-8965) copy platform specific js into platform_www when adding new platforms for browserify workflow * Add support to specify a build config file. If none is specified `build.json` in the project root is used as a default This closes #215 * [CB-9030](https://issues.apache.org/jira/browse/CB-9030): Modifies superspawn to support a "chmod" option. When truthy, attempts to set the target file mode to 755 before executing. Specifies this argument as truthy for common CLI operations (compile, run, and steps in plugman). Didn't add it for hooks runner since that particular mode is in legacy support. * [CB-8989](https://issues.apache.org/jira/browse/CB-8989) - cordova-lib jasmine tests are failing on older hardware * [CB-6462](https://issues.apache.org/jira/browse/CB-6462) [CB-6026](https://issues.apache.org/jira/browse/CB-6026) - Orientation preference now updates `UISupportedInterfaceOrientations~ipad` too. * [CB-8898](https://issues.apache.org/jira/browse/CB-8898) Introduces `requirements` cordova module * Update elementtree dependency to 0.1.6. Note it has a breaking API change. https://github.com/racker/node-elementtree/issues/24 (closes #209) * [CB-8757](https://issues.apache.org/jira/browse/CB-8757) Resolve symlinks in order to avoid relative path issues (close #212) * [CB-8956](https://issues.apache.org/jira/browse/CB-8956) Remove hardcoded reference to registry.npmjs.org * [CB-8934](https://issues.apache.org/jira/browse/CB-8934) fixed regression with projects config.json not being used in cordova create * [CB-8908](https://issues.apache.org/jira/browse/CB-8908) Make fetching via git faster via --depth=1 * [CB-8897](https://issues.apache.org/jira/browse/CB-8897) Make default icon/splash on Android map to mdpi ### 5.0.0 (Apr 16, 2015) * [CB-8865](https://issues.apache.org/jira/browse/CB-8865) fixed plugman.help() * Pinned Cordova-Android version 4.0.0 * [CB-8775](https://issues.apache.org/jira/browse/CB-8775) updated warning message to be more descriptive * Fix getPlatformVersion fails for paths with spaces * [CB-8799](https://issues.apache.org/jira/browse/CB-8799) Save plugin/platform src and version to 'spec' attribute. * [CB-8807](https://issues.apache.org/jira/browse/CB-8807) Platform Add fails to add plugins with variables. * [CB-8832](https://issues.apache.org/jira/browse/CB-8832) Fix iOS icon copying logic to not use default for every size * Updated pinned versions of windows and wp8 * [CB-8775](https://issues.apache.org/jira/browse/CB-8775) adding a plugin will still copy it to plugins folder, except if the plugin's new or old id is already installed. * [CB-8775](https://issues.apache.org/jira/browse/CB-8775) removed failing test * Fix setGlobalPreference() in ConfigParser * removed mostly unused relativePath checking and added missing cases for isAbsolutePath * use string method for clarity * [CB-8775](https://issues.apache.org/jira/browse/CB-8775) new style plugins won't install if same RDS plugin is installed and vice versa * [CB-8791](https://issues.apache.org/jira/browse/CB-8791) Recognize UAP as a valid TargetPlatformIdentifier * [CB-8784](https://issues.apache.org/jira/browse/CB-8784) Prepare with no platforms should restore all platforms. * Fix plugman install failure on iOS containing & * [CB-8703](https://issues.apache.org/jira/browse/CB-8703): Test failure after merge to head. * [CB-8703](https://issues.apache.org/jira/browse/CB-8703): Add support for semver and device-specific targeting of config-file to Windows * [CB-8596](https://issues.apache.org/jira/browse/CB-8596) Expose APIs to retrieve platforms and plugins saved in config.xml. * [CB-8741](https://issues.apache.org/jira/browse/CB-8741) Make plugin --save work more like npm install * [CB-8755](https://issues.apache.org/jira/browse/CB-8755) Plugin --save: Multiple config.xml entries don't get removed * [CB-8754](https://issues.apache.org/jira/browse/CB-8754) Auto-restoring a plugin fails when adding a platform. * [CB-8651](https://issues.apache.org/jira/browse/CB-8651) Restoring platforms causes plugin install to be triggered twice (close #196) * [CB-8731](https://issues.apache.org/jira/browse/CB-8731) updated app hello world dependency to 3.9.0 * [CB-8757](https://issues.apache.org/jira/browse/CB-8757) ios: Make paths with --link relative to the real project path (close #192) * [CB-8286](https://issues.apache.org/jira/browse/CB-8286) Fix regression from e70432f2: Never want to link to app-hello-world * [CB-8737](https://issues.apache.org/jira/browse/CB-8737) Available platforms list includes extraneous values * Bugfix to json.parse before using cfg * Add merges/ by default, now all tests pass * Move cordova-app-hello-world dependency to cordova-lib * Support the old 4-argument version of create again * [CB-8286](https://issues.apache.org/jira/browse/CB-8286) Update create.js to always require passing in a www * Show npm failure message when plugin fetch fails * [CB-8725](https://issues.apache.org/jira/browse/CB-8725) Fix plugin add from npm when authenticated to CPR * [CB-8499](https://issues.apache.org/jira/browse/CB-8499) Remove project_dir from (un)installers signature * Add addElement() to ConfigParser * [CB-8696](https://issues.apache.org/jira/browse/CB-8696) Fix fetching of dependencies with semver constraints rather than exact versions * [CB-7747](https://issues.apache.org/jira/browse/CB-7747) Add `` for App Store on iOS * Export PlatformProjectAdapter from platforms.js * Allow subdirs for icons on BB10 * [CB-8670](https://issues.apache.org/jira/browse/CB-8670) Error when set engine name to "cordova-windows" in plugin.xml * Allow hyphen in platform name * [CB-8521](https://issues.apache.org/jira/browse/CB-8521) Cleans up plugin metadata save method * [CB-8521](https://issues.apache.org/jira/browse/CB-8521) Adds `cordova plugin save` which saves all installed plugins to config.xml * [CB-7698](https://issues.apache.org/jira/browse/CB-7698) BugFix: For plugins which require variables, 'cordova plugin add FOO' should fail when no variables specified. * Add setGlobalPreference() to ConfigParser * [CB-8499](https://issues.apache.org/jira/browse/CB-8499) Merge platforms.js from cordova and plugman * rename references to feature to plugin * Deprecate the old feature syntax from config.xml * [CB-8634](https://issues.apache.org/jira/browse/CB-8634) Fixes missed merge/rebase issue * [CB-8634](https://issues.apache.org/jira/browse/CB-8634) Adds support for custom branches for `cordova platform add` * [CB-8633](https://issues.apache.org/jira/browse/CB-8633) BugFix: Support for urls to tarballs was broken * [CB-8499](https://issues.apache.org/jira/browse/CB-8499) `cordova platform save`: save installed platforms and their sources (versions/git_urls/folders) into config.xml * [CB-8499](https://issues.apache.org/jira/browse/CB-8499) When deleting a platform, remove it from platforms.json * [CB-8499](https://issues.apache.org/jira/browse/CB-8499) When adding a platform, capture version/folder/url being added to allow us to be able to save all installed platforms and their versions later on by doing 'cordova platform save' * [CB-8602](https://issues.apache.org/jira/browse/CB-8602) plugman: publish fail early if unsupported npm is active * [CB-7747](https://issues.apache.org/jira/browse/CB-7747) Add ``s to default template * [CB-8616](https://issues.apache.org/jira/browse/CB-8616) Support 9-patch images for default android splashscreen * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) fixed regex in isValidCprName * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) updated version of registry mapper and cordova plugin rm code * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) merged fetchNPM and fetchPlugReg into fetchPlugin * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) updated regex in isValidCprName to exclude matching @version * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) split up changePluginId into two functions * [CB-8457](https://issues.apache.org/jira/browse/CB-8457) Ignore version specifier when running hooks (close #165) * [CB-8578](https://issues.apache.org/jira/browse/CB-8578) `cordova plugin add ` should be able to restore urls and folders in addition to versions. (close #173) * [CB-7827](https://issues.apache.org/jira/browse/CB-7827) Add support for `android-activityName` within `config.xml` (close #171) * Add org.apache.cordova.test-framework to plugman publish whitelist * [CB-8577](https://issues.apache.org/jira/browse/CB-8577) - Read plugin variables from correct tag * [CB-8555](https://issues.apache.org/jira/browse/CB-8555) Incremented package version to -dev * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) added plugin-name support for removing plugins. * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) Skip CPR if pluginID isn't reverse domain name style * [CB-8551](https://issues.apache.org/jira/browse/CB-8551) added npm fetching as fallback ### 4.3.0 (Feb 27, 2015) * updated pinned versions of ios to 3.8.0 and android to 3.7.1 * [CB-8524](https://issues.apache.org/jira/browse/CB-8524) Switched to the latest Windows release * changed createpackage.json keyword to ecosystem:cordova * [CB-8448](https://issues.apache.org/jira/browse/CB-8448) add support for activities * [CB-8482](https://issues.apache.org/jira/browse/CB-8482) rename: platformId -> platformName * [CB-8482](https://issues.apache.org/jira/browse/CB-8482): Update engine syntax within config.xml * Organize save logic some more * --save flag for plugins * fix for test after prepare changes * restore plugins and platforms on prepare * [CB-8472](https://issues.apache.org/jira/browse/CB-8472) Can't find config.xml error installing browser platform after plugin. (close #167) * [CB-8469](https://issues.apache.org/jira/browse/CB-8469) android: Call into platform's build.js after `plugin add` so that Android Studio will work without needing an explicit command-line build first * [CB-8123](https://issues.apache.org/jira/browse/CB-8123) Fix JSHINT issue. * [CB-8123](https://issues.apache.org/jira/browse/CB-8123) Fix path handling so tests work on any platform. * [CB-8123](https://issues.apache.org/jira/browse/CB-8123) Rename further windows platform related files. * [CB-8123](https://issues.apache.org/jira/browse/CB-8123) Rename windows platform related files. * [CB-8123](https://issues.apache.org/jira/browse/CB-8123) Plugin references can target specific windows platforms. * [CB-8420](https://issues.apache.org/jira/browse/CB-8420) Make `cordova plugin add FOO` use version from config.xml (close #162) * [CB-8239](https://issues.apache.org/jira/browse/CB-8239) Fix `cordova platform add PATH` when PATH is relative and CWD != project root * [CB-8227](https://issues.apache.org/jira/browse/CB-8227) CB8237 [CB-8238](https://issues.apache.org/jira/browse/CB-8238) Add --save flag and autosave to 'cordova platform add', 'cordova platform remove' and 'cordova platform update' * [CB-8409](https://issues.apache.org/jira/browse/CB-8409) compile: bubble failures * [CB-8239](https://issues.apache.org/jira/browse/CB-8239) Fix "platform update" should ignore `` (close #159) * [CB-8390](https://issues.apache.org/jira/browse/CB-8390) android: Make `` work with Gradle * [CB-8416](https://issues.apache.org/jira/browse/CB-8416) updated plugman publish to temporarily rename existing package.json files * [CB-8416](https://issues.apache.org/jira/browse/CB-8416): added `plugman createpackagejson .` command to create a package.json from plugin.xml * [CB-6973](https://issues.apache.org/jira/browse/CB-6973) add spec-plugman to npm run jshint * [CB-6973](https://issues.apache.org/jira/browse/CB-6973) fix spec-plugman jshint failures * [CB-6973](https://issues.apache.org/jira/browse/CB-6973) have base rules in jshintrc for spec-plugman * [CB-8377](https://issues.apache.org/jira/browse/CB-8377) Fixed tag parsing (close #156) * [CB-5696](https://issues.apache.org/jira/browse/CB-5696) find ios project directory using the xcode project file (close #151) * [CB-8373](https://issues.apache.org/jira/browse/CB-8373) android: Add gradle references to project.properties rather than build.gradle * [CB-8370](https://issues.apache.org/jira/browse/CB-8370) Make "plugman publish" without args default to CWD * Fix publish type-error introduced in recent commit 15adc1b9fcc069438f5 * [CB-8366](https://issues.apache.org/jira/browse/CB-8366) android: Remove empty `` directory upon uninstall * [CB-6973](https://issues.apache.org/jira/browse/CB-6973) Enable JSHint for spec-cordova * [CB-8239](https://issues.apache.org/jira/browse/CB-8239) Add support for git urls to 'cordova platform add' (close #148) * [CB-8358](https://issues.apache.org/jira/browse/CB-8358) Add `--link` for `platform add` and `platform update` * [CB-6973](https://issues.apache.org/jira/browse/CB-6973) remove base rules from individual files in src * [CB-6973](https://issues.apache.org/jira/browse/CB-6973) have base rules in .jshintrc file * Add shims to undo breaking change in a20b3ae3 (didn't realize PluginInfo was exported) * [CB-8354](https://issues.apache.org/jira/browse/CB-8354) Add --link support for iOS source and header files * Make all ad-hoc plugin.xml parsing use PluginInfo instead * Make all usages of PluginInfo use PluginInfoProvider instead * Add PluginInfoProvider for better caching of PluginInfo * [CB-8284](https://issues.apache.org/jira/browse/CB-8284) revert npm dependency due to issues with registry * [CB-8223](https://issues.apache.org/jira/browse/CB-8223) Expose config.xml in the Browser platform (close #149) * [CB-8168](https://issues.apache.org/jira/browse/CB-8168) --list support for cordova-lib (close #145) * [Amazon] Improve error message when `` is missing `target-dir` * refactor: Make addUninstalledPluginToPrepareQueue take pluginId rather than dirName * Chnage plugman test plugins to have IDs as directory names * Make all test plugin IDs unique * Empty out contents of plugin test files (and delete some unused ones) * [CB-4789](https://issues.apache.org/jira/browse/CB-4789) refactor: Remove config_changes.get/set_platform_json in favour of PlatformJson * [CB-8319](https://issues.apache.org/jira/browse/CB-8319) Remove config_changes module from plugman's public API * [CB-8314](https://issues.apache.org/jira/browse/CB-8314) Speed up Travis CI (close #150) * refactor: Extract PlatformJson and munge-util into separate modules * refactor: Move ConfigFile and ConfigKeeper into their own files * [CB-8285](https://issues.apache.org/jira/browse/CB-8285) Fix regression caused by c49eaa86c92b (PluginInfo's are cached, don't change them) * [CB-8208](https://issues.apache.org/jira/browse/CB-8208) Made CI systems to get cordova-js dependency from gihub (close #146) * [CB-8285](https://issues.apache.org/jira/browse/CB-8285) Don't create .fetch.json files within plugin directories * [CB-8286](https://issues.apache.org/jira/browse/CB-8286) Never persist value of create --link-to within .cordova/config.json * [CB-8288](https://issues.apache.org/jira/browse/CB-8288) Don't set config.setAutoPersist() in cordova.create * Fix create spec sometimes failing because it's deleted its own tmp directory * [CB-8153](https://issues.apache.org/jira/browse/CB-8153) generate cordova_plugins.json for browserify based projects * [CB-8043](https://issues.apache.org/jira/browse/CB-8043) [CB-6462](https://issues.apache.org/jira/browse/CB-6462) [CB-6105](https://issues.apache.org/jira/browse/CB-6105) Refactor orientation preference support (close #128) * FirefoxOS parser: allow passing in a ConfigParser object * Parsers: extend base parser with helper functions * [CB-8244](https://issues.apache.org/jira/browse/CB-8244) android: Have `plugin add --link` create symlinks for ``, ``, etc * [CB-8244](https://issues.apache.org/jira/browse/CB-8244) Pass options object to platform handlers in plugman (commit attempt #2) * [CB-8226](https://issues.apache.org/jira/browse/CB-8226) 'cordova platform add' : Look up version in config.xml if no version specified * Delete root .npmignore, since there's no node module there ### 4.2.0 (Jan 06, 2015) * `ConfigParser`: refactor `getPreference()` * Parsers: add base parser (parser.js) and make platform parsers inherit from it * Parsers: assign methods without overriding the prototype * [CB-8225](https://issues.apache.org/jira/browse/CB-8225) Add Unit Tests for `platform.js/add` function (closes #138) * [CB-8230](https://issues.apache.org/jira/browse/CB-8230) Make `project.properties` optional for Android sub-libraries * [CB-8215](https://issues.apache.org/jira/browse/CB-8215) Improve error message when `` is missing `target-dir` on android * [CB-8217](https://issues.apache.org/jira/browse/CB-8217) Fix plugin add --link when plugin given as relative path * [CB-8216](https://issues.apache.org/jira/browse/CB-8216) Resolve plugin paths relative to original CWD * [CB-7311](https://issues.apache.org/jira/browse/CB-7311) Fix tests on windows for iOS parser * [CB-7803](https://issues.apache.org/jira/browse/CB-7803) Allow adding any platform on any host OS (close #126) * [CB-8155](https://issues.apache.org/jira/browse/CB-8155) Do not fail plugin installation from git url with --link (close #129) * Updates README with description of npm commands for this package * [CB-8129](https://issues.apache.org/jira/browse/CB-8129) Adds 'npm run cover' command to generate tests coverage report (close #131) * [CB-8114](https://issues.apache.org/jira/browse/CB-8114) Specify a cache-min-time for plugins (closes #133) * [CB-8190](https://issues.apache.org/jira/browse/CB-8190) Make plugman config/cache directory to be customizable via PLUGMAN_HOME (close #134) * [CB-7863](https://issues.apache.org/jira/browse/CB-7863) Fixed broken test run on Windows 8.1 caused by incorrect use of promises (close #132, close #112) * [CB-7610](https://issues.apache.org/jira/browse/CB-7610) Fix `cordova plugin add d:\path` (or any other non-c: path) (close #135) * [CB-8179](https://issues.apache.org/jira/browse/CB-8179) Corrected latest wp8 version * [CB-8158](https://issues.apache.org/jira/browse/CB-8158) added hasModule check to browserify code * [CB-8173](https://issues.apache.org/jira/browse/CB-8173) Point to the latest ubuntu version * [CB-8179](https://issues.apache.org/jira/browse/CB-8179) Point to the latest wp8 version * [CB-8158](https://issues.apache.org/jira/browse/CB-8158) adding symbolList to cordova.js * [CB-8154](https://issues.apache.org/jira/browse/CB-8154) Fix errors adding platforms or plugins * browserify: updated require to use symbollist * Amazon related changes. Added a type named "gradleReference" in framework according to https://git-wip-us.apache.org/repos/asf?p=cordova-lib.git;a=commit;h=02a96d757acc604610eb403cf11f79513ead4ac5 * [CB-7736](https://issues.apache.org/jira/browse/CB-7736) Update npm dep to promote qs module to 1.0 * Added a missing "else" keyword. * [CB-8086](https://issues.apache.org/jira/browse/CB-8086) Fixed framework tests. * [CB-8086](https://issues.apache.org/jira/browse/CB-8086) Added an explanatory comment. * [CB-8086](https://issues.apache.org/jira/browse/CB-8086) Prefixed subprojects with package name. * [CB-8067](https://issues.apache.org/jira/browse/CB-8067) externalized valid-identifier it is it's own module * Added identifier checking for app id, searches for java+C# reserved words * [CB-6472](https://issues.apache.org/jira/browse/CB-6472) Adding content to -Info.plist - Unexpected behaviour * [CB-8053](https://issues.apache.org/jira/browse/CB-8053): Including a project reference in a plugin fails on Windows platform. * Pass the searchpath when installing plugins * Add a type named "gradleReference" in framework ### 4.1.2 (Nov 13, 2014) * [CB-7079](https://issues.apache.org/jira/browse/CB-7079) Allow special characters and digits in id when publishing to plugins registry * [CB-7988](https://issues.apache.org/jira/browse/CB-7988): Update platform versions for iOS, wp8 & Windows to 3.7.0 * [CB-7846](https://issues.apache.org/jira/browse/CB-7846) Fix plugin deletion when dependency plugin does not exist * [CB-6992](https://issues.apache.org/jira/browse/CB-6992) Fix build issue on iOS when app name contains accented characters * [CB-7890](https://issues.apache.org/jira/browse/CB-7890) validate file copy operations in plugman * [CB-7884](https://issues.apache.org/jira/browse/CB-7884) moved platform metadata to platformsConfig.json * Amazon Specific changes: Added support for SdkVersion * Expose PluginInfo from cordova-lib * [CB-7839](https://issues.apache.org/jira/browse/CB-7839) android: Fix versionCode logic when version is less than 3 digits * [CB-7033](https://issues.apache.org/jira/browse/CB-7033) Improve cordova platform check * [CB-7311](https://issues.apache.org/jira/browse/CB-7311) Fix xcode project manipulation on Windows host * [CB-7820](https://issues.apache.org/jira/browse/CB-7820) Make cordova platform restore not stop if a platforms fails to restore * [CB-7649](https://issues.apache.org/jira/browse/CB-7649) Support iPhone 6 Plus Icon in CLI config.xml * [CB-7647](https://issues.apache.org/jira/browse/CB-7647) Support new iPhone 6 and 6 Plus Images in the CLI config.xml * [CB-7909](https://issues.apache.org/jira/browse/CB-7909) "plugman platform add" fixes * Enable platform-specific id for android and ios * Check for a CORDOVA_HOME environment variable to create a global config path ### 4.0.0 (Oct 10, 2014) * Bumped version to 4.0.0 to be semVer complient and to match cli version * Pinned dependencies in package.json * updated platforms.js for 3.6.4 * [CB-5390](https://issues.apache.org/jira/browse/CB-5390) Uninstall - recursively remove dependencies of dependencies * fixes HooksRunner test - should run before_plugin_uninstall * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) getPluginsHookScripts to work if plugin platform not defined * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Context opts should copy not reference * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Fixed tests - removed output * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Fixed HooksRunner and tests Avoided issue with parallel tests running Added checks for handling mocked config.xml and package.json in HooksRunner and scriptsFinder Addressed jshint issues Renamed ScriptsFinder to scriptsFinder * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Addressed community review notes: Removed commonModules from Context Renamed Hooker and subclasses to HooksRunner and scriptsFinder Moved scriptsRunner code into HooksRunner * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Replaced CordovaError throwings with Error per @kamrik review Extracted prepareOptions Hooker method * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Docs: deprecated .cordova/hooks + other minor updates * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Updated hooks documentation * [CB-6481](https://issues.apache.org/jira/browse/CB-6481) Added unified hooks support for cordova app and plugins * [CB-7572](https://issues.apache.org/jira/browse/CB-7572) Serve - respond with 304 when resource not modified * computeCommitId for browserify workflow fixed to handle cli and non cli workflows:q * [CB-7219](https://issues.apache.org/jira/browse/CB-7219) prepare-browserify now supports commitId and platformVersion for cordovajs * [CB-7219](https://issues.apache.org/jira/browse/CB-7219): initial work for cordova.js platformVersion * [CB-7219](https://issues.apache.org/jira/browse/CB-7219) prepare-browserify now supports commitId and platformVersion for cordovajs * [CB-7219](https://issues.apache.org/jira/browse/CB-7219): initial work for cordova.js platformVersion * [CB-7383](https://issues.apache.org/jira/browse/CB-7383) Updated version and RELEASENOTES.md for release 0.21.13 * Fix [CB-7615](https://issues.apache.org/jira/browse/CB-7615) Read config.xml after pre-prepare hooks fire * [CB-7578](https://issues.apache.org/jira/browse/CB-7578) Windows. Fix platform name reported by pre_package hook * [CB-7576](https://issues.apache.org/jira/browse/CB-7576) Support 'windows' merges folder for Windows platform * Revert "Merge branch 'browserPlatform' of https://github.com/surajpindoria/cordova-lib" * Added tests for browser platform ### 0.21.13 * remove shrinkwrap ### 0.21.12 * [CB-7383](https://issues.apache.org/jira/browse/CB-7383): depend on a newer version of cordova-js, bump self version ### 0.21.11 * bump version numbers of platforms to 3.6.3 ### 0.21.10 (Sep 05, 2014) * [CB-7457](https://issues.apache.org/jira/browse/CB-7457) - cordova plugin add --searchpath does not recurse through subfolders when a plugin.xml is malformed in one of them * [CB-7457](https://issues.apache.org/jira/browse/CB-7457) - Add malformed plugin for tests * [Windows8] Fix failing test to match updated functionality * [CB-7420](https://issues.apache.org/jira/browse/CB-7420) Windows. Plugin s are removed from platform during prepare * Windows helper. Removes unnecessary $(MSBuildThisFileDirectory) * updated Releasenotes.md * updated version to 0.21.10-dev * [CB-7457](https://issues.apache.org/jira/browse/CB-7457) - cordova plugin add --searchpath does not recurse through subfolders when a plugin.xml is malformed in one of them * [CB-7457](https://issues.apache.org/jira/browse/CB-7457) - Add malformed plugin for tests * [Windows8] Fix failing test to match updated functionality * updated Releasenotes.md * updated version to 0.21.10-dev * updated version, updated ffos to use 3.6.1, updated cordova-js dependency to be strcit * [CB-7383](https://issues.apache.org/jira/browse/CB-7383) Incremented package version to -dev * updated platforms.js to use 3.6.0 * Updated version and RELEASENOTES.md for release 0.21.8 * [CB-5535](https://issues.apache.org/jira/browse/CB-5535): Remove "--arc" from ios platform creation args * Windows helper. Removes unnecessary $(MSBuildThisFileDirectory) * [CB-7420](https://issues.apache.org/jira/browse/CB-7420) Windows. Plugin s are removed from platform during prepare * [CB-7416](https://issues.apache.org/jira/browse/CB-7416) Fixes file path reference when adding new source file * [CB-7416](https://issues.apache.org/jira/browse/CB-7416) handleInstall tests for null platformTag. removed uncalled 'hasPlatformSection' from PluginInfo.js * Remove use of path.join for manifest.launch_path * [CB-7347](https://issues.apache.org/jira/browse/CB-7347) Improve cordova platform add /path/to handling * [CB-7118](https://issues.apache.org/jira/browse/CB-7118) (fix jshint warnings) * [CB-7114](https://issues.apache.org/jira/browse/CB-7114) Android: add support of min/max/target SDK to config.xml * [CB-7118](https://issues.apache.org/jira/browse/CB-7118) Use updated version of node-xcode * [CB-7118](https://issues.apache.org/jira/browse/CB-7118) iOS: add target-device and MinimumOSVersion support to config.xml * ubuntu: support incremental builds * ubuntu: support target-dir for resource-file * ubuntu: use common.copyFile * ubuntu: check icon existence * ffos: Make author url optional * [CB-7142](https://issues.apache.org/jira/browse/CB-7142) Add to for "plugin restore" command * Set git clone depth to 10 for Travis to make it faster * windows: update as per changed manifest file names * Don't spy and expect it to call the other spy ... * Well that looks like an error * Fixing failing tests: update_proj should be update_project * Fix failing tests. update_jsproj and update_csproj are now just update_proj * Fix jshint errors in amazon_fireos_parser : mixed single/double quotes * [CB-6699](https://issues.apache.org/jira/browse/CB-6699) Include files from www folder via single element (use ** glob pattern) * Taking care of dashes in amazon-fireos platform name. * Upleveled amazon-fireos changes. * Fix link/copy parent check for windows * Style fixes - comments * Fix error in comments for munge functions * Add link to BuildBot at ci.cordova.io in README * [CB-7255](https://issues.apache.org/jira/browse/CB-7255) Fixed writing plist unescaped * Allow plugin modules to be .json files * Style fixes - white space only * Add JSCS config file * [CB-7260](https://issues.apache.org/jira/browse/CB-7260) Get cordova-android 3.5.1 instead of 3.5.0 * [CB-7228](https://issues.apache.org/jira/browse/CB-7228): Fixed issue with "cordova prepare --browserify" * [CB-7234](https://issues.apache.org/jira/browse/CB-7234) added better outputs for plugin registry workflows * [CB-7100](https://issues.apache.org/jira/browse/CB-7100): Use npm based lazy-load by default * [CB-7091](https://issues.apache.org/jira/browse/CB-7091): Remove check_requirements() funcs from platform parsers * [CB-7091](https://issues.apache.org/jira/browse/CB-7091): Remove check_requirements() funcs from platform parsers * [CB-7140](https://issues.apache.org/jira/browse/CB-7140) Check plugin versions in local search path * [CB-7001](https://issues.apache.org/jira/browse/CB-7001): Create a --browserify option for run action * [CB-7228](https://issues.apache.org/jira/browse/CB-7228): Cordova prepare --browserify runs on all installed plugins * [CB-7190](https://issues.apache.org/jira/browse/CB-7190): Add browserify support in cordova-lib/cordova-cli * Remove references to "firefoxos" * Browser platform is now being created from cli * Created new files for browser ### 0.21.8 (Aug 29, 2014) * [CB-5535](https://issues.apache.org/jira/browse/CB-5535): Remove "--arc" from ios platform creation args * [CB-7416](https://issues.apache.org/jira/browse/CB-7416) Fixes file path reference when adding new source file * [CB-7416](https://issues.apache.org/jira/browse/CB-7416) handleInstall tests for null platformTag. removed uncalled 'hasPlatformSection' from PluginInfo.js * Remove use of path.join for manifest.launch_path * [CB-7347](https://issues.apache.org/jira/browse/CB-7347) Improve cordova platform add /path/to handling * [CB-7118](https://issues.apache.org/jira/browse/CB-7118) (fix jshint warnings) * [CB-7114](https://issues.apache.org/jira/browse/CB-7114) Android: add support of min/max/target SDK to config.xml * [CB-7118](https://issues.apache.org/jira/browse/CB-7118) Use updated version of node-xcode * [CB-7118](https://issues.apache.org/jira/browse/CB-7118) iOS: add target-device and MinimumOSVersion support to config.xml * ubuntu: support incremental builds * ubuntu: support target-dir for resource-file * ubuntu: use common.copyFile * ubuntu: check icon existence * ffos: Make author url optional * [CB-7142](https://issues.apache.org/jira/browse/CB-7142) Add to for "plugin restore" command * Set git clone depth to 10 for Travis to make it faster * windows: update as per changed manifest file names * Don't spy and expect it to call the other spy ... * Well that looks like an error * Fixing failing tests: update_proj should be update_project * Fix failing tests. update_jsproj and update_csproj are now just update_proj * Fix jshint errors in amazon_fireos_parser : mixed single/double quotes * [CB-6699](https://issues.apache.org/jira/browse/CB-6699) Include files from www folder via single element (use ** glob pattern) * Allow plugin modules to be .json files * Taking care of dashes in amazon-fireos platform name. * Upleveled amazon-fireos changes. * Fix link/copy parent check for windows * Style fixes - comments * Fix error in comments for munge functions * Add link to BuildBot at ci.cordova.io in README * [CB-7255](https://issues.apache.org/jira/browse/CB-7255) Fixed writing plist unescaped * Style fixes - white space only * Add JSCS config file * [CB-7228](https://issues.apache.org/jira/browse/CB-7228): Fixed issue with "cordova prepare --browserify" * [CB-7001](https://issues.apache.org/jira/browse/CB-7001): Create a --browserify option for run action * [CB-7228](https://issues.apache.org/jira/browse/CB-7228): Cordova prepare --browserify runs on all installed plugins * [CB-7190](https://issues.apache.org/jira/browse/CB-7190): Add browserify support in cordova-lib/cordova-cli * [CB-7260](https://issues.apache.org/jira/browse/CB-7260) Get cordova-android 3.5.1 instead of 3.5.0 * [CB-7001](https://issues.apache.org/jira/browse/CB-7001): Create a --browserify option for run action * [CB-7228](https://issues.apache.org/jira/browse/CB-7228): Cordova prepare --browserify runs on all installed plugins * [CB-7190](https://issues.apache.org/jira/browse/CB-7190): Add browserify support in cordova-lib/cordova-cli * [CB-7234](https://issues.apache.org/jira/browse/CB-7234) added better outputs for plugin registry workflows * [CB-7100](https://issues.apache.org/jira/browse/CB-7100): Use npm based lazy-load by default * [CB-7091](https://issues.apache.org/jira/browse/CB-7091): Remove check_requirements() funcs from platform parsers * [CB-7091](https://issues.apache.org/jira/browse/CB-7091): Remove check_requirements() funcs from platform parsers * [CB-7140](https://issues.apache.org/jira/browse/CB-7140) Check plugin versions in local search path * small refactor for missing code block after conditional statement * [CB-7203](https://issues.apache.org/jira/browse/CB-7203) isRelativePath needs to pass path through * [CB-7199](https://issues.apache.org/jira/browse/CB-7199) control git/npm using platform.js * [CB-7199](https://issues.apache.org/jira/browse/CB-7199) control git/npm using platform.js * Fix style errors - make jshint happy * [CB-6756](https://issues.apache.org/jira/browse/CB-6756) Adds save and restore command for platforms. * Add VERSION files to fix failing tests (forgot to git add in b7781cb) * [CB-7132](https://issues.apache.org/jira/browse/CB-7132) Fix regression regarding default resources * [CB-7187](https://issues.apache.org/jira/browse/CB-7187) Make CoreLocation a required library only for cordova-ios < 3.6.0 * Add AppVeyor badge to README * Add Travis and npm badges to README.md * fix(tests): cordova/lazy_load spec on Windows * Fix plugman/install spec * build configuration for AppVeyor * build configurations for Travis * [CB-7124](https://issues.apache.org/jira/browse/CB-7124) Wrap the cordova platform string in Platform object * [CB-7140](https://issues.apache.org/jira/browse/CB-7140): Switch to using PluginInfo in plugman/fetch.js * Minor style fixes in fetch.js * [CB-7078](https://issues.apache.org/jira/browse/CB-7078): Disable serve.spec.js * [CB-6512](https://issues.apache.org/jira/browse/CB-6512): platform add was using wrong www/cordova.js * [CB-7083](https://issues.apache.org/jira/browse/CB-7083) Missing SDKReference support on Windows Phone * [CB-6874](https://issues.apache.org/jira/browse/CB-6874) Consolidate tag additions into 1 ItemGroup * [CB-7100](https://issues.apache.org/jira/browse/CB-7100): Use npm based lazy-load by default * [CB-7091](https://issues.apache.org/jira/browse/CB-7091): Remove check_requirements() funcs from platform parsers * [CB-7091](https://issues.apache.org/jira/browse/CB-7091): Don't call check_requirements during platform add * Fix typo in comment. * [CB-7087](https://issues.apache.org/jira/browse/CB-7087) Retire blackberry10/ directory * [CB-6776](https://issues.apache.org/jira/browse/CB-6776): Fix uri/url renaming bug * Remove npm-shrinkwrap.json ### 0.21.4 (Jun 23, 2014) * [CB-3571](https://issues.apache.org/jira/browse/CB-3571), [CB-2606](https://issues.apache.org/jira/browse/CB-2606): support for splashscreens * [CB-6976](https://issues.apache.org/jira/browse/CB-6976) Add support for Windows Universal apps (Windows 8.1 and WP 8.1) * Use Plugininfo module to determine plugin id and version * Fix plugin check error, when plugin dependency with specific version is given * [CB-6709](https://issues.apache.org/jira/browse/CB-6709) Do not create merges/ folder when adding a platform * [CB-6140](https://issues.apache.org/jira/browse/CB-6140) Don't allow deletion of platform dependencies * [CB-6698](https://issues.apache.org/jira/browse/CB-6698): Fix 'android update lib-project' to work with paths containing spaces * [CB-6973](https://issues.apache.org/jira/browse/CB-6973): Run JSHint on all code in src/ via npm test * [CB-6542](https://issues.apache.org/jira/browse/CB-6542): Delay creating project until there's some chance that it will succeed * folder_contents() now ignores .svn folders * [CB-6970](https://issues.apache.org/jira/browse/CB-6970) Share win project files manipulation code between cordova and plugman * [CB-6954](https://issues.apache.org/jira/browse/CB-6954): Share events.js between cordova and plugman * [CB-6698](https://issues.apache.org/jira/browse/CB-6698) Automatically copy sub-libraries to project's directory * Revert "CB-6698 Resolve android relative to plugin_dir when custom=true" * [CB-6942](https://issues.apache.org/jira/browse/CB-6942) Describe running hooks only in verbose mode. * [CB-6512](https://issues.apache.org/jira/browse/CB-6512): Allow "cordova platform add /path/to/platform/files" * Update hooks-README.md - shebang line in hooks on Windows. * [CB-6895](https://issues.apache.org/jira/browse/CB-6895) Add more config properties into manifest * Allow "cordova platform add platform@version" * Add util func for chaining promises * removing doWrap from prepare * adding configurable attribute * cleaning up plugman.js for uninstall * adding param to uninstall * adding support for prepare flag * adding prepare-browserify * adding options to prepare * adding and freezing cordova-js * [CB-6879](https://issues.apache.org/jira/browse/CB-6879) config parser breakout into a cordova level module * [CB-6698](https://issues.apache.org/jira/browse/CB-6698) Resolve android relative to plugin_dir when custom=true * Fix tests on node 0.11.x * Fix android unit tests to not expect end of line. * [CB-6024](https://issues.apache.org/jira/browse/CB-6024): Accept cli vars as part of opts param * Refer properties-parser package from NPM. * [CB-6859](https://issues.apache.org/jira/browse/CB-6859) Removed all wp7 references, tests still passing * Extract AndroidProject class into a separate .js file * [CB-6698](https://issues.apache.org/jira/browse/CB-6698): Support library references for Android via the framework tag * [CB-6854](https://issues.apache.org/jira/browse/CB-6854) Strip BOM when adding cordova.define() to js-modules * Add npm cache based downloading to lazy_load * Use PluginInfo in plugman/install.js * Extend PluginInfo to parse more of plugin.xml * [CB-6772](https://issues.apache.org/jira/browse/CB-6772) Provide a default for AndroidLaunchMode * [CB-6711](https://issues.apache.org/jira/browse/CB-6711): Use parseProjectFile when working with XCode projects. * Start using PluginInfo object in plugman/install.js * [CB-6709](https://issues.apache.org/jira/browse/CB-6709) Remove merges/ folder for default apps * support for shrinkwrap flag * Initial implementation for restore and save plugin * [CB-6668](https://issues.apache.org/jira/browse/CB-6668): Use for "plugin ls" when is missing. * Add --noregstry flag for disabling plugin lookup in the registry * Remove --force from default npm settings for plugin registry * Use "npm info" for fetching plugin metadata * Use "npm cache add" for downloading plugins * [CB-6691](https://issues.apache.org/jira/browse/CB-6691): Change some instances of Error() to CordovaError() ### 0.21.1 Initial release v0.21.1 (picks up from the same version number as plugman was). ================================================ FILE: cordova-lib.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const common = require('cordova-common'); module.exports = { set binname (name) { this.cordova.binname = name; }, get binname () { return this.cordova.binname; }, events: common.events, configparser: common.ConfigParser, PluginInfo: common.PluginInfo, CordovaError: common.CordovaError, plugman: require('./src/plugman/plugman'), cordova: require('./src/cordova/cordova'), cordova_platforms: require('./src/platforms/platforms') }; ================================================ FILE: eslint.config.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const { defineConfig, globalIgnores } = require('eslint/config'); const nodeConfig = require('@cordova/eslint-config/node'); const nodeTestConfig = require('@cordova/eslint-config/node-tests'); module.exports = defineConfig([ globalIgnores([ '**/coverage/', 'spec/cordova/fixtures/*', 'spec/plugman/projects/*', 'spec/plugman/plugins/*', 'spec/cordova/temp/*' ]), ...nodeConfig, ...nodeTestConfig.map(config => ({ files: ['spec/**/*.js', 'integration-tests/**/*.js'], ...config })) ]); ================================================ FILE: integration-tests/HooksRunner.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. **/ const fs = require('node:fs'); const path = require('node:path'); const timers = require('node:timers/promises'); const et = require('elementtree'); const HooksRunner = require('../src/hooks/HooksRunner'); const cordova = require('../src/cordova/cordova'); const { tmpDir } = require('../spec/helpers'); const { PluginInfo, ConfigParser } = require('cordova-common'); const ext = process.platform === 'win32' ? 'bat' : 'sh'; const fixtures = path.join(__dirname, '../spec/cordova/fixtures'); describe('HooksRunner', function () { let tmp, project, hooksRunner; // This prepares a project that we will copy and use for all tests beforeEach(() => { tmp = tmpDir('hooks_test'); project = path.join(tmp, 'project'); // Copy base project fixture fs.cpSync(path.join(fixtures, 'basePkgJson'), project, { recursive: true }); // Copy project hooks const hooksDir = path.join(fixtures, 'projectHooks'); fs.cpSync(hooksDir, path.join(project, 'scripts'), { recursive: true }); // Change into our project directory process.chdir(project); process.env.PWD = project; // this is used by cordovaUtil.isCordova hooksRunner = new HooksRunner(project); }); afterEach(() => { process.chdir(path.join(__dirname, '..')); // Non e2e tests assume CWD is repo root. fs.rmSync(tmp, { recursive: true, force: true }); }); it('Test 001 : should throw if provided directory is not a cordova project', function () { expect(_ => new HooksRunner(tmp)).toThrow(); }); it('Test 002 : should not throw if provided directory is a cordova project', function () { expect(_ => new HooksRunner(project)).not.toThrow(); }); describe('fire method', function () { const test_event = 'before_build'; let hooksOrderFile; beforeEach(function () { hooksOrderFile = path.join(project, 'hooks_order.txt'); fs.rmSync(hooksOrderFile, { recursive: true, force: true }); }); // helper methods function getActualHooksOrder () { const fileContents = fs.readFileSync(hooksOrderFile, 'ascii'); return fileContents.match(/\d+/g).map(Number); } function checkHooksOrderFile () { expect(hooksOrderFile).toExist(); const hooksOrder = getActualHooksOrder(); const sortedHooksOrder = hooksOrder.slice(0).sort((a, b) => a - b); expect(hooksOrder).toEqual(sortedHooksOrder); } function addHooks (hooksXml, doc) { const hooks = et.parse(hooksXml); for (const el of hooks.getroot().findall('./*')) { doc.getroot().append(el); } } describe('application hooks', function () { const BASE_HOOKS = ` `; const WINDOWS_HOOKS = ` `; const ANDROID_HOOKS = ` `; function addHooksToConfig (hooksXml) { const config = new ConfigParser(path.join(project, 'config.xml')); addHooks(hooksXml, config.doc); config.write(); } it('Test 006 : should execute hook scripts serially from config.xml', function () { addHooksToConfig(BASE_HOOKS); return hooksRunner.fire(test_event) .then(checkHooksOrderFile); }); it('Test 007 : should execute hook scripts serially from config.xml including platform scripts', function () { addHooksToConfig(BASE_HOOKS); addHooksToConfig(WINDOWS_HOOKS); return hooksRunner.fire(test_event) .then(checkHooksOrderFile); }); it('Test 008 : should filter hook scripts from config.xml by platform', function () { addHooksToConfig(BASE_HOOKS); addHooksToConfig(WINDOWS_HOOKS); addHooksToConfig(ANDROID_HOOKS); const hookOptions = { cordova: { platforms: ['android'] } }; return hooksRunner.fire(test_event, hookOptions).then(function () { checkHooksOrderFile(); const baseScriptResults = [8, 9]; const androidPlatformScriptsResults = [14, 15]; const expectedResults = baseScriptResults.concat(androidPlatformScriptsResults); expect(getActualHooksOrder()).toEqual(expectedResults); }); }); it('Test 023 : should error if any hook fails', function () { const FAIL_HOOK = ` `; addHooksToConfig(FAIL_HOOK); return expectAsync( hooksRunner.fire('fail') ).toBeRejectedWithError(); }); it('Test 024 : should not error if the hook is unrecognized', function () { return hooksRunner.fire('CLEAN YOUR SHORTS GODDAMNIT LIKE A BIG BOY!'); }); }); describe('plugin hooks', function () { const PLUGIN_BASE_HOOKS = ` `; const PLUGIN_WINDOWS_HOOKS = ` `; const PLUGIN_ANDROID_HOOKS = ` `; const testPlugin = 'com.plugin.withhooks'; const testPluginFixture = path.join(fixtures, 'plugins', testPlugin); let testPluginInstalledPath; beforeEach(() => { // Add the test plugin to our project testPluginInstalledPath = path.join(project, 'plugins', testPlugin); fs.cpSync(testPluginFixture, testPluginInstalledPath, { recursive: true }); }); function addHooksToPlugin (hooksXml) { const config = new PluginInfo(testPluginInstalledPath); addHooks(hooksXml, config._et); const configPath = path.join(testPluginInstalledPath, 'plugin.xml'); fs.writeFileSync(configPath, config._et.write({ indent: 4 })); } it('Test 009 : should execute hook scripts serially from plugin.xml', function () { addHooksToPlugin(PLUGIN_BASE_HOOKS); return hooksRunner.fire(test_event) .then(checkHooksOrderFile); }); it('Test 010 : should execute hook scripts serially from plugin.xml including platform scripts', function () { addHooksToPlugin(PLUGIN_BASE_HOOKS); addHooksToPlugin(PLUGIN_WINDOWS_HOOKS); return hooksRunner.fire(test_event) .then(checkHooksOrderFile); }); it('Test 011 : should filter hook scripts from plugin.xml by platform', function () { addHooksToPlugin(PLUGIN_BASE_HOOKS); addHooksToPlugin(PLUGIN_WINDOWS_HOOKS); addHooksToPlugin(PLUGIN_ANDROID_HOOKS); const hookOptions = { cordova: { platforms: ['android'] } }; return hooksRunner.fire(test_event, hookOptions).then(function () { checkHooksOrderFile(); const baseScriptResults = [21, 22]; const androidPlatformScriptsResults = [26]; const expectedResults = baseScriptResults.concat(androidPlatformScriptsResults); expect(getActualHooksOrder()).toEqual(expectedResults); }); }); }); describe('nohooks option', () => { it('Test 013 : should not execute the designated hook when --nohooks option specifies the exact hook name', async () => { const hookOptions = { nohooks: [test_event] }; expect(await hooksRunner.fire(test_event, hookOptions)) .toBe('hook before_build is disabled.'); }); it('Test 014 : should not execute matched hooks when --nohooks option specifies a hook pattern', async () => { const hookOptions = { nohooks: ['ba'] }; for (const e of ['foo', 'bar', 'baz']) { expect(await hooksRunner.fire(e, hookOptions)) .toBe(e === 'foo' ? undefined : `hook ${e} is disabled.`); } }); it('Test 015 : should not execute any hooks when --nohooks option specifies .', async () => { const hookOptions = { nohooks: ['.'] }; for (const e of ['foo', 'bar', 'baz']) { expect(await hooksRunner.fire(e, hookOptions)) .toBe(`hook ${e} is disabled.`); } }); }); describe('module-level hooks (event handlers)', function () { let handler; beforeEach(() => { handler = jasmine.createSpy('testHandler').and.resolveTo(); cordova.on(test_event, handler); }); afterEach(function () { cordova.removeAllListeners(test_event); }); it('Test 016 : should fire handlers that were attached using cordova.on', function () { return hooksRunner.fire(test_event).then(function () { expect(handler).toHaveBeenCalled(); }); }); it('Test 017 : should pass the project root folder as parameter into the module-level handlers', function () { return hooksRunner.fire(test_event).then(function () { expect(handler).toHaveBeenCalledWith(jasmine.objectContaining({ projectRoot: project })); }); }); it('Test 018 : should be able to stop listening to events using cordova.off', function () { cordova.off(test_event, handler); return hooksRunner.fire(test_event).then(function () { expect(handler).not.toHaveBeenCalled(); }); }); it('Test 019 : should execute async event listeners serially', function () { const order = []; // Delay 100 ms here to check that h2 is not executed until after // the promise returned by h1 is resolved. const h1 = _ => timers.setTimeout(100).then(_ => order.push(1)); const h2 = _ => Promise.resolve().then(_ => order.push(2)); cordova.on(test_event, h1); cordova.on(test_event, h2); return hooksRunner.fire(test_event) .then(_ => expect(order).toEqual([1, 2])); }); it('Test 021 : should pass options passed to fire into handlers', async () => { const hookOptions = { test: 'funky' }; await hooksRunner.fire(test_event, hookOptions); expect(handler).toHaveBeenCalledWith(hookOptions); }); }); }); }); ================================================ FILE: integration-tests/fetch.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const fs = require('node:fs'); const helpers = require('../spec/helpers'); const cordova = require('../src/cordova/cordova'); const TIMEOUT = 60 * 1000; const plugins_dir = path.join(__dirname, '..', 'spec', 'plugman', 'plugins'); const plugins = { Test1: path.join(plugins_dir, 'dependencies', 'Test1'), Test2: path.join(plugins_dir, 'dependencies', 'Test2'), Test3: path.join(plugins_dir, 'dependencies', 'Test3'), Test4: path.join(plugins_dir, 'dependencies', 'Test4') }; describe('end-to-end plugin dependency tests', function () { helpers.setDefaultTimeout(TIMEOUT); // This prepares a project that we will copy and use for all tests let preparedProject; beforeAll(function () { preparedProject = helpers.tmpDir('plugin_dependency_test_project'); return helpers.getFixture('projectWithPlatform').copyTo(preparedProject); }); afterAll(function () { process.chdir(__dirname); // Needed to rm the dir on Windows. fs.rmSync(preparedProject, { recursive: true, force: true }); }); let tmpDir, project, pluginsDir; beforeEach(function () { tmpDir = helpers.tmpDir('plugin_dependency_test'); project = path.join(tmpDir, 'project'); pluginsDir = path.join(project, 'plugins'); fs.cpSync(preparedProject, project, { recursive: true }); process.chdir(project); delete process.env.PWD; }); afterEach(function () { process.chdir(__dirname); // Needed to rm the dir on Windows. fs.rmSync(tmpDir, { recursive: true, force: true }); }); it('Test 029 : should fail if dependency already installed is wrong version', function () { return Promise.resolve() .then(function () { return cordova.plugin('add', 'cordova-plugin-file'); }).then(function () { expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist(); return cordova.plugin('add', plugins.Test1); }).catch(function (err) { expect(err.message).toContain('does not satisfy dependency plugin requirement'); }); }); it('Test 030 : should pass if dependency already installed is wrong version with --force', function () { return Promise.resolve() .then(function () { return cordova.plugin('add', 'cordova-plugin-file'); }) .then(function () { expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist(); return cordova.plugin('add', plugins.Test1, { force: true }); }) .then(function () { expect(path.join(pluginsDir, 'Test1')).toExist(); }); }); it('Test 031 : should pass if dependency already installed is same major version (if specific version is specified)', function () { // Test1 requires cordova-plugin-file version 2.0.0 (which should automatically turn into ^2.0.0); we'll install version 2.1.0 return Promise.resolve() .then(function () { return cordova.plugin('add', 'cordova-plugin-file@2.1.0'); }) .then(function () { expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist(); return cordova.plugin('add', plugins.Test1); }) .then(function () { expect(path.join(pluginsDir, 'Test1')).toExist(); }); }); it('Test 032 : should handle two plugins with same dependent plugin', function () { // Test1 and Test2 have compatible dependencies on cordova-plugin-file // Test1 and Test3 have incompatible dependencies on cordova-plugin-file return Promise.resolve() .then(function () { return cordova.plugin('add', plugins.Test1); }) .then(function () { expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist(); expect(path.join(pluginsDir, 'Test1')).toExist(); return cordova.plugin('add', plugins.Test2); }) .then(function () { return cordova.plugin('add', plugins.Test3); }) .catch(function (err) { expect(path.join(pluginsDir, 'Test2')).toExist(); expect(path.join(pluginsDir, 'Test3')).not.toExist(); expect(err.message).toContain('does not satisfy dependency plugin requirement'); }); }); it('Test 033 : should use a dev version of a dependent plugin if it is already installed', function () { // Test4 has this dependency in its plugin.xml: // return Promise.resolve() .then(function () { return cordova.plugin('add', 'https://github.com/apache/cordova-plugin-file'); }) .then(function () { return cordova.plugin('add', plugins.Test4); }) .then(function () { expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist(); expect(path.join(pluginsDir, 'Test4')).toExist(); }); }); }); ================================================ FILE: integration-tests/pkgJson.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const fs = require('node:fs'); const semver = require('semver'); const { listPlatforms, requireNoCache } = require('../src/cordova/util'); const { tmpDir: getTmpDir, testPlatform, getFixture, setDefaultTimeout } = require('../spec/helpers'); const projectTestHelpers = require('../spec/project-test-helpers'); const cordova = require('../src/cordova/cordova'); describe('pkgJson', function () { const TIMEOUT = 150 * 1000; setDefaultTimeout(TIMEOUT); const fixturesPath = path.join(__dirname, '../spec/cordova/fixtures'); let tmpDir, project, pkgJsonPath; const { getPkgJsonPath, setupBaseProject, getCfg, getPkgJson, setPkgJson } = projectTestHelpers(() => project); beforeEach(() => { tmpDir = getTmpDir('pkgJson'); project = path.join(tmpDir, 'project'); pkgJsonPath = getPkgJsonPath(); delete process.env.PWD; }); afterEach(() => { process.chdir(__dirname); // Needed to rm the dir on Windows. fs.rmSync(tmpDir, { recursive: true, force: true }); }); // Copies a fixture to temp dir to avoid modifiying it as they get installed as symlinks function copyFixture (fixtureRelativePath) { const fixturePath = path.join(fixturesPath, fixtureRelativePath); const tmpPath = path.join(tmpDir, path.basename(fixtureRelativePath)); fs.cpSync(fixturePath, tmpPath, { recursive: true }); return tmpPath; } function installedPlatforms () { // Sort platform list to allow for easy pseudo set equality return listPlatforms(project).sort(); } function platformVersion (platformName) { const p = path.join(project, 'platforms', platformName, 'cordova/Api.js'); expect(p).toExist(); return requireNoCache(p).version(); } function pluginVersion (pluginName) { const p = path.join(project, 'plugins', pluginName, 'package.json'); expect(p).toExist(); return JSON.parse(fs.readFileSync(p)).version; } function specSatisfiedBy (version) { return { asymmetricMatch: spec => semver.satisfies(version, spec), jasmineToString: _ => `` }; } function specWithMinSatisfyingVersion (version) { return { asymmetricMatch: spec => !semver.intersects(spec, `<${version}`) && semver.intersects(spec, `>=${version}`), jasmineToString: _ => `` }; } const customMatchers = { toSatisfy: () => ({ compare (version, spec) { const pass = semver.satisfies(version, spec); const expectation = (pass ? 'not ' : '') + 'to satisfy'; return { pass, message: `expected ${version} ${expectation} ${spec}` }; } }), tohaveMinSatisfyingVersion: () => ({ compare (spec, version) { const pass = specWithMinSatisfyingVersion(version).asymmetricMatch(spec); const expectation = (pass ? 'not ' : '') + 'to have minimal satisfying version'; return { pass, message: `expected ${spec} ${expectation} ${version}` }; } }) }; // Add our custom matchers beforeEach(() => jasmine.addMatchers(customMatchers)); // This group of tests checks if plugins are added and removed as expected from package.json. describe('plugin end-to-end', function () { const pluginId = 'cordova-plugin-device'; // Prepare a project with platform that can be reused by below tests let projectFixture; beforeAll(() => { projectFixture = getTmpDir('pkgJson-project-fixture'); return getFixture('projectWithPlatform').copyTo(projectFixture); }); afterAll(() => { process.chdir(__dirname); // Needed to rm the dir on Windows. fs.rmSync(projectFixture, { recursive: true, force: true }); }); beforeEach(function () { fs.cpSync(projectFixture, project, { recursive: true }); process.chdir(project); }); it('Test#001 : should successfully add and remove a plugin with save and correct spec', function () { // No plugins in config or pkg.json yet. expect(getCfg().getPluginIdList()).toEqual([]); expect(getPkgJson('cordova.plugins')).toBeUndefined(); // Add the plugin with --save. return cordova.plugin('add', `${pluginId}@1.1.2`, { save: true }) .then(function () { // Check that the plugin and spec add was successful to pkg.json. expect(getPkgJson('cordova.plugins')[pluginId]).toBeDefined(); expect(getPkgJson('devDependencies')[pluginId]).tohaveMinSatisfyingVersion('1.1.2'); expect(getCfg().getPluginIdList()).toEqual([]); }).then(function () { // And now remove it with --save. return cordova.plugin('rm', pluginId, { save: true }); }).then(function () { // Expect plugin to be removed from pkg.json. expect(getPkgJson('cordova.plugins')[pluginId]).toBeUndefined(); expect(getPkgJson('devDependencies')[pluginId]).toBeUndefined(); }); }); it('Test#002 : should NOT add a plugin to package.json if --save is not used', function () { const SAVED_PLUGIN = 'cordova-plugin-geolocation'; expect(pkgJsonPath).toExist(); // Add the geolocation plugin with --save. return cordova.plugin('add', SAVED_PLUGIN, { save: true }) .then(function () { // Add a second plugin without save. return cordova.plugin('add', pluginId); }).then(function () { // Expect that only the plugin that had --save was added. expect(getPkgJson('cordova.plugins')).toEqual({ [SAVED_PLUGIN]: jasmine.anything() }); }); }); it('Test#003 : should NOT remove plugin from package.json if there is no --save', function () { expect(pkgJsonPath).toExist(); // Add the plugin with --save. return cordova.plugin('add', pluginId, { save: true }) .then(function () { expect(getPkgJson('cordova.plugins')).toEqual({ [pluginId]: {} }); }).then(function () { // And now remove it, but without --save. return cordova.plugin('rm', pluginId); }).then(function () { // The plugin should still be in package.json. expect(getPkgJson('cordova.plugins')).toEqual({ [pluginId]: {} }); }); }); it('Test#004 : should successfully add and remove a plugin with variables and save to package.json', function () { expect(pkgJsonPath).toExist(); // Add the plugin with --save. return cordova.plugin('add', pluginId, { save: true, cli_variables: { someKey: 'someValue' } }) .then(function () { // Check the plugin add was successful and that variables have been added too. expect(getPkgJson('cordova.plugins')).toEqual({ [pluginId]: { someKey: 'someValue' } }); }).then(function () { // And now remove it with --save. return cordova.plugin('rm', pluginId, { save: true }); }).then(function () { // Checking that the plugin and variables were removed successfully. expect(getPkgJson('cordova.plugins')).toEqual({}); }); }); it('Test#005 : should successfully add and remove multiple plugins with save & fetch', function () { const OTHER_PLUGIN = 'cordova-plugin-device-motion'; expect(pkgJsonPath).toExist(); // Add the plugin with --save. return cordova.plugin('add', [pluginId, OTHER_PLUGIN], { save: true }) .then(function () { // Check that the plugin add was successful. expect(getPkgJson('cordova.plugins')).toEqual({ [pluginId]: {}, [OTHER_PLUGIN]: {} }); expect(getPkgJson('devDependencies')).toEqual({ 'cordova-android': jasmine.any(String), [pluginId]: jasmine.any(String), [OTHER_PLUGIN]: jasmine.any(String) }); }).then(function () { // And now remove it with --save. return cordova.plugin('rm', [pluginId, OTHER_PLUGIN], { save: true }); }).then(function () { // Checking that the plugin removed is in not in the platforms. expect(getPkgJson('cordova.plugins')).toEqual({}); expect(getPkgJson('devDependencies')).toEqual({ 'cordova-android': jasmine.any(String) }); }); }); // Test #023 : if pkg.json and config.xml have no platforms/plugins/spec. // and --save is called, use the pinned version or plugin pkg.json version. it('Test#023 : use pinned/lastest version if there is no platform/plugin version passed in and no platform/plugin versions in pkg.json or config.xml', function () { const PLATFORM = 'ios'; const PLUGIN = 'cordova-plugin-geolocation'; // Pkg.json has no platform or plugin or specs. expect(getPkgJson('cordova.platforms')).not.toContain(PLATFORM); expect(getPkgJson(`devDependencies.${PLUGIN}`)).toBeUndefined(); // Config.xml has no platform or plugin or specs. expect(getCfg().getEngines()).not.toContain(PLATFORM); expect(getCfg().getPluginIdList()).toEqual([]); return cordova.platform('add', PLATFORM, { save: true }) .then(function () { expect(getPkgJson('cordova.platforms')).toContain(PLATFORM); }).then(function () { return cordova.plugin('add', PLUGIN, { save: true }); }).then(function () { const iosJson = JSON.parse(fs.readFileSync(path.join(project, 'platforms/ios/ios.json'))); expect(iosJson.installed_plugins[PLUGIN]).toBeDefined(); // Check that installed version satisfies the dependency spec const version = pluginVersion(PLUGIN); expect(version).toSatisfy(getPkgJson(`devDependencies.${PLUGIN}`)); }); }); // Test#025: has a pkg.json. Checks if local path is added to pkg.json for platform and plugin add. it('Test#025 : if you add a plugin with local path, pkg.json gets updated', () => { const PLUGIN = 'cordova-lib-test-plugin'; const pluginPath = copyFixture(path.join('plugins', PLUGIN)); // Run cordova plugin add local path --save return cordova.plugin('add', pluginPath, { save: true }) .then(() => { // Pkg.json has test plugin. expect(getPkgJson(`cordova.plugins.${PLUGIN}`)).toBeDefined(); expect(getPkgJson(`devDependencies.${PLUGIN}`)).toBeDefined(); }); }); it('Test#026 : should successfully add a plugin with git/semver combo', async () => { const TAG = '#semver:2.0.x'; const URL = `https://github.com/apache/cordova-plugin-device.git${TAG}`; expect(getPkgJson('cordova.plugins')).toBeUndefined(); expect(getPkgJson(`devDependencies.${pluginId}`)).toBeUndefined(); await cordova.plugin('add', URL, { save: true }); expect(getPkgJson('cordova.plugins')[pluginId]).toBeDefined(); expect(getPkgJson('devDependencies')[pluginId]).toContain(TAG); }); }); // This group of tests checks if platforms are added and removed as expected from package.json. describe('platform end-to-end with --save', function () { beforeEach(() => setupBaseProject()); it('Test#006 : platform is added and removed correctly with --save', function () { expect(pkgJsonPath).toExist(); expect(installedPlatforms()).toEqual([]); // Add the testing platform with --save. return cordova.platform('add', testPlatform, { save: true }).then(function () { // Check the platform add was successful. expect(installedPlatforms()).toEqual([testPlatform]); expect(getPkgJson('cordova.platforms')).toEqual([testPlatform]); }).then(function () { // And now remove it with --save. return cordova.platform('rm', testPlatform, { save: true }); }).then(function () { // Checking that the platform removed is in not in the platforms key. expect(getPkgJson('cordova.platforms')).toEqual([]); }); }); it('Test#007 : should not remove platforms from package.json when removing without --save', function () { expect(pkgJsonPath).toExist(); expect(installedPlatforms()).toEqual([]); // Add the testing platform with --save. return cordova.platform('add', testPlatform, { save: true }).then(function () { // Check the platform add was successful. expect(installedPlatforms()).toEqual([testPlatform]); expect(getPkgJson('cordova.platforms')).toEqual([testPlatform]); }).then(function () { // And now remove it without --save. return cordova.platform('rm', testPlatform); }).then(function () { // Check that the removed platform is still in cordova.platforms... expect(getPkgJson('cordova.platforms')).toEqual([testPlatform]); // ... but is not actually installed any longer expect(installedPlatforms()).toEqual([]); }); }); it('Test#008 : should not add platform to package.json when adding without --save', function () { expect(getPkgJson('cordova')).toBeUndefined(); // Add platform without --save. return cordova.platform('add', testPlatform) .then(function () { // Test platform should have been installed but not added to pkg.json expect(installedPlatforms()).toEqual([testPlatform]); expect(getPkgJson('cordova')).toBeUndefined(); }); }); it('Test#009 : should only add the platform to package.json with --save', function () { const platformNotToAdd = 'ios'; expect(pkgJsonPath).toExist(); // Add a platform without --save. return cordova.platform('add', platformNotToAdd) .then(function () { // And now add another platform with --save. return cordova.platform('add', testPlatform, { save: true }); }).then(function () { // Check that only the platform added with --save was added to package.json. expect(getPkgJson('cordova.platforms')).toEqual([testPlatform]); }); }); it('Test#010 : two platforms are added and removed correctly with --save', function () { // No platforms installed nor saved in config or pkg.json yet. expect(getPkgJson('cordova')).toBeUndefined(); expect(getCfg().getEngines()).toEqual([]); expect(installedPlatforms()).toEqual([]); // Add the testing platform with --save and add specific version to android platform. return cordova.platform('add', ['android@9.0.0', 'ios@6.1.0'], { save: true }).then(function () { expect(installedPlatforms()).toEqual(['android', 'ios']); // Check the platform add was successful in platforms list and // dependencies should have specific version from add. expect(getPkgJson('cordova.platforms')).toEqual(['android', 'ios']); expect(getPkgJson('devDependencies')).toEqual({ 'cordova-android': specWithMinSatisfyingVersion('9.0.0'), 'cordova-ios': specWithMinSatisfyingVersion('6.1.0') }); expect(getCfg().getEngines()).toEqual([]); }).then(function () { // And now remove it with --save. return cordova.platform('rm', ['android', 'ios'], { save: true }); }).then(function () { // Expect platforms to be uninstalled & removed from config files expect(getPkgJson('cordova.platforms')).toEqual([]); expect(getPkgJson('devDependencies') || {}).toEqual({}); expect(getCfg().getEngines()).toEqual([]); expect(installedPlatforms()).toEqual([]); }); }); xit('Test#012 : platform with local path is added correctly with --save', () => { const PLATFORM = 'android'; const platformPath = path.join(tmpDir, PLATFORM); expect(getPkgJson('cordova')).toBeUndefined(); expect(installedPlatforms()).toEqual([]); return getFixture('androidPlatform').copyTo(platformPath) .then(() => { return cordova.platform('add', platformPath, { save: true }); }) .then(() => { expect(installedPlatforms()).toEqual([PLATFORM]); expect(getPkgJson('cordova.platforms')).toEqual([PLATFORM]); expect(getPkgJson(`devDependencies.cordova-${PLATFORM}`)).toBeDefined(); }); }); }); // Test #020 : pkg.json contains platform/spec and plugin/spec and config.xml does not describe('with differing config files', function () { beforeEach(() => setupBaseProject()); /** Test#020 will check that pkg.json, config.xml, platforms.json, and cordova platform ls * are updated with the correct (platform and plugin) specs from pkg.json. */ // @todo add a test to also support checking dependencies it('Test#020 : During add, if pkg.json has a spec, use that one.', function () { const PLATFORM = 'ios'; const PLUGIN = 'cordova-plugin-splashscreen'; setPkgJson('cordova.platforms', [PLATFORM]); setPkgJson('devDependencies', { [PLUGIN]: '^3.2.2', [`cordova-${PLATFORM}`]: '^6.1.0' }); // config.xml has no platforms or plugins yet. expect(getCfg().getEngines()).toEqual([]); expect(getCfg().getPluginIdList()).toEqual([]); expect(installedPlatforms()).toEqual([]); return cordova.platform('add', PLATFORM, { save: true }).then(function () { // No change to pkg.json platforms or spec for ios. expect(getPkgJson('cordova.platforms')).toEqual([PLATFORM]); // Config.xml and ios/cordova/version check. const version = platformVersion(PLATFORM); // Check that pkg.json and ios/cordova/version versions "satisfy" each other. const pkgSpec = getPkgJson(`devDependencies.cordova-${PLATFORM}`); expect(version).toSatisfy(pkgSpec); }).then(function () { return cordova.plugin('add', PLUGIN, { save: true }); }).then(function () { // Check that installed version satisfies the dependency spec expect(pluginVersion(PLUGIN)).toSatisfy(getPkgJson(`devDependencies.${PLUGIN}`)); }); }, TIMEOUT * 2); /** Test#021 during add, this test will check that pkg.json, config.xml, platforms.json, * and cordova platform ls are updated with the correct platform/plugin spec from config.xml. */ it('Test#021 : If config.xml has a spec (and none was specified and pkg.json does not have one), use config.', function () { const PLATFORM = 'ios'; const PLUGIN = 'cordova-plugin-splashscreen'; getCfg() .addEngine(PLATFORM, '~4.2.1') .addPlugin({ name: PLUGIN, spec: '~3.2.2' }) .write(); expect(installedPlatforms()).toEqual([]); // Remove for testing purposes so platform is not pre-installed. return cordova.platform('rm', PLATFORM, { save: true }).then(function () { return cordova.platform('add', PLATFORM, { save: true }); }).then(function () { // pkg.json has new platform. expect(getPkgJson('cordova.platforms')).toEqual([PLATFORM]); }).then(function () { return cordova.plugin('add', PLUGIN, { save: true }); }).then(function () { expect(getCfg().getPlugins()).toEqual([{ name: PLUGIN, spec: specSatisfiedBy(pluginVersion(PLUGIN)), variables: {} }]); }); }); /** Test#022 : when adding with a specific platform version, always use that one * regardless of what is in package.json or config.xml. */ it('Test#022 : when adding with a specific platform version, always use that one.', function () { const PLATFORM = 'ios'; const PLUGIN = 'cordova-plugin-splashscreen'; setPkgJson('cordova.platforms', [PLATFORM]); setPkgJson('devDependencies', { [`cordova-${PLATFORM}`]: '^6.1.0', [PLUGIN]: '^3.2.2' }); getCfg() .addEngine(PLATFORM, '~6.1.0') .addPlugin({ name: PLUGIN, spec: '~3.2.1' }) .write(); expect(installedPlatforms()).toEqual([]); return cordova.platform('add', `${PLATFORM}@6.1.0`, { save: true }).then(function () { // Pkg.json has ios. expect(getPkgJson('cordova.platforms')).toEqual([PLATFORM]); }).then(function () { return cordova.plugin('add', `${PLUGIN}@4.0.0`, { save: true }); }).then(function () { // Check that installed version satisfies the dependency spec const version = pluginVersion(PLUGIN); expect(version).toSatisfy(getCfg().getPlugin(PLUGIN).spec); expect(version).toSatisfy(getPkgJson(`devDependencies.${PLUGIN}`)); }); }); }); }); ================================================ FILE: integration-tests/platform.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const fs = require('node:fs'); const rewire = require('rewire'); const { tmpDir: getTmpDir, testPlatform, setDefaultTimeout } = require('../spec/helpers'); const { listPlatforms } = require('../src/cordova/util'); const cordova = require('../src/cordova/cordova'); const plugman = require('../src/plugman/plugman'); const fixturesDir = path.join(__dirname, '..', 'spec', 'cordova', 'fixtures'); const pluginFixturesDir = path.join(fixturesDir, 'plugins'); describe('cordova/platform end-to-end', () => { const TIMEOUT = 240 * 1000; setDefaultTimeout(TIMEOUT); let tmpDir, project, pluginsDir, platformsDir, nodeModulesDir, testPlatformDir; beforeEach(() => { tmpDir = getTmpDir('cordova-platform-e2e-test'); project = path.join(tmpDir, 'project'); pluginsDir = path.join(project, 'plugins'); platformsDir = path.join(project, 'platforms'); nodeModulesDir = path.join(project, 'node_modules'); testPlatformDir = path.join(platformsDir, testPlatform); fs.cpSync(path.join(fixturesDir, 'basePkgJson'), project, { recursive: true }); process.chdir(project); }); afterEach(() => { process.chdir(__dirname); // Needed to rm the dir on Windows. fs.rmSync(tmpDir, { recursive: true, force: true }); }); function installedPlatforms () { return listPlatforms(project); } it('Test 001 : should successfully run', () => { // Check there are no platforms yet. expect(installedPlatforms()).toEqual([]); return Promise.resolve() .then(() => { // Add the testing platform. return cordova.platform('add', [testPlatform]); }) .then(() => { // Check the platform add was successful. expect(testPlatformDir).toExist(); expect(path.join(testPlatformDir, 'cordova')).toExist(); expect(installedPlatforms()).toEqual([testPlatform]); }) .then(() => { // Spy on Api.updatePlatform since it always rejects otherwise const Api = require(path.join(nodeModulesDir, 'cordova-android')); spyOn(Api, 'updatePlatform').and.returnValue(Promise.resolve()); spyOn(require('../src/cordova/util'), 'getPlatformApiFunction').and.returnValue(Api); return cordova.platform('update', [testPlatform]).then(_ => Api); }) .then(Api => { expect(Api.updatePlatform).toHaveBeenCalled(); // Platform should still be in platform ls. expect(installedPlatforms()).toEqual([testPlatform]); }) .then(() => { // And now remove it. return cordova.platform('rm', [testPlatform]); }) .then(() => { // It should be gone. expect(testPlatformDir).not.toExist(); expect(installedPlatforms()).toEqual([]); }); }); it('Test 002 : should install plugins correctly while adding platform', () => { spyOn(plugman, 'install').and.callThrough(); const prepare = require('../src/cordova/prepare'); const prepareSpy = jasmine.createSpy('prepare', prepare).and.callThrough(); Object.assign(prepareSpy, prepare); // This is all just to get the prepareSpy to be used by `platform.add` const platform = rewire('../src/cordova/platform'); const addHelper = rewire('../src/cordova/platform/addHelper'); const requireFake = jasmine.createSpy('require', addHelper.__get__('require')).and.callThrough(); requireFake.withArgs('../prepare').and.returnValue(prepareSpy); addHelper.__set__({ require: requireFake }); platform.__set__({ addHelper }); return Promise.resolve() .then(() => { return cordova.plugin('add', path.join(pluginFixturesDir, 'test')); }) .then(() => { return platform('add', [testPlatform]); }) .then(() => { // Check the platform add was successful. expect(testPlatformDir).toExist(); // Check that plugin files exists in www dir expect(path.join(testPlatformDir, 'platform_www/test.js')).toExist(); // should call prepare after plugins were installed into platform expect(plugman.install).toHaveBeenCalledBefore(prepareSpy); }); }); it('Test 007 : should add and remove platform from node_modules directory', () => { return Promise.resolve() .then(() => { return cordova.platform('add', 'ios', { save: true }); }) .then(() => { expect(path.join(nodeModulesDir, 'cordova-ios')).toExist(); expect(path.join(platformsDir, 'ios')).toExist(); return cordova.platform('add', 'android@9.0.0'); }) .then(() => { expect(path.join(nodeModulesDir, 'cordova-android')).toExist(); expect(path.join(platformsDir, 'android')).toExist(); return cordova.platform('rm', 'ios', { save: true }); }) .then(() => { expect(path.join(nodeModulesDir, 'cordova-ios')).not.toExist(); expect(path.join(platformsDir, 'ios')).not.toExist(); return cordova.platform('rm', 'android', { save: true }); }) .then(() => { expect(path.join(nodeModulesDir, 'cordova-android')).not.toExist(); expect(path.join(platformsDir, 'android')).not.toExist(); }); }); it('Test 008 : should remove dependency when removing parent plugin', () => { return Promise.resolve() .then(() => { return cordova.platform('add', testPlatform); }) .then(() => { return cordova.plugin('add', 'cordova-plugin-media', { save: true }); }) .then(() => { expect(path.join(pluginsDir, 'cordova-plugin-media')).toExist(); expect(path.join(pluginsDir, 'cordova-plugin-file')).toExist(); expect(path.join(nodeModulesDir, 'cordova-plugin-media')).toExist(); expect(path.join(nodeModulesDir, 'cordova-plugin-file')).toExist(); return cordova.plugin('rm', 'cordova-plugin-media', { save: true }); }) .then(() => { expect(path.join(pluginsDir, 'cordova-plugin-media')).not.toExist(); expect(path.join(pluginsDir, 'cordova-plugin-file')).not.toExist(); expect(path.join(nodeModulesDir, 'cordova-plugin-media')).not.toExist(); expect(path.join(nodeModulesDir, 'cordova-plugin-file')).not.toExist(); }); }); it('Test 009 : should add and remove 3rd party platforms', () => { return Promise.resolve() .then(() => { // add cordova-android instead of android return cordova.platform('add', 'cordova-android@9.0.0'); }) .then(() => { // 3rd party platform from npm return cordova.platform('add', 'cordova-platform-test'); }) .then(() => { expect(path.join(platformsDir, 'android')).toExist(); expect(path.join(platformsDir, 'cordova-platform-test')).toExist(); expect(installedPlatforms()).toEqual(jasmine.arrayWithExactContents([ 'android', 'cordova-platform-test' ])); }); }); }); ================================================ FILE: integration-tests/plugin.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const helpers = require('../spec/helpers'); const events = require('cordova-common').events; const cordova = require('../src/cordova/cordova'); const platforms = require('../src/platforms/platforms'); const plugman = require('../src/plugman/plugman'); const install = require('../src/plugman/install'); const plugin_util = require('../src/cordova/plugin/util'); const HooksRunner = require('../src/hooks/HooksRunner'); const util = require('../src/cordova/util'); const tmpDir = helpers.tmpDir('plugin_test'); const preparedProject = path.join(tmpDir, 'prepared-project'); const fixturesDir = path.join(__dirname, '..', 'spec', 'cordova', 'fixtures'); const pluginsDir = path.join(fixturesDir, 'plugins'); const pluginId = 'org.apache.cordova.fakeplugin1'; const org_test_defaultvariables = 'org.test.defaultvariables'; // This plugin is published to npm and defines cordovaDependencies // in its package.json. Based on the dependencies and the version of // cordova-android installed in our test project, the CLI should // select version 1.1.2 of the plugin. We don't actually fetch from // npm, but we do check the npm info. const npmInfoTestPlugin = 'cordova-lib-test-plugin'; const npmInfoTestPluginVersion = '1.1.2'; const scopedTestPlugin = '@cordova/plugin-test-dummy'; const testGitPluginRepository = 'https://github.com/apache/cordova-plugin-device.git'; const testGitPluginId = 'cordova-plugin-device'; let results; // Runs: list, add, list function addPlugin (project, target, id, options) { // Check there are no plugins yet. return cordova.plugin('list').then(function () { expect(results).toMatch(/No plugins added/gi); }).then(function () { // Add a fake plugin from fixtures. return cordova.plugin('add', target, options); }).then(function () { expect(path.join(project, 'plugins', id, 'plugin.xml')).toExist(); }).then(function () { return cordova.plugin('ls'); }).then(function () { expect(results).toContain(id); }); } // Runs: remove, list function removePlugin (project, id) { return cordova.plugin('rm', id) .then(function () { // The whole dir should be gone. expect(path.join(project, 'plugins', id)).not.toExist(); }).then(function () { return cordova.plugin('ls'); }).then(function () { expect(results).toMatch(/No plugins added/gi); }); } // We can't call add with a searchpath or else we will conflict with other tests // that use a searchpath. See loadLocalPlugins() in plugman/fetch.js for details. // The searchpath behavior gets tested in the plugman spec function mockPluginFetch (project, id, dir) { spyOn(plugman, 'fetch').and.callFake(function (target, pluginPath, fetchOptions) { const dest = path.join(project, 'plugins', id); fs.cpSync(path.join(dir, 'plugin.xml'), path.join(dest, 'plugin.xml')); return Promise.resolve(dest); }); } describe('plugin end-to-end', function () { let project; events.on('results', function (res) { results = res; }); beforeAll(() => { return helpers.getFixture('projectWithPlatform').copyTo(preparedProject); }, 20000); beforeEach(function () { project = path.join(tmpDir, `project-${Date.now()}`); // Reset our test project and change into it fs.cpSync(preparedProject, project, { recursive: true }); process.chdir(project); // Reset origCwd before each spec to respect chdirs util._resetOrigCwd(); delete process.env.PWD; spyOn(platforms, 'getPlatformApi').and.callThrough(); spyOn(install, 'runInstall').and.callThrough(); }); afterEach(function () { process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows. fs.rmSync(project, { recursive: true, force: true }); }); it('Test 001 : should successfully add and remove a plugin with no options', function () { return addPlugin(project, path.join(pluginsDir, 'fake1'), pluginId) .then(function () { expect(install.runInstall).toHaveBeenCalled(); expect(platforms.getPlatformApi.calls.count()).toEqual(1); return removePlugin(project, pluginId); }).then(function () { expect(platforms.getPlatformApi.calls.count()).toEqual(2); }); }, 30000); it('Test 004 : should successfully add a plugin using relative path when running from subdir inside of project', function () { // Copy plugin to subdir inside of the project. This is required since path.relative // returns an absolute path when source and dest are on different drives const plugindir = path.join(project, 'custom-plugins/some-plugin-inside-subfolder'); fs.cpSync(path.join(pluginsDir, 'fake1'), plugindir, { recursive: true }); // Create a subdir, where we're going to run cordova from const subdir = path.join(project, 'bin'); fs.mkdirSync(subdir, { recursive: true }); process.chdir(subdir); // Add plugin using relative path return addPlugin(project, path.relative(subdir, plugindir), pluginId) .then(function () { return removePlugin(project, pluginId); }); }, 30000); it('Test 005 : should respect preference default values', function () { const plugin_util = require('../src/cordova/plugin/util'); spyOn(plugin_util, 'mergeVariables').and.returnValue({ REQUIRED: 'NO', REQUIRED_ANDROID: 'NO' }); return addPlugin(project, path.join(pluginsDir, org_test_defaultvariables), org_test_defaultvariables, { cli_variables: { REQUIRED: 'NO', REQUIRED_ANDROID: 'NO' } }) .then(function () { const platformJsonPath = path.join(project, 'plugins', helpers.testPlatform + '.json'); const installed_plugins = require(platformJsonPath).installed_plugins; const defaultPluginPreferences = installed_plugins[org_test_defaultvariables]; expect(defaultPluginPreferences).toBeDefined(); expect(defaultPluginPreferences.DEFAULT).toBe('yes'); expect(defaultPluginPreferences.DEFAULT_ANDROID).toBe('yes'); expect(defaultPluginPreferences.REQUIRED_ANDROID).toBe('NO'); expect(defaultPluginPreferences.REQUIRED).toBe('NO'); return removePlugin(project, org_test_defaultvariables); }); }, 30000); it('Test 006 : should successfully add a plugin when specifying CLI variables', function () { return addPlugin(project, path.join(pluginsDir, org_test_defaultvariables), org_test_defaultvariables, { cli_variables: { REQUIRED: 'yes', REQUIRED_ANDROID: 'yes' } }); }, 30000); it('Test 007 : should not check npm info when using the searchpath flag', function () { mockPluginFetch(project, npmInfoTestPlugin, path.join(pluginsDir, npmInfoTestPlugin)); spyOn(plugin_util, 'info'); return addPlugin(project, npmInfoTestPlugin, npmInfoTestPlugin, { searchpath: pluginsDir }) .then(function () { expect(plugin_util.info).not.toHaveBeenCalled(); const fetchOptions = plugman.fetch.calls.mostRecent().args[2]; expect(fetchOptions.searchpath[0]).toExist(); }); }, 30000); it('Test 008 : should not check npm info when using the noregistry flag', function () { mockPluginFetch(project, npmInfoTestPlugin, path.join(pluginsDir, npmInfoTestPlugin)); spyOn(plugin_util, 'info'); return addPlugin(project, npmInfoTestPlugin, npmInfoTestPlugin, { noregistry: true }) .then(function () { expect(plugin_util.info).not.toHaveBeenCalled(); const fetchOptions = plugman.fetch.calls.mostRecent().args[2]; expect(fetchOptions.noregistry).toBeTruthy(); }); }, 30000); it('Test 009 : should not check npm info when fetching from a Git repository', function () { spyOn(plugin_util, 'info'); return addPlugin(project, testGitPluginRepository, testGitPluginId) .then(function () { expect(plugin_util.info).not.toHaveBeenCalled(); }); }, 30000); it('Test 010 : should select the plugin version based on npm info when fetching from npm', function () { mockPluginFetch(project, npmInfoTestPlugin, path.join(pluginsDir, npmInfoTestPlugin)); spyOn(plugin_util, 'info').and.callThrough(); // Pretend to have cordova-android 5.2.2 installed to force the // expected version outcome for the plugin below const targetVersion = '5.2.2'; const apiFile = path.join(project, 'node_modules/cordova-android/lib/Api.js'); const apiString = fs.readFileSync(apiFile, 'utf8') .replace('const VERSION = require(\'../package\').version;', `const VERSION = '${targetVersion}';`); fs.writeFileSync(apiFile, apiString, 'utf8'); return addPlugin(project, npmInfoTestPlugin, npmInfoTestPlugin) .then(function () { expect(plugin_util.info).toHaveBeenCalled(); const fetchTarget = plugman.fetch.calls.mostRecent().args[0]; expect(fetchTarget).toEqual(npmInfoTestPlugin + '@' + npmInfoTestPluginVersion); }); }, 30000); it('Test 011 : should handle scoped npm packages', function () { mockPluginFetch(project, scopedTestPlugin, path.join(pluginsDir, scopedTestPlugin)); spyOn(plugin_util, 'info').and.returnValue(Promise.resolve({})); return addPlugin(project, scopedTestPlugin, scopedTestPlugin, {}) .then(function () { // Check to make sure that we are at least trying to get the correct package. // This package is not published to npm, so we can't truly do end-to-end tests expect(plugin_util.info).toHaveBeenCalledWith([scopedTestPlugin]); const fetchTarget = plugman.fetch.calls.mostRecent().args[0]; expect(fetchTarget).toEqual(scopedTestPlugin); }); }, 30000); it('Test 012 : should handle scoped npm packages with given version tags', function () { const scopedPackage = scopedTestPlugin + '@latest'; mockPluginFetch(project, scopedTestPlugin, path.join(pluginsDir, scopedTestPlugin)); spyOn(plugin_util, 'info'); return addPlugin(project, scopedPackage, scopedTestPlugin, {}) .then(function () { expect(plugin_util.info).not.toHaveBeenCalled(); const fetchTarget = plugman.fetch.calls.mostRecent().args[0]; expect(fetchTarget).toEqual(scopedPackage); }); }, 30000); it('Test 013 : should be able to add and remove scoped npm packages without screwing up everything', () => { mockPluginFetch(project, scopedTestPlugin, path.join(pluginsDir, scopedTestPlugin)); spyOn(plugin_util, 'info').and.returnValue(Promise.resolve({})); return addPlugin(project, scopedTestPlugin, scopedTestPlugin, {}) .then(() => { expect(plugin_util.info).toHaveBeenCalledWith([scopedTestPlugin]); const fetchTarget = plugman.fetch.calls.mostRecent().args[0]; expect(fetchTarget).toEqual(scopedTestPlugin); return removePlugin(project, scopedTestPlugin); }); }, 30000); it('Test 014 : should run plugin (un)install hooks with correct opts', async () => { const testPluginInstalledPath = path.join(project, 'plugins', npmInfoTestPlugin); const expectedOpts = jasmine.objectContaining({ cordova: { platforms: [helpers.testPlatform], plugins: [npmInfoTestPlugin], version: require('../package').version }, plugin: { id: npmInfoTestPlugin, platform: helpers.testPlatform, dir: testPluginInstalledPath, pluginInfo: jasmine.objectContaining({ id: npmInfoTestPlugin, dir: testPluginInstalledPath }) }, projectRoot: project }); mockPluginFetch(project, npmInfoTestPlugin, path.join(pluginsDir, npmInfoTestPlugin)); spyOn(HooksRunner.prototype, 'fire').and.callThrough(); await cordova.plugin('add', npmInfoTestPlugin); await cordova.plugin('rm', npmInfoTestPlugin); HooksRunner.prototype.fire.calls.allArgs() .filter(([hook]) => /_plugin_(un)?install$/.test(hook)) .forEach(([hook, opts]) => { expect(opts) .withContext(`${hook} hook options`) .toEqual(expectedOpts); }); }, 20 * 1000); }); ================================================ FILE: integration-tests/plugman_fetch.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const rewire = require('rewire'); const fetch = rewire('../src/plugman/fetch'); const metadata = require('../src/plugman/util/metadata'); const temp = path.join(os.tmpdir(), 'plugman', 'fetch'); const plugins_dir = path.join(__dirname, '..', 'spec', 'plugman', 'plugins'); let test_plugin = path.join(plugins_dir, 'org.test.plugins.childbrowser'); const test_pkgjson_plugin = path.join(plugins_dir, 'pkgjson-test-plugin'); const test_plugin_searchpath = path.join(test_plugin, '..'); const test_plugin_id = 'org.test.plugins.childbrowser'; const test_plugin_version = '0.6.0'; const { asymmetricMatchers: { pathNormalizingTo } } = require('../spec/helpers'); describe('fetch', function () { describe('local plugins', function () { let sym; beforeEach(function () { fs.rmSync(temp, { recursive: true, force: true }); spyOn(fs, 'rmSync'); sym = spyOn(fs, 'symlinkSync'); spyOn(fs, 'cpSync').and.callThrough(); spyOn(metadata, 'save_fetch_metadata'); const fetchSpy = jasmine.createSpy('fetch') .and.callFake(x => Promise.resolve(x)); fetch.__set__({ localPlugins: null, fetch: fetchSpy }); }); it('Test 001 : should copy locally-available plugin to plugins directory', function () { return fetch(test_plugin, temp).then(function () { expect(fs.cpSync).toHaveBeenCalledWith(test_plugin, path.join(temp, test_plugin_id), jasmine.objectContaining({ dereference: true })); }); }); it('Test 008 : should copy locally-available plugin to plugins directory when spaces in path', () => { const testPluginWithSpace = path.join(temp, 'folder with space/org.test.plugins.childbrowser'); fs.cpSync(test_plugin, testPluginWithSpace, { recursive: true }); fs.cpSync.calls.reset(); return fetch(testPluginWithSpace, temp).then(() => { expect(fs.cpSync).toHaveBeenCalledWith(testPluginWithSpace, path.join(temp, test_plugin_id), jasmine.any(Object)); }); }); it('Test 002 : should copy locally-available plugin to plugins directory when adding a plugin with searchpath argument', function () { return fetch(test_plugin_id, temp, { searchpath: test_plugin_searchpath }).then(function () { expect(fs.cpSync).toHaveBeenCalledWith( pathNormalizingTo(test_plugin), path.join(temp, test_plugin_id), jasmine.objectContaining({ dereference: true }) ); }); }); it('Test 003 : should create a symlink if used with `link` param', function () { return fetch(test_plugin, temp, { link: true }).then(function () { expect(sym).toHaveBeenCalledWith(test_plugin, path.join(temp, test_plugin_id), 'junction'); }); }); it('Test 004 : should fail when the expected ID doesn\'t match', function () { return fetch(test_plugin, temp, { expected_id: 'wrongID' }) .then(function () { expect('this call').toBe('fail'); }, function (err) { expect('' + err).toContain('Expected plugin to have ID "wrongID" but got'); }); }); it('Test 005 : should succeed when the expected ID is correct', function () { return fetch(test_plugin, temp, { expected_id: test_plugin_id }).then(function () { expect().nothing(); }); }); it('Test 006 : should fail when the expected ID with version specified doesn\'t match', function () { return fetch(test_plugin, temp, { expected_id: test_plugin_id + '@wrongVersion' }) .then(function () { expect('this call').toBe('fail'); }, function (err) { expect('' + err).toContain('to satisfy version "wrongVersion" but got'); }); }); it('Test 007 : should succeed when the plugin version specified is correct', function () { const exp_id = test_plugin_id + '@' + test_plugin_version; return fetch(test_plugin, temp, { expected_id: exp_id }).then(function () { expect().nothing(); }); }); it('Test 027 : should copy locally-available plugin to plugins directory', function () { return fetch(test_pkgjson_plugin, temp).then(function () { expect(fs.cpSync).toHaveBeenCalledWith(test_pkgjson_plugin, path.join(temp, 'pkgjson-test-plugin'), jasmine.objectContaining({ dereference: true })); expect(fetch.__get__('fetch')).toHaveBeenCalledTimes(1); }); }); it('Test 028 : should fail when locally-available plugin is missing pacakge.json', function () { test_plugin = path.join(plugins_dir, 'org.test.androidonly'); return expectAsync( fetch(test_plugin, temp) ).toBeRejectedWithError(/needs a valid package\.json/); }); }); describe('fetch recursive error CB-8809', function () { const srcDir = path.join(plugins_dir, 'recursivePlug'); const appDir = path.join(plugins_dir, 'recursivePlug', 'demo'); fetch.__set__('fetch', function (pluginDir) { return Promise.resolve(pluginDir); }); it('Test 021 : should skip copy to avoid recursive error', function () { spyOn(fs, 'cpSync'); return fetch(srcDir, appDir).then(function () { expect(fs.cpSync).not.toHaveBeenCalled(); }); }); }); }); ================================================ FILE: integration-tests/plugman_uninstall.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const rewire = require('rewire'); const { PluginInfo, events } = require('cordova-common'); const common = require('../spec/common'); const install = require('../src/plugman/install'); const platforms = require('../src/platforms/platforms'); const { tmpDir: getTmpDir, getFixture } = require('../spec/helpers.js'); const tmpDir = getTmpDir('plugman_uninstall_test'); const projectsPath = path.join(tmpDir, 'projects'); const project = path.join(tmpDir, 'project'); const plugins_install_dir = path.join(project, 'cordova/plugins'); const plugins_dir = path.join(__dirname, '../spec/plugman/plugins'); const plugins = { 'org.test.plugins.dummyplugin': path.join(plugins_dir, 'org.test.plugins.dummyplugin'), A: path.join(plugins_dir, 'dependencies', 'A'), C: path.join(plugins_dir, 'dependencies', 'C') }; function setupProject (name) { const projectPath = path.join(projectsPath, name); fs.cpSync(projectPath, project, { recursive: true }); } describe('plugman/uninstall', () => { let uninstall, emit; beforeAll(() => { const project1 = path.join(projectsPath, 'uninstall.test'); const project2 = path.join(projectsPath, 'uninstall.test2'); const project3 = path.join(projectsPath, 'uninstall.test3'); return Promise.resolve() .then(_ => getFixture('androidApp').copyTo(project1)) .then(_ => getFixture('androidApp').copyTo(project2)) .then(_ => getFixture('androidApp').copyTo(project3)) .then(function () { return install('android', project1, plugins['org.test.plugins.dummyplugin']); }).then(function () { return install('android', project1, plugins.A); }).then(function () { return install('android', project2, plugins.C); }).then(function () { return install('android', project2, plugins.A); }).then(function () { return install('android', project3, plugins.A); }).then(function () { return install('android', project3, plugins.C); }).then(function (result) { expect(result).toEqual(true); }); }, 20000); beforeEach(() => { uninstall = rewire('../src/plugman/uninstall'); uninstall.__set__('npmUninstall', jasmine.createSpy().and.returnValue(Promise.resolve())); emit = spyOn(events, 'emit'); }); afterEach(() => { // Just so everything fails if someone does not setup their project fs.rmSync(project, { recursive: true, force: true }); }); afterAll(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); describe('uninstallPlatform', function () { const dummy_id = 'org.test.plugins.dummyplugin'; beforeEach(function () { setupProject('uninstall.test'); }); describe('success', function () { it('Test 002 : should get PlatformApi instance for platform and invoke its\' removePlugin method', function () { const platformApi = { removePlugin: jasmine.createSpy('removePlugin').and.returnValue(Promise.resolve()) }; const getPlatformApi = spyOn(platforms, 'getPlatformApi').and.returnValue(platformApi); return uninstall.uninstallPlatform('android', project, dummy_id) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('android', project); expect(platformApi.removePlugin).toHaveBeenCalled(); }); }); it('Test 003 : should return propagate value returned by PlatformApi removePlugin method', function () { const platformApi = { removePlugin: jasmine.createSpy('removePlugin') }; spyOn(platforms, 'getPlatformApi').and.returnValue(platformApi); const existsSyncOrig = fs.existsSync; spyOn(fs, 'existsSync').and.callFake(function (file) { if (file.indexOf(dummy_id) >= 0) return true; return existsSyncOrig.call(fs, file); }); const fakeProvider = jasmine.createSpyObj('fakeProvider', ['get']); const dummyPluginInfo = new PluginInfo(plugins['org.test.plugins.dummyplugin']); fakeProvider.get.and.returnValue(dummyPluginInfo); function validateReturnedResultFor (values, expectedResult) { return values.reduce(function (promise, value) { return promise .then(function () { platformApi.removePlugin.and.returnValue(Promise.resolve(value)); return uninstall.uninstallPlatform('android', project, dummy_id, null, { pluginInfoProvider: fakeProvider, platformVersion: '9.9.9' }); }) .then(function (result) { expect(!!result).toEqual(expectedResult); }, function (err) { expect(err).toBeUndefined(); }); }, Promise.resolve()); } return validateReturnedResultFor([true, {}, [], 'foo', function () {}], true) .then(function () { return validateReturnedResultFor([false, null, undefined, ''], false); }); }); it('Test 014 : should uninstall dependent plugins', function () { return uninstall.uninstallPlatform('android', project, 'A') .then(function (result) { expect(emit).toHaveBeenCalledWith('log', 'Uninstalling 2 dependent plugins.'); }); }); }); describe('failure ', function () { it('Test 004 : should throw if platform is unrecognized', function () { return expectAsync( uninstall.uninstallPlatform('atari', project, 'SomePlugin') ).toBeRejectedWithError('Platform "atari" not supported.'); }); it('Test 005 : should throw if plugin is missing', function () { return expectAsync( uninstall.uninstallPlatform('android', project, 'SomePluginThatDoesntExist') ).toBeRejectedWithError( 'Plugin "SomePluginThatDoesntExist" not found. Already uninstalled?' ); }); }); }); describe('uninstallPlugin', function () { describe('with dependencies', function () { it('Test 006 : should delete all dependent plugins', function () { setupProject('uninstall.test'); return uninstall.uninstallPlugin('A', plugins_install_dir) .then(function (result) { const del = common.spy.getDeleted(emit); expect(del).toEqual([ 'Deleted plugin "C"', 'Deleted plugin "D"', 'Deleted plugin "A"' ]); }); }); it('Test 007 : should fail if plugin is a required dependency', function () { setupProject('uninstall.test'); return expectAsync( uninstall.uninstallPlugin('C', plugins_install_dir) ).toBeRejectedWithError( 'Plugin "C" is required by (A) and cannot be removed (hint: use -f or --force)' ); }); it('Test 008 : allow forcefully removing a plugin', function () { setupProject('uninstall.test'); return uninstall.uninstallPlugin('C', plugins_install_dir, { force: true }) .then(function () { const del = common.spy.getDeleted(emit); expect(del).toEqual(['Deleted plugin "C"']); }); }); it('Test 009 : never remove top level plugins if they are a dependency', function () { setupProject('uninstall.test2'); return uninstall.uninstallPlugin('A', plugins_install_dir) .then(function () { const del = common.spy.getDeleted(emit); expect(del).toEqual([ 'Deleted plugin "D"', 'Deleted plugin "A"' ]); }); }); it('Test 010 : should not remove dependent plugin if it was installed after as top-level', function () { setupProject('uninstall.test3'); return uninstall.uninstallPlugin('A', plugins_install_dir) .then(function () { const del = common.spy.getDeleted(emit); expect(del).toEqual([ 'Deleted plugin "D"', 'Deleted plugin "A"' ]); }); }); }); }); describe('uninstall', function () { beforeEach(() => { setupProject('uninstall.test'); }); describe('failure', function () { it('Test 011 : should throw if platform is unrecognized', function () { return expectAsync( uninstall('atari', project, 'SomePlugin') ).toBeRejectedWithError('Platform "atari" not supported.'); }); it('Test 012 : should throw if plugin is missing', function () { return expectAsync( uninstall('android', project, 'SomePluginThatDoesntExist') ).toBeRejectedWithError( 'Plugin "SomePluginThatDoesntExist" not found. Already uninstalled?' ); }); }); }); describe('end', function () { // TODO this was some test/teardown hybrid. // We should either add more expectations or get rid of it it('Test 013 : end', function () { setupProject('uninstall.test'); return uninstall('android', project, plugins['org.test.plugins.dummyplugin']) .then(function () { // Fails... A depends on return uninstall('android', project, plugins.C); }).catch(function (err) { expect(err.stack).toMatch(/The plugin 'C' is required by \(A\), skipping uninstallation./); }).then(function () { // dependencies on C,D ... should this only work with --recursive? prompt user..? return uninstall('android', project, plugins.A); }); }); }); }); ================================================ FILE: licence_checker.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. ignored-packages: - spdx-exceptions@2.5.0 ================================================ FILE: package.json ================================================ { "author": "Apache Software Foundation", "name": "cordova-lib", "license": "Apache-2.0", "description": "Apache Cordova tools core lib and API", "version": "13.0.1-dev", "repository": "github:apache/cordova-lib", "bugs": "https://github.com/apache/cordova-lib/issues", "main": "cordova-lib.js", "engines": { "node": ">=20.17.0 || >=22.9.0" }, "dependencies": { "cordova-common": "^6.0.0", "cordova-fetch": "^5.0.0", "detect-indent": "^6.1.0", "detect-newline": "^3.1.0", "execa": "^5.1.1", "globby": "^11.1.0", "semver": "^7.7.2", "stringify-package": "^1.0.1", "write-file-atomic": "^7.0.0" }, "devDependencies": { "@cordova/eslint-config": "^6.0.0", "c8": "^10.1.3", "cordova-android": "^13.0.0", "elementtree": "^0.1.7", "jasmine": "^5.10.0", "jasmine-spec-reporter": "^7.0.0", "rewire": "^9.0.1" }, "scripts": { "test": "npm run lint && npm run test:coverage", "test:unit": "jasmine \"spec/**/*.spec.js\"", "test:e2e": "jasmine \"integration-tests/**/*.spec.js\"", "test:all": "npm run test:unit && npm run test:e2e", "test:coverage": "c8 npm run test:all", "lint": "eslint ." }, "c8": { "all": true, "exclude": [ "templates/", "integration-tests/", "coverage/", "spec/" ], "reporter": [ "lcov", "text" ] } } ================================================ FILE: spec/common.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ let common = {}; module.exports = common = { spy: { getInstall: function (emitSpy) { return common.spy.startsWith(emitSpy, 'Install start'); }, getDeleted: function (emitSpy) { return common.spy.startsWith(emitSpy, 'Deleted'); }, startsWith: function (emitSpy, string) { return common.spy.getReceivedMessages(emitSpy) .filter(msg => msg.startsWith(string)); }, getReceivedMessages (emitSpy) { return emitSpy.calls.allArgs() .map(([, msg]) => msg); } } }; ================================================ FILE: spec/cordova/build.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const platforms = require('../../src/platforms/platforms'); const HooksRunner = require('../../src/hooks/HooksRunner'); const util = require('../../src/cordova/util'); describe('build command', function () { const project_dir = '/some/path'; let cordovaBuild, cordovaPrepare, cordovaCompile; beforeEach(function () { spyOn(util, 'isCordova').and.returnValue(project_dir); spyOn(util, 'cdProjectRoot').and.returnValue(project_dir); spyOn(util, 'listPlatforms').and.returnValue(Object.keys(platforms)); spyOn(HooksRunner.prototype, 'fire').and.returnValue(Promise.resolve()); cordovaBuild = rewire('../../src/cordova/build'); cordovaPrepare = jasmine.createSpy('cordovaPrepare').and.returnValue(Promise.resolve()); cordovaCompile = jasmine.createSpy('cordovaCompile').and.returnValue(Promise.resolve()); cordovaBuild.__set__({ cordovaPrepare, cordovaCompile }); }); describe('failure', function () { it('Test 001 : should not run inside a project with no platforms', function () { util.listPlatforms.and.returnValue([]); return expectAsync( cordovaBuild() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); }); it('Test 002 : should not run outside of a Cordova-based project', function () { util.isCordova.and.returnValue(false); return expectAsync( cordovaBuild() ).toBeRejectedWithError( 'Current working directory is not a Cordova-based project.' ); }); }); describe('success', function () { it('Test 003 : should run inside a Cordova-based project with at least one added platform and call both prepare and compile', function () { return cordovaBuild(['android', 'ios']).then(function () { const opts = Object({ platforms: ['android', 'ios'], verbose: false, options: Object({ }) }); expect(cordovaPrepare).toHaveBeenCalledWith(opts); expect(cordovaCompile).toHaveBeenCalledWith(opts); }); }); it('Test 004 : should pass down options', function () { return cordovaBuild({ platforms: ['android'], options: { release: true } }).then(function () { const opts = { platforms: ['android'], options: { release: true }, verbose: false }; expect(cordovaPrepare).toHaveBeenCalledWith(opts); expect(cordovaCompile).toHaveBeenCalledWith(opts); }); }); }); describe('hooks', function () { describe('when platforms are added', function () { it('Test 006 : should fire before hooks through the hooker module', function () { return cordovaBuild(['android', 'ios']).then(function () { expect(HooksRunner.prototype.fire.calls.argsFor(0)) .toEqual(['before_build', { verbose: false, platforms: ['android', 'ios'], options: {} }]); }); }); it('Test 007 : should fire after hooks through the hooker module', function () { return cordovaBuild('android').then(function () { expect(HooksRunner.prototype.fire.calls.argsFor(1)) .toEqual(['after_build', { platforms: ['android'], verbose: false, options: {} }]); }); }); }); describe('with no platforms added', function () { it('Test 008 : should not fire the hooker', function () { util.listPlatforms.and.returnValue([]); return expectAsync( cordovaBuild() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); }); }); }); }); ================================================ FILE: spec/cordova/compile.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova = require('../../src/cordova/cordova'); const platforms = require('../../src/platforms/platforms'); const HooksRunner = require('../../src/hooks/HooksRunner'); const util = require('../../src/cordova/util'); const supported_platforms = Object.keys(platforms).filter(function (p) { return p !== 'www'; }); describe('compile command', function () { let is_cordova; let list_platforms; let fire; let platformApi; let getPlatformApi; const project_dir = '/some/path'; beforeEach(function () { is_cordova = spyOn(util, 'isCordova').and.returnValue(project_dir); spyOn(util, 'cdProjectRoot').and.returnValue(project_dir); list_platforms = spyOn(util, 'listPlatforms').and.returnValue(supported_platforms); fire = spyOn(HooksRunner.prototype, 'fire').and.returnValue(Promise.resolve()); platformApi = { build: jasmine.createSpy('build').and.returnValue(Promise.resolve()) }; getPlatformApi = spyOn(platforms, 'getPlatformApi').and.returnValue(platformApi); }); describe('failure', function () { it('Test 001 : should not run inside a Cordova-based project with no added platforms by calling util.listPlatforms', function () { list_platforms.and.returnValue([]); return expectAsync( cordova.compile() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); }); it('Test 002 : should not run outside of a Cordova-based project', function () { is_cordova.and.returnValue(false); return expectAsync( cordova.compile() ).toBeRejectedWithError(); }); }); describe('success', function () { it('Test 003 : should run inside a Cordova-based project with at least one added platform and shell out to build', function () { return cordova.compile(['android', 'ios']) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('android'); expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.build).toHaveBeenCalled(); }); }); it('Test 004 : should pass down optional parameters', function () { return cordova.compile({ platforms: ['blackberry10'], options: { release: true } }) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('blackberry10'); expect(platformApi.build).toHaveBeenCalledWith({ release: true }); }); }); }); describe('hooks', function () { describe('when platforms are added', function () { it('Test 006 : should fire before hooks through the hooker module', function () { return cordova.compile(['android', 'ios']) .then(function () { expect(fire.calls.argsFor(0)).toEqual(['before_compile', { verbose: false, platforms: ['android', 'ios'], options: {} }]); }); }); it('Test 007 : should fire after hooks through the hooker module', function () { return cordova.compile('android') .then(function () { expect(fire.calls.argsFor(1)).toEqual(['after_compile', { verbose: false, platforms: ['android'], options: {} }]); }); }); }); describe('with no platforms added', function () { it('Test 008 : should not fire the hooker', async function () { list_platforms.and.returnValue([]); await expectAsync( cordova.compile() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); expect(fire).not.toHaveBeenCalled(); }); }); }); }); ================================================ FILE: spec/cordova/cordova-lib.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ describe('cordova lib main export', () => { it('should be available', function () { // Verify that cordova-lib.js can be loaded expect(require('../../cordova-lib')).toBeDefined(); }); }); ================================================ FILE: spec/cordova/emulate.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const { events } = require('cordova-common'); const platforms = require('../../src/platforms/platforms'); const HooksRunner = require('../../src/hooks/HooksRunner'); const util = require('../../src/cordova/util'); const supported_platforms = Object.keys(platforms); describe('emulate command', function () { const project_dir = '/some/path'; let cordovaEmulate, cordovaPrepare, platformApi, getPlatformApi, targets; beforeEach(function () { spyOn(util, 'isCordova').and.returnValue(project_dir); spyOn(util, 'cdProjectRoot').and.returnValue(project_dir); spyOn(util, 'listPlatforms').and.returnValue(supported_platforms); spyOn(HooksRunner.prototype, 'fire').and.returnValue(Promise.resolve()); spyOn(events, 'emit'); cordovaEmulate = rewire('../../src/cordova/emulate'); cordovaPrepare = jasmine.createSpy('cordovaPrepare').and.returnValue(Promise.resolve()); targets = jasmine.createSpy('targets').and.returnValue(Promise.resolve()); cordovaEmulate.__set__({ cordovaPrepare, targets }); platformApi = { run: jasmine.createSpy('run').and.returnValue(Promise.resolve()), build: jasmine.createSpy('build').and.returnValue(Promise.resolve()), listTargets: jasmine.createSpy('listTargets').and.returnValue(Promise.resolve()) }; getPlatformApi = spyOn(platforms, 'getPlatformApi').and.returnValue(platformApi); }); describe('failure', function () { it('Test 001 : should not run inside a Cordova-based project with no added platforms by calling util.listPlatforms', function () { util.listPlatforms.and.returnValue([]); return expectAsync( cordovaEmulate() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); }); it('Test 002 : should not run outside of a Cordova-based project', function () { util.isCordova.and.returnValue(false); return expectAsync( cordovaEmulate() ).toBeRejectedWithError(); }); }); describe('list', function () { it('should warn if platforms are not specified', function () { const result = cordovaEmulate({ platforms: [], options: { list: true } }); expect(result).toBeFalsy(); expect(events.emit).toHaveBeenCalledWith('warn', 'A platform must be provided when using the "--list" flag.'); }); it('should try to use the Platform API to list emulator targets', function () { return cordovaEmulate({ platforms: ['ios'], options: { list: true } }) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.listTargets).toHaveBeenCalledWith(jasmine.objectContaining({ options: jasmine.objectContaining({ device: false, emulator: true }) })); }); }); it('should fall back to the pre-Platform API targets', function () { delete platformApi.listTargets; return cordovaEmulate({ platforms: ['ios'], options: { list: true } }) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(targets).toHaveBeenCalledWith(jasmine.objectContaining({ options: jasmine.objectContaining({ device: false, emulator: true }) })); expect(events.emit).toHaveBeenCalledWith('warn', 'Please update to the latest platform release to ensure uninterrupted fetching of target lists.'); }); }); }); describe('success', function () { it('Test 003 : should run inside a Cordova-based project with at least one added platform and call prepare and shell out to the emulate script', function () { return cordovaEmulate(['android', 'ios']) .then(function () { expect(cordovaPrepare).toHaveBeenCalledWith(jasmine.objectContaining({ platforms: ['android', 'ios'] })); expect(getPlatformApi).toHaveBeenCalledWith('android'); expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.build).toHaveBeenCalled(); expect(platformApi.run).toHaveBeenCalled(); }); }); it('Test 004 : should pass down options', function () { return cordovaEmulate({ platforms: ['ios'], options: { optionTastic: true } }) .then(function () { expect(cordovaPrepare).toHaveBeenCalledWith(jasmine.objectContaining({ platforms: ['ios'] })); expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.build).toHaveBeenCalledWith({ device: false, emulator: true, optionTastic: true }); expect(platformApi.run).toHaveBeenCalledWith({ device: false, emulator: true, optionTastic: true, nobuild: true }); }); }); it('Test 005 : should skip preparing if --noprepare is passed', function () { return cordovaEmulate({ platforms: ['ios'], options: { noprepare: true } }) .then(function () { expect(cordovaPrepare).not.toHaveBeenCalledWith(jasmine.objectContaining({ platforms: ['ios'] })); expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.build).toHaveBeenCalledWith({ device: false, emulator: true, noprepare: true }); expect(platformApi.run).toHaveBeenCalledWith({ device: false, emulator: true, noprepare: true, nobuild: true }); }); }); describe('run parameters should not be altered by intermediate build command', function () { beforeEach(function () { platformApi.build.and.callFake(opts => { opts.couldBeModified = 'insideBuild'; return Promise.resolve(); }); }); it('Test 006 : should leave parameters unchanged', function () { const baseOptions = { password: '1q1q', device: false, emulator: true }; const expectedRunOptions = Object.assign({ nobuild: true }, baseOptions); const expectedBuildOptions = Object.assign({ couldBeModified: 'insideBuild' }, baseOptions); return cordovaEmulate({ platforms: ['blackberry10'], options: { password: '1q1q' } }) .then(function () { expect(cordovaPrepare).toHaveBeenCalledWith({ platforms: ['blackberry10'], options: expectedBuildOptions, verbose: false }); expect(platformApi.build).toHaveBeenCalledWith(expectedBuildOptions); expect(platformApi.run).toHaveBeenCalledWith(expectedRunOptions); }); }); }); it('Test 007 : should call platform\'s build method', function () { return cordovaEmulate({ platforms: ['blackberry10'] }) .then(function () { expect(cordovaPrepare).toHaveBeenCalled(); expect(platformApi.build).toHaveBeenCalledWith({ device: false, emulator: true }); expect(platformApi.run).toHaveBeenCalledWith(jasmine.objectContaining({ nobuild: true })); }); }); it('Test 008 : should not call build if --nobuild option is passed', function () { return cordovaEmulate({ platforms: ['blackberry10'], options: { nobuild: true } }) .then(function () { expect(cordovaPrepare).toHaveBeenCalled(); expect(platformApi.build).not.toHaveBeenCalled(); expect(platformApi.run).toHaveBeenCalledWith(jasmine.objectContaining({ nobuild: true })); }); }); }); describe('hooks', function () { describe('when platforms are added', function () { it('Test 009 : should fire before hooks through the hooker module', function () { return cordovaEmulate(['android', 'ios']) .then(function () { expect(HooksRunner.prototype.fire).toHaveBeenCalledWith('before_emulate', jasmine.objectContaining({ verbose: false, platforms: ['android', 'ios'], options: jasmine.any(Object) })); }); }); it('Test 010 : should fire after hooks through the hooker module', function () { return cordovaEmulate('android') .then(function () { expect(HooksRunner.prototype.fire).toHaveBeenCalledWith('after_emulate', jasmine.objectContaining({ verbose: false, platforms: ['android'], options: jasmine.any(Object) })); }); }); }); describe('with no platforms added', function () { it('Test 011 : should not fire the hooker', async function () { util.listPlatforms.and.returnValue([]); await expectAsync( cordovaEmulate() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); expect(HooksRunner.prototype.fire).not.toHaveBeenCalled(); }); }); }); }); ================================================ FILE: spec/cordova/fixtures/basePkgJson/config.xml ================================================ TestBase A sample Apache Cordova application that responds to the deviceready event. Apache Cordova Team ================================================ FILE: spec/cordova/fixtures/basePkgJson/package.json ================================================ { "name": "testbase", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/cordova/fixtures/basePkgJson/plugins/.svn ================================================ ================================================ FILE: spec/cordova/fixtures/basePkgJson/www/css/index.css ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ * { -webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adjust last value opacity 0 to 1.0 */ } body { -webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */ -webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */ -webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */ background-color:#E4E4E4; background-image:linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); background-image:-webkit-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); background-image:-ms-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); background-image:-webkit-gradient( linear, left top, left bottom, color-stop(0, #A7A7A7), color-stop(0.51, #E4E4E4) ); background-attachment:fixed; font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif; font-size:12px; height:100%; margin:0px; padding:0px; text-transform:uppercase; width:100%; } /* Portrait layout (default) */ .app { background:url(../img/logo.png) no-repeat center top; /* 170px x 200px */ position:absolute; /* position in the center of the screen */ left:50%; top:50%; height:50px; /* text area height */ width:225px; /* text area width */ text-align:center; padding:180px 0px 0px 0px; /* image height is 200px (bottom 20px are overlapped with text) */ margin:-115px 0px 0px -112px; /* offset vertical: half of image height and text area height */ /* offset horizontal: half of text area width */ } /* Landscape layout (with min-width) */ @media screen and (min-aspect-ratio: 1/1) and (min-width:400px) { .app { background-position:left center; padding:75px 0px 75px 170px; /* padding-top + padding-bottom + text area = image height */ margin:-90px 0px 0px -198px; /* offset vertical: half of image height */ /* offset horizontal: half of image width and text area width */ } } h1 { font-size:24px; font-weight:normal; margin:0px; overflow:visible; padding:0px; text-align:center; } .event { border-radius:4px; -webkit-border-radius:4px; color:#FFFFFF; font-size:12px; margin:0px 30px; padding:2px 0px; } .event.listening { background-color:#333333; display:block; } .event.received { background-color:#4B946A; display:none; } @keyframes fade { from { opacity: 1.0; } 50% { opacity: 0.4; } to { opacity: 1.0; } } @-webkit-keyframes fade { from { opacity: 1.0; } 50% { opacity: 0.4; } to { opacity: 1.0; } } .blink { animation:fade 3000ms infinite; -webkit-animation:fade 3000ms infinite; } ================================================ FILE: spec/cordova/fixtures/basePkgJson/www/index.html ================================================ Hello World

Apache Cordova

================================================ FILE: spec/cordova/fixtures/basePkgJson/www/js/index.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var app = { // Application Constructor initialize: function() { this.bindEvents(); }, // Bind Event Listeners // // Bind any events that are required on startup. Common events are: // 'load', 'deviceready', 'offline', and 'online'. bindEvents: function() { document.addEventListener('deviceready', this.onDeviceReady, false); }, // deviceready Event Handler // // The scope of 'this' is the event. In order to call the 'receivedEvent' // function, we must explicity call 'app.receivedEvent(...);' onDeviceReady: function() { app.receivedEvent('deviceready'); }, // Update DOM on a Received Event receivedEvent: function(id) { var parentElement = document.getElementById(id); var listeningElement = parentElement.querySelector('.listening'); var receivedElement = parentElement.querySelector('.received'); listeningElement.setAttribute('style', 'display:none;'); receivedElement.setAttribute('style', 'display:block;'); console.log('Received Event: ' + id); } }; ================================================ FILE: spec/cordova/fixtures/basePkgJson/www/spec.html ================================================ Jasmine Spec Runner ================================================ FILE: spec/cordova/fixtures/plugins/@cordova/plugin-test-dummy/package.json ================================================ { "name": "@cordova/plugin-test-dummy", "version": "0.0.1", "author": "Apache Software Foundation", "license": "Apache-2.0" } ================================================ FILE: spec/cordova/fixtures/plugins/@cordova/plugin-test-dummy/plugin.xml ================================================ ================================================ FILE: spec/cordova/fixtures/plugins/com.plugin.withhooks/package.json ================================================ { "name": "withhooks", "version": "3.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/cordova/fixtures/plugins/com.plugin.withhooks/plugin.xml ================================================ Plugin with hooks ================================================ FILE: spec/cordova/fixtures/plugins/com.plugin.withhooks/scripts/android/androidBeforeBuild.js ================================================ module.exports = function(context) { var orderLogger = require(require('node:path').join(context.opts.projectRoot, 'scripts', 'orderLogger')); orderLogger.logOrder('26', context); }; ================================================ FILE: spec/cordova/fixtures/plugins/com.plugin.withhooks/scripts/beforeBuild.bat ================================================ @echo off echo 22 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/plugins/com.plugin.withhooks/scripts/beforeBuild.js ================================================ module.exports = function(context) { var orderLogger = require(require('node:path').join(context.opts.projectRoot, 'scripts', 'orderLogger')); orderLogger.logOrder('21', context); }; ================================================ FILE: spec/cordova/fixtures/plugins/com.plugin.withhooks/scripts/beforeBuild.sh ================================================ #!/bin/sh echo 22 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/plugins/com.plugin.withhooks/scripts/windows/windowsBeforeBuild.js ================================================ module.exports = function(context) { var orderLogger = require(require('node:path').join(context.opts.projectRoot, 'scripts', 'orderLogger')); orderLogger.logOrder('24', context); }; ================================================ FILE: spec/cordova/fixtures/plugins/cordova-lib-test-plugin/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: spec/cordova/fixtures/plugins/cordova-lib-test-plugin/NOTICE ================================================ Apache Cordova Copyright 2012 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). ================================================ FILE: spec/cordova/fixtures/plugins/cordova-lib-test-plugin/package.json ================================================ { "name": "cordova-lib-test-plugin", "version": "1.3.1", "description": "Empty plugin used as part of the tests in cordova-lib", "cordova": { "id": "cordova-lib-test-plugin", "platforms": [] }, "repository": { "type": "git", "url": "git://git-wip-us.apache.org/repos/asf/cordova-lib.git" }, "author": "Apache Software Foundation", "license": "Apache-2.0", "engines": { "cordovaDependencies": { "0.0.0": { "cordova-android": "<2.1.0" }, "1.1.2": { "cordova-android": ">=2.1.0 <7.0.0" }, "1.3.0": { "cordova-android": "7.0.0" } } } } ================================================ FILE: spec/cordova/fixtures/plugins/cordova-lib-test-plugin/plugin.xml ================================================ cordova-lib-test-plugin ================================================ FILE: spec/cordova/fixtures/plugins/fake1/package.json ================================================ { "name": "fake1", "version": "1.3.1", "description": "Empty plugin used as part of the tests in cordova-lib", "cordova": { "id": "fake1", "platforms": [] }, "repository": { "type": "git", "url": "git://git-wip-us.apache.org/repos/asf/cordova-lib.git" }, "author": "Apache Software Foundation", "license": "Apache-2.0", "engines": { "cordovaDependencies": { "0.0.0": { "cordova-android": "<2.1.0" }, "1.1.2": { "cordova-android": ">=2.1.0 <7.0.0" }, "1.3.0": { "cordova-android": "7.0.0" } } } } ================================================ FILE: spec/cordova/fixtures/plugins/fake1/plugin.xml ================================================ Fake1 Cordova fake plugin for tests Apache 2.0 cordova,cli,test ================================================ FILE: spec/cordova/fixtures/plugins/org.test.defaultvariables/package.json ================================================ { "name": "org.test.defaultvariables", "version": "1.3.1", "description": "Empty plugin used as part of the tests in cordova-lib", "cordova": { "id": "cordova-lib-test-plugin", "platforms": [] }, "repository": { "type": "git", "url": "git://git-wip-us.apache.org/repos/asf/cordova-lib.git" }, "author": "Apache Software Foundation", "license": "Apache-2.0", "engines": { "cordovaDependencies": { "0.0.0": { "cordova-android": "<2.1.0" }, "1.1.2": { "cordova-android": ">=2.1.0 <7.0.0" }, "1.3.0": { "cordova-android": "7.0.0" } } } } ================================================ FILE: spec/cordova/fixtures/plugins/org.test.defaultvariables/plugin.xml ================================================ Test plugin variables ================================================ FILE: spec/cordova/fixtures/plugins/test/package.json ================================================ { "name": "0", "version": "3.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/cordova/fixtures/plugins/test/plugin.xml ================================================ Test Plugin ================================================ FILE: spec/cordova/fixtures/plugins/test/www/test.js ================================================ ================================================ FILE: spec/cordova/fixtures/projectHooks/android/appAndroidBeforeBuild.bat ================================================ @echo off echo 14 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/projectHooks/android/appAndroidBeforeBuild.js ================================================ module.exports = function(context) { var orderLogger = require(require('node:path').join(context.opts.projectRoot, 'scripts', 'orderLogger')); orderLogger.logOrder('15', context); }; ================================================ FILE: spec/cordova/fixtures/projectHooks/android/appAndroidBeforeBuild.sh ================================================ #!/bin/sh echo 14 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/projectHooks/appBeforeBuild02.js ================================================ module.exports = function(context) { var orderLogger = require(require('node:path').join(context.opts.projectRoot, 'scripts', 'orderLogger')); orderLogger.logOrder('09', context); }; ================================================ FILE: spec/cordova/fixtures/projectHooks/appBeforeBuild1.bat ================================================ @echo off echo 08 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/projectHooks/appBeforeBuild1.sh ================================================ #!/bin/sh echo 08 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/projectHooks/fail.js ================================================ module.exports = () => { throw new Error(); }; ================================================ FILE: spec/cordova/fixtures/projectHooks/orderLogger.js ================================================ module.exports.logOrder = function logOrder(index, context) { var indexWithEOL = index + require('node:os').EOL; var path = require('node:path'); var fs = require('node:fs'); if(context) { fs.appendFileSync(path.join(context.opts.projectRoot, 'hooks_order.txt'), indexWithEOL); } else { fs.appendFileSync('hooks_order.txt', indexWithEOL); } }; ================================================ FILE: spec/cordova/fixtures/projectHooks/windows/appWindowsBeforeBuild.bat ================================================ @echo off echo 11 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/projectHooks/windows/appWindowsBeforeBuild.js ================================================ module.exports = function(context) { var orderLogger = require(require('node:path').join(context.opts.projectRoot, 'scripts', 'orderLogger')); orderLogger.logOrder('12', context); }; ================================================ FILE: spec/cordova/fixtures/projectHooks/windows/appWindowsBeforeBuild.sh ================================================ echo 11 >> hooks_order.txt ================================================ FILE: spec/cordova/fixtures/projects/ProjectMetadata/config.xml ================================================ ProjectMetadata A sample Apache Cordova application that responds to the deviceready event. Apache Cordova Team ================================================ FILE: spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/Api.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // This is NOT a complete API but a stub to test platforms.getPlatformApi const PLATFORM = 'windows'; function Api (platform, rootDir) { this.platform = platform; this.root = rootDir; }; Api.createPlatform = rootDir => new Api(PLATFORM, rootDir); module.exports = Api; ================================================ FILE: spec/cordova/platform/addHelper.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const fs = require('node:fs'); const util = require('node:util'); const events = require('cordova-common').events; const rewire = require('rewire'); const cordova_util = require('../../../src/cordova/util'); const platforms = require('../../../src/platforms'); const plugman = require('../../../src/plugman/plugman'); const fetch_metadata = require('../../../src/plugman/util/metadata'); describe('cordova/platform/addHelper', function () { const projectRoot = '/some/path'; let cfg_parser_mock, fake_platform, fetch_mock, hooks_mock, package_json_mock, platform_addHelper, platform_api_mock, prepare_mock; beforeEach(function () { fake_platform = { platform: 'atari' }; package_json_mock = { cordova: {}, dependencies: {}, devDependencies: {} }; hooks_mock = jasmine.createSpyObj('hooksRunner mock', ['fire']); hooks_mock.fire.and.returnValue(Promise.resolve()); cfg_parser_mock = function () {}; cfg_parser_mock.prototype = jasmine.createSpyObj('config parser mock', [ 'write', 'removeEngine', 'addEngine', 'getHookScripts' ]); fetch_mock = jasmine.createSpy('fetch mock').and.returnValue(Promise.resolve()); prepare_mock = jasmine.createSpy('prepare mock').and.returnValue(Promise.resolve()); const preparePlatforms = jasmine.createSpy('preparePlatforms mock').and.returnValue(Promise.resolve()); prepare_mock.preparePlatforms = preparePlatforms; // `cordova.prepare` is never saved to a variable, so we need to fake `require` platform_addHelper = rewire('../../../src/cordova/platform/addHelper'); const testSubjectRequire = platform_addHelper.__get__('require'); const requireFake = jasmine.createSpy('require', testSubjectRequire).and.callThrough(); requireFake.withArgs('../prepare').and.returnValue(prepare_mock); const getPlatformDetailsFromDir = jasmine.createSpy('getPlatformDetailsFromDir').and.returnValue(Promise.resolve(fake_platform)); platform_addHelper.__set__({ ConfigParser: cfg_parser_mock, fetch: fetch_mock, require: requireFake, getPlatformDetailsFromDir, preparePlatforms }); spyOn(fs, 'mkdirSync'); spyOn(fs, 'existsSync').and.returnValue(false); spyOn(fs, 'readFileSync'); spyOn(fs, 'writeFileSync'); spyOn(cordova_util, 'projectConfig').and.returnValue(path.join(projectRoot, 'config.xml')); spyOn(cordova_util, 'isDirectory').and.returnValue(false); spyOn(cordova_util, 'fixRelativePath').and.callFake(function (input) { return input; }); spyOn(cordova_util, 'isUrl').and.returnValue(false); spyOn(cordova_util, 'removePlatformPluginsJson'); spyOn(platforms, 'hostSupports').and.returnValue(true); spyOn(events, 'emit'); // Fake platform details we will use for our mocks, returned by either // getPlatfromDetailsFromDir (in the local-directory case), or // downloadPlatform (in every other case) spyOn(platform_addHelper, 'downloadPlatform').and.returnValue(Promise.resolve(fake_platform)); spyOn(platform_addHelper, 'getVersionFromConfigFile').and.returnValue(false); spyOn(platform_addHelper, 'installPluginsForNewPlatform').and.returnValue(Promise.resolve()); platform_api_mock = jasmine.createSpyObj('platform api mock', ['createPlatform', 'updatePlatform']); platform_api_mock.createPlatform.and.returnValue(Promise.resolve()); platform_api_mock.updatePlatform.and.returnValue(Promise.resolve()); spyOn(cordova_util, 'getPlatformApiFunction').and.returnValue(platform_api_mock); spyOn(cordova_util, 'requireNoCache').and.returnValue({}); }); describe('error/warning conditions', function () { it('should require specifying at least one platform', async () => { for (const targets of [[], undefined, null]) { await expectAsync(platform_addHelper('add', hooks_mock, projectRoot, targets)) .withContext(`targets = ${util.inspect(targets)}`) .toBeRejectedWithError(/No platform specified\./); } }); it('should log if host OS does not support the specified platform', function () { platforms.hostSupports.and.returnValue(false); return platform_addHelper('add', hooks_mock, projectRoot, ['atari']).then(function () { expect(platforms.hostSupports).toHaveBeenCalled(); expect(events.emit).toHaveBeenCalledWith('warning', jasmine.stringMatching(/WARNING: Applications/)); }); }); it('should throw if platform was already added before adding', function () { fs.existsSync.and.returnValue('/some/path/platforms/ios'); return expectAsync( platform_addHelper('add', hooks_mock, projectRoot, ['ios']) ).toBeRejectedWithError(/already added\./); }); it('should throw if platform was not added before updating', function () { return expectAsync( platform_addHelper('update', hooks_mock, projectRoot, ['atari']) ).toBeRejectedWithError( 'Platform "atari" is not yet added. See `cordova platform list`.' ); }); }); describe('happy path (success conditions)', function () { it('should fire the before_platform_* hook', function () { return platform_addHelper('add', hooks_mock, projectRoot, ['atari']).then(_ => { expect(hooks_mock.fire).toHaveBeenCalledWith('before_platform_add', jasmine.any(Object)); }); }); describe('platform spec inference', function () { it('should retrieve platform details from directories-specified-as-platforms using getPlatformDetailsFromDir', function () { const directory_to_platform = '/path/to/cordova-atari'; cordova_util.isDirectory.and.returnValue(true); fetch_mock.and.returnValue(Promise.resolve(directory_to_platform)); return platform_addHelper('add', hooks_mock, projectRoot, [directory_to_platform], { restoring: true }).then(function () { expect(platform_addHelper.__get__('getPlatformDetailsFromDir')).toHaveBeenCalledWith(directory_to_platform, null); expect(platform_addHelper.downloadPlatform).not.toHaveBeenCalled(); }); }); it('should retrieve platform details from URLs-specified-as-platforms using downloadPlatform', function () { cordova_util.isUrl.and.returnValue(true); const url_to_platform = 'http://github.com/apache/cordova-atari'; return platform_addHelper('add', hooks_mock, projectRoot, [url_to_platform], { restoring: true }).then(function () { expect(platform_addHelper.downloadPlatform).toHaveBeenCalledWith(projectRoot, null, url_to_platform, jasmine.any(Object)); }); }); it('should use spec from config.xml if package.json does not contain dependency for platform', function () { package_json_mock.dependencies = {}; cordova_util.requireNoCache.and.returnValue(package_json_mock); fs.existsSync.and.callFake(function (filePath) { return path.basename(filePath) === 'package.json'; }); return platform_addHelper('add', hooks_mock, projectRoot, ['windows'], { restoring: true }).then(function () { expect(platform_addHelper.getVersionFromConfigFile).toHaveBeenCalled(); }); }); it('should attempt to retrieve from config.xml if exists and package.json does not', function () { return platform_addHelper('add', hooks_mock, projectRoot, ['atari'], { restoring: true }).then(function () { expect(platform_addHelper.getVersionFromConfigFile).toHaveBeenCalled(); }); }); it('should invoke fetch if provided as an option and spec is a directory', function () { cordova_util.isDirectory.and.returnValue(projectRoot); cordova_util.fixRelativePath.and.returnValue(projectRoot); spyOn(path, 'resolve').and.callThrough(); return platform_addHelper('add', hooks_mock, projectRoot, ['ios'], { save: true, restoring: true }).then(function () { expect(fetch_mock).toHaveBeenCalled(); }); }); }); describe('platform api invocation', function () { it('should invoke the createPlatform platform API method when adding a platform, providing destination location, parsed config file and platform detail options as arguments', function () { return platform_addHelper('add', hooks_mock, projectRoot, ['ios'], { save: true, restoring: true }).then(function (result) { expect(platform_api_mock.createPlatform).toHaveBeenCalled(); }); }); it('should invoke the update platform API method when updating a platform, providing destination location and plaform detail options as arguments', function () { cordova_util.isDirectory.and.returnValue(true); fs.existsSync.and.returnValue(true); return platform_addHelper('update', hooks_mock, projectRoot, ['ios'], { restoring: true }).then(function (result) { expect(platform_api_mock.updatePlatform).toHaveBeenCalled(); }); }); }); describe('after platform api invocation', function () { describe('when the restoring option is not provided', function () { it('should invoke preparePlatforms twice (?!?), once before installPluginsForNewPlatforms and once after... ?!', function () { const preparePlatforms = platform_addHelper.__get__('preparePlatforms'); return platform_addHelper('add', hooks_mock, projectRoot, ['atari'], { save: true }).then(function (result) { expect(preparePlatforms).toHaveBeenCalledWith(['atari'], '/some/path', { searchpath: undefined }); }); }); }); it('should invoke the installPluginsForNewPlatforms method in the platform-add case', function () { return platform_addHelper('add', hooks_mock, projectRoot, ['atari'], { save: true, restoring: true }).then(function (result) { expect(platform_addHelper.installPluginsForNewPlatform).toHaveBeenCalled(); }); }); describe('if the project contains a package.json', function () { it('should use getVersionFromPackageJson to determine platform version', async () => { const getVersionFromPackageJson = jasmine.createSpy('getVersionFromPackageJson') .and.returnValue('1.2.3'); platform_addHelper.__set__({ getVersionFromPackageJson, readPackageJsonIfExists: () => package_json_mock }); await platform_addHelper('add', hooks_mock, projectRoot, ['ios'], { save: true, restoring: true }); expect(getVersionFromPackageJson).toHaveBeenCalledWith('ios', package_json_mock); expect(platform_addHelper.getVersionFromConfigFile).not.toHaveBeenCalled(); }); it('should write out the platform just added/updated to the cordova.platforms property of package.json', function () { fs.readFileSync.and.returnValue('file'); fs.existsSync.and.callFake(function (filePath) { if (path.basename(filePath) === 'package.json') { return true; } else { return false; } }); package_json_mock.cordova = { platforms: ['ios'] }; cordova_util.requireNoCache.and.returnValue(package_json_mock); return platform_addHelper('add', hooks_mock, projectRoot, ['android'], { save: true, restoring: true }).then(function (result) { expect(fs.writeFileSync).toHaveBeenCalled(); }); }); it('should only write the package.json file if it was modified', function () { package_json_mock.cordova = { platforms: ['ios'] }; cordova_util.requireNoCache.and.returnValue(package_json_mock); return platform_addHelper('add', hooks_mock, projectRoot, ['ios'], { save: true, restoring: true }).then(function (result) { expect(fs.writeFileSync).not.toHaveBeenCalled(); }); }); it('should file the after_platform_* hook', function () { return platform_addHelper('add', hooks_mock, projectRoot, ['atari'], { save: true, restoring: true }).then(function (result) { expect(hooks_mock.fire).toHaveBeenCalledWith('before_platform_add', Object({ save: true, restoring: true })); }); }); }); }); }); describe('downloadPlatform', function () { beforeEach(function () { platform_addHelper.downloadPlatform.and.callThrough(); }); describe('errors', function () { it('should reject the promise should fetch fail', function () { fetch_mock.and.returnValue(Promise.reject(new Error('fetch has failed, rejecting promise'))); return expectAsync( platform_addHelper.downloadPlatform(projectRoot, 'android', '67') ).toBeRejectedWithError( /fetch has failed, rejecting promise/ ); }); }); describe('happy path', function () { it('should invoke cordova-fetch if fetch was provided as an option', function () { fetch_mock.and.returnValue(true); return platform_addHelper.downloadPlatform(projectRoot, 'android', '6.0.0').then(function () { expect(fetch_mock).toHaveBeenCalledWith('cordova-android@6.0.0', projectRoot, undefined); }); }); it('should pass along a libDir argument to getPlatformDetailsFromDir on a successful platform download', function () { cordova_util.isUrl.and.returnValue(true); return platform_addHelper.downloadPlatform(projectRoot, 'android', 'https://github.com/apache/cordova-android', { save: true }).then(function () { expect(platform_addHelper.__get__('getPlatformDetailsFromDir')).toHaveBeenCalled(); }); }, 60000); }); }); describe('installPluginsForNewPlatform', function () { beforeEach(function () { spyOn(plugman, 'install').and.returnValue(Promise.resolve()); spyOn(cordova_util, 'findPlugins').and.returnValue(['cordova-plugin-whitelist']); spyOn(fetch_metadata, 'get_fetch_metadata').and.returnValue({}); platform_addHelper.installPluginsForNewPlatform.and.callThrough(); }); // Call installPluginsForNewPlatform with some preset test arguments function installPluginsForNewPlatformWithTestArgs () { return platform_addHelper.installPluginsForNewPlatform('atari', projectRoot, {}); } it('should immediately return if there are no plugins to install into the platform', function () { cordova_util.findPlugins.and.returnValue([]); return installPluginsForNewPlatformWithTestArgs().then(() => { expect(plugman.install).not.toHaveBeenCalled(); }); }); it('should invoke plugman.install, giving correct platform, plugin and other arguments', function () { return installPluginsForNewPlatformWithTestArgs().then(() => { expect(events.emit).toHaveBeenCalledWith( 'verbose', 'Installing plugin "cordova-plugin-whitelist" following successful platform add of atari' ); expect(plugman.install).toHaveBeenCalledTimes(1); expect(plugman.install).toHaveBeenCalledWith( 'atari', path.normalize('/some/path/platforms/atari'), 'cordova-plugin-whitelist', path.normalize('/some/path/plugins'), { searchpath: undefined, usePlatformWww: true, is_top_level: undefined, force: undefined, save: false } ); }); }); it('should properly signal a top level plugin to plugman.install,', () => { fetch_metadata.get_fetch_metadata.and.returnValue({ is_top_level: true }); return installPluginsForNewPlatformWithTestArgs().then(() => { expect(plugman.install).toHaveBeenCalledTimes(1); const installOptions = plugman.install.calls.argsFor(0)[4]; expect(installOptions.is_top_level).toBe(true); }); }); it('should invoke plugman.install with correct plugin ID for a scoped plugins', () => { const pkgJsonPluginIds = ['@cordova/cordova-plugin-scoped', 'cordova-plugin-whitelist']; platform_addHelper.__set__({ readPackageJsonIfExists: jasmine.createSpy('readPackageJsonIfExists').and.returnValue({ ...package_json_mock, cordova: { plugins: Object.fromEntries(pkgJsonPluginIds.map(pluginId => [pluginId, {}])) } }) }); cordova_util.findPlugins.and.returnValue([pkgJsonPluginIds[1], pkgJsonPluginIds[0]]); return installPluginsForNewPlatformWithTestArgs().then(() => { expect(plugman.install).toHaveBeenCalledTimes(pkgJsonPluginIds.length); const installedPluginIds = pkgJsonPluginIds.map(function (pkgJsonPluginId, index) { return plugman.install.calls.argsFor(index)[2]; }); expect(installedPluginIds).toEqual(pkgJsonPluginIds); }); }); it('should include any plugin variables as options when invoking plugman install', function () { const variables = {}; fetch_metadata.get_fetch_metadata.and.returnValue({ variables }); return installPluginsForNewPlatformWithTestArgs().then(() => { expect(events.emit).toHaveBeenCalledWith( 'verbose', 'Found variables for "cordova-plugin-whitelist". Processing as cli_variables.' ); expect(plugman.install).toHaveBeenCalledTimes(1); const installOptions = plugman.install.calls.argsFor(0)[4]; expect(installOptions.cli_variables).toBe(variables); }); }); }); describe('getVersionFromPackageJson', () => { let getVersionFromPackageJson; beforeEach(() => { getVersionFromPackageJson = platform_addHelper.__get__('getVersionFromPackageJson'); }); it('gets the platform version from dependencies or devDependencies, preferring the latter', () => { const pkgJson = { dependencies: { 'cordova-ios': '1.2.3-ios', 'cordova-android': '1.2.3-android' }, devDependencies: { 'cordova-ios': '1.2.3-ios.dev' } }; expect(getVersionFromPackageJson('android', pkgJson)).toBe('1.2.3-android'); expect(getVersionFromPackageJson('ios', pkgJson)).toBe('1.2.3-ios.dev'); expect(getVersionFromPackageJson('osx', pkgJson)).toBeUndefined(); }); it('gets the platform version given either long or short name', () => { const pkgJson = { devDependencies: { 'cordova-ios': '1.2.3-ios.dev' } }; expect(getVersionFromPackageJson('ios', pkgJson)).toBe('1.2.3-ios.dev'); expect(getVersionFromPackageJson('cordova-ios', pkgJson)).toBe('1.2.3-ios.dev'); }); it('handles empty package.json objects', () => { expect(getVersionFromPackageJson('ios', undefined)).toBeUndefined(); expect(getVersionFromPackageJson('ios', {})).toBeUndefined(); }); }); }); ================================================ FILE: spec/cordova/platform/getPlatformDetailsFromDir.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const rewire = require('rewire'); const cordova_util = require('../../../src/cordova/util'); const platform_getPlatformDetails = rewire('../../../src/cordova/platform/getPlatformDetailsFromDir'); const events = require('cordova-common').events; describe('cordova/platform/getPlatformDetailsFromDir', function () { const package_json_mock = jasmine.createSpyObj('package json mock', ['cordova', 'dependencies']); package_json_mock.name = 'io.cordova.hellocordova'; package_json_mock.version = '1.0.0'; beforeEach(function () { spyOn(fs, 'existsSync'); spyOn(cordova_util, 'requireNoCache'); spyOn(events, 'emit'); }); it('should throw if no config.xml or pkgJson', function () { return expectAsync( platform_getPlatformDetails('dir', ['ios']) ).toBeRejectedWithError( /does not seem to contain a valid package.json or a valid Cordova platform/ ); }); it('should throw if no platform is provided', function () { cordova_util.requireNoCache.and.returnValue({}); return expectAsync( platform_getPlatformDetails('dir') ).toBeRejectedWithError( /does not seem to contain a Cordova platform:/ ); }); it('should return a promise with platform and version', function () { fs.existsSync.and.callFake(function (filePath) { if (path.basename(filePath) === 'package.json') { return true; } else { return false; } }); cordova_util.requireNoCache.and.returnValue(package_json_mock); return platform_getPlatformDetails('dir', ['cordova-android']) .then(function (result) { expect(result.platform).toBe('io.cordova.hellocordova'); expect(result.version).toBe('1.0.0'); }); }); it('should remove the cordova- prefix from the platform name for known platforms', function () { expect(platform_getPlatformDetails.platformFromName('cordova-ios')).toBe('ios'); expect(events.emit).toHaveBeenCalledWith('verbose', jasmine.stringMatching(/Removing "cordova-" prefix/)); }); }); ================================================ FILE: spec/cordova/platform/index.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const platform = rewire('../../../src/cordova/platform'); const cordova_util = require('../../../src/cordova/util'); describe('cordova/platform', function () { const projectRoot = 'somepath'; beforeEach(function () { // TODO: if we can change HooksRunner from a prototypal class to a function or object, // we could eliminate the need for rewire here and use just jasmine spies. platform.__set__('HooksRunner', function () {}); spyOn(cordova_util, 'cdProjectRoot').and.returnValue(projectRoot); }); describe('main module function', function () { describe('error/warning conditions', function () { // TODO: what about other commands? update? save? it('should require at least one platform for add and remove commands', async function () { // targets = empty array await expectAsync( platform('add', []) ).toBeRejectedWithError( /You need to qualify.* with one or more platforms/i ); // targets = null await expectAsync( platform('remove', null) ).toBeRejectedWithError( /You need to qualify.* with one or more platforms/i ); }); }); describe('handling of targets parameter', function () { const cmd = 'add'; beforeEach(function () { spyOn(platform, cmd).and.returnValue(true); }); it('should be able to handle an array of platform targets', function () { const targets = ['nokia brick', 'HAM radio', 'nintendo wii']; return platform(cmd, targets) .then(function () { expect(platform[cmd]).toHaveBeenCalledWith(jasmine.any(Object), projectRoot, targets, jasmine.any(Object)); }); }); it('should be able to handle a single platform target string', function () { const target = 'motorola razr'; return platform(cmd, target) .then(function () { expect(platform[cmd]).toHaveBeenCalledWith(jasmine.any(Object), projectRoot, [target], jasmine.any(Object)); }); }); }); describe('happy path (success conditions)', function () { it('should direct `add` commands to the `add` method/module', function () { spyOn(platform, 'add').and.returnValue(true); return platform('add', ['android']) .then(function () { expect(platform.add).toHaveBeenCalled(); }); }); it('should direct `remove` + `rm` commands to the `remove` method/module', function () { spyOn(platform, 'remove').and.returnValue(true); return platform('remove', ['android']) .then(function () { expect(platform.remove).toHaveBeenCalled(); }).then(function () { platform.remove.calls.reset(); // reset spy counter return platform('rm', ['android']); }).then(function () { expect(platform.remove).toHaveBeenCalled(); }); }); it('should direct `update` + `up` commands to the `update` method/module', function () { spyOn(platform, 'update').and.returnValue(true); return platform('update', ['android']) .then(function () { expect(platform.update).toHaveBeenCalled(); }).then(function () { platform.update.calls.reset(); // reset spy counter return platform('up', ['android']); }).then(function () { expect(platform.update).toHaveBeenCalled(); }); }); it('should direct `list`, all other commands and no command at all to the `list` method/module', function () { spyOn(platform, 'list').and.returnValue(true); // test the `list` command directly return platform('list') .then(function () { expect(platform.list).toHaveBeenCalled(); }).then(function () { platform.list.calls.reset(); // reset spy counter // test the list catch-all return platform('please give me the list command'); }).then(function () { expect(platform.list).toHaveBeenCalled(); }).then(function () { platform.list.calls.reset(); // reset spy counter // test the lack of providing an argument. return platform(); }).then(function () { expect(platform.list).toHaveBeenCalled(); }); }); }); }); }); ================================================ FILE: spec/cordova/platform/list.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const events = require('cordova-common').events; const platform_list = require('../../../src/cordova/platform/list'); const cordova_util = require('../../../src/cordova/util'); describe('cordova/platform/list', function () { let hooks_mock; const projectRoot = '/some/path'; beforeEach(function () { hooks_mock = jasmine.createSpyObj('hooksRunner mock', ['fire']); hooks_mock.fire.and.returnValue(Promise.resolve()); spyOn(cordova_util, 'getInstalledPlatformsWithVersions').and.callThrough(); spyOn(events, 'emit'); spyOn(cordova_util, 'requireNoCache').and.returnValue({}); }); it('should fire the before_platform_ls hook', function () { platform_list(hooks_mock, projectRoot, { save: true }); expect(hooks_mock.fire).toHaveBeenCalledWith('before_platform_ls', Object({ save: true })); }); it('should fire the after_platform_ls hook', function () { return platform_list(hooks_mock, projectRoot, { save: true }) .then(function (result) { expect(hooks_mock.fire).toHaveBeenCalledWith('after_platform_ls', Object({ save: true })); }); }); it('should print results of available platforms', function () { return platform_list(hooks_mock, projectRoot, { save: true }) .then(function (result) { expect(events.emit).toHaveBeenCalledWith('results', jasmine.stringMatching(/Installed platforms:/)); }); }); it('should return platform list', function () { const platformList = ['android', 'ios']; expect(platform_list.addDeprecatedInformationToPlatforms(platformList).toString()).toBe('android,ios'); }); }); ================================================ FILE: spec/cordova/platform/listDeprecated.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const events = require('cordova-common').events; const platform_list = rewire('../../../src/cordova/platform/list'); describe('cordova/platform/list show deprecated platforms info', function () { let hooks_mock; const projectRoot = '/some/path'; beforeEach(function () { // use mock platforms info so that this test can working properly // with or without any deprecated platforms platform_list.__set__({ cordova_util: { getInstalledPlatformsWithVersions: () => Promise.resolve({}) }, platforms: { hostSupports: () => true, info: { android: { version: '1.2.3', deprecated: false }, wp7: { version: '4.5.6', deprecated: true } }, list: ['android', 'wp7'] } }); hooks_mock = jasmine.createSpyObj('hooksRunner mock', ['fire']); hooks_mock.fire.and.returnValue(Promise.resolve()); spyOn(events, 'emit'); }); it('shows available platforms with deprecated info', () => { return platform_list(hooks_mock, projectRoot, { save: true }) .then((result) => { expect(events.emit).toHaveBeenCalledWith('results', jasmine.stringMatching(/Available platforms:\n\s\sandroid\n\s\swp7 \(deprecated\)/)); }); }); it('returns platform list with deprecated info', function () { const platformList = ['android 1.2.3', 'wp7 4.5.6']; expect(platform_list.addDeprecatedInformationToPlatforms(platformList)).toEqual(['android 1.2.3', 'wp7 4.5.6 (deprecated)']); }); }); ================================================ FILE: spec/cordova/platform/remove.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const fs = require('node:fs'); const events = require('cordova-common').events; const rewire = require('rewire'); const cordova_util = require('../../../src/cordova/util'); const promiseutil = require('../../../src/util/promise-util'); describe('cordova/platform/remove', function () { const projectRoot = '/some/path'; const cfg_parser_mock = function () {}; let hooks_mock; const package_json_mock = jasmine.createSpyObj('package json mock', ['cordova', 'dependencies']); package_json_mock.dependencies = {}; package_json_mock.cordova = {}; let platform_remove; beforeEach(function () { hooks_mock = jasmine.createSpyObj('hooksRunner mock', ['fire']); hooks_mock.fire.and.returnValue(Promise.resolve()); cfg_parser_mock.prototype = jasmine.createSpyObj('config parser mock', ['write', 'getEngines', 'removeEngine']); platform_remove = rewire('../../../src/cordova/platform/remove'); platform_remove.__set__({ HooksRunner: _ => _, npmUninstall: _ => Promise.resolve(''), ConfigParser: cfg_parser_mock }); spyOn(fs, 'existsSync').and.returnValue(false); spyOn(fs, 'writeFileSync'); spyOn(cordova_util, 'removePlatformPluginsJson'); spyOn(events, 'emit'); spyOn(cordova_util, 'requireNoCache').and.returnValue({}); cfg_parser_mock.prototype.getEngines.and.returnValue([{ name: 'atari', spec: '^0.0.1' }]); }); describe('error/warning conditions', function () { it('should require specifying at least one platform', function () { return expectAsync( platform_remove('remove', hooks_mock) ).toBeRejectedWithError(/No platform\(s\) specified\./); }); }); describe('happy path (success conditions)', function () { it('should fire the before_platform_* hook', function () { platform_remove(hooks_mock, projectRoot, ['atari'], { save: true }); expect(hooks_mock.fire).toHaveBeenCalledWith('before_platform_rm', jasmine.any(Object)); }); it('should remove .json file from plugins directory', function () { return platform_remove(hooks_mock, projectRoot, ['atari'], { save: true }) .then(function () { expect(cordova_util.removePlatformPluginsJson).toHaveBeenCalled(); }); }); it('should remove from config.xml and platforms.json', function () { return platform_remove(hooks_mock, projectRoot, ['atari'], { save: true }) .then(function () { expect(cordova_util.removePlatformPluginsJson).toHaveBeenCalled(); expect(cfg_parser_mock.prototype.write).toHaveBeenCalled(); expect(events.emit).toHaveBeenCalledWith('log', jasmine.stringMatching(/Removing platform atari from config.xml file/)); expect(events.emit).toHaveBeenCalledWith('verbose', jasmine.stringMatching(/Removing platform atari from platforms.json file/)); }); }); it('should remove from package.json', function () { package_json_mock.cordova = { platforms: ['atari'] }; cordova_util.requireNoCache.and.returnValue(package_json_mock); spyOn(fs, 'readFileSync').and.returnValue('file'); fs.existsSync.and.callFake(function (filePath) { if (path.basename(filePath) === 'package.json') { return true; } else { return false; } }); return platform_remove(hooks_mock, projectRoot, ['atari'], { save: true }) .then(function () { expect(fs.writeFileSync).toHaveBeenCalled(); expect(events.emit).toHaveBeenCalledWith('log', jasmine.stringMatching(/Removing atari from cordova.platforms array in package.json/)); }); }); it('fetch should be called', function () { spyOn(promiseutil, 'Q_chainmap').and.returnValue(true); return platform_remove(hooks_mock, projectRoot, ['atari'], {}) .then(function () { expect(promiseutil.Q_chainmap).toHaveBeenCalled(); expect(hooks_mock.fire).toHaveBeenCalledWith('after_platform_rm', {}); }); }); it('should file the after_platform_* hook', function () { return platform_remove(hooks_mock, projectRoot, ['atari'], { save: true }) .then(function (result) { expect(hooks_mock.fire).toHaveBeenCalledWith('after_platform_rm', Object({ save: true })); }); }); }); }); ================================================ FILE: spec/cordova/platforms/platforms.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const rewire = require('rewire'); const events = require('cordova-common').events; const util = require('../../../src/cordova/util'); const platforms = rewire('../../../src/platforms/platforms'); const CORDOVA_ROOT = path.join(__dirname, '../fixtures/projects/platformApi'); const PLATFORM_WITH_API = path.join(CORDOVA_ROOT, 'platforms/windows'); const PLATFORM_SYMLINK = path.join(os.tmpdir(), 'cordova_windows_symlink'); describe('platforms/platforms', () => { it('should have getPlatformApi function as a property', function () { expect(platforms.getPlatformApi).toBeDefined(); expect(typeof platforms.getPlatformApi).toBe('function'); }); it('should have all and only the supported platforms', function () { expect(Object.keys(platforms)).toEqual(jasmine.arrayWithExactContents([ 'android', 'browser', 'ios', 'electron' ])); }); describe('getPlatformApi method', function () { let isCordova; beforeEach(function () { // reset api cache after each spec platforms.__set__('cachedApis', {}); isCordova = spyOn(util, 'isCordova').and.returnValue(CORDOVA_ROOT); fs.rmSync(PLATFORM_SYMLINK, { recursive: true, force: true }); fs.symlinkSync(PLATFORM_WITH_API, PLATFORM_SYMLINK); }); it('should return PlatformApi class defined by platform', function () { spyOn(events, 'emit').and.returnValue(true); spyOn(util, 'convertToRealPathSafe').and.callThrough(); spyOn(util, 'requireNoCache').and.callThrough(); const platformApi = platforms.getPlatformApi('windows', PLATFORM_WITH_API); expect(platformApi).toBeDefined(); expect(platformApi.platform).toEqual('windows'); expect(events.emit.calls.count()).toEqual(1); expect(events.emit.calls.argsFor(0)[1]).toMatch('Loaded API for windows project'); expect(util.convertToRealPathSafe.calls.count()).toEqual(1); expect(util.isCordova.calls.count()).toEqual(0); expect(util.requireNoCache.calls.count()).toEqual(1); expect(util.requireNoCache.calls.argsFor(0)[0]).toEqual(path.join(CORDOVA_ROOT, 'platforms/windows/cordova/Api.js')); }); it('should cache PlatformApi instance for further calls', function () { const platformApi = platforms.getPlatformApi('windows', PLATFORM_WITH_API); expect(platformApi.fakeProperty).not.toBeDefined(); platformApi.fakeProperty = 'fakeValue'; expect(platforms.getPlatformApi('windows', PLATFORM_WITH_API).fakeProperty).toBe('fakeValue'); }); it('should resolve symlinks before creating an instance', function () { const platformApi = platforms.getPlatformApi('windows', PLATFORM_SYMLINK); expect(platforms.getPlatformApi('windows', PLATFORM_WITH_API)).toBe(platformApi); }); it('should return cached instance by symlink to project root', function () { platforms.getPlatformApi('windows', PLATFORM_WITH_API).fakeProperty = 'fakeValue'; expect(platforms.getPlatformApi('windows', PLATFORM_SYMLINK).fakeProperty).toBe('fakeValue'); }); it('should succeed if called inside of cordova project w/out platformRoot param', function () { const platformApi = platforms.getPlatformApi('windows'); expect(platformApi).toBeDefined(); expect(platformApi.platform).toEqual('windows'); }); it('should throw if called outside of cordova project w/out platformRoot param', function () { isCordova.and.returnValue(false); expect(function () { platforms.getPlatformApi('windows'); }).toThrow(); }); it('should throw for unknown platform', function () { expect(function () { platforms.getPlatformApi('invalid_platform'); }).toThrow(); }); it('should throw for nonsense www platform', function () { expect(function () { platforms.getPlatformApi('www'); }).toThrow(); }); }); }); ================================================ FILE: spec/cordova/plugin/add.getFetchVersion.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const { events } = require('cordova-common'); const cordovaUtil = require('../../../src/cordova/util'); const pluginUtil = require('../../../src/cordova/plugin/util'); const pluginAdd = require('../../../src/cordova/plugin/add'); const cordovaVersion = '3.4.2'; // Used to extract the constraint, the installed version, and the required // semver range from a warning message const UNMET_REQ_REGEX = /\s+([^\s]+)[^\d]+(\d+\.\d+\.\d+) in project, (.+) required\)/; function unmetRequirementsCollector (warning) { const match = UNMET_REQ_REGEX.exec(warning); if (!match) return; unmetRequirementsCollector.store.push({ dependency: match[1], installed: match[2], required: match[3] }); } // Checks the warnings that were printed by the CLI to ensure that the code is // listing the correct reasons for failure. Checks against the global warnings // object which is reset before each test function expectUnmetRequirements (expected) { const actual = unmetRequirementsCollector.store; expect(actual).toEqual(jasmine.arrayWithExactContents(expected)); } // Helper functions for creating the requirements objects taken by // expectUnmetRequirements() function getPlatformRequirement (requirement) { return { dependency: 'cordova-android', installed: '3.1.0', required: requirement }; } function getCordovaRequirement (requirement) { return { dependency: 'cordova', installed: cordovaVersion, required: requirement }; } function getPluginRequirement (requirement) { return { dependency: 'ca.filmaj.AndroidPlugin', installed: '4.2.0', required: requirement }; } describe('plugin fetching version selection', () => { let project, testPlugin; beforeEach(() => { unmetRequirementsCollector.store = []; // We generate warnings when we don't fetch latest. Collect them to make sure we // are making the correct warnings events.on('warn', unmetRequirementsCollector); testPlugin = { version: '2.3.0', name: 'test-plugin', engines: { cordovaDependencies: {} }, versions: [ '0.0.2', '0.7.0', '1.0.0', '1.1.0', '1.1.3', '1.3.0', '1.7.0', '1.7.1', '2.0.0-rc.1', '2.0.0-rc.2', '2.0.0', '2.3.0' ] }; spyOn(pluginUtil, 'getInstalledPlugins').and.returnValue([ { id: 'ca.filmaj.AndroidPlugin', version: '4.2.0' } ]); spyOn(cordovaUtil, 'getInstalledPlatformsWithVersions').and.returnValue( Promise.resolve({ android: '3.1.0' }) ); }); afterEach(() => { events.removeListener('warn', unmetRequirementsCollector); }); function getFetchVersion (plugin) { return pluginAdd.getFetchVersion(project, plugin, cordovaVersion); } it('Test 001 : should handle a mix of upper bounds and single versions', () => { testPlugin.engines.cordovaDependencies = { '0.0.0': { 'cordova-android': '1.0.0' }, '0.0.2': { 'cordova-android': '>1.0.0' }, '<1.0.0': { 'cordova-android': '<2.0.0' }, '1.0.0': { 'cordova-android': '>2.0.0' }, '1.7.0': { 'cordova-android': '>4.0.0' }, '<2.3.0': { 'cordova-android': '<6.1.1' }, '2.3.0': { 'cordova-android': '6.1.1' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.3.0'); expectUnmetRequirements([getPlatformRequirement('6.1.1')]); }); }); it('Test 002 : should apply upper bound engine constraints when there are no unspecified constraints above the upper bound', () => { testPlugin.engines.cordovaDependencies = { '1.0.0': { 'cordova-android': '>2.0.0' }, '1.7.0': { 'cordova-android': '>4.0.0' }, '<2.3.0': { 'cordova-android': '<6.1.1', 'ca.filmaj.AndroidPlugin': '<1.0.0' }, '2.3.0': { 'cordova-android': '6.1.1' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([getPlatformRequirement('6.1.1')]); }); }); it('Test 003 : should apply upper bound engine constraints when there are unspecified constraints above the upper bound', () => { testPlugin.engines.cordovaDependencies = { '0.0.0': {}, '2.0.0': { 'cordova-android': '~5.0.0' }, '<1.0.0': { 'cordova-android': '>5.0.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.7.1'); expectUnmetRequirements([getPlatformRequirement('~5.0.0')]); }); }); it('Test 004 : should handle the case where there are no constraints for earliest releases', () => { testPlugin.engines.cordovaDependencies = { '1.0.0': { 'cordova-android': '~5.0.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('0.7.0'); expectUnmetRequirements([getPlatformRequirement('~5.0.0')]); }); }); it('Test 005 : should handle the case where the lowest version is unsatisfied', () => { testPlugin.engines.cordovaDependencies = { '0.0.2': { 'cordova-android': '~5.0.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([getPlatformRequirement('~5.0.0')]); }); }); it('Test 006 : should handle upperbounds if no single version constraints are given', () => { testPlugin.engines.cordovaDependencies = { '<1.0.0': { 'cordova-android': '<2.0.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('2.3.0'); expectUnmetRequirements([]); }); }); it('Test 007 : should apply upper bounds greater than highest version', () => { testPlugin.engines.cordovaDependencies = { '0.0.0': {}, '<5.0.0': { 'cordova-android': '<2.0.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([getPlatformRequirement('<2.0.0')]); }); }); it('Test 008 : should treat empty constraints as satisfied', () => { testPlugin.engines.cordovaDependencies = { '1.0.0': {}, '1.1.0': { 'cordova-android': '>5.0.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.0.0'); expectUnmetRequirements([getPlatformRequirement('>5.0.0')]); }); }); it('Test 009 : should ignore an empty cordovaDependencies entry', () => { testPlugin.engines.cordovaDependencies = {}; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([]); }); }); it('Test 010 : should ignore a badly formatted semver range', () => { testPlugin.engines.cordovaDependencies = { '1.1.3': { 'cordova-android': 'badSemverRange' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('2.3.0'); expectUnmetRequirements([]); }); }); it('Test 011 : should respect unreleased versions in constraints', () => { testPlugin.engines.cordovaDependencies = { '1.0.0': { 'cordova-android': '3.1.0' }, '1.1.2': { 'cordova-android': '6.1.1' }, '1.3.0': { 'cordova-android': '6.1.1' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.1.0'); expectUnmetRequirements([getPlatformRequirement('6.1.1')]); }); }); it('Test 012 : should respect plugin constraints', () => { testPlugin.engines.cordovaDependencies = { '0.0.0': { 'ca.filmaj.AndroidPlugin': '1.2.0' }, '1.1.3': { 'ca.filmaj.AndroidPlugin': '<5.0.0 || >2.3.0' }, '2.3.0': { 'ca.filmaj.AndroidPlugin': '6.1.1' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('2.0.0'); expectUnmetRequirements([getPluginRequirement('6.1.1')]); }); }); it('Test 013 : should respect cordova constraints', () => { testPlugin.engines.cordovaDependencies = { '0.0.0': { cordova: '>1.0.0' }, '1.1.3': { cordova: '<3.0.0 || >4.0.0' }, '2.3.0': { cordova: '6.1.1' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.1.0'); expectUnmetRequirements([getCordovaRequirement('6.1.1')]); }); }); it('Test 014 : should not include pre-release versions', () => { testPlugin.engines.cordovaDependencies = { '0.0.0': {}, '2.0.0': { 'cordova-android': '>5.0.0' } }; // Should not return 2.0.0-rc.2 return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.7.1'); expectUnmetRequirements([getPlatformRequirement('>5.0.0')]); }); }); it('Test 015 : should not fail if there is no engine in the npm info', () => { delete testPlugin.engines; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([]); }); }); it('Test 016 : should not fail if there is no cordovaDependencies in the engines', () => { testPlugin.engines = { node: '>7.0.0', npm: '~2.0.0' }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([]); }); }); it('Test 017 : should handle extra whitespace', () => { testPlugin.engines.cordovaDependencies = { ' 1.0.0 ': {}, '2.0.0 ': { ' cordova-android': '~5.0.0 ' }, ' < 1.0.0\t': { ' cordova-android ': ' > 5.0.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.7.1'); expectUnmetRequirements([getPlatformRequirement('~5.0.0')]); }); }); it('Test 018 : should ignore badly typed version requirement entries', () => { testPlugin.engines.cordovaDependencies = { '1.1.0': ['cordova', '5.0.0'], '1.3.0': undefined, '1.7.0': null }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('2.3.0'); expectUnmetRequirements([]); }); }); it('Test 019 : should ignore badly typed constraint entries', () => { testPlugin.engines.cordovaDependencies = { '0.0.2': { cordova: 1 }, '0.7.0': { cordova: {} }, '1.0.0': { cordova: undefined }, '1.1.3': { 8: '5.0.0' }, '1.3.0': { cordova: [] }, '1.7.1': { cordova: null } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('2.3.0'); expectUnmetRequirements([]); }); }); it('Test 020 : should ignore bad semver versions', () => { testPlugin.engines.cordovaDependencies = { '0.0.0': { 'cordova-android': '5.0.0' }, notAVersion: { 'cordova-android': '3.1.0' }, '^1.1.2': { 'cordova-android': '3.1.0' }, '<=1.3.0': { 'cordova-android': '3.1.0' }, '1.0': { 'cordova-android': '3.1.0' }, 2: { 'cordova-android': '3.1.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([getPlatformRequirement('5.0.0')]); }); }); it('Test 021 : should not fail if there are bad semver versions', () => { testPlugin.engines.cordovaDependencies = { notAVersion: { 'cordova-android': '3.1.0' }, '^1.1.2': { 'cordova-android': '3.1.0' }, '<=1.3.0': { 'cordova-android': '3.1.0' }, '1.0.0': { 'cordova-android': '~3' }, // Good semver '2.0.0': { 'cordova-android': '5.1.0' }, // Good semver '1.0': { 'cordova-android': '3.1.0' }, 2: { 'cordova-android': '3.1.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.7.1'); expectUnmetRequirements([getPlatformRequirement('5.1.0')]); }); }); it('Test 022 : should properly warn about multiple unmet requirements', () => { testPlugin.engines.cordovaDependencies = { '1.7.0': { 'cordova-android': '>5.1.0', 'ca.filmaj.AndroidPlugin': '3.1.0', cordova: '3.4.2' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.3.0'); expectUnmetRequirements([ getPlatformRequirement('>5.1.0'), getPluginRequirement('3.1.0') ]); }); }); it('Test 023 : should properly warn about both unmet latest and upper bound requirements', () => { testPlugin.engines.cordovaDependencies = { '1.7.0': { 'cordova-android': '>5.1.0' }, '<5.0.0': { 'cordova-android': '>7.1.0', 'ca.filmaj.AndroidPlugin': '3.1.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe(null); expectUnmetRequirements([ getPlatformRequirement('>5.1.0 AND >7.1.0'), getPluginRequirement('3.1.0') ]); }); }); it('Test 024 : should not warn about versions past latest', () => { testPlugin.engines.cordovaDependencies = { '1.7.0': { 'cordova-android': '>5.1.0' }, '7.0.0': { 'cordova-android': '>7.1.0', 'ca.filmaj.AndroidPlugin': '3.1.0' } }; return getFetchVersion(testPlugin).then(version => { expect(version).toBe('1.3.0'); expectUnmetRequirements([getPlatformRequirement('>5.1.0')]); }); }); }); ================================================ FILE: spec/cordova/plugin/add.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const rewire = require('rewire'); const plugman = require('../../../src/plugman/plugman'); const cordova_util = require('../../../src/cordova/util'); const events = require('cordova-common').events; const plugin_util = require('../../../src/cordova/plugin/util'); describe('cordova/plugin/add', function () { const projectRoot = '/some/path'; let hook_mock; const Cfg_parser_mock = function () {}; const plugin_info_provider_mock = function () {}; let plugin_info; let package_json_mock; let add; beforeEach(function () { add = rewire('../../../src/cordova/plugin/add'); hook_mock = jasmine.createSpyObj('hooks runner mock', ['fire']); hook_mock.fire.and.returnValue(Promise.resolve()); Cfg_parser_mock.prototype = jasmine.createSpyObj('config parser prototype mock', ['getPlugin']); add.__set__('ConfigParser', Cfg_parser_mock); plugin_info = jasmine.createSpyObj('pluginInfo', ['getPreferences']); plugin_info.getPreferences.and.returnValue({}); plugin_info.dir = 'some\\plugin\\path'; plugin_info.id = 'cordova-plugin-device'; plugin_info.version = '1.0.0'; plugin_info_provider_mock.prototype = jasmine.createSpyObj('plugin info provider mock', ['get']); plugin_info_provider_mock.prototype.get = function (directory) { // id version dir getPreferences() engines engines.cordovaDependencies name versions return plugin_info; }; add.__set__('PluginInfoProvider', plugin_info_provider_mock); spyOn(fs, 'existsSync').and.returnValue(false); spyOn(fs, 'writeFileSync'); package_json_mock = jasmine.createSpyObj('package json mock', ['cordova', 'dependencies', 'devDependencies']); package_json_mock.cordova = {}; package_json_mock.dependencies = {}; package_json_mock.devDependencies = {}; // requireNoCache is used to require package.json spyOn(cordova_util, 'requireNoCache').and.returnValue(package_json_mock); spyOn(events, 'emit'); spyOn(plugin_util, 'info').and.returnValue(Promise.resolve()); spyOn(add, 'getFetchVersion').and.returnValue(Promise.resolve()); }); describe('main method', function () { beforeEach(function () { spyOn(add, 'determinePluginTarget').and.callFake(function (projRoot, cfg, target, opts) { return Promise.resolve(target); }); spyOn(plugman, 'fetch').and.callFake(function (target, pluginPath, opts) { return Promise.resolve(target); }); spyOn(plugman, 'install').and.returnValue(Promise.resolve(true)); spyOn(cordova_util, 'listPlatforms').and.callFake(function () { return ['android']; }); spyOn(cordova_util, 'findPlugins').and.returnValue({ plugins: [] }); }); describe('error/warning conditions', function () { it('should error out if at least one plugin is not specified', function () { return expectAsync( add(projectRoot, hook_mock, { plugins: [] }) ).toBeRejectedWithError(/No plugin specified/); }); it('should error out if any mandatory plugin variables are not provided', function () { plugin_info.getPreferences.and.returnValue({ some: undefined }); return expectAsync( add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'] }) ).toBeRejectedWithError(/Variable\(s\) missing \(use: --variable/); }); }); describe('happy path', function () { it('should fire the before_plugin_add hook', function () { return add(projectRoot, hook_mock, { plugins: ['https://github.com/apache/cordova-plugin-device'], save: true }).then(function () { expect(hook_mock.fire).toHaveBeenCalledWith('before_plugin_add', jasmine.any(Object)); }); }); it('should determine where to fetch a plugin from using determinePluginTarget and invoke plugman.fetch with the resolved target', function () { return add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'] }).then(function () { expect(add.determinePluginTarget).toHaveBeenCalledWith(projectRoot, jasmine.any(Object), 'cordova-plugin-device', jasmine.any(Object)); expect(plugman.fetch).toHaveBeenCalledWith('cordova-plugin-device', path.join(projectRoot, 'plugins'), jasmine.any(Object)); }); }); it('should retrieve any variables for the plugin from config.xml and add them as cli variables only when the variables were not already provided via options', function () { const cfg_plugin_variables = { some: 'variable' }; Cfg_parser_mock.prototype.getPlugin.and.callFake(function (plugin_id) { return { variables: cfg_plugin_variables }; }); return add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'] }).then(function () { // confirm cli_variables are undefind expect(add.determinePluginTarget).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), jasmine.objectContaining({ variables: undefined })); expect(plugman.install).toHaveBeenCalled(); // check that the plugin variables from config.xml got added to cli_variables expect(plugman.install).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), jasmine.anything(), jasmine.objectContaining({ cli_variables: cfg_plugin_variables })); }); }); it('should invoke plugman.install for each platform added to the project', function () { return add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'] }).then(function () { expect(plugman.install).toHaveBeenCalledWith('android', jasmine.any(String), jasmine.any(String), jasmine.any(String), jasmine.any(Object)); }); }); it('should invoke plugman.install with the full ID of a scoped plugin', () => { const scopedPluginId = '@cordova/plugin-thinger'; plugin_info.id = scopedPluginId; return add(projectRoot, hook_mock, { plugins: [scopedPluginId] }).then(() => { const actualPluginId = plugman.install.calls.argsFor(0)[2]; expect(actualPluginId).toBe(scopedPluginId); }); }); it('should save plugin variable information to package.json file (if exists)', function () { const cli_plugin_variables = { some: 'variable' }; fs.existsSync.and.callFake(function (file_path) { if (path.basename(file_path) === 'package.json') { return true; } else { return false; } }); spyOn(fs, 'readFileSync').and.returnValue('file'); return add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'], cli_variables: cli_plugin_variables, save: 'true' }).then(function () { expect(fs.writeFileSync).toHaveBeenCalledWith(jasmine.any(String), JSON.stringify({ cordova: { plugins: { 'cordova-plugin-device': cli_plugin_variables } }, dependencies: {}, devDependencies: {} }, null, 2) + '\n', 'utf8'); }); }); it('should overwrite plugin information in config.xml after a successful installation', function () { const cfg_plugin_variables = { some: 'variable' }; const cli_plugin_variables = { some: 'new_variable' }; Cfg_parser_mock.prototype.getPlugin.and.callFake(function (plugin_id) { return { variables: cfg_plugin_variables }; }); return add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'], cli_variables: cli_plugin_variables, save: 'true' }).then(function () { // confirm cli_variables got passed through expect(add.determinePluginTarget).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), jasmine.objectContaining({ variables: cli_plugin_variables })); // check that the plugin variables from config.xml got added to cli_variables expect(plugman.install).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), jasmine.anything(), jasmine.objectContaining({ cli_variables: cli_plugin_variables })); }); }); it('should invoke preparePlatforms if plugman.install returned a falsey value', function () { plugman.install.and.resolveTo(false); const preparePlatforms = jasmine.createSpy('preparePlatforms mock').and.resolveTo(); add.__set__({ preparePlatforms }); return add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'] }).then(() => { expect(preparePlatforms).toHaveBeenCalled(); }); }); it('should fire after_plugin_add hook', function () { return add(projectRoot, hook_mock, { plugins: ['cordova-plugin-device'] }).then(function () { expect(hook_mock.fire).toHaveBeenCalledWith('after_plugin_add', jasmine.any(Object)); }); }); }); }); describe('determinePluginTarget helper method', function () { beforeEach(function () { spyOn(cordova_util, 'isDirectory').and.returnValue(false); spyOn(add, 'getVersionFromConfigFile').and.returnValue(undefined); package_json_mock.dependencies['cordova-plugin-device'] = undefined; }); afterEach(function () { }); it('should return the target directly if the target is pluginSpec-parseable', function () { return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device@1.0.0', {}).then(function (target) { expect(target).toEqual('cordova-plugin-device@1.0.0'); }); }); it('should return the target directly if the target is a URL', function () { return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'https://github.com/apache/cordova-plugin-device.git', {}).then(function (target) { expect(target).toEqual('https://github.com/apache/cordova-plugin-device.git'); }); }); it('should return the target directly if the target is a directory', function () { cordova_util.isDirectory.and.returnValue(true); return add.determinePluginTarget(projectRoot, Cfg_parser_mock, '../some/dir/cordova-plugin-device', {}).then(function (target) { expect(target).toEqual('../some/dir/cordova-plugin-device'); }); }); it('should retrieve plugin version from package.json (if exists)', function () { fs.existsSync.and.callFake(function (file_path) { if (path.basename(file_path) === 'package.json') { return true; } else { return false; } }); package_json_mock.dependencies['cordova-plugin-device'] = '^1.0.0'; return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}).then(function (target) { expect(target).toEqual('cordova-plugin-device@^1.0.0'); }); }); it('should retrieve plugin version from package.json devDependencies (if exists)', function () { fs.existsSync.and.callFake(function (file_path) { if (path.basename(file_path) === 'package.json') { return true; } else { return false; } }); package_json_mock.devDependencies['cordova-plugin-device'] = '^1.0.0'; return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}).then(function (target) { expect(target).toEqual('cordova-plugin-device@^1.0.0'); }); }); it('should retrieve plugin version from config.xml as a last resort', function () { add.getVersionFromConfigFile.and.returnValue('~1.0.0'); return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}).then(function (target) { expect(add.getVersionFromConfigFile).toHaveBeenCalled(); expect(target).toEqual('cordova-plugin-device@~1.0.0'); }); }); it('should return plugin version retrieved from package.json or config.xml if it is a URL', function () { fs.existsSync.and.callFake(function (file_path) { if (path.basename(file_path) === 'package.json') { return true; } else { return false; } }); package_json_mock.dependencies['cordova-plugin-device'] = 'https://github.com/apache/cordova-plugin-device.git'; return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}).then(function (target) { expect(target).toEqual('https://github.com/apache/cordova-plugin-device.git'); }); }); it('should return plugin version retrieved from package.json or config.xml if it is a directory', function () { fs.existsSync.and.callFake(function (file_path) { if (path.basename(file_path) === 'package.json') { return true; } else { return false; } }); cordova_util.isDirectory.and.callFake(function (dir) { if (dir === '../some/dir/cordova-plugin-device') { return true; } return false; }); package_json_mock.dependencies['cordova-plugin-device'] = '../some/dir/cordova-plugin-device'; return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}).then(function (target) { expect(target).toEqual('../some/dir/cordova-plugin-device'); }); }); it('should return plugin version retrieved from package.json or config.xml if it has a scope', function () { fs.existsSync.and.callFake(function (file_path) { if (path.basename(file_path) === 'package.json') { return true; } else { return false; } }); package_json_mock.dependencies['@cordova/cordova-plugin-device'] = '^1.0.0'; return add.determinePluginTarget(projectRoot, Cfg_parser_mock, '@cordova/cordova-plugin-device', {}).then(function (target) { expect(target).toEqual('@cordova/cordova-plugin-device@^1.0.0'); }); }); describe('with no version inferred from config files or provided plugin target', function () { describe('when searchpath or noregistry flag is provided', function () { it('should end up just returning the target passed in case of searchpath', function () { return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', { searchpath: 'some/path' }) .then(function (target) { expect(target).toEqual('cordova-plugin-device'); expect(events.emit).toHaveBeenCalledWith('verbose', 'Not checking npm info for cordova-plugin-device because searchpath or noregistry flag was given'); }); }); it('should end up just returning the target passed in case of noregistry', function () { return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', { noregistry: true }) .then(function (target) { expect(target).toEqual('cordova-plugin-device'); expect(events.emit).toHaveBeenCalledWith('verbose', 'Not checking npm info for cordova-plugin-device because searchpath or noregistry flag was given'); }); }); }); describe('when registry/npm is to be used (neither searchpath nor noregistry flag is provided)', function () { it('should retrieve plugin info via registry.info', function () { return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}) .then(function (target) { expect(plugin_util.info).toHaveBeenCalledWith(['cordova-plugin-device']); expect(events.emit).toHaveBeenCalledWith('verbose', 'Attempting to use npm info for cordova-plugin-device to choose a compatible release'); expect(target).toEqual('cordova-plugin-device'); }); }); it('should feed registry.info plugin information into getFetchVersion', function () { plugin_util.info.and.returnValue(Promise.resolve({ plugin: 'info' })); return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}) .then(function (target) { expect(plugin_util.info).toHaveBeenCalled(); expect(add.getFetchVersion).toHaveBeenCalledWith(jasmine.anything(), { plugin: 'info' }, jasmine.anything()); expect(target).toEqual('cordova-plugin-device'); expect(events.emit).toHaveBeenCalledWith('verbose', 'Attempting to use npm info for cordova-plugin-device to choose a compatible release'); }); }); it('should return the target as plugin-id@fetched-version', function () { add.getFetchVersion.and.returnValue(Promise.resolve('1.0.0')); return add.determinePluginTarget(projectRoot, Cfg_parser_mock, 'cordova-plugin-device', {}) .then(function (target) { expect(plugin_util.info).toHaveBeenCalled(); expect(add.getFetchVersion).toHaveBeenCalled(); expect(target).toEqual('cordova-plugin-device@1.0.0'); expect(events.emit).toHaveBeenCalledWith('verbose', 'Attempting to use npm info for cordova-plugin-device to choose a compatible release'); }); }); }); }); }); describe('parseSource helper method', function () { it('should return target when url is passed', function () { expect(add.parseSource('https://github.com/apache/cordova-plugin-device', {})).toEqual('https://github.com/apache/cordova-plugin-device'); }); it('should return target when local path is passed', function () { fs.existsSync.and.returnValue(true); expect(add.parseSource('../cordova-plugin-device', {})).toEqual('../cordova-plugin-device'); }); it('should return null when target is not url or local path', function () { expect(add.parseSource('cordova-plugin-device', {})).toEqual(null); }); }); describe('getVersionFromConfigFile helper method', function () { it('should return spec', function () { const fakePlugin = {}; fakePlugin.name = ''; fakePlugin.spec = '1.0.0'; fakePlugin.variables = {}; Cfg_parser_mock.prototype.getPlugin.and.returnValue(fakePlugin); const new_cfg = new Cfg_parser_mock(); expect(add.getVersionFromConfigFile('cordova-plugin-device', new_cfg)).toEqual('1.0.0'); }); }); // TODO: reorganize these tests once the logic here is understood! -filmaj describe('unit tests to replace integration-tests/plugin_fetch.spec.js', function () { // See also the tests in spec/cordova/plugin/add.getFetchVersion.spec.js describe('getFetchVersion helper method', function () { let pluginInfo; beforeEach(function () { add.getFetchVersion.and.callThrough(); pluginInfo = {}; spyOn(plugin_util, 'getInstalledPlugins').and.returnValue([]); spyOn(cordova_util, 'getInstalledPlatformsWithVersions').and.returnValue(Promise.resolve({})); spyOn(add, 'determinePluginVersionToFetch'); }); it('should resolve with null if plugin info does not contain engines and engines.cordovaDependencies properties', function () { return add.getFetchVersion(projectRoot, pluginInfo, '7.0.0') .then(function (value) { expect(value).toBe(null); }); }); it('should retrieve installed plugins and installed platforms version and feed that information into determinePluginVersionToFetch', function () { plugin_util.getInstalledPlugins.and.returnValue([{ id: '@cordova/plugin-thinger', version: '2.0.0' }]); cordova_util.getInstalledPlatformsWithVersions.and.returnValue(Promise.resolve({ android: '6.0.0' })); pluginInfo.engines = {}; pluginInfo.engines.cordovaDependencies = { '^1.0.0': { cordova: '>7.0.0' } }; return add.getFetchVersion(projectRoot, pluginInfo, '7.0.0') .then(function () { expect(plugin_util.getInstalledPlugins).toHaveBeenCalledWith(projectRoot); expect(cordova_util.getInstalledPlatformsWithVersions).toHaveBeenCalledWith(projectRoot); expect(add.determinePluginVersionToFetch).toHaveBeenCalledWith(pluginInfo, { '@cordova/plugin-thinger': '2.0.0' }, { android: '6.0.0' }, '7.0.0'); }); }); }); // TODO More work to be done here to replace plugin_fetch.spec.js describe('determinePluginVersionToFetch helper method', function () { let pluginInfo; beforeEach(function () { pluginInfo = {}; pluginInfo.name = 'cordova-plugin-device'; pluginInfo.versions = ['0.1.0', '1.0.0', '1.5.0', '2.0.0']; spyOn(add, 'getFailedRequirements').and.returnValue([]); spyOn(add, 'findVersion').and.returnValue(null); spyOn(add, 'listUnmetRequirements'); }); it('should return null if no valid semver versions exist and no upperbound constraints were placed', function () { pluginInfo.engines = {}; pluginInfo.engines.cordovaDependencies = { '^1.0.0': { cordova: '<7.0.0' } }; expect(add.determinePluginVersionToFetch(pluginInfo, {}, {}, '7.0.0')).toBe(null); expect(events.emit).toHaveBeenCalledWith('verbose', jasmine.stringMatching(/Ignoring invalid version/)); }); it('should return null and fetching latest version of plugin', function () { add.getFailedRequirements.and.returnValue(['2.0.0']); pluginInfo.engines = {}; pluginInfo.engines.cordovaDependencies = { '1.0.0': { cordova: '<7.0.0' }, '<3.0.0': { cordova: '>=7.0.0' } }; expect(add.determinePluginVersionToFetch(pluginInfo, {}, {}, '7.0.0')).toBe(null); expect(events.emit).toHaveBeenCalledWith('warn', jasmine.stringMatching(/Current project does not satisfy/)); }); it('should return highest version of plugin available based on constraints', function () { pluginInfo.engines = {}; pluginInfo.engines.cordovaDependencies = { '1.0.0': { cordova: '<7.0.0' }, '<3.0.0': { cordova: '>=7.0.0' } }; expect(add.determinePluginVersionToFetch(pluginInfo, {}, {}, '7.0.0')).toEqual('2.0.0'); }); }); describe('getFailedRequirements helper method', function () { it('should remove prerelease version', function () { const semver = require('semver'); spyOn(semver, 'prerelease').and.returnValue('7.0.1'); spyOn(semver, 'inc').and.callThrough(); expect(add.getFailedRequirements({ cordova: '>=7.0.0' }, {}, {}, '7.0.0').length).toBe(0); expect(semver.inc).toHaveBeenCalledWith('7.0.0', 'patch'); }); it('should return an empty array if no failed requirements', function () { expect(add.getFailedRequirements({ cordova: '>=7.0.0' }, {}, {}, '7.0.0').length).toBe(0); }); it('should return an empty array if invalid dependency constraint', function () { expect(add.getFailedRequirements({ 1: 'wrong' }, {}, {}, '7.0.0').length).toBe(0); expect(events.emit).toHaveBeenCalledWith('verbose', jasmine.stringMatching(/Ignoring invalid plugin dependency constraint/)); }); it('should return an array with failed plugin requirements ', function () { expect(add.getFailedRequirements({ 'cordova-plugin-camera': '>1.0.0' }, { 'cordova-plugin-camera': '1.0.0' }, {}, '7.0.0')).toEqual([{ dependency: 'cordova-plugin-camera', installed: '1.0.0', required: '>1.0.0' }]); }); it('should return an array with failed cordova requirements ', function () { expect(add.getFailedRequirements({ cordova: '>=7.0.0' }, {}, {}, '6.5.0')).toEqual([{ dependency: 'cordova', installed: '6.5.0', required: '>=7.0.0' }]); }); it('should return an array with failed platform requirements ', function () { expect(add.getFailedRequirements({ 'cordova-android': '>=6.0.0' }, {}, { android: '5.5.0' }, '7.0.0')).toEqual([{ dependency: 'cordova-android', installed: '5.5.0', required: '>=6.0.0' }]); }); }); describe('listUnmetRequirements helper method', function () { it('should emit warnings for failed requirements', function () { add.listUnmetRequirements('cordova-plugin-device', [{ dependency: 'cordova', installed: '6.5.0', required: '>=7.0.0' }]); expect(events.emit).toHaveBeenCalledWith('warn', 'Unmet project requirements for latest version of cordova-plugin-device:'); expect(events.emit).toHaveBeenCalledWith('warn', ' cordova (6.5.0 in project, >=7.0.0 required)'); }); }); describe('findVersion helper method', function () { it('should return null if version is not in array', function () { expect(add.findVersion(['0.0.1', '1.0.0', '2.0.0'], '0.0.0')).toEqual(null); }); it('should return the version if it is in the array', function () { expect(add.findVersion(['0.0.1', '1.0.0', '2.0.0'], '1.0.0')).toEqual('1.0.0'); }); }); }); }); ================================================ FILE: spec/cordova/plugin/index.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const plugin = rewire('../../../src/cordova/plugin'); const cordova_util = require('../../../src/cordova/util'); describe('cordova/plugin', function () { const projectRoot = '/some/path'; const hook_mock = function () {}; beforeEach(function () { spyOn(cordova_util, 'cdProjectRoot').and.returnValue(projectRoot); plugin.__set__('HooksRunner', hook_mock); }); describe('error conditions', function () { it('should require at least one target for add and rm commands', function () { return expectAsync( plugin('add', null) ).toBeRejectedWithError(/one or more plugins/); }); }); describe('handling/massaging of parameters', function () { const cmd = 'add'; beforeEach(function () { spyOn(plugin, cmd).and.returnValue(true); }); it('should be able to handle an array of platform targets', function () { const targets = ['plugin1', 'plugin2', 'plugin3']; return plugin(cmd, targets) .then(function () { expect(plugin[cmd]).toHaveBeenCalledWith(projectRoot, jasmine.any(Object), Object({ options: [], plugins: ['plugin1', 'plugin2', 'plugin3'] })); }); }); it('should be able to handle a single string as a target', function () { const targets = 'plugin1'; return plugin(cmd, targets) .then(function () { expect(plugin[cmd]).toHaveBeenCalledWith(projectRoot, jasmine.any(Object), Object({ options: [], plugins: ['plugin1'] })); }); }); it('should transform targets that start with a dash into options', function () { const targets = '-plugin1'; return plugin(cmd, targets) .then(function () { expect(plugin[cmd]).toHaveBeenCalledWith(projectRoot, jasmine.any(Object), Object({ options: ['-plugin1'], plugins: [] })); }); }); it('should also include targets into a plugins property on options', function () { const options = { save: true }; const targets = 'plugin1'; return plugin(cmd, targets, options) .then(function () { expect(plugin[cmd]).toHaveBeenCalledWith(projectRoot, jasmine.any(Object), Object({ save: true, options: [], plugins: ['plugin1'] })); }); }); }); describe('happy path', function () { it('should direct "add" command to the "add" submodule', function () { spyOn(plugin, 'add').and.returnValue(true); return plugin('add', ['cordova-plugin-splashscreen']) .then(function () { expect(plugin.add).toHaveBeenCalled(); }); }); it('should direct "rm" and "remove" commands to the "remove" submodule', function () { spyOn(plugin, 'remove').and.returnValue(true); return plugin('remove', ['cordova-plugin-splashscreen']) .then(function () { expect(plugin.remove).toHaveBeenCalled(); }); }); it('should direct "list", all other commands and no command at all to the "list" submodule', function () { spyOn(plugin, 'list').and.returnValue(true); return plugin('list') .then(function () { expect(plugin.list).toHaveBeenCalled(); }); }); }); }); ================================================ FILE: spec/cordova/plugin/list.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const list = require('../../../src/cordova/plugin/list'); const plugin_util = require('../../../src/cordova/plugin/util'); const events = require('cordova-common').events; const semver = require('semver'); describe('cordova/plugin/list', function () { const projectRoot = '/some/path'; let hook_mock; const fake_plugins_list = [{ id: 'VRPlugin', version: '1.0.0', name: 'VR' }, { id: 'MastodonSocialPlugin', version: '2.0.0', name: 'Mastodon' }]; beforeEach(function () { hook_mock = jasmine.createSpyObj('hooks runner mock', ['fire']); hook_mock.fire.and.returnValue(Promise.resolve()); spyOn(plugin_util, 'getInstalledPlugins').and.returnValue(Promise.resolve(fake_plugins_list)); spyOn(events, 'emit'); }); it('should fire the before_plugin_ls hook', function () { const opts = { important: 'options' }; return list(projectRoot, hook_mock, opts).then(function () { expect(hook_mock.fire).toHaveBeenCalledWith('before_plugin_ls', opts); }); }); it('should emit a "no plugins added" result if there are no installed plugins', function () { plugin_util.getInstalledPlugins.and.returnValue([]); return list(projectRoot, hook_mock).then(function () { expect(events.emit).toHaveBeenCalledWith('results', jasmine.stringMatching(/No plugins added/)); }); }); it('should warn if plugin list contains dependencies that are missing', function () { const fake_plugins_list = [{ id: 'VRPlugin', deps: '1' }]; plugin_util.getInstalledPlugins.and.returnValue(Promise.resolve(fake_plugins_list)); return list(projectRoot, hook_mock).then(function () { expect(events.emit).toHaveBeenCalledWith('results', jasmine.stringMatching(/WARNING, missing dependency/)); }); }); xit('should warn if plugin list contains a plugin dependency that does not have a version satisfied', function () { spyOn(semver, 'satisfies').and.returnValue(false); const fake_plugins_list = [{ id: 'VRPlugin', version: '1', deps: '1' }]; plugin_util.getInstalledPlugins.and.returnValue(Promise.resolve(fake_plugins_list)); return list(projectRoot, hook_mock).then(function () { expect(events.emit).toHaveBeenCalledWith('results', jasmine.stringMatching(/WARNING, broken dependency/)); }); }); it('should emit a result containing a description of plugins installed', function () { return list(projectRoot, hook_mock).then(function () { expect(events.emit).toHaveBeenCalledWith('results', jasmine.stringMatching('VRPlugin 1.0.0')); expect(events.emit).toHaveBeenCalledWith('results', jasmine.stringMatching('MastodonSocialPlugin 2.0.0')); }); }); it('should fire the after_plugin_ls hook', function () { const opts = { important: 'options' }; return list(projectRoot, hook_mock, opts).then(function () { expect(hook_mock.fire).toHaveBeenCalledWith('after_plugin_ls', opts); }); }); it('should resolve the promise by returning an array of plugin ids installed', function () { return list(projectRoot, hook_mock).then(function (results) { expect(results).toEqual(['VRPlugin', 'MastodonSocialPlugin']); }); }); }); ================================================ FILE: spec/cordova/plugin/plugin_spec_parser.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const pluginSpec = require('../../../src/cordova/plugin/plugin_spec_parser'); describe('methods for parsing npm plugin packages', function () { function checkPluginSpecParsing (testString, id, version) { const parsedSpec = pluginSpec.parse(testString); expect(parsedSpec.id).toEqual(id || testString); expect(parsedSpec.version).toEqual(version); } it('Test 001 : should handle package names with no scope or version', function () { checkPluginSpecParsing('test-plugin', 'test-plugin', null); }); it('Test 002 : should handle package names with a version', function () { checkPluginSpecParsing('test-plugin@1.0.0', 'test-plugin', '1.0.0'); checkPluginSpecParsing('test-plugin@latest', 'test-plugin', 'latest'); }); it('Test 003 : should handle package names with a scope', function () { checkPluginSpecParsing('@test/test-plugin', '@test/test-plugin', null); }); it('Test 004 : should handle package names with a scope and a version', function () { checkPluginSpecParsing('@test/test-plugin@1.0.0', '@test/test-plugin', '1.0.0'); checkPluginSpecParsing('@test/test-plugin@latest', '@test/test-plugin', 'latest'); }); it('Test 005 : should handle invalid package specs', function () { checkPluginSpecParsing('@nonsense', null, null); checkPluginSpecParsing('@/nonsense', null, null); checkPluginSpecParsing('@', null, null); checkPluginSpecParsing('@nonsense@latest', null, null); checkPluginSpecParsing('@/@', null, null); checkPluginSpecParsing('/', null, null); checkPluginSpecParsing('../../@directory', null, null); checkPluginSpecParsing('@directory/../@directory', null, null); checkPluginSpecParsing('./directory', null, null); checkPluginSpecParsing('directory/directory', null, null); checkPluginSpecParsing('http://cordova.apache.org', null, null); }); }); ================================================ FILE: spec/cordova/plugin/remove.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const timers = require('node:timers/promises'); const rewire = require('rewire'); const remove = rewire('../../../src/cordova/plugin/remove'); const cordova_util = require('../../../src/cordova/util'); const metadata = require('../../../src/plugman/util/metadata'); const events = require('cordova-common').events; const plugman = require('../../../src/plugman/plugman'); const plugin_util = require('../../../src/cordova/plugin/util'); describe('cordova/plugin/remove', function () { const projectRoot = '/some/path'; let hook_mock; const cfg_parser_mock = function () {}; const plugin_info_provider_mock = function () {}; let plugin_info; const package_json_mock = jasmine.createSpyObj('package json mock', ['cordova', 'dependencies']); package_json_mock.dependencies = {}; package_json_mock.cordova = {}; package_json_mock.cordova.plugins = {}; beforeEach(function () { spyOn(events, 'emit'); spyOn(fs, 'writeFileSync'); spyOn(fs, 'existsSync'); spyOn(remove, 'validatePluginId'); spyOn(cordova_util, 'listPlatforms').and.returnValue(['ios', 'android']); spyOn(plugman.uninstall, 'uninstallPlatform').and.returnValue(Promise.resolve()); spyOn(plugman.uninstall, 'uninstallPlugin').and.returnValue(Promise.resolve()); hook_mock = jasmine.createSpyObj('hooks runner mock', ['fire']); hook_mock.fire.and.returnValue(Promise.resolve()); cfg_parser_mock.prototype = jasmine.createSpyObj('config parser mock', ['write', 'getPlugin', 'removePlugin']); remove.__set__('ConfigParser', cfg_parser_mock); plugin_info_provider_mock.prototype = jasmine.createSpyObj('plugin info provider mock', ['get', 'getPreferences']); plugin_info_provider_mock.prototype.get = function (directory) { // id version dir getPreferences() engines engines.cordovaDependencies name versions return plugin_info; }; remove.__set__({ PluginInfoProvider: plugin_info_provider_mock, preparePlatforms: jasmine.createSpy('preparePlatforms') }); }); describe('error/warning conditions', function () { it('should require that a plugin be provided', function () { return expectAsync( remove(projectRoot, null) ).toBeRejectedWithError(/No plugin specified/); }); it('should require that a provided plugin be installed in the current project', function () { const opts = { plugins: [undefined] }; return expectAsync( remove(projectRoot, 'plugin', hook_mock, opts) ).toBeRejectedWithError(/is not present in the project/); }); }); describe('happy path', function () { it('should fire the before_plugin_rm hook', function () { const opts = { important: 'options', plugins: [] }; return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(hook_mock.fire).toHaveBeenCalledWith('before_plugin_rm', opts); }); }); it('should call plugman.uninstall.uninstallPlatform for each platform installed in the project and for each provided plugin', function () { spyOn(plugin_util, 'mergeVariables'); remove.validatePluginId.and.returnValue('cordova-plugin-splashscreen'); const opts = { important: 'options', plugins: ['cordova-plugin-splashscreen'] }; return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(plugman.uninstall.uninstallPlatform).toHaveBeenCalled(); expect(events.emit).toHaveBeenCalledWith('verbose', jasmine.stringMatching('plugman.uninstall on plugin "cordova-plugin-splashscreen" for platform "ios"')); expect(events.emit).toHaveBeenCalledWith('verbose', jasmine.stringMatching('plugman.uninstall on plugin "cordova-plugin-splashscreen" for platform "android"')); }); }); it('should trigger a prepare if plugman.uninstall.uninstallPlatform returned something falsy', function () { spyOn(plugin_util, 'mergeVariables'); remove.validatePluginId.and.returnValue('cordova-plugin-splashscreen'); plugman.uninstall.uninstallPlatform.and.returnValue(Promise.resolve(false)); const opts = { important: 'options', plugins: ['cordova-plugin-splashscreen'] }; return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(plugman.uninstall.uninstallPlatform).toHaveBeenCalled(); }); }); it('should call plugman.uninstall.uninstallPlugin once plugin has been uninstalled for each platform', function () { spyOn(plugin_util, 'mergeVariables'); remove.validatePluginId.and.returnValue('cordova-plugin-splashscreen'); const opts = { important: 'options', plugins: ['cordova-plugin-splashscreen'] }; return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(plugman.uninstall.uninstallPlugin).toHaveBeenCalled(); }); }); it('should call uninstallPlugin in order and only finish once all plugins are done', function () { const plugins = ['cordova-plugin-ice-cream', 'cordova-plugin-hot-steam']; // We delay the uninstall of the first plugin to give the second // one the chance to finish early if Promises are handled wrong. const observedOrder = []; plugman.uninstall.uninstallPlugin.and.callFake(target => { return timers.setTimeout(target.endsWith('cream') ? 100 : 0) .then(_ => observedOrder.push(target)); }); spyOn(plugin_util, 'mergeVariables'); remove.validatePluginId.and.returnValues(...plugins); return remove(projectRoot, plugins, hook_mock, { plugins }) .then(_ => expect(observedOrder).toEqual(plugins)); }); describe('when save option is provided or autosave config is on', function () { let opts; beforeEach(function () { spyOn(plugin_util, 'mergeVariables'); spyOn(cordova_util, 'projectConfig').and.returnValue('config.xml'); spyOn(cordova_util, 'findPlugins').and.returnValue([]); spyOn(metadata, 'remove_fetch_metadata').and.returnValue(true); fs.existsSync.and.returnValue(true); remove.validatePluginId.and.returnValue('cordova-plugin-splashscreen'); opts = { save: true, important: 'options', plugins: ['cordova-plugin-splashscreen'] }; }); it('should remove provided plugins from config.xml', function () { spyOn(cordova_util, 'requireNoCache').and.returnValue(true); cfg_parser_mock.prototype.getPlugin.and.returnValue({}); return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(cfg_parser_mock.prototype.removePlugin).toHaveBeenCalled(); expect(cfg_parser_mock.prototype.write).toHaveBeenCalled(); expect(events.emit).toHaveBeenCalledWith('log', jasmine.stringMatching('Removing plugin cordova-plugin-splashscreen from config.xml file')); }); }); it('should remove provided plugins from package.json (if exists)', function () { spyOn(fs, 'readFileSync').and.returnValue('file'); spyOn(cordova_util, 'requireNoCache').and.returnValue(package_json_mock); return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(fs.writeFileSync).toHaveBeenCalled(); expect(events.emit).toHaveBeenCalledWith('log', jasmine.stringMatching('Removing cordova-plugin-splashscreen from package.json')); }); }); }); it('should remove fetch metadata from fetch.json', function () { plugin_info_provider_mock.prototype.getPreferences.and.returnValue(true); spyOn(plugin_util, 'mergeVariables'); spyOn(metadata, 'remove_fetch_metadata').and.callThrough(); remove.validatePluginId.and.returnValue('cordova-plugin-splashscreen'); const opts = { important: 'options', plugins: ['cordova-plugin-splashscreen'] }; return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(metadata.remove_fetch_metadata).toHaveBeenCalled(); expect(events.emit).toHaveBeenCalledWith('verbose', jasmine.stringMatching('Removing plugin cordova-plugin-splashscreen from fetch.json')); }); }); it('should fire the after_plugin_rm hook', function () { const opts = { important: 'options', plugins: [] }; return remove(projectRoot, 'cordova-plugin-splashscreen', hook_mock, opts).then(function () { expect(hook_mock.fire).toHaveBeenCalledWith('after_plugin_rm', opts); }); }); }); }); ================================================ FILE: spec/cordova/plugin/util.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const rewire = require('rewire'); const plugin_util = rewire('../../../src/cordova/plugin/util'); const events = require('cordova-common').events; describe('cordova/plugin/util', function () { const plugin_info_mock = function () {}; const cfg_parser_mock = function () {}; beforeEach(function () { spyOn(fs, 'rmSync'); spyOn(events, 'emit'); cfg_parser_mock.prototype = jasmine.createSpyObj('config parser protytpe mock', ['getPlugin']); plugin_util.__set__('ConfigParser', cfg_parser_mock); plugin_info_mock.prototype = jasmine.createSpyObj('plugin info provider prototype mock', ['getAllWithinSearchPath', 'getPreferences']); plugin_util.__set__('PluginInfoProvider', plugin_info_mock); }); describe('getInstalledPlugins helper method', function () { it('should return result of PluginInfoProvider\'s getAllWithinSearchPath method', function () { const plugins_list = ['VRPlugin', 'MastodonSocialPlugin']; plugin_info_mock.prototype.getAllWithinSearchPath.and.returnValue(plugins_list); expect(plugin_util.getInstalledPlugins('/some/path/to/a/project')).toEqual(plugins_list); }); }); describe('mergeVariables happy path', function () { it('should return variable from cli', function () { cfg_parser_mock.prototype.getPlugin.and.returnValue(undefined); plugin_info_mock.prototype.getPreferences.and.returnValue({}); const opts = { cli_variables: { FCM_VERSION: '9.0.0' } }; expect(plugin_util.mergeVariables(plugin_info_mock.prototype, cfg_parser_mock.prototype, opts)).toEqual({ FCM_VERSION: '9.0.0' }); }); it('should return empty object if there are no config and no cli variables', function () { cfg_parser_mock.prototype.getPlugin.and.returnValue(undefined); plugin_info_mock.prototype.getPreferences.and.returnValue({}); const opts = { cli_variables: {} }; expect(plugin_util.mergeVariables(plugin_info_mock.prototype, cfg_parser_mock.prototype, opts)).toEqual({}); }); it('cli variable takes precedence over config.xml', function () { cfg_parser_mock.prototype.getPlugin.and.returnValue(undefined); plugin_info_mock.prototype.getPreferences.and.returnValue({ name: 'phonegap-plugin-push', spec: '/Users/auso/cordova/phonegap-plugin-push', variables: { FCM_VERSION: '11.0.1' } }); const opts = { cli_variables: { FCM_VERSION: '9.0.0' } }; expect(plugin_util.mergeVariables(plugin_info_mock.prototype, cfg_parser_mock.prototype, opts)).toEqual({ FCM_VERSION: '9.0.0' }); }); it('use config.xml variable if no cli variable is passed in', function () { cfg_parser_mock.prototype.getPlugin.and.returnValue({ name: 'phonegap-plugin-push', spec: '/Users/auso/cordova/phonegap-plugin-push', variables: { FCM_VERSION: '11.0.1' } }); plugin_info_mock.prototype.getPreferences.and.returnValue({}); const opts = { cli_variables: {} }; expect(plugin_util.mergeVariables(plugin_info_mock.prototype, cfg_parser_mock.prototype, opts)).toEqual({ FCM_VERSION: '11.0.1' }); }); it('should get missed variables', function () { cfg_parser_mock.prototype.getPlugin.and.returnValue(undefined); plugin_info_mock.prototype.getPreferences.and.returnValue({ key: 'FCM_VERSION', value: undefined }); const opts = { cli_variables: {} }; expect(function () { plugin_util.mergeVariables(plugin_info_mock.prototype, cfg_parser_mock.prototype, opts); }).toThrow(); expect(fs.rmSync).toHaveBeenCalledWith(undefined, { recursive: true, force: true }); expect(events.emit).toHaveBeenCalledWith('verbose', 'Removing undefined because mandatory plugin variables were missing.'); }); }); }); ================================================ FILE: spec/cordova/prepare/platforms.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const rewire = require('rewire'); const util = require('../../../src/cordova/util'); const platforms = require('../../../src/platforms/platforms'); const PlatformJson = require('cordova-common').PlatformJson; const project_dir = '/some/path'; describe('cordova/prepare/platforms', () => { let preparePlatforms, platform_munger_mock; beforeEach(function () { preparePlatforms = rewire('../../../src/cordova/prepare/platforms'); platform_munger_mock = class { add_config_changes () {} }; spyOn(platform_munger_mock.prototype, 'add_config_changes').and.returnValue({ save_all: jasmine.createSpy('platform munger save mock') }); preparePlatforms.__set__({ ConfigParser: class {}, PlatformMunger: platform_munger_mock }); spyOn(platforms, 'getPlatformApi').and.returnValue({ prepare: jasmine.createSpy('prepare').and.returnValue(Promise.resolve()) }); spyOn(PlatformJson, 'load'); spyOn(util, 'projectConfig').and.returnValue(project_dir); spyOn(util, 'projectWww').and.returnValue(path.join(project_dir, 'www')); }); it('should retrieve the platform API via getPlatformApi per platform provided, and invoke the prepare method from that API', () => { return preparePlatforms(['android'], project_dir, {}).then(() => { expect(platforms.getPlatformApi).toHaveBeenCalledWith('android'); expect(platforms.getPlatformApi().prepare).toHaveBeenCalled(); }); }); it('should handle config changes by invoking add_config_changes and save_all', () => { const pmmp = platform_munger_mock.prototype; return preparePlatforms(['android'], project_dir, {}).then(() => { expect(pmmp.add_config_changes).toHaveBeenCalled(); expect(pmmp.add_config_changes().save_all).toHaveBeenCalled(); }); }); }); ================================================ FILE: spec/cordova/prepare.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const rewire = require('rewire'); const util = require('../../src/cordova/util'); const prepare = rewire('../../src/cordova/prepare'); const restore = require('../../src/cordova/restore-util'); const platforms = require('../../src/platforms/platforms'); const HooksRunner = require('../../src/hooks/HooksRunner'); const PlatformJson = require('cordova-common').PlatformJson; const project_dir = '/some/path'; describe('cordova/prepare', function () { let platform_api_prepare_mock; let platform_api_add_mock; let platform_api_remove_mock; beforeEach(function () { platform_api_prepare_mock = jasmine.createSpy('prepare').and.returnValue(Promise.resolve()); platform_api_add_mock = jasmine.createSpy('add').and.returnValue(Promise.resolve()); platform_api_remove_mock = jasmine.createSpy('remove').and.returnValue(Promise.resolve()); spyOn(platforms, 'getPlatformApi').and.callFake(function (platform, rootDir) { return { prepare: platform_api_prepare_mock, getPlatformInfo: jasmine.createSpy('getPlatformInfo').and.returnValue({ locations: { www: path.join(project_dir, 'platforms', platform, 'www') } }), removePlugin: platform_api_add_mock, addPlugin: platform_api_remove_mock }; }); spyOn(PlatformJson, 'load'); spyOn(HooksRunner.prototype, 'fire').and.returnValue(Promise.resolve()); spyOn(util, 'isCordova').and.returnValue(true); }); describe('main method', function () { beforeEach(function () { spyOn(restore, 'installPlatformsFromConfigXML').and.returnValue(Promise.resolve()); spyOn(restore, 'installPluginsFromConfigXML').and.returnValue(Promise.resolve()); spyOn(util, 'cdProjectRoot').and.returnValue(project_dir); spyOn(util, 'preProcessOptions').and.callFake(function (options) { const platforms = options.platforms || []; return { platforms }; }); spyOn(prepare, 'preparePlatforms').and.returnValue(Promise.resolve()); }); describe('failure', function () { it('should invoke util.preProcessOptions as preflight task checker, which, if fails, should trigger promise rejection and only fire the before_prepare hook', async function () { util.preProcessOptions.and.callFake(function () { throw new Error('preProcessOption error'); }); await expectAsync( prepare({}) ).toBeRejectedWithError('preProcessOption error'); expect(HooksRunner.prototype.fire).toHaveBeenCalledWith('before_prepare', jasmine.any(Object)); }); it('should invoke util.cdProjectRoot as a preflight task checker, which, if fails, should trigger a promise rejection and fire no hooks', async function () { util.cdProjectRoot.and.callFake(function () { throw new Error('cdProjectRoot error'); }); await expectAsync( prepare({}) ).toBeRejectedWithError('cdProjectRoot error'); expect(HooksRunner.prototype.fire).not.toHaveBeenCalled(); }); }); describe('success', function () { it('should fire the before_prepare hook and provide platform and path information as arguments', function () { return prepare({}).then(function () { expect(HooksRunner.prototype.fire).toHaveBeenCalledWith('before_prepare', jasmine.any(Object)); }); }); it('should invoke restore module\'s installPlatformsFromConfigXML method', function () { return prepare({}).then(function () { expect(restore.installPlatformsFromConfigXML).toHaveBeenCalled(); }); }); it('should retrieve PlatformApi instances for each platform provided', function () { return prepare({ platforms: ['android', 'ios'] }).then(function () { expect(platforms.getPlatformApi).toHaveBeenCalledTimes(4); // expect(platforms.getPlatformApi).toHaveBeenCalledWith(['android', path.join('some','path','platforms','android')], ['ios', path.join('some','path','platforms','ios')], ['android'], ['ios']); expect(platforms.getPlatformApi).toHaveBeenCalledWith('android', path.join('/', 'some', 'path', 'platforms', 'android')); expect(platforms.getPlatformApi).toHaveBeenCalledWith('ios', path.join('/', 'some', 'path', 'platforms', 'ios')); expect(platforms.getPlatformApi).toHaveBeenCalledWith('android'); expect(platforms.getPlatformApi).toHaveBeenCalledWith('ios'); }); }); it('should invoke restore module\'s installPluginsFromConfigXML method', function () { return prepare({ platforms: [] }).then(function () { expect(restore.installPluginsFromConfigXML).toHaveBeenCalled(); }); }); it('should invoke preparePlatforms method, providing the appropriate platforms', function () { return prepare({ platforms: ['android'] }).then(function () { expect(prepare.preparePlatforms).toHaveBeenCalledWith(['android'], '/some/path', jasmine.any(Object)); }); }); it('should fire the after_prepare hook and provide platform and path information as arguments', function () { return prepare({ platforms: ['android'] }).then(function () { expect(HooksRunner.prototype.fire).toHaveBeenCalledWith('after_prepare', jasmine.any(Object)); }); }); }); }); }); ================================================ FILE: spec/cordova/project-metadata-apis.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const cordova = require('../../src/cordova/cordova'); describe('retrieval of project metadata', function () { const projectRoot = path.resolve(__dirname, 'fixtures/projects/ProjectMetadata'); it('Test 001 : retrieve platforms saved in config.xml', function () { const androidVersion = '3.7.1'; const browserSrc = 'https://github.com/apache/cordova-browser.git'; return cordova.projectMetadata.getPlatforms(projectRoot) .then(function (platforms) { expect(platforms.length).toBe(2); // Android platform has version defined in deprecated version field - should still work. const androidPlatform = findPlatform(platforms, 'android'); expect(androidPlatform).not.toBeNull(); expect(androidPlatform.version).toBe(androidVersion); expect(androidPlatform.src).toBeUndefined(); // Browser platform has source defined in the spec field. const browserPlatform = findPlatform(platforms, 'browser'); expect(browserPlatform).not.toBeNull(); expect(browserPlatform.version).toBeUndefined(); expect(browserPlatform.src).toBe(browserSrc); }); }); it('Test 002 : retrieve plugins saved in config.xml', function () { const deviceId = 'org.apache.cordova.device'; const deviceVersion = '0.3.0'; const cameraId = 'org.apache.cordova.camera'; const cameraSrc = 'https://github.com/apache/cordova-plugin-camera.git'; const cameraVariableName = 'TEST_VARIABLE'; const cameraVariableValue = 'My Test Variable'; const fileId = 'org.apache.cordova.file'; const fileSource = 'https://github.com/apache/cordova-plugin-file.git'; return cordova.projectMetadata.getPlugins(projectRoot) .then(function (plugins) { expect(plugins.length).toBe(3); // Device plugin uses current spec attribute to specify version - should be returned in version field. const devicePlugin = findPlugin(plugins, deviceId); expect(devicePlugin).not.toBeNull(); expect(devicePlugin.version).toBe(deviceVersion); expect(devicePlugin.src).toBeUndefined(); const deviceVariables = devicePlugin.variables; expect(deviceVariables).not.toBeNull(); expect(Array.isArray(deviceVariables)).toBeTruthy(); expect(deviceVariables.length).toBe(0); // Camera plugin uses deprecated src attribute - still should work. const cameraPlugin = findPlugin(plugins, cameraId); expect(cameraPlugin).not.toBeNull(); expect(cameraPlugin.src).toBe(cameraSrc); expect(cameraPlugin.version).toBeUndefined(); const cameraVariables = cameraPlugin.variables; expect(cameraVariables).not.toBeNull(); expect(cameraVariables.length).toBe(1); expect(cameraVariables[0].name).toBe(cameraVariableName); expect(cameraVariables[0].value).toBe(cameraVariableValue); // File plugin uses deprecated src and version attributes - version should be ignored. const filePlugin = findPlugin(plugins, fileId); expect(filePlugin).not.toBeNull(); expect(filePlugin.version).toBeUndefined(); expect(filePlugin.src).toBe(fileSource); const fileVariables = filePlugin.variables; expect(fileVariables).not.toBeNull(); expect(Array.isArray(fileVariables)).toBeTruthy(); expect(fileVariables.length).toBe(0); }); }); }); function findPlatform (platforms, platformName) { for (let i = 0; i < platforms.length; i++) { if (platforms[i].name === platformName) { return platforms[i]; } } return null; } function findPlugin (plugins, pluginId) { for (let i = 0; i < plugins.length; i++) { if (plugins[i].name === pluginId) { return plugins[i]; } } return null; } ================================================ FILE: spec/cordova/requirements.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const util = require('../../src/cordova/util'); const requirements = rewire('../../src/cordova/requirements'); const project_dir = '/some/path'; describe('cordova/requirements', function () { beforeEach(function () { spyOn(util, 'isCordova').and.returnValue(project_dir); }); describe('main method', function () { it('should fail if no platforms are added', function () { return requirements([]).then(function () { }, function (err) { expect(err).toEqual(jasmine.any(Error)); expect(err.message).toMatch('No platforms added'); }); }); }); }); ================================================ FILE: spec/cordova/restore-util.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const { tmpDir: getTmpDir, testPlatform } = require('../helpers'); const projectTestHelpers = require('../project-test-helpers'); /** * Checks if "cordova/restore-util" is restoring platforms and plugins as * expected given different configurations of package.json and/or config.xml. */ describe('cordova/restore-util', () => { let tmpDir, project, pkgJsonPath, configXmlPath; let restore, cordovaPlatform, cordovaPlugin; const { getPkgJsonPath, getConfigXmlPath, setupBaseProject, getCfg, getPkgJson, setPkgJson } = projectTestHelpers(() => project); beforeEach(() => { tmpDir = getTmpDir('pkgJson'); project = path.join(tmpDir, 'project'); pkgJsonPath = getPkgJsonPath(); configXmlPath = getConfigXmlPath(); delete process.env.PWD; restore = require('../../src/cordova/restore-util'); cordovaPlugin = require('../../src/cordova/plugin'); spyOn(cordovaPlugin, 'add') .and.returnValue(Promise.resolve()); cordovaPlatform = require('../../src/cordova/platform'); spyOn(cordovaPlatform, 'add') .and.returnValue(Promise.resolve()); setupBaseProject(); }); afterEach(() => { process.chdir(__dirname); // Needed to rm the dir on Windows. fs.rmSync(tmpDir, { recursive: true, force: true }); }); function getCfgEngineNames (cfg = getCfg()) { return cfg.getEngines().map(({ name }) => name); } function platformPkgName (platformName) { return platformName.replace(/^(?:cordova-)?/, 'cordova-'); } function expectPluginsInPkgJson (plugins) { const pkgJson = getPkgJson(); expect(pkgJson).toBeDefined(); // Check that cordova.plugins key in package.json contains the expected // variables and ONLY them const variables = plugins.reduce((o, { name, variables }) => { o[name] = variables; return o; }, {}); expect(pkgJson.cordova).toBeDefined(); expect(pkgJson.cordova.plugins).toEqual(variables); // Check that dependencies key in package.json contains the expected specs // We only check the specs for plugins where an expected spec was given const expectedSpecs = plugins.reduce((o, { name, spec }) => { if (spec) o[name] = spec; return o; }, {}); if (Object.keys(expectedSpecs).length > 0) { const specs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies); expect(specs).toEqual(jasmine.objectContaining(expectedSpecs)); } } function expectPlatformAdded (platform) { const expectedOpts = { platforms: jasmine.arrayContaining([ jasmine.stringMatching(platform) ]) }; expect(cordovaPlatform.add).toHaveBeenCalledWith( jasmine.anything(), jasmine.any(String), jasmine.arrayContaining([platform]), jasmine.objectContaining(expectedOpts) ); } function expectPluginAdded (plugin) { const expectedOpts = { plugins: jasmine.arrayContaining([ jasmine.stringMatching(plugin.name) ]) }; if ('variables' in plugin) { expectedOpts.cli_variables = plugin.variables; } expect(cordovaPlugin.add).toHaveBeenCalledWith( jasmine.any(String), jasmine.anything(), jasmine.objectContaining(expectedOpts) ); } function expectPluginsAddedAndSavedToPkgJson (plugins) { expectPluginsInPkgJson(plugins); plugins.forEach(expectPluginAdded); } describe('installPlatformsFromConfigXML', () => { it('Test#001 : should restore saved platform from package.json', () => { setPkgJson('cordova.platforms', [testPlatform]); return restore.installPlatformsFromConfigXML().then(() => { // Check that the platform was properly restored expectPlatformAdded(testPlatform); }); }); it('Test#017 : should restore saved platform from package.json using an URL spec', () => { const PLATFORM = 'browser'; const PLATFORM_URL = 'https://github.com/apache/cordova-browser'; setPkgJson('dependencies', { [platformPkgName(PLATFORM)]: `git+${PLATFORM_URL}.git` }); setPkgJson('cordova.platforms', [PLATFORM]); return restore.installPlatformsFromConfigXML().then(() => { // Check config.xml for spec modification. expect(getCfg().getEngines()).not.toEqual([{ name: PLATFORM, spec: `git+${PLATFORM_URL}.git` }]); // No change to pkg.json. const pkgJson = getPkgJson(); expect(pkgJson.cordova.platforms).toEqual([PLATFORM]); const specs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies); expect(specs[platformPkgName(PLATFORM)]).toEqual(`git+${PLATFORM_URL}.git`); }); }); it('Test#004 : should not modify either file if both have the same platforms', () => { getCfg() .addEngine(testPlatform) .write(); setPkgJson('cordova.platforms', [testPlatform]); const getModTimes = _ => ({ cfg: fs.statSync(configXmlPath).mtime, pkg: fs.statSync(pkgJsonPath).mtime }); const modTimes = getModTimes(); return restore.installPlatformsFromConfigXML().then(() => { expect(getModTimes()).toEqual(modTimes); }); }); it('Test#005 : should update package.json to include platforms from config.xml', () => { const PLATFORM_1 = 'android'; const PLATFORM_2 = 'browser'; getCfg() .addEngine(PLATFORM_1, '7.0.0') .addEngine(PLATFORM_2, '^5.0.3') .write(); setPkgJson('cordova.platforms', [testPlatform]); return restore.installPlatformsFromConfigXML().then(() => { const pkgJson = getPkgJson(); // Expect both pkg.json and config.xml to each have both platforms in their arrays. expect(getCfgEngineNames()).toEqual([PLATFORM_1, PLATFORM_2]); expect(pkgJson.cordova.platforms).toEqual([PLATFORM_1, PLATFORM_2]); // Platform specs from config.xml have been added to pkg.json. const specs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies); expect(specs).toEqual({ [platformPkgName(PLATFORM_1)]: '7.0.0', [platformPkgName(PLATFORM_2)]: '^5.0.3' }); }); }); it('Test#006 : should update a package.json without `cordova` key to match platforms from config.xml', () => { getCfg() .addEngine('android') .write(); return restore.installPlatformsFromConfigXML().then(() => { // Expect no change to config.xml. expect(getCfgEngineNames()).toEqual(['android']); // Expect cordova key and 'android' platform to be added to pkg.json. expect(getPkgJson('cordova.platforms')).toEqual(['android']); }); }); it('Test#007 : should find platform spec', () => { setPkgJson('cordova.platforms', ['android']); setPkgJson('devDependencies', { 'cordova-android': '1.0.0' }); return restore.installPlatformsFromConfigXML(['android'], {}).then(() => { expect(cordovaPlatform.add).toHaveBeenCalledWith( jasmine.anything(), jasmine.anything(), ['android@1.0.0'], jasmine.anything() ); }); }); it('Test#016 : should restore platforms & plugins and create a missing package.json', () => { getCfg() .addEngine(testPlatform) .write(); fs.rmSync(pkgJsonPath, { recursive: true, force: true }); return restore.installPlatformsFromConfigXML().then(() => { // Package.json should be auto-created using values from config.xml const cfg = getCfg(); expect(getPkgJson()).toEqual(jasmine.objectContaining({ name: cfg.packageName().toLowerCase(), version: cfg.version(), displayName: cfg.name() })); }); }); }); // These tests will check the plugin/variable list in package.json and config.xml. describe('installPluginsFromConfigXML', () => { beforeEach(() => { // Add some platform to test the plugins with setPkgJson('cordova.platforms', [testPlatform]); }); it('Test#011 : restores saved plugin', () => { setPkgJson('dependencies', { 'cordova-plugin-camera': '^2.3.0' }); setPkgJson('cordova.plugins', { 'cordova-plugin-camera': { variable_1: 'value_1' } }); return restore.installPluginsFromConfigXML({ save: true }).then(() => { expectPluginAdded({ name: 'cordova-plugin-camera', spec: '^2.3.0', variables: { variable_1: 'value_1' } }); }); }); it('Test#012 : restores saved plugin using an URL spec', () => { const PLUGIN_ID = 'cordova-plugin-splashscreen'; const PLUGIN_URL = 'https://github.com/apache/cordova-plugin-splashscreen'; setPkgJson('dependencies', { [PLUGIN_ID]: `git+${PLUGIN_URL}.git` }); setPkgJson('cordova.plugins', { [PLUGIN_ID]: {} }); return restore.installPluginsFromConfigXML({ save: true }).then(() => { expectPluginAdded({ name: PLUGIN_ID, spec: `git+${PLUGIN_URL}.git`, variables: {} }); }); }); it('Test#013 : does NOT detect plugins from dependencies ', () => { setPkgJson('dependencies', { 'cordova-plugin-device': '~1.0.0' }); setPkgJson('devDependencies', { 'cordova-plugin-camera': '~1.0.0' }); return restore.installPluginsFromConfigXML({ save: true }).then(() => { expect(cordovaPlugin.add).not.toHaveBeenCalled(); }); }); it('Test#014 : adds any plugins only present in config.xml to pkg.json', () => { getCfg() .addPlugin({ name: 'cordova-plugin-device', spec: '~1.0.0', variables: {} }) .write(); setPkgJson('cordova.plugins', { 'cordova-plugin-camera': {} }); setPkgJson('devDependencies', { 'cordova-plugin-camera': '^2.3.0' }); return restore.installPluginsFromConfigXML({ save: true }).then(() => { expectPluginsAddedAndSavedToPkgJson([{ name: 'cordova-plugin-camera', spec: '^2.3.0', variables: {} }, { name: 'cordova-plugin-device', spec: '~1.0.0', variables: {} }]); }); }); it('Test#015 : prefers pkg.json plugins over those from config.xml', () => { getCfg() .addPlugin({ name: 'cordova-plugin-camera', spec: '~2.2.0', variables: { common_var: 'xml', xml_var: 'foo' } }) .write(); setPkgJson('cordova.plugins', { 'cordova-plugin-camera': { common_var: 'json', json_var: 'foo' } }); setPkgJson('devDependencies', { 'cordova-plugin-camera': '^2.3.0' }); return restore.installPluginsFromConfigXML({ save: true }).then(() => { expectPluginsAddedAndSavedToPkgJson([{ name: 'cordova-plugin-camera', spec: '^2.3.0', variables: { common_var: 'json', json_var: 'foo' } }]); }); }); it('Test#018 : does NOT produce conflicting dependencies', () => { getCfg() .addPlugin({ name: 'cordova-plugin-camera', spec: '~2.2.0', variables: { common_var: 'xml', xml_var: 'foo' } }) .write(); setPkgJson('dependencies', { 'cordova-plugin-camera': '^2.3.0' }); return restore.installPluginsFromConfigXML({ save: true }).then(() => { expectPluginsAddedAndSavedToPkgJson([{ name: 'cordova-plugin-camera', spec: '^2.3.0', variables: { common_var: 'xml', xml_var: 'foo' } }]); const pluginOccurences = !!getPkgJson('dependencies.cordova-plugin-camera') + !!getPkgJson('devDependencies.cordova-plugin-camera'); expect(pluginOccurences).toBe(1); }); }); }); }); ================================================ FILE: spec/cordova/run.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const { events } = require('cordova-common'); const platforms = require('../../src/platforms/platforms'); const HooksRunner = require('../../src/hooks/HooksRunner'); const util = require('../../src/cordova/util'); const supported_platforms = Object.keys(platforms).filter(function (p) { return p !== 'www'; }); describe('run command', function () { const project_dir = '/some/path'; let cordovaRun, cordovaPrepare, platformApi, getPlatformApi, targets; beforeEach(function () { spyOn(util, 'isCordova').and.returnValue(project_dir); spyOn(util, 'cdProjectRoot').and.returnValue(project_dir); spyOn(util, 'listPlatforms').and.returnValue(supported_platforms); spyOn(HooksRunner.prototype, 'fire').and.returnValue(Promise.resolve()); spyOn(events, 'emit'); cordovaRun = rewire('../../src/cordova/run'); cordovaPrepare = jasmine.createSpy('cordovaPrepare').and.returnValue(Promise.resolve()); targets = jasmine.createSpy('targets').and.returnValue(Promise.resolve()); cordovaRun.__set__({ cordovaPrepare, targets }); platformApi = { run: jasmine.createSpy('run').and.returnValue(Promise.resolve()), build: jasmine.createSpy('build').and.returnValue(Promise.resolve()), listTargets: jasmine.createSpy('listTargets').and.returnValue(Promise.resolve()) }; getPlatformApi = spyOn(platforms, 'getPlatformApi').and.returnValue(platformApi); }); describe('failure', function () { it('Test 001 : should not run inside a Cordova-based project with no added platforms by calling util.listPlatforms', function () { util.listPlatforms.and.returnValue([]); return expectAsync( cordovaRun() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); }); it('Test 002 : should not run outside of a Cordova-based project', function () { const msg = 'Dummy message about not being in a cordova dir.'; util.cdProjectRoot.and.throwError(new Error(msg)); util.isCordova.and.returnValue(false); return expectAsync( cordovaRun() ).toBeRejectedWithError(msg); }); }); describe('list', function () { it('should warn if platforms are not specified', function () { const result = cordovaRun({ platforms: [], options: { list: true } }); expect(result).toBeFalsy(); expect(events.emit).toHaveBeenCalledWith('warn', 'A platform must be provided when using the "--list" flag.'); }); it('should try to use the Platform API to list emulator targets', function () { return cordovaRun({ platforms: ['ios'], options: { list: true } }) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.listTargets).toHaveBeenCalledWith(jasmine.objectContaining({ options: jasmine.objectContaining({ list: true }) })); }); }); it('should pass options down', function () { return cordovaRun({ platforms: ['ios'], options: { list: true, device: true } }) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.listTargets).toHaveBeenCalledWith(jasmine.objectContaining({ options: jasmine.objectContaining({ device: true }) })); }); }); it('should fall back to the pre-Platform API targets', function () { delete platformApi.listTargets; return cordovaRun({ platforms: ['ios'], options: { list: true } }) .then(function () { expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(targets).toHaveBeenCalledWith(jasmine.objectContaining({ options: jasmine.objectContaining({ list: true }) })); expect(events.emit).toHaveBeenCalledWith('warn', 'Please update to the latest platform release to ensure uninterrupted fetching of target lists.'); }); }); }); describe('success', function () { it('Test 003 : should call prepare before actually run platform ', function () { return cordovaRun(['android', 'ios']).then(function () { expect(cordovaPrepare.calls.argsFor(0)).toEqual([{ platforms: ['android', 'ios'], verbose: false, options: {} }]); }); }); it('Test 004 : should get PlatformApi instance for each platform and call its\' run method', function () { return cordovaRun(['android', 'ios']).then(function () { expect(getPlatformApi).toHaveBeenCalledWith('android'); expect(getPlatformApi).toHaveBeenCalledWith('ios'); expect(platformApi.build).toHaveBeenCalled(); expect(platformApi.run).toHaveBeenCalled(); }); }); it('Test 005 : should pass down parameters', function () { return cordovaRun({ platforms: ['blackberry10'], options: { password: '1q1q' } }).then(function () { expect(cordovaPrepare).toHaveBeenCalledWith({ platforms: ['blackberry10'], options: { password: '1q1q' }, verbose: false }); expect(platformApi.build).toHaveBeenCalledWith({ password: '1q1q' }); expect(platformApi.run).toHaveBeenCalledWith({ password: '1q1q', nobuild: true }); }); }); it('Test 006 : should skip preparing if --noprepare is passed', function () { return cordovaRun({ platforms: ['blackberry10'], options: { noprepare: true } }).then(function () { expect(cordovaPrepare).not.toHaveBeenCalledWith(jasmine.objectContaining({ platforms: ['blackberry10'] })); expect(platformApi.build).toHaveBeenCalledWith({ noprepare: true }); expect(platformApi.run).toHaveBeenCalledWith({ noprepare: true, nobuild: true }); }); }); it('Test 007 : should call platform\'s build method', function () { return cordovaRun({ platforms: ['blackberry10'] }) .then(function () { expect(cordovaPrepare).toHaveBeenCalled(); expect(platformApi.build).toHaveBeenCalledWith({}); expect(platformApi.run).toHaveBeenCalledWith(jasmine.objectContaining({ nobuild: true })); }); }); it('Test 008 : should not call build if --nobuild option is passed', function () { return cordovaRun({ platforms: ['blackberry10'], options: { nobuild: true } }) .then(function () { expect(cordovaPrepare).toHaveBeenCalled(); expect(platformApi.build).not.toHaveBeenCalled(); expect(platformApi.run).toHaveBeenCalledWith(jasmine.objectContaining({ nobuild: true })); }); }); describe('run parameters should not be altered by intermediate build command', function () { beforeEach(function () { platformApi.build.and.callFake(opts => { opts.couldBeModified = 'insideBuild'; return Promise.resolve(); }); }); it('Test 009 : should leave parameters unchanged', function () { return cordovaRun({ platforms: ['blackberry10'], options: { password: '1q1q' } }).then(function () { expect(cordovaPrepare).toHaveBeenCalledWith({ platforms: ['blackberry10'], options: { password: '1q1q', couldBeModified: 'insideBuild' }, verbose: false }); expect(platformApi.build).toHaveBeenCalledWith({ password: '1q1q', couldBeModified: 'insideBuild' }); expect(platformApi.run).toHaveBeenCalledWith({ password: '1q1q', nobuild: true }); }); }); }); }); describe('hooks', function () { describe('when platforms are added', function () { it('Test 010 : should fire before hooks through the hooker module', function () { return cordovaRun(['android', 'ios']).then(function () { expect(HooksRunner.prototype.fire.calls.argsFor(0)) .toEqual(['before_run', { platforms: ['android', 'ios'], verbose: false, options: {} }]); }); }); it('Test 011 : should fire after hooks through the hooker module', function () { return cordovaRun('android').then(function () { expect(HooksRunner.prototype.fire.calls.argsFor(2)) .toEqual(['after_run', { platforms: ['android'], verbose: false, options: {} }]); }); }); }); describe('with no platforms added', function () { it('Test 012 : should not fire the hooker', async function () { util.listPlatforms.and.returnValue([]); await expectAsync( cordovaRun() ).toBeRejectedWithError( 'No platforms added to this project. Please use `cordova platform add `.' ); expect(HooksRunner.prototype.fire).not.toHaveBeenCalled(); }); }); }); }); ================================================ FILE: spec/cordova/util.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const util = require('../../src/cordova/util'); const helpers = require('../helpers'); const cwd = process.cwd(); const origPWD = process.env.PWD; describe('util module', function () { let temp; beforeEach(() => { temp = helpers.tmpDir('cordova.util.spec'); }); afterEach(() => { process.chdir(__dirname); fs.rmSync(temp, { recursive: true, force: true }); }); describe('isCordova method', function () { let somedir, anotherdir; beforeEach(() => { // Base test directory setup somedir = path.join(temp, 'somedir'); anotherdir = path.join(somedir, 'anotherdir'); fs.mkdirSync(anotherdir, { recursive: true }); }); afterEach(function () { process.env.PWD = origPWD; process.chdir(cwd); }); it('Test 002 : should return false if it cannot find a Cordova project directory up the directory tree', function () { expect(util.isCordova(somedir)).toEqual(false); }); it('Test 003 : should recognize a Cordova project directory', function () { fs.mkdirSync(path.join(somedir, 'www'), { recursive: true }); fs.writeFileSync(path.join(somedir, 'www', 'config.xml'), '', 'utf8'); expect(util.isCordova(somedir)).toEqual(somedir); }); it('Test 004 : should ignore PWD when it is undefined', function () { delete process.env.PWD; fs.mkdirSync(path.join(somedir, 'www'), { recursive: true }); fs.writeFileSync(path.join(somedir, 'config.xml'), '', 'utf8'); process.chdir(anotherdir); expect(util.isCordova()).toEqual(somedir); }); it('Test 005 : should use PWD when available', function () { fs.mkdirSync(path.join(somedir, 'www'), { recursive: true }); fs.writeFileSync(path.join(somedir, 'www', 'config.xml'), '', 'utf8'); process.env.PWD = anotherdir; process.chdir(path.sep); expect(util.isCordova()).toEqual(somedir); }); it('Test 006 : should use cwd as a fallback when PWD is not a cordova dir', function () { fs.mkdirSync(path.join(somedir, 'www'), { recursive: true }); fs.writeFileSync(path.join(somedir, 'www', 'config.xml'), '', 'utf8'); process.env.PWD = path.sep; process.chdir(anotherdir); expect(util.isCordova()).toEqual(somedir); }); it('Test 007 : should ignore platform www/config.xml', function () { fs.mkdirSync(path.join(somedir, 'www'), { recursive: true }); fs.mkdirSync(path.join(anotherdir, 'www'), { recursive: true }); fs.writeFileSync(path.join(anotherdir, 'www', 'config.xml'), '', 'utf8'); fs.writeFileSync(path.join(somedir, 'config.xml'), '', 'utf8'); expect(util.isCordova(anotherdir)).toEqual(somedir); }); }); describe('listPlatforms method', function () { it('Test 009 : should only return supported platform directories present in a cordova project dir', function () { const platforms = path.join(temp, 'platforms'); fs.mkdirSync(path.join(platforms, 'android'), { recursive: true }); fs.mkdirSync(path.join(platforms, 'ios'), { recursive: true }); fs.mkdirSync(path.join(platforms, 'wp8'), { recursive: true }); fs.mkdirSync(path.join(platforms, 'atari'), { recursive: true }); // create a typical platforms.json file, it should not be returned as a platform fs.writeFileSync(path.join(platforms, 'platforms.json'), '{}', 'utf8'); const res = util.listPlatforms(temp); expect(res.length).toEqual(4); }); }); describe('getInstalledPlatformsWithVersions method', function () { it('Test 010 : should get the supported platforms in the cordova project dir along with their reported versions', function () { const PLATFORM = 'cordova-android'; const platformPath = path.join(temp, 'platforms', PLATFORM); return helpers.getFixture('androidApp').copyTo(platformPath) .then(_ => util.getInstalledPlatformsWithVersions(temp)) .then(versions => expect(versions[PLATFORM]).toBe('13.0.0')); }); }); describe('getPlatformVersion method', () => { it('should get the version from a legacy platform', () => { const PLATFORM_VERSION = '1.2.3-dev'; fs.mkdirSync(path.join(temp, 'cordova'), { recursive: true }); fs.writeFileSync(path.join(temp, 'cordova', 'version'), ` #!/usr/bin/env node console.log('${PLATFORM_VERSION}'); `.trim(), 'utf8'); const version = util.getPlatformVersion(temp); expect(version).toBe(PLATFORM_VERSION); }); }); describe('findPlugins method', function () { let pluginsDir, plugins; function expectFindPluginsToReturn (expectedPlugins) { expect(util.findPlugins(pluginsDir)) .toEqual(jasmine.arrayWithExactContents(expectedPlugins)); } beforeEach(function () { pluginsDir = path.join(temp, 'plugins'); plugins = ['foo', 'bar', 'baz']; plugins.forEach(plugin => { fs.mkdirSync(path.join(pluginsDir, plugin), { recursive: true }); }); }); it('Test 011 : should only return plugin directories present in a cordova project dir', function () { expectFindPluginsToReturn(plugins); }); it('Test 012 : should not return ".svn" directories', function () { fs.mkdirSync(path.join(pluginsDir, '.svn'), { recursive: true }); expectFindPluginsToReturn(plugins); }); it('Test 013 : should not return "CVS" directories', function () { fs.mkdirSync(path.join(pluginsDir, 'CVS'), { recursive: true }); expectFindPluginsToReturn(plugins); }); it('Test 031 : should return plugin symlinks', function () { const linkedPluginPath = path.join(temp, 'linked-plugin'); const pluginLinkPath = path.join(pluginsDir, 'plugin-link'); fs.mkdirSync(linkedPluginPath, { recursive: true }); fs.symlinkSync(linkedPluginPath, pluginLinkPath); expectFindPluginsToReturn(plugins.concat('plugin-link')); }); it('Test 032 : should work with scoped plugins', () => { fs.mkdirSync(path.join(pluginsDir, '@baz/foo'), { recursive: true }); expectFindPluginsToReturn(plugins.concat('@baz/foo')); }); }); describe('preprocessOptions method', function () { let isCordova, listPlatforms; const DEFAULT_OPTIONS = { // 'android' is here because we create a spy // for listPlatforms below that returns 'android' platforms: ['android'], verbose: false }; beforeEach(function () { isCordova = spyOn(util, 'isCordova').and.returnValue('/fake/path'); listPlatforms = spyOn(util, 'listPlatforms').and.returnValue(['android']); }); it('Test 014 : should throw if called outside of cordova project', function () { isCordova.and.returnValue(false); expect(function () { util.preProcessOptions(); }).toThrow(); }); it('Test 015 : should throw when no platforms added to project', function () { listPlatforms.and.returnValue([]); expect(function () { util.preProcessOptions(); }).toThrow(); }); it('Test 016 : should return default options when no arguments passed', function () { expect(util.preProcessOptions()).toEqual(jasmine.objectContaining(DEFAULT_OPTIONS)); }); it('Test 017 : should accept single string argument as platform name', function () { expect(util.preProcessOptions('ios')).toEqual(jasmine.objectContaining({ platforms: ['ios'] })); }); it('Test 018 : should accept array of strings as platform names', function () { expect(util.preProcessOptions(['ios', 'windows'])).toEqual(jasmine.objectContaining({ platforms: ['ios', 'windows'] })); }); it('Test 019 : should fall back to installed platform if input doesn\'t contain platforms list', function () { expect(util.preProcessOptions({ verbose: true })) .toEqual(jasmine.objectContaining({ platforms: ['android'], verbose: true })); }); it('Test 020 : should pick buildConfig if no option is provided, but buildConfig.json exists', function () { spyOn(fs, 'existsSync').and.returnValue(true); // Using path.join below to normalize path separators expect(util.preProcessOptions()) .toEqual(jasmine.objectContaining({ options: { buildConfig: path.join('/fake/path/build.json') } })); }); describe('getPlatformApiFunction', function () { it('Test 030 : successfully find platform Api', function () { const FIXTURE_PROJECT = path.join(__dirname, 'fixtures/projects/platformApi/'); const API_PATH = path.join(FIXTURE_PROJECT, 'platforms/windows/cordova/Api.js'); const Api = util.getPlatformApiFunction(API_PATH, 'windows'); expect(Api.createPlatform().platform).toBe('windows'); }); it('successfully loads platform Api from node_modules', () => { const Api = util.getPlatformApiFunction(null, 'android'); expect(Api).toBe(require('cordova-android')); }); }); }); }); ================================================ FILE: spec/fixture-helper.js ================================================ /*! Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const fsp = require('node:fs/promises'); const path = require('node:path'); const { ConfigParser, events } = require('cordova-common'); const platformAdd = require('../src/cordova/platform').add; const HooksRunner = require('../src/hooks/HooksRunner'); /** * Creates a function that provides access to various fixtures by name. * * @param {Function} tmpDir creates a temp dir when called and returns its path * @returns {Function} that provides access to various fixtures by name */ module.exports = function fixtureHelper (tmpDir) { let fixturesBaseDir; // Setup and teardown the directory where we setup our fixtures beforeAll(() => { fixturesBaseDir = tmpDir(); }); afterAll(() => fs.rmSync(fixturesBaseDir, { recursive: true, force: true })); // The recipes for building the different kinds of fixture. // Resolve to the fixture path. const fixtureConstructors = { // Creates a stand-alone cordova-android app (platform-centered) androidApp () { const PlatformApi = require('cordova-android'); const appPath = path.join(fixturesBaseDir, 'android-app'); // We need to provide a ConfigParser instance to createPlatform :( const cfgXmlPath = require.resolve('cordova-android/templates/project/res/xml/config.xml'); const config = new ConfigParser(cfgXmlPath); // Create the app folder and return its path return PlatformApi.createPlatform(appPath, config, null, events) // Make our node_modules accessible from the app dir to make // platform modules work when they are required from the app dir. .then(_ => linkToGlobalModulesFrom(appPath)) .then(_ => appPath); }, // Creates a cordova project with one platform installed async projectWithPlatform () { const projectFixture = path.join(__dirname, 'cordova/fixtures/basePkgJson'); const projectPath = path.join(fixturesBaseDir, 'project-with-platform'); fs.cpSync(projectFixture, projectPath, { recursive: true }); process.chdir(projectPath); // Talk about a clunky interface :( const platforms = ['android']; const opts = { platforms, save: true }; const hooksRunner = new HooksRunner(projectPath); await platformAdd(hooksRunner, projectPath, platforms, opts); return projectPath; }, androidPlatform () { return path.dirname(require.resolve('cordova-android/package')); } }; // The fixture cache; contains promises to paths of already created fixtures const fixturePromises = {}; // Finally, the public interface we provide our consumers. We intentionally // provide only a method to copy the fixtures, so that tests cannot alter // the global fixtures in any way. return function getFixture (name) { if (!(name in fixturePromises)) { fixturePromises[name] = Promise.resolve(fixtureConstructors[name]()); } return { async copyTo (targetPath) { const fixturePath = await fixturePromises[name]; fs.cpSync(fixturePath, targetPath, { recursive: true }); return targetPath; } }; }; }; function linkToGlobalModulesFrom (dir) { return fsp.symlink( path.join(__dirname, '../node_modules'), path.join(dir, 'node_modules'), 'junction' ); } ================================================ FILE: spec/helper.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const { events } = require('cordova-common'); const { SpecReporter } = require('jasmine-spec-reporter'); // Node.js throws an Error if the `error` event is emitted on a EventEmitter // instance that has no handlers attached for it. This often masks the actual // error that was causing the issue. So we attach a listener here. events.on('error', console.error); jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; jasmine.getEnv().clearReporters(); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayPending: true, displayDuration: true }, summary: { displayDuration: true, displayStacktrace: 'raw' } })); ================================================ FILE: spec/helpers.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const fs = require('node:fs'); const os = require('node:os'); const ConfigParser = require('cordova-common').ConfigParser; const fixtureHelper = require('./fixture-helper'); // Just use Android everywhere; we're mocking out any calls to the `android` binary. module.exports.testPlatform = 'android'; function getConfigPath (dir) { // if path ends with 'config.xml', return it if (dir.endsWith('config.xml')) { return dir; } // otherwise, add 'config.xml' to the end of it return path.join(dir, 'config.xml'); } module.exports.tmpDir = function (suffix = 'test') { const dir = path.join(os.tmpdir(), `cordova-lib-${suffix}-`); return fs.realpathSync(fs.mkdtempSync(dir)); }; /** * Provides access to various fixtures by name. * * @param {String} fixtureName name of fixture that should be accessed * @returns {Object} with a `copyTo` method that asynchronously copies the * fixture to the given target directory */ module.exports.getFixture = fixtureHelper(() => exports.tmpDir('fixtures')); /** * Sets the default timeout for the current suite. * * Restores the previous timeout after the suite has finished. The timeout * applies to all `before*`, `after*` and `it` blocks within the suite. * * Must be called before defining any of the aforementioned blocks. * * @param {Number} timeout The desired default timeout in ms */ module.exports.setDefaultTimeout = timeout => { const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; // Ensure new default timeout applies to beforeAll blocks beforeAll(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; }); // Refresh setting before every test, just to be safe beforeEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; }); // Revert to original setting after the last afterAll afterAll(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); }; module.exports.setEngineSpec = function (appPath, engine, spec) { appPath = getConfigPath(appPath); const parser = new ConfigParser(appPath); parser.removeEngine(engine); parser.addEngine(engine, spec); parser.write(); }; module.exports.getEngineSpec = function (appPath, engine) { appPath = getConfigPath(appPath); const parser = new ConfigParser(appPath); const engines = parser.getEngines(); for (let i = 0; i < engines.length; i++) { if (engines[i].name === module.exports.testPlatform) { return engines[i].spec; } } return null; }; module.exports.removeEngine = function (appPath, engine) { appPath = getConfigPath(appPath); const parser = new ConfigParser(appPath); parser.removeEngine(module.exports.testPlatform); parser.write(); }; module.exports.setPluginSpec = function (appPath, plugin, spec) { appPath = getConfigPath(appPath); const parser = new ConfigParser(appPath); const p = parser.getPlugin(plugin); let variables = []; if (p) { parser.removePlugin(p.name); if (p.variables.length && p.variables.length > 0) { variables = p.variables; } } parser.addPlugin({ name: plugin, spec }, variables); parser.write(); }; module.exports.getPluginSpec = function (appPath, plugin) { appPath = getConfigPath(appPath); const parser = new ConfigParser(appPath); const p = parser.getPlugin(plugin); if (p) { return p.spec; } return null; }; module.exports.getPluginVariable = function (appPath, plugin, variable) { appPath = getConfigPath(appPath); const parser = new ConfigParser(appPath); const p = parser.getPlugin(plugin); if (p && p.variables) { return p.variables[variable]; } return null; }; module.exports.removePlugin = function (appPath, plugin) { appPath = getConfigPath(appPath); const parser = new ConfigParser(appPath); parser.removePlugin(plugin); parser.write(); }; module.exports.getConfigContent = function (appPath) { const configFile = path.join(appPath, 'config.xml'); return fs.readFileSync(configFile, 'utf-8'); }; module.exports.writeConfigContent = function (appPath, configContent) { const configFile = path.join(appPath, 'config.xml'); fs.writeFileSync(configFile, configContent, 'utf-8'); }; module.exports.asymmetricMatchers = { pathNormalizingTo (expectedPath) { return { asymmetricMatch: actualPath => path.normalize(actualPath) === expectedPath, jasmineToString: _ => `` }; } }; const customMatchers = { toExist: () => ({ compare (file) { const pass = fs.existsSync(file); const expectation = (pass ? 'not ' : '') + 'to exist'; return { pass, message: `expected ${file} ${expectation}` }; } }) }; // Add our custom matchers beforeEach(() => jasmine.addMatchers(customMatchers)); ================================================ FILE: spec/hooks/Context.spec.js ================================================ /*! Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const { CordovaError } = require('cordova-common'); describe('hooks/Context', () => { let Context; beforeEach(() => { Context = rewire('../../src/hooks/Context'); }); describe('cordova', () => { let context; beforeEach(() => { spyOn(Context.prototype, 'requireCordovaModule'); context = new Context(); }); it('is only loaded when accessed', () => { expect(context.requireCordovaModule).not.toHaveBeenCalled(); }); it('is set to require("cordova-lib").cordova', () => { const cordova = Symbol('cordova'); context.requireCordovaModule.and.returnValue({ cordova }); expect(context.cordova).toBe(cordova); expect(context.requireCordovaModule).toHaveBeenCalledWith('cordova-lib'); }); }); describe('requireCordovaModule', () => { let requireCordovaModule; beforeEach(() => { requireCordovaModule = Context.prototype.requireCordovaModule; }); it('correctly resolves cordova-* dependencies', () => { const cordovaCommon = require('cordova-common'); expect(requireCordovaModule('cordova-common')).toBe(cordovaCommon); }); it('correctly resolves inner modules of cordova-* dependencies', () => { const MODULE = 'cordova-common/src/events'; expect(requireCordovaModule(MODULE)).toBe(require(MODULE)); }); it('correctly resolves cordova-lib', () => { const cordovaLib = require('../..'); expect(requireCordovaModule('cordova-lib')).toBe(cordovaLib); }); it('correctly resolves inner modules of cordova-lib', () => { const platforms = require('../../src/platforms/platforms'); expect(requireCordovaModule('cordova-lib/src/platforms/platforms')).toBe(platforms); }); it('correctly resolves inner modules of cordova-lib', () => { const platforms = require('../../src/platforms/platforms'); expect(requireCordovaModule('cordova-lib/src/platforms/platforms')).toBe(platforms); }); describe('with stubbed require', () => { let requireSpy; beforeEach(() => { requireSpy = jasmine.createSpy('require'); Context.__set__({ require: requireSpy }); }); it('correctly handles module names that start with "cordova-lib"', () => { requireCordovaModule('cordova-libre'); expect(requireSpy).toHaveBeenCalledWith('cordova-libre'); }); it('throws if non-cordova module is requested', () => { const expectErrorOnRequire = m => expect(() => requireCordovaModule(m)) .toThrowError(CordovaError, /non-cordova module/); expectErrorOnRequire('q'); expectErrorOnRequire('.'); expectErrorOnRequire('..'); expectErrorOnRequire('./asd'); expectErrorOnRequire('../qwe'); expectErrorOnRequire('/foo'); }); }); }); }); ================================================ FILE: spec/plugman/install.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const semver = require('semver'); const rewire = require('rewire'); const { events, PlatformJson } = require('cordova-common'); const { spy: emitSpyHelper } = require('../common'); const knownPlatforms = require('../../src/platforms/platforms'); const { tmpDir, getFixture } = require('../helpers'); const temp_dir = tmpDir('plugman-install-test'); const project = path.join(temp_dir, 'android_install'); const plugins_dir = path.join(__dirname, 'plugins'); const plugins_install_dir = path.join(project, 'cordova', 'plugins'); function pluginDir (pluginId) { const base = pluginId.length === 1 ? path.join(plugins_dir, 'dependencies') : plugins_dir; return path.join(base, pluginId); } const results = {}; const TIMEOUT = 9000; const existsSync = fs.existsSync; // Mocked functions for tests const fake = { existsSync: { noPlugins (path) { // fake installed plugin directories as 'not found' if (path.slice(-5) !== '.json' && path.indexOf(plugins_install_dir) >= 0) { return false; } return existsSync(path); } }, fetch: { dependencies (id, dir) { if (id === pluginDir('A')) { return Promise.resolve(id); } // full path to plugin return Promise.resolve(path.join(plugins_dir, 'dependencies', id)); } } }; describe('plugman/install', () => { let install = require('../../src/plugman/install'); let fetchSpy; let execaSpy; beforeAll(() => { let api; return getFixture('androidApp').copyTo(project) .then(_ => { results.emit_results = []; events.on('results', result => results.emit_results.push(result)); // Every time when addPlugin is called it will return some truthy value const returnValues = [true, {}, [], 'foo', () => {}][Symbol.iterator](); api = knownPlatforms.getPlatformApi('android', project); const addPluginOrig = api.addPlugin; spyOn(api, 'addPlugin').and.callFake(function () { return addPluginOrig.apply(api, arguments) .then(_ => returnValues.next()); }); return install('android', project, pluginDir('org.test.plugins.dummyplugin')); }) .then(result => { expect(result).toBeTruthy(); return install('android', project, pluginDir('com.cordova.engine')); }).then(result => { expect(result).toBeTruthy(); return install('android', project, pluginDir('org.test.plugins.childbrowser')); }).then(result => { expect(result).toBeTruthy(); return install('android', project, pluginDir('com.adobe.vars'), plugins_install_dir, { cli_variables: { API_KEY: 'batman' } }); }).then(result => { expect(result).toBeTruthy(); return install('android', project, pluginDir('org.test.defaultvariables'), plugins_install_dir, { cli_variables: { API_KEY: 'batman' } }); }).then(result => { expect(result).toBeTruthy(); api.addPlugin.and.callThrough(); events.removeAllListeners('results'); }); }, 3 * TIMEOUT); afterAll(() => { fs.rmSync(temp_dir, { recursive: true, force: true }); }); beforeEach(() => { install = rewire('../../src/plugman/install'); fetchSpy = jasmine.createSpy('plugmanFetch').and.returnValue(Promise.resolve(pluginDir('com.cordova.engine'))); install.__set__({ plugmanFetch: fetchSpy }); execaSpy = jasmine.createSpy('execa'); execaSpy.and.returnValue(Promise.resolve({ stdout: '' })); install.__set__('execa', execaSpy); spyOn(fs, 'mkdirSync'); spyOn(fs, 'writeFileSync'); spyOn(fs, 'cpSync'); spyOn(fs, 'rmSync'); spyOn(PlatformJson.prototype, 'addInstalledPluginToPrepareQueue'); }); describe('success', () => { it('Test 002 : should emit a results event with platform-agnostic ', () => { // org.test.plugins.childbrowser expect(results.emit_results[0]).toBe('No matter what platform you are installing to, this notice is very important.'); }, TIMEOUT); it('Test 003 : should emit a results event with platform-specific ', () => { // org.test.plugins.childbrowser expect(results.emit_results[1]).toBe('Please make sure you read this because it is very important to complete the installation of your plugin.'); }, TIMEOUT); it('Test 004 : should interpolate variables into tags', () => { // VariableBrowser expect(results.emit_results[2]).toBe('Remember that your api key is batman!'); }, TIMEOUT); it('Test 005 : should call fetch if provided plugin cannot be resolved locally', () => { fetchSpy.and.returnValue(Promise.resolve(pluginDir('org.test.plugins.dummyplugin'))); spyOn(fs, 'existsSync').and.callFake(fake.existsSync.noPlugins); return install('android', project, 'CLEANYOURSHORTS') .then(() => { expect(fetchSpy).toHaveBeenCalled(); }); }); describe('engine versions', () => { let satisfies; beforeEach(() => { satisfies = spyOn(semver, 'satisfies').and.returnValue(true); spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false); }); it('Test 007 : should check version if plugin has engine tag', () => { execaSpy.and.returnValue(Promise.resolve({ stdout: '2.5.0' })); return install('android', project, pluginDir('com.cordova.engine')) .then(() => { expect(satisfies).toHaveBeenCalledWith('2.5.0', '>=1.0.0', { loose: true, includePrerelease: true }); }); }, TIMEOUT); it('Test 008 : should check version and munge it a little if it has "rc" in it so it plays nice with semver (introduce a dash in it)', () => { execaSpy.and.returnValue(Promise.resolve({ stdout: '3.0.0rc1' })); return install('android', project, pluginDir('com.cordova.engine')) .then(() => { expect(satisfies).toHaveBeenCalledWith('3.0.0-rc1', '>=1.0.0', { loose: true, includePrerelease: true }); }); }, TIMEOUT); it('Test 009 : should check specific platform version over cordova version if specified', () => { execaSpy.and.returnValue(Promise.resolve({ stdout: '3.1.0' })); return install('android', project, pluginDir('com.cordova.engine-android')) .then(() => { expect(satisfies).toHaveBeenCalledWith('3.1.0', '>=3.1.0', { loose: true, includePrerelease: true }); }); }, TIMEOUT); it('Test 010 : should check platform sdk version if specified', () => { const cordovaVersion = require('../../package.json').version; execaSpy.and.returnValue(Promise.resolve({ stdout: '18' })); return install('android', project, pluginDir('com.cordova.engine-android')) .then(() => { expect(satisfies.calls.count()).toBe(3); // expect(satisfies.calls.argsFor(0)).toEqual([cordovaVersion, '>=3.0.0', { loose: true, includePrerelease: true }]); // expect(satisfies.calls.argsFor(1)).toEqual(['18.0.0', '>=3.1.0', { loose: true, includePrerelease: true }]); // expect(satisfies.calls.argsFor(2)).toEqual(['18.0.0', '>=18', { loose: true, includePrerelease: true }]); }); }, TIMEOUT); it('Test 011 : should check engine versions', () => { return install('android', project, pluginDir('com.cordova.engine')) .then(() => { const plugmanVersion = require('../../package.json').version; const cordovaVersion = require('../../package.json').version; expect(satisfies.calls.count()).toBe(4); // expect(satisfies.calls.argsFor(0)).toEqual([cordovaVersion, '>=2.3.0', { loose: true, includePrerelease: true }]); // expect(satisfies.calls.argsFor(1)).toEqual([plugmanVersion, '>=0.10.0', { loose: true, includePrerelease: true }]); // expect(satisfies.calls.argsFor(2)).toEqual([null, '>=1.0.0', { loose: true, includePrerelease: true }]); // expect(satisfies.calls.argsFor(3)).toEqual([null, '>=3.0.0', { loose: true, includePrerelease: true }]); }); }, TIMEOUT); it('Test 012 : should not check custom engine version that is not supported for platform', () => { return install('blackberry10', project, pluginDir('com.cordova.engine')) .then(() => { // Version >=3.0.0 of `mega-boring-plugin` is specified with platform="ios|android" expect(satisfies.calls.count()).toBe(3); expect(satisfies).not.toHaveBeenCalledWith(jasmine.anything(), '>=3.0.0', { loose: true, includePrerelease: true }); }); }, TIMEOUT); }); describe('with dependencies', () => { let emit; beforeEach(() => { spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false); spyOn(fs, 'existsSync').and.callFake(fake.existsSync.noPlugins); fetchSpy.and.callFake(fake.fetch.dependencies); emit = spyOn(events, 'emit'); execaSpy.and.returnValue(Promise.resolve({ stdout: '9.0.0' })); class PlatformApiMock { static addPlugin () { return Promise.resolve(); } } spyOn(knownPlatforms, 'getPlatformApi').and.returnValue(PlatformApiMock); }); it('Test 015 : should install specific version of dependency', () => { // Plugin I depends on C@1.0.0 emit.calls.reset(); return install('android', project, pluginDir('I')) .then(() => { expect(fetchSpy).toHaveBeenCalledWith('C@1.0.0', jasmine.any(String), jasmine.any(Object)); expect(emitSpyHelper.getInstall(emit)).toEqual([ 'Install start for "C" on android.', 'Install start for "I" on android.' ]); }, TIMEOUT); }, TIMEOUT); it('Test 016 : should install any dependent plugins if missing', () => { emit.calls.reset(); return install('android', project, pluginDir('A')) .then(() => { expect(emitSpyHelper.getInstall(emit)).toEqual([ 'Install start for "C" on android.', 'Install start for "D" on android.', 'Install start for "A" on android.' ]); }); }, TIMEOUT); it('Test 017 : should install any dependent plugins from registry when url is not defined', () => { emit.calls.reset(); return install('android', project, pluginDir('A')) .then(() => { expect(emitSpyHelper.getInstall(emit)).toEqual([ 'Install start for "C" on android.', 'Install start for "D" on android.', 'Install start for "A" on android.' ]); }); }, TIMEOUT); it('Test 018 : should process all dependent plugins with alternate routes to the same plugin', () => { // Plugin F depends on A, C, D and E emit.calls.reset(); return install('android', project, pluginDir('F')) .then(() => { expect(emitSpyHelper.getInstall(emit)).toEqual([ 'Install start for "C" on android.', 'Install start for "D" on android.', 'Install start for "A" on android.', 'Install start for "D" on android.', 'Install start for "F" on android.' ]); }); }, TIMEOUT); it('Test 019 : should throw if there is a cyclic dependency', () => { return expectAsync( install('android', project, pluginDir('G')) ).toBeRejectedWithError( 'Cyclic dependency from G to H' ); }, TIMEOUT); it('Test 020 : install subdir relative to top level plugin if no fetch meta', () => { return install('android', project, pluginDir('B')) .then(() => { expect(emitSpyHelper.getInstall(emit)).toEqual([ 'Install start for "D" on android.', 'Install start for "E" on android.', 'Install start for "B" on android.' ]); }); }, TIMEOUT); it('Test 021 : install uses meta data (if available) of top level plugin source', () => { // Fake metadata so plugin 'B' appears from 'meta/B' const meta = require('../../src/plugman/util/metadata'); spyOn(meta, 'get_fetch_metadata').and.callFake(() => { return { source: { type: 'dir', url: path.join(pluginDir('B'), '..', 'meta') } }; }); return install('android', project, pluginDir('B')) .then(() => { expect(emitSpyHelper.getInstall(emit)).toEqual([ 'Install start for "D" on android.', 'Install start for "E" on android.', 'Install start for "B" on android.' ]); const copy = emitSpyHelper.startsWith(emit, 'Copying from'); expect(copy.length).toBe(3); expect(copy[0].indexOf(path.normalize('meta/D')) > 0).toBe(true); expect(copy[1].indexOf(path.normalize('meta/subdir/E')) > 0).toBe(true); }); }, TIMEOUT); }); }); describe('failure', () => { it('Test 023 : should throw if variables are missing', () => { spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false); return expectAsync( install('android', project, pluginDir('com.adobe.vars')) ).toBeRejectedWithError( 'Variable(s) missing: API_KEY' ); }, TIMEOUT); it('Test 025 :should not fail when trying to install plugin less than minimum version. Skip instead ', () => { spyOn(semver, 'satisfies').and.returnValue(false); execaSpy.and.returnValue(Promise.resolve({ stdout: '0.0.1' })); return install('android', project, pluginDir('com.cordova.engine')) .then(result => { expect(result).toBe(true); }); }, TIMEOUT); it('Test 026 : should throw if the engine scriptSrc escapes out of the plugin dir.', () => { spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false); return expectAsync( install('android', project, pluginDir('org.test.invalid.engine.script')) ).toBeRejectedWithError(/^Security violation:/); }, TIMEOUT); it('Test 027 : should throw if a non-default cordova engine platform attribute is not defined.', () => { spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false); return expectAsync( install('android', project, pluginDir('org.test.invalid.engine.no.platform')) ).toBeRejectedWithError(); }, TIMEOUT); it('Test 028 : should throw if a non-default cordova engine scriptSrc attribute is not defined.', () => { spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false); return expectAsync( install('android', project, pluginDir('org.test.invalid.engine.no.scriptSrc')) ).toBeRejectedWithError(); }, TIMEOUT); }); }); ================================================ FILE: spec/plugman/plugins/com.adobe.vars/plugin.xml ================================================ Use Variables Remember that your api key is $API_KEY! $PACKAGE_NAME $PACKAGE_NAME ================================================ FILE: spec/plugman/plugins/com.cordova.engine/megaBoringVersion ================================================ ./com.cordova.engine/megaBoringVersion ================================================ FILE: spec/plugman/plugins/com.cordova.engine/megaFunVersion ================================================ ./com.cordova.engine/megaFunVersion ================================================ FILE: spec/plugman/plugins/com.cordova.engine/plugin.xml ================================================ Engine Choo Choo ================================================ FILE: spec/plugman/plugins/com.cordova.engine-android/plugin.xml ================================================ Engine Choo Choo ================================================ FILE: spec/plugman/plugins/dependencies/A/plugin.xml ================================================ Plugin A ================================================ FILE: spec/plugman/plugins/dependencies/A/src/android/A.java ================================================ ./dependencies/A/src/android/A.java ================================================ FILE: spec/plugman/plugins/dependencies/A/src/ios/APluginCommand.h ================================================ ./dependencies/A/src/ios/APluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/A/src/ios/APluginCommand.m ================================================ ./dependencies/A/src/ios/APluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/A/www/plugin-a.js ================================================ //dependencies/A/www/plugin-a.js ================================================ FILE: spec/plugman/plugins/dependencies/B/plugin.xml ================================================ Plugin B ================================================ FILE: spec/plugman/plugins/dependencies/B/src/android/B.java ================================================ ./dependencies/B/src/android/B.java ================================================ FILE: spec/plugman/plugins/dependencies/B/src/ios/BPluginCommand.h ================================================ ./dependencies/B/src/ios/BPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/B/src/ios/BPluginCommand.m ================================================ ./dependencies/B/src/ios/BPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/B/www/plugin-b.js ================================================ //dependencies/B/www/plugin-b.js ================================================ FILE: spec/plugman/plugins/dependencies/C/plugin.xml ================================================ Plugin C ================================================ FILE: spec/plugman/plugins/dependencies/C/src/android/C.java ================================================ ./dependencies/C/src/android/C.java ================================================ FILE: spec/plugman/plugins/dependencies/C/src/ios/CPluginCommand.h ================================================ ./dependencies/C/src/ios/CPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/C/src/ios/CPluginCommand.m ================================================ ./dependencies/C/src/ios/CPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/C/www/plugin-c.js ================================================ //dependencies/C/www/plugin-c.js ================================================ FILE: spec/plugman/plugins/dependencies/C@1.0.0/package.json ================================================ { "name": "c", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/plugman/plugins/dependencies/C@1.0.0/plugin.xml ================================================ Plugin C ================================================ FILE: spec/plugman/plugins/dependencies/C@1.0.0/src/android/C.java ================================================ ./dependencies/C@1.0.0/src/android/C.java ================================================ FILE: spec/plugman/plugins/dependencies/C@1.0.0/src/ios/CPluginCommand.h ================================================ ./dependencies/C@1.0.0/src/ios/CPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/C@1.0.0/src/ios/CPluginCommand.m ================================================ ./dependencies/C@1.0.0/src/ios/CPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/C@1.0.0/www/plugin-c.js ================================================ //dependencies/Cv1/www/plugin-c.js ================================================ FILE: spec/plugman/plugins/dependencies/D/plugin.xml ================================================ Plugin D ================================================ FILE: spec/plugman/plugins/dependencies/D/src/android/D.java ================================================ ./dependencies/D/src/android/D.java ================================================ FILE: spec/plugman/plugins/dependencies/D/src/ios/DPluginCommand.h ================================================ ./dependencies/D/src/ios/DPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/D/src/ios/DPluginCommand.m ================================================ ./dependencies/D/src/ios/DPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/D/www/plugin-d.js ================================================ //dependencies/D/www/plugin-d.js ================================================ FILE: spec/plugman/plugins/dependencies/E/plugin.xml ================================================ Plugin E ================================================ FILE: spec/plugman/plugins/dependencies/E/src/android/E.java ================================================ ./dependencies/E/src/android/E.java ================================================ FILE: spec/plugman/plugins/dependencies/E/src/ios/EPluginCommand.h ================================================ ./dependencies/E/src/ios/EPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/E/src/ios/EPluginCommand.m ================================================ ./dependencies/E/src/ios/EPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/E/www/plugin-d.js ================================================ //dependencies/E/www/plugin-d.js ================================================ FILE: spec/plugman/plugins/dependencies/F/plugin.xml ================================================ Plugin F ================================================ FILE: spec/plugman/plugins/dependencies/F/src/android/F.java ================================================ ./dependencies/F/src/android/F.java ================================================ FILE: spec/plugman/plugins/dependencies/F/src/ios/FPluginCommand.h ================================================ ./dependencies/F/src/ios/FPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/F/src/ios/FPluginCommand.m ================================================ ./dependencies/F/src/ios/FPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/F/www/plugin-f.js ================================================ //dependencies/F/www/plugin-f.js ================================================ FILE: spec/plugman/plugins/dependencies/G/plugin.xml ================================================ Plugin G ================================================ FILE: spec/plugman/plugins/dependencies/G/src/android/G.java ================================================ ./dependencies/G/src/android/G.java ================================================ FILE: spec/plugman/plugins/dependencies/G/src/ios/EPluginCommand.m ================================================ ./dependencies/G/src/ios/EPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/G/src/ios/GPluginCommand.h ================================================ ./dependencies/G/src/ios/GPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/G/www/plugin-g.js ================================================ //dependencies/G/www/plugin-g.js ================================================ FILE: spec/plugman/plugins/dependencies/H/plugin.xml ================================================ Plugin H ================================================ FILE: spec/plugman/plugins/dependencies/H/src/android/H.java ================================================ ./dependencies/H/src/android/H.java ================================================ FILE: spec/plugman/plugins/dependencies/H/src/ios/HPluginCommand.h ================================================ ./dependencies/H/src/ios/HPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/H/src/ios/HPluginCommand.m ================================================ ./dependencies/H/src/ios/HPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/H/www/plugin-h.js ================================================ //dependencies/H/www/plugin-h.js ================================================ FILE: spec/plugman/plugins/dependencies/I/plugin.xml ================================================ Plugin I ================================================ FILE: spec/plugman/plugins/dependencies/I/src/android/I.java ================================================ ./dependencies/I/src/android/I.java ================================================ FILE: spec/plugman/plugins/dependencies/I/src/ios/IPluginCommand.h ================================================ ./dependencies/C/src/ios/CPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/I/src/ios/IPluginCommand.m ================================================ ./dependencies/I/src/ios/IPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/I/www/plugin-i.js ================================================ //dependencies/I/www/plugin-i.js ================================================ FILE: spec/plugman/plugins/dependencies/README.md ================================================ Here's a general overview of how the plugins in this directory are dependent on each other: F / \ A \ B / \ \ / \ C '---D--' E G <-> H I -> C@1.0.0 Test1 --> cordova-plugin-file@2.0.0 Test2 --> cordova-plugin-file@2.X.0 Test3 --> cordova-plugin-file@3.0.0 Test4 --> cordova-plugin-file@https://github.com/apache/cordova-plugin-file ================================================ FILE: spec/plugman/plugins/dependencies/Test1/package.json ================================================ { "name": "test1", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/plugman/plugins/dependencies/Test1/plugin.xml ================================================ Plugin Test1 ================================================ FILE: spec/plugman/plugins/dependencies/Test1/src/android/Test1.java ================================================ ./dependencies/Test1/src/android/Test1.java ================================================ FILE: spec/plugman/plugins/dependencies/Test1/www/plugin-test.js ================================================ //dependencies/Test1/www/plugin-test.js ================================================ FILE: spec/plugman/plugins/dependencies/Test2/package.json ================================================ { "name": "test2", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/plugman/plugins/dependencies/Test2/plugin.xml ================================================ Plugin Test2 ================================================ FILE: spec/plugman/plugins/dependencies/Test2/src/android/Test2.java ================================================ ./dependencies/Test2/src/android/Test2.java ================================================ FILE: spec/plugman/plugins/dependencies/Test2/www/plugin-test.js ================================================ //dependencies/Test2/www/plugin-test.js ================================================ FILE: spec/plugman/plugins/dependencies/Test3/package.json ================================================ { "name": "test3", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/plugman/plugins/dependencies/Test3/plugin.xml ================================================ Plugin Test3 ================================================ FILE: spec/plugman/plugins/dependencies/Test3/src/android/Test3.java ================================================ ./dependencies/Test3/src/android/Test3.java ================================================ FILE: spec/plugman/plugins/dependencies/Test3/www/plugin-test.js ================================================ //dependencies/Test3/www/plugin-test.js ================================================ FILE: spec/plugman/plugins/dependencies/Test4/package.json ================================================ { "name": "test4", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: spec/plugman/plugins/dependencies/Test4/plugin.xml ================================================ Plugin Test4 ================================================ FILE: spec/plugman/plugins/dependencies/Test4/src/android/Test4.java ================================================ ./dependencies/Test3/src/android/Test4.java ================================================ FILE: spec/plugman/plugins/dependencies/Test4/www/plugin-test.js ================================================ //dependencies/Test4/www/plugin-test.js ================================================ FILE: spec/plugman/plugins/dependencies/meta/D/plugin.xml ================================================ Plugin D ================================================ FILE: spec/plugman/plugins/dependencies/meta/D/src/android/D.java ================================================ ./dependencies/meta/D/src/android/D.java ================================================ FILE: spec/plugman/plugins/dependencies/meta/D/src/ios/DPluginCommand.h ================================================ ./dependencies/meta/D/src/ios/DPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/meta/D/src/ios/DPluginCommand.m ================================================ ./dependencies/meta/D/src/ios/DPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/meta/D/www/plugin-d.js ================================================ //dependencies/meta/D/www/plugin-d.js ================================================ FILE: spec/plugman/plugins/dependencies/meta/subdir/E/plugin.xml ================================================ Plugin E ================================================ FILE: spec/plugman/plugins/dependencies/meta/subdir/E/src/android/E.java ================================================ ./dependencies/meta/subdir/E/src/android/E.java ================================================ FILE: spec/plugman/plugins/dependencies/meta/subdir/E/src/ios/EPluginCommand.h ================================================ ./dependencies/meta/subdir/E/src/ios/EPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/meta/subdir/E/src/ios/EPluginCommand.m ================================================ ./dependencies/meta/subdir/E/src/ios/EPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/meta/subdir/E/www/plugin-e.js ================================================ //dependencies/meta/subdir/E/www/plugin-e.js ================================================ FILE: spec/plugman/plugins/dependencies/subdir/E/plugin.xml ================================================ Plugin E ================================================ FILE: spec/plugman/plugins/dependencies/subdir/E/src/android/E.java ================================================ ./dependencies/subdir/E/src/android/E.java ================================================ FILE: spec/plugman/plugins/dependencies/subdir/E/src/ios/EPluginCommand.h ================================================ ./dependencies/subdir/E/src/ios/EPluginCommand.h ================================================ FILE: spec/plugman/plugins/dependencies/subdir/E/src/ios/EPluginCommand.m ================================================ ./dependencies/subdir/E/src/ios/EPluginCommand.m ================================================ FILE: spec/plugman/plugins/dependencies/subdir/E/www/plugin-e.js ================================================ //dependencies/subdir/E/www/plugin-e.js ================================================ FILE: spec/plugman/plugins/org.test.androidonly/plugin.xml ================================================ JavaScript in yo droidz ================================================ FILE: spec/plugman/plugins/org.test.androidonly/www/android.js ================================================ //org.test.androidonly/www/android.js ================================================ FILE: spec/plugman/plugins/org.test.defaultvariables/plugin.xml ================================================ Use Default Variables ================================================ FILE: spec/plugman/plugins/org.test.invalid.engine.no.platform/plugin.xml ================================================ Engine Choo Choo ================================================ FILE: spec/plugman/plugins/org.test.invalid.engine.no.scriptSrc/plugin.xml ================================================ Engine Choo Choo ================================================ FILE: spec/plugman/plugins/org.test.invalid.engine.script/plugin.xml ================================================ Engine Choo Choo ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/package.json ================================================ { "name": "childbrowser", "version": "1.3.1", "description": "Empty plugin used as part of the tests in cordova-lib", "cordova": { "id": "childbrowser", "platforms": [] }, "repository": { "type": "git", "url": "git://git-wip-us.apache.org/repos/asf/cordova-lib.git" }, "author": "Apache Software Foundation", "license": "Apache-2.0", "engines": { "cordovaDependencies": { "0.0.0": { "cordova-android": "<2.1.0" }, "1.1.2": { "cordova-android": ">=2.1.0 <7.0.0" }, "1.3.0": { "cordova-android": "7.0.0" } } } } ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/plugin.xml ================================================ Child Browser No matter what platform you are installing to, this notice is very important. Please make sure you read this because it is very important to complete the installation of your plugin. $APP_ID PackageName $PACKAGE_NAME ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/android/ChildBrowser.java ================================================ ./org.test.plugins.childbrowser/src/android/org.test.plugins.childbrowser.java ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/ChildBrowserCommand.h ================================================ ./org.test.plugins.childbrowser/src/ios/ChildBrowserCommand.h ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/ChildBrowserCommand.m ================================================ ./org.test.plugins.childbrowser/src/ios/ChildBrowserCommand.m ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/ChildBrowserViewController.h ================================================ ./org.test.plugins.childbrowser/src/ios/ChildBrowserViewController.h ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/ChildBrowserViewController.m ================================================ ./org.test.plugins.childbrowser/src/ios/ChildBrowserViewController.m ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/ChildBrowserViewController.xib ================================================ ./org.test.plugins.childbrowser/src/ios/ChildBrowserViewController.xib ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/TargetDirTest.h ================================================ ./org.test.plugins.childbrowser/src/ios/TargetDirTest.h ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/TargetDirTest.m ================================================ ./org.test.plugins.childbrowser/src/ios/TargetDirTest.m ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/preserveDirs/PreserveDirsTest.h ================================================ ./org.test.plugins.childbrowser/src/ios/preserveDirs/PreserveDirsTest.h ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/src/ios/preserveDirs/PreserveDirsTest.m ================================================ ./org.test.plugins.childbrowser/src/ios/preserveDirs/PreserveDirsTest.m ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/www/childbrowser.js ================================================ //org.test.plugins.childbrowser/www/childbrowser.js ================================================ FILE: spec/plugman/plugins/org.test.plugins.childbrowser/www/childbrowser_file.html ================================================ ./org.test.plugins.childbrowser/www/childbrowser_file.html ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/android-resource.xml ================================================ ./org.test.plugins.dummyplugin/android-resource.xml ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/extra.gradle ================================================ tasks.create(name: 'hello') { doLast { println "hello" } } ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/plugin-lib/AndroidManifest.xml ================================================ ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/plugin-lib/libFile ================================================ libFile contents ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/plugin-lib/project.properties ================================================ target=android-11 ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/plugin-lib2/AndroidManifest.xml ================================================ ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/plugin-lib2/libFile ================================================ libFile contents ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/plugin-lib2/project.properties ================================================ target=android-11 ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/plugin.xml ================================================ dummyplugin my description Apache Software Foundation dummy,plugin Apache-2.0 ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/android/DummyPlugin.java ================================================ ./org.test.plugins.dummyplugin/src/android/DummyPlugin.java ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/Custom.framework/someFheader.h ================================================ ./org.test.plugins.dummyplugin/src/ios/Custom.framework/someFheader.h ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/Custom.framework/somebinlib ================================================ ./org.test.plugins.dummyplugin/src/ios/Custom.framework/somebinlib ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/DummyPlugin.bundle ================================================ ./org.test.plugins.dummyplugin/src/ios/DummyPlugin.bundle ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/DummyPluginCommand.h ================================================ ./org.test.plugins.dummyplugin/src/ios/DummyPluginCommand.h ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/DummyPluginCommand.m ================================================ ./org.test.plugins.dummyplugin/src/ios/DummyPluginCommand.m ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/SourceWithFramework.m ================================================ ./org.test.plugins.dummyplugin/src/ios/SourceWithFramework.m ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/TargetDirTest.h ================================================ ./org.test.plugins.dummyplugin/src/ios/TargetDirTest.h ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/src/ios/TargetDirTest.m ================================================ ./org.test.plugins.dummyplugin/src/ios/TargetDirTest.m ================================================ FILE: spec/plugman/plugins/org.test.plugins.dummyplugin/www/dummyplugin.js ================================================ //org.test.plugins.dummyplugin/www/dummyplugin.js ================================================ FILE: spec/plugman/plugins/pkgjson-test-plugin/package.json ================================================ { "name": "pkgjson-test-plugin", "version": "1.3.0", "description": "Empty plugin used as part of the tests", "cordova": { "id": "pkgjson-test-plugin", "platforms": [] }, "repository": { "type": "git", "url": "git://git-wip-us.apache.org/repos/asf/cordova-lib.git" }, "author": "Apache Software Foundation", "license": "Apache-2.0", "engines": { "cordovaDependencies": { "0.0.0": { "cordova-android": "<2.1.0" }, "1.1.2": { "cordova-android": ">=2.1.0 <4.0.0" }, "1.3.0": { "cordova-android": "4.0.0" } } } } ================================================ FILE: spec/plugman/plugins/pkgjson-test-plugin/plugin.xml ================================================ cordova-lib-test-plugin ================================================ FILE: spec/plugman/plugins/recursivePlug/asset.txt ================================================ //asset file ================================================ FILE: spec/plugman/plugins/recursivePlug/demo/config.xml ================================================ HelloCordova A sample Apache Cordova application that responds to the deviceready event. Apache Cordova Team ================================================ FILE: spec/plugman/plugins/recursivePlug/package.json ================================================ { "name": "recursive", "version": "1.3.1", "description": "Empty plugin used as part of the tests in cordova-lib", "cordova": { "id": "recursive", "platforms": [] }, "repository": { "type": "git", "url": "git://git-wip-us.apache.org/repos/asf/cordova-lib.git" }, "author": "Apache Software Foundation", "license": "Apache-2.0", "engines": { "cordovaDependencies": { "0.0.0": { "cordova-android": "<2.1.0" }, "1.1.2": { "cordova-android": ">=2.1.0 <7.0.0" }, "1.3.0": { "cordova-android": "7.0.0" } } } } ================================================ FILE: spec/plugman/plugins/recursivePlug/plugin.xml ================================================ test recursive ================================================ FILE: spec/plugman/util/dependencies.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const dependencies = require('../../../src/plugman/util/dependencies'); const xml_helpers = require('cordova-common').xmlHelpers; const PlatformJson = require('cordova-common').PlatformJson; const PluginInfoProvider = require('cordova-common').PluginInfoProvider; describe('dependency module', function () { describe('generateDependencyInfo method', function () { it('Test 001 : should return a list of top-level plugins based on what is inside a platform.json file', function () { const tlps = { hello: '', isitme: '', yourelookingfor: '' }; const platformJson = new PlatformJson('filePath', 'platform', { installed_plugins: tlps, dependent_plugins: [] }); const pluginInfoProvider = new PluginInfoProvider(); Object.keys(tlps).forEach(function (k) { pluginInfoProvider.put({ id: k, dir: path.join('plugins_dir', k), getDependencies: function () { return []; } }); }); spyOn(xml_helpers, 'parseElementtreeSync').and.returnValue({ findall: function () {} }); const obj = dependencies.generateDependencyInfo(platformJson, 'plugins_dir', pluginInfoProvider); expect(obj.top_level_plugins).toEqual(Object.keys(tlps)); }); it('Test 002 : should return a dependency graph for the plugins', function () { const tlps = { A: '', B: '' }; const plugins_dir = path.join(__dirname, '..', 'plugins', 'dependencies'); const platformJson = new PlatformJson(plugins_dir, 'android', { installed_plugins: tlps, dependent_plugins: [] }); const obj = dependencies.generateDependencyInfo(platformJson, plugins_dir, new PluginInfoProvider()); expect(obj.graph.getChain('A')).toEqual(['C', 'D']); expect(obj.graph.getChain('B')).toEqual(['D', 'E']); }); }); }); ================================================ FILE: spec/plugman/util/metadata.spec.js ================================================ /*! Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const rewire = require('rewire'); const pluginsDir = path.normalize('/plugins_dir/'); const fetchJsonPath = path.join(pluginsDir, 'fetch.json'); let fetchJson = null; const fsMock = { readFileSync: () => fetchJson, existsSync: () => fetchJson !== null, writeFileSync (_, data) { fetchJson = data; }, unlinkSync () { fetchJson = null; } }; // expect fsMock to only operate on fetchJsonPath Object.entries(fsMock).forEach(([key, fn]) => { fsMock[key] = (...args) => { expect(args[0]).toBe(fetchJsonPath); return fn(...args); }; }); describe('plugman.metadata', () => { const TEST_PLUGIN = 'cordova-plugin-thinger'; let metadata; beforeEach(() => { metadata = rewire('../../../src/plugman/util/metadata'); metadata.__set__('fs', fsMock); fetchJson = JSON.stringify({ [TEST_PLUGIN]: { metadata: 'matches' } }); }); describe('get_fetch_metadata', () => { const get_fetch_metadata = pluginId => metadata.get_fetch_metadata(pluginsDir, pluginId); it('should return an empty object if there is no record', () => { fetchJson = null; expect(get_fetch_metadata(TEST_PLUGIN)).toEqual({}); }); it('should return the fetch metadata in plugins_dir/fetch.json if it is there', () => { expect(get_fetch_metadata(TEST_PLUGIN)).toEqual({ metadata: 'matches' }); }); it('should return the fetch metadata in plugins_dir/fetch.json for a scoped plugin', () => { const meta = { metadata: 'matches' }; fetchJson = JSON.stringify({ '@cordova/plugin-thinger': meta }); expect(get_fetch_metadata('@cordova/plugin-thinger')).toEqual(meta); }); describe('cache behaviour', () => { beforeEach(() => { spyOn(fsMock, 'readFileSync').and.callThrough(); spyOn(fsMock, 'existsSync').and.callThrough(); }); it('with no cache, it should read from the filesystem', () => { expect(get_fetch_metadata(TEST_PLUGIN)).toEqual({ metadata: 'matches' }); expect(fsMock.existsSync).toHaveBeenCalled(); expect(fsMock.readFileSync).toHaveBeenCalled(); }); it('with a cache, it should read from the cache', () => { metadata.__set__('cachedJson', { 'cordova-plugin-thinger': { metadata: 'cached' } }); expect(get_fetch_metadata(TEST_PLUGIN)).toEqual({ metadata: 'cached' }); expect(fsMock.existsSync).not.toHaveBeenCalled(); expect(fsMock.readFileSync).not.toHaveBeenCalled(); }); }); }); describe('save_fetch_metadata', () => { const save_fetch_metadata = (...args) => metadata.save_fetch_metadata(pluginsDir, ...args); it('should save plugin metadata to a new fetch.json', () => { fetchJson = null; const meta = { metadata: 'saved' }; save_fetch_metadata('@cordova/plugin-thinger', meta); expect(JSON.parse(fetchJson)).toEqual({ '@cordova/plugin-thinger': meta }); }); it('should save plugin metadata to an existing fetch.json', () => { const meta = { metadata: 'saved' }; const oldFetchJson = { 'some-other-plugin': { metadata: 'not-touched' } }; fetchJson = JSON.stringify(oldFetchJson); save_fetch_metadata('@cordova/plugin-thinger', meta); expect(JSON.parse(fetchJson)).toEqual({ '@cordova/plugin-thinger': meta, ...oldFetchJson }); }); }); describe('remove_fetch_metadata', () => { const remove_fetch_metadata = (...args) => metadata.remove_fetch_metadata(pluginsDir, ...args); it('should remove metadata', () => { remove_fetch_metadata(TEST_PLUGIN); expect(JSON.parse(fetchJson)).toEqual({}); }); }); }); ================================================ FILE: spec/plugman/variable-merge.spec.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const rewire = require('rewire'); const variable_merge = rewire('../../src/plugman/variable-merge'); describe('mergeVariables', function () { const plugin_info_provider_mock = function () {}; let plugin_info; beforeEach(function () { plugin_info = jasmine.createSpyObj('pluginInfo', ['getPreferences']); plugin_info.dir = 'some\\plugin\\path'; plugin_info.id = 'cordova-plugin-device'; plugin_info.version = '1.0.0'; plugin_info_provider_mock.prototype = jasmine.createSpyObj('plugin info provider mock', ['get']); plugin_info_provider_mock.prototype.get = function (directory) { return plugin_info; }; variable_merge.__set__('PluginInfoProvider', plugin_info_provider_mock); }); it('use plugin.xml if no cli/config variables', function () { plugin_info.getPreferences.and.returnValue({ FCM_VERSION: '11.0.1' }); const opts = { cli_variables: { } }; expect(variable_merge.mergeVariables('some/path', 'android', opts)).toEqual({ FCM_VERSION: '11.0.1' }); }); it('cli & config variables take precedence over plugin.xml ', function () { plugin_info.getPreferences.and.returnValue({ FCM_VERSION: '11.0.1' }); const opts = { cli_variables: { FCM_VERSION: '9.0.0' } }; expect(variable_merge.mergeVariables('some/path', 'android', opts)).toEqual({ FCM_VERSION: '9.0.0' }); }); it('should return no variables', function () { plugin_info.getPreferences.and.returnValue({}); const opts = { cli_variables: {} }; expect(variable_merge.mergeVariables('some/path', 'android', opts)).toEqual({}); }); it('should throw error if variables are missing', function () { plugin_info.getPreferences.and.returnValue({ foo: '' }); const opts = { cli_variables: {} }; expect(function () { variable_merge.mergeVariables('some/path', 'android', opts); }).toThrow(); }); }); ================================================ FILE: spec/project-test-helpers.js ================================================ /*! Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const { ConfigParser } = require('cordova-common'); const fixturesPath = path.join(__dirname, 'cordova/fixtures'); // TODO remove this once apache/cordova-common#38 // and apache/cordova-common#39 are resolved class TestConfigParser extends ConfigParser { addPlugin (plugin) { return (super.addPlugin(plugin, plugin.variables), this); } addEngine (...args) { return (super.addEngine(...args), this); } } module.exports = function projectTestHelpers (getProjectPath) { const getPkgJsonPath = () => path.join(getProjectPath(), 'package.json'); const getConfigXmlPath = () => path.join(getProjectPath(), 'config.xml'); function setupBaseProject () { const projectPath = getProjectPath(); fs.cpSync(path.join(fixturesPath, 'basePkgJson'), projectPath, { recursive: true }); process.chdir(projectPath); // It's quite bland, I assure you expect(getCfg().getPlugins()).toEqual([]); expect(getCfg().getEngines()).toEqual([]); expect(getPkgJson('cordova')).toBeUndefined(); expect(getPkgJson('devDependencies')).toBeUndefined(); } function getCfg () { const configXmlPath = getConfigXmlPath(); expect(configXmlPath).toExist(); return new TestConfigParser(configXmlPath); } function getPkgJson (propPath) { const pkgJsonPath = getPkgJsonPath(); expect(pkgJsonPath).toExist(); const keys = propPath ? propPath.split('.') : []; const jsonobj = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); return keys.reduce((obj, key) => { expect(obj).toBeDefined(); return obj[key]; }, jsonobj); } function setPkgJson (propPath, value) { const pkgJsonPath = getPkgJsonPath(); expect(pkgJsonPath).toExist(); const keys = propPath.split('.'); const target = keys.pop(); const pkgJsonObj = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); const parentObj = keys.reduce((obj, key) => { return obj[key] || (obj[key] = {}); }, pkgJsonObj); parentObj[target] = value; fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJsonObj, null, 2), 'utf8'); } return { getPkgJsonPath, getConfigXmlPath, setupBaseProject, getCfg, getPkgJson, setPkgJson }; }; ================================================ FILE: spec/support/jasmine.json ================================================ { "spec_dir": "spec", "helpers": [ "helpers.js", "helper.js" ], "random": false } ================================================ FILE: src/cordova/build.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordovaUtil = require('./util'); const HooksRunner = require('../hooks/HooksRunner'); const cordovaPrepare = require('./prepare'); const cordovaCompile = require('./compile'); // Returns a promise. module.exports = function build (options) { return Promise.resolve().then(function () { const projectRoot = cordovaUtil.cdProjectRoot(); options = cordovaUtil.preProcessOptions(options); // fire build hooks const hooksRunner = new HooksRunner(projectRoot); return hooksRunner.fire('before_build', options) .then(function () { return cordovaPrepare(options); }).then(function () { return cordovaCompile(options); }).then(function () { return hooksRunner.fire('after_build', options); }); }); }; ================================================ FILE: src/cordova/clean.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('./util'); const HooksRunner = require('../hooks/HooksRunner'); const events = require('cordova-common').events; const chain = require('../util/promise-util').Q_chainmap; const platform_lib = require('../platforms/platforms'); // Returns a promise. module.exports = function clean (options) { return Promise.resolve().then(function () { const projectRoot = cordova_util.cdProjectRoot(); options = cordova_util.preProcessOptions(options); const hooksRunner = new HooksRunner(projectRoot); return hooksRunner.fire('before_clean', options) .then(function () { return chain(options.platforms, function (platform) { events.emit('verbose', 'Running cleanup for ' + platform + ' platform.'); return platform_lib .getPlatformApi(platform) .clean(); }); }) .then(function () { return hooksRunner.fire('after_clean', options); }); }); }; ================================================ FILE: src/cordova/compile.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('./util'); const HooksRunner = require('../hooks/HooksRunner'); const promiseUtil = require('../util/promise-util'); const platform_lib = require('../platforms/platforms'); // Returns a promise. module.exports = function compile (options) { return Promise.resolve() .then(function () { const projectRoot = cordova_util.cdProjectRoot(); options = cordova_util.preProcessOptions(options); const hooksRunner = new HooksRunner(projectRoot); return hooksRunner.fire('before_compile', options) .then(function () { return promiseUtil.Q_chainmap(options.platforms, function (platform) { return platform_lib .getPlatformApi(platform) .build(Object.assign({}, options.options)); }); }).then(function () { return hooksRunner.fire('after_compile', options); }); }); }; ================================================ FILE: src/cordova/cordova.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_events = require('cordova-common').events; const cordova_util = require('./util'); const off = function () { cordova_events.removeListener.apply(cordova_events, arguments); }; const emit = function () { cordova_events.emit.apply(cordova_events, arguments); }; module.exports = { get binname () { return cordova_util.binname; }, set binname (name) { cordova_util.binname = name; }, on: function () { cordova_events.on.apply(cordova_events, arguments); }, off, removeListener: off, removeAllListeners: function () { cordova_events.removeAllListeners.apply(cordova_events, arguments); }, emit, trigger: emit, findProjectRoot: function (opt_startDir) { return cordova_util.isCordova(opt_startDir); }, prepare: require('./prepare'), build: require('./build'), emulate: require('./emulate'), plugin: require('./plugin'), plugins: require('./plugin'), platform: require('./platform'), platforms: require('./platform'), compile: require('./compile'), run: require('./run'), targets: require('./targets'), requirements: require('./requirements'), projectMetadata: require('./project_metadata'), clean: require('./clean') }; ================================================ FILE: src/cordova/emulate.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('./util'); const { events } = require('cordova-common'); const HooksRunner = require('../hooks/HooksRunner'); const platform_lib = require('../platforms/platforms'); const cordovaPrepare = require('./prepare'); // Support for listing targets. const targets = require('./targets'); // Returns a promise. module.exports = function emulate (options = {}) { const { options: cliArgs } = options; if (cliArgs?.list) { const { platforms } = options; if (platforms.length <= 0) { events.emit('warn', 'A platform must be provided when using the "--list" flag.'); return false; } cordova_util.cdProjectRoot(); options = cordova_util.preProcessOptions(options); options.options.device = false; options.options.emulator = true; return Promise.resolve(platforms.map(function (platform) { const platformApi = platform_lib.getPlatformApi(platform); if (platformApi?.listTargets) { // Use Platform's API to fetch target list when available return platformApi.listTargets(options); } else { events.emit('warn', 'Please update to the latest platform release to ensure uninterrupted fetching of target lists.'); // fallback to original method. return targets(options); } })); } return Promise.resolve().then(function () { const projectRoot = cordova_util.cdProjectRoot(); options = cordova_util.preProcessOptions(options); options.options.device = false; options.options.emulator = true; const optsClone = Object.assign({}, options.options); // This is needed as .build modifies opts optsClone.nobuild = true; const hooksRunner = new HooksRunner(projectRoot); return hooksRunner.fire('before_emulate', options) .then(function () { if (!options.options.noprepare) { // Run a prepare first! return cordovaPrepare(options); } }).then(function () { // Deploy in parallel (output gets intermixed though...) return Promise.all(options.platforms.map(function (platform) { const buildPromise = options.options.nobuild ? Promise.resolve() : platform_lib.getPlatformApi(platform).build(options.options); return buildPromise .then(function () { return hooksRunner.fire('before_deploy', options); }) .then(function () { return platform_lib.getPlatformApi(platform).run(optsClone); }); })); }).then(function () { return hooksRunner.fire('after_emulate', options); }); }); }; ================================================ FILE: src/cordova/platform/addHelper.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const semver = require('semver'); const fetch = require('cordova-fetch'); const CordovaError = require('cordova-common').CordovaError; const ConfigParser = require('cordova-common').ConfigParser; const PlatformJson = require('cordova-common').PlatformJson; const events = require('cordova-common').events; const cordova_util = require('../util'); const promiseutil = require('../../util/promise-util'); const platforms = require('../../platforms'); const detectIndent = require('detect-indent'); const detectNewline = require('detect-newline'); const stringifyPackage = require('stringify-package'); const getPlatformDetailsFromDir = require('./getPlatformDetailsFromDir'); const preparePlatforms = require('../prepare/platforms'); module.exports = addHelper; module.exports.getVersionFromConfigFile = getVersionFromConfigFile; module.exports.downloadPlatform = downloadPlatform; module.exports.installPluginsForNewPlatform = installPluginsForNewPlatform; function addHelper (cmd, hooksRunner, projectRoot, targets, opts) { let msg; if (!targets || !targets.length) { msg = 'No platform specified. Please specify a platform to ' + cmd + '. ' + 'See `' + cordova_util.binname + ' platform list`.'; return Promise.reject(new CordovaError(msg)); } for (let i = 0; i < targets.length; i++) { if (!platforms.hostSupports(targets[i])) { msg = 'WARNING: Applications for platform ' + targets[i] + ' can not be built on this OS - ' + process.platform + '.'; events.emit('warning', msg); } } const xml = cordova_util.projectConfig(projectRoot); const cfg = new ConfigParser(xml); opts = opts || {}; // The "platforms" dir is safe to delete, it's almost equivalent to // cordova platform rm const platformsDir = path.join(projectRoot, 'platforms'); fs.mkdirSync(platformsDir, { recursive: true }); return hooksRunner.fire('before_platform_' + cmd, opts) .then(function () { const platformsToSave = []; return promiseutil.Q_chainmap(targets, function (target) { // For each platform, download it and call its helper script. let platform; let spec; const parts = target.split('@'); if (parts.length > 1 && parts[0] === '') { // scoped package platform = '@' + parts[1]; spec = parts[2]; } else { platform = parts[0]; spec = parts[1]; } return Promise.resolve().then(function () { // if platform is a local path or url, it should be assigned // to spec if (cordova_util.isDirectory(path.resolve(platform)) || cordova_util.isUrl(platform)) { spec = platform; platform = null; } // If there is no spec specified, try to get spec from package.json if (spec === undefined && cmd === 'add') { const pkgJson = readPackageJsonIfExists(projectRoot); spec = getVersionFromPackageJson(platform, pkgJson); } // if we still have no spec, try to get it from config.xml if (platform && spec === undefined && cmd === 'add') { events.emit('verbose', 'No version supplied. Retrieving version from config.xml...'); spec = module.exports.getVersionFromConfigFile(platform, cfg); } // Handle local paths if (spec) { const maybeDir = cordova_util.fixRelativePath(spec); if (cordova_util.isDirectory(maybeDir)) { return fetch(path.resolve(maybeDir), projectRoot, opts) .then(function (directory) { return getPlatformDetailsFromDir(directory, platform); }); } } return module.exports.downloadPlatform(projectRoot, platform, spec, opts); }).then(function (platDetails) { platform = platDetails.platform; const platformPath = path.join(projectRoot, 'platforms', platform); const platformAlreadyAdded = fs.existsSync(platformPath); if (cmd === 'add') { // TODO: Can we check for this before downloading the platform? if (platformAlreadyAdded) { throw new CordovaError('Platform ' + platform + ' already added.'); } // Remove the .json file from the plugins directory, so we start clean (otherwise we // can get into trouble not installing plugins if someone deletes the platform folder but // .json still exists). cordova_util.removePlatformPluginsJson(projectRoot, target); } else if (cmd === 'update') { // TODO: can we check for this before downloading the platform? if (!platformAlreadyAdded) { throw new CordovaError('Platform "' + platform + '" is not yet added. See `' + cordova_util.binname + ' platform list`.'); } } if (semver.prerelease(platDetails.version)) { msg = `Warning: using prerelease platform ${platform}@${platDetails.version}.`; msg += `\nUse 'cordova platform add ${platform}@latest to add the latest published version instead.`; events.emit('warn', msg); } // TODO: NIT: can we rename this to platformapi options so as to not confuse with addHelper options? const options = { // We need to pass a platformDetails into update/create // since PlatformApiPoly needs to know something about // platform, it is going to create. platformDetails: platDetails, link: opts.link }; events.emit('log', (cmd === 'add' ? 'Adding ' : 'Updating ') + platform + ' project...'); const PlatformApi = cordova_util.getPlatformApiFunction(platDetails.libDir, platform); const destination = path.resolve(projectRoot, 'platforms', platform); const promise = cmd === 'add' ? PlatformApi.createPlatform.bind(null, destination, cfg, options, events) : PlatformApi.updatePlatform.bind(null, destination, options, events); // TODO: if we return the promise immediately, can we not unindent the promise .then()s by one indent? return promise() .then(function () { if (!opts.restoring) { return preparePlatforms([platform], projectRoot, { searchpath: opts.searchpath }); } }) .then(function () { if (cmd === 'add') { return module.exports.installPluginsForNewPlatform(platform, projectRoot, opts); } }) .then(function () { // TODO: didnt we just do this two promise then's ago? if (!opts.restoring) { // Call prepare for the current platform if we're not restoring from config.xml. const prepOpts = { platforms: [platform], searchpath: opts.searchpath, save: opts.save || false }; // delete require.cache[require.resolve('../cordova')] return require('../prepare')(prepOpts); } }) .then(function () { const saveVersion = !spec || semver.validRange(spec, true); // Save platform@spec into platforms.json, where 'spec' is a version or a soure location. If a // source location was specified, we always save that. Otherwise we save the version that was // actually installed. const versionToSave = saveVersion ? platDetails.version : spec; events.emit('verbose', 'Saving ' + platform + '@' + versionToSave + ' into platforms.json'); if (opts.save) { // Similarly here, we save the source location if that was specified, otherwise the version that // was installed. However, we save it with the "~" attribute (this allows for patch updates). spec = saveVersion ? '~' + platDetails.version : spec; // Save to add to pacakge.json's cordova.platforms array in the next then. platformsToSave.push(platform); } }); }); }).then(function () { // save installed platforms to cordova.platforms array in package.json let pkgJson; const pkgJsonPath = path.join(projectRoot, 'package.json'); let modifiedPkgJson = false; if (fs.existsSync(pkgJsonPath)) { pkgJson = cordova_util.requireNoCache(path.join(pkgJsonPath)); } if (pkgJson === undefined) { return; } if (pkgJson.cordova === undefined) { pkgJson.cordova = {}; } if (pkgJson.cordova.platforms === undefined) { pkgJson.cordova.platforms = []; } platformsToSave.forEach(function (plat) { if (pkgJson.cordova.platforms.indexOf(plat) === -1) { events.emit('verbose', 'adding ' + plat + ' to cordova.platforms array in package.json'); pkgJson.cordova.platforms.push(plat); modifiedPkgJson = true; } }); // Save to package.json. if (modifiedPkgJson === true) { const file = fs.readFileSync(pkgJsonPath, 'utf8'); const indent = detectIndent(file).indent || ' '; const newline = detectNewline(file); fs.writeFileSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), 'utf8'); } }); }).then(function () { return hooksRunner.fire('after_platform_' + cmd, opts); }); } function readPackageJsonIfExists (packageDir) { const pkgJsonPath = path.join(packageDir, 'package.json'); return fs.existsSync(pkgJsonPath) ? cordova_util.requireNoCache(pkgJsonPath) : undefined; } function getVersionFromPackageJson (platform, { dependencies, devDependencies } = {}) { const deps = { ...dependencies, ...devDependencies }; return deps[`cordova-${platform}`] || deps[platform]; } function getVersionFromConfigFile (platform, cfg) { // Get appropriate version from config.xml const engine = cfg.getEngines().find(eng => { return eng.name.toLowerCase() === platform.toLowerCase(); }); return engine && engine.spec; } // Downloads via npm or via git clone (tries both) // Returns a Promise function downloadPlatform (projectRoot, platform, version, opts) { let target = version ? (platform + '@' + version) : platform; return Promise.resolve().then(function () { // append cordova to platform if (platform in platforms.info) { target = 'cordova-' + target; } // gitURLs don't supply a platform, it equals null if (!platform) { target = version; } events.emit('log', 'Using cordova-fetch for ' + target); return fetch(target, projectRoot, opts); }).catch(function (error) { const message = 'Failed to fetch platform ' + target + '\nProbably this is either a connection problem, or platform spec is incorrect.' + '\nCheck your connection and platform name/version/URL.' + '\n' + error; return Promise.reject(new CordovaError(message)); }).then(function (libDir) { return getPlatformDetailsFromDir(libDir, platform); }); } function installPluginsForNewPlatform (platform, projectRoot, opts) { // Install all currently installed plugins into this new platform. const plugins_dir = path.join(projectRoot, 'plugins'); // Get a list of all currently installed plugins, ignoring those that have already been installed for this platform // during prepare (installed from config.xml). const platformJson = PlatformJson.load(plugins_dir, platform); let plugins = cordova_util.findPlugins(plugins_dir).filter(function (plugin) { return !platformJson.isPluginInstalled(plugin); }); if (plugins.length === 0) { return Promise.resolve(); } // If package.json includes the plugins, we use that for proper sort order const pkgJson = readPackageJsonIfExists(projectRoot); if (pkgJson?.cordova?.plugins) { const pkgPluginIDs = Object.keys(pkgJson.cordova.plugins); plugins = plugins.sort(function (a, b) { return pkgPluginIDs.indexOf(a) - pkgPluginIDs.indexOf(b); }); } const output = path.join(projectRoot, 'platforms', platform); const plugman = require('../../plugman/plugman'); const fetchMetadata = require('../../plugman/util/metadata'); // Install them serially. return plugins.reduce(function (soFar, plugin) { return soFar.then(function () { events.emit('verbose', 'Installing plugin "' + plugin + '" following successful platform add of ' + platform); // Get plugin variables from fetch.json if have any and pass them as cli_variables to plugman const pluginMetadata = fetchMetadata.get_fetch_metadata(plugins_dir, plugin); const options = { searchpath: opts.searchpath, // Set up platform to install asset files/js modules to /platform_www dir // instead of /www. This is required since on each prepare platform's www dir is changed // and files from 'platform_www' merged into 'www'. Thus we need to persist these // files platform_www directory, so they'll be applied to www on each prepare. // NOTE: there is another code path for plugin installation (see CB-10274 and the // related PR: https://github.com/apache/cordova-lib/pull/360) so we need to // specify the option below in both places usePlatformWww: true, is_top_level: pluginMetadata.is_top_level, force: opts.force, save: opts.save || false }; const variables = pluginMetadata && pluginMetadata.variables; if (variables) { events.emit('verbose', 'Found variables for "' + plugin + '". Processing as cli_variables.'); options.cli_variables = variables; } return plugman.install(platform, output, plugin, plugins_dir, options); }); }, Promise.resolve()); } ================================================ FILE: src/cordova/platform/getPlatformDetailsFromDir.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const CordovaError = require('cordova-common').CordovaError; const events = require('cordova-common').events; const cordova_util = require('../util'); const platforms = require('../../platforms/platforms'); module.exports = getPlatformDetailsFromDir; module.exports.platformFromName = platformFromName; // Gets platform details from a directory function getPlatformDetailsFromDir (dir, platformIfKnown) { const libDir = path.resolve(dir); let platform; let version; // console.log("getPlatformDetailsFromDir : ", dir, platformIfKnown, libDir); try { const pkgPath = path.join(libDir, 'package.json'); const pkg = cordova_util.requireNoCache(pkgPath); platform = module.exports.platformFromName(pkg.name); version = pkg.version; } catch (e) { return Promise.reject(new CordovaError('The provided path does not seem to contain a valid package.json or a valid Cordova platform: ' + libDir)); } // platform does NOT have to exist in 'platforms', but it should have a name, and a version if (!version || !platform) { return Promise.reject(new CordovaError('The provided path does not seem to contain a ' + 'Cordova platform: ' + libDir)); } return Promise.resolve({ libDir, platform, version }); } /** * Removes the cordova- prefix from the platform's name for known platforms. * @param {string} name - platform name * @returns {string} */ function platformFromName (name) { let platName = name; const platMatch = /^cordova-([a-z0-9-]+)$/.exec(name); if (platMatch && (platMatch[1] in platforms)) { platName = platMatch[1]; events.emit('verbose', 'Removing "cordova-" prefix from ' + name); } return platName; } ================================================ FILE: src/cordova/platform/index.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('../util'); const HooksRunner = require('../../hooks/HooksRunner'); const CordovaError = require('cordova-common').CordovaError; const platforms = require('../../platforms/platforms'); const addHelper = require('./addHelper'); module.exports = platform; module.exports.add = function add (hooksRunner, projectRoot, targets, opts) { return addHelper('add', hooksRunner, projectRoot, targets, opts); }; module.exports.update = function update (hooksRunner, projectRoot, targets, opts) { return addHelper('update', hooksRunner, projectRoot, targets, opts); }; module.exports.remove = require('./remove'); module.exports.list = require('./list'); module.exports.getPlatformDetailsFromDir = require('./getPlatformDetailsFromDir'); // Expose the platform parsers on top of this command for (const p in platforms) { module.exports[p] = platforms[p]; } /** * Handles all cordova platform commands. * @param {string} command - Command to execute (add, rm, ls, update, save) * @param {Object[]} targets - Array containing platforms to execute commands on * @param {Object} opts * @returns {Promise} */ function platform (command, targets, opts) { // CB-10519 wrap function code into promise so throwing error // would result in promise rejection instead of uncaught exception return Promise.resolve().then(function () { let msg; const projectRoot = cordova_util.cdProjectRoot(); const hooksRunner = new HooksRunner(projectRoot); if (arguments.length === 0) command = 'ls'; if (targets && !(targets instanceof Array)) targets = [targets]; // TODO: wouldn't update need a platform, too? what about save? if ((command === 'add' || command === 'rm' || command === 'remove') && (!targets || (targets instanceof Array && targets.length === 0))) { msg = 'You need to qualify `' + command + '` with one or more platforms!'; return Promise.reject(new CordovaError(msg)); } opts = opts || {}; opts.platforms = targets; switch (command) { case 'add': return module.exports.add(hooksRunner, projectRoot, targets, opts); case 'rm': case 'remove': return module.exports.remove(hooksRunner, projectRoot, targets, opts); case 'update': case 'up': return module.exports.update(hooksRunner, projectRoot, targets, opts); default: return module.exports.list(hooksRunner, projectRoot, opts); } }); } ================================================ FILE: src/cordova/platform/list.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const events = require('cordova-common').events; const cordova_util = require('../util'); const platforms = require('../../platforms'); module.exports = list; module.exports.addDeprecatedInformationToPlatforms = addDeprecatedInformationToPlatforms; function list (hooksRunner, projectRoot, opts) { return hooksRunner.fire('before_platform_ls', opts) .then(function () { return cordova_util.getInstalledPlatformsWithVersions(projectRoot); }).then(function (platformMap) { // Exrtacted the installed platforms const installed = Object.keys(platformMap) .map(p => platformMap[p] ? p + ' ' + platformMap[p] : p) .sort(); const installedResult = addDeprecatedInformationToPlatforms(installed).join('\n '); events.emit('results', `Installed platforms:\n ${installedResult}`); // Get the avaliable platforms excluding the installed ones const available = platforms.list .filter(platforms.hostSupports) .filter(p => !platformMap[p]) .sort(); const availableResult = addDeprecatedInformationToPlatforms(available).join('\n '); events.emit('results', `Available platforms:\n ${availableResult}`); }).then(function () { return hooksRunner.fire('after_platform_ls', opts); }); } function addDeprecatedInformationToPlatforms (platformsList) { platformsList = platformsList.map(function (p) { const platformKey = p.split(' ')[0]; // Remove Version Information // allow for 'unknown' platforms, which will not exist in platforms if ((platforms.info[platformKey] || {}).deprecated) { p = p.concat(' ', '(deprecated)'); } return p; }); return platformsList; } ================================================ FILE: src/cordova/platform/remove.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const CordovaError = require('cordova-common').CordovaError; const ConfigParser = require('cordova-common').ConfigParser; const events = require('cordova-common').events; const npmUninstall = require('cordova-fetch').uninstall; const cordova_util = require('../util'); const promiseutil = require('../../util/promise-util'); const platforms = require('../../platforms/platforms'); const detectIndent = require('detect-indent'); const detectNewline = require('detect-newline'); const stringifyPackage = require('stringify-package'); module.exports = remove; function remove (hooksRunner, projectRoot, targets, opts) { if (!targets || !targets.length) { return Promise.reject(new CordovaError('No platform(s) specified. Please specify platform(s) to remove. See `' + cordova_util.binname + ' platform list`.')); } return hooksRunner.fire('before_platform_rm', opts) .then(function () { targets.forEach(function (target) { fs.rmSync(path.join(projectRoot, 'platforms', target), { recursive: true, force: true }); cordova_util.removePlatformPluginsJson(projectRoot, target); }); }).then(function () { let modifiedPkgJson = false; let pkgJson; const pkgJsonPath = path.join(projectRoot, 'package.json'); // If statement to see if pkgJsonPath exists in the filesystem if (fs.existsSync(pkgJsonPath)) { pkgJson = cordova_util.requireNoCache(pkgJsonPath); } if (opts.save) { targets.forEach(function (target) { const platformName = target.split('@')[0]; const xml = cordova_util.projectConfig(projectRoot); const cfg = new ConfigParser(xml); if (cfg.getEngines && cfg.getEngines().some(function (e) { return e.name === platformName; })) { events.emit('log', 'Removing platform ' + target + ' from config.xml file...'); cfg.removeEngine(platformName); cfg.write(); } // If package.json exists and contains a specified platform in cordova.platforms, it will be removed. if (pkgJson !== undefined && pkgJson.cordova !== undefined && pkgJson.cordova.platforms !== undefined) { const index = pkgJson.cordova.platforms.indexOf(platformName); // Check if platform exists in platforms array. if (pkgJson.cordova.platforms !== undefined && index > -1) { events.emit('log', 'Removing ' + platformName + ' from cordova.platforms array in package.json'); pkgJson.cordova.platforms.splice(index, 1); modifiedPkgJson = true; } } }); // Write out new package.json if changes have been made. if (modifiedPkgJson === true) { const file = fs.readFileSync(pkgJsonPath, 'utf8'); const indent = detectIndent(file).indent || ' '; const newline = detectNewline(file); fs.writeFileSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), 'utf8'); } } }).then(function () { // Remove targets from platforms.json. targets.forEach(function (target) { events.emit('verbose', 'Removing platform ' + target + ' from platforms.json file...'); }); }).then(function () { // Remove from node_modules if it exists return promiseutil.Q_chainmap(targets, function (target) { if (target in platforms) { target = 'cordova-' + target; } // Edits package.json. return npmUninstall(target, projectRoot, opts); }); }).then(function () { return hooksRunner.fire('after_platform_rm', opts); }); } ================================================ FILE: src/cordova/plugin/add.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const semver = require('semver'); const url = require('url'); const detectIndent = require('detect-indent'); const detectNewline = require('detect-newline'); const stringifyPackage = require('stringify-package'); const cordova_util = require('../util'); const plugin_util = require('./util'); const cordova_pkgJson = require('../../../package.json'); const pluginSpec = require('./plugin_spec_parser'); const plugman = require('../../plugman/plugman'); const chainMap = require('../../util/promise-util').Q_chainmap; const ConfigParser = require('cordova-common').ConfigParser; const CordovaError = require('cordova-common').CordovaError; const PluginInfoProvider = require('cordova-common').PluginInfoProvider; const events = require('cordova-common').events; const preparePlatforms = require('../prepare/platforms'); module.exports = add; module.exports.determinePluginTarget = determinePluginTarget; module.exports.parseSource = parseSource; module.exports.getVersionFromConfigFile = getVersionFromConfigFile; module.exports.getFetchVersion = getFetchVersion; module.exports.determinePluginVersionToFetch = determinePluginVersionToFetch; module.exports.getFailedRequirements = getFailedRequirements; module.exports.findVersion = findVersion; module.exports.listUnmetRequirements = listUnmetRequirements; function add (projectRoot, hooksRunner, opts) { if (!opts.plugins || !opts.plugins.length) { return Promise.reject(new CordovaError('No plugin specified. Please specify a plugin to add.')); } let pluginInfo; let shouldRunPrepare = false; const pluginPath = path.join(projectRoot, 'plugins'); const platformList = cordova_util.listPlatforms(projectRoot); const xml = cordova_util.projectConfig(projectRoot); const cfg = new ConfigParser(xml); let searchPath = opts.searchpath; if (typeof searchPath === 'string') { searchPath = searchPath.split(path.delimiter); } // Blank it out to appease unit tests. if (searchPath && searchPath.length === 0) { searchPath = undefined; } opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) }; return hooksRunner.fire('before_plugin_add', opts) .then(function () { const pluginInfoProvider = new PluginInfoProvider(); return opts.plugins.reduce(function (soFar, target) { return soFar.then(function () { if (target[target.length - 1] === path.sep) { target = target.substring(0, target.length - 1); } // Fetch the plugin first. const fetchOptions = { searchpath: searchPath, noregistry: opts.noregistry, save: opts.save, nohooks: opts.nohooks, link: opts.link, pluginInfoProvider, variables: opts.cli_variables, is_top_level: true, save_exact: opts.save_exact || false, production: opts.production }; return module.exports.determinePluginTarget(projectRoot, cfg, target, fetchOptions).then(function (resolvedTarget) { target = resolvedTarget; events.emit('verbose', 'Calling plugman.fetch on plugin "' + target + '"'); return plugman.fetch(target, pluginPath, fetchOptions); }); }).then(function (directory) { return pluginInfoProvider.get(directory); }).then(function (plugInfoProvider) { pluginInfo = plugInfoProvider; return plugin_util.mergeVariables(pluginInfo, cfg, opts); }).then(function (variables) { opts.cli_variables = variables; // Iterate (in serial!) over all platforms in the project and install the plugin. return chainMap(platformList, function (platform) { const platformRoot = path.join(projectRoot, 'platforms', platform); const options = { cli_variables: opts.cli_variables || {}, save: opts.save, searchpath: searchPath, noregistry: opts.noregistry, link: opts.link, pluginInfoProvider, // Set up platform to install asset files/js modules to /platform_www dir // instead of /www. This is required since on each prepare platform's www dir is changed // and files from 'platform_www' merged into 'www'. Thus we need to persist these // files platform_www directory, so they'll be applied to www on each prepare. usePlatformWww: true, nohooks: opts.nohooks, force: opts.force, save_exact: opts.save_exact || false, production: opts.production }; events.emit('verbose', 'Calling plugman.install on plugin "' + pluginInfo.dir + '" for platform "' + platform); return plugman.install(platform, platformRoot, pluginInfo.id, pluginPath, options) .then(function (didPrepare) { // If platform does not returned anything we'll need // to trigger a prepare after all plugins installed if (!didPrepare) shouldRunPrepare = true; }); }) .then(_ => pluginInfo); }).then(function (pluginInfo) { let pkgJson; const pkgJsonPath = path.join(projectRoot, 'package.json'); // save to package.json if (opts.save) { // If statement to see if pkgJsonPath exists in the filesystem if (fs.existsSync(pkgJsonPath)) { // Delete any previous caches of require(package.json) pkgJson = cordova_util.requireNoCache(pkgJsonPath); } // If package.json exists, the plugin object and plugin name // will be added to package.json if not already there. if (pkgJson) { pkgJson.cordova = pkgJson.cordova || {}; pkgJson.cordova.plugins = pkgJson.cordova.plugins || {}; // Plugin and variables are added. pkgJson.cordova.plugins[pluginInfo.id] = opts.cli_variables; events.emit('log', 'Adding ' + pluginInfo.id + ' to package.json'); // Write to package.json const file = fs.readFileSync(pkgJsonPath, 'utf8'); const indent = detectIndent(file).indent || ' '; const newline = detectNewline(file); fs.writeFileSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), 'utf8'); } const src = module.exports.parseSource(target, opts); const attributes = { name: pluginInfo.id }; if (src) { attributes.spec = src; } else { const ver = '~' + pluginInfo.version; if (pkgJson && pkgJson.dependencies && pkgJson.dependencies[pluginInfo.id]) { attributes.spec = pkgJson.dependencies[pluginInfo.id]; } else if (pkgJson && pkgJson.devDependencies && pkgJson.devDependencies[pluginInfo.id]) { attributes.spec = pkgJson.devDependencies[pluginInfo.id]; } else { attributes.spec = ver; } } } }); }, Promise.resolve()); }).then(function () { // CB-11022 We do not need to run prepare after plugin install until shouldRunPrepare flag is set to true if (!shouldRunPrepare) { return Promise.resolve(); } // Need to require right here instead of doing this at the beginning of file // otherwise tests are failing without any real reason. // TODO: possible circular dependency? return preparePlatforms(platformList, projectRoot, opts); }).then(function () { opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) }; return hooksRunner.fire('after_plugin_add', opts); }); } function determinePluginTarget (projectRoot, cfg, target, fetchOptions) { const parsedSpec = pluginSpec.parse(target); const id = parsedSpec.package || target; // CB-10975 We need to resolve relative path to plugin dir from app's root before checking whether if it exists const maybeDir = cordova_util.fixRelativePath(id); if (parsedSpec.version || cordova_util.isUrl(id) || cordova_util.isDirectory(maybeDir)) { return Promise.resolve(target); } // Require project pkgJson. let pkgJson; const pkgJsonPath = path.join(projectRoot, 'package.json'); const cordovaVersion = cordova_pkgJson.version; if (fs.existsSync(pkgJsonPath)) { pkgJson = cordova_util.requireNoCache(pkgJsonPath); } // If no parsedSpec.version, use the one from pkg.json or config.xml. if (!parsedSpec.version) { // Retrieve from pkg.json. if (pkgJson && pkgJson.dependencies && pkgJson.dependencies[id]) { events.emit('verbose', 'No version specified for ' + id + ', retrieving version from package.json'); parsedSpec.version = pkgJson.dependencies[id]; } else if (pkgJson && pkgJson.devDependencies && pkgJson.devDependencies[id]) { events.emit('verbose', 'No version specified for ' + id + ', retrieving version from package.json'); parsedSpec.version = pkgJson.devDependencies[id]; } else { // If no version is specified, retrieve the version (or source) from config.xml. events.emit('verbose', 'No version specified for ' + id + ', retrieving version from config.xml'); parsedSpec.version = module.exports.getVersionFromConfigFile(id, cfg); } } // If parsedSpec.version satisfies pkgJson version, no writing to pkg.json. Only write when // it does not satisfy. /* if (parsedSpec.version) { if (pkgJson && pkgJson.dependencies && pkgJson.dependencies[parsedSpec.package]) { //it can only go in here if var noSymbolVersion = parsedSpec.version; if (parsedSpec.version.charAt(0) === '^' || parsedSpec.version.charAt(0) === '~') { noSymbolVersion = parsedSpec.version.slice(1); } if (cordova_util.isUrl(parsedSpec.version) || cordova_util.isDirectory(parsedSpec.version)) { if (pkgJson.dependencies[parsedSpec.package] !== parsedSpec.version) { pkgJson.dependencies[parsedSpec.package] = parsedSpec.version; } if (fetchOptions.save === true) { fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); } } else if (!semver.satisfies(noSymbolVersion, pkgJson.dependencies[parsedSpec.package])) { pkgJson.dependencies[parsedSpec.package] = parsedSpec.version; if (fetchOptions.save === true) { fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), 'utf8'); } } } } */ if (cordova_util.isUrl(parsedSpec.version) || cordova_util.isDirectory(parsedSpec.version)) { return Promise.resolve(parsedSpec.version); } // If version exists in pkg.json or config.xml, use that. if (parsedSpec.version) { return Promise.resolve(id + '@' + parsedSpec.version); } // If no version is given at all and we are fetching from npm, we // can attempt to use the Cordova dependencies the plugin lists in // their package.json const shouldUseNpmInfo = !fetchOptions.searchpath && !fetchOptions.noregistry; events.emit('verbose', 'No version for ' + parsedSpec.package + ' saved in config.xml or package.json'); if (shouldUseNpmInfo) { events.emit('verbose', 'Attempting to use npm info for ' + parsedSpec.package + ' to choose a compatible release'); } else { events.emit('verbose', 'Not checking npm info for ' + parsedSpec.package + ' because searchpath or noregistry flag was given'); } // if noregistry or searchpath are true, then shouldUseNpmInfo is false. Just return target // else run `npm info` on the target via registry.info so we could get // engines elemenent in package.json. Pass that info to getFetchVersion // which determines the correct plugin to fetch based on engines element. return (shouldUseNpmInfo ? plugin_util.info([id]) .then(function (pluginInfo) { return module.exports.getFetchVersion(projectRoot, pluginInfo, cordovaVersion); }) : Promise.resolve(null)) .then(function (fetchVersion) { return fetchVersion ? (id + '@' + fetchVersion) : target; }); } function parseSource (target, opts) { // @todo Use 'url.URL' constructor instead since 'url.parse' was deprecated since v11.0.0 var uri = url.parse(target); // eslint-disable-line if (uri.protocol && uri.protocol !== 'file:' && uri.protocol[1] !== ':' && !target.match(/^\w+:\\/)) { return target; } else { const plugin_dir = cordova_util.fixRelativePath(path.join(target, (opts.subdir || '.'))); if (fs.existsSync(plugin_dir)) { return target; } } return null; } function getVersionFromConfigFile (plugin, cfg) { const parsedSpec = pluginSpec.parse(plugin); const pluginEntry = cfg.getPlugin(parsedSpec.id); return pluginEntry && pluginEntry.spec; } /** * Gets the version of a plugin that should be fetched for a given project based * on the plugin's engine information from NPM and the platforms/plugins installed * in the project. The cordovaDependencies object in the package.json's engines * entry takes the form of an object that maps plugin versions to a series of * constraints and semver ranges. For example: * * { plugin-version: { constraint: semver-range, ...}, ...} * * Constraint can be a plugin, platform, or cordova version. Plugin-version * can be either a single version (e.g. 3.0.0) or an upper bound (e.g. <3.0.0) * * @param {string} projectRoot The path to the root directory of the project * @param {object} pluginInfo The NPM info of the plugin to be fetched (e.g. the * result of calling `registry.info()`) * @param {string} cordovaVersion The semver version of cordova-lib * * @return {Promise} A promise that will resolve to either a string * if there is a version of the plugin that this * project satisfies or null if there is not */ function getFetchVersion (projectRoot, pluginInfo, cordovaVersion) { // Figure out the project requirements if (pluginInfo.engines && pluginInfo.engines.cordovaDependencies) { // grab array of already installed plugins const pluginList = plugin_util.getInstalledPlugins(projectRoot); const pluginMap = {}; pluginList.forEach(function (plugin) { pluginMap[plugin.id] = plugin.version; }); return cordova_util.getInstalledPlatformsWithVersions(projectRoot) .then(function (platformVersions) { return module.exports.determinePluginVersionToFetch( pluginInfo, pluginMap, platformVersions, cordovaVersion); }); } else { // If we have no engine, we want to fall back to the default behavior events.emit('verbose', 'npm info for ' + pluginInfo.name + ' did not contain any engine info. Fetching latest release'); return Promise.resolve(null); } } // For upper bounds in cordovaDependencies const UPPER_BOUND_REGEX = /^<\d+\.\d+\.\d+$/; /* * The engine entry maps plugin versions to constraints like so: * { * '1.0.0' : { 'cordova': '<5.0.0' }, * '<2.0.0': { * 'cordova': '>=5.0.0', * 'cordova-ios': '~5.0.0', * 'cordova-plugin-camera': '~5.0.0' * }, * '3.0.0' : { 'cordova-ios': '>5.0.0' } * } * * TODO: provide a better function description once logic is groked * TODO: update comment below once tests are rewritten/moved around. * See cordova-spec/plugin_fetch.spec.js for test cases and examples */ function determinePluginVersionToFetch (pluginInfo, pluginMap, platformMap, cordovaVersion) { const allVersions = pluginInfo.versions; const engine = pluginInfo.engines.cordovaDependencies; const name = pluginInfo.name; // Filters out pre-release versions const latest = semver.maxSatisfying(allVersions, '>=0.0.0'); const versions = []; let upperBound = null; let upperBoundRange = null; let upperBoundExists = false; // TODO: lots of 'versions' being thrown around in this function: cordova version, // platform version, plugin version. The below for loop: what version is it // iterating over? plugin version? please clarify the variable name. for (const version in engine) { // if a single version && less than latest if (semver.valid(semver.clean(version)) && semver.lte(version, latest)) { versions.push(version); } else { // Check if this is an upperbound; validRange() handles whitespace const cleanedRange = semver.validRange(version); if (cleanedRange && UPPER_BOUND_REGEX.exec(cleanedRange)) { upperBoundExists = true; // We only care about the highest upper bound that our project does not support if (module.exports.getFailedRequirements(engine[version], pluginMap, platformMap, cordovaVersion).length !== 0) { const maxMatchingUpperBound = cleanedRange.substring(1); if (maxMatchingUpperBound && (!upperBound || semver.gt(maxMatchingUpperBound, upperBound))) { upperBound = maxMatchingUpperBound; upperBoundRange = version; } } } else { events.emit('verbose', 'Ignoring invalid version in ' + name + ' cordovaDependencies: ' + version + ' (must be a single version <= latest or an upper bound)'); } } } // If there were no valid requirements, we fall back to old behavior if (!upperBoundExists && versions.length === 0) { events.emit('verbose', 'Ignoring ' + name + ' cordovaDependencies entry because it did not contain any valid plugin version entries'); return null; } // Handle the lower end of versions by giving them a satisfied engine if (!module.exports.findVersion(versions, '0.0.0')) { versions.push('0.0.0'); engine['0.0.0'] = {}; } // Add an entry after the upper bound to handle the versions above the // upper bound but below the next entry. For example: 0.0.0, <1.0.0, 2.0.0 // needs a 1.0.0 entry that has the same engine as 0.0.0 if (upperBound && !module.exports.findVersion(versions, upperBound) && !semver.gt(upperBound, latest)) { versions.push(upperBound); let below = semver.maxSatisfying(versions, upperBoundRange); // Get the original entry without trimmed whitespace below = below ? module.exports.findVersion(versions, below) : null; engine[upperBound] = below ? engine[below] : {}; } // Sort in descending order; we want to start at latest and work back versions.sort(semver.rcompare); for (let i = 0; i < versions.length; i++) { if (upperBound && semver.lt(versions[i], upperBound)) { // Because we sorted in desc. order, if the upper bound we found // applies to this version (and thus the ones below) we can just // quit break; } const range = i ? ('>=' + versions[i] + ' <' + versions[i - 1]) : ('>=' + versions[i]); const maxMatchingVersion = semver.maxSatisfying(allVersions, range); if (maxMatchingVersion && module.exports.getFailedRequirements(engine[versions[i]], pluginMap, platformMap, cordovaVersion).length === 0) { // Because we sorted in descending order, we can stop searching once // we hit a satisfied constraint if (maxMatchingVersion !== latest) { const failedReqs = module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion); // Warn the user that we are not fetching latest module.exports.listUnmetRequirements(name, failedReqs); events.emit('warn', 'Fetching highest version of ' + name + ' that this project supports: ' + maxMatchingVersion + ' (latest is ' + latest + ')'); } return maxMatchingVersion; } } // No version of the plugin is satisfied. In this case, we fall back to // fetching the latest version, but also output a warning const latestFailedReqs = versions.length > 0 ? module.exports.getFailedRequirements(engine[versions[0]], pluginMap, platformMap, cordovaVersion) : []; // If the upper bound is greater than latest, we need to combine its engine // requirements with latest to print out in the warning if (upperBound && semver.satisfies(latest, upperBoundRange)) { const upperFailedReqs = module.exports.getFailedRequirements(engine[upperBoundRange], pluginMap, platformMap, cordovaVersion); upperFailedReqs.forEach(function (failedReq) { for (let i = 0; i < latestFailedReqs.length; i++) { if (latestFailedReqs[i].dependency === failedReq.dependency) { // Not going to overcomplicate things and actually merge the ranges latestFailedReqs[i].required += ' AND ' + failedReq.required; return; } } // There is no req to merge it with latestFailedReqs.push(failedReq); }); } module.exports.listUnmetRequirements(name, latestFailedReqs); events.emit('warn', 'Current project does not satisfy the engine requirements specified by any version of ' + name + '. Fetching latest version of plugin anyway (may be incompatible)'); // No constraints were satisfied return null; } /* * Returns an array full of objects of dependency requirements that are not met. * reqs - CordovaDependency object from plugin's package.json * pluginMap - previously installed plugins in the project * platformMap - previously installed platforms in the project * cordovaVersion - version of cordova being used */ function getFailedRequirements (reqs, pluginMap, platformMap, cordovaVersion) { const failed = []; let version = cordovaVersion; if (semver.prerelease(version)) { // semver.inc with 'patch' type removes prereleased tag from version version = semver.inc(version, 'patch'); } for (const req in reqs) { if (Object.prototype.hasOwnProperty.call(reqs, req) && typeof req === 'string' && semver.validRange(reqs[req])) { let badInstalledVersion = null; // remove potential whitespace const trimmedReq = req.trim(); if (pluginMap[trimmedReq] && !semver.satisfies(pluginMap[trimmedReq], reqs[req])) { badInstalledVersion = pluginMap[req]; } else if (trimmedReq === 'cordova' && !semver.satisfies(version, reqs[req])) { badInstalledVersion = cordovaVersion; } else if (trimmedReq.indexOf('cordova-') === 0) { // Might be a platform constraint const platform = trimmedReq.substring(8); if (platformMap[platform] && !semver.satisfies(platformMap[platform], reqs[req])) { badInstalledVersion = platformMap[platform]; } } if (badInstalledVersion) { failed.push({ dependency: trimmedReq, installed: badInstalledVersion.trim(), required: reqs[req].trim() }); } } else { events.emit('verbose', 'Ignoring invalid plugin dependency constraint ' + req + ':' + reqs[req]); } } return failed; } // return the version if it is in the versions array // return null if the version doesn't exist in the array function findVersion (versions, version) { const cleanedVersion = semver.clean(version); for (let i = 0; i < versions.length; i++) { if (semver.clean(versions[i]) === cleanedVersion) { return versions[i]; } } return null; } // emits warnings to users of failed dependnecy requirements in their projects function listUnmetRequirements (name, failedRequirements) { events.emit('warn', 'Unmet project requirements for latest version of ' + name + ':'); failedRequirements.forEach(function (req) { events.emit('warn', ' ' + req.dependency + ' (' + req.installed + ' in project, ' + req.required + ' required)'); }); } ================================================ FILE: src/cordova/plugin/index.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('../util'); const CordovaError = require('cordova-common').CordovaError; const HooksRunner = require('../../hooks/HooksRunner'); module.exports = plugin; module.exports.add = require('./add'); module.exports.remove = require('./remove'); module.exports.list = require('./list'); function plugin (command, targets, opts) { // CB-10519 wrap function code into promise so throwing error // would result in promise rejection instead of uncaught exception return Promise.resolve().then(function () { const projectRoot = cordova_util.cdProjectRoot(); // Dance with all the possible call signatures we've come up over the time. They can be: // 1. plugin() -> list the plugins // 2. plugin(command, Array of targets, maybe opts object) // 3. plugin(command, target1, target2, target3 ... ) // The targets are not really targets, they can be a mixture of plugins and options to be passed to plugman. command = command || 'ls'; targets = targets || []; opts = opts || {}; if (opts.length) { // This is the case with multiple targets as separate arguments and opts is not opts but another target. targets = Array.prototype.slice.call(arguments, 1); opts = {}; } if (!Array.isArray(targets)) { // This means we had a single target given as string. targets = [targets]; } opts.options = opts.options || []; opts.plugins = []; const hooksRunner = new HooksRunner(projectRoot); // Massage plugin name(s) / path(s) if (!targets || !targets.length) { // TODO: what if command provided is 'remove' ? shouldnt search need a target too? if (command === 'add' || command === 'rm') { return Promise.reject(new CordovaError('You need to qualify `' + cordova_util.binname + ' plugin add` or `' + cordova_util.binname + ' plugin remove` with one or more plugins!')); } else { targets = []; } } // Split targets between plugins and options // Assume everything after a token with a '-' is an option for (let i = 0; i < targets.length; i++) { if (targets[i].match(/^-/)) { opts.options = targets.slice(i); break; } else { opts.plugins.push(targets[i]); } } switch (command) { case 'add': return module.exports.add(projectRoot, hooksRunner, opts); case 'rm': case 'remove': return module.exports.remove(projectRoot, targets, hooksRunner, opts); default: return module.exports.list(projectRoot, hooksRunner); } }); } ================================================ FILE: src/cordova/plugin/list.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const semver = require('semver'); const events = require('cordova-common').events; const plugin_util = require('./util'); const cordova_util = require('../util'); module.exports = list; function list (projectRoot, hooksRunner, opts) { const pluginsList = []; return hooksRunner.fire('before_plugin_ls', opts) .then(function () { return plugin_util.getInstalledPlugins(projectRoot); }).then(function (plugins) { if (plugins.length === 0) { events.emit('results', 'No plugins added. Use `' + cordova_util.binname + ' plugin add `.'); return; } const pluginsDict = {}; const lines = []; let txt, p; for (let i = 0; i < plugins.length; i++) { p = plugins[i]; pluginsDict[p.id] = p; pluginsList.push(p.id); txt = p.id + ' ' + p.version + ' "' + (p.name || p.description) + '"'; lines.push(txt); } // Add warnings for deps with wrong versions. for (const id in pluginsDict) { p = pluginsDict[id]; for (const depId in p.deps) { const dep = pluginsDict[depId]; if (!dep) { txt = 'WARNING, missing dependency: plugin ' + id + ' depends on ' + depId + ' but it is not installed'; lines.push(txt); } else if (!semver.satisfies(dep.version, p.deps[depId].version)) { txt = 'WARNING, broken dependency: plugin ' + id + ' depends on ' + depId + ' ' + p.deps[depId].version + ' but installed version is ' + dep.version; lines.push(txt); } } } events.emit('results', lines.join('\n')); }) .then(function () { return hooksRunner.fire('after_plugin_ls', opts); }) .then(function () { return pluginsList; }); } ================================================ FILE: src/cordova/plugin/plugin_spec_parser.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // npm packages follow the pattern of (@scope/)?package(@spec)? where scope and tag are optional const NPM_SPEC_REGEX = /^(@[^/]+\/)?([^@/]+)(?:@(.+))?$/; module.exports.parse = parse; /** * Represents a parsed specification for a plugin * @class * @param {String} raw The raw specification (i.e. provided by the user) * @param {String} id The id of the package if this is an npm package * @param {String} version The version specified for the package if this is an npm package */ function PluginSpec (raw, id, version) { /** @member {String|null} The id of the plugin or the raw plugin spec if it is not an npm package */ this.id = id || raw; /** @member {String|null} The specified version of the plugin or null if no version was specified */ this.version = version || null; /** @member {String|null} The npm package of the plugin (with scope) or null if this is not a spec for an npm package */ this.package = id; } /** * Tries to parse the given string as an npm-style package specification of * the form (@scope/)?package(@version)? and return the various parts. * * @param {String} raw The string to be parsed * @return {PluginSpec} The parsed plugin spec */ function parse (raw) { const split = NPM_SPEC_REGEX.exec(raw); if (split) { return new PluginSpec(raw, (split[1] || '') + split[2], split[3]); } return new PluginSpec(raw); } ================================================ FILE: src/cordova/plugin/remove.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const ConfigParser = require('cordova-common').ConfigParser; const CordovaError = require('cordova-common').CordovaError; const events = require('cordova-common').events; const cordova_util = require('../util'); const plugin_util = require('./util'); const plugman = require('../../plugman/plugman'); const metadata = require('../../plugman/util/metadata'); const PluginInfoProvider = require('cordova-common').PluginInfoProvider; const detectIndent = require('detect-indent'); const detectNewline = require('detect-newline'); const stringifyPackage = require('stringify-package'); const { Q_chainmap } = require('../../util/promise-util'); const preparePlatforms = require('../prepare/platforms'); module.exports = remove; module.exports.validatePluginId = validatePluginId; function remove (projectRoot, targets, hooksRunner, opts) { if (!targets || !targets.length) { return Promise.reject(new CordovaError('No plugin specified. Please specify a plugin to remove. See: ' + cordova_util.binname + ' plugin list.')); } const pluginPath = path.join(projectRoot, 'plugins'); const plugins = cordova_util.findPlugins(pluginPath); const platformList = cordova_util.listPlatforms(projectRoot); let shouldRunPrepare = false; const xml = cordova_util.projectConfig(projectRoot); const cfg = new ConfigParser(xml); opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) }; return hooksRunner.fire('before_plugin_rm', opts) .then(function () { return Q_chainmap(opts.plugins, removePlugin); }).then(function () { // CB-11022 We do not need to run prepare after plugin install until shouldRunPrepare flag is set to true if (!shouldRunPrepare) { return Promise.resolve(); } return preparePlatforms(platformList, projectRoot, opts); }).then(function () { opts.cordova = { plugins: cordova_util.findPlugins(pluginPath) }; return hooksRunner.fire('after_plugin_rm', opts); }); function removePlugin (target) { return Promise.resolve() .then(function () { const validatedPluginId = module.exports.validatePluginId(target, plugins); if (!validatedPluginId) { throw new CordovaError('Plugin "' + target + '" is not present in the project. See `' + cordova_util.binname + ' plugin list`.'); } target = validatedPluginId; }).then(function () { // Iterate over all installed platforms and uninstall. // If this is a web-only or dependency-only plugin, then // there may be nothing to do here except remove the // reference from the platform's plugin config JSON. return Q_chainmap(platformList, platform => removePluginFromPlatform(target, platform) ); }).then(function () { // TODO: Should only uninstallPlugin when no platforms have it. return plugman.uninstall.uninstallPlugin(target, pluginPath, opts); }).then(function () { if (!opts.save) return; persistRemovalToCfg(target); persistRemovalToPkg(target); }).then(function () { // Remove plugin from fetch.json events.emit('verbose', 'Removing plugin ' + target + ' from fetch.json'); metadata.remove_fetch_metadata(pluginPath, target); }); } function removePluginFromPlatform (target, platform) { let platformRoot; return Promise.resolve().then(function () { platformRoot = path.join(projectRoot, 'platforms', platform); const directory = path.join(pluginPath, target); const pluginInfo = new PluginInfoProvider().get(directory); events.emit('verbose', 'Calling plugman.uninstall on plugin "' + target + '" for platform "' + platform + '"'); opts.force = opts.force || false; return plugin_util.mergeVariables(pluginInfo, cfg, opts); }).then(function (variables) { // leave opts.cli_variables untouched, so values discarded by mergeVariables() // for this platform are still available for other platforms const platformOpts = { ...opts, cli_variables: variables }; return plugman.uninstall.uninstallPlatform(platform, platformRoot, target, pluginPath, platformOpts) .then(function (didPrepare) { // If platform does not returned anything we'll need // to trigger a prepare after all plugins installed // TODO: if didPrepare is falsy, what does that imply? WHY are we doing this? if (!didPrepare) shouldRunPrepare = true; }); }); } function persistRemovalToCfg (target) { const configPath = cordova_util.projectConfig(projectRoot); if (fs.existsSync(configPath)) { // should not happen with real life but needed for tests const configXml = new ConfigParser(configPath); if (configXml.getPlugin(target)) { events.emit('log', 'Removing plugin ' + target + ' from config.xml file...'); configXml.removePlugin(target); configXml.write(); } } } function persistRemovalToPkg (target) { let pkgJson; const pkgJsonPath = path.join(projectRoot, 'package.json'); // If statement to see if pkgJsonPath exists in the filesystem if (fs.existsSync(pkgJsonPath)) { // delete any previous caches of require(package.json) pkgJson = cordova_util.requireNoCache(pkgJsonPath); } // If package.json exists and contains a specified plugin in cordova['plugins'], it will be removed if (pkgJson !== undefined && pkgJson.cordova !== undefined && pkgJson.cordova.plugins !== undefined) { events.emit('log', 'Removing ' + target + ' from package.json'); // Remove plugin from package.json delete pkgJson.cordova.plugins[target]; // Write out new package.json with plugin removed correctly. const file = fs.readFileSync(pkgJsonPath, 'utf8'); const indent = detectIndent(file).indent || ' '; const newline = detectNewline(file); fs.writeFileSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), 'utf8'); } } } function validatePluginId (pluginId, installedPlugins) { if (installedPlugins.indexOf(pluginId) >= 0) { return pluginId; } if (pluginId.indexOf('cordova-plugin-') < 0) { return validatePluginId('cordova-plugin-' + pluginId, installedPlugins); } } ================================================ FILE: src/cordova/plugin/util.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const execa = require('execa'); const PluginInfoProvider = require('cordova-common').PluginInfoProvider; const events = require('cordova-common').events; const CordovaError = require('cordova-common').CordovaError; const fetch = require('cordova-fetch'); module.exports.getInstalledPlugins = getInstalledPlugins; module.exports.mergeVariables = mergeVariables; module.exports.info = info; function getInstalledPlugins (projectRoot) { const pluginsDir = path.join(projectRoot, 'plugins'); // TODO: This should list based off of platform.json, not directories within plugins/ const pluginInfoProvider = new PluginInfoProvider(); return pluginInfoProvider.getAllWithinSearchPath(pluginsDir); } /* * Merges cli and config.xml variables. * * @param {object} pluginInfo * @param {object} config.xml * @param {object} options * * @return {object} object containing the new merged variables */ function mergeVariables (pluginInfo, cfg, opts) { // Validate top-level required variables const pluginVariables = pluginInfo.getPreferences(); opts.cli_variables = opts.cli_variables || {}; const pluginEntry = cfg.getPlugin(pluginInfo.id); // Get variables from config.xml const configVariables = pluginEntry ? pluginEntry.variables : {}; // Add config variable if it's missing in cli_variables Object.keys(configVariables).forEach(function (variable) { opts.cli_variables[variable] = opts.cli_variables[variable] || configVariables[variable]; }); const missingVariables = Object.keys(pluginVariables) .filter(function (variableName) { // discard variables with default value return !(pluginVariables[variableName] || opts.cli_variables[variableName]); }); if (missingVariables.length) { events.emit('verbose', 'Removing ' + pluginInfo.dir + ' because mandatory plugin variables were missing.'); fs.rmSync(pluginInfo.dir, { recursive: true, force: true }); const msg = 'Variable(s) missing (use: --variable ' + missingVariables.join('=value --variable ') + '=value).'; throw new CordovaError(msg); } return opts.cli_variables; } function info (plugin) { const viewArgs = ['view']; plugin = plugin.shift(); viewArgs.push(plugin); viewArgs.push('--json'); // check if npm is installed return fetch.isNpmInstalled() .then(function () { return execa('npm', viewArgs) .then(({ stdout: info }) => { const pluginInfo = JSON.parse(info); return pluginInfo; }); }); } ================================================ FILE: src/cordova/prepare/platforms.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const cordova_util = require('../util'); const ConfigParser = require('cordova-common').ConfigParser; const PlatformJson = require('cordova-common').PlatformJson; const PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger; const platforms = require('../../platforms/platforms'); module.exports = preparePlatforms; /** * Calls `platformApi.prepare` for each platform in project * * @param {string[]} platformList List of platforms, added to current project * @param {string} projectRoot Project root directory * * @return {Promise} */ function preparePlatforms (platformList, projectRoot, options) { return Promise.all(platformList.map(function (platform) { // TODO: this need to be replaced by real projectInfo // instance for current project. const project = { root: projectRoot, projectConfig: new ConfigParser(cordova_util.projectConfig(projectRoot)), locations: { plugins: path.join(projectRoot, 'plugins'), www: cordova_util.projectWww(projectRoot), rootConfigXml: cordova_util.projectConfig(projectRoot) } }; // platformApi prepare takes care of all functionality // which previously had been executed by cordova.prepare: // - reset config.xml and then merge changes from project's one, // - update www directory from project's one and merge assets from platform_www, // - reapply config changes, made by plugins, // - update platform's project // Please note that plugins' changes, such as installed js files, assets and // config changes is not being reinstalled on each prepare. const platformApi = platforms.getPlatformApi(platform); return platformApi.prepare(project, Object.assign({}, options)) .then(function () { // Handle edit-config in config.xml const platformRoot = path.join(projectRoot, 'platforms', platform); const platformJson = PlatformJson.load(platformRoot, platform); const munger = new PlatformMunger(platform, platformRoot, platformJson); // the boolean argument below is "should_increment" munger.add_config_changes(project.projectConfig, true).save_all(); }); })); } ================================================ FILE: src/cordova/prepare.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const cordova_util = require('./util'); const platforms = require('../platforms/platforms'); const HooksRunner = require('../hooks/HooksRunner'); const restore = require('./restore-util'); module.exports = prepare; module.exports.preparePlatforms = require('./prepare/platforms'); function prepare (options) { return Promise.resolve().then(function () { const projectRoot = cordova_util.cdProjectRoot(); options = options || { verbose: false, platforms: [], options: {} }; options.save = options.save || false; const hooksRunner = new HooksRunner(projectRoot); return hooksRunner.fire('before_prepare', options) .then(function () { return restore.installPlatformsFromConfigXML(options.platforms, { searchpath: options.searchpath, restoring: true }); }) .then(function () { options = cordova_util.preProcessOptions(options); const paths = options.platforms.map(function (p) { const platform_path = path.join(projectRoot, 'platforms', p); return platforms.getPlatformApi(p, platform_path).getPlatformInfo().locations.www; }); options.paths = paths; }).then(function () { options = cordova_util.preProcessOptions(options); return restore.installPluginsFromConfigXML(options); }).then(function () { options = cordova_util.preProcessOptions(options); // Iterate over each added platform return module.exports.preparePlatforms(options.platforms, projectRoot, options); }).then(function () { options.paths = options.platforms.map(function (platform) { return platforms.getPlatformApi(platform).getPlatformInfo().locations.www; }); return hooksRunner.fire('after_prepare', options); }); }); } ================================================ FILE: src/cordova/project_metadata.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('./util'); const ConfigParser = require('cordova-common').ConfigParser; const semver = require('semver'); /** Returns all the platforms that are currently saved into config.xml * @return {Promise<{name: string, version: string, src: string}[]>} * e.g: [ {name: 'android', version: '3.5.0'}, {name: 'ios', src: 'git://...'} ] */ function getPlatforms (projectRoot) { const xml = cordova_util.projectConfig(projectRoot); const cfg = new ConfigParser(xml); // If an engine's 'version' property is really its source, map that to the appropriate field. const engines = cfg.getEngines().map(function (engine) { const result = { name: engine.name }; if (semver.validRange(engine.spec, true)) { result.version = engine.spec; } else { result.src = engine.spec; } return result; }); return Promise.resolve(engines); } /** Returns all the plugins that are currently saved into config.xml * @return {Promise<{id: string, version: string, variables: {name: string, value: string}[]}[]>} * e.g: [ {id: 'org.apache.cordova.device', variables: [{name: 'APP_ID', value: 'my-app-id'}, {name: 'APP_NAME', value: 'my-app-name'}]} ] */ function getPlugins (projectRoot) { const xml = cordova_util.projectConfig(projectRoot); const cfg = new ConfigParser(xml); // Map variables object to an array const plugins = cfg.getPlugins().map(function (plugin) { const result = { name: plugin.name }; if (semver.validRange(plugin.spec, true)) { result.version = plugin.spec; } else { result.src = plugin.spec; } const variablesObject = plugin.variables; const variablesArray = []; if (variablesObject) { for (const variable in variablesObject) { variablesArray.push({ name: variable, value: variablesObject[variable] }); } } result.variables = variablesArray; return result; }); return Promise.resolve(plugins); } module.exports = { getPlatforms, getPlugins }; ================================================ FILE: src/cordova/requirements.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('./util'); const { CordovaError } = require('cordova-common'); const knownPlatforms = require('../platforms/platforms'); /** * Runs requirements check against platforms specified in 'platfoms' argument * * @param {String[]} platforms List of platforms for requirements check. If * none, all platforms, added to project will be checked * * @return {Promise} Promise fullfilled with map of platforms and * requirements check results for each platform. */ module.exports = function check_reqs (platforms) { return Promise.resolve().then(() => { const normalizedPlatforms = cordova_util.preProcessOptions(platforms).platforms; return Promise.all( normalizedPlatforms.map(getPlatformRequirementsOrError) ).then(results => normalizedPlatforms.reduce((acc, platform, idx) => Object.assign(acc, { [platform]: results[idx] }) , {}) ); }); }; function getPlatformRequirementsOrError (platform) { return knownPlatforms .getPlatformApi(platform) .requirements() .catch(err => new CordovaError(err)); } ================================================ FILE: src/cordova/restore-util.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const cordova_util = require('./util'); const ConfigParser = require('cordova-common').ConfigParser; const events = require('cordova-common').events; const semver = require('semver'); const detectIndent = require('detect-indent'); const detectNewline = require('detect-newline'); const stringifyPackage = require('stringify-package'); const writeFileAtomicSync = require('write-file-atomic').sync; exports.installPluginsFromConfigXML = installPluginsFromConfigXML; exports.installPlatformsFromConfigXML = installPlatformsFromConfigXML; // Install platforms looking at config.xml and package.json (if there is one). function installPlatformsFromConfigXML (platforms, opts) { events.emit('verbose', 'Checking for saved platforms that haven\'t been added to the project'); const installAllPlatforms = !platforms || platforms.length === 0; const projectRoot = cordova_util.getProjectRoot(); const platformRoot = path.join(projectRoot, 'platforms'); const pkgJsonPath = path.join(projectRoot, 'package.json'); const confXmlPath = cordova_util.projectConfig(projectRoot); const cfg = new ConfigParser(confXmlPath); let pkgJson = {}; let indent = ' '; let newline = '\n'; if (fs.existsSync(pkgJsonPath)) { const fileData = fs.readFileSync(pkgJsonPath, 'utf8'); indent = detectIndent(fileData).indent; newline = detectNewline(fileData); pkgJson = JSON.parse(fileData); } else { if (cfg.packageName()) { pkgJson.name = cfg.packageName().toLowerCase(); } if (cfg.version()) { pkgJson.version = cfg.version(); } if (cfg.name()) { pkgJson.displayName = cfg.name(); } } pkgJson.dependencies = pkgJson.dependencies || {}; pkgJson.devDependencies = pkgJson.devDependencies || {}; pkgJson.cordova = pkgJson.cordova || {}; pkgJson.cordova.platforms = pkgJson.cordova.platforms || []; const pkgPlatforms = pkgJson.cordova.platforms.slice(); const pkgSpecs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies); // Check for platforms listed in config.xml const cfgPlatforms = cfg.getEngines(); cfgPlatforms.forEach(engine => { const platformModule = engine.name.startsWith('cordova-') ? engine.name : `cordova-${engine.name}`; // If package.json includes the platform, we use that config // Otherwise, we need to add the platform to package.json if (!pkgPlatforms.includes(engine.name) || (engine.spec && !(platformModule in pkgSpecs))) { events.emit('info', `Platform '${engine.name}' found in config.xml... Migrating it to package.json`); // If config.xml has a spec for the platform and package.json has // not, add the spec to devDependencies of package.json if (engine.spec && !(platformModule in pkgSpecs)) { pkgJson.devDependencies[platformModule] = engine.spec; } if (!pkgPlatforms.includes(engine.name)) { pkgJson.cordova.platforms.push(engine.name); } } }); // Now that platforms have been updated, re-fetch them from package.json const platformIDs = pkgJson.cordova.platforms.slice(); if (platformIDs.length !== pkgPlatforms.length) { // We've modified package.json and need to save it writeFileAtomicSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), { encoding: 'utf8' }); } const specs = Object.assign({}, pkgJson.dependencies || {}, pkgJson.devDependencies); const platformInfo = platformIDs.map(plID => ({ name: plID, spec: specs[`cordova-${plID}`] || specs[plID] })); let platformName = ''; function restoreCallback (platform) { platformName = platform.name; const platformPath = path.join(platformRoot, platformName); if (fs.existsSync(platformPath) || (!installAllPlatforms && !platforms.includes(platformName))) { // Platform already exists return Promise.resolve(); } events.emit('log', `Discovered platform "${platformName}". Adding it to the project`); // Install from given URL if defined or using a plugin id. If spec // isn't a valid version or version range, assume it is the location to // install from. // CB-10761 If plugin spec is not specified, use plugin name let installFrom = platform.spec || platformName; if (platform.spec && semver.validRange(platform.spec, true)) { installFrom = platformName + '@' + platform.spec; } const cordovaPlatform = require('./platform'); return cordovaPlatform('add', installFrom, opts); } function errCallback (error) { // CB-10921 emit a warning in case of error const msg = `Failed to restore platform "${platformName}". You might need to try adding it again. Error: ${error}`; process.exitCode = 1; events.emit('warn', msg); return Promise.reject(error); } // CB-9278 : Run `platform add` serially, one platform after another // Otherwise, we get a bug where the following line: https://github.com/apache/cordova-lib/blob/0b0dee5e403c2c6d4e7262b963babb9f532e7d27/cordova-lib/src/util/npm-helper.js#L39 // gets executed simultaneously by each platform and leads to an exception being thrown return platformInfo.reduce(function (soFar, platform) { return soFar.then(() => restoreCallback(platform), errCallback); }, Promise.resolve()); } // Returns a promise. function installPluginsFromConfigXML (args) { events.emit('verbose', 'Checking for saved plugins that haven\'t been added to the project'); const projectRoot = cordova_util.getProjectRoot(); const pluginsRoot = path.join(projectRoot, 'plugins'); const pkgJsonPath = path.join(projectRoot, 'package.json'); const confXmlPath = cordova_util.projectConfig(projectRoot); let pkgJson = {}; let indent = ' '; let newline = '\n'; if (fs.existsSync(pkgJsonPath)) { const fileData = fs.readFileSync(pkgJsonPath, 'utf8'); indent = detectIndent(fileData).indent; newline = detectNewline(fileData); pkgJson = JSON.parse(fileData); } pkgJson.dependencies = pkgJson.dependencies || {}; pkgJson.devDependencies = pkgJson.devDependencies || {}; pkgJson.cordova = pkgJson.cordova || {}; pkgJson.cordova.plugins = pkgJson.cordova.plugins || {}; const pkgPluginIDs = Object.keys(pkgJson.cordova.plugins); const pkgSpecs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies); // Check for plugins listed in config.xml const cfg = new ConfigParser(confXmlPath); const cfgPluginIDs = cfg.getPluginIdList(); cfgPluginIDs.forEach(plID => { // If package.json includes the plugin, we use that config // Otherwise, we need to add the plugin to package.json if (!pkgPluginIDs.includes(plID)) { events.emit('info', `Plugin '${plID}' found in config.xml... Migrating it to package.json`); const cfgPlugin = cfg.getPlugin(plID); // If config.xml has a spec for the plugin and package.json has not, // add the spec to devDependencies of package.json if (cfgPlugin.spec && !(plID in pkgSpecs)) { pkgJson.devDependencies[plID] = cfgPlugin.spec; } pkgJson.cordova.plugins[plID] = Object.assign({}, cfgPlugin.variables); } }); // Now that plugins have been updated, re-fetch them from package.json const pluginIDs = Object.keys(pkgJson.cordova.plugins); if (pluginIDs.length !== pkgPluginIDs.length) { // We've modified package.json and need to save it writeFileAtomicSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), { encoding: 'utf8' }); } const specs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies); const plugins = pluginIDs.map(plID => ({ name: plID, spec: specs[plID], variables: pkgJson.cordova.plugins[plID] || {} })); let pluginName = ''; function restoreCallback (pluginConfig) { pluginName = pluginConfig.name; const pluginPath = path.join(pluginsRoot, pluginName); if (fs.existsSync(pluginPath)) { // Plugin already exists return Promise.resolve(); } events.emit('log', `Discovered plugin "${pluginName}". Adding it to the project`); // Install from given URL if defined or using a plugin id. If spec isn't a valid version or version range, // assume it is the location to install from. // CB-10761 If plugin spec is not specified, use plugin name let installFrom = pluginConfig.spec || pluginName; if (pluginConfig.spec && semver.validRange(pluginConfig.spec, true)) { installFrom = pluginName + '@' + pluginConfig.spec; } // Add feature preferences as CLI variables if have any const options = { cli_variables: pluginConfig.variables, searchpath: args.searchpath, save: args.save || false }; const plugin = require('./plugin'); return plugin('add', installFrom, options); } function errCallback (error) { // CB-10921 emit a warning in case of error const msg = `Failed to restore plugin "${pluginName}". You might need to try adding it again. Error: ${error}`; process.exitCode = 1; events.emit('warn', msg); } // CB-9560 : Run `plugin add` serially, one plugin after another // We need to wait for the plugin and its dependencies to be installed // before installing the next root plugin otherwise we can have common // plugin dependencies installed twice which throws a nasty error. return plugins.reduce(function (soFar, plugin) { return soFar.then(() => restoreCallback(plugin), errCallback); }, Promise.resolve()); } ================================================ FILE: src/cordova/run.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const cordova_util = require('./util'); const { events } = require('cordova-common'); const HooksRunner = require('../hooks/HooksRunner'); const platform_lib = require('../platforms/platforms'); const cordovaPrepare = require('./prepare'); // Support for listing targets. const targets = require('./targets'); // Returns a promise. module.exports = function run (options = {}) { const { options: cliArgs } = options; if (cliArgs?.list) { const { platforms } = options; if (platforms.length <= 0) { events.emit('warn', 'A platform must be provided when using the "--list" flag.'); return false; } return Promise.resolve(platforms.map(function (platform) { const platformApi = platform_lib.getPlatformApi(platform); if (platformApi?.listTargets) { // Use Platform's API to fetch target list when available return platformApi.listTargets(options); } else { events.emit('warn', 'Please update to the latest platform release to ensure uninterrupted fetching of target lists.'); // fallback to original method. return targets(options); } })); } return Promise.resolve().then(function () { const projectRoot = cordova_util.cdProjectRoot(); options = cordova_util.preProcessOptions(options); // This is needed as .build modifies opts const optsClone = Object.assign({}, options.options); optsClone.nobuild = true; const hooksRunner = new HooksRunner(projectRoot); return hooksRunner.fire('before_run', options) .then(function () { if (!options.options.noprepare) { // Run a prepare first, then shell out to run return cordovaPrepare(options); } }).then(function () { // Deploy in parallel (output gets intermixed though...) return Promise.all(options.platforms.map(function (platform) { const buildPromise = options.options.nobuild ? Promise.resolve() : platform_lib.getPlatformApi(platform).build(options.options); return buildPromise .then(function () { return hooksRunner.fire('before_deploy', options); }) .then(function () { return platform_lib.getPlatformApi(platform).run(optsClone); }); })); }).then(function () { return hooksRunner.fire('after_run', options); }); }); }; ================================================ FILE: src/cordova/targets.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const execa = require('execa'); const cordova_util = require('./util'); const events = require('cordova-common').events; function handleError (error) { if (error.code === 'ENOENT') { events.emit('warn', 'Platform does not support ' + this.script); } else { events.emit('warn', 'An unexpected error has occured while running ' + this.script + ':\n' + error.message); } } function displayDevices (projectRoot, platform, options) { const caller = { script: 'list-devices' }; events.emit('log', 'Available ' + platform + ' devices:'); const cmd = path.join(projectRoot, 'platforms', platform, 'cordova', 'lib', 'list-devices'); return execa(cmd, options.argv, { stdio: 'inherit' }).then(data => data.stdout, handleError.bind(caller)); } function displayVirtualDevices (projectRoot, platform, options) { const caller = { script: 'list-emulator-images' }; events.emit('log', 'Available ' + platform + ' virtual devices:'); const cmd = path.join(projectRoot, 'platforms', platform, 'cordova', 'lib', 'list-emulator-images'); return execa(cmd, options.argv, { stdio: 'inherit' }).then(data => data.stdout, handleError.bind(caller)); } module.exports = function targets (options) { const projectRoot = cordova_util.cdProjectRoot(); options = cordova_util.preProcessOptions(options); let result = Promise.resolve(); options.platforms.forEach(function (platform) { if (options.options.device) { result = result.then(displayDevices.bind(null, projectRoot, platform, options.options)); } else if (options.options.emulator) { result = result.then(displayVirtualDevices.bind(null, projectRoot, platform, options.options)); } else { result = result.then(displayDevices.bind(null, projectRoot, platform, options.options)) .then(displayVirtualDevices.bind(null, projectRoot, platform, options.options)); } }); return result; }; ================================================ FILE: src/cordova/util.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const events = require('cordova-common').events; const CordovaError = require('cordova-common').CordovaError; const globby = require('globby'); let origCwd = null; exports.binname = 'cordova'; exports.isCordova = isCordova; exports.getProjectRoot = getProjectRoot; exports.cdProjectRoot = cdProjectRoot; exports.listPlatforms = listPlatforms; exports.findPlugins = findPlugins; exports.projectWww = projectWww; exports.projectConfig = projectConfig; exports.preProcessOptions = preProcessOptions; exports.getOrigWorkingDirectory = getOrigWorkingDirectory; exports._resetOrigCwd = _resetOrigCwd; exports.fixRelativePath = fixRelativePath; exports.convertToRealPathSafe = convertToRealPathSafe; exports.isDirectory = isDirectory; exports.isUrl = isUrl; exports.getInstalledPlatformsWithVersions = getInstalledPlatformsWithVersions; exports.requireNoCache = requireNoCache; exports.getPlatformApiFunction = getPlatformApiFunction; exports.removePlatformPluginsJson = removePlatformPluginsJson; exports.getPlatformVersion = getPlatformVersionOrNull; // Remove .json file from plugins directory. function removePlatformPluginsJson (projectRoot, target) { const plugins_json = path.join(projectRoot, 'plugins', target + '.json'); fs.rmSync(plugins_json, { recursive: true, force: true }); } function requireNoCache (pkgJsonPath) { delete require.cache[require.resolve(pkgJsonPath)]; const returnVal = require(pkgJsonPath); delete require.cache[require.resolve(pkgJsonPath)]; return returnVal; } function isUrl (value) { try { const u = value && new URL(value); return !!(u && u.protocol && u.protocol.length > 2); // Account for windows c:/ paths } catch (e) { return false; } } function isRootDir (dir) { if (fs.existsSync(path.join(dir, 'www'))) { if (fs.existsSync(path.join(dir, 'config.xml'))) { // For sure is. if (fs.existsSync(path.join(dir, 'platforms'))) { return 2; } else { return 1; } } // Might be (or may be under platforms/). if (fs.existsSync(path.join(dir, 'www', 'config.xml'))) { return 1; } } return 0; } // Runs up the directory chain looking for a .cordova directory. // IF it is found we are in a Cordova project. // Omit argument to use CWD. function isCordova (dir) { if (!dir) { // Prefer PWD over cwd so that symlinked dirs within your PWD work correctly (CB-5687). const pwd = process.env.PWD; const cwd = process.cwd(); if (pwd && pwd !== cwd && pwd !== 'undefined') { return this.isCordova(pwd) || this.isCordova(cwd); } return this.isCordova(cwd); } let bestReturnValueSoFar = false; for (let i = 0; i < 1000; ++i) { const result = isRootDir(dir); if (result === 2) { return dir; } if (result === 1) { bestReturnValueSoFar = dir; } const parentDir = path.normalize(path.join(dir, '..')); // Detect fs root. if (parentDir === dir) { return bestReturnValueSoFar; } dir = parentDir; } console.error('Hit an unhandled case in util.isCordova'); return false; } /** * Returns the project root directory path. * * Throws a CordovaError if not in a Cordova project. */ function getProjectRoot () { const projectRoot = convertToRealPathSafe(this.isCordova()); if (!projectRoot) { throw new CordovaError('Current working directory is not a Cordova-based project.'); } return projectRoot; } // Cd to project root dir and return its path. Throw CordovaError if not in a Corodva project. function cdProjectRoot () { const projectRoot = this.getProjectRoot(); if (!origCwd) { origCwd = process.env.PWD || process.cwd(); } process.env.PWD = projectRoot; process.chdir(projectRoot); return projectRoot; } function getOrigWorkingDirectory () { return origCwd || process.env.PWD || process.cwd(); } function _resetOrigCwd () { origCwd = null; } // Fixes up relative paths that are no longer valid due to chdir() within cdProjectRoot(). function fixRelativePath (value, /* optional */ cwd) { // Don't touch absolute paths. if (value[1] === ':' || value[0] === path.sep) { return value; } const newDir = cwd || process.env.PWD || process.cwd(); const origDir = getOrigWorkingDirectory(); const pathDiff = path.relative(newDir, origDir); const ret = path.normalize(path.join(pathDiff, value)); return ret; } // Resolve any symlinks in order to avoid relative path issues. See https://issues.apache.org/jira/browse/CB-8757 function convertToRealPathSafe (path) { if (path && fs.existsSync(path)) { return fs.realpathSync(path); } return path; } function listPlatforms (project_dir) { const platforms_dir = path.join(project_dir, 'platforms'); if (!fs.existsSync(platforms_dir)) { return []; } // get subdirs (that are actually dirs, and not files) const subdirs = fs.readdirSync(platforms_dir) .filter(function (file) { return isDirectory(path.join(platforms_dir, file)); }); return subdirs; } function getInstalledPlatformsWithVersions (project_dir) { return Promise.resolve(listPlatforms(project_dir).reduce((result, p) => { try { const platformPath = path.join(project_dir, 'platforms', p); result[p] = getPlatformVersion(platformPath) || null; } catch (e) { result[p] = 'broken'; } return result; }, {})); } function getPlatformVersion (platformPath) { try { // Major Platforms for Cordova 10+ return requireNoCache( path.join(platformPath, 'cordova/Api') ).version(); } catch (e) { // Platforms pre-Cordova 10 return require('execa').sync( process.argv0, // node [path.join(platformPath, 'cordova/version')] ).stdout; } } function getPlatformVersionOrNull (platformPath) { try { return getPlatformVersion(platformPath); } catch (e) { return null; } } // list the directories in the path, ignoring any files function findPlugins (pluginDir) { if (!fs.existsSync(pluginDir)) return []; return globby.sync(['*', '!@*', '@*/*', '!CVS'], { cwd: pluginDir, onlyDirectories: true }); } function projectWww (projectDir) { return path.join(projectDir, 'www'); } function projectConfig (projectDir) { const rootPath = path.join(projectDir, 'config.xml'); const wwwPath = path.join(projectDir, 'www', 'config.xml'); if (fs.existsSync(rootPath)) { return rootPath; } else if (fs.existsSync(wwwPath)) { return wwwPath; } return false; } function preProcessOptions (inputOptions) { /** * Current Desired Arguments * options: {verbose: boolean, platforms: [String], options: [String]} * Accepted Arguments * platformList: [String] -- assume just a list of platforms * platform: String -- assume this is a platform */ let result = inputOptions || {}; if (Array.isArray(inputOptions)) { result = { platforms: inputOptions }; } else if (typeof inputOptions === 'string') { result = { platforms: [inputOptions] }; } result.verbose = result.verbose || false; result.platforms = result.platforms || []; result.options = result.options || {}; const projectRoot = this.isCordova(); if (!projectRoot) { throw new CordovaError('Current working directory is not a Cordova-based project.'); } const projectPlatforms = this.listPlatforms(projectRoot); if (projectPlatforms.length === 0) { throw new CordovaError('No platforms added to this project. Please use `' + exports.binname + ' platform add `.'); } if (result.platforms.length === 0) { result.platforms = projectPlatforms; } if (!result.options.buildConfig && fs.existsSync(path.join(projectRoot, 'build.json'))) { result.options.buildConfig = path.join(projectRoot, 'build.json'); } return result; } /** * Checks to see if the argument is a directory * * @param {string} dir - string representing path of directory * @return {boolean} */ function isDirectory (dir) { try { return fs.lstatSync(dir).isDirectory(); } catch (e) { return false; } } // Returns the API of the platform contained in `dir`. // Potential errors : module isn't found, can't load or doesn't implement the expected interface. function getPlatformApiFunction (dir, platform) { let PlatformApi; try { // First try to load the platform API from the platform project // This is necessary to support older platform API versions PlatformApi = exports.requireNoCache(dir); } catch (loadFromDirError) { events.emit('verbose', `Unable to load Platform API from ${dir}:`); events.emit('verbose', CordovaError.fullStack(loadFromDirError)); const cdvPlatform = platform.replace(/^(?:cordova-)?/, 'cordova-'); try { // Load the platform API directly from node_modules PlatformApi = require(cdvPlatform); } catch (loadByNameError) { events.emit('verbose', `Unable to load module ${cdvPlatform} by name:`); events.emit('verbose', CordovaError.fullStack(loadByNameError)); throw new CordovaError(`Could not load API for ${platform} project ${dir}`); } } // Module doesn't implement the expected interface if (!PlatformApi || !PlatformApi.createPlatform) { throw new Error(`The package at "${dir}" does not appear to implement the Cordova Platform API.`); } events.emit('verbose', `Loaded API for ${platform} project ${dir}`); return PlatformApi; } ================================================ FILE: src/hooks/Context.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const { CordovaError } = require('cordova-common'); /** * Creates hook script context * @constructor * @param {String} hook The hook type * @param {Object} opts Hook options * @returns {Object} */ function Context (hook, opts) { this.hook = hook; // create new object, to avoid affecting input opts in other places // For example context.opts.plugin = Object is done, then it affects by reference this.opts = Object.assign({}, opts); this.cmdLine = process.argv.join(' '); // Lazy-load cordova to avoid cyclical dependency Object.defineProperty(this, 'cordova', { get () { return this.requireCordovaModule('cordova-lib').cordova; } }); } /** * Requires the specified Cordova module. * * This method should only be used to require packages named `cordova-*`. * Public modules of such a Cordova module can be required by giving their * full package path: `cordova-foo/bar` for example. * * @param {String} modulePath Module path as specified above * @returns {*} The required Cordova module */ Context.prototype.requireCordovaModule = function (modulePath) { const [pkg, ...pkgPath] = modulePath.split('/'); if (!pkg.match(/^cordova-[^/]+/)) { throw new CordovaError( 'Using "requireCordovaModule" to load non-cordova module ' + `"${modulePath}" is not supported. Instead, add this module to ` + 'your dependencies and use regular "require" to load it.' ); } // We can only resolve `cordova-lib` by name if this module is installed as // a dependency of the current main module (e.g. when running `cordova`). // To handle `cordova-lib` paths correctly in all other cases too, we // require them using relative paths. return pkg === 'cordova-lib' ? require(path.posix.join('../..', ...pkgPath)) : require(modulePath); }; module.exports = Context; ================================================ FILE: src/hooks/HooksRunner.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const execa = require('execa'); const cordovaUtil = require('../cordova/util'); const scriptsFinder = require('./scriptsFinder'); const Context = require('./Context'); const { CordovaError, events } = require('cordova-common'); /** * Tries to create a HooksRunner for passed project root. * @constructor */ function HooksRunner (projectRoot) { const root = cordovaUtil.isCordova(projectRoot); if (!root) throw new CordovaError('Not a Cordova project ("' + projectRoot + '"), can\'t use hooks.'); else this.projectRoot = root; } /** * Fires all event handlers and scripts for a passed hook type. * Returns a promise. */ HooksRunner.prototype.fire = function fire (hook, opts) { if (isHookDisabled(opts, hook)) { return Promise.resolve('hook ' + hook + ' is disabled.'); } // args check if (!hook) { throw new Error('hook type is not specified'); } opts = this.prepareOptions(opts); // first run hook event listeners, then run hook script files return runJobsSerially([ ...getEventHandlerJobs(hook, opts), ...getHookJobs(hook, opts) ]); }; /** * Refines passed options so that all required parameters are set. */ HooksRunner.prototype.prepareOptions = function (opts) { opts = opts || {}; opts.projectRoot = this.projectRoot; opts.cordova = opts.cordova || {}; opts.cordova.platforms = opts.cordova.platforms || opts.platforms || cordovaUtil.listPlatforms(opts.projectRoot); opts.cordova.platforms = opts.cordova.platforms.map(function (platform) { return platform.split('@')[0]; }); opts.cordova.plugins = opts.cordova.plugins || opts.plugins || cordovaUtil.findPlugins(path.join(opts.projectRoot, 'plugins')); try { opts.cordova.version = opts.cordova.version || require('../../package').version; } catch (ex) { events.emit('warn', 'HooksRunner could not load package.json: ' + ex.message); } return opts; }; module.exports = HooksRunner; /** * Executes hook event handlers serially. Doesn't require a HooksRunner to be constructed. * Returns a promise. */ module.exports.fire = globalFire; function globalFire (hook, opts) { if (isHookDisabled(opts, hook)) { return Promise.resolve('hook ' + hook + ' is disabled.'); } opts = opts || {}; return runJobsSerially(getEventHandlerJobs(hook, opts)); } function getEventHandlerJobs (hook, opts) { return events.listeners(hook) .map(handler => () => handler(opts)); } function getHookJobs (hook, opts) { const scripts = scriptsFinder.getHookScripts(hook, opts); if (scripts.length === 0) { events.emit('verbose', `No scripts found for hook "${hook}".`); } const context = new Context(hook, opts); return scripts.map(script => () => runScript(script, context)); } function runJobsSerially (jobs) { return jobs.reduce((acc, job) => acc.then(job), Promise.resolve()); } /** * Async runs single script file. */ function runScript (script, context) { let source; let relativePath; if (script.plugin) { source = 'plugin ' + script.plugin.id; relativePath = path.join('plugins', script.plugin.id, script.path); } else { source = 'config.xml'; relativePath = path.normalize(script.path); } events.emit('verbose', 'Executing script found in ' + source + ' for hook "' + context.hook + '": ' + relativePath); const runScriptStrategy = path.extname(script.path).toLowerCase() === '.js' ? runScriptViaModuleLoader : runScriptViaChildProcessSpawn; return runScriptStrategy(script, context); } /** * Runs script using require. * Returns a promise. */ function runScriptViaModuleLoader (script, context) { if (!fs.existsSync(script.fullPath)) { events.emit('warn', 'Script file does\'t exist and will be skipped: ' + script.fullPath); return Promise.resolve(); } const scriptFn = require(script.fullPath); context.scriptLocation = script.fullPath; if (script.plugin) { context.opts.plugin = script.plugin; } // We can't run script if it is a plain Node script - it will run its commands when we require it. // This is not a desired case as we want to pass context, but added for compatibility. if (scriptFn instanceof Function) { // If hook is async it can return promise instance and we will handle it. return Promise.resolve(scriptFn(context)); } else { return Promise.resolve(); } } /** * Runs script using child_process spawn method. * Returns a promise. */ function runScriptViaChildProcessSpawn (script, context) { const opts = context.opts; const command = script.fullPath; const args = [opts.projectRoot]; const execOpts = { cwd: opts.projectRoot, stdio: 'inherit', env: { CORDOVA_VERSION: require('../../package').version, CORDOVA_PLATFORMS: opts.platforms ? opts.platforms.join() : '', CORDOVA_PLUGINS: opts.plugins ? opts.plugins.join() : '', CORDOVA_HOOK: script.fullPath, CORDOVA_CMDLINE: process.argv.join(' ') } }; events.emit('log', `Running hook: ${command} ${args.join(' ')}`); return execa(command, args, execOpts) .then(data => data.stdout); } /** * Checks if the given hook type is disabled at the command line option. * @param {Object} opts - the option object that contains command line options * @param {String} hook - the hook type * @returns {Boolean} return true if the passed hook is disabled. */ function isHookDisabled (opts, hook) { if (opts === undefined || opts.nohooks === undefined) { return false; } return opts.nohooks.some(pattern => hook.match(pattern) !== null); } ================================================ FILE: src/hooks/scriptsFinder.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const cordovaUtil = require('../cordova/util'); const events = require('cordova-common').events; const PluginInfoProvider = require('cordova-common').PluginInfoProvider; const ConfigParser = require('cordova-common').ConfigParser; /** * Implements logic to retrieve hook script files defined in special folders and configuration * files: config.xml, hooks/hook_type, plugins/../plugin.xml, etc */ module.exports = { /** * Returns all script files for the hook type specified. */ getHookScripts: function (hook, opts) { // args check if (!hook) { throw new Error('hook type is not specified'); } return getApplicationHookScripts(hook, opts) .concat(getPluginsHookScripts(hook, opts)); } }; /** * Gets all scripts defined in config.xml with the specified type and platforms. */ function getApplicationHookScripts (hook, opts) { // args check if (!hook) { throw new Error('hook type is not specified'); } const configPath = cordovaUtil.projectConfig(opts.projectRoot); const configXml = new ConfigParser(configPath); return configXml.getHookScripts(hook, opts.cordova.platforms) .map(scriptElement => ({ path: scriptElement.attrib.src, fullPath: path.join(opts.projectRoot, scriptElement.attrib.src) })); } /** * Returns script files defined by plugin developers as part of plugin.xml. */ function getPluginsHookScripts (hook, opts) { // args check if (!hook) { throw new Error('hook type is not specified'); } // In case before_plugin_install, after_plugin_install, before_plugin_uninstall hooks we receive opts.plugin and // retrieve scripts exclusive for this plugin. if (opts.plugin) { events.emit('verbose', 'Finding scripts for "' + hook + '" hook from plugin ' + opts.plugin.id + ' on ' + opts.plugin.platform + ' platform only.'); // if plugin hook is not run for specific platform then use all available platforms return getPluginScriptFiles(opts.plugin, hook, opts.plugin.platform ? [opts.plugin.platform] : opts.cordova.platforms); } return getAllPluginsHookScriptFiles(hook, opts); } /** * Gets hook scripts defined by the plugin. */ function getPluginScriptFiles (plugin, hook, platforms) { const scriptElements = plugin.pluginInfo.getHookScripts(hook, platforms); return scriptElements.map(function (scriptElement) { return { path: scriptElement.attrib.src, fullPath: path.join(plugin.dir, scriptElement.attrib.src), plugin }; }); } /** * Gets hook scripts defined by all plugins. */ function getAllPluginsHookScriptFiles (hook, opts) { let scripts = []; let currentPluginOptions; const plugins = (new PluginInfoProvider()).getAllWithinSearchPath(path.join(opts.projectRoot, 'plugins')); plugins.forEach(function (pluginInfo) { currentPluginOptions = { id: pluginInfo.id, pluginInfo, dir: pluginInfo.dir }; scripts = scripts.concat(getPluginScriptFiles(currentPluginOptions, hook, opts.cordova.platforms)); }); return scripts; } ================================================ FILE: src/platforms/index.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); const fs = require('node:fs'); const util = require('../cordova/util'); const platforms = require('./platformsConfig.json'); const events = require('cordova-common').events; // Avoid loading the same platform projects more than once (identified by path) const cachedApis = {}; // getPlatformApi() should be the only method of instantiating the // PlatformProject classes for now. function getPlatformApi (platform, platformRootDir) { // if platformRootDir is not specified, try to detect it first if (!platformRootDir) { const projectRootDir = util.isCordova(); platformRootDir = projectRootDir && path.join(projectRootDir, 'platforms', platform); } if (!platformRootDir) { // If platformRootDir is still undefined, then we're probably is not inside of cordova project throw new Error('Current location is not a Cordova project'); } // CB-11174 Resolve symlinks first before working with root directory platformRootDir = util.convertToRealPathSafe(platformRootDir); // Make sure the platforms/platform folder exists if (!fs.existsSync(platformRootDir)) { throw new Error('The platform "' + platform + '" does not appear to have been added to this project.'); } let platformApi; const cached = cachedApis[platformRootDir]; const libDir = path.join(platformRootDir, 'cordova', 'Api.js'); if (cached && cached.platform === platform) { platformApi = cached; } else { const PlatformApi = util.getPlatformApiFunction(libDir, platform); platformApi = new PlatformApi(platform, platformRootDir, events); cachedApis[platformRootDir] = platformApi; } return platformApi; } // Used to prevent attempts of installing platforms that are not supported on // the host OS. E.g. ios on linux. function hostSupports (platform) { const { hostos } = platforms[platform] || {}; return !hostos || hostos.includes(process.platform); } module.exports = { getPlatformApi, hostSupports, info: platforms, list: Object.keys(platforms) }; ================================================ FILE: src/platforms/platforms.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const { getPlatformApi, info } = require('.'); // Shallow-copy so that we don't affect other instances below module.exports = Object.assign({}, info); // We don't want these methods to be enumerable on the platforms object, because we expect enumerable properties of the // platforms object to be platforms. Object.defineProperties(module.exports, { getPlatformApi: { value: getPlatformApi, configurable: true, enumerable: false, writable: true } }); ================================================ FILE: src/platforms/platformsConfig.json ================================================ { "ios": { "hostos": ["darwin"], "url": "https://github.com/apache/cordova-ios.git", "deprecated": false }, "android": { "url": "https://github.com/apache/cordova-android.git", "deprecated": false }, "browser": { "parser_file": "../cordova/metadata/browser_parser", "handler_file": "../plugman/platforms/browser", "url": "https://github.com/apache/cordova-browser.git", "deprecated": false }, "electron": { "url": "https://github.com/apache/cordova-electron.git", "deprecated": false } } ================================================ FILE: src/plugman/fetch.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const semver = require('semver'); const fetch = require('cordova-fetch'); const PluginInfoProvider = require('cordova-common').PluginInfoProvider; const CordovaError = require('cordova-common').CordovaError; const events = require('cordova-common').events; const metadata = require('./util/metadata'); const pluginSpec = require('../cordova/plugin/plugin_spec_parser'); const cordovaUtil = require('../cordova/util'); let projectRoot; // Cache of PluginInfo objects for plugins in search path. let localPlugins = null; // possible options: link, subdir, git_ref, client, expected_id // Returns a promise. module.exports = fetchPlugin; function fetchPlugin (plugin_src, plugins_dir, options) { // Ensure the containing directory exists. fs.mkdirSync(plugins_dir, { recursive: true }); options = options || {}; options.subdir = options.subdir || '.'; options.searchpath = options.searchpath || []; if (typeof options.searchpath === 'string') { options.searchpath = options.searchpath.split(path.delimiter); } const pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider(); try { // clone from git repository const uri = new URL(plugin_src); // If the hash exists, it has the form from npm: http://foo.com/bar#git-ref[:subdir] // git-ref can be a commit SHA, a tag, or a branch // NB: No leading or trailing slash on the subdir. if (uri.hash) { const result = uri.hash.match(/^#([^:]*)(?::\/?(.*?)\/?)?$/); if (result) { if (result[1]) { options.git_ref = result[1]; } if (result[2]) { options.subdir = result[2]; } } } } catch (_) { } return Promise.resolve().then(function () { let plugin_dir = cordovaUtil.fixRelativePath(path.join(plugin_src, options.subdir)); return Promise.resolve().then(function () { // check if it is a local path if (fs.existsSync(plugin_dir)) { if (!fs.existsSync(path.join(plugin_dir, 'package.json'))) { return Promise.reject(new CordovaError('Invalid Plugin! ' + plugin_dir + ' needs a valid package.json')); } projectRoot = path.join(plugins_dir, '..'); // Plugman projects need to go up two directories to reach project root. // Plugman projects have an options.projectRoot variable if (options.projectRoot) { projectRoot = options.projectRoot; } return fetch(path.resolve(plugin_dir), projectRoot, options) .then(function (directory) { return { pinfo: pluginInfoProvider.get(directory), fetchJsonSource: { type: 'local', path: path.relative(projectRoot, directory) } }; }).catch(function (error) { // something went wrong with cordova-fetch return Promise.reject(new CordovaError(error.message)); }); } // If there is no such local path or it's a git URL, it's a plugin id or id@versionspec. // First look for it in the local search path (if provided). const pinfo = findLocalPlugin(plugin_src, options.searchpath, pluginInfoProvider); if (pinfo) { events.emit('verbose', 'Found ' + plugin_src + ' at ' + pinfo.dir); return { pinfo, fetchJsonSource: { type: 'local', path: pinfo.dir } }; } else if (options.noregistry) { return Promise.reject(new CordovaError( 'Plugin ' + plugin_src + ' not found locally. ' + 'Note, plugin registry was disabled by --noregistry flag.' )); } // If not found in local search path, fetch from the registry. const parsedSpec = pluginSpec.parse(plugin_src); let P; let skipCopyingPlugin; plugin_dir = path.join(plugins_dir, parsedSpec.id); // if the plugin has already been fetched, use it. if (fs.existsSync(plugin_dir)) { P = Promise.resolve(plugin_dir); skipCopyingPlugin = true; } else { // use cordova-fetch projectRoot = path.join(plugins_dir, '..'); // Plugman projects need to go up two directories to reach project root. // Plugman projects have an options.projectRoot variable if (options.projectRoot) { projectRoot = options.projectRoot; } P = fetch(plugin_src, projectRoot, options); skipCopyingPlugin = false; } return P .catch(function (error) { const message = 'Failed to fetch plugin ' + plugin_src + ' via registry.' + '\nProbably this is either a connection problem, or plugin spec is incorrect.' + '\nCheck your connection and plugin name/version/URL.' + '\n' + error; return Promise.reject(new CordovaError(message)); }) .then(function (dir) { return { pinfo: pluginInfoProvider.get(dir), fetchJsonSource: { type: 'registry', id: plugin_src }, skipCopyingPlugin }; }); }).then(function (result) { options.plugin_src_dir = result.pinfo.dir; let P; if (result.skipCopyingPlugin) { P = Promise.resolve(options.plugin_src_dir); } else { P = Promise.resolve(copyPlugin(result.pinfo, plugins_dir, options.link && result.fetchJsonSource.type === 'local')); } return P.then(function (dir) { result.dest = dir; return result; }); }); }).then(function (result) { checkID(options.expected_id, result.pinfo); const data = { source: result.fetchJsonSource }; data.is_top_level = options.is_top_level; data.variables = options.variables || {}; metadata.save_fetch_metadata(plugins_dir, result.pinfo.id, data); return result.dest; }); } // Helper function for checking expected plugin IDs against reality. function checkID (expectedIdAndVersion, pinfo) { if (!expectedIdAndVersion) return; const parsedSpec = pluginSpec.parse(expectedIdAndVersion); if (parsedSpec.id !== pinfo.id) { throw new Error('Expected plugin to have ID "' + parsedSpec.id + '" but got "' + pinfo.id + '".'); } if (parsedSpec.version && !semver.satisfies(pinfo.version, parsedSpec.version)) { throw new Error('Expected plugin ' + pinfo.id + ' to satisfy version "' + parsedSpec.version + '" but got "' + pinfo.version + '".'); } } // Note, there is no cache invalidation logic for local plugins. // As of this writing loadLocalPlugins() is never called with different // search paths and such case would not be handled properly. function loadLocalPlugins (searchpath, pluginInfoProvider) { if (localPlugins) { // localPlugins already populated, nothing to do. // just in case, make sure it was loaded with the same search path if (localPlugins.searchpath.join(path.delimiter) !== searchpath.join(path.delimiter)) { const msg = 'loadLocalPlugins called twice with different search paths. ' + 'Support for this is not implemented. Using previously cached path.'; events.emit('warn', msg); } return; } // Populate localPlugins object. localPlugins = {}; localPlugins.searchpath = searchpath; localPlugins.plugins = {}; searchpath.forEach(function (dir) { const ps = pluginInfoProvider.getAllWithinSearchPath(dir); ps.forEach(function (p) { const versions = localPlugins.plugins[p.id] || []; versions.push(p); localPlugins.plugins[p.id] = versions; }); }); } // If a plugin is fund in local search path, return a PluginInfo for it. // Ignore plugins that don't satisfy the required version spec. // If several versions are present in search path, return the latest. // Examples of accepted plugin_src strings: // org.apache.cordova.file // org.apache.cordova.file@>=1.2.0 function findLocalPlugin (plugin_src, searchpath, pluginInfoProvider) { loadLocalPlugins(searchpath, pluginInfoProvider); const parsedSpec = pluginSpec.parse(plugin_src); const versionspec = parsedSpec.version || '*'; let latest = null; const versions = localPlugins.plugins[parsedSpec.id]; if (!versions) return null; versions.forEach(function (pinfo) { // Ignore versions that don't satisfy the the requested version range. // Ignore -dev suffix because latest semver versions doesn't handle it properly (CB-9421) if (!semver.satisfies(pinfo.version.replace(/-dev$/, ''), versionspec)) { return; } if (!latest) { latest = pinfo; return; } if (semver.gt(pinfo.version, latest.version)) { latest = pinfo; } }); return latest; } // Copy or link a plugin from plugin_dir to plugins_dir/plugin_id. // if alternative ID of plugin exists in plugins_dir/plugin_id, skip copying function copyPlugin (pinfo, plugins_dir, link) { const plugin_dir = pinfo.dir; const dest = path.join(plugins_dir, pinfo.id); fs.rmSync(dest, { recursive: true, force: true }); if (!link && dest.indexOf(path.resolve(plugin_dir) + path.sep) === 0) { events.emit('verbose', 'Copy plugin destination is child of src. Forcing --link mode.'); link = true; } if (link) { const isRelativePath = plugin_dir.charAt(1) !== ':' && plugin_dir.charAt(0) !== path.sep; const fixedPath = isRelativePath ? path.join(path.relative(plugins_dir, process.env.PWD || process.cwd()), plugin_dir) : plugin_dir; events.emit('verbose', 'Linking "' + dest + '" => "' + fixedPath + '"'); fs.symlinkSync(fixedPath, dest, 'junction'); } else { events.emit('verbose', 'Copying plugin "' + plugin_dir + '" => "' + dest + '"'); fs.cpSync(plugin_dir, dest, { recursive: true, dereference: true }); } return dest; } ================================================ FILE: src/plugman/install.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const execa = require('execa'); const ActionStack = require('cordova-common').ActionStack; const semver = require('semver'); const PlatformJson = require('cordova-common').PlatformJson; const CordovaError = require('cordova-common').CordovaError; const platform_modules = require('../platforms/platforms'); const events = require('cordova-common').events; const HooksRunner = require('../hooks/HooksRunner'); const pluginSpec = require('../cordova/plugin/plugin_spec_parser'); const cordovaUtil = require('../cordova/util'); const PluginInfo = require('cordova-common').PluginInfo; const PluginInfoProvider = require('cordova-common').PluginInfoProvider; const variableMerge = require('./variable-merge'); const plugmanFetch = require('./fetch'); const DepGraph = require('./util/dep-graph'); const isWindows = (os.platform().substr(0, 3) === 'win'); /* INSTALL FLOW ------------ There are four functions install "flows" through. Here is an attempt at providing a high-level logic flow overview. 1. module.exports (installPlugin) a) checks that the platform is supported b) converts oldIds into newIds (CPR -> npm) c) invokes possiblyFetch 2. possiblyFetch a) checks that the plugin is fetched. if so, calls runInstall b) if not, invokes plugman.fetch, and when done, calls runInstall 3. runInstall a) checks if the plugin is already installed. if so, calls back (done). b) if possible, will check the version of the project and make sure it is compatible with the plugin (checks tags) c) makes sure that any variables required by the plugin are specified. if they are not specified, plugman will throw or callback with an error. d) if dependencies are listed in the plugin, it will recurse for each dependent plugin, autoconvert IDs to newIDs and call possiblyFetch (2) on each one. When each dependent plugin is successfully installed, it will then proceed to call handleInstall (4) 4. handleInstall a) queues up actions into a queue (asset, source-file, headers, etc) b) processes the queue c) calls back (done) */ // possible options: subdir, cli_variables, www_dir // Returns a promise. module.exports = function installPlugin (platform, project_dir, id, plugins_dir, options) { project_dir = cordovaUtil.convertToRealPathSafe(project_dir); plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir); options = options || {}; if (!Object.prototype.hasOwnProperty.call(options, 'is_top_level')) options.is_top_level = true; plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins'); const current_stack = new ActionStack(); return possiblyFetch(id, plugins_dir, options) .then(function (plugin_dir) { return module.exports.runInstall(current_stack, platform, project_dir, plugin_dir, plugins_dir, options); }); }; // possible options: subdir, cli_variables, www_dir, git_ref, is_top_level // Returns a promise. function possiblyFetch (id, plugins_dir, options) { const parsedSpec = pluginSpec.parse(id); // if plugin is a relative path, check if it already exists const plugin_src_dir = isAbsolutePath(id) ? id : path.join(plugins_dir, parsedSpec.id); // Check that the plugin has already been fetched. if (fs.existsSync(plugin_src_dir)) { return Promise.resolve(plugin_src_dir); } const opts = Object.assign({}, options, { client: 'plugman' }); return plugmanFetch(id, plugins_dir, opts); } function checkEngines (engines) { for (let i = 0; i < engines.length; i++) { const { currentVersion, minVersion, name } = engines[i]; if (semver.satisfies(currentVersion, minVersion, { loose: true, includePrerelease: true }) || currentVersion === null) { continue; // engine ok! } else { const msg = `Plugin doesn't support this project's ${name} version. ${name}: ${currentVersion}, failed version requirement: ${minVersion}`; events.emit('warn', msg); return Promise.reject(Object.assign(new Error(), { skip: true })); } } return Promise.resolve(true); } function cleanVersionOutput (version, name) { let out = version.trim(); const rc_index = out.indexOf('rc'); const dev_index = out.indexOf('dev'); if (rc_index > -1) { out = out.substr(0, rc_index) + '-' + out.substr(rc_index); } // put a warning about using the dev branch if (dev_index > -1) { // some platform still lists dev branches as just dev, set to null and continue if (out === 'dev') { out = null; } events.emit('verbose', name + ' has been detected as using a development branch. Attemping to install anyways.'); } // add extra period/digits to conform to semver - some version scripts will output // just a major or major minor version number const majorReg = /\d+/; const minorReg = /\d+\.\d+/; const patchReg = /\d+\.\d+\.\d+/; if (!patchReg.test(out)) { if (minorReg.test(out)) { out = out.match(minorReg)[0] + '.0'; } else if (majorReg.test(out)) { out = out.match(majorReg)[0] + '.0.0'; } } return out; } // exec engine scripts in order to get the current engine version // Returns a promise for the array of engines. function callEngineScripts (engines, project_dir) { return Promise.all( engines.map(function (engine) { // CB-5192; on Windows scriptSrc doesn't have file extension so we shouldn't check whether the script exists const scriptPath = engine.scriptSrc || null; if (scriptPath && (isWindows || fs.existsSync(engine.scriptSrc))) { if (!isWindows) { // not required on Windows fs.chmodSync(engine.scriptSrc, '755'); } return execa(scriptPath) .then(({ stdout }) => { engine.currentVersion = cleanVersionOutput(stdout, engine.name); if (engine.currentVersion === '') { events.emit('warn', engine.name + ' version check returned nothing (' + scriptPath + '), continuing anyways.'); engine.currentVersion = null; } }, () => { events.emit('warn', engine.name + ' version check failed (' + scriptPath + '), continuing anyways.'); engine.currentVersion = null; }) .then(_ => engine); } else { if (engine.currentVersion) { engine.currentVersion = cleanVersionOutput(engine.currentVersion, engine.name); } else { events.emit('warn', engine.name + ' version not detected (lacks script ' + scriptPath + ' ), continuing.'); } return Promise.resolve(engine); } }) ); } // return only the engines we care about/need function getEngines (pluginInfo, platform, project_dir, plugin_dir) { const engines = pluginInfo.getEngines(); const defaultEngines = require('./util/default-engines')(project_dir); const uncheckedEngines = []; let cordovaEngineIndex, cordovaPlatformEngineIndex, theName, platformIndex, defaultPlatformIndex; // load in known defaults and update when necessary engines.forEach(function (engine) { theName = engine.name; // check to see if the engine is listed as a default engine if (defaultEngines[theName]) { // make sure engine is for platform we are installing on defaultPlatformIndex = defaultEngines[theName].platform.indexOf(platform); if (defaultPlatformIndex > -1 || defaultEngines[theName].platform === '*') { defaultEngines[theName].minVersion = defaultEngines[theName].minVersion ? defaultEngines[theName].minVersion : engine.version; defaultEngines[theName].currentVersion = defaultEngines[theName].currentVersion ? defaultEngines[theName].currentVersion : null; defaultEngines[theName].scriptSrc = defaultEngines[theName].scriptSrc ? defaultEngines[theName].scriptSrc : null; defaultEngines[theName].name = theName; // set the indices so we can pop the cordova engine when needed if (theName === 'cordova') cordovaEngineIndex = uncheckedEngines.length; if (theName === 'cordova-' + platform) cordovaPlatformEngineIndex = uncheckedEngines.length; uncheckedEngines.push(defaultEngines[theName]); } // check for other engines } else { if (typeof engine.platform === 'undefined' || typeof engine.scriptSrc === 'undefined') { throw new CordovaError('engine.platform or engine.scriptSrc is not defined in custom engine "' + theName + '" from plugin "' + pluginInfo.id + '" for ' + platform); } platformIndex = engine.platform.indexOf(platform); // CB-7183: security check for scriptSrc path escaping outside the plugin const scriptSrcPath = path.resolve(plugin_dir, engine.scriptSrc); if (scriptSrcPath.indexOf(plugin_dir) !== 0) { throw new Error('Security violation: scriptSrc ' + scriptSrcPath + ' is out of plugin dir ' + plugin_dir); } if (platformIndex > -1 || engine.platform === '*') { uncheckedEngines.push({ name: theName, platform: engine.platform, scriptSrc: scriptSrcPath, minVersion: engine.version }); } } }); // make sure we check for platform req's and not just cordova reqs if (cordovaEngineIndex && cordovaPlatformEngineIndex) uncheckedEngines.pop(cordovaEngineIndex); return uncheckedEngines; } // possible options: cli_variables, www_dir, is_top_level // Returns a promise. module.exports.runInstall = runInstall; function runInstall (actions, platform, project_dir, plugin_dir, plugins_dir, options) { project_dir = cordovaUtil.convertToRealPathSafe(project_dir); plugin_dir = cordovaUtil.convertToRealPathSafe(plugin_dir); plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir); options = options || {}; options.graph = options.graph || new DepGraph(); options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider(); const pluginInfoProvider = options.pluginInfoProvider; const pluginInfo = pluginInfoProvider.get(plugin_dir); let filtered_variables = {}; const platformJson = PlatformJson.load(plugins_dir, platform); if (platformJson.isPluginInstalled(pluginInfo.id)) { if (options.is_top_level) { let msg = 'Plugin "' + pluginInfo.id + '" already installed on ' + platform + '.'; if (platformJson.isPluginDependent(pluginInfo.id)) { msg += ' Making it top-level.'; platformJson.makeTopLevel(pluginInfo.id).save(); } events.emit('log', msg); } else { events.emit('log', 'Dependent plugin "' + pluginInfo.id + '" already installed on ' + platform + '.'); } // CB-11022 return true always in this case since if the plugin is installed // we don't need to call prepare in any way return Promise.resolve(true); } events.emit('log', 'Installing "' + pluginInfo.id + '" for ' + platform); const theEngines = getEngines(pluginInfo, platform, project_dir, plugin_dir); const install = { actions, platform, project_dir, plugins_dir, top_plugin_id: pluginInfo.id, top_plugin_dir: plugin_dir }; return Promise.resolve().then(function () { if (options.platformVersion) { return Promise.resolve(options.platformVersion); } return Promise.resolve(cordovaUtil.getPlatformVersion(project_dir)); }).then(function (platformVersion) { options.platformVersion = platformVersion; return callEngineScripts(theEngines, path.resolve(plugins_dir, '..')); }).then(function (engines) { return checkEngines(engines); }).then(function () { filtered_variables = variableMerge.mergeVariables(plugin_dir, platform, options); install.filtered_variables = filtered_variables; // Check for dependencies const dependencies = pluginInfo.getDependencies(platform); if (dependencies.length) { return installDependencies(install, dependencies, options); } return Promise.resolve(true); } ).then( function () { const install_plugin_dir = path.join(plugins_dir, pluginInfo.id); // may need to copy to destination... if (!fs.existsSync(install_plugin_dir)) { copyPlugin(plugin_dir, plugins_dir, options.link, pluginInfoProvider); } const projectRoot = cordovaUtil.isCordova(); if (projectRoot) { // using unified hooksRunner const hookOptions = { cordova: { platforms: [platform] }, plugin: { id: pluginInfo.id, pluginInfo, platform: install.platform, dir: install.top_plugin_dir }, nohooks: options.nohooks }; // CB-10708 This is the case when we're trying to install plugin using plugman to specific // platform inside of the existing CLI project. In this case we need to put plugin's files // into platform_www but plugman CLI doesn't allow us to do that, so we set it here options.usePlatformWww = true; const hooksRunner = new HooksRunner(projectRoot); return hooksRunner.fire('before_plugin_install', hookOptions).then(function () { return handleInstall(actions, pluginInfo, platform, project_dir, plugins_dir, install_plugin_dir, filtered_variables, options); }).then(function (installResult) { return hooksRunner.fire('after_plugin_install', hookOptions) // CB-11022 Propagate install result to caller to be able to avoid unnecessary prepare .then(_ => installResult); }); } else { return handleInstall(actions, pluginInfo, platform, project_dir, plugins_dir, install_plugin_dir, filtered_variables, options); } } ).catch( function (error) { if (error.skip) { events.emit('warn', 'Skipping \'' + pluginInfo.id + '\' for ' + platform); } else { events.emit('warn', 'Failed to install \'' + pluginInfo.id + '\': ' + error.stack); throw error; } } ); } function installDependencies (install, dependencies, options) { events.emit('verbose', 'Dependencies detected, iterating through them...'); options.searchpath = options.searchpath || []; // Search for dependency by Id is: // a) Look for {$top_plugins}/{$depId} directory // b) Scan the top level plugin directory {$top_plugins} for matching id (searchpath) // c) Fetch from registry return dependencies.reduce(function (soFar, dep) { return soFar.then( function () { dep.git_ref = dep.commit; if (dep.subdir) { dep.subdir = path.normalize(dep.subdir); } // We build the dependency graph only to be able to detect cycles, getChain will throw an error if it detects one options.graph.add(install.top_plugin_id, dep.id); options.graph.getChain(install.top_plugin_id); return tryFetchDependency(dep, install, options) .then( function (url) { dep.url = url; return installDependency(dep, install, options); } ); } ); }, Promise.resolve(true)); } function tryFetchDependency (dep, install, options) { // Handle relative dependency paths by expanding and resolving them. // The easy case of relative paths is to have a URL of '.' and a different subdir. // TODO: Implement the hard case of different repo URLs, rather than the special case of // same-repo-different-subdir. let relativePath; if (dep.url === '.') { // Look up the parent plugin's fetch metadata and determine the correct URL. const fetchdata = require('./util/metadata').get_fetch_metadata(install.plugins_dir, install.top_plugin_id); if (!fetchdata || !(fetchdata.source && fetchdata.source.type)) { relativePath = dep.subdir || dep.id; events.emit('warn', 'No fetch metadata found for plugin ' + install.top_plugin_id + '. checking for ' + relativePath + ' in ' + options.searchpath.join(',')); return Promise.resolve(relativePath); } // Now there are two cases here: local directory, and git URL. if (fetchdata.source.type === 'local') { dep.url = fetchdata.source.path; return execa.command('git rev-parse --show-toplevel', { cwd: dep.url }) .catch(err => { if (err.exitCode === 128) { throw new Error('Plugin ' + dep.id + ' is not in git repository. All plugins must be in a git repository.'); } else { throw new Error('Failed to locate git repository for ' + dep.id + ' plugin.'); } }) .then(({ stdout: git_repo }) => { // Clear out the subdir since the url now contains it const url = path.join(git_repo, dep.subdir); dep.subdir = ''; return Promise.resolve(url); }).catch(function () { return Promise.resolve(dep.url); }); } else if (fetchdata.source.type === 'git') { return Promise.resolve(fetchdata.source.url); } else if (fetchdata.source.type === 'dir') { // Note: With fetch() independant from install() // $md5 = md5(uri) // Need a Hash(uri) --> $tmpDir/cordova-fetch/git-hostname.com-$md5/ // plugin[id].install.source --> searchpath that matches fetch uri // mapping to a directory of OS containing fetched plugins let tmpDir = fetchdata.source.url; tmpDir = tmpDir.replace('$tmpDir', os.tmpdir()); let pluginSrc = ''; if (dep.subdir.length) { // Plugin is relative to directory pluginSrc = path.join(tmpDir, dep.subdir); } // Try searchpath in dir, if that fails re-fetch if (!pluginSrc.length || !fs.existsSync(pluginSrc)) { pluginSrc = dep.id; // Add search path if (options.searchpath.indexOf(tmpDir) === -1) { options.searchpath.unshift(tmpDir); } // place at top of search } return Promise.resolve(pluginSrc); } } // Test relative to parent folder if (dep.url && !isAbsolutePath(dep.url)) { relativePath = path.resolve(install.top_plugin_dir, '../' + dep.url); if (fs.existsSync(relativePath)) { dep.url = relativePath; } } // CB-4770: registry fetching if (dep.url === undefined) { dep.url = dep.id; } return Promise.resolve(dep.url); } function installDependency (dep, install, options) { let opts; dep.install_dir = path.join(install.plugins_dir, dep.id); events.emit('verbose', 'Requesting plugin "' + (dep.version ? dep.id + '@' + dep.version : dep.id) + '".'); if (fs.existsSync(dep.install_dir)) { const pluginInfo = new PluginInfo(dep.install_dir); const version_installed = pluginInfo.version; let version_required = dep.version; if (dep.version) { if (Number(dep.version.replace('.', ''))) { version_required = '^' + dep.version; } } // strip -dev from the installed plugin version so it properly passes // semver.satisfies let stripped_version; if (version_installed.includes('-dev')) { stripped_version = semver.inc(version_installed, 'patch'); } if (options.force || semver.satisfies(version_installed, version_required, /* loose= */true) || semver.satisfies(stripped_version, version_required, /* loose= */true) || version_required === null || version_required === undefined || version_required === '') { events.emit('log', 'Plugin dependency "' + (version_installed ? dep.id + '@' + version_installed : dep.id) + '" already fetched, using that version.'); } else { const msg = 'Version of installed plugin: "' + dep.id + '@' + version_installed + '" does not satisfy dependency plugin requirement "' + dep.id + '@' + version_required + '". Try --force to use installed plugin as dependency.'; return Promise.resolve() .then(function () { // Remove plugin return fs.rmSync(path.join(install.plugins_dir, install.top_plugin_id), { recursive: true, force: true }); }).then(function () { // Return promise chain and finally reject return Promise.reject(new CordovaError(msg)); }); } opts = Object.assign({}, options, { cli_variables: install.filtered_variables, is_top_level: false }); return module.exports.runInstall(install.actions, install.platform, install.project_dir, dep.install_dir, install.plugins_dir, opts); } else { events.emit('verbose', 'Plugin dependency "' + dep.id + '" not fetched, retrieving then installing.'); opts = Object.assign({}, options, { cli_variables: install.filtered_variables, is_top_level: false, subdir: dep.subdir, git_ref: dep.git_ref, expected_id: dep.id }); const dep_src = dep.url.length ? dep.url : (dep.version ? dep.id + '@' + dep.version : dep.id); return possiblyFetch(dep_src, install.plugins_dir, opts) .then( function (plugin_dir) { return module.exports.runInstall(install.actions, install.platform, install.project_dir, plugin_dir, install.plugins_dir, opts); } ); } } function handleInstall (actions, pluginInfo, platform, project_dir, plugins_dir, plugin_dir, filtered_variables, options) { // @tests - important this event is checked spec/install.spec.js events.emit('verbose', 'Install start for "' + pluginInfo.id + '" on ' + platform + '.'); options.variables = filtered_variables; return platform_modules.getPlatformApi(platform, project_dir) .addPlugin(pluginInfo, options) .then(function (result) { events.emit('verbose', 'Install complete for ' + pluginInfo.id + ' on ' + platform + '.'); // Add plugin to installed list. This already done in platform, // but need to be duplicated here to manage dependencies properly. PlatformJson.load(plugins_dir, platform) .addPlugin(pluginInfo.id, filtered_variables, options.is_top_level) .save(); // WIN! // Log out plugin INFO element contents in case additional install steps are necessary const info_strings = pluginInfo.getInfo(platform) || []; info_strings.forEach(function (info) { events.emit('results', interp_vars(filtered_variables, info)); }); // Propagate value, returned by platform's addPlugin method to caller return Promise.resolve(result); }); } function interp_vars (vars, text) { vars && Object.keys(vars).forEach(function (key) { const regExp = new RegExp('\\$' + key, 'g'); text = text.replace(regExp, vars[key]); }); return text; } function isAbsolutePath (_path) { // some valid abs paths: 'c:' '/' '\' and possibly ? 'file:' 'http:' return _path && (_path.charAt(0) === path.sep || _path.indexOf(':') > 0); } // Copy or link a plugin from plugin_dir to plugins_dir/plugin_id. function copyPlugin (plugin_src_dir, plugins_dir, link, pluginInfoProvider) { const pluginInfo = new PluginInfo(plugin_src_dir); const dest = path.join(plugins_dir, pluginInfo.id); if (link) { events.emit('verbose', 'Symlinking from location "' + plugin_src_dir + '" to location "' + dest + '"'); fs.rmSync(dest, { recursive: true, force: true }); fs.ensureSymlinkSync(plugin_src_dir, dest, 'junction'); } else { events.emit('verbose', 'Copying from location "' + plugin_src_dir + '" to location "' + dest + '"'); fs.cpSync(plugin_src_dir, dest, { recursive: true }); } pluginInfo.dir = dest; pluginInfoProvider.put(pluginInfo); return dest; } ================================================ FILE: src/plugman/plugman.js ================================================ /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // copyright (c) 2013 Andrew Lunny, Adobe Systems const events = require('cordova-common').events; const plugman = { on: events.on.bind(events), off: events.removeListener.bind(events), removeAllListeners: events.removeAllListeners.bind(events), emit: events.emit.bind(events), install: require('./install'), uninstall: require('./uninstall'), fetch: require('./fetch') }; module.exports = plugman; ================================================ FILE: src/plugman/uninstall.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const ActionStack = require('cordova-common').ActionStack; const dependencies = require('./util/dependencies'); const CordovaError = require('cordova-common').CordovaError; const events = require('cordova-common').events; const platform_modules = require('../platforms/platforms'); const promiseutil = require('../util/promise-util'); const HooksRunner = require('../hooks/HooksRunner'); const cordovaUtil = require('../cordova/util'); const npmUninstall = require('cordova-fetch').uninstall; const PlatformJson = require('cordova-common').PlatformJson; const PluginInfoProvider = require('cordova-common').PluginInfoProvider; const variableMerge = require('../plugman/variable-merge'); // possible options: cli_variables, www_dir // Returns a promise. module.exports = uninstall; function uninstall (platform, project_dir, id, plugins_dir, options) { project_dir = cordovaUtil.convertToRealPathSafe(project_dir); plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir); options = options || {}; options.is_top_level = true; options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider(); plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins'); // Allow `id` to be a path to a file. const xml_path = path.join(id, 'plugin.xml'); if (fs.existsSync(xml_path)) { id = options.pluginInfoProvider.get(id).id; } return module.exports.uninstallPlatform(platform, project_dir, id, plugins_dir, options) .then(function () { return module.exports.uninstallPlugin(id, plugins_dir, options); }); } // Returns a promise. module.exports.uninstallPlatform = function (platform, project_dir, id, plugins_dir, options) { project_dir = cordovaUtil.convertToRealPathSafe(project_dir); plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir); options = options || {}; options.is_top_level = true; options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider(); plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins'); if (!platform_modules[platform]) { return Promise.reject(new CordovaError('Platform "' + platform + '" not supported.')); } const plugin_dir = path.join(plugins_dir, id); if (!fs.existsSync(plugin_dir)) { return Promise.reject(new CordovaError('Plugin "' + id + '" not found. Already uninstalled?')); } const current_stack = new ActionStack(); return Promise.resolve().then(function () { if (options.platformVersion) { return Promise.resolve(options.platformVersion); } return Promise.resolve(cordovaUtil.getPlatformVersion(project_dir)); }).then(function (platformVersion) { options.platformVersion = platformVersion; return runUninstallPlatform(current_stack, platform, project_dir, plugin_dir, plugins_dir, options); }); }; // Returns a promise. module.exports.uninstallPlugin = function (id, plugins_dir, options) { plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir); options = options || {}; options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider(); const pluginInfoProvider = options.pluginInfoProvider; const plugin_dir = path.join(plugins_dir, id); // @tests - important this event is checked spec/uninstall.spec.js events.emit('log', 'Removing "' + id + '"'); // If already removed, skip. if (!fs.existsSync(plugin_dir)) { events.emit('verbose', 'Plugin "' + id + '" already removed (' + plugin_dir + ')'); return Promise.resolve(); } /* * Deletes plugin from plugins directory and node_modules directory. * * @param {String} id the id of the plugin being removed * * @return {Promise||Error} Returns a empty promise or a promise of doing the npm uninstall */ const doDelete = function (id) { const plugin_dir = path.join(plugins_dir, id); if (!fs.existsSync(plugin_dir)) { events.emit('verbose', 'Plugin "' + id + '" already removed (' + plugin_dir + ')'); return Promise.resolve(); } fs.rmSync(plugin_dir, { recursive: true, force: true }); events.emit('verbose', 'Deleted plugin "' + id + '"'); // remove plugin from node_modules directory return npmUninstall(id, options.projectRoot, options); }; // We've now lost the metadata for the plugins that have been uninstalled, so we can't use that info. // Instead, we list all dependencies of the target plugin, and check the remaining metadata to see if // anything depends on them, or if they're listed as top-level. // If neither, they can be deleted. const top_plugin_id = id; // Recursively remove plugins which were installed as dependents (that are not top-level) const toDelete = []; function findDependencies (pluginId) { const depPluginDir = path.join(plugins_dir, pluginId); // Skip plugin check for dependencies if it does not exist (CB-7846). if (!fs.existsSync(depPluginDir)) { events.emit('verbose', 'Plugin "' + pluginId + '" does not exist (' + depPluginDir + ')'); return; } const pluginInfo = pluginInfoProvider.get(depPluginDir); // TODO: Should remove dependencies in a separate step, since dependencies depend on platform. const deps = pluginInfo.getDependencies(); deps.forEach(function (d) { if (toDelete.indexOf(d.id) === -1) { toDelete.push(d.id); findDependencies(d.id); } }); } findDependencies(top_plugin_id); toDelete.push(top_plugin_id); // Okay, now we check if any of these are depended on, or top-level. // Find the installed platforms by whether they have a metadata file. const platforms = Object.keys(platform_modules).filter(function (platform) { return fs.existsSync(path.join(plugins_dir, platform + '.json')); }); // Can have missing plugins on some platforms when not supported.. const dependList = {}; platforms.forEach(function (platform) { const platformJson = PlatformJson.load(plugins_dir, platform); const depsInfo = dependencies.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider); const tlps = depsInfo.top_level_plugins; let deps; // Top-level deps must always be explicitely asked to remove by user tlps.forEach(function (plugin_id) { if (top_plugin_id === plugin_id) { return; } const i = toDelete.indexOf(plugin_id); if (i >= 0) { toDelete.splice(i, 1); } }); toDelete.forEach(function (plugin) { deps = dependencies.dependents(plugin, depsInfo, platformJson, pluginInfoProvider); const i = deps.indexOf(top_plugin_id); if (i >= 0) { deps.splice(i, 1); } // remove current/top-level plugin as blocking uninstall if (deps.length) { dependList[plugin] = deps.join(', '); } }); }); const dependPluginIds = toDelete.filter(function (plugin_id) { return dependList[plugin_id]; }); const createMsg = function (plugin_id) { return 'Plugin "' + plugin_id + '" is required by (' + dependList[plugin_id] + ')'; }; const createMsg2 = function (plugin_id) { return createMsg(plugin_id) + ' and cannot be removed (hint: use -f or --force)'; }; if (!options.force && dependPluginIds.includes(top_plugin_id)) { const msg = createMsg2(top_plugin_id); return Promise.reject(new CordovaError(msg)); } // action emmiting events. if (options.force) { dependPluginIds.forEach(function (plugin_id) { const msg = createMsg(plugin_id); events.emit('log', msg + ' but forcing removal.'); }); } else { dependPluginIds.forEach(function (plugin_id) { const msg = createMsg2(plugin_id); events.emit('warn', msg); }); } const deletePluginIds = options.force ? toDelete : toDelete.filter(function (plugin_id) { return !dependList[plugin_id]; }); const deleteExecList = deletePluginIds.map(function (plugin_id) { return function () { return doDelete(plugin_id); }; }); return deleteExecList.reduce(function (acc, deleteExec) { return acc.then(deleteExec); }, Promise.resolve()); }; // possible options: cli_variables, www_dir, is_top_level // Returns a promise function runUninstallPlatform (actions, platform, project_dir, plugin_dir, plugins_dir, options) { const pluginInfoProvider = options.pluginInfoProvider; // If this plugin is not really installed, return (CB-7004). if (!fs.existsSync(plugin_dir)) { return Promise.resolve(true); } const pluginInfo = pluginInfoProvider.get(plugin_dir); const plugin_id = pluginInfo.id; // Merge cli_variables and plugin.xml variables const variables = variableMerge.mergeVariables(plugin_dir, platform, options); // Deps info can be passed recusively const platformJson = PlatformJson.load(plugins_dir, platform); const depsInfo = options.depsInfo || dependencies.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider); // Check that this plugin has no dependents. const dependents = dependencies.dependents(plugin_id, depsInfo, platformJson, pluginInfoProvider); if (options.is_top_level && dependents && dependents.length > 0) { const msg = 'The plugin \'' + plugin_id + '\' is required by (' + dependents.join(', ') + ')'; if (options.force) { events.emit('warn', msg + ' but forcing removal'); } else { return Promise.reject(new CordovaError(msg + ', skipping uninstallation. (try --force if trying to update)')); } } // Check how many dangling dependencies this plugin has. const deps = depsInfo.graph.getChain(plugin_id); const danglers = dependencies.danglers(plugin_id, depsInfo, platformJson, pluginInfoProvider); let promise; if (deps && deps.length && danglers && danglers.length) { // @tests - important this event is checked spec/uninstall.spec.js events.emit('log', 'Uninstalling ' + danglers.length + ' dependent plugins.'); promise = promiseutil.Q_chainmap(danglers, function (dangler) { const dependent_path = path.join(plugins_dir, dangler); const opts = Object.assign({}, options, { is_top_level: depsInfo.top_level_plugins.indexOf(dangler) > -1, depsInfo }); return runUninstallPlatform(actions, platform, project_dir, dependent_path, plugins_dir, opts); }); } else { promise = Promise.resolve(); } const projectRoot = cordovaUtil.isCordova(); if (projectRoot) { // CB-10708 This is the case when we're trying to uninstall plugin using plugman from specific // platform inside of the existing CLI project. This option is usually set by cordova-lib for CLI projects // but since we're running this code through plugman, we need to set it here implicitly options.usePlatformWww = true; options.cli_variables = variables; } return promise.then(function () { if (!projectRoot) return; const hooksRunner = new HooksRunner(projectRoot); const hooksRunnerOptions = { cordova: { platforms: [platform] }, plugin: { id: pluginInfo.id, pluginInfo, platform, dir: plugin_dir } }; return hooksRunner.fire('before_plugin_uninstall', hooksRunnerOptions); }).then(function () { return handleUninstall(actions, platform, pluginInfo, project_dir, options.www_dir, plugins_dir, options.is_top_level, options); }); } // Returns a promise. function handleUninstall (actions, platform, pluginInfo, project_dir, www_dir, plugins_dir, is_top_level, options) { events.emit('log', 'Uninstalling ' + pluginInfo.id + ' from ' + platform); // Set up platform to uninstall asset files/js modules // from /platform_www dir instead of /www. options.usePlatformWww = true; return platform_modules.getPlatformApi(platform, project_dir) .removePlugin(pluginInfo, options) .then(function (result) { // Remove plugin from installed list. This already done in platform, // but need to be duplicated here to remove plugin entry from project's // plugin list to manage dependencies properly. PlatformJson.load(plugins_dir, platform) .removePlugin(pluginInfo.id, is_top_level) .save(); // CB-11022 propagate `removePlugin` result to the caller return Promise.resolve(result); }); } ================================================ FILE: src/plugman/util/default-engines.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const path = require('node:path'); module.exports = function (project_dir) { return { cordova: { platform: '*', currentVersion: require('../../../package.json').version }, 'cordova-plugman': { platform: '*', currentVersion: require('../../../package.json').version }, 'cordova-android': { platform: 'android', scriptSrc: path.join(project_dir, 'cordova', 'version') }, 'cordova-ios': { platform: 'ios', scriptSrc: path.join(project_dir, 'cordova', 'version') }, 'cordova-osx': { platform: 'osx', scriptSrc: path.join(project_dir, 'cordova', 'version') }, 'cordova-windows': { platform: 'windows', scriptSrc: path.join(project_dir, 'cordova', 'version') }, 'cordova-browser': { platform: 'browser', scriptSrc: path.join(project_dir, 'cordova', 'version') }, 'cordova-electron': { platform: 'electron', scriptSrc: path.join(project_dir, 'cordova', 'version') }, 'apple-xcode': { platform: 'ios', scriptSrc: path.join(project_dir, 'cordova', 'apple_xcode_version') }, 'apple-ios': { platform: 'ios', scriptSrc: path.join(project_dir, 'cordova', 'apple_ios_version') }, 'apple-osx': { platform: 'ios', scriptSrc: path.join(project_dir, 'cordova', 'apple_osx_version') }, 'android-sdk': { platform: 'android', scriptSrc: path.join(project_dir, 'cordova', 'android_sdk_version') } }; }; ================================================ FILE: src/plugman/util/dep-graph.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ module.exports = class DepGraph { constructor () { this.graph = new Map(); } add (id, dep) { if (!this.graph.has(id)) { this.graph.set(id, new Set()); } this.graph.get(id).add(dep); if (!this.graph.has(dep)) { this.graph.set(dep, new Set()); } } getChain (id) { const visited = new Set(); const stack = new Set(); const result = []; const traverse = (node, parent = null) => { if (stack.has(node)) { throw new Error(`Cyclic dependency from ${parent} to ${node}`); } if (!this.graph.has(node)) return; stack.add(node); for (const dep of this.graph.get(node)) { if (!visited.has(dep)) { traverse(dep, node); result.push(dep); visited.add(dep); } } stack.delete(node); }; traverse(id); return result; } }; ================================================ FILE: src/plugman/util/dependencies.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); const events = require('cordova-common').events; const DepGraph = require('./dep-graph'); let pkg; module.exports = pkg = { generateDependencyInfo: function (platformJson, plugins_dir, pluginInfoProvider) { const json = platformJson.root; // TODO: store whole dependency tree in plugins/[platform].json // in case plugins are forcefully removed... const tlps = []; const graph = new DepGraph(); Object.keys(json.installed_plugins).forEach(function (plugin_id) { tlps.push(plugin_id); const plugin_dir = path.join(plugins_dir, plugin_id); const pluginInfo = pluginInfoProvider.get(plugin_dir); const deps = pluginInfo.getDependencies(platformJson.platform); deps.forEach(function (dep) { graph.add(plugin_id, dep.id); }); }); Object.keys(json.dependent_plugins).forEach(function (plugin_id) { const plugin_dir = path.join(plugins_dir, plugin_id); // dependency plugin does not exist (CB-7846) if (!fs.existsSync(plugin_dir)) { events.emit('verbose', 'Plugin "' + plugin_id + '" does not exist (' + plugin_dir + ')'); return; } const pluginInfo = pluginInfoProvider.get(plugin_dir); const deps = pluginInfo.getDependencies(platformJson.platform); deps.forEach(function (dep) { graph.add(plugin_id, dep.id); }); }); return { graph, top_level_plugins: tlps }; }, // Returns a list of top-level plugins which are (transitively) dependent on the given plugin. dependents: function (plugin_id, plugins_dir, platformJson, pluginInfoProvider) { let depsInfo; if (typeof plugins_dir === 'object') { depsInfo = plugins_dir; } else { depsInfo = pkg.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider); } const graph = depsInfo.graph; const tlps = depsInfo.top_level_plugins; const dependents = tlps.filter(function (tlp) { return tlp !== plugin_id && graph.getChain(tlp).indexOf(plugin_id) >= 0; }); return dependents; }, // Returns a list of plugins which the given plugin depends on, for which it is the only dependent. // In other words, if the given plugin were deleted, these dangling dependencies should be deleted too. danglers: function (plugin_id, plugins_dir, platformJson, pluginInfoProvider) { let depsInfo; if (typeof plugins_dir === 'object') { depsInfo = plugins_dir; } else { depsInfo = pkg.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider); } const { graph, top_level_plugins } = depsInfo; // get plugin_id's dependencies const dependencies = graph.getChain(plugin_id); // Calculate the set of all top-level plugins and their transitive dependencies const otherTlps = top_level_plugins.filter(tlp => tlp !== plugin_id); const otherTlpDeps = otherTlps.map(tlp => graph.getChain(tlp)); const remainingPlugins = new Set(otherTlps.concat(...otherTlpDeps)); // dependencies - remainingPlugins return dependencies.filter(p => !remainingPlugins.has(p)); } }; ================================================ FILE: src/plugman/util/metadata.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('node:fs'); const path = require('node:path'); let cachedJson = null; function getJson (pluginsDir) { if (!cachedJson) { const fetchJsonPath = path.join(pluginsDir, 'fetch.json'); if (fs.existsSync(fetchJsonPath)) { cachedJson = JSON.parse(fs.readFileSync(fetchJsonPath, 'utf-8')); } else { cachedJson = {}; } } return cachedJson; } exports.get_fetch_metadata = function (pluginsDir, pluginId) { const metadataJson = getJson(pluginsDir); return metadataJson[pluginId] || {}; }; exports.save_fetch_metadata = function (pluginsDir, pluginId, data) { const metadataJson = getJson(pluginsDir); metadataJson[pluginId] = data; const fetchJsonPath = path.join(pluginsDir, 'fetch.json'); fs.writeFileSync(fetchJsonPath, JSON.stringify(metadataJson, null, 2), 'utf-8'); }; exports.remove_fetch_metadata = function (pluginsDir, pluginId) { const metadataJson = getJson(pluginsDir); delete metadataJson[pluginId]; const fetchJsonPath = path.join(pluginsDir, 'fetch.json'); fs.writeFileSync(fetchJsonPath, JSON.stringify(metadataJson, null, 2), 'utf-8'); }; ================================================ FILE: src/plugman/variable-merge.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const PluginInfoProvider = require('cordova-common').PluginInfoProvider; module.exports.mergeVariables = mergeVariables; /* * At this point, cli and config vars have already merged. * Merges those vars (cli and config) with plugin.xml variables. * * @param {string} plugin directory * @param {string} platform * @param {object} options * * @return {object} list of filtered variables */ function mergeVariables (plugin_dir, platform, options) { options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider(); const pluginInfo = options.pluginInfoProvider.get(plugin_dir); const prefs = pluginInfo.getPreferences(platform); const keys = Object.keys(prefs); options.cli_variables = options.cli_variables || {}; // For all vars defined in prefs, take the value from cli_variables or // default to the value from prefs if truthy. const mergedVars = {}; for (const key of keys) { if (key in options.cli_variables) { mergedVars[key] = options.cli_variables[key]; } else if (prefs[key]) { mergedVars[key] = options.cli_variables[key] = prefs[key]; } } // Test for missing vars after having applied defaults const mergedVarKeys = new Set(Object.keys(mergedVars)); const missing_vars = keys.filter(key => !mergedVarKeys.has(key)); if (missing_vars.length > 0) { throw new Error('Variable(s) missing: ' + missing_vars.join(', ')); } return mergedVars; } ================================================ FILE: src/util/promise-util.js ================================================ /** Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Given a function and an array of values, creates a chain of promises that // will sequentially execute func(args[i]). // Returns a promise. // function Q_chainmap (args, func) { return Promise.resolve().then(function (inValue) { return args.reduce(function (soFar, arg) { return soFar.then(function (val) { return func(arg, val); }); }, Promise.resolve(inValue)); }); } exports.Q_chainmap = Q_chainmap;