licensed 3.2.3 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -1
  3. data/Rakefile +5 -3
  4. data/docs/adding_a_new_source.md +32 -3
  5. data/docs/commands/README.md +1 -1
  6. data/docs/commands/status.md +14 -10
  7. data/docs/sources/cargo.md +19 -0
  8. data/docs/sources/yarn.md +5 -4
  9. data/lib/licensed/reporters/status_reporter.rb +1 -1
  10. data/lib/licensed/sources/cargo.rb +70 -0
  11. data/lib/licensed/sources/manifest.rb +17 -22
  12. data/lib/licensed/sources/npm.rb +17 -7
  13. data/lib/licensed/sources/nuget.rb +2 -2
  14. data/lib/licensed/sources/source.rb +22 -3
  15. data/lib/licensed/sources/yarn/berry.rb +85 -0
  16. data/lib/licensed/sources/yarn/v1.rb +152 -0
  17. data/lib/licensed/sources/yarn.rb +15 -130
  18. data/lib/licensed/sources.rb +1 -0
  19. data/lib/licensed/version.rb +1 -1
  20. data/licensed.gemspec +1 -1
  21. metadata +6 -32
  22. data/.github/dependabot.yml +0 -19
  23. data/.github/workflows/release.yml +0 -213
  24. data/.github/workflows/test.yml +0 -543
  25. data/.gitignore +0 -57
  26. data/.licensed.yml +0 -7
  27. data/.rubocop.yml +0 -8
  28. data/.ruby-version +0 -1
  29. data/docker/Dockerfile.build-linux +0 -15
  30. data/script/bootstrap +0 -6
  31. data/script/cibuild +0 -7
  32. data/script/console +0 -15
  33. data/script/package +0 -20
  34. data/script/packages/build +0 -95
  35. data/script/packages/linux +0 -57
  36. data/script/packages/mac +0 -41
  37. data/script/setup +0 -5
  38. data/script/source-setup/bower +0 -17
  39. data/script/source-setup/bundler +0 -20
  40. data/script/source-setup/cabal +0 -19
  41. data/script/source-setup/composer +0 -38
  42. data/script/source-setup/git_submodule +0 -39
  43. data/script/source-setup/go +0 -31
  44. data/script/source-setup/mix +0 -19
  45. data/script/source-setup/npm +0 -34
  46. data/script/source-setup/nuget +0 -17
  47. data/script/source-setup/pip +0 -29
  48. data/script/source-setup/pipenv +0 -21
  49. data/script/source-setup/swift +0 -22
  50. data/script/source-setup/yarn +0 -17
  51. data/script/test +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e2043fe7541ca6458302eab4e81fabdc22d874d5e80498eaac0f1551d7796e8
4
- data.tar.gz: abe1b03af0e02be363661d357e82cac6b53a127a6fd01cfef2c7ba2b6c174116
3
+ metadata.gz: 1a241c3ec016e1b2f49cc7a4ed53c53ee07a45fb5dc5f1b6655e6c4e5acf2d6d
4
+ data.tar.gz: 26e55577302098d09128c87d422856307841fa85dd95c181a7fe9280713ee644
5
5
  SHA512:
6
- metadata.gz: 8555b427c46ab7e0198cf4ac71ed02fae65a230576057bd6d2cbf38e5d26491479444cfc4ed6ec78549e615c5b8cf6d71ce762b31552bf7bfd1d348e228b1055
7
- data.tar.gz: 30da66cc1abb37677768dab09d79f93c17df25a7d0a73e06dbfdcb51ce7bb3ea66af5962e97631a019a8119498f4b0ebdeaca46667cb8b2b3d3fe0a2bb63c254
6
+ metadata.gz: 4358bc3c0f238d569beb172ded8589088336a64ccac81f55f2f669e7619c59c5590fdda1b88c5d3812cc8f554af2381ec1f74f40798634a547a6e8884d33c10e
7
+ data.tar.gz: 751818fb0934e5cf80629971267373117d1649d6ec65f8ae35477f53153307a4fee7893d9182f9b112fe622d8554656ca4892717084793b31577cb1b86557fad
data/CHANGELOG.md CHANGED
@@ -6,6 +6,56 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## 3.4.1
10
+
11
+ 2022-01-07
12
+
13
+ ### Fixed
14
+
15
+ - Malformed package.json files will no longer crash yarn dependency detection (https://github.com/github/licensed/pull/431)
16
+
17
+ ## 3.4.0
18
+
19
+ 2021-12-14
20
+
21
+ ### Added
22
+
23
+ - New Yarn enumerator with support for berry versions (https://github.com/github/licensed/pull/423)
24
+
25
+ ### Fixed
26
+
27
+ - Error handling cases return correct values in the Yarn enumerator (https://github.com/github/licensed/pull/425)
28
+ - Fixed link in command documentation (:tada: @chibicco https://github.com/github/licensed/pull/416)
29
+ - Fixed minor backwards compatibility issue for Ruby 2.3 support (:tada: @dzunk https://github.com/github/licensed/pull/414)
30
+
31
+ ### Changed
32
+
33
+ - Licensed's own dependencies are cached in the repository and kept up to date with GitHub Actions (https://github.com/github/licensed/pull/421)
34
+
35
+ ## 3.3.1
36
+
37
+ 2021-10-07
38
+
39
+ ### Fixed
40
+
41
+ - Fix evaluation of peer dependencies with npm 7 (:tada: @manuelpuyol https://github.com/github/licensed/pull/411)
42
+
43
+ ### Changed
44
+
45
+ - Manifest source evaluation performance improvements (https://github.com/github/licensed/pull/407)
46
+
47
+ ## 3.3.0
48
+
49
+ 2021-09-18
50
+
51
+ ### Added
52
+
53
+ - New cargo source enumerates rust dependencies (https://github.com/github/licensed/pull/404)
54
+
55
+ ### Changed
56
+
57
+ - Removed non-functional files from gem builds (https://github.com/github/licensed/pull/405)
58
+
9
59
  ## 3.2.3
10
60
 
11
61
  2021-09-14
@@ -497,4 +547,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
497
547
 
498
548
  Initial release :tada:
499
549
 
500
- [Unreleased]: https://github.com/github/licensed/compare/3.2.3...HEAD
550
+ [Unreleased]: https://github.com/github/licensed/compare/3.4.1...HEAD
data/Rakefile CHANGED
@@ -2,13 +2,16 @@
2
2
  require "bundler/gem_tasks"
3
3
  require "rake/testtask"
4
4
  require "rubocop/rake_task"
5
+ require "licensed"
5
6
 
6
7
  desc "Run source setup scripts"
7
8
  task :setup, [:arguments] do |task, args|
8
9
  arguments = args[:arguments].to_s.split
9
10
  force = arguments.include?("-f") ? "-f" : ""
10
11
 
11
- Dir["script/source-setup/*"].each do |script|
12
+ Dir["script/source-setup/**/*"].each do |script|
13
+ next if File.directory?(script)
14
+
12
15
  # green
13
16
  puts "\033[32mRunning #{script}.\e[0m"
14
17
 
@@ -27,8 +30,7 @@ task :setup, [:arguments] do |task, args|
27
30
  end
28
31
  end
29
32
 
30
- sources_search = File.expand_path("lib/licensed/sources/*.rb", __dir__)
31
- sources = Dir[sources_search].map { |f| File.basename(f, ".*") }
33
+ sources = Licensed::Sources::Source.sources.map { |source| source.full_type }
32
34
 
33
35
  namespace :test do
34
36
  sources.each do |source|
@@ -13,8 +13,37 @@ Dependency enumerators inherit and override the [`Licensed::Sources::Source`](..
13
13
 
14
14
  ### Optional method overrides
15
15
 
16
- 1. `Licensed::Sources::Source.type`
17
- - Returns the name of the current dependency enumerator as it is found in a licensed configuration file.
16
+ 1. `Licensed::Sources::Source.type_and_version`
17
+ - Returns the name, and optionally a version, of the current dependency enumerator as it is found in a licensed configuration file. See [the method description](../lib/licensed/sources/source.rb#L38-L41) for more details
18
+
19
+ ### Implementing an enumerator for a new version of an existing source
20
+
21
+ If a package manager introduces breaking changes, it can be easier to build a new implementation rather than making a single class work for all cases. To enable seamless migration between source versions, the implementation for each version of the source enumerator should return the same `.type` and determine whether the version implementation should run in `#enabled?`.
22
+
23
+ The sections below describe what was done when adding a new version for the `yarn` source. Following these steps will make sure that the new version implementation follows the expected patterns for local development and test scenarios.
24
+
25
+ #### Migrating the file structure for a single source enumerator to enable multiple source enumerator versions
26
+
27
+ The following steps will migrate the source to the pattern expected for multi-version source enumerators.
28
+
29
+ The enumerators source code file is likely named to closely match the source enumerator, e.g. `lib/licensed/sources/yarn.rb`
30
+
31
+ 1. Create a new directory matching the name of the source and move the existing enumerator into the new folder with a version descriptive name, e.g. `lib/licensed/sources/yarn/v1.rb`
32
+ 1. Update the source enumerator class name to include a version identifier, e.g. `Licensed::Sources::Yarn::V1`
33
+ 1. Make similar changes for the source's [unit test fixtures](../test/fixtures), [unit test file](../test/sources) and [setup script](../scripts/source-setup), moving these files into subfolders and renaming the files to match the change in (1)
34
+ - Also be sure to update any references to old paths or class names
35
+ 1. If needed, update the source's `#type_and_version` to include a version value as a second array value
36
+ - If this isn't already set, the default implementation will return the type and version as the last two part names of the class name, snake cased and with a `/` delimeter, e.g. `yarn/v1`
37
+ 1. Update the source's `#enabled?` method, adding a version check to ensure that the source only runs in the expected scenario
38
+ 1. Add a new generic source file in `lib/licensed/sources` that `require`s the new file, e.g. `lib/licensed/sources/yarn.rb`
39
+ - This is also an ideal spot to put shared code in a module that can be included in one or more versions of the source enumerator
40
+ 1. Update any references to the source in scripting and GitHub Actions automation to use the new versioned identifier, e.g. `yarn/v1` instead of the unversioned identifier.
41
+
42
+ #### Adding a new implementation for the new version of the source
43
+
44
+ 1. Add the new implementation to the source's `lib/licensed/sources` subfolder.
45
+ 1. If there is shared code that can be reused between multiple source enumerator versions, put it in a module in the source's base file, e.g. `lib/licensed/sources/yarn.rb`. Include the module in the version implementations.
46
+ 1. Ensure that the new version implementation checks for the expected source enumerator version in `#enabled?`
18
47
 
19
48
  ## Determining if dependencies should be enumerated
20
49
 
@@ -24,7 +53,7 @@ whether `Licensed::Source::Sources#enumerate_dependencies` should be called on t
24
53
  Determining whether dependencies should be enumerated depends on whether all the tools or files needed to find dependencies are present.
25
54
  For example, to enumerate `npm` dependencies the `npm` CLI tool must be found with `Licensed::Shell.tool_available?` and a `package.json` file needs to exist in the licensed app's configured [`source_path`](./configuration.md#configuration-paths).
26
55
 
27
- ### Gating functionality when required tools are not available.
56
+ ### Gating functionality when required tools are not available
28
57
 
29
58
  When adding new dependency sources, ensure that `script/bootstrap` scripting and tests are only run if the required tooling is available on the development machine.
30
59
 
@@ -8,7 +8,7 @@ Run `licensed -h` to see help content for running licensed commands.
8
8
  - [migrate](migrate.md)
9
9
  - [notices](notices.md)
10
10
  - [status](status.md)
11
- - [version](verison.md)
11
+ - [version](version.md)
12
12
 
13
13
  Most commands accept a `-c`/`--config` option to specify a path to a configuration file or directory. If a directory is specified, `licensed` will look in that directory for a file named (in order of preference):
14
14
 
@@ -39,30 +39,34 @@ The following data is reported for each dependency when the YAML or JSON report
39
39
 
40
40
  ### cached dependency record not found
41
41
 
42
- **Cause:** A dependency was found while running `licensed status` that does not have a corresponding cached metadata file
43
- **Resolution:** Run `licensed cache` to update the metadata cache and create the missing metadata file
42
+ *Cause:* A dependency was found while running `licensed status` that does not have a corresponding cached metadata file
43
+
44
+ *Resolution:* Run `licensed cache` to update the metadata cache and create the missing metadata file
44
45
 
45
46
  ### cached dependency record out of date
46
47
 
47
- **Cause:** A dependency was found while running `licensed status` with a different version than is contained in the dependency's cached metadata file
48
- **Resolution:** Run `licensed cache` to update the out-of-date metadata files
48
+ *Cause:* A dependency was found while running `licensed status` with a different version than is contained in the dependency's cached metadata file
49
+
50
+ *Resolution:* Run `licensed cache` to update the out-of-date metadata files
49
51
 
50
52
  ### missing license text
51
53
 
52
- **Cause:** A license determination was made, e.g. from package metadata, but no license text was found.
53
- **Resolution:** Manually verify whether the dependency includes a file containing license text. If the dependency code that was downloaded locally does not contain the license text, please check the dependency source at the version listed in the dependency's cached metadata file to see if there is license text that can be used.
54
+ *Cause:* A license determination was made, e.g. from package metadata, but no license text was found.
55
+
56
+ *Resolution:* Manually verify whether the dependency includes a file containing license text. If the dependency code that was downloaded locally does not contain the license text, please check the dependency source at the version listed in the dependency's cached metadata file to see if there is license text that can be used.
54
57
 
55
58
  If the dependency does not include license text but does specify that it uses a specific license, please copy the standard license text from a [well known source](https://opensource.org/licenses).
56
59
 
57
60
  ### license text has changed and needs re-review. if the new text is ok, remove the `review_changed_license` flag from the cached record
58
61
 
59
- **Cause:** A dependency that is set as [reviewed] in the licensed configuration file has substantially changed and should be re-reviewed.
60
- **Resolution:** Review the changes to the license text and classification, along with other metadata contained in the cached file for the dependency. If the dependency is still allowable for use in your project, remove the `review_changed_license` key from the cached record file.
62
+ *Cause:* A dependency that is set as [reviewed] in the licensed configuration file has substantially changed and should be re-reviewed.
63
+
64
+ *Resolution:* Review the changes to the license text and classification, along with other metadata contained in the cached file for the dependency. If the dependency is still allowable for use in your project, remove the `review_changed_license` key from the cached record file.
61
65
 
62
66
  ### license needs review
63
67
 
64
- **Cause:** A dependency is using a license that is not in the configured [allowed list of licenses][allowed], and the dependency has not been marked [ignored] or [reviewed].
65
- **Resolution:** Review the dependency's usage and specified license with someone familiar with OSS licensing and compliance rules to determine whether the dependency is allowable. Some common resolutions:
68
+ *Cause:* A dependency is using a license that is not in the configured [allowed list of licenses][allowed], and the dependency has not been marked [ignored] or [reviewed].
69
+ *Resolution:* Review the dependency's usage and specified license with someone familiar with OSS licensing and compliance rules to determine whether the dependency is allowable. Some common resolutions:
66
70
 
67
71
  1. The dependency's specified license text differed enough from the standard license text that it was not recognized and classified as `other`. If, with human review, the license text is recognizable then update the `license: other` value in the cached metadata file to the correct license.
68
72
  - An updated classification will persist through version upgrades until the detected license contents have changed. The determination is made by [licensee/licensee](https://github.com/licensee/licensee), the library which this tool uses to detect and classify license contents.
@@ -0,0 +1,19 @@
1
+ # Cargo
2
+
3
+ The cargo source will detect dependencies when `Cargo.toml` is found at an apps `source_path`. The source uses the `cargo metadata` CLI and reports on all dependencies that are listed in the output in `resolve.nodes`, excluding packages that are listed in `workspace_members`.
4
+
5
+ ## Metadata CLI options
6
+
7
+ Licensed by default runs `cargo metadata --format-version=1`. You can specify additional CLI options by specifying them in your licensed configuration file under `cargo.metadata_options`. The configuration can be set as a string, or as an array of strings for multiple options.
8
+
9
+ ```yml
10
+ cargo:
11
+ metadata_options: '--all-features'
12
+ ```
13
+
14
+ ```yml
15
+ cargo:
16
+ metadata_options:
17
+ - '--all-features'
18
+ - '--filter-platform x86_64-pc-windows-msvc'
19
+ ```
data/docs/sources/yarn.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  The yarn source will detect dependencies when `package.json` and `yarn.lock` are found at an app's `source_path`.
4
4
 
5
- It uses `yarn list` to enumerate dependencies and `yarn info` to get metadata on each package.
5
+ It uses the `yarn` CLI commands to enumerate dependencies and gather metadata on each package.
6
6
 
7
- ### Including development dependencies
7
+ ## Including development dependencies
8
8
 
9
- Yarn versions < 1.3.0 will always include non-production dependencies due to a bug in those yarn versions.
9
+ **Note** Yarn versions < 1.3.0 will always include non-production dependencies due to a bug in those versions of yarn.
10
+ **Note** Yarn versions > 2.0 will always include non-production dependencies due to lack of filtering of production vs non-production dependencies in the yarn CLI.
10
11
 
11
- Starting with yarn version >= 1.3.0, the yarn source excludes non-production dependencies by default. To include development and test dependencies, set `production_only: false` in `.licensed.yml`.
12
+ For yarn versions between 1.3.0 and 2.0, the yarn source excludes non-production dependencies by default. To include development and test dependencies in these versions, set `production_only: false` in `.licensed.yml`.
12
13
 
13
14
  ```yml
14
15
  yarn:
@@ -49,7 +49,7 @@ module Licensed
49
49
  errored_reports = all_reports.select { |r| r.errors.any? }.to_a
50
50
 
51
51
  dependency_count = all_reports.count { |r| r.target.is_a?(Licensed::Dependency) }
52
- error_count = errored_reports.sum { |r| r.errors.size }
52
+ error_count = errored_reports.reduce(0) { |count, r| count + r.errors.size }
53
53
 
54
54
  if error_count > 0
55
55
  shell.newline
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Licensed
6
+ module Sources
7
+ class Cargo < Source
8
+ # Source is enabled when the cargo tool and Cargo.toml manifest file are available
9
+ def enabled?
10
+ return false unless Licensed::Shell.tool_available?("cargo")
11
+ config.pwd.join("Cargo.toml").exist?
12
+ end
13
+
14
+ def enumerate_dependencies
15
+ packages.map do |package|
16
+ Dependency.new(
17
+ name: "#{package["name"]}-#{package["version"]}",
18
+ version: package["version"],
19
+ path: File.dirname(package["manifest_path"]),
20
+ metadata: {
21
+ "name" => package["name"],
22
+ "type" => Cargo.type,
23
+ "summary" => package["description"],
24
+ "homepage" => package["homepage"]
25
+ }
26
+ )
27
+ end
28
+ end
29
+
30
+ # Returns the package data for all dependencies used to build the current package
31
+ def packages
32
+ cargo_metadata_resolved_node_ids.map { |id| cargo_metadata_packages[id] }
33
+ end
34
+
35
+ # Returns the ids of all resolved nodes used to build the current package
36
+ def cargo_metadata_resolved_node_ids
37
+ cargo_metadata.dig("resolve", "nodes")
38
+ .map { |node| node["id"] }
39
+ .reject { |id| cargo_metadata_workspace_members.include?(id) }
40
+
41
+ end
42
+
43
+ # Returns a hash of id => package pairs sourced from the "packages" cargo metadata property
44
+ def cargo_metadata_packages
45
+ @cargo_metadata_packages ||= cargo_metadata["packages"].each_with_object({}) do |package, hsh|
46
+ hsh[package["id"]] = package
47
+ end
48
+ end
49
+
50
+ # Returns a set of the ids of packages in the current workspace
51
+ def cargo_metadata_workspace_members
52
+ @cargo_metadata_workspace_members ||= Set.new(Array(cargo_metadata["workspace_members"]))
53
+ end
54
+
55
+ # Returns parsed JSON metadata returned from the cargo CLI
56
+ def cargo_metadata
57
+ @cargo_metadata ||= JSON.parse(cargo_metadata_command)
58
+ rescue JSON::ParserError => e
59
+ message = "Licensed was unable to parse the output from 'cargo metadata'. JSON Error: #{e.message}"
60
+ raise Licensed::Sources::Source::Error, message
61
+ end
62
+
63
+ # Runs a command to get cargo metadata for the current package
64
+ def cargo_metadata_command
65
+ options = Array(config.dig("cargo", "metadata_options")).flat_map(&:split)
66
+ Licensed::Shell.execute("cargo", "metadata", "--format-version=1", *options)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -61,7 +61,7 @@ module Licensed
61
61
  manifest.each_with_object({}) do |(src, package_name), hsh|
62
62
  next if src.nil? || src.empty?
63
63
  hsh[package_name] ||= []
64
- hsh[package_name] << File.join(config.root, src)
64
+ hsh[package_name] << File.absolute_path(src, config.root)
65
65
  end
66
66
  end
67
67
 
@@ -130,19 +130,17 @@ module Licensed
130
130
  @configured_dependencies ||= begin
131
131
  dependencies = config.dig("manifest", "dependencies")&.dup || {}
132
132
 
133
- dependencies.each do |name, patterns|
133
+ dependencies.each_with_object({}) do |(name, patterns), hsh|
134
134
  # map glob pattern(s) listed for the dependency to a listing
135
135
  # of files that match the patterns and are not excluded
136
- dependencies[name] = files_from_pattern_list(patterns) & included_files
136
+ hsh[name] = files_from_pattern_list(patterns) & included_files
137
137
  end
138
-
139
- dependencies
140
138
  end
141
139
  end
142
140
 
143
141
  # Returns the set of project files that are included in dependency evaluation
144
142
  def included_files
145
- @sources ||= all_files - files_from_pattern_list(config.dig("manifest", "exclude"))
143
+ @included_files ||= tracked_files - files_from_pattern_list(config.dig("manifest", "exclude"))
146
144
  end
147
145
 
148
146
  # Finds and returns all files in the project that match
@@ -151,26 +149,23 @@ module Licensed
151
149
  return Set.new if patterns.nil? || patterns.empty?
152
150
 
153
151
  # evaluate all patterns from the project root
154
- Dir.chdir config.root do
155
- Array(patterns).reduce(Set.new) do |files, pattern|
156
- if pattern.start_with?("!")
157
- # if the pattern is an exclusion, remove all matching files
158
- # from the result
159
- files - Dir.glob(pattern[1..-1], File::FNM_DOTMATCH)
160
- else
161
- # if the pattern is an inclusion, add all matching files
162
- # to the result
163
- files + Dir.glob(pattern, File::FNM_DOTMATCH)
164
- end
152
+ Array(patterns).each_with_object(Set.new) do |pattern, files|
153
+ if pattern.start_with?("!")
154
+ # if the pattern is an exclusion, remove all matching files
155
+ # from the result
156
+ files.subtract(Dir.glob(pattern[1..-1], File::FNM_DOTMATCH, base: config.root))
157
+ else
158
+ # if the pattern is an inclusion, add all matching files
159
+ # to the result
160
+ files.merge(Dir.glob(pattern, File::FNM_DOTMATCH, base: config.root))
165
161
  end
166
162
  end
167
163
  end
168
164
 
169
- # Returns all tracked files in the project
170
- def all_files
171
- # remove files if they are tracked but don't exist on the file system
172
- @all_files ||= Set.new(Licensed::Git.files || [])
173
- .delete_if { |f| !File.exist?(File.join(Licensed::Git.repository_root, f)) }
165
+ # Returns all tracked files in the project as the intersection of what git tracks and the files in the project
166
+ def tracked_files
167
+ @tracked_files ||= Set.new(Array(Licensed::Git.files)) &
168
+ Set.new(Dir.glob("**/*", File::FNM_DOTMATCH, base: config.root))
174
169
  end
175
170
 
176
171
  class Dependency < Licensed::Dependency
@@ -23,10 +23,6 @@ module Licensed
23
23
  end
24
24
  end
25
25
 
26
- def self.type
27
- "npm"
28
- end
29
-
30
26
  def enabled?
31
27
  Licensed::Shell.tool_available?("npm") && File.exist?(config.pwd.join("package.json"))
32
28
  end
@@ -66,15 +62,17 @@ module Licensed
66
62
 
67
63
  # Recursively parse dependency JSON data. Returns a hash mapping the
68
64
  # package name to it's metadata
69
- def recursive_dependencies(dependencies, result = {})
65
+ def recursive_dependencies(dependencies, result = {}, parent = nil)
70
66
  dependencies.each do |name, dependency|
71
- next if dependency["peerMissing"]
67
+ next if missing_peer?(parent, dependency, name)
72
68
  next if yarn_lock_present && dependency["missing"]
73
69
  next if dependency["extraneous"] && dependency["missing"]
74
70
 
75
71
  dependency["name"] = name
72
+ dependency["version"] ||= extract_version(parent, name) if dependency["missing"]
73
+
76
74
  (result[name] ||= []) << dependency
77
- recursive_dependencies(dependency["dependencies"] || {}, result)
75
+ recursive_dependencies(dependency["dependencies"] || {}, result, dependency)
78
76
  end
79
77
  result
80
78
  end
@@ -135,6 +133,18 @@ module Licensed
135
133
  def include_non_production?
136
134
  config.dig("npm", "production_only") == false
137
135
  end
136
+
137
+ def missing_peer?(parent, dependency, name)
138
+ dependency["peerMissing"] || (dependency["missing"] && peer_dependency(parent, name))
139
+ end
140
+
141
+ def peer_dependency(parent, name)
142
+ parent&.dig("peerDependencies", name)
143
+ end
144
+
145
+ def extract_version(parent, name)
146
+ parent&.dig("_dependencies", name) || peer_dependency(parent, name)
147
+ end
138
148
  end
139
149
  end
140
150
  end
@@ -7,8 +7,8 @@ module Licensed
7
7
  # Only supports ProjectReference (project.assets.json) style restore used in .NET Core.
8
8
  # Does not currently support packages.config style restore.
9
9
  class NuGet < Source
10
- def self.type
11
- "nuget"
10
+ def self.type_and_version
11
+ ["nuget"]
12
12
  end
13
13
 
14
14
  class NuGetDependency < Licensed::Dependency
@@ -19,13 +19,32 @@ module Licensed
19
19
  (@sources ||= []) << klass
20
20
  end
21
21
 
22
- # Returns the source name as the snake cased class name
22
+ # Returns the source name as the first snake cased class or module name
23
+ # following "Licensed::Sources::". This is the type that is included
24
+ # in metadata files and cache paths.
25
+ # e.g. for `Licensed::Sources::Yarn::V1`, this returns "yarn"
23
26
  def type
24
- self.name.split(/::/)
25
- .last
27
+ type_and_version[0]
28
+ end
29
+
30
+ # Returns the source name as a "/" delimited string of all the module and
31
+ # class names following "Licensed::Sources::". This is the type that is
32
+ # used to distinguish multiple versions of a sources from each other.
33
+ # e.g. for `Licensed::Sources::Yarn::V1`, this returns `yarn/v1`
34
+ def full_type
35
+ type_and_version.join("/")
36
+ end
37
+
38
+ # Returns an array that includes the source's type name at the first index, and
39
+ # optionally a version string for the source as the second index.
40
+ # Callers should override this function and not `type` or `full_type` when
41
+ # needing to adjust the default type and version parsing logic
42
+ def type_and_version
43
+ self.name.gsub("#{Licensed::Sources.name}::", "")
26
44
  .gsub(/([A-Z\d]+)([A-Z][a-z])/, "\\1_\\2".freeze)
27
45
  .gsub(/([a-z\d])([A-Z])/, "\\1_\\2".freeze)
28
46
  .downcase
47
+ .split("::")
29
48
  end
30
49
  end
31
50
 
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+
4
+ module Licensed
5
+ module Sources
6
+ class Yarn::Berry < Source
7
+ include Licensed::Sources::Yarn
8
+
9
+ def self.version_requirement
10
+ Gem::Requirement.new(">= 2.0")
11
+ end
12
+
13
+ def enumerate_dependencies
14
+ packages.map do |name, package|
15
+ Dependency.new(
16
+ name: name,
17
+ version: package["version"],
18
+ path: package["path"],
19
+ metadata: {
20
+ "type" => self.class.type,
21
+ "name" => package["name"],
22
+ "homepage" => package["homepage"]
23
+ }
24
+ )
25
+ end
26
+ end
27
+
28
+ # Finds packages that the current project relies on based on the output from `yarn info`
29
+ def packages
30
+ # parse all lines of output to json and find one that is "type": "tree"
31
+ yarn_info = JSON.parse("[#{yarn_info_command.lines.join(",")}]")
32
+ mapped_packages = yarn_info.reduce({}) do |accum, package|
33
+ name, _ = package["value"].rpartition("@")
34
+ version = package.dig("children", "Version")
35
+ id = "#{name}-#{version}"
36
+
37
+ accum[name] ||= []
38
+ accum[name] << {
39
+ "id" => id,
40
+ "name" => name,
41
+ "version" => version,
42
+ "homepage" => package.dig("children", "Manifest", "Homepage"),
43
+ "path" => dependency_paths[id]
44
+ }
45
+ accum
46
+ end
47
+
48
+ mapped_packages.each_with_object({}) do |(name, results), hsh|
49
+ results.uniq! { |package| package["version"] }
50
+ if results.size == 1
51
+ # if there is only one package for a name, reference it by name
52
+ hsh[name] = results[0]
53
+ else
54
+ # if there is more than one package for a name, reference each by id
55
+ results.each do |package|
56
+ hsh[package["id"]] = package
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # Returns a hash that maps all dependency names to their location on disk
63
+ # by parsing every package.json file under node_modules.
64
+ def dependency_paths
65
+ @dependency_paths ||= Dir.glob(config.pwd.join("node_modules/**/package.json")).each_with_object({}) do |file, hsh|
66
+ begin
67
+ dirname = File.dirname(file)
68
+ json = JSON.parse(File.read(file))
69
+ hsh["#{json["name"]}-#{json["version"]}"] = dirname
70
+ rescue JSON::ParserError
71
+ # don't crash execution if there is a problem parsing a package.json file
72
+ # if the bad package.json file relates to a package that licensed should be reporting on
73
+ # then this will still result in an error about a missing package
74
+ end
75
+ end
76
+ end
77
+
78
+ # Returns the output from running `yarn list` to get project dependencies
79
+ def yarn_info_command
80
+ args = %w(--json --manifest --recursive --all)
81
+ Licensed::Shell.execute("yarn", "info", *args)
82
+ end
83
+ end
84
+ end
85
+ end