diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2fda9e5c..0c5e5c23 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,7 +40,7 @@ jobs: run: sbt cliJS/fullLinkJS - name: JS API build - run: sbt aqua-api/fullLinkJS + run: sbt aqua-apiJS/fullLinkJS - name: JS LSP build run: sbt language-server-apiJS/fullLinkJS @@ -55,7 +55,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: aqua-api - path: api/aqua-api/target/scala-*/aqua-api-opt/main.js + path: api/aqua-api/.js/target/scala-*/aqua-api-opt/main.js - name: Upload aqua-lsp artifact uses: actions/upload-artifact@v3 diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index cd4db9fb..bfaa664f 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -63,7 +63,7 @@ jobs: - name: JS API build env: SNAPSHOT: ${{ steps.version.outputs.id }} - run: sbt aqua-api/fastOptJS + run: sbt aqua-apiJS/fastOptJS - name: Upload aqua-cli artifact uses: actions/upload-artifact@v3 @@ -75,7 +75,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: aqua-api - path: api/aqua-api/target/scala-*/aqua-api-fastopt.js + path: api/aqua-api/.js/target/scala-*/aqua-api-fastopt.js - name: Upload aqua-lsp artifact uses: actions/upload-artifact@v3 diff --git a/api/aqua-api-npm/aqua-api.d.ts b/api/aqua-api-npm/aqua-api.d.ts index a32b26e1..11daff69 100644 --- a/api/aqua-api-npm/aqua-api.d.ts +++ b/api/aqua-api-npm/aqua-api.d.ts @@ -2,11 +2,13 @@ import type {FunctionCallDef, ServiceDef} from "@fluencelabs/fluence/dist/intern export class AquaConfig { constructor(logLevel: string, constants: string[], noXor: boolean, noRelay: boolean); + constructor(logLevel: string, constants: string[], noXor: boolean, noRelay: boolean, targetType: string); logLevel?: string constants?: string[] noXor?: boolean noRelay?: boolean + targetType?: string } export class AquaFunction { @@ -14,11 +16,19 @@ export class AquaFunction { script: string } +export class GeneratedSource { + name: string + tsSource?: string + jsSource?: string + tsTypes?: string +} + export class CompilationResult { services: Record functions: Record functionCall?: AquaFunction errors: string[] + generatedSources: GeneratedSource[] } export class Input { diff --git a/api/aqua-api-npm/package-lock.json b/api/aqua-api-npm/package-lock.json index c04f8506..1fbd0b9b 100644 --- a/api/aqua-api-npm/package-lock.json +++ b/api/aqua-api-npm/package-lock.json @@ -5247,7 +5247,7 @@ "node_modules/node-fetch": { "name": "@achingbrain/node-fetch", "version": "2.6.7", - "resolved": "https://npm.fluence.dev/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", + "resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==", "dev": true, "license": "MIT", @@ -11491,8 +11491,7 @@ "dev": true }, "node-fetch": { - "version": "npm:@achingbrain/node-fetch@2.6.7", - "resolved": "https://npm.fluence.dev/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", + "version": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==", "dev": true }, diff --git a/api/aqua-api-npm/package.json b/api/aqua-api-npm/package.json index 0711c625..97ae6d4d 100644 --- a/api/aqua-api-npm/package.json +++ b/api/aqua-api-npm/package.json @@ -9,8 +9,8 @@ "meta-utils.js" ], "scripts": { - "move:scalajs": "cp ../aqua-api/target/scala-3.1.3/aqua-api-opt/main.js ./aqua-api.js", - "move:fast": "cp ../aqua-api/target/scala-3.1.3/aqua-api-fastopt/main.js ./aqua-api.js" + "move:scalajs": "cp ../aqua-api/.js/target/scala-3.3.0/aqua-api-opt/main.js ./aqua-api.js", + "move:fast": "cp ../aqua-api/.js/target/scala-3.3.0/aqua-api-fastopt/main.js ./aqua-api.js" }, "repository": { "type": "git", diff --git a/api/aqua-api/.js/src/main/scala/api/AquaAPI.scala b/api/aqua-api/.js/src/main/scala/api/AquaAPI.scala new file mode 100644 index 00000000..daa83963 --- /dev/null +++ b/api/aqua-api/.js/src/main/scala/api/AquaAPI.scala @@ -0,0 +1,229 @@ +package api + +import api.types.{AquaConfig, AquaFunction, CompilationResult, GeneratedSource, Input} +import aqua.ErrorRendering.showError +import aqua.raw.value.ValueRaw +import aqua.api.{APICompilation, AquaAPIConfig} +import aqua.api.TargetType.* +import aqua.backend.air.AirBackend +import aqua.backend.{AirFunction, Backend, Generated} +import aqua.compiler.* +import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId} +import aqua.logging.{LogFormatter, LogLevels} +import aqua.constants.Constants +import aqua.io.* +import aqua.raw.ops.Call +import aqua.run.{CallInfo, CallPreparer, CliFunc, FuncCompiler, RunPreparer} +import aqua.parser.lexer.{LiteralToken, Token} +import aqua.parser.lift.FileSpan.F +import aqua.parser.lift.{FileSpan, Span} +import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError, ParserError} +import aqua.semantics.{CompilerState, HeaderError, RulesViolated, WrongAST} +import aqua.{AquaIO, SpanParser} +import aqua.model.transform.{Transform, TransformConfig} +import aqua.backend.api.APIBackend +import aqua.backend.js.JavaScriptBackend +import aqua.backend.ts.TypeScriptBackend +import aqua.definitions.FunctionDef +import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec} +import cats.data.Validated.{invalidNec, validNec, Invalid, Valid} +import cats.syntax.applicative.* +import cats.syntax.apply.* +import cats.syntax.flatMap.* +import cats.syntax.functor.* +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import cats.syntax.show.* +import cats.syntax.traverse.* +import fs2.io.file.{Files, Path} +import scribe.Logging + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.scalajs.js.{|, undefined, Promise, UndefOr} +import scala.scalajs.js +import scala.scalajs.js.JSConverters.* +import scala.scalajs.js.annotation.* +import aqua.js.{FunctionDefJs, ServiceDefJs, VarJson} +import aqua.model.AquaContext +import aqua.raw.ops.CallArrowRawTag +import aqua.raw.value.{LiteralRaw, VarRaw} +import aqua.res.AquaRes +import cats.Applicative + +@JSExportTopLevel("Aqua") +object AquaAPI extends App with Logging { + + /** + * All-in-one function that support different inputs and backends + * @param input can be a path to aqua file, string with a code or a function call + * @param imports list of paths + * @param aquaConfigJS compiler config + * @return compiler results depends on input and config + */ + @JSExport + def compile( + input: types.Input | types.Path | types.Call, + imports: js.Array[String], + aquaConfigJS: js.UndefOr[AquaConfig] + ): Promise[CompilationResult] = { + aquaConfigJS.toOption + .map(cjs => AquaConfig.fromJS(cjs)) + .getOrElse( + validNec(AquaAPIConfig()) + ).map { config => + val importsList = imports.toList + + input match { + case i: (types.Input | types.Path) => + compileAll(i, importsList, config) + case c: types.Call => + compileCall(c, importsList, config) + + } + } match { + case Valid(v) => v.unsafeToFuture().toJSPromise + case Invalid(errs) => js.Promise.resolve(CompilationResult.errs(errs.toChain.toList)) + } + } + + // Compile all non-call inputs + private def compileAll( + input: types.Input | types.Path, + imports: List[String], + config: AquaAPIConfig + ): IO[CompilationResult] = { + val backend: Backend = config.targetType match { + case AirType => APIBackend + case TypeScriptType => TypeScriptBackend() + case JavaScriptType => JavaScriptBackend() + } + + extension (res: IO[ValidatedNec[String, Chain[AquaCompiled[FileModuleId]]]]) + def toResult: IO[CompilationResult] = res.map( + _.andThen { compiled => + config.targetType match { + case AirType => validNec(generatedToAirResult(compiled)) + case TypeScriptType => compiledToTsSourceResult(compiled) + case JavaScriptType => compiledToJsSourceResult(compiled) + } + }.leftMap(errorsToResult).merge + ) + + input match { + case i: types.Input => + APICompilation + .compileString( + i.input, + imports, + config, + backend + ) + .toResult + case p: types.Path => + APICompilation + .compilePath( + p.path, + imports, + config, + backend + ) + .toResult + } + + } + + private def compileCall(call: types.Call, imports: List[String], config: AquaAPIConfig) = { + val path = call.input match { + case i: types.Input => i.input + case p: types.Path => p.path + } + + extension (res: IO[ValidatedNec[String, (FunctionDef, String)]]) + def callToResult: IO[CompilationResult] = res.map( + _.map { case (definitions, air) => + CompilationResult.result(call = Some(AquaFunction(FunctionDefJs(definitions), air))) + }.leftMap(errorsToResult).merge + ) + + APICompilation + .compileCall( + call.functionCall, + path, + imports, + config, + vr => VarJson.checkDataGetServices(vr, Some(call.arguments)).map(_._1) + ) + .callToResult + } + + private def errorsToResult(errors: NonEmptyChain[String]): CompilationResult = { + CompilationResult.errs(errors.toChain.toList) + } + + private def findBySuffix( + compiled: Seq[Generated], + suffix: String, + errorMsg: String + ): ValidatedNec[String, String] = { + Validated + .fromOption( + compiled.find(_.suffix == suffix).map(_.content), { + logger.error(errorMsg) + NonEmptyChain.one(errorMsg) + } + ) + } + + extension (res: Chain[Validated[NonEmptyChain[String], GeneratedSource]]) + def toSourcesResult: ValidatedNec[String, CompilationResult] = + res.sequence.map(_.toList.toJSArray).map(sources => CompilationResult.result(sources = sources)) + + private def compiledToTsSourceResult( + compiled: Chain[AquaCompiled[FileModuleId]] + ): ValidatedNec[String, CompilationResult] = { + compiled.map { c => + findBySuffix( + c.compiled, + TypeScriptBackend.ext, + "Unreachable. TypeScript backend must generate content." + ) + .map(GeneratedSource.tsSource(c.sourceId.toString, _)) + }.toSourcesResult + } + + private def compiledToJsSourceResult( + compiled: Chain[AquaCompiled[FileModuleId]] + ): ValidatedNec[String, CompilationResult] = { + compiled.map { c => + findBySuffix( + c.compiled, + JavaScriptBackend.dtsExt, + "Unreachable. JavaScript backend must generate '.d.ts' content." + ).andThen { tsTypes => + findBySuffix( + c.compiled, + JavaScriptBackend.ext, + "Unreachable. JavaScript backend must generate '.js' content." + ).map(jsSource => GeneratedSource.jsSource(c.sourceId.toString, jsSource, tsTypes)) + } + + }.toSourcesResult + } + + private def generatedToAirResult( + compiled: Chain[AquaCompiled[FileModuleId]] + ): CompilationResult = { + val generated = compiled.toList.flatMap(_.compiled) + val serviceDefs = generated.flatMap(_.services).map(s => s.name -> ServiceDefJs(s)) + val functions = generated.flatMap( + _.air.map(as => (as.name, AquaFunction(FunctionDefJs(as.funcDef), as.air))) + ) + + CompilationResult.result( + js.Dictionary.apply(serviceDefs: _*), + js.Dictionary.apply(functions: _*) + ) + + } +} diff --git a/api/aqua-api/.js/src/main/scala/api/types/InputTypes.scala b/api/aqua-api/.js/src/main/scala/api/types/InputTypes.scala new file mode 100644 index 00000000..96c2f852 --- /dev/null +++ b/api/aqua-api/.js/src/main/scala/api/types/InputTypes.scala @@ -0,0 +1,73 @@ +package api.types + +import aqua.api.AquaAPIConfig +import aqua.api.TargetType.* +import aqua.js.{FunctionDefJs, ServiceDefJs} +import aqua.model.transform.TransformConfig +import cats.data.Validated.{Invalid, Valid, invalidNec, validNec} +import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec} + +import scala.scalajs.js +import scala.scalajs.js.JSConverters.* +import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel} +import scala.scalajs.js.| + +@JSExportTopLevel("Input") +class Input( + @JSExport + val input: String +) + +@JSExportTopLevel("Path") +class Path( + @JSExport + val path: String +) + +@JSExportTopLevel("Call") +class Call( + @JSExport + val functionCall: String, + @JSExport + val arguments: js.Dynamic, + @JSExport + val input: Path | Input +) + +@JSExportTopLevel("AquaConfig") +class AquaConfig( + @JSExport + val logLevel: js.UndefOr[String], + @JSExport + val constants: js.UndefOr[js.Array[String]], + @JSExport + val noXor: js.UndefOr[Boolean], + @JSExport + val noRelay: js.UndefOr[Boolean], + @JSExport + val targetType: js.UndefOr[String] +) + +object AquaConfig { + + def fromJS(cjs: AquaConfig): ValidatedNec[String, AquaAPIConfig] = { + cjs.targetType.toOption + .map(_.toLowerCase()) + .map { + case "typescript" => validNec(TypeScriptType) + case "javascript" => validNec(JavaScriptType) + case "air" => validNec(AirType) + case _ => invalidNec("Target can be only 'typescript', 'javascript', or 'air'") + } + .getOrElse(validNec(AirType)) + .map { target => + AquaAPIConfig( + target, + cjs.logLevel.getOrElse("info"), + cjs.constants.map(_.toList).getOrElse(Nil), + cjs.noXor.getOrElse(false), + cjs.noRelay.getOrElse(false) + ) + } + } +} diff --git a/api/aqua-api/.js/src/main/scala/api/types/OutputTypes.scala b/api/aqua-api/.js/src/main/scala/api/types/OutputTypes.scala new file mode 100644 index 00000000..9954510f --- /dev/null +++ b/api/aqua-api/.js/src/main/scala/api/types/OutputTypes.scala @@ -0,0 +1,66 @@ +package api.types + +import aqua.js.{FunctionDefJs, ServiceDefJs} +import aqua.model.transform.TransformConfig +import cats.data.Validated.{Invalid, Valid, invalidNec, validNec} +import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec} + +import scala.scalajs.js +import scala.scalajs.js.JSConverters.* +import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel} +import scala.scalajs.js.| + +@JSExportTopLevel("AquaFunction") +case class AquaFunction( + @JSExport + funcDef: FunctionDefJs, + @JSExport + script: String +) + +@JSExportTopLevel("GeneratedSource") +case class GeneratedSource( + @JSExport + val name: String, + @JSExport + val tsSource: js.UndefOr[String], + @JSExport + val jsSource: js.UndefOr[String], + @JSExport + val tsTypes: js.UndefOr[String] +) + +object GeneratedSource { + def tsSource(name: String, tsSource: String) = new GeneratedSource(name, tsSource, null, null) + def jsSource(name: String, jsSource: String, tsTypes: String) = new GeneratedSource(name, null, jsSource, tsTypes) +} + +@JSExportTopLevel("CompilationResult") +class CompilationResult( + @JSExport + val services: js.Dictionary[ServiceDefJs], + @JSExport + val functions: js.Dictionary[AquaFunction], + @JSExport + val functionCall: js.UndefOr[AquaFunction], + @JSExport + val generatedSources: js.Array[GeneratedSource], + @JSExport + val errors: js.Array[String] +) + +object CompilationResult { + + def result( + services: js.Dictionary[ServiceDefJs] = js.Dictionary(), + functions: js.Dictionary[AquaFunction] = js.Dictionary(), + call: Option[AquaFunction] = None, + sources: js.Array[GeneratedSource] = js.Array() + ): CompilationResult = + new CompilationResult(services, functions, call.orNull, sources, js.Array()) + + def errs( + errors: List[String] + ): CompilationResult = + CompilationResult(js.Dictionary(), js.Dictionary(), null, null, errors.toJSArray) +} diff --git a/api/aqua-api/.jvm/src/main/scala/aqua/api/Test.scala b/api/aqua-api/.jvm/src/main/scala/aqua/api/Test.scala new file mode 100644 index 00000000..1734ddb1 --- /dev/null +++ b/api/aqua-api/.jvm/src/main/scala/aqua/api/Test.scala @@ -0,0 +1,18 @@ +package aqua.api +import cats.effect.{IO, IOApp} +import aqua.backend.js.JavaScriptBackend +import aqua.backend.ts.TypeScriptBackend +import aqua.api.TargetType.JavaScriptType + +object Test extends IOApp.Simple { + override def run: IO[Unit] = { + val input = + """func getNumber(number: u32) -> u32: + | <- number + |""".stripMargin + APICompilation.compileString(input, Nil, AquaAPIConfig(targetType = JavaScriptType), JavaScriptBackend()).map { + res => + println(res) + } + } +} diff --git a/api/aqua-api/src/main/scala/aqua/api/APICompilation.scala b/api/aqua-api/src/main/scala/aqua/api/APICompilation.scala new file mode 100644 index 00000000..b7e183b2 --- /dev/null +++ b/api/aqua-api/src/main/scala/aqua/api/APICompilation.scala @@ -0,0 +1,164 @@ +package aqua.api + +import aqua.ErrorRendering.showError +import aqua.raw.value.ValueRaw +import aqua.api.AquaAPIConfig +import aqua.backend.{AirFunction, Backend, Generated} +import aqua.compiler.* +import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId} +import aqua.logging.{LogFormatter, LogLevels} +import aqua.constants.Constants +import aqua.io.* +import aqua.raw.ops.Call +import aqua.run.{CallInfo, CallPreparer, CliFunc, FuncCompiler, RunPreparer} +import aqua.parser.lexer.{LiteralToken, Token} +import aqua.parser.lift.FileSpan.F +import aqua.parser.lift.{FileSpan, Span} +import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError, ParserError} +import aqua.semantics.{CompilerState, HeaderError, RulesViolated, WrongAST} +import aqua.{AquaIO, SpanParser} +import aqua.model.transform.{Transform, TransformConfig} +import aqua.backend.api.APIBackend +import aqua.definitions.FunctionDef +import aqua.model.AquaContext +import aqua.res.AquaRes +import cats.Applicative +import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec} +import cats.data.Validated.{Invalid, Valid, invalid, invalidNec, validNec} +import cats.syntax.applicative.* +import cats.syntax.apply.* +import cats.syntax.flatMap.* +import cats.syntax.functor.* +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import cats.syntax.show.* +import cats.syntax.traverse.* +import fs2.io.file.{Files, Path} +import scribe.Logging + +object APICompilation { + + def compileCall( + functionStr: String, + pathStr: String, + imports: List[String], + aquaConfig: AquaAPIConfig, + fillWithTypes: List[ValueRaw] => ValidatedNec[String, List[ValueRaw]] + ): IO[ValidatedNec[String, (FunctionDef, String)]] = { + implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] + ( + LogLevels.levelFromString(aquaConfig.logLevel), + Constants.parse(aquaConfig.constants) + ).mapN { (level, constants) => + + val transformConfig = aquaConfig.getTransformConfig.copy(constants = constants) + + LogFormatter.initLogger(Some(level)) + + new FuncCompiler[IO]( + Some(RelativePath(Path(pathStr))), + imports.map(Path.apply), + transformConfig + ).compile().map { contextV => + contextV.andThen { context => + CliFunc + .fromString(functionStr) + .leftMap(errs => NonEmptyChain.fromNonEmptyList(errs)) + .andThen { cliFunc => + FuncCompiler.findFunction(context, cliFunc).andThen { arrow => + fillWithTypes(cliFunc.args).andThen { argsWithTypes => + val func = cliFunc.copy(args = argsWithTypes) + val preparer = new RunPreparer( + func, + arrow, + transformConfig + ) + preparer.prepare().map { ci => + (ci.definitions, ci.air) + } + } + } + } + }.leftMap(_.map(_.show).distinct) + + } + } match { + case Valid(pr) => pr + case Invalid(errs) => IO.pure(Invalid(NonEmptyChain.fromNonEmptyList(errs))) + } + } + + def compilePath( + pathStr: String, + imports: List[String], + aquaConfig: AquaAPIConfig, + backend: Backend + ): IO[ValidatedNec[String, Chain[AquaCompiled[FileModuleId]]]] = { + implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] + val path = Path(pathStr) + val sources = new AquaFileSources[IO](path, imports.map(Path.apply)) + compileRaw( + aquaConfig, + sources, + backend + ) + } + + def compileString( + input: String, + imports: List[String], + aquaConfig: AquaAPIConfig, + backend: Backend + ): IO[ValidatedNec[String, Chain[AquaCompiled[FileModuleId]]]] = { + implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] + val path = Path("") + + val strSources: AquaFileSources[IO] = + new AquaFileSources[IO](path, imports.map(Path.apply)) { + override def sources: IO[ValidatedNec[AquaFileError, Chain[(FileModuleId, String)]]] = { + IO.pure(Valid(Chain.one((FileModuleId(path), input)))) + } + } + compileRaw( + aquaConfig, + strSources, + backend + ) + } + + private def compileRaw( + aquaConfig: AquaAPIConfig, + sources: AquaSources[IO, AquaFileError, FileModuleId], + backend: Backend + ): IO[ValidatedNec[String, Chain[AquaCompiled[FileModuleId]]]] = { + + ( + LogLevels.levelFromString(aquaConfig.logLevel), + Constants.parse(aquaConfig.constants) + ).traverseN { (level, constants) => + + LogFormatter.initLogger(Some(level)) + + val config = AquaCompilerConf(constants) + val transformConfig = aquaConfig.getTransformConfig + + CompilerAPI + .compile[IO, AquaFileError, FileModuleId, FileSpan.F]( + sources, + SpanParser.parser, + new AirValidator[IO] { + override def init(): IO[Unit] = Applicative[IO].pure(()) + override def validate(airs: List[AirFunction]): IO[ValidatedNec[String, Unit]] = + Applicative[IO].pure(validNec(())) + }, + new Backend.Transform: + override def transform(ex: AquaContext): AquaRes = + Transform.contextRes(ex, transformConfig) + + override def generate(aqua: AquaRes): Seq[Generated] = backend.generate(aqua) + , + config + ).map(_.leftMap(_.map(_.show).distinct)) + }.map(_.leftMap(NonEmptyChain.fromNonEmptyList).andThen(identity)) + } +} diff --git a/api/aqua-api/src/main/scala/aqua/api/AquaAPI.scala b/api/aqua-api/src/main/scala/aqua/api/AquaAPI.scala deleted file mode 100644 index 862a8fd8..00000000 --- a/api/aqua-api/src/main/scala/aqua/api/AquaAPI.scala +++ /dev/null @@ -1,233 +0,0 @@ -package aqua.api - -import aqua.ErrorRendering.showError -import aqua.api.types.{AquaAPIConfig, AquaConfig, AquaFunction, CompilationResult, Input} -import aqua.backend.{AirFunction, Backend, Generated} -import aqua.compiler.* -import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId} -import aqua.logging.{LogFormatter, LogLevels} -import aqua.constants.Constants -import aqua.io.* -import aqua.raw.ops.Call -import aqua.run.{CallInfo, CallPreparer, CliFunc, FuncCompiler, RunPreparer} -import aqua.parser.lexer.{LiteralToken, Token} -import aqua.parser.lift.FileSpan.F -import aqua.parser.lift.{FileSpan, Span} -import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError, ParserError} -import aqua.semantics.{CompilerState, HeaderError, RulesViolated, WrongAST} -import aqua.{AquaIO, SpanParser} -import aqua.model.transform.{Transform, TransformConfig} -import aqua.backend.api.APIBackend -import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec} -import cats.data.Validated.{Invalid, Valid, invalidNec, validNec} -import cats.syntax.applicative.* -import cats.syntax.apply.* -import cats.syntax.flatMap.* -import cats.syntax.functor.* -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import cats.syntax.show.* -import cats.syntax.traverse.* -import fs2.io.file.{Files, Path} -import scribe.Logging - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future -import scala.scalajs.js.| -import scala.scalajs.js -import scala.scalajs.js.JSConverters.* -import scala.scalajs.js.annotation.* -import scala.scalajs.js.{UndefOr, undefined} -import aqua.js.{FunctionDefJs, ServiceDefJs, VarJson} -import aqua.model.AquaContext -import aqua.raw.ops.CallArrowRawTag -import aqua.raw.value.{LiteralRaw, VarRaw} -import aqua.res.AquaRes -import cats.Applicative - -@JSExportTopLevel("Aqua") -object AquaAPI extends App with Logging { - - def getTag(serviceId: String, value: VarRaw) = { - CallArrowRawTag.service( - LiteralRaw.quote(serviceId), - value.name, - Call(List.empty, List(Call.Export(value.name, value.baseType))) - ) - } - - @JSExport - def compile( - input: types.Input | types.Path | types.Call, - imports: js.Array[String], - aquaConfigJS: js.UndefOr[AquaConfig] - ) = { - val aquaConfig: AquaAPIConfig = - aquaConfigJS.toOption.map(cjs => AquaAPIConfig.fromJS(cjs)).getOrElse(AquaAPIConfig()) - - val importsList = imports.toList - - input match { - case i: types.Input => compileString(i.input, importsList, aquaConfig) - case p: types.Path => compilePath(p.path, importsList, aquaConfig) - case c: types.Call => - val path = c.input match { - case i: types.Input => i.input - case p: types.Path => p.path - } - compileCall(c.functionCall, c.arguments, path, importsList, aquaConfig) - - } - } - - def compileCall( - functionStr: String, - arguments: js.Dynamic, - pathStr: String, - imports: List[String], - aquaConfig: AquaAPIConfig - ): js.Promise[CompilationResult] = { - implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] - ( - LogLevels.levelFromString(aquaConfig.logLevel), - Constants.parse(aquaConfig.constants) - ).mapN { (level, constants) => - - val transformConfig = aquaConfig.getTransformConfig.copy(constants = constants) - - LogFormatter.initLogger(Some(level)) - - new FuncCompiler[IO]( - Some(RelativePath(Path(pathStr))), - imports.map(Path.apply), - transformConfig - ).compile() - .map { contextV => - contextV.andThen { context => - CliFunc - .fromString(functionStr) - .leftMap(errs => NonEmptyChain.fromNonEmptyList(errs)) - .andThen { cliFunc => - FuncCompiler.findFunction(context, cliFunc).andThen { arrow => - VarJson.checkDataGetServices(cliFunc.args, Some(arguments)).andThen { - case (argsWithTypes, _) => - val func = cliFunc.copy(args = argsWithTypes) - val preparer = new RunPreparer( - func, - arrow, - transformConfig - ) - preparer.prepare().map { ci => - AquaFunction(FunctionDefJs(ci.definitions), ci.air) - } - } - } - } - } - } - .flatMap { - case Valid(result) => IO.pure(CompilationResult.result(js.Dictionary(), js.Dictionary(), Some(result))) - case Invalid(err) => - val errs = err.map(_.show).distinct.toChain.toList - IO.pure(CompilationResult.errs(errs)) - } - .unsafeToFuture() - .toJSPromise - } match { - case Valid(pr) => pr - case Invalid(err) => js.Promise.resolve(CompilationResult.errs(err.toList)) - } - - } - - def compilePath( - pathStr: String, - imports: List[String], - aquaConfig: AquaAPIConfig - ): js.Promise[CompilationResult] = { - implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] - val path = Path(pathStr) - val sources = new AquaFileSources[IO](path, imports.map(Path.apply)) - compileRaw(aquaConfig, sources) - } - - def compileString( - input: String, - imports: List[String], - aquaConfig: AquaAPIConfig - ): js.Promise[CompilationResult] = { - implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] - val path = Path("") - - val strSources: AquaFileSources[IO] = - new AquaFileSources[IO](path, imports.map(Path.apply)) { - override def sources: IO[ValidatedNec[AquaFileError, Chain[(FileModuleId, String)]]] = { - IO.pure(Valid(Chain.one((FileModuleId(path), input)))) - } - } - compileRaw(aquaConfig, strSources) - } - - def compileRaw( - aquaConfig: AquaAPIConfig, - sources: AquaSources[IO, AquaFileError, FileModuleId] - ): js.Promise[CompilationResult] = { - - ( - LogLevels.levelFromString(aquaConfig.logLevel), - Constants.parse(aquaConfig.constants) - ).mapN { (level, constants) => - - LogFormatter.initLogger(Some(level)) - - val config = AquaCompilerConf(constants) - val transformConfig = aquaConfig.getTransformConfig - - val proc = for { - res <- CompilerAPI - .compile[IO, AquaFileError, FileModuleId, FileSpan.F]( - sources, - SpanParser.parser, - new AirValidator[IO] { - override def init(): IO[Unit] = Applicative[IO].pure(()) - override def validate(airs: List[AirFunction]): IO[ValidatedNec[String, Unit]] = - Applicative[IO].pure(validNec(())) - }, - new Backend.Transform: - override def transform(ex: AquaContext): AquaRes = - Transform.contextRes(ex, transformConfig) - - override def generate(aqua: AquaRes): Seq[Generated] = APIBackend.generate(aqua) - , - config - ) - jsResult <- res match { - case Valid(compiled) => - val allGenerated: List[Generated] = compiled.toList.flatMap(_.compiled) - val serviceDefs = allGenerated.flatMap(_.services).map(s => s.name -> ServiceDefJs(s)) - val functions = allGenerated.flatMap( - _.air.map(as => (as.name, AquaFunction(FunctionDefJs(as.funcDef), as.air))) - ) - - IO.pure( - CompilationResult.result( - js.Dictionary.apply(serviceDefs: _*), - js.Dictionary.apply(functions: _*), - None - ) - ) - case Invalid(errChain) => - val errors = errChain.map(_.show).distinct.toChain.toList - IO.pure[CompilationResult](CompilationResult.errs(errors)) - } - } yield { - jsResult - } - - proc.unsafeToFuture().toJSPromise - } match { - case Valid(pr) => pr - case Invalid(err) => js.Promise.resolve(CompilationResult.errs(err.toList)) - } - } -} diff --git a/api/aqua-api/src/main/scala/aqua/api/AquaAPIConfig.scala b/api/aqua-api/src/main/scala/aqua/api/AquaAPIConfig.scala new file mode 100644 index 00000000..9129c965 --- /dev/null +++ b/api/aqua-api/src/main/scala/aqua/api/AquaAPIConfig.scala @@ -0,0 +1,18 @@ +package aqua.api +import aqua.model.transform.TransformConfig + +enum TargetType: + case TypeScriptType, JavaScriptType, AirType + +case class AquaAPIConfig( + targetType: TargetType = TargetType.AirType, + logLevel: String = "info", + constants: List[String] = Nil, + noXor: Boolean = false, + noRelay: Boolean = false +) { + + def getTransformConfig: TransformConfig = + if (noRelay) TransformConfig(relayVarName = None, wrapWithXor = !noXor) + else TransformConfig(wrapWithXor = !noXor) +} diff --git a/api/aqua-api/src/main/scala/aqua/api/types/Types.scala b/api/aqua-api/src/main/scala/aqua/api/types/Types.scala deleted file mode 100644 index 743eaba4..00000000 --- a/api/aqua-api/src/main/scala/aqua/api/types/Types.scala +++ /dev/null @@ -1,103 +0,0 @@ -package aqua.api.types - -import aqua.api.* -import aqua.js.{FunctionDefJs, ServiceDefJs} -import aqua.model.transform.TransformConfig - -import scala.scalajs.js -import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel} -import scala.scalajs.js.| -import scala.scalajs.js.JSConverters.* - -@JSExportTopLevel("AquaFunction") -case class AquaFunction( - @JSExport - funcDef: FunctionDefJs, - @JSExport - script: String -) - -@JSExportTopLevel("Input") -class Input( - @JSExport - val input: String -) - -@JSExportTopLevel("Path") -class Path( - @JSExport - val path: String -) - -@JSExportTopLevel("Call") -class Call( - @JSExport - val functionCall: String, - @JSExport - val arguments: js.Dynamic, - @JSExport - val input: Path | Input -) - -@JSExportTopLevel("AquaConfig") -class AquaConfig( - @JSExport - val logLevel: js.UndefOr[String], - @JSExport - val constants: js.UndefOr[js.Array[String]], - @JSExport - val noXor: js.UndefOr[Boolean], - @JSExport - val noRelay: js.UndefOr[Boolean] -) - -@JSExportTopLevel("CompilationResult") -class CompilationResult( - @JSExport - val services: js.Dictionary[ServiceDefJs], - @JSExport - val functions: js.Dictionary[AquaFunction], - @JSExport - val functionCall: js.UndefOr[AquaFunction], - @JSExport - val errors: js.Array[String] -) - -object CompilationResult { - - def result( - services: js.Dictionary[ServiceDefJs], - functions: js.Dictionary[AquaFunction], - call: Option[AquaFunction] - ): CompilationResult = - new CompilationResult(services, functions, call.orNull, js.Array()) - - def errs( - errors: List[String] - ): CompilationResult = - CompilationResult(js.Dictionary(), js.Dictionary(), null, errors.toJSArray) -} - -case class AquaAPIConfig( - logLevel: String = "info", - constants: List[String] = Nil, - noXor: Boolean = false, - noRelay: Boolean = false -) { - - def getTransformConfig: TransformConfig = - if (noRelay) TransformConfig(relayVarName = None, wrapWithXor = !noXor) - else TransformConfig(wrapWithXor = !noXor) -} - -object AquaAPIConfig { - - def fromJS(cjs: AquaConfig): AquaAPIConfig = { - AquaAPIConfig( - cjs.logLevel.getOrElse("info"), - cjs.constants.map(_.toList).getOrElse(Nil), - cjs.noXor.getOrElse(false), - cjs.noRelay.getOrElse(false) - ) - } -} diff --git a/backend/ts/src/main/scala/aqua/backend/js/JavaScriptBackend.scala b/backend/ts/src/main/scala/aqua/backend/js/JavaScriptBackend.scala index 59a7473f..da2fd024 100644 --- a/backend/ts/src/main/scala/aqua/backend/js/JavaScriptBackend.scala +++ b/backend/ts/src/main/scala/aqua/backend/js/JavaScriptBackend.scala @@ -4,10 +4,7 @@ import aqua.backend.ts.TypeScriptTypes import aqua.backend.* import aqua.res.AquaRes -case class JavaScriptBackend(isOldFluenceJs: Boolean, client: String = Backend.client) extends Backend { - - val ext = ".js" - val tsExt = ".d.ts" +case class JavaScriptBackend(isOldFluenceJs: Boolean = false, client: String = Backend.client) extends Backend { val types = TypeScriptTypes(client) def typesFile(res: AquaRes): Generated = { @@ -31,13 +28,18 @@ case class JavaScriptBackend(isOldFluenceJs: Boolean, client: String = Backend.c | |/* eslint-enable */""".stripMargin - Generated(tsExt, body, Nil) + Generated(JavaScriptBackend.dtsExt, body, Nil) } override def generate(res: AquaRes): Seq[Generated] = if (res.isEmpty) Nil else { val (airs, script) = OutputFile(res).generate(EmptyTypes, true, isOldFluenceJs) - Generated(ext, script, airs) :: typesFile(res) :: Nil + Generated(JavaScriptBackend.ext, script, airs) :: typesFile(res) :: Nil } } + +object JavaScriptBackend { + val ext = ".js" + val dtsExt = ".d.ts" +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala index 2bb7166b..f75f912c 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala @@ -4,14 +4,16 @@ import aqua.backend.{Backend, Generated, OutputFile} import aqua.res.AquaRes import cats.data.NonEmptyChain -case class TypeScriptBackend(isOldFluenceJs: Boolean, client: String = Backend.client) extends Backend { - - val ext = ".ts" +case class TypeScriptBackend(isOldFluenceJs: Boolean = false, client: String = Backend.client) extends Backend { override def generate(res: AquaRes): Seq[Generated] = if (res.isEmpty) Nil else { - val (airs, script) = OutputFile(res).generate(TypeScriptTypes(client), false, isOldFluenceJs) - Generated(ext, script, airs) :: Nil + val (airs, script) = OutputFile(res).generate(TypeScriptTypes(client), isJs = false, isOldFluenceJs) + Generated(TypeScriptBackend.ext, script, airs) :: Nil } } + +object TypeScriptBackend { + val ext = ".ts" +} diff --git a/build.sbt b/build.sbt index 982488d8..34550ea8 100644 --- a/build.sbt +++ b/build.sbt @@ -143,16 +143,21 @@ lazy val `js-imports` = project .settings(commons: _*) .dependsOn(`js-exports`, transform.js) -lazy val `aqua-api` = project +lazy val `aqua-api` = crossProject(JSPlatform, JVMPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .in(file("api/aqua-api")) - .enablePlugins(ScalaJSPlugin) .settings(commons: _*) + .dependsOn(`aqua-run`, `backend-api`) + +lazy val `aqua-apiJS` = `aqua-api`.js .settings( scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), scalaJSUseMainModuleInitializer := true, Test / test := {} ) - .dependsOn(`js-exports`, `aqua-run`.js, `backend-api`.js) + .enablePlugins(ScalaJSPlugin) + .dependsOn(`js-exports`) lazy val types = crossProject(JVMPlatform, JSPlatform) .withoutSuffixFor(JVMPlatform)