licensed 3.2.2 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -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/sources/cargo.md +19 -0
  7. data/docs/sources/yarn.md +5 -4
  8. data/lib/licensed/commands/cache.rb +4 -2
  9. data/lib/licensed/commands/command.rb +5 -2
  10. data/lib/licensed/reporters/status_reporter.rb +1 -1
  11. data/lib/licensed/sources/bundler/missing_specification.rb +10 -7
  12. data/lib/licensed/sources/cargo.rb +70 -0
  13. data/lib/licensed/sources/manifest.rb +17 -22
  14. data/lib/licensed/sources/npm.rb +17 -7
  15. data/lib/licensed/sources/nuget.rb +2 -2
  16. data/lib/licensed/sources/source.rb +22 -3
  17. data/lib/licensed/sources/yarn/berry.rb +79 -0
  18. data/lib/licensed/sources/yarn/v1.rb +146 -0
  19. data/lib/licensed/sources/yarn.rb +15 -130
  20. data/lib/licensed/sources.rb +1 -0
  21. data/lib/licensed/version.rb +1 -1
  22. data/licensed.gemspec +1 -1
  23. metadata +6 -32
  24. data/.github/dependabot.yml +0 -19
  25. data/.github/workflows/release.yml +0 -213
  26. data/.github/workflows/test.yml +0 -528
  27. data/.gitignore +0 -57
  28. data/.licensed.yml +0 -7
  29. data/.rubocop.yml +0 -8
  30. data/.ruby-version +0 -1
  31. data/docker/Dockerfile.build-linux +0 -15
  32. data/script/bootstrap +0 -6
  33. data/script/cibuild +0 -7
  34. data/script/console +0 -15
  35. data/script/package +0 -20
  36. data/script/packages/build +0 -95
  37. data/script/packages/linux +0 -57
  38. data/script/packages/mac +0 -41
  39. data/script/setup +0 -5
  40. data/script/source-setup/bower +0 -17
  41. data/script/source-setup/bundler +0 -20
  42. data/script/source-setup/cabal +0 -19
  43. data/script/source-setup/composer +0 -38
  44. data/script/source-setup/git_submodule +0 -39
  45. data/script/source-setup/go +0 -31
  46. data/script/source-setup/mix +0 -19
  47. data/script/source-setup/npm +0 -34
  48. data/script/source-setup/nuget +0 -17
  49. data/script/source-setup/pip +0 -29
  50. data/script/source-setup/pipenv +0 -21
  51. data/script/source-setup/swift +0 -22
  52. data/script/source-setup/yarn +0 -17
  53. data/script/test +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ed29bbc7c7c643bf2eda9aacca1d64bd5ccb6e9211e4eeda006c33ec1fe3900
4
- data.tar.gz: eeefafd3638220b419d57353947ab74a17bc5504fca50562a2d63baf0562e485
3
+ metadata.gz: a3cb5bf8c20bf1177466536fbc30bc29135a9e76933e225af8353ba8eb89205d
4
+ data.tar.gz: 26acda6f4b9d90c2457725ecda8f0b00bc7fc793a4993a1e6aa9a6810a995d04
5
5
  SHA512:
6
- metadata.gz: 9d54389571ce81aadee0c3a46871f38c8c87768d86abd8c1ef50978378d23d4a17524c4188de8f306f4156a2b0ae670a5e437279aa458113b89e311919296332
7
- data.tar.gz: 3a93838012493a51fd430815d42fa7b3b52ba85ea2513a879517a437e00af05d47f7325cfdeac2440adf299f05af0b0fbf2b0a16bb4b34caa81db4becc632808
6
+ metadata.gz: 5d71df74f6fca5c309230b18a7423f608cdf61ca0a5ddf9f45116b75681196a15ecee70d4d56877841b82c1d75d43fa573182ec300f1976a1e815f169de1598f
7
+ data.tar.gz: 645c4bf3cbb162e46061b730f947f0299336506fa425640f215e0e96ccb89124ef0e8a9725e6b9044fcd1c8e38c8809b7f5a911479d6583cc097b15d1f4a48c3
data/CHANGELOG.md CHANGED
@@ -6,6 +6,57 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## 3.4.0
10
+
11
+ 2021-12-14
12
+
13
+ ### Added
14
+
15
+ - New Yarn enumerator with support for berry versions (https://github.com/github/licensed/pull/423)
16
+
17
+ ### Fixed
18
+
19
+ - Error handling cases return correct values in the Yarn enumerator (https://github.com/github/licensed/pull/425)
20
+ - Fixed link in command documentation (:tada: @chibicco https://github.com/github/licensed/pull/416)
21
+ - Fixed minor backwards compatibility issue for Ruby 2.3 support (:tada: @dzunk https://github.com/github/licensed/pull/414)
22
+
23
+ ### Changed
24
+
25
+ - Licensed's own dependencies are cached in the repository and kept up to date with GitHub Actions (https://github.com/github/licensed/pull/421)
26
+
27
+ ## 3.3.1
28
+
29
+ 2021-10-07
30
+
31
+ ### Fixed
32
+
33
+ - Fix evaluation of peer dependencies with npm 7 (:tada: @manuelpuyol https://github.com/github/licensed/pull/411)
34
+
35
+ ### Changed
36
+
37
+ - Manifest source evaluation performance improvements (https://github.com/github/licensed/pull/407)
38
+
39
+ ## 3.3.0
40
+
41
+ 2021-09-18
42
+
43
+ ### Added
44
+
45
+ - New cargo source enumerates rust dependencies (https://github.com/github/licensed/pull/404)
46
+
47
+ ### Changed
48
+
49
+ - Removed non-functional files from gem builds (https://github.com/github/licensed/pull/405)
50
+
51
+ ## 3.2.3
52
+
53
+ 2021-09-14
54
+
55
+ ### Fixed
56
+
57
+ - Bundler source will no longer infinitely recurse when enumerating specifications (https://github.com/github/licensed/pull/402)
58
+ - Using the `--sources` command line option will no longer delete skipped sources' cached files (https://github.com/github/licensed/pull/401)
59
+
9
60
  ## 3.2.2
10
61
 
11
62
  2021-09-09
@@ -488,4 +539,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
488
539
 
489
540
  Initial release :tada:
490
541
 
491
- [Unreleased]: https://github.com/github/licensed/compare/3.2.2...HEAD
542
+ [Unreleased]: https://github.com/github/licensed/compare/3.4.0...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
 
@@ -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:
@@ -39,11 +39,13 @@ module Licensed
39
39
  #
40
40
  # Returns whether the command succeeded for the dependency source enumerator
41
41
  def run_source(app, source, report)
42
+ result = super
43
+
42
44
  # add the full cache path to the list of cache paths
43
45
  # that should be cleaned up after the command run
44
- cache_paths << app.cache_path.join(source.class.type)
46
+ cache_paths << app.cache_path.join(source.class.type) unless result == :skipped
45
47
 
46
- super
48
+ result
47
49
  end
48
50
 
49
51
  # Cache dependency record data.
@@ -121,13 +121,16 @@ module Licensed
121
121
  # source - A dependency source enumerator
122
122
  # report - A report object for this source
123
123
  #
124
- # Returns whether the command succeeded for the dependency source enumerator
124
+ # Returns whether the command succeeded, failed, or was skipped for the dependency source enumerator
125
125
  def run_source(app, source, report)
126
126
  reporter.begin_report_source(source, report)
127
127
 
128
128
  if !sources_overrides.empty? && !sources_overrides.include?(source.class.type)
129
129
  report.warnings << "skipped source"
130
- return true
130
+
131
+ # return a symbol to speficy the source was skipped.
132
+ # This is truthy and will result in the source being considered successful
133
+ return :skipped
131
134
  end
132
135
 
133
136
  dependencies = source.dependencies.sort_by { |dependency| dependency.name }
@@ -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
@@ -38,17 +38,20 @@ module Licensed
38
38
  "could not find #{name} (#{version}) in any sources"
39
39
  end
40
40
  end
41
+
42
+ module LazySpecification
43
+ def __materialize__
44
+ spec = super
45
+ return spec if spec
46
+
47
+ Licensed::Bundler::MissingSpecification.new(name: name, version: version, platform: platform, source: source)
48
+ end
49
+ end
41
50
  end
42
51
  end
43
52
 
44
53
  module Bundler
45
54
  class LazySpecification
46
- alias_method :orig_materialize, :__materialize__
47
- def __materialize__
48
- spec = orig_materialize
49
- return spec if spec
50
-
51
- Licensed::Bundler::MissingSpecification.new(name: name, version: version, platform: platform, source: source)
52
- end
55
+ prepend ::Licensed::Bundler::LazySpecification
53
56
  end
54
57
  end
@@ -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,79 @@
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
+ dirname = File.dirname(file)
67
+ json = JSON.parse(File.read(file))
68
+ hsh["#{json["name"]}-#{json["version"]}"] = dirname
69
+ end
70
+ end
71
+
72
+ # Returns the output from running `yarn list` to get project dependencies
73
+ def yarn_info_command
74
+ args = %w(--json --manifest --recursive --all)
75
+ Licensed::Shell.execute("yarn", "info", *args)
76
+ end
77
+ end
78
+ end
79
+ end