#370 #377 #378 Builtin as default import and minor changes (#384)

This commit is contained in:
Dima 2021-12-03 20:30:00 +03:00 committed by GitHub
parent e22867caa4
commit 3e762d6654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 240 additions and 106 deletions

View File

@ -22,12 +22,6 @@ jobs:
### Setup
- uses: olafurpg/setup-scala@v10
### Update & build
- name: Assembly
run: sbt cli/assembly
env:
BUILD_NUMBER: ${{ github.run_number }}
- name: JS build
run: sbt cliJS/fullLinkJS
env:
@ -55,12 +49,6 @@ jobs:
env:
BUILD_NUMBER: ${{ github.run_number }}
- name: Check .jar exists
run: |
JAR="cli/.jvm/target/scala-3.0.2/aqua-${{ env.VERSION }}.jar"
stat "$JAR"
echo "JAR=$JAR" >> $GITHUB_ENV
- name: Check .js exists
run: |
JS="cli/.js/target/scala-3.0.2/cli-opt/aqua-${{ env.VERSION }}.js"
@ -74,7 +62,6 @@ jobs:
node-version: "15"
registry-url: "https://registry.npmjs.org"
- run: cp ${{ env.JAR }} ./npm/aqua.jar
- run: cp ${{ env.JS }} ./npm/aqua.js
- run: npm version ${{ env.VERSION }}
@ -118,7 +105,6 @@ jobs:
draft: false
prerelease: false
files: |
${{ env.JAR }}
${{ env.JS }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,8 +1,13 @@
package aqua
import aqua.js.{AvmLogLevel, FluenceJSLogLevel, Meta, Module}
import fs2.io.file.Path
import scribe.Level
object Utils {
import scala.util.Try
object LogLevelTransformer {
def logLevelToAvm(logLevel: Level): AvmLogLevel = {
logLevel match {
case Level.Trace => "trace"

View File

@ -1,18 +1,39 @@
package aqua
import aqua.js.{Meta, Module}
import cats.effect.ExitCode
import cats.effect.kernel.Async
import com.monovore.decline.Opts
import fs2.io.file.Files
import scala.concurrent.ExecutionContext
import fs2.io.file.{Files, Path}
import scala.concurrent.ExecutionContext
import aqua.run.RunOpts
import aqua.keypair.KeyPairOpts
import scribe.Logging
import scala.util.Try
// JS-specific options and subcommands
object PlatformOpts {
def opts[F[_]: Files: AquaIO: Async](implicit ec: ExecutionContext): Opts[F[ExitCode]] =
Opts.subcommand(RunOpts.runCommand[F]) orElse
Opts.subcommand(KeyPairOpts.createKeypair[F])
}
object PlatformOpts extends Logging {
def opts[F[_]: Files: AquaIO: Async](implicit ec: ExecutionContext): Opts[F[ExitCode]] =
Opts.subcommand(RunOpts.runCommand[F]) orElse
Opts.subcommand(KeyPairOpts.createKeypair[F])
// get path to node modules if there is `aqua-lib` module with `builtin.aqua` in it
def getGlobalNodeModulePath: Option[Path] = {
val meta = Meta.metaUrl
val req = Module.createRequire(meta)
Try {
// this can throw an error
val pathStr = req.resolve("@fluencelabs/aqua-lib/builtin.aqua").toString
// hack
Path(pathStr).parent.map(_.resolve("../.."))
}.getOrElse {
// we don't care about path if there is no builtins, but must write an error
logger.error("Unexpected. Cannot find 'aqua-lib' dependency with `builtin.aqua` in it")
None
}
}
}

View File

@ -1,4 +1,4 @@
package aqua
package aqua.js
import aqua.backend.{ArgDefinition, FunctionDef, NamesConfig, TypeDefinition}
import aqua.model.transform.TransformConfig

View File

@ -1,5 +1,6 @@
package aqua
package aqua.js
import aqua.*
import aqua.backend.{ArgDefinition, FunctionDef, NamesConfig, TypeDefinition}
import scala.concurrent.Promise

View File

@ -0,0 +1,25 @@
package aqua.js
import scala.scalajs.js
import scala.scalajs.js.annotation.{JSExportAll, JSImport}
object Meta {
// get `import`.meta.url info from javascript
// it is needed for `createRequire` function
@js.native
@JSImport("./utils.js", "metaUrl")
val metaUrl: String = js.native
}
@js.native
@JSImport("module", JSImport.Namespace)
object Module extends js.Object {
// make it possible to use `require` in ES module type
def createRequire(str: String): Require = js.native
}
trait Require extends js.Object {
def resolve(str: String): Any
}

View File

@ -1,18 +1,18 @@
package aqua.keypair
import cats.effect.ExitCode
import cats.effect.kernel.{Async}
import cats.Monad
import cats.implicits.catsSyntaxApplicativeId
import cats.Applicative
import aqua.io.OutputPrinter
import aqua.js.KeyPair
import aqua.keypair.KeyPairShow.show
import cats.Applicative.ops.toAllApplicativeOps
import cats.syntax.show._
import cats.effect.ExitCode
import cats.effect.kernel.Async
import cats.implicits.catsSyntaxApplicativeId
import cats.syntax.show.*
import cats.{Applicative, Monad}
import com.monovore.decline.{Command, Opts}
import scribe.Logging
import scala.concurrent.{ExecutionContext, Future}
import aqua.KeyPair
import KeyPairShow.show
import scala.concurrent.{ExecutionContext, Future}
// Options and commands to work with KeyPairs
object KeyPairOpts extends Logging {
@ -33,7 +33,7 @@ object KeyPairOpts extends Logging {
KeyPair.randomEd25519().toFuture.pure[F]
)
.map(keypair =>
println(s"${keypair.show}")
OutputPrinter.print(s"${keypair.show}")
ExitCode.Success
)
)

View File

@ -1,19 +1,19 @@
package aqua.keypair
import scala.scalajs.js.JSON
import scala.scalajs.js
import java.util.Base64
import aqua.KeyPair
import aqua.js.KeyPair
import cats.Show
import java.util.Base64
import scala.scalajs.js
import scala.scalajs.js.JSON
object KeyPairShow {
def stringify(keypair: KeyPair): String = {
val encoder = Base64.getEncoder()
val kp = js.Dynamic.literal(
peerId = keypair.Libp2pPeerId.toB58String(),
secretKey = encoder.encodeToString(keypair.toEd25519PrivateKey().toArray.map(s => s.toByte)),
publicKey = encoder.encodeToString(keypair.Libp2pPeerId.pubKey.bytes.toArray.map(s => s.toByte)),
peerId = keypair.Libp2pPeerId.toB58String(),
secretKey = encoder.encodeToString(keypair.toEd25519PrivateKey().toArray.map(s => s.toByte)),
publicKey = encoder.encodeToString(keypair.Libp2pPeerId.pubKey.bytes.toArray.map(s => s.toByte)),
)
JSON.stringify(kp, space = 4)

View File

@ -7,7 +7,8 @@ import aqua.backend.ts.TypeScriptBackend
import aqua.backend.{FunctionDef, Generated}
import aqua.compiler.{AquaCompiled, AquaCompiler}
import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId}
import aqua.io.AquaFileError
import aqua.io.{AquaFileError, OutputPrinter}
import aqua.js.*
import aqua.model.func.raw.{CallArrowTag, CallServiceTag, FuncOp, FuncOps}
import aqua.model.func.{Call, FuncCallable}
import aqua.model.transform.res.{AquaRes, FuncRes}
@ -34,6 +35,7 @@ import scribe.Logging
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.scalajs.js
import scala.scalajs.js.Dynamic.global as g
import scala.scalajs.js.JSConverters.*
import scala.scalajs.js.JSON
import scala.scalajs.js.annotation.*
@ -61,7 +63,7 @@ object RunCommand extends Logging {
)(implicit
ec: ExecutionContext
): F[Unit] = {
FluenceUtils.setLogLevel(Utils.logLevelToFluenceJS(config.logLevel))
FluenceUtils.setLogLevel(LogLevelTransformer.logLevelToFluenceJS(config.logLevel))
// stops peer in any way at the end of execution
val resource = Resource.make(Fluence.getPeer().pure[F]) { peer =>
@ -74,10 +76,10 @@ object RunCommand extends Logging {
secretKey <- keyPairOrNull(config.secretKey)
_ <- Fluence
.start(
PeerConfig(multiaddr, config.timeout, Utils.logLevelToAvm(config.logLevel), secretKey)
PeerConfig(multiaddr, config.timeout, LogLevelTransformer.logLevelToAvm(config.logLevel), secretKey)
)
.toFuture
_ = println("Your peerId: " + peer.getStatus().peerId)
_ = OutputPrinter.print("Your peerId: " + peer.getStatus().peerId)
promise = Promise.apply[Unit]()
_ = CallJsFunction.registerUnitService(
peer,
@ -88,7 +90,7 @@ object RunCommand extends Logging {
// if an input function returns a result, our success will be after it is printed
// otherwise finish after JS SDK will finish sending a request
// TODO use custom function for output
println(str)
OutputPrinter.print(str)
promise.success(())
}
)
@ -112,7 +114,6 @@ object RunCommand extends Logging {
// func wrapFunc():
// res <- funcCallable(args:_*)
// Console.print(res)
// TODO: now it supports only one result. If funcCallable will return multiple results, only first will be printed
private def wrapCall(
funcName: String,
funcCallable: FuncCallable,
@ -157,7 +158,7 @@ object RunCommand extends Logging {
"Function execution failed by timeout. You can increase timeout with '--timeout' option in milliseconds or check if your code can hang while executing."
} else t.getMessage
// TODO use custom function for error output
println(message)
OutputPrinter.error(message)
}
/**
@ -178,9 +179,9 @@ object RunCommand extends Logging {
)(implicit ec: ExecutionContext): F[Unit] = {
implicit val aio: AquaIO[IO] = new AquaFilesIO[IO]
val sources = new AquaFileSources[F](input, imports)
for {
prelude <- Prelude.init()
sources = new AquaFileSources[F](input, prelude.importPaths)
// compile only context to wrap and call function later
compileResult <- Clock[F].timed(
AquaCompiler
@ -204,7 +205,7 @@ object RunCommand extends Logging {
val air = FuncAirGen(funcRes).generate.show
if (runConfig.printAir) {
println(air)
OutputPrinter.print(air)
}
funcCall[F](multiaddr, air, definitions, runConfig).map { _ =>
@ -223,7 +224,7 @@ object RunCommand extends Logging {
logger.debug(s"Call time: ${callTime.toMillis}ms")
result.fold(
{ (errs: NonEmptyChain[String]) =>
errs.toChain.toList.foreach(err => println(err + "\n"))
errs.toChain.toList.foreach(err => OutputPrinter.error(err + "\n"))
},
identity
)

View File

@ -13,7 +13,7 @@ import cats.syntax.applicative.*
import cats.syntax.apply.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.{Id, Monad, ~>}
import cats.{~>, Id, Monad}
import com.monovore.decline.{Command, Opts}
import fs2.io.file.Files
import scribe.Logging
@ -24,9 +24,10 @@ import scala.concurrent.ExecutionContext
object RunOpts extends Logging {
val timeoutOpt: Opts[Int] =
Opts.option[Int]("timeout", "Request timeout in milliseconds", "t")
Opts
.option[Int]("timeout", "Request timeout in milliseconds", "t")
.withDefault(7000)
val multiaddrOpt: Opts[String] =
Opts
.option[String]("addr", "Relay multiaddress", "a")
@ -57,7 +58,6 @@ object RunOpts extends Logging {
.map(_ => true)
.withDefault(false)
val funcOpt: Opts[(String, List[LiteralModel])] =
Opts
.option[String]("func", "Function to call with args", "f")
@ -70,7 +70,9 @@ object RunOpts extends Logging {
case _ => false
}
if (hasVars) {
Validated.invalidNel("Function can have only literal arguments, no variables or constants allowed at the moment")
Validated.invalidNel(
"Function can have only literal arguments, no variables or constants allowed at the moment"
)
} else {
val args = expr.args.collect { case l @ Literal(_, _) =>
LiteralModel(l.value, l.ts)
@ -85,9 +87,26 @@ object RunOpts extends Logging {
def runOptions[F[_]: Files: AquaIO: Async](implicit
ec: ExecutionContext
): Opts[F[cats.effect.ExitCode]] =
(AppOpts.inputOpts[F], AppOpts.importOpts[F], multiaddrOpt, funcOpt, timeoutOpt, AppOpts.logLevelOpt, printAir, AppOpts.wrapWithOption(secretKeyOpt)).mapN {
case (inputF, importF, multiaddr, (func, args), timeout, logLevel, printAir, secretKey) =>
(
AppOpts.inputOpts[F],
AppOpts.importOpts[F],
multiaddrOpt,
funcOpt,
timeoutOpt,
AppOpts.logLevelOpt,
printAir,
AppOpts.wrapWithOption(secretKeyOpt)
).mapN {
case (
inputF,
importF,
multiaddr,
(func, args),
timeout,
logLevel,
printAir,
secretKey
) =>
scribe.Logger.root
.clearHandlers()
.clearModifiers()
@ -114,7 +133,14 @@ object RunOpts extends Logging {
},
{ imps =>
RunCommand
.run(multiaddr, func, args, input, imps, RunConfig(timeout, logLevel, printAir, secretKey))
.run(
multiaddr,
func,
args,
input,
imps,
RunConfig(timeout, logLevel, printAir, secretKey)
)
.map(_ => cats.effect.ExitCode.Success)
}
)

View File

@ -2,8 +2,10 @@ package aqua
import cats.effect.ExitCode
import com.monovore.decline.Opts
import fs2.io.file.Path
// Scala-specific options and subcommands
object PlatformOpts {
def opts[F[_]]: Opts[F[ExitCode]] = Opts.never
def opts[F[_]]: Opts[F[ExitCode]] = Opts.never
def getGlobalNodeModulePath: Option[Path] = None
}

View File

@ -14,7 +14,7 @@ import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.syntax.traverse.*
import cats.{Comonad, Functor, Monad, ~>}
import cats.{~>, Comonad, Functor, Monad}
import com.monovore.decline.Opts.help
import com.monovore.decline.{Opts, Visibility}
import fs2.io.file.{Files, Path}
@ -92,14 +92,19 @@ object AppOpts {
.map(s => checkPath[F](s))
def outputOpts[F[_]: Monad: Files]: Opts[F[ValidatedNec[String, Option[Path]]]] =
Opts.option[String]("output", "Path to the output directory. Will be created if not exists", "o")
Opts
.option[String]("output", "Path to the output directory. Will be created if not exists", "o")
.map(s => Option(s))
.withDefault(None)
.map(_.map(checkOutput[F]).getOrElse(Validated.validNec[String, Option[Path]](None).pure[F]))
def importOpts[F[_]: Monad: Files]: Opts[F[ValidatedNec[String, List[Path]]]] =
Opts
.options[String]("import", "Path to the directory to import from. May be used several times", "m")
.options[String](
"import",
"Path to the directory to import from. May be used several times",
"m"
)
.orEmpty
.map { ps =>
val checked: List[F[ValidatedNec[String, Path]]] = ps.map { pStr =>
@ -116,21 +121,7 @@ object AppOpts {
}
}
// check if node_modules directory exists and add it in imports list
val nodeModules = Path("node_modules")
val nodeImportF: F[Option[Path]] = Files[F].exists(nodeModules).flatMap {
case true =>
Files[F].isDirectory(nodeModules).map(isDir => if (isDir) Some(nodeModules) else None )
case false => None.pure[F]
}
for {
result <- checked.sequence.map(_.sequence)
nodeImport <- nodeImportF
} yield {
result.map(_ ++ nodeImport)
}
checked.sequence.map(_.sequence)
}
def constantOpts[F[_]: LiftParser: Comonad]: Opts[List[TransformConfig.Const]] =
@ -193,7 +184,10 @@ object AppOpts {
val scriptOpt: Opts[Boolean] =
Opts
.flag("scheduled", "Generate air code for script storage. Without error handling wrappers and hops on relay. Will ignore other options")
.flag(
"scheduled",
"Generate air code for script storage. Without error handling wrappers and hops on relay. Will ignore other options"
)
.map(_ => true)
.withDefault(false)

View File

@ -66,7 +66,7 @@ object AquaCli extends IOApp with Logging {
constantOpts[Id],
dryOpt,
scriptOpt
).mapN {
).mapN {
case (inputF, importsF, outputF, toAirOp, toJs, noRelayOp, noXorOp, h, v, logLevel, constants, isDryRun, isScheduled) =>
scribe.Logger.root
.clearHandlers()

View File

@ -13,8 +13,9 @@ import cats.data.*
import cats.parse.LocationMap
import cats.syntax.applicative.*
import cats.syntax.functor.*
import cats.syntax.flatMap.*
import cats.syntax.show.*
import cats.{Applicative, Eval, Monad, Show, ~>}
import cats.{~>, Applicative, Eval, Monad, Show}
import fs2.io.file.{Files, Path}
import scribe.Logging
@ -36,17 +37,22 @@ object AquaPathCompiler extends Logging {
transformConfig: TransformConfig
): F[ValidatedNec[String, Chain[String]]] = {
import ErrorRendering.showError
val sources = new AquaFileSources[F](srcPath, imports)
AquaCompiler
.compileTo[F, AquaFileError, FileModuleId, FileSpan.F, String](
sources,
SpanParser.parser,
backend,
transformConfig,
targetPath.map(sources.write).getOrElse(dry[F])
)
(for {
prelude <- Prelude.init(withPrelude = false)
sources = new AquaFileSources[F](srcPath, imports ++ prelude.importPaths)
compiler <- AquaCompiler
.compileTo[F, AquaFileError, FileModuleId, FileSpan.F, String](
sources,
SpanParser.parser,
backend,
transformConfig,
targetPath.map(sources.write).getOrElse(dry[F])
)
} yield {
compiler
// 'distinct' to delete all duplicated errors
.map(_.leftMap(_.map(_.show).distinct))
}).map(_.leftMap(_.map(_.show).distinct))
}
def dry[F[_]: Applicative](

View File

@ -0,0 +1,39 @@
package aqua
import cats.Monad
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import fs2.io.file.{Files, Path}
import scribe.Logging
import scala.util.Try
/**
* @param importPaths list of paths where imports will be searched
*/
case class Prelude(importPaths: List[Path])
// JS-specific functions
object Prelude extends Logging {
def init[F[_]: Files: Monad](withPrelude: Boolean = true): F[Prelude] = {
// check if node_modules directory exists and add it in imports list
val nodeModules = Path("node_modules")
val nodeImportF: F[Option[Path]] = Files[F].exists(nodeModules).flatMap {
case true =>
Files[F].isDirectory(nodeModules).map(isDir => if (isDir) Some(nodeModules) else None)
case false => None.pure[F]
}
nodeImportF.map { nodeImport =>
val imports =
if (withPrelude)
nodeImport.toList ++ PlatformOpts.getGlobalNodeModulePath.toList
else nodeImport.toList
new Prelude(imports)
}
}
}

View File

@ -0,0 +1,14 @@
package aqua.io
// Uses to print outputs in CLI
// TODO: add F[_], cause it is effect
object OutputPrinter {
def print(str: String): Unit = {
println(str)
}
def error(str: String): Unit = {
println(str)
}
}

25
npm/package-lock.json generated
View File

@ -9,7 +9,8 @@
"version": "0.0.0",
"license": "Apache-2.0",
"dependencies": {
"@fluencelabs/fluence": "0.15.1"
"@fluencelabs/aqua-lib": "0.2.1",
"@fluencelabs/fluence": "0.15.2"
},
"bin": {
"aqua": "index.js",
@ -75,6 +76,11 @@
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="
},
"node_modules/@fluencelabs/aqua-lib": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@fluencelabs/aqua-lib/-/aqua-lib-0.2.1.tgz",
"integrity": "sha512-uLP9mbgFHR1Q1FYhehasNxNBlTclBsjNI9MvIPF8oXtVJtnvPi+R4rGGTOHtRJukunxhpAV/svWQU9a2BRyDmQ=="
},
"node_modules/@fluencelabs/avm": {
"version": "0.16.0-restriction-operator.9",
"resolved": "https://registry.npmjs.org/@fluencelabs/avm/-/avm-0.16.0-restriction-operator.9.tgz",
@ -84,9 +90,9 @@
}
},
"node_modules/@fluencelabs/fluence": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@fluencelabs/fluence/-/fluence-0.15.1.tgz",
"integrity": "sha512-ZHLw85XgVMglCVJjGkdGRFzL7kO2x31BCYDt4BVlMCE/S2nFSsVHU8DO35Jlh40QZhQdN3F5dbJpkgdcwdC8bw==",
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/@fluencelabs/fluence/-/fluence-0.15.2.tgz",
"integrity": "sha512-RWGh70XkqcJusaqB4TR0tVBSVkzlMU9krwALQmgilLTxaSBMPtB6xMt13ceEJ/G6BwsLZWdgY2Wy6GvdSheKaw==",
"dependencies": {
"@chainsafe/libp2p-noise": "4.0.0",
"@fluencelabs/avm": "0.16.0-restriction-operator.9",
@ -2818,6 +2824,11 @@
}
}
},
"@fluencelabs/aqua-lib": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@fluencelabs/aqua-lib/-/aqua-lib-0.2.1.tgz",
"integrity": "sha512-uLP9mbgFHR1Q1FYhehasNxNBlTclBsjNI9MvIPF8oXtVJtnvPi+R4rGGTOHtRJukunxhpAV/svWQU9a2BRyDmQ=="
},
"@fluencelabs/avm": {
"version": "0.16.0-restriction-operator.9",
"resolved": "https://registry.npmjs.org/@fluencelabs/avm/-/avm-0.16.0-restriction-operator.9.tgz",
@ -2827,9 +2838,9 @@
}
},
"@fluencelabs/fluence": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@fluencelabs/fluence/-/fluence-0.15.1.tgz",
"integrity": "sha512-ZHLw85XgVMglCVJjGkdGRFzL7kO2x31BCYDt4BVlMCE/S2nFSsVHU8DO35Jlh40QZhQdN3F5dbJpkgdcwdC8bw==",
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/@fluencelabs/fluence/-/fluence-0.15.2.tgz",
"integrity": "sha512-RWGh70XkqcJusaqB4TR0tVBSVkzlMU9krwALQmgilLTxaSBMPtB6xMt13ceEJ/G6BwsLZWdgY2Wy6GvdSheKaw==",
"requires": {
"@chainsafe/libp2p-noise": "4.0.0",
"@fluencelabs/avm": "0.16.0-restriction-operator.9",

View File

@ -6,8 +6,8 @@
"files": [
"aqua.js",
"index.js",
"index-java.js",
"error.js"
"error.js",
"utils.js"
],
"bin": {
"aqua": "index.js",
@ -18,7 +18,8 @@
"from:scalajs": "cp ../cli/.js/target/scala-3.0.2/cli-opt/main.js ./aqua.js && npm run run -- $@"
},
"dependencies": {
"@fluencelabs/fluence": "0.15.1"
"@fluencelabs/fluence": "0.15.2",
"@fluencelabs/aqua-lib": "0.2.1"
},
"repository": {
"type": "git",

2
npm/utils.js Normal file
View File

@ -0,0 +1,2 @@
// It should work in scala as js.`import`.meta.url, but it doesn't compile for some reasons
export const metaUrl = import.meta.url

View File

@ -1,4 +1,4 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0")