socket 1.1.105 → 1.1.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/dist/cli.js +433 -25
- package/dist/cli.js.map +1 -1
- package/dist/constants.js +4 -4
- package/dist/constants.js.map +1 -1
- package/dist/socket-facts.init.gradle +92 -16
- package/dist/socket-facts.plugin.scala +416 -0
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/manifest/cmd-manifest-gradle.d.mts.map +1 -1
- package/dist/types/commands/manifest/cmd-manifest-kotlin.d.mts.map +1 -1
- package/dist/types/commands/manifest/cmd-manifest-scala.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +3 -1
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts +9 -0
- package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts.map +1 -0
- package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
- package/dist/types/commands/scan/cmd-scan-create.d.mts.map +1 -1
- package/dist/types/utils/socket-json.d.mts +5 -0
- package/dist/types/utils/socket-json.d.mts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
package socket
|
|
2
|
+
|
|
3
|
+
import sbt._
|
|
4
|
+
import sbt.Keys._
|
|
5
|
+
|
|
6
|
+
import org.apache.ivy.Ivy
|
|
7
|
+
import org.apache.ivy.core.cache.DefaultRepositoryCacheManager
|
|
8
|
+
import org.apache.ivy.core.module.descriptor.{ Artifact, ModuleDescriptor }
|
|
9
|
+
import org.apache.ivy.core.module.id.ModuleRevisionId
|
|
10
|
+
import org.apache.ivy.core.report.ResolveReport
|
|
11
|
+
import org.apache.ivy.core.resolve.{ IvyNode, ResolveOptions }
|
|
12
|
+
|
|
13
|
+
import scala.collection.mutable
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Socket facts plugin for sbt.
|
|
17
|
+
*
|
|
18
|
+
* Emits a single `.socket.facts.json` at the build root describing the
|
|
19
|
+
* resolved dependency graph of every project in the build, in the canonical
|
|
20
|
+
* SocketFacts schema (mirrors socket-facts.init.gradle on the gradle side):
|
|
21
|
+
*
|
|
22
|
+
* { "components": SF_Artifact[] }
|
|
23
|
+
*
|
|
24
|
+
* Each Maven component is
|
|
25
|
+
* { type: 'maven', namespace, name, version?, qualifiers? } &
|
|
26
|
+
* { id, direct?, dev?, dependencies? }
|
|
27
|
+
*
|
|
28
|
+
* The graph is read from Ivy resolution metadata only: `setDownload(false)`
|
|
29
|
+
* means no artifact jars are fetched, just the POM/ivy.xml needed to compute
|
|
30
|
+
* the transitive closure — we never consume bandwidth pulling jars.
|
|
31
|
+
*
|
|
32
|
+
* By default only the project's real dependency configurations are resolved
|
|
33
|
+
* (`compile`, `runtime`, `test`, `provided`, `optional`); the Scala
|
|
34
|
+
* compiler/scaladoc toolchain, sbt-native-packager configs (debian, docker,
|
|
35
|
+
* universal, ...), `-internal` duplicates and the sources/docs/pom artifact
|
|
36
|
+
* configs are skipped. They aren't the project's declared dependencies (the
|
|
37
|
+
* pom-path manifest omits them too) and resolving them dominates cost on large
|
|
38
|
+
* builds. Override the set with `-Dsocket.configs=comma,separated` glob
|
|
39
|
+
* patterns (case-sensitive; `*` = any sequence, `?` = single char). e.g.
|
|
40
|
+
* `compile,test` to keep only those scopes, `*Test*` to add custom test-
|
|
41
|
+
* like configs. One component is emitted per
|
|
42
|
+
* resolved module (org:name:version); a module's alternate artifacts
|
|
43
|
+
* (sources/javadoc classifier jars) are the same package, so they collapse
|
|
44
|
+
* into that single component rather than adding duplicates. `test`-scoped
|
|
45
|
+
* configs are tagged `dev`.
|
|
46
|
+
*
|
|
47
|
+
* Unresolved dependencies are fatal by default (non-zero exit) rather than
|
|
48
|
+
* silently dropped — the env is the user's own, set up to resolve their deps.
|
|
49
|
+
* `-Dsocket.ignoreUnresolved=true` downgrades that to a warning and emits the
|
|
50
|
+
* resolvable deps anyway.
|
|
51
|
+
*
|
|
52
|
+
* Intra-build project dependencies are omitted: sbt's `dependsOn` is a
|
|
53
|
+
* classpath dependency, not an Ivy one, so siblings rarely appear in a
|
|
54
|
+
* project's resolve, and a sibling referenced as an explicit library
|
|
55
|
+
* dependency is filtered out by coordinate. Each project's own external deps
|
|
56
|
+
* are aggregated, and resolution is per-project, so divergent per-module
|
|
57
|
+
* versions are all reported.
|
|
58
|
+
*
|
|
59
|
+
* Delivery: shipped as source and dropped into an isolated `-Dsbt.global.base`
|
|
60
|
+
* plugins dir, so it activates on any project without installation. It is
|
|
61
|
+
* compiled by the sbt meta-build, whose Scala is 2.10 for sbt 0.13 and 2.12
|
|
62
|
+
* for sbt 1.x — so this file must compile on both. Reaching into Ivy (stable
|
|
63
|
+
* across those sbt versions) keeps the code free of the version-specific sbt
|
|
64
|
+
* APIs that would otherwise need reflection, and lets us scope resolution to
|
|
65
|
+
* the configs we want (which sbt's own `update`/`updateFull` can't).
|
|
66
|
+
*/
|
|
67
|
+
object SocketFactsPlugin extends AutoPlugin {
|
|
68
|
+
override def trigger = allRequirements
|
|
69
|
+
|
|
70
|
+
// The configurations resolved by default: the project's real dependency
|
|
71
|
+
// scopes. Override via `-Dsocket.configs`. Everything else (toolchain,
|
|
72
|
+
// packager, `-internal`, sources/docs/pom) is skipped — not declared deps,
|
|
73
|
+
// and costly to resolve.
|
|
74
|
+
private val DefaultConfs =
|
|
75
|
+
Set("compile", "optional", "provided", "runtime", "test")
|
|
76
|
+
|
|
77
|
+
// Must stay in sync with `DOT_SOCKET_DOT_FACTS_JSON` in src/constants.mts
|
|
78
|
+
// (TS side). Scala can't import the TS constant, so the two strings are
|
|
79
|
+
// intentionally duplicated; change them together.
|
|
80
|
+
private val SocketFactsFilename = ".socket.facts.json"
|
|
81
|
+
|
|
82
|
+
object autoImport {
|
|
83
|
+
val socketFacts =
|
|
84
|
+
taskKey[Unit]("Emit a Socket facts JSON for the whole build")
|
|
85
|
+
}
|
|
86
|
+
import autoImport._
|
|
87
|
+
|
|
88
|
+
override def projectSettings: Seq[Setting[_]] = Seq(
|
|
89
|
+
// Run once for the whole build; the task itself gathers every project via
|
|
90
|
+
// ScopeFilter, so we don't want sbt to also fan it out to aggregates.
|
|
91
|
+
// Note: `in` (not the newer `key / scope` slash syntax) is intentional —
|
|
92
|
+
// slash syntax doesn't exist in sbt 0.13, which we still support. The
|
|
93
|
+
// 1.5+ deprecation warning it triggers is harmless and only surfaces on a
|
|
94
|
+
// cold compile. Same goes for `baseDirectory in ThisBuild` below.
|
|
95
|
+
aggregate in socketFacts := false,
|
|
96
|
+
socketFacts := {
|
|
97
|
+
val log = streams.value.log
|
|
98
|
+
val modules = ivyModule.all(ScopeFilter(inAnyProject)).value
|
|
99
|
+
val buildRoot = (baseDirectory in ThisBuild).value
|
|
100
|
+
|
|
101
|
+
// First pass: every project's own coordinate (org:name:version), so
|
|
102
|
+
// intra-build deps are omitted even when referenced as explicit library
|
|
103
|
+
// deps. Keying on the full GAV (not just org:name) means an external dep
|
|
104
|
+
// that merely shares an org:name with a build project is still emitted.
|
|
105
|
+
val projectCoords = mutable.HashSet.empty[String]
|
|
106
|
+
modules.foreach { module =>
|
|
107
|
+
module.withModule(log) { (_, md, _) =>
|
|
108
|
+
projectCoords += gavKey(md.getModuleRevisionId)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
val nodes = mutable.LinkedHashMap.empty[String, Node]
|
|
113
|
+
val unresolved = mutable.LinkedHashSet.empty[String]
|
|
114
|
+
|
|
115
|
+
// Second pass: resolve each project metadata-only and fold its graph in.
|
|
116
|
+
modules.foreach { module =>
|
|
117
|
+
module.withModule(log) { (ivy, md, _) =>
|
|
118
|
+
collectResolved(ivy, md, projectCoords, nodes, unresolved)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (unresolved.nonEmpty) {
|
|
123
|
+
val ignore = boolProp("socket.ignoreUnresolved")
|
|
124
|
+
if (ignore) {
|
|
125
|
+
log.warn(
|
|
126
|
+
"Socket facts: skipping " + unresolved.size +
|
|
127
|
+
" unresolved dependency(ies) (ignore-unresolved):"
|
|
128
|
+
)
|
|
129
|
+
unresolved.toList.sorted.foreach(u => log.warn(" - " + u))
|
|
130
|
+
} else {
|
|
131
|
+
log.error("Socket facts: could not resolve these dependencies:")
|
|
132
|
+
unresolved.toList.sorted.foreach(u => log.error(" - " + u))
|
|
133
|
+
sys.error(
|
|
134
|
+
"Socket facts aborted: " + unresolved.size +
|
|
135
|
+
" unresolved dependency(ies). Pass --ignore-unresolved to skip " +
|
|
136
|
+
"them, or fix resolution (repositories, credentials, offline " +
|
|
137
|
+
"cache) and retry."
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (nodes.isEmpty) {
|
|
143
|
+
// println (not log.info) so the line reaches stdout without sbt's
|
|
144
|
+
// `[info]` prefix, matching what the CLI parses for.
|
|
145
|
+
println("[socket-facts] no resolvable dependencies in build, skipping")
|
|
146
|
+
} else {
|
|
147
|
+
val outDir = sys.props.get("socket.outputDirectory") match {
|
|
148
|
+
case Some(d) if d.nonEmpty => new File(d)
|
|
149
|
+
case _ => buildRoot
|
|
150
|
+
}
|
|
151
|
+
outDir.mkdirs()
|
|
152
|
+
val outName = sys.props.get("socket.outputFile") match {
|
|
153
|
+
case Some(f) if f.nonEmpty => f
|
|
154
|
+
case _ => SocketFactsFilename
|
|
155
|
+
}
|
|
156
|
+
val outFile = new File(outDir, outName)
|
|
157
|
+
IO.write(outFile, renderJson(nodes))
|
|
158
|
+
// println (not log.info) so the line reaches stdout without sbt's
|
|
159
|
+
// `[info]` prefix, matching what the CLI parses for.
|
|
160
|
+
println("Socket facts file written to: " + outFile.getAbsolutePath)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// Build a name-matcher closed over `-Dsocket.configs`. When set, patterns
|
|
166
|
+
// are matched as case-sensitive globs (`*` = any sequence, `?` = single
|
|
167
|
+
// char) so the same flag shape works as on `socket manifest gradle
|
|
168
|
+
// --facts`. With no wildcards a pattern is just an exact-name match,
|
|
169
|
+
// which preserves the prior comma-separated-names semantics. When unset
|
|
170
|
+
// we fall back to exact membership in DefaultConfs.
|
|
171
|
+
private def buildConfigMatcher(): String => Boolean =
|
|
172
|
+
sys.props.get("socket.configs") match {
|
|
173
|
+
case Some(s) if s.trim.nonEmpty =>
|
|
174
|
+
val patterns = s
|
|
175
|
+
.split(",")
|
|
176
|
+
.map(_.trim)
|
|
177
|
+
.filter(_.nonEmpty)
|
|
178
|
+
.map(globToRegex)
|
|
179
|
+
.toList
|
|
180
|
+
if (patterns.isEmpty) { (name: String) =>
|
|
181
|
+
DefaultConfs.contains(name)
|
|
182
|
+
} else { (name: String) =>
|
|
183
|
+
patterns.exists(_.matcher(name).matches())
|
|
184
|
+
}
|
|
185
|
+
case _ => (name: String) => DefaultConfs.contains(name)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private def globToRegex(glob: String): java.util.regex.Pattern = {
|
|
189
|
+
val sb = new StringBuilder
|
|
190
|
+
glob.foreach {
|
|
191
|
+
case '*' => sb.append(".*")
|
|
192
|
+
case '?' => sb.append('.')
|
|
193
|
+
case c if "\\.^$|+()[]{}".indexOf(c.toInt) >= 0 =>
|
|
194
|
+
sb.append('\\').append(c)
|
|
195
|
+
case c => sb.append(c)
|
|
196
|
+
}
|
|
197
|
+
java.util.regex.Pattern.compile(sb.toString)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private def boolProp(name: String): Boolean =
|
|
201
|
+
java.lang.Boolean.parseBoolean(sys.props.getOrElse(name, "false"))
|
|
202
|
+
|
|
203
|
+
// Resolve one project's module metadata-only and fold its graph into the
|
|
204
|
+
// shared, build-wide node map. Takes the stable Ivy types (not sbt's
|
|
205
|
+
// IvySbt#Module, which moved packages between 0.13 and 1.x).
|
|
206
|
+
private def collectResolved(
|
|
207
|
+
ivy: Ivy,
|
|
208
|
+
md: ModuleDescriptor,
|
|
209
|
+
projectCoords: scala.collection.Set[String],
|
|
210
|
+
nodes: mutable.LinkedHashMap[String, Node],
|
|
211
|
+
unresolved: mutable.LinkedHashSet[String]
|
|
212
|
+
): Unit = {
|
|
213
|
+
val rootMrid = md.getModuleRevisionId
|
|
214
|
+
val matcher = buildConfigMatcher()
|
|
215
|
+
val confs = md.getConfigurationsNames.filter(matcher)
|
|
216
|
+
if (confs.nonEmpty) {
|
|
217
|
+
// Don't revalidate cached metadata over the network: with release
|
|
218
|
+
// coordinates the cached POM/ivy.xml never changes, so HEAD/GET-ing each
|
|
219
|
+
// cached module per resolve is pure overhead (~30% of warm-cache time).
|
|
220
|
+
// Missing metadata is still fetched (this is not cache-only), so cold
|
|
221
|
+
// caches still work — we just never re-check what we already have.
|
|
222
|
+
ivy.getSettings.getDefaultRepositoryCacheManager match {
|
|
223
|
+
case drcm: DefaultRepositoryCacheManager =>
|
|
224
|
+
drcm.setCheckmodified(false)
|
|
225
|
+
drcm.setUseOrigin(true)
|
|
226
|
+
drcm.setDefaultTTL(Long.MaxValue)
|
|
227
|
+
case _ =>
|
|
228
|
+
}
|
|
229
|
+
val options = new ResolveOptions()
|
|
230
|
+
options.setDownload(false)
|
|
231
|
+
options.setTransitive(true)
|
|
232
|
+
options.setConfs(confs)
|
|
233
|
+
// Skip Ivy's report rendering — it re-walks the graph and we don't use it.
|
|
234
|
+
options.setOutputReport(false)
|
|
235
|
+
val report: ResolveReport = ivy.resolve(md, options)
|
|
236
|
+
|
|
237
|
+
report.getUnresolvedDependencies.foreach { node =>
|
|
238
|
+
unresolved += node.getId.toString
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Pass 1: emit one node per resolved module, and remember which
|
|
242
|
+
// component id each Ivy module maps to (for wiring caller edges).
|
|
243
|
+
val mridToId = mutable.HashMap.empty[String, String]
|
|
244
|
+
val pass1 = report.getDependencies.iterator()
|
|
245
|
+
while (pass1.hasNext) {
|
|
246
|
+
val ivyNode = pass1.next().asInstanceOf[IvyNode]
|
|
247
|
+
if (isEmittable(ivyNode, projectCoords)) {
|
|
248
|
+
val mrid = ivyNode.getResolvedId
|
|
249
|
+
// prod = reached by any non-test config; a dep reached only via test
|
|
250
|
+
// configs is dev.
|
|
251
|
+
val prod =
|
|
252
|
+
ivyNode.getRootModuleConfigurations.exists(c => !isTestConf(c))
|
|
253
|
+
val coord = coordFor(ivyNode, mrid)
|
|
254
|
+
val node = nodes.getOrElseUpdate(coord.id, new Node(coord))
|
|
255
|
+
if (prod) {
|
|
256
|
+
node.prod = true
|
|
257
|
+
}
|
|
258
|
+
mridToId(mrid.toString) = coord.id
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Pass 2: wire caller edges. A caller that is the project root marks the
|
|
263
|
+
// node `direct`; any other caller becomes its parent.
|
|
264
|
+
val pass2 = report.getDependencies.iterator()
|
|
265
|
+
while (pass2.hasNext) {
|
|
266
|
+
val ivyNode = pass2.next().asInstanceOf[IvyNode]
|
|
267
|
+
if (isEmittable(ivyNode, projectCoords)) {
|
|
268
|
+
mridToId.get(ivyNode.getResolvedId.toString).foreach { childId =>
|
|
269
|
+
ivyNode.getAllCallers.foreach { caller =>
|
|
270
|
+
val callerMrid = caller.getModuleRevisionId
|
|
271
|
+
if (callerMrid == rootMrid) {
|
|
272
|
+
nodes(childId).direct = true
|
|
273
|
+
} else {
|
|
274
|
+
mridToId
|
|
275
|
+
.get(callerMrid.toString)
|
|
276
|
+
.foreach(parentId => nodes(parentId).children += childId)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// A configuration whose name mentions "test" (test, test-internal,
|
|
286
|
+
// IntegrationTest, ...) contributes dev dependencies. Name-based, mirroring
|
|
287
|
+
// the gradle script, so it also catches custom test-like configs.
|
|
288
|
+
private def isTestConf(name: String): Boolean =
|
|
289
|
+
name.toLowerCase.contains("test")
|
|
290
|
+
|
|
291
|
+
// A dependency node is emittable when it actually resolved to a real module
|
|
292
|
+
// that isn't a project in this build and isn't a conflict loser. Failed or
|
|
293
|
+
// unloaded nodes are skipped here (reading their metadata throws); they're
|
|
294
|
+
// reported separately via the resolve report's unresolved list.
|
|
295
|
+
private def isEmittable(
|
|
296
|
+
node: IvyNode,
|
|
297
|
+
projectCoords: scala.collection.Set[String]
|
|
298
|
+
): Boolean = {
|
|
299
|
+
val mrid = node.getResolvedId
|
|
300
|
+
mrid != null &&
|
|
301
|
+
node.getModuleRevision != null &&
|
|
302
|
+
!node.hasProblem &&
|
|
303
|
+
!projectCoords.contains(gavKey(mrid)) &&
|
|
304
|
+
!node.isCompletelyEvicted
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Build-wide coordinate key (org:name:version) identifying a project in this
|
|
308
|
+
// build, so its own modules are omitted when they surface in another
|
|
309
|
+
// project's resolve. Full GAV (not just org:name) so a same-named external
|
|
310
|
+
// dependency at a different version is still emitted.
|
|
311
|
+
private def gavKey(mrid: ModuleRevisionId): String =
|
|
312
|
+
mrid.getOrganisation + ":" + mrid.getName + ":" + mrid.getRevision
|
|
313
|
+
|
|
314
|
+
private def coordFor(node: IvyNode, mrid: ModuleRevisionId): Coord =
|
|
315
|
+
Coord(mrid.getOrganisation, mrid.getName, mrid.getRevision, primaryExt(node))
|
|
316
|
+
|
|
317
|
+
// The packaging extension of the module's main (classifier-less) artifact.
|
|
318
|
+
// Reading artifact metadata never triggers a download. Defaults to jar,
|
|
319
|
+
// which is correct for the overwhelming majority of Maven dependencies.
|
|
320
|
+
private def primaryExt(node: IvyNode): String = {
|
|
321
|
+
val artifacts = node.getAllArtifacts
|
|
322
|
+
if (artifacts == null) {
|
|
323
|
+
"jar"
|
|
324
|
+
} else {
|
|
325
|
+
artifacts.find(a => classifierOf(a).isEmpty).map(extOf).getOrElse("jar")
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private def extOf(a: Artifact): String = {
|
|
330
|
+
val e = a.getExt
|
|
331
|
+
if (e == null || e.isEmpty) "jar" else e
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private def classifierOf(a: Artifact): String = {
|
|
335
|
+
val extra = a.getExtraAttributes
|
|
336
|
+
val raw =
|
|
337
|
+
if (extra.get("classifier") != null) extra.get("classifier")
|
|
338
|
+
else extra.get("m:classifier")
|
|
339
|
+
if (raw == null) "" else raw.toString
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private def renderJson(nodes: mutable.LinkedHashMap[String, Node]): String = {
|
|
343
|
+
val sorted = nodes.values.toList.sortBy(_.coord.id)
|
|
344
|
+
val sb = new StringBuilder
|
|
345
|
+
sb.append("{\n \"components\": [\n")
|
|
346
|
+
sorted.zipWithIndex.foreach {
|
|
347
|
+
case (node, idx) =>
|
|
348
|
+
appendComponent(sb, node)
|
|
349
|
+
if (idx < sorted.size - 1) {
|
|
350
|
+
sb.append(",")
|
|
351
|
+
}
|
|
352
|
+
sb.append("\n")
|
|
353
|
+
}
|
|
354
|
+
sb.append(" ]\n}\n")
|
|
355
|
+
sb.toString
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private def appendComponent(sb: StringBuilder, node: Node): Unit = {
|
|
359
|
+
val c = node.coord
|
|
360
|
+
val fields = mutable.ListBuffer.empty[String]
|
|
361
|
+
fields += "\"type\": \"maven\""
|
|
362
|
+
fields += "\"namespace\": " + jsonString(c.org)
|
|
363
|
+
fields += "\"name\": " + jsonString(c.name)
|
|
364
|
+
if (c.version.nonEmpty) {
|
|
365
|
+
fields += "\"version\": " + jsonString(c.version)
|
|
366
|
+
}
|
|
367
|
+
if (c.ext.nonEmpty) {
|
|
368
|
+
fields += "\"qualifiers\": { \"ext\": " + jsonString(c.ext) + " }"
|
|
369
|
+
}
|
|
370
|
+
fields += "\"id\": " + jsonString(c.id)
|
|
371
|
+
if (node.direct) {
|
|
372
|
+
fields += "\"direct\": true"
|
|
373
|
+
}
|
|
374
|
+
if (!node.prod) {
|
|
375
|
+
fields += "\"dev\": true"
|
|
376
|
+
}
|
|
377
|
+
if (node.children.nonEmpty) {
|
|
378
|
+
val depLines = node.children.toList.map(d => " " + jsonString(d))
|
|
379
|
+
fields += "\"dependencies\": [\n" + depLines.mkString(",\n") + "\n ]"
|
|
380
|
+
}
|
|
381
|
+
sb.append(" {\n ")
|
|
382
|
+
sb.append(fields.mkString(",\n "))
|
|
383
|
+
sb.append("\n }")
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private def jsonString(s: String): String = {
|
|
387
|
+
val sb = new StringBuilder("\"")
|
|
388
|
+
s.foreach {
|
|
389
|
+
case '"' => sb.append("\\\"")
|
|
390
|
+
case '\\' => sb.append("\\\\")
|
|
391
|
+
case '\n' => sb.append("\\n")
|
|
392
|
+
case '\r' => sb.append("\\r")
|
|
393
|
+
case '\t' => sb.append("\\t")
|
|
394
|
+
case ch if ch < 0x20 => sb.append("\\u%04x".format(ch.toInt))
|
|
395
|
+
case ch => sb.append(ch)
|
|
396
|
+
}
|
|
397
|
+
sb.append("\"")
|
|
398
|
+
sb.toString
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// A resolved Maven coordinate.
|
|
402
|
+
private final case class Coord(
|
|
403
|
+
org: String,
|
|
404
|
+
name: String,
|
|
405
|
+
version: String,
|
|
406
|
+
ext: String
|
|
407
|
+
) {
|
|
408
|
+
val id: String = org + ":" + name + ":" + version
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private final class Node(val coord: Coord) {
|
|
412
|
+
val children = mutable.TreeSet.empty[String]
|
|
413
|
+
var prod = false
|
|
414
|
+
var direct = false
|
|
415
|
+
}
|
|
416
|
+
}
|