├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── app └── .gitkeep ├── blueprints └── ember-cli-dependency-lint │ ├── files │ └── config │ │ └── dependency-lint.js │ └── index.js ├── config └── environment.js ├── ember-cli-build.js ├── index.js ├── lib ├── .eslintrc.js ├── broccoli │ └── project-dependency-linter.js ├── commands │ └── dependency-lint.js └── utils │ ├── dependents-to-string.js │ ├── discover-addon-versions.js │ ├── read-config.js │ └── validate-project.js ├── package.json ├── testem.js ├── tests-node ├── .eslintrc.js ├── acceptance │ └── dependency-lint-test.js ├── fixtures │ └── test-app │ │ ├── config │ │ └── dependency-lint.js │ │ └── lib │ │ ├── addon-v1 │ │ ├── index.js │ │ └── package.json │ │ ├── addon-v2 │ │ ├── index.js │ │ └── package.json │ │ ├── addon-with-custom-version-specifier │ │ ├── index.js │ │ └── package.json │ │ ├── requires-v1 │ │ ├── index.js │ │ └── package.json │ │ └── requires-v2 │ │ ├── index.js │ │ └── package.json ├── helpers │ ├── dedent.js │ ├── mock-config │ │ ├── dependency-lint.js │ │ └── index.js │ └── mock-project.js └── unit │ └── utils │ ├── dependents-to-string-test.js │ ├── discover-addon-versions-test.js │ └── validate-project-test.js ├── tests ├── .eslintrc.js ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ └── components │ │ │ └── .gitkeep │ ├── config │ │ ├── dependency-lint.js │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── .gitkeep │ └── resolver.js ├── index.html ├── integration │ └── .gitkeep ├── test-helper.js └── unit │ └── .gitkeep ├── vendor └── .gitkeep └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | 12 | # misc 13 | /coverage/ 14 | 15 | # ember-try 16 | /.node_modules.ember-try/ 17 | /bower.json.ember-try 18 | /package.json.ember-try 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | extends: [ 8 | 'eslint:recommended' 9 | ], 10 | env: { 11 | browser: true 12 | }, 13 | rules: { 14 | }, 15 | overrides: [ 16 | // node files 17 | { 18 | files: [ 19 | '.template-lintrc.js', 20 | 'ember-cli-build.js', 21 | 'index.js', 22 | 'testem.js', 23 | 'blueprints/*/index.js', 24 | 'config/**/*.js', 25 | 'tests/dummy/config/**/*.js', 26 | 'lib/**', 27 | 'tests-node/**', 28 | ], 29 | excludedFiles: [ 30 | 'addon/**', 31 | 'addon-test-support/**', 32 | 'app/**', 33 | 'tests/dummy/app/**' 34 | ], 35 | parserOptions: { 36 | sourceType: 'script', 37 | ecmaVersion: 2015 38 | }, 39 | env: { 40 | browser: false, 41 | node: true 42 | }, 43 | plugins: ['node'], 44 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 45 | // add your custom rules and overrides for node files here 46 | }) 47 | }, 48 | 49 | // node tests 50 | { 51 | files: ['tests-node/**'], 52 | rules: { 53 | 'node/no-unpublished-require': 'off' 54 | } 55 | } 56 | ] 57 | }; 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/ 15 | /libpeerconnection.log 16 | /npm-debug.log* 17 | /testem.log 18 | /yarn-error.log 19 | 20 | # ember-try 21 | /.node_modules.ember-try/ 22 | /bower.json.ember-try 23 | /package.json.ember-try 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.eslintignore 13 | /.eslintrc.js 14 | /.gitignore 15 | /.watchmanconfig 16 | /.travis.yml 17 | /bower.json 18 | /config/ember-try.js 19 | /ember-cli-build.js 20 | /testem.js 21 | /tests/ 22 | /yarn.lock 23 | .gitkeep 24 | 25 | # ember-try 26 | /.node_modules.ember-try/ 27 | /bower.json.ember-try 28 | /package.json.ember-try 29 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended' 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - 12 7 | 8 | sudo: false 9 | dist: trusty 10 | 11 | addons: 12 | chrome: stable 13 | 14 | cache: 15 | yarn: true 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | 22 | jobs: 23 | fail_fast: true 24 | 25 | include: 26 | - stage: "Tests" 27 | name: "Browser Tests" 28 | script: 29 | - yarn lint:hbs 30 | - yarn lint:js 31 | - yarn test-browser 32 | - name: "Node Tests" 33 | script: 34 | - yarn test-node 35 | 36 | install: 37 | - yarn install --no-lockfile --non-interactive 38 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 Time Marches Ever On (November 5, 2020) 2 | ### Added 3 | - `@embroider/macros` has been added to the default ignore list 4 | 5 | ### Breaking 6 | - Dropped support for Node 6 and 8 7 | 8 | ## 1.1.3 Spruce Forest (December 14, 2018) 9 | ### Added 10 | - `ember-auto-import` has been added to the default ignore list, as it is only a build-time addon 11 | - `ember-cli-typescript` has been added to the default ignore list, as it is only a build-time addon 12 | 13 | ## 1.1.2 Stay In Line (December 10, 2018) 14 | ### Added 15 | - `ember-cli-htmlbars-inline-precompile` has been added to the default ignore list, as it is only a build-time addon 16 | 17 | ## 1.1.1 Help Me Out (October 4, 2018) 18 | ### Added 19 | - `ember-compatibility-helpers` has been added to the default ignore list, as it has no runtime components that could clash 20 | 21 | ## 1.0.2 Star Light, Star Bright (March 17, 2017) 22 | ### Fixed 23 | - A version specifier of `'*'` now accepts prerelease versions as well. This is contrary to [node-semver's behavior](https://github.com/npm/node-semver#prerelease-tags), which was designed for safety when upgrading a single package across prerelease versions, but is consistent with the need to be able to designate "no really, anything goes" here. ([#5](https://github.com/salsify/ember-cli-dependency-lint/issues/5)) 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-cli-dependency-lint [![Build Status](https://travis-ci.org/salsify/ember-cli-dependency-lint.svg?branch=master)](https://travis-ci.org/salsify/ember-cli-dependency-lint) 2 | 3 | This addon adds lint tests that verify only one version of any given addon will be activated in the final built application. 4 | 5 | ## Motivation 6 | 7 | Suppose you're happily building an application using [`ember-modal-dialog`](https://github.com/yapplabs/ember-modal-dialog), which in turn relies on [`ember-wormhole`](https://github.com/yapplabs/ember-wormhole) at `0.3.x`. You then go add [`ember-power-select`](https://github.com/cibernox/ember-power-select), which relies also relies on `ember-wormhole` via [`ember-basic-dropdown`](https://github.com/cibernox/ember-basic-dropdown), but at `0.5.x`. Your dependencies might now look like this: 8 | 9 | ``` 10 | my-app 11 | ├─┬ ember-modal-dialog 12 | │ └── ember-wormhole@0.3.6 13 | └─┬ ember-power-select 14 | └─┬ ember-basic-dropdown 15 | └── ember-wormhole@0.5.1 16 | ``` 17 | 18 | Your package manager notices the conflicting version requirements for `ember-wormhole` and helpfully makes sure each addon gets the version it's asking for. But your final built application will only have one copy of `ember-wormhole`—which version will it be? 19 | 20 | In the end, Ember CLI will merge both versions together, with files from one version clobbering files from the other whenever they have the same name. This also means either `ember-modal-dialog` or `ember-power-select` will wind up attempting to use a version of `ember-wormhole` that it's not expecting, which can lead to anything from hard exceptions to subtle behavioral bugs. 21 | 22 | ## Solution 23 | 24 | In the scenario described above, the version conflict arose because of adding a new dependency, but it can also happen when you update an existing one. Regardless of how it happens, it may or may not immediately be obvious that something is wrong. The things that break may be subtle, or in untested edges cases in your application. 25 | 26 | The purpose of this addon is to detect that situation as soon as it happens and inform you about it, allowing you the opportunity to make an informed decision about how to handle it. 27 | 28 | ### Usage 29 | 30 | For each addon in your project, ember-cli-dependency-lint will create a passing or failing test case depending on whether you have conflicting versions of that addon present. This way, the next time you run your tests after introducing a dependency conflict, you'll immediately know about the problem. 31 | 32 | ![image](https://cloud.githubusercontent.com/assets/108688/22833669/c5d35a9a-ef80-11e6-8043-9c6de18e8d6e.png) 33 | 34 | You can also manually run `ember dependency-lint` to get a more detailed report. This can be useful while debugging a dependency conflict, as it's much faster than rebuilding your test suite each time. 35 | 36 | ![image](https://cloud.githubusercontent.com/assets/108688/22833728/009c1bd0-ef81-11e6-853c-8516f13b58fd.png) 37 | 38 | Run `ember help dependency-lint` for more details on this command. 39 | 40 | ### Dealing with Conflicts 41 | 42 | In the `ember-wormhole` example above, you have several options you might choose from: 43 | 44 | - pin your app's `ember-power-select` dependency to an older version that uses `ember-wormhole` 0.3 (if one exists) until `ember-modal-dialog` is updated 45 | - fork `ember-modal-dialog` and make whatever changes are necessary for it to work with `ember-wormhole` 0.5, then use your fork until those changes are accepted upstream 46 | - add a `resolutions` entry to your `package.json` to force a specific version of `ember-wormhole` to be used in spite of the differing version constraints (test carefully if you choose this option!) 47 | 48 | ### Build-time Addons 49 | 50 | Some addons don't actually add files to your application tree, so they don't have the conflict problem described above. In fact, for some addons (like preprocessors such as `ember-cli-babel`), insisting on a single version is undesirable. Different addons your app uses should be able to compile using whatever tooling they like without conflicting with one another. 51 | 52 | Out of the box, this addon automatically allows for multiple arbitrary versions of: 53 | - `@embroider/macros` 54 | - `ember-cli-htmlbars` 55 | - `ember-cli-babel` 56 | - `ember-cli-sass` 57 | - `ember-cli-node-assets` 58 | - `ember-compatibility-helpers` 59 | - `ember-cli-htmlbars-inline-precompile` 60 | - `ember-auto-import` 61 | - `ember-cli-typescript` 62 | 63 | Instructions for allowing multiple versions of other addons (or overriding these defaults) can be found below. 64 | 65 | ## Configuration 66 | 67 | Configuration for this addon is specified in a dedicated file in your project's `config` folder. For apps, this will be `config/dependency-lint.js`, and for addons, this will be the dummy app's `tests/dummy/config/dependency-lint.js`. 68 | 69 | ### Lint Tests 70 | 71 | For each addon dependency in your project, ember-cli-dependency-lint will generate a passing or failing test case (similar to other linting addons like `ember-cli-eslint`). If you only ever want to manually check your dependencies, you can set the `generateTests` flag to `false`. 72 | 73 | ```js 74 | // config/dependency-lint.js 75 | module.exports = { 76 | generateTests: false 77 | }; 78 | ``` 79 | 80 | ### Allowed Versions 81 | 82 | Out of the box, ember-cli-dependency-lint expects to find at most one version of any addon in an app's dependency tree, but it doesn't care precisely what that version is. To either tighten or loosen that restriction for a given addon, you can provide a [semver](https://github.com/npm/node-semver) specifier. For build-time-only addons, adding an entry here can be useful, but if the conflicting addon has runtime code, using `resolutions` or one of the other approaches outlined in **Dealing with Conflicts** above is strongly recommended. 83 | 84 | ```js 85 | // config/dependency-lint.js 86 | module.exports = { 87 | allowedVersions: { 88 | // Fails unless every instance of addon-a is exactly version 1.2.3 89 | 'addon-a': '1.2.3', 90 | 91 | // Fails unless every instance of addon-b is either 1.2.3 or 1.2.4 92 | 'addon-b': '1.2.3 || 1.2.4', 93 | 94 | // Allows any version of addon-c such that 1.0.4 <= version < 2.0.0 95 | 'addon-c': '^1.0.4', 96 | 97 | // Allows any number of arbitrary versions of addon-d (default for the addons listed above in Build-time Addons) 98 | 'addon-d': '*' 99 | } 100 | }; 101 | ``` 102 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/app/.gitkeep -------------------------------------------------------------------------------- /blueprints/ember-cli-dependency-lint/files/config/dependency-lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /blueprints/ember-cli-dependency-lint/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-cli-dependency-lint', 6 | 7 | normalizeEntityName() { 8 | 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | const readConfig = require('./lib/utils/read-config'); 5 | 6 | module.exports = { 7 | name: require('./package').name, 8 | 9 | init() { 10 | this._super.init && this._super.init.apply(this, arguments); 11 | this.lintConfig = readConfig(this.project); 12 | }, 13 | 14 | includedCommands() { 15 | return { 16 | 'dependency-lint': require('./lib/commands/dependency-lint'), 17 | }; 18 | }, 19 | 20 | lintTree(type) { 21 | const project = this.project; 22 | if (type === 'tests' && this.lintConfig.generateTests !== false) { 23 | const ProjectDependencyLinter = require('./lib/broccoli/project-dependency-linter'); 24 | return new ProjectDependencyLinter({ project }); 25 | } 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | node: true, 5 | es6: true, 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lib/broccoli/project-dependency-linter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Plugin = require('broccoli-plugin'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const validateProject = require('../utils/validate-project'); 8 | const dependentsToString = require('../utils/dependents-to-string'); 9 | 10 | /** 11 | * Broccoli plugin that emits a test file with passing/failing tests for addons 12 | * based on the versions of that addon in the dependency tree. 13 | */ 14 | module.exports = class ProjectDependencyLinter extends Plugin { 15 | constructor(options) { 16 | super([], { 17 | persistentOutput: true, 18 | annotation: 'ProjectDependencyLinter', 19 | }); 20 | 21 | this.project = options.project; 22 | } 23 | 24 | build() { 25 | const outputPath = path.join(this.outputPath, 'dependencies.lint-test.js'); 26 | if (!fs.existsSync(outputPath)) { 27 | const results = validateProject(this.project); 28 | const testResults = results.map(addon => this._resultFor(addon)); 29 | const testFile = this.project.generateTestFile('DependencyLint', testResults); 30 | fs.writeFileSync(outputPath, testFile, 'utf-8'); 31 | } 32 | } 33 | 34 | _resultFor(validation) { 35 | let errorMessage = validation.specifier 36 | ? `Expected all versions of ${validation.addon} to satisfy "${validation.specifier}"` 37 | : `Expected only one version of ${validation.addon}`; 38 | 39 | if (!validation.valid) { 40 | errorMessage += `, but found\n${dependentsToString(validation.addon, validation.dependents)}`; 41 | } 42 | 43 | // The test writer needs newlines escaped 44 | errorMessage = errorMessage.replace(/\n/g, '\\n'); 45 | 46 | return { name: validation.addon, passed: validation.valid, errorMessage }; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /lib/commands/dependency-lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const semver = require('semver'); 4 | const chalk = require('chalk'); 5 | 6 | const validateProject = require('../utils/validate-project'); 7 | const dependentsToString = require('../utils/dependents-to-string'); 8 | 9 | module.exports = { 10 | name: 'dependency-lint', 11 | description: 'Validate that only acceptable versions of addon dependencies are present.', 12 | works: 'insideProject', 13 | 14 | availableOptions: Object.freeze([ 15 | { 16 | name: 'include-valid', 17 | type: Boolean, 18 | default: false, 19 | description: 'whether to print dependencies that pass validation', 20 | }, 21 | ]), 22 | 23 | run(options) { 24 | const results = validateProject(this.project); 25 | 26 | for (const result of results) { 27 | if (!result.valid || options.includeValid) { 28 | this._printDependents(result); 29 | } 30 | } 31 | 32 | if (!results.every(result => result.valid)) { 33 | return Promise.reject(); 34 | } 35 | }, 36 | 37 | _printDependents(validation) { 38 | const printer = version => this._dependencyString(validation, version); 39 | 40 | this.ui.writeLine(chalk.underline(validation.addon)); 41 | this.ui.writeLine(`Allowed: ${validation.specifier || '(any single version)'}`); 42 | this.ui.writeLine(`Found: ${Object.keys(validation.dependents).join(', ')}`); 43 | this.ui.writeLine(dependentsToString(validation.addon, validation.dependents, printer)); 44 | }, 45 | 46 | _dependencyString(validation, version) { 47 | const string = `${validation.addon}@${version}`; 48 | const valid = validation.specifier ? semver.satisfies(version, validation.specifier) : validation.valid; 49 | return chalk[valid ? 'green' : 'red'](string); 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /lib/utils/dependents-to-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const archy = require('archy'); 4 | 5 | /** 6 | * Given an addon name, a hash of dependents by version (as returned by discoverAddonVersions), 7 | * and optionally a function to determine how the addon itself is printed, returns a string 8 | * containing a printable version of the structure. 9 | */ 10 | module.exports = function dependentsToString(addonName, dependents, printer) { 11 | const tree = {}; 12 | 13 | for (const version of Object.keys(dependents)) { 14 | for (const dependent of dependents[version]) { 15 | let node = tree; 16 | for (const layer of dependent) { 17 | if (!node[layer]) node[layer] = {}; 18 | node = node[layer]; 19 | } 20 | node[addonName] = printer ? printer(version) : `${addonName}@${version}`; 21 | } 22 | } 23 | 24 | const root = Object.keys(tree)[0]; 25 | return archy(transformTree(root, addonName, tree[root])); 26 | }; 27 | 28 | // Transform the tree from a structure that's convenient to build into the one archy expects 29 | function transformTree(name, addon, tree) { 30 | return { 31 | label: name, 32 | nodes: sortKeys(addon, tree).map((key) => { 33 | if (key === addon) { 34 | return tree[key]; 35 | } else { 36 | return transformTree(key, addon, tree[key]); 37 | } 38 | }), 39 | }; 40 | } 41 | 42 | // Boost the addon in question to the top, then alphabetize the rest 43 | function sortKeys(addon, tree) { 44 | return Object.keys(tree).sort((a, b) => { 45 | if (a === addon) { 46 | return -1; 47 | } else if (b === addon) { 48 | return 1; 49 | } else { 50 | return a < b ? -1 : 1; 51 | } 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /lib/utils/discover-addon-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Given a Project instance, traverses the addon inclusion tree to discover all included 5 | * versions of all addons in the build, producing a structure in the format: 6 | * 7 | * { 8 | * "addon-name": { 9 | * "addon.version": [ 10 | * ["project", "immediate-dependent"], 11 | * ["project", "path-to", "nested-dependent"] 12 | * ] 13 | * } 14 | * } 15 | */ 16 | module.exports = function discoverAddonVersions(project) { 17 | const versions = Object.create(null); 18 | traverseAddonVersions([project.name()], project.addons, versions); 19 | return versions; 20 | }; 21 | 22 | function traverseAddonVersions(parentPath, addons, versions) { 23 | for (const addon of addons) { 24 | // In-repo addons may have no version, but they're tied to their parent so that's okay 25 | if (addon.pkg.version) { 26 | const addonDependents = versions[addon.pkg.name] || (versions[addon.pkg.name] = Object.create(null)); 27 | const versionDependents = addonDependents[addon.pkg.version] || (addonDependents[addon.pkg.version] = []); 28 | 29 | versionDependents.push(parentPath); 30 | } 31 | 32 | traverseAddonVersions(parentPath.concat(addon.pkg.name), addon.addons || [], versions); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/utils/read-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | /** 6 | * Given a Project instance, returns the custom dependency-lint config 7 | * for that project (if any). 8 | */ 9 | module.exports = function(project) { 10 | const configDirectory = path.dirname(project.configPath()); 11 | try { 12 | return require(`${configDirectory}/dependency-lint`); 13 | } catch (error) { 14 | if (error.code === 'MODULE_NOT_FOUND') { 15 | return {}; 16 | } else { 17 | throw error; 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/validate-project.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const semver = require('semver'); 4 | const readConfig = require('./read-config'); 5 | const discoverAddonVersions = require('./discover-addon-versions'); 6 | 7 | /** 8 | * Given a Project instance, returns an array of validation results for all addons 9 | * present in the project. Each element of the array will have three keys: 10 | * name: the name of the addon 11 | * valid: a boolean indicating whether the addon passed validation 12 | * specifier: the user-configured semver specifier (if any) 13 | * dependents: a hash mapping versions of the addon to an array of addons 14 | * that depend on that version 15 | */ 16 | module.exports = function validateProject(project) { 17 | const allowedVersions = Object.assign({}, DEFAULTS, readConfig(project).allowedVersions); 18 | const discoveredVersions = discoverAddonVersions(project); 19 | 20 | return Object.keys(discoveredVersions).map((addon) => { 21 | const dependents = discoveredVersions[addon]; 22 | const specifier = allowedVersions[addon]; 23 | const valid = validateAddonVersions(dependents, specifier); 24 | return { addon, dependents, specifier, valid }; 25 | }); 26 | }; 27 | 28 | function validateAddonVersions(dependents, specifier) { 29 | const versions = Object.keys(dependents); 30 | if (specifier) { 31 | return specifier === '*' || versions.every(version => semver.satisfies(version, specifier)); 32 | } else { 33 | return versions.length === 1; 34 | } 35 | } 36 | 37 | const DEFAULTS = { 38 | '@ember/test-waiters': '>=1.2.0', 39 | '@embroider/macros': '*', 40 | 'ember-cli-htmlbars': '*', 41 | 'ember-cli-babel': '*', 42 | 'ember-cli-sass': '*', 43 | 'ember-cli-node-assets': '*', 44 | 'ember-compatibility-helpers': '*', 45 | 'ember-cli-htmlbars-inline-precompile': '*', 46 | 'ember-auto-import': '*', 47 | 'ember-cli-typescript': '*', 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-cli-dependency-lint", 3 | "version": "2.0.1", 4 | "description": "Lint your app's addon dependencies, making sure you only have one version of each.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "repository": "salsify/ember-cli-dependency-lint", 9 | "license": "MIT", 10 | "author": "", 11 | "directories": { 12 | "doc": "doc", 13 | "test": "tests" 14 | }, 15 | "scripts": { 16 | "build": "ember build", 17 | "lint:hbs": "ember-template-lint .", 18 | "lint:js": "eslint .", 19 | "start": "ember server", 20 | "test": "ember test", 21 | "test:all": "ember try:each", 22 | "test-browser": "ember test", 23 | "test-node": "mocha tests-node/{unit,acceptance}/**/*-test.js" 24 | }, 25 | "dependencies": { 26 | "archy": "^1.0.0", 27 | "broccoli-plugin": "^1.3.0", 28 | "chalk": "^2.3.0", 29 | "semver": "^5.5.0" 30 | }, 31 | "devDependencies": { 32 | "@ember/optional-features": "^0.7.0", 33 | "chai": "^4.2.0", 34 | "console-ui": "^2.2.2", 35 | "ember-ajax": "^3.1.0", 36 | "ember-cli": "~3.4.3", 37 | "ember-cli-addon-tests": "^0.11.0", 38 | "ember-cli-babel": "^7.1.2", 39 | "ember-cli-dependency-checker": "^3.0.0", 40 | "ember-cli-htmlbars": "^3.0.0", 41 | "ember-cli-htmlbars-inline-precompile": "^1.0.3", 42 | "ember-cli-inject-live-reload": "^1.8.2", 43 | "ember-cli-qunit": "^4.3.2", 44 | "ember-cli-sri": "^2.1.1", 45 | "ember-cli-template-lint": "^1.0.0-beta.1", 46 | "ember-cli-test-loader": "^2.2.0", 47 | "ember-cli-uglify": "^2.1.0", 48 | "ember-disable-prototype-extensions": "^1.1.3", 49 | "ember-export-application-global": "^2.0.0", 50 | "ember-load-initializers": "^1.1.0", 51 | "ember-maybe-import-regenerator": "^0.1.6", 52 | "ember-resolver": "^5.0.1", 53 | "ember-source": "~3.4.0", 54 | "eslint": "^5.6.1", 55 | "eslint-plugin-node": "^7.0.1", 56 | "loader.js": "^4.7.0", 57 | "mocha": "^5.2.0", 58 | "qunit-dom": "^0.8.0", 59 | "strip-indent": "^2.0.0" 60 | }, 61 | "engines": { 62 | "node": "10.* || >= 12.*" 63 | }, 64 | "ember-addon": { 65 | "configPath": "tests/dummy/config" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless', 16 | '--disable-gpu', 17 | '--disable-dev-shm-usage', 18 | '--disable-software-rasterizer', 19 | '--mute-audio', 20 | '--remote-debugging-port=0', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests-node/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | node: true, 5 | mocha: true, 6 | es6: true, 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tests-node/acceptance/dependency-lint-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dedent = require('../helpers/dedent'); 4 | const expect = require('chai').expect; 5 | const AddonTestApp = require('ember-cli-addon-tests').AddonTestApp; 6 | const ui = new (require('console-ui'))({ 7 | outputStream: process.stdout, 8 | ci: process.env.CI, 9 | }); 10 | 11 | describe('Acceptance Tests', () => { 12 | let app; 13 | 14 | before(function() { 15 | this.timeout(300000); 16 | 17 | app = new AddonTestApp(); 18 | 19 | ui.startProgress('Creating dummy app...'); 20 | return app.create('test-app', { fixturesPath: 'tests-node/fixtures' }).then(() => { 21 | ui.stopProgress(); 22 | }); 23 | }); 24 | 25 | describe('valid dependencies', () => { 26 | beforeEach(() => { 27 | app.editPackageJSON((pkg) => { 28 | pkg['ember-addon'] = { 29 | paths: [ 30 | 'lib/addon-v1', 31 | 'lib/requires-v1', 32 | ], 33 | }; 34 | }); 35 | }); 36 | 37 | it('command exits with zero status and no output', function() { 38 | this.timeout(10000); 39 | 40 | return app.run('ember', 'dependency-lint').then((result) => { 41 | expect(result.code).to.equal(0); 42 | expect(result.output.join('')).not.to.include('Allowed:'); 43 | }); 44 | }); 45 | 46 | it('command includes valid dependencies when requested', function() { 47 | this.timeout(10000); 48 | 49 | return app.run('ember', 'dependency-lint', '--include-valid').then((result) => { 50 | const expected = dedent` 51 | addon 52 | Allowed: (any single version) 53 | Found: 1.0.0 54 | test-app 55 | ├── addon@1.0.0 56 | └─┬ requires-v1 57 | └── addon@1.0.0 58 | 59 | requires-v1 60 | Allowed: (any single version) 61 | Found: 1.0.0 62 | test-app 63 | └── requires-v1@1.0.0 64 | `; 65 | 66 | expect(result.code).to.equal(0); 67 | expect(result.output.join('')).to.include(expected); 68 | }); 69 | }); 70 | 71 | it('generated lint tests pass', function() { 72 | this.timeout(60000); 73 | 74 | return app.run('ember', 'test').then((result) => { 75 | const output = result.output.join(''); 76 | 77 | expect(result.code).to.equal(0); 78 | expect(output).to.match(/\nok[^\n]*DependencyLint: addon/); 79 | expect(output).to.match(/\nok[^\n]*DependencyLint: requires-v1/); 80 | }); 81 | }); 82 | }); 83 | 84 | describe('invalid dependencies', () => { 85 | beforeEach(() => { 86 | app.editPackageJSON((pkg) => { 87 | pkg['ember-addon'] = { 88 | paths: [ 89 | 'lib/addon-v1', 90 | 'lib/requires-v2', 91 | ], 92 | }; 93 | }); 94 | }); 95 | 96 | it('command exits with nonzero status and shows errors', function() { 97 | this.timeout(10000); 98 | 99 | return app.run('ember', 'dependency-lint').then( 100 | () => Promise.reject(new Error('Expected command to fail')), 101 | (result) => { 102 | const output = result.output.join(''); 103 | 104 | expect(result.code).to.equal(1); 105 | 106 | expect(output).to.include(dedent` 107 | addon 108 | Allowed: (any single version) 109 | Found: 1.0.0, 2.0.0 110 | test-app 111 | ├── addon@1.0.0 112 | └─┬ requires-v2 113 | └── addon@2.0.0 114 | `); 115 | 116 | expect(output).not.to.include(dedent` 117 | requires-v2 118 | Allowed: (any single version) 119 | Found: 1.0.0 120 | `); 121 | } 122 | ); 123 | }); 124 | 125 | it('command includes valid dependencies when requested', function() { 126 | this.timeout(10000); 127 | 128 | return app.run('ember', 'dependency-lint', '--include-valid').then( 129 | () => Promise.reject(new Error('Expected command to fail')), 130 | (result) => { 131 | const output = result.output.join(''); 132 | 133 | expect(result.code).to.equal(1); 134 | 135 | expect(output).to.include(dedent` 136 | addon 137 | Allowed: (any single version) 138 | Found: 1.0.0, 2.0.0 139 | test-app 140 | ├── addon@1.0.0 141 | └─┬ requires-v2 142 | └── addon@2.0.0 143 | `); 144 | 145 | expect(output).to.include(dedent` 146 | requires-v2 147 | Allowed: (any single version) 148 | Found: 1.0.0 149 | `); 150 | } 151 | ); 152 | }); 153 | 154 | it('generated lint tests fail', function() { 155 | this.timeout(60000); 156 | 157 | return app.run('ember', 'test').then( 158 | () => Promise.reject(new Error('Expected command to fail')), 159 | (result) => { 160 | const output = result.output.join(''); 161 | 162 | expect(result.code).to.equal(1); 163 | expect(output).to.match(/\nnot ok[^\n]*DependencyLint: addon/); 164 | expect(output).to.match(/\nok[^\n]*DependencyLint: requires-v2/); 165 | } 166 | ); 167 | }); 168 | }); 169 | 170 | describe('custom configuration', () => { 171 | before(() => { 172 | app.editPackageJSON((pkg) => { 173 | pkg['ember-addon'] = { 174 | paths: [ 175 | 'lib/addon-with-custom-version-specifier', 176 | ], 177 | }; 178 | }); 179 | }); 180 | 181 | it('honors explicit version specifiers', function() { 182 | this.timeout(10000); 183 | 184 | return app.run('ember', 'dependency-lint').then( 185 | () => Promise.reject(new Error('Expected command to fail')), 186 | (result) => { 187 | const output = result.output.join(''); 188 | 189 | expect(result.code).to.equal(1); 190 | 191 | expect(output).to.include(dedent` 192 | addon-with-custom-version-specifier 193 | Allowed: xxx 194 | Found: 1.0.0 195 | test-app 196 | └── addon-with-custom-version-specifier@1.0.0 197 | `); 198 | } 199 | ); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/config/dependency-lint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | allowedVersions: { 3 | 'addon-with-custom-version-specifier': 'xxx', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/addon-v1/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'addon' 3 | }; 4 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/addon-v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "addon", 3 | "version": "1.0.0", 4 | "keywords": [ 5 | "ember-addon" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/addon-v2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'addon' 3 | }; 4 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/addon-v2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "addon", 3 | "version": "2.0.0", 4 | "keywords": [ 5 | "ember-addon" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/addon-with-custom-version-specifier/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'addon-with-custom-version-specifier' 3 | }; 4 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/addon-with-custom-version-specifier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "addon-with-custom-version-specifier", 3 | "version": "1.0.0", 4 | "keywords": [ 5 | "ember-addon" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/requires-v1/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'requires-v1' 3 | }; 4 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/requires-v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requires-v1", 3 | "version": "1.0.0", 4 | "keywords": [ 5 | "ember-addon" 6 | ], 7 | "ember-addon": { 8 | "paths": ["../addon-v1"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/requires-v2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'requires-v2' 3 | }; 4 | -------------------------------------------------------------------------------- /tests-node/fixtures/test-app/lib/requires-v2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requires-v2", 3 | "version": "1.0.0", 4 | "keywords": [ 5 | "ember-addon" 6 | ], 7 | "ember-addon": { 8 | "paths": ["../addon-v2"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests-node/helpers/dedent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stripIndent = require('strip-indent'); 4 | 5 | module.exports = function dedent(parts) { 6 | return stripIndent(parts[0].substring(1)).trim() + '\n'; 7 | }; 8 | -------------------------------------------------------------------------------- /tests-node/helpers/mock-config/dependency-lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | -------------------------------------------------------------------------------- /tests-node/helpers/mock-config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require('./dependency-lint'); 4 | 5 | exports.clearConfig = function() { 6 | for (const key of Object.keys(config)) { 7 | delete config[key]; 8 | } 9 | }; 10 | 11 | exports.setConfig = function(newConfig) { 12 | Object.assign(config, newConfig); 13 | }; 14 | -------------------------------------------------------------------------------- /tests-node/helpers/mock-project.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.project = function mockProject(name, addons) { 4 | return { 5 | name: () => name, 6 | configPath: () => require.resolve('./mock-config'), 7 | addons, 8 | }; 9 | }; 10 | 11 | exports.addon = function(name, version, children) { 12 | return { 13 | pkg: { name, version }, 14 | addons: children || [], 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /tests-node/unit/utils/dependents-to-string-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const dedent = require('../../helpers/dedent'); 3 | 4 | const dependentsToString = require('../../../lib/utils/dependents-to-string'); 5 | 6 | describe('dependentsToString', () => { 7 | it('prints simple trees', () => { 8 | const dependents = { 9 | '1.0.0': [ 10 | ['foo'], 11 | ['foo', 'bar'], 12 | ], 13 | }; 14 | 15 | expect(dependentsToString('my-addon', dependents)).to.equal(dedent` 16 | foo 17 | ├── my-addon@1.0.0 18 | └─┬ bar 19 | └── my-addon@1.0.0 20 | `); 21 | }); 22 | 23 | it('hoists the addon in question and alphabetizes the rest', () => { 24 | const dependents = { 25 | '1.0.0': [ 26 | ['root'], 27 | ['root', 'qqqqq'], 28 | ['root', 'qqqqq', 'aaaaa'], 29 | ], 30 | '1.2.3': [ 31 | ['root', 'zzzzz'], 32 | ['root', 'aaaaa'], 33 | ['root', 'qqqqq', 'zzzzz'], 34 | ], 35 | }; 36 | 37 | expect(dependentsToString('mmmmm', dependents)).to.equal(dedent` 38 | root 39 | ├── mmmmm@1.0.0 40 | ├─┬ aaaaa 41 | │ └── mmmmm@1.2.3 42 | ├─┬ qqqqq 43 | │ ├── mmmmm@1.0.0 44 | │ ├─┬ aaaaa 45 | │ │ └── mmmmm@1.0.0 46 | │ └─┬ zzzzz 47 | │ └── mmmmm@1.2.3 48 | └─┬ zzzzz 49 | └── mmmmm@1.2.3 50 | `); 51 | }); 52 | 53 | it('allows for custom formatting of the addon name', () => { 54 | const printer = version => `${version}<->${version.split('').reverse().join('')}`; 55 | const dependents = { 56 | '1.0.0': [['foo']], 57 | '2.3.4': [['foo', 'bar']], 58 | }; 59 | 60 | expect(dependentsToString('my-addon', dependents, printer)).to.equal(dedent` 61 | foo 62 | ├── 1.0.0<->0.0.1 63 | └─┬ bar 64 | └── 2.3.4<->4.3.2 65 | `); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests-node/unit/utils/discover-addon-versions-test.js: -------------------------------------------------------------------------------- 1 | const discoverAddonVersions = require('../../../lib/utils/discover-addon-versions'); 2 | const mock = require('../../helpers/mock-project'); 3 | const expect = require('chai').expect; 4 | 5 | describe('discoverAddonVersions', () => { 6 | it('emits the versions at the root', () => { 7 | const project = mock.project('root', [ 8 | mock.addon('foo', '1.2.3'), 9 | mock.addon('bar', '1.0.0'), 10 | ]); 11 | 12 | expect(discoverAddonVersions(project)).to.deep.equal({ 13 | foo: { 14 | '1.2.3': [['root']], 15 | }, 16 | bar: { 17 | '1.0.0': [['root']], 18 | }, 19 | }); 20 | }); 21 | 22 | it('emits nested versions', () => { 23 | const project = mock.project('root', [ 24 | mock.addon('foo', '1.2.3'), 25 | mock.addon('bar', '1.0.0', [ 26 | mock.addon('baz', '5.0.1'), 27 | ]), 28 | ]); 29 | 30 | expect(discoverAddonVersions(project)).to.deep.equal({ 31 | foo: { 32 | '1.2.3': [['root']], 33 | }, 34 | bar: { 35 | '1.0.0': [['root']], 36 | }, 37 | baz: { 38 | '5.0.1': [['root', 'bar']], 39 | }, 40 | }); 41 | }); 42 | 43 | it('coalesces same versions found in different locations', function() { 44 | const project = mock.project('root', [ 45 | mock.addon('foo', '1.2.3'), 46 | mock.addon('bar', '1.0.0', [ 47 | mock.addon('foo', '1.2.3'), 48 | mock.addon('baz', '5.0.1', [ 49 | mock.addon('foo', '1.2.3'), 50 | ]), 51 | ]), 52 | ]); 53 | 54 | expect(discoverAddonVersions(project)).to.deep.equal({ 55 | foo: { 56 | '1.2.3': [['root'], ['root', 'bar'], ['root', 'bar', 'baz']], 57 | }, 58 | bar: { 59 | '1.0.0': [['root']], 60 | }, 61 | baz: { 62 | '5.0.1': [['root', 'bar']], 63 | }, 64 | }); 65 | }); 66 | 67 | it('records different versions found in different locations', function() { 68 | const project = mock.project('root', [ 69 | mock.addon('foo', '2.0.1'), 70 | mock.addon('bar', '1.0.0', [ 71 | mock.addon('foo', '1.2.5'), 72 | mock.addon('baz', '5.0.1', [ 73 | mock.addon('foo', '1.2.3'), 74 | ]), 75 | ]), 76 | ]); 77 | 78 | expect(discoverAddonVersions(project)).to.deep.equal({ 79 | foo: { 80 | '2.0.1': [['root']], 81 | '1.2.5': [['root', 'bar']], 82 | '1.2.3': [['root', 'bar', 'baz']], 83 | }, 84 | bar: { 85 | '1.0.0': [['root']], 86 | }, 87 | baz: { 88 | '5.0.1': [['root', 'bar']], 89 | }, 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests-node/unit/utils/validate-project-test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../../helpers/mock-project'); 2 | const config = require('../../helpers/mock-config'); 3 | const expect = require('chai').expect; 4 | 5 | const validateProject = require('../../../lib/utils/validate-project'); 6 | 7 | describe('validateAddonVersions', () => { 8 | afterEach(() => { 9 | config.clearConfig(); 10 | }); 11 | 12 | it('passes when only one version is included with no specifier', () => { 13 | const project = mock.project('my-app', [ 14 | mock.addon('my-addon', '1.2.3'), 15 | ]); 16 | 17 | expect(validateProject(project)).to.deep.equal([{ 18 | addon: 'my-addon', 19 | valid: true, 20 | specifier: undefined, 21 | dependents: { 22 | '1.2.3': [['my-app']], 23 | }, 24 | }]); 25 | }); 26 | 27 | it('lists all dependents for a given version of an addon', function() { 28 | const project = mock.project('my-app', [ 29 | mock.addon('my-addon', '1.2.3'), 30 | mock.addon('foo', '1.0.0', [ 31 | mock.addon('my-addon', '1.2.3'), 32 | mock.addon('bar', '1.0.0', [ 33 | mock.addon('baz', '1.0.0', [ 34 | mock.addon('my-addon', '1.2.3'), 35 | ]), 36 | ]), 37 | ]), 38 | ]); 39 | 40 | expect(validateProject(project)).to.deep.equal([ 41 | { 42 | addon: 'my-addon', 43 | valid: true, 44 | specifier: undefined, 45 | dependents: { 46 | '1.2.3': [ 47 | ['my-app'], 48 | ['my-app', 'foo'], 49 | ['my-app', 'foo', 'bar', 'baz'], 50 | ], 51 | }, 52 | }, 53 | { 54 | addon: 'foo', 55 | valid: true, 56 | specifier: undefined, 57 | dependents: { 58 | '1.0.0': [['my-app']], 59 | }, 60 | }, 61 | { 62 | addon: 'bar', 63 | valid: true, 64 | specifier: undefined, 65 | dependents: { 66 | '1.0.0': [['my-app', 'foo']], 67 | }, 68 | }, 69 | { 70 | addon: 'baz', 71 | valid: true, 72 | specifier: undefined, 73 | dependents: { 74 | '1.0.0': [['my-app', 'foo', 'bar']], 75 | }, 76 | }, 77 | ]); 78 | }); 79 | 80 | it('allows prerelease versions with a `*` specifier', () => { 81 | const project = mock.project('my-app', [ 82 | mock.addon('my-addon', '1.2.3'), 83 | mock.addon('foo', '1.0.0', [ 84 | mock.addon('my-addon', '2.0.0-beta.1'), 85 | ]), 86 | ]); 87 | 88 | config.setConfig({ 89 | allowedVersions: { 90 | 'my-addon': '*', 91 | }, 92 | }); 93 | 94 | expect(validateProject(project)).to.deep.equal([ 95 | { 96 | addon: 'my-addon', 97 | valid: true, 98 | specifier: '*', 99 | dependents: { 100 | '1.2.3': [['my-app']], 101 | '2.0.0-beta.1': [['my-app', 'foo']] 102 | } 103 | }, 104 | { 105 | addon: 'foo', 106 | valid: true, 107 | specifier: undefined, 108 | dependents: { 109 | '1.0.0': [['my-app']] 110 | } 111 | } 112 | ]); 113 | }); 114 | 115 | it('fails when multiple versions are included with no specifier', () => { 116 | const project = mock.project('my-app', [ 117 | mock.addon('my-addon', '1.2.4'), 118 | mock.addon('foo', '1.0.0', [ 119 | mock.addon('my-addon', '1.2.3'), 120 | ]), 121 | ]); 122 | 123 | expect(validateProject(project)).to.deep.equal([ 124 | { 125 | addon: 'my-addon', 126 | valid: false, 127 | specifier: undefined, 128 | dependents: { 129 | '1.2.3': [['my-app', 'foo']], 130 | '1.2.4': [['my-app']], 131 | }, 132 | }, 133 | { 134 | addon: 'foo', 135 | valid: true, 136 | specifier: undefined, 137 | dependents: { 138 | '1.0.0': [['my-app']], 139 | }, 140 | }, 141 | ]); 142 | }); 143 | 144 | it('fails when only one version is included that doesn\'t satisfy the specifier', () => { 145 | const project = mock.project('my-app', [ 146 | mock.addon('my-addon', '1.2.3'), 147 | ]); 148 | 149 | config.setConfig({ 150 | allowedVersions: { 151 | 'my-addon': '^1.2.4', 152 | }, 153 | }); 154 | 155 | expect(validateProject(project)).to.deep.equal([{ 156 | addon: 'my-addon', 157 | valid: false, 158 | specifier: '^1.2.4', 159 | dependents: { 160 | '1.2.3': [['my-app']], 161 | }, 162 | }]); 163 | }); 164 | 165 | it('passes when only one version is included that satisfies the specifier', () => { 166 | const project = mock.project('my-app', [ 167 | mock.addon('my-addon', '1.2.3'), 168 | ]); 169 | 170 | config.setConfig({ 171 | allowedVersions: { 172 | 'my-addon': '^1.2.0', 173 | }, 174 | }); 175 | 176 | expect(validateProject(project)).to.deep.equal([{ 177 | addon: 'my-addon', 178 | valid: true, 179 | specifier: '^1.2.0', 180 | dependents: { 181 | '1.2.3': [['my-app']], 182 | }, 183 | }]); 184 | }); 185 | 186 | it('fails when multiple versions are included and one doesn\'t satisfy the specifier', () => { 187 | const project = mock.project('my-app', [ 188 | mock.addon('my-addon', '1.4.2'), 189 | mock.addon('foo', '1.0.0', [ 190 | mock.addon('my-addon', '1.2.3'), 191 | ]), 192 | ]); 193 | 194 | config.setConfig({ 195 | allowedVersions: { 196 | 'my-addon': '^1.4.0', 197 | }, 198 | }); 199 | 200 | expect(validateProject(project)).to.deep.equal([ 201 | { 202 | addon: 'my-addon', 203 | valid: false, 204 | specifier: '^1.4.0', 205 | dependents: { 206 | '1.2.3': [['my-app', 'foo']], 207 | '1.4.2': [['my-app']], 208 | }, 209 | }, 210 | { 211 | addon: 'foo', 212 | valid: true, 213 | specifier: undefined, 214 | dependents: { 215 | '1.0.0': [['my-app']], 216 | }, 217 | }, 218 | ]); 219 | }); 220 | 221 | it('passes when multiple versions are included that satisfy the specifier', () => { 222 | const project = mock.project('my-app', [ 223 | mock.addon('my-addon', '1.4.2'), 224 | mock.addon('foo', '1.0.0', [ 225 | mock.addon('my-addon', '1.4.3'), 226 | ]), 227 | ]); 228 | 229 | config.setConfig({ 230 | allowedVersions: { 231 | 'my-addon': '^1.4.0', 232 | }, 233 | }); 234 | 235 | expect(validateProject(project)).to.deep.equal([ 236 | { 237 | addon: 'my-addon', 238 | valid: true, 239 | specifier: '^1.4.0', 240 | dependents: { 241 | '1.4.3': [['my-app', 'foo']], 242 | '1.4.2': [['my-app']], 243 | }, 244 | }, 245 | { 246 | addon: 'foo', 247 | valid: true, 248 | specifier: undefined, 249 | dependents: { 250 | '1.0.0': [['my-app']], 251 | }, 252 | }, 253 | ]); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | 'embertest': true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/config/dependency-lint.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "jquery-integration": false 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/tests/unit/.gitkeep -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salsify/ember-cli-dependency-lint/ce3d85c02d04901198cfcd99e2864787ff5d92ba/vendor/.gitkeep --------------------------------------------------------------------------------