From e1756313e1d2598ed62264110d0a9d2a2c19fd0a Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Sat, 23 Jul 2022 19:28:45 +0200 Subject: [PATCH 01/10] (JS) Expose handler function type to allow manual exporting of Lambda handler Related to #246 --- .../scala/feral/lambda/IOLambdaPlatform.scala | 13 +++- .../feral/lambda/ExportedLambdaSuite.scala | 76 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala diff --git a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala index 08bf57e8..d5ae84ee 100644 --- a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -26,12 +26,23 @@ import scala.scalajs.js.| private[lambda] trait IOLambdaPlatform[Event, Result] { this: IOLambda[Event, Result] => + /** + * Lambda handler. Implement this type with a val and a call to `handlerFn` to export your + * handler. + * + * @example + * {{{ + * val handler: HandlerFn = handlerFn + * }}} + */ + final type HandlerFn = js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] + final def main(args: Array[String]): Unit = js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn) protected def handlerName: String = getClass.getSimpleName.init - private lazy val handlerFn + protected lazy val handlerFn : js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] = { (event: js.Any, context: facade.Context) => (for { diff --git a/lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala b/lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala new file mode 100644 index 00000000..44636574 --- /dev/null +++ b/lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala @@ -0,0 +1,76 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda + +import cats.effect.IO +import cats.effect.Ref +import cats.effect.kernel.Resource +import cats.syntax.all._ +import io.circe.scalajs._ +import munit.CatsEffectSuite + +import scala.scalajs.js +import scala.scalajs.js.| + +class ExportedLambdaSuite extends CatsEffectSuite { + test("exported lambda") { + val context = DummyContext.asInstanceOf[facade.Context] + + for { + allocationCounter <- IO.ref(0) + invokeCounter <- IO.ref(0) + lambda = new CountingIOLambda(allocationCounter, invokeCounter) + + _ <- ('0' to 'z') + .map(_.toString) + .toList + .traverse(x => + IO.fromPromise(IO(lambda.impl(x, context))) + .assertEquals(x.asJsAny.asInstanceOf[js.Any | Unit])) + + _ <- allocationCounter.get.assertEquals(1) + _ <- invokeCounter.get.assertEquals(75) + } yield () + + } +} + +class CountingIOLambda(allocationCounter: Ref[IO, Int], invokeCounter: Ref[IO, Int]) + extends IOLambda[String, String] { + + override def handler: Resource[IO, LambdaEnv[IO, String] => IO[Option[String]]] = + Resource + .eval(allocationCounter.getAndUpdate(_ + 1)) + .as(_.event.map(Some(_)) <* invokeCounter.getAndUpdate(_ + 1)) + + // This would be exported with `@JSExportTopLevel("handler")` + def impl: HandlerFn = handlerFn +} + +object DummyContext extends js.Object { + def callbackWaitsForEmptyEventLoop: Boolean = true + def functionName: String = "" + def functionVersion: String = "" + def invokedFunctionArn: String = "" + def memoryLimitInMB: String = "512" + def awsRequestId: String = "" + def logGroupName: String = "" + def logStreamName: String = "" + def identity: js.UndefOr[CognitoIdentity] = js.undefined + def clientContext: js.UndefOr[ClientContext] = js.undefined + def getRemainingTimeInMillis(): Double = 0 +} From ce19f010ed7cd1f71fb3ee293f63032646ac15dd Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Sat, 30 Jul 2022 13:26:35 +0200 Subject: [PATCH 02/10] make handlerFn final --- lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala index d5ae84ee..e387a3cf 100644 --- a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -42,7 +42,7 @@ private[lambda] trait IOLambdaPlatform[Event, Result] { protected def handlerName: String = getClass.getSimpleName.init - protected lazy val handlerFn + final protected lazy val handlerFn : js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] = { (event: js.Any, context: facade.Context) => (for { From 8face8d390580db18ec3257a30ea9819254d70cd Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Sat, 30 Jul 2022 14:27:20 +0200 Subject: [PATCH 03/10] add scripted test --- .../scala/feral/lambda/IOLambdaPlatform.scala | 12 ++- .../feral/lambda/ExportedLambdaSuite.scala | 76 ------------------- .../iolambda-simple/build.sbt | 3 + .../iolambda-simple/project/build.properties | 1 + .../iolambda-simple/project/plugins.sbt | 1 + .../src/main/scala/mySimpleHandler.scala | 10 +++ .../iolambda-simple/test | 4 + .../iolambda-simple/test-export.js | 4 + 8 files changed, 34 insertions(+), 77 deletions(-) delete mode 100644 lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala create mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt create mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/build.properties create mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/plugins.sbt create mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala create mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test create mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js diff --git a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala index e387a3cf..308c9a27 100644 --- a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -22,6 +22,7 @@ import io.circe.scalajs._ import scala.scalajs.js import scala.scalajs.js.JSConverters._ import scala.scalajs.js.| +import scala.util.Try private[lambda] trait IOLambdaPlatform[Event, Result] { this: IOLambda[Event, Result] => @@ -37,8 +38,17 @@ private[lambda] trait IOLambdaPlatform[Event, Result] { */ final type HandlerFn = js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] - final def main(args: Array[String]): Unit = + final def main(args: Array[String]): Unit = { + // `exports` will throw a ReferenceError in ESModule (because `exports` comes from commonjs modules) + def isESModule = Try(js.Dynamic.global.exports).isFailure + + if (isESModule) { + throw new Error( + s"Cannot run ES Module with main module. Use `@JSExportTopLevel` or CommonJS instead. See https://github.com/typelevel/feral#readme") + } + js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn) + } protected def handlerName: String = getClass.getSimpleName.init diff --git a/lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala b/lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala deleted file mode 100644 index 44636574..00000000 --- a/lambda/js/src/test/scala/feral/lambda/ExportedLambdaSuite.scala +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2021 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package feral.lambda - -import cats.effect.IO -import cats.effect.Ref -import cats.effect.kernel.Resource -import cats.syntax.all._ -import io.circe.scalajs._ -import munit.CatsEffectSuite - -import scala.scalajs.js -import scala.scalajs.js.| - -class ExportedLambdaSuite extends CatsEffectSuite { - test("exported lambda") { - val context = DummyContext.asInstanceOf[facade.Context] - - for { - allocationCounter <- IO.ref(0) - invokeCounter <- IO.ref(0) - lambda = new CountingIOLambda(allocationCounter, invokeCounter) - - _ <- ('0' to 'z') - .map(_.toString) - .toList - .traverse(x => - IO.fromPromise(IO(lambda.impl(x, context))) - .assertEquals(x.asJsAny.asInstanceOf[js.Any | Unit])) - - _ <- allocationCounter.get.assertEquals(1) - _ <- invokeCounter.get.assertEquals(75) - } yield () - - } -} - -class CountingIOLambda(allocationCounter: Ref[IO, Int], invokeCounter: Ref[IO, Int]) - extends IOLambda[String, String] { - - override def handler: Resource[IO, LambdaEnv[IO, String] => IO[Option[String]]] = - Resource - .eval(allocationCounter.getAndUpdate(_ + 1)) - .as(_.event.map(Some(_)) <* invokeCounter.getAndUpdate(_ + 1)) - - // This would be exported with `@JSExportTopLevel("handler")` - def impl: HandlerFn = handlerFn -} - -object DummyContext extends js.Object { - def callbackWaitsForEmptyEventLoop: Boolean = true - def functionName: String = "" - def functionVersion: String = "" - def invokedFunctionArn: String = "" - def memoryLimitInMB: String = "512" - def awsRequestId: String = "" - def logGroupName: String = "" - def logStreamName: String = "" - def identity: js.UndefOr[CognitoIdentity] = js.undefined - def clientContext: js.UndefOr[ClientContext] = js.undefined - def getRemainingTimeInMillis(): Double = 0 -} diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt new file mode 100644 index 00000000..6eda7b71 --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt @@ -0,0 +1,3 @@ +scalaVersion := "2.13.8" +enablePlugins(LambdaJSPlugin) +scalaJSUseMainModuleInitializer := false diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/build.properties b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/build.properties new file mode 100644 index 00000000..22af2628 --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.7.1 diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/plugins.sbt b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/plugins.sbt new file mode 100644 index 00000000..f878a0fb --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.typelevel" % "sbt-feral-lambda" % sys.props("plugin.version")) diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala new file mode 100644 index 00000000..dc93deb0 --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala @@ -0,0 +1,10 @@ +import cats.effect._ +import feral.lambda._ +import scala.scalajs.js.annotation._ + +object mySimpleHandler extends IOLambda.Simple[Unit, INothing] { + def apply(event: Unit, context: Context[IO], init: Init): IO[Option[INothing]] = IO.none + + @JSExportTopLevel("exportedHandler") + val impl: HandlerFn = handlerFn +} diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test new file mode 100644 index 00000000..752897ff --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test @@ -0,0 +1,4 @@ +> npmPackage +$ exists target/scala-2.13/npm-package/index.js +$ exists target/scala-2.13/npm-package/package.json +$ exec node test-export.mjs diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js new file mode 100644 index 00000000..81efa010 --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js @@ -0,0 +1,4 @@ +if (typeof require('./target/scala-2.13/npm-package/index.js').exportedHandler === 'function') + process.exit(0) +else + process.exit(1) From b11009928c747268ce8027ee365270ab8b76ae57 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Sat, 30 Jul 2022 14:42:03 +0200 Subject: [PATCH 04/10] update docs --- README.md | 26 ++++++++++++++++++- .../scala/feral/lambda/IOLambdaPlatform.scala | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c35f7b2..2286b118 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ Next, implement your Lambda. Please refer to the [examples](examples/src/main/sc There are several options to deploy your Lambda. For example you can use the [Lambda console](https://docs.aws.amazon.com/lambda/latest/dg/foundation-console.html), the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html), or the [serverless framework](https://www.serverless.com/framework/docs/providers/aws/guide/deploying). -To deploy a Scala.js Lambda, you will need to know the following: +### CommonJS + +To deploy a CommonJS Scala.js Lambda, you will need to know the following: 1. The runtime for your Lambda is Node.js 16. 2. The handler for your Lambda is `index.yourLambdaName`. - `index` refers to the `index.js` file containing the JavaScript sources for your Lambda. @@ -31,6 +33,28 @@ To deploy a Scala.js Lambda, you will need to know the following: 3. Run `sbt npmPackage` to package your Lambda for deployment. Note that you can currently only have one Lambda per sbt (sub-)project. If you have multiple, you will need to select the one to deploy using `Compile / mainClass := Some("my.lambda.handler")`. 4. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory. +### ES Modules + +It's possible to use ES Modules to run your Lambda. You need the following steps instead of the above: + +1. The runtime for your Lambda is Node.js 16. +2. Add a `@JSExportTopLevel` inside your lambda object: + ```scala + @JSExportTopLevel("yourLambdaName") + val impl: HandlerFn = handlerFn + ``` + - The type `HandlerFn` is important so Scala.JS will emit your lambda as a JavaScript function. + - `val` is important so your lambda function is emitted as a value, instead of a function returning another function. + - The handler for your lambda is `index.yourLambdaName` +4. Change these settings in your `build.sbt`: + ```scala + scalaJSUseMainModuleInitializer := false + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule).withOutputPatterns(OutputPatterns.fromJSFile("%.mjs"))) + ``` +4. Run `sbt npmPackage` to package your Lambda for deployment. You can have multiple lambda's by exporting multiple handler functions. +5. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory. + + As the feral project develops, one of the goals is to provide an sbt plugin that simplifies and automates the deployment process. If this appeals to you, please contribute feature requests, ideas, and/or code! ## Why go feral? diff --git a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala index 308c9a27..89ffea88 100644 --- a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -44,7 +44,7 @@ private[lambda] trait IOLambdaPlatform[Event, Result] { if (isESModule) { throw new Error( - s"Cannot run ES Module with main module. Use `@JSExportTopLevel` or CommonJS instead. See https://github.com/typelevel/feral#readme") + s"Cannot run ES Module with main module. Use `@JSExportTopLevel` or CommonJS instead. See https://github.com/typelevel/feral#es-modules") } js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn) From afe6f14e950afecb03a0dab76320e8ad01a1a251 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Sat, 30 Jul 2022 15:10:17 +0200 Subject: [PATCH 05/10] update scripted test to use esm like in readme --- README.md | 12 +++++++++--- .../iolambda-simple/build.sbt | 3 --- .../iolambda-simple/test-export.js | 4 ---- .../lambda-js-plugin/iolambda-export/build.sbt | 7 +++++++ .../iolambda-export}/project/build.properties | 0 .../iolambda-export}/project/plugins.sbt | 0 .../src/main/scala/mySimpleHandler.scala | 0 .../iolambda-export}/test | 0 .../lambda-js-plugin/iolambda-export/test-export.mjs | 5 +++++ 9 files changed, 21 insertions(+), 10 deletions(-) delete mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt delete mode 100644 sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js create mode 100644 sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/build.sbt rename sbt-lambda/src/sbt-test/{lambda-js-export-plugin/iolambda-simple => lambda-js-plugin/iolambda-export}/project/build.properties (100%) rename sbt-lambda/src/sbt-test/{lambda-js-export-plugin/iolambda-simple => lambda-js-plugin/iolambda-export}/project/plugins.sbt (100%) rename sbt-lambda/src/sbt-test/{lambda-js-export-plugin/iolambda-simple => lambda-js-plugin/iolambda-export}/src/main/scala/mySimpleHandler.scala (100%) rename sbt-lambda/src/sbt-test/{lambda-js-export-plugin/iolambda-simple => lambda-js-plugin/iolambda-export}/test (100%) create mode 100644 sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test-export.mjs diff --git a/README.md b/README.md index 2286b118..cfdd0a44 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ There are several options to deploy your Lambda. For example you can use the [La ### CommonJS To deploy a CommonJS Scala.js Lambda, you will need to know the following: + 1. The runtime for your Lambda is Node.js 16. 2. The handler for your Lambda is `index.yourLambdaName`. - `index` refers to the `index.js` file containing the JavaScript sources for your Lambda. @@ -46,15 +47,20 @@ It's possible to use ES Modules to run your Lambda. You need the following steps - The type `HandlerFn` is important so Scala.JS will emit your lambda as a JavaScript function. - `val` is important so your lambda function is emitted as a value, instead of a function returning another function. - The handler for your lambda is `index.yourLambdaName` -4. Change these settings in your `build.sbt`: +3. Change these settings in your `build.sbt`: + ```scala + import org.scalajs.linker.interface.OutputPatterns + import _root_.io.circe.syntax._ + scalaJSUseMainModuleInitializer := false - scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule).withOutputPatterns(OutputPatterns.fromJSFile("%.mjs"))) + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule)) + npmPackageAdditionalNpmConfig := Map("type" -> "module".asJson) ``` + 4. Run `sbt npmPackage` to package your Lambda for deployment. You can have multiple lambda's by exporting multiple handler functions. 5. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory. - As the feral project develops, one of the goals is to provide an sbt plugin that simplifies and automates the deployment process. If this appeals to you, please contribute feature requests, ideas, and/or code! ## Why go feral? diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt deleted file mode 100644 index 6eda7b71..00000000 --- a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/build.sbt +++ /dev/null @@ -1,3 +0,0 @@ -scalaVersion := "2.13.8" -enablePlugins(LambdaJSPlugin) -scalaJSUseMainModuleInitializer := false diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js b/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js deleted file mode 100644 index 81efa010..00000000 --- a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test-export.js +++ /dev/null @@ -1,4 +0,0 @@ -if (typeof require('./target/scala-2.13/npm-package/index.js').exportedHandler === 'function') - process.exit(0) -else - process.exit(1) diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/build.sbt b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/build.sbt new file mode 100644 index 00000000..a36239a4 --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/build.sbt @@ -0,0 +1,7 @@ +import _root_.io.circe.syntax._ + +scalaVersion := "2.13.8" +enablePlugins(LambdaJSPlugin) +scalaJSUseMainModuleInitializer := false +scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule)) +npmPackageAdditionalNpmConfig := Map("type" -> "module".asJson) diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/build.properties b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/build.properties similarity index 100% rename from sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/build.properties rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/build.properties diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/plugins.sbt b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/plugins.sbt similarity index 100% rename from sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/project/plugins.sbt rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/plugins.sbt diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/src/main/scala/mySimpleHandler.scala similarity index 100% rename from sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/src/main/scala/mySimpleHandler.scala diff --git a/sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test similarity index 100% rename from sbt-lambda/src/sbt-test/lambda-js-export-plugin/iolambda-simple/test rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test-export.mjs b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test-export.mjs new file mode 100644 index 00000000..a31dd1cc --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test-export.mjs @@ -0,0 +1,5 @@ +if (typeof (await import('./target/scala-2.13/npm-package/index.js')).exportedHandler === 'function') { + process.exit(0); +} else { + process.exit(1); +} From eca166f9d5ef60e03a95ce3c221061ad44153a7e Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Mon, 1 Aug 2022 09:53:29 +0200 Subject: [PATCH 06/10] Update sbt-npm-package --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 4f0b88ad..1f88cced 100644 --- a/build.sbt +++ b/build.sbt @@ -127,7 +127,7 @@ lazy val sbtLambda = project name := "sbt-feral-lambda", crossScalaVersions := Seq(Scala212), addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion), - addSbtPlugin("io.chrisdavenport" %% "sbt-npm-package" % "0.1.0"), + addSbtPlugin("io.chrisdavenport" %% "sbt-npm-package" % "0.1.1"), buildInfoPackage := "feral.lambda.sbt", buildInfoKeys += organization, scriptedLaunchOpts := { From ffd412a35873c1802bfdabdc5c0d50823c0410ed Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Mon, 1 Aug 2022 09:53:43 +0200 Subject: [PATCH 07/10] Set tlBaseVersion to 0.2 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 1f88cced..64496f86 100644 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ name := "feral" -ThisBuild / tlBaseVersion := "0.1" +ThisBuild / tlBaseVersion := "0.2" ThisBuild / startYear := Some(2021) ThisBuild / developers := List( From d012d732c6f98d10164c8de894e430eaf6240da0 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Mon, 1 Aug 2022 09:54:17 +0200 Subject: [PATCH 08/10] Remove main and handlerName for JS lambda --- .../scala/feral/lambda/IOLambdaPlatform.scala | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala index 89ffea88..391db1ac 100644 --- a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -22,7 +22,6 @@ import io.circe.scalajs._ import scala.scalajs.js import scala.scalajs.js.JSConverters._ import scala.scalajs.js.| -import scala.util.Try private[lambda] trait IOLambdaPlatform[Event, Result] { this: IOLambda[Event, Result] => @@ -33,25 +32,12 @@ private[lambda] trait IOLambdaPlatform[Event, Result] { * * @example * {{{ + * @JSExportTopLevel("handler") * val handler: HandlerFn = handlerFn * }}} */ final type HandlerFn = js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] - final def main(args: Array[String]): Unit = { - // `exports` will throw a ReferenceError in ESModule (because `exports` comes from commonjs modules) - def isESModule = Try(js.Dynamic.global.exports).isFailure - - if (isESModule) { - throw new Error( - s"Cannot run ES Module with main module. Use `@JSExportTopLevel` or CommonJS instead. See https://github.com/typelevel/feral#es-modules") - } - - js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn) - } - - protected def handlerName: String = getClass.getSimpleName.init - final protected lazy val handlerFn : js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] = { (event: js.Any, context: facade.Context) => From acdfc25f10e4184a9dd238a1e0f4ab032463dd8e Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Mon, 1 Aug 2022 09:54:36 +0200 Subject: [PATCH 09/10] Update readme --- README.md | 55 ++++++++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index cfdd0a44..0d929822 100644 --- a/README.md +++ b/README.md @@ -23,43 +23,24 @@ Next, implement your Lambda. Please refer to the [examples](examples/src/main/sc There are several options to deploy your Lambda. For example you can use the [Lambda console](https://docs.aws.amazon.com/lambda/latest/dg/foundation-console.html), the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html), or the [serverless framework](https://www.serverless.com/framework/docs/providers/aws/guide/deploying). -### CommonJS - -To deploy a CommonJS Scala.js Lambda, you will need to know the following: +To deploy a Scala.js Lambda, you will need to know the following: 1. The runtime for your Lambda is Node.js 16. -2. The handler for your Lambda is `index.yourLambdaName`. - - `index` refers to the `index.js` file containing the JavaScript sources for your Lambda. - - `yourLambdaName` is the name of the Scala `object` you created that extends from `IOLambda`. -3. Run `sbt npmPackage` to package your Lambda for deployment. Note that you can currently only have one Lambda per sbt (sub-)project. If you have multiple, you will need to select the one to deploy using `Compile / mainClass := Some("my.lambda.handler")`. -4. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory. +2. Add a `@JSExportTopLevel` inside your lambda object: -### ES Modules + ```scala + @JSExportTopLevel("yourLambdaName") + val impl: HandlerFn = handlerFn + ``` -It's possible to use ES Modules to run your Lambda. You need the following steps instead of the above: + - The type `HandlerFn` is important so Scala.js will emit your lambda as a JavaScript function. + - `val` is important so your lambda function is emitted as a value containing a function, instead of a function returning another function. + - The handler for your Lambda is `index.yourLambdaName` + - `index` refers to the `index.js` file containing the JavaScript sources for your Lambda. + - `yourLambdaName` is the name of the export and can be changed if desired. -1. The runtime for your Lambda is Node.js 16. -2. Add a `@JSExportTopLevel` inside your lambda object: - ```scala - @JSExportTopLevel("yourLambdaName") - val impl: HandlerFn = handlerFn - ``` - - The type `HandlerFn` is important so Scala.JS will emit your lambda as a JavaScript function. - - `val` is important so your lambda function is emitted as a value, instead of a function returning another function. - - The handler for your lambda is `index.yourLambdaName` -3. Change these settings in your `build.sbt`: - - ```scala - import org.scalajs.linker.interface.OutputPatterns - import _root_.io.circe.syntax._ - - scalaJSUseMainModuleInitializer := false - scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule)) - npmPackageAdditionalNpmConfig := Map("type" -> "module".asJson) - ``` - -4. Run `sbt npmPackage` to package your Lambda for deployment. You can have multiple lambda's by exporting multiple handler functions. -5. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory. +3. Run `sbt npmPackage` to package your Lambda for deployment. You can have multiple lambda's per sbt project by exporting multiple handler functions. +4. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory. As the feral project develops, one of the goals is to provide an sbt plugin that simplifies and automates the deployment process. If this appeals to you, please contribute feature requests, ideas, and/or code! @@ -67,13 +48,13 @@ As the feral project develops, one of the goals is to provide an sbt plugin that The premise that you can (and should!) write production-ready serverless functions in Scala targeting JavaScript may be a surprising one. This project—and the rapid maturity of the Typelevel.js ecosystem—is motivated by three ideas. -1. **JavaScript is the ideal compile target for serverless functions.** - - There are a lot of reasons for this, cold-start being one of them, but more generally it's important to remember what the JVM is and is not good at. In particular, the JVM excels at long-lived multithreaded applications which are relatively memory-heavy and rely on medium-lifespan heap allocations. So in other words, persistent microservices. +1. **JavaScript is the ideal compile target for serverless functions.** + + There are a lot of reasons for this, cold-start being one of them, but more generally it's important to remember what the JVM is and is not good at. In particular, the JVM excels at long-lived multithreaded applications which are relatively memory-heavy and rely on medium-lifespan heap allocations. So in other words, persistent microservices. - Serverless functions are, by definition, not this. They are not persistent, they are (generally) single-threaded, and they need to start very quickly with minimal warming. They do often apply moderate-to-significant heap pressure, but this factor is more than outweighed by the others. + Serverless functions are, by definition, not this. They are not persistent, they are (generally) single-threaded, and they need to start very quickly with minimal warming. They do often apply moderate-to-significant heap pressure, but this factor is more than outweighed by the others. - V8 (the JavaScript engine in Node.js) is a very good runtime for these kinds of use-cases. Realistically, it may be the best-optimized runtime in existence for these requirements, similar to how the JVM is likely the best-optimized runtime in existence for the persistent microservices case. + V8 (the JavaScript engine in Node.js) is a very good runtime for these kinds of use-cases. Realistically, it may be the best-optimized runtime in existence for these requirements, similar to how the JVM is likely the best-optimized runtime in existence for the persistent microservices case. 2. **Scala.js and Cats Effect work together to provide powerful, well-defined semantics for writing JavaScript applications.** From 6e9bee9b2712f15e821d6df22566d64fa4e0013d Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Mon, 1 Aug 2022 10:04:54 +0200 Subject: [PATCH 10/10] update scripted tests --- .../src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala | 2 +- .../{iolambda-export => iolambda-es}/build.sbt | 2 -- .../{iolambda-export => iolambda-es}/project/build.properties | 0 .../{iolambda-export => iolambda-es}/project/plugins.sbt | 0 .../src/main/scala/mySimpleHandler.scala | 2 +- .../lambda-js-plugin/{iolambda-export => iolambda-es}/test | 0 .../{iolambda-export => iolambda-es}/test-export.mjs | 2 +- .../iolambda-simple/src/main/scala/mySimpleHandler.scala | 4 ++++ 8 files changed, 7 insertions(+), 5 deletions(-) rename sbt-lambda/src/sbt-test/lambda-js-plugin/{iolambda-export => iolambda-es}/build.sbt (58%) rename sbt-lambda/src/sbt-test/lambda-js-plugin/{iolambda-export => iolambda-es}/project/build.properties (100%) rename sbt-lambda/src/sbt-test/lambda-js-plugin/{iolambda-export => iolambda-es}/project/plugins.sbt (100%) rename sbt-lambda/src/sbt-test/lambda-js-plugin/{iolambda-export => iolambda-es}/src/main/scala/mySimpleHandler.scala (87%) rename sbt-lambda/src/sbt-test/lambda-js-plugin/{iolambda-export => iolambda-es}/test (100%) rename sbt-lambda/src/sbt-test/lambda-js-plugin/{iolambda-export => iolambda-es}/test-export.mjs (73%) diff --git a/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala b/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala index 8472329b..6ebc1fe7 100644 --- a/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala +++ b/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala @@ -38,7 +38,7 @@ object LambdaJSPlugin extends AutoPlugin { override def projectSettings: Seq[Setting[_]] = Seq( libraryDependencies += BuildInfo.organization %%% BuildInfo.name.drop(4) % BuildInfo.version, - scalaJSUseMainModuleInitializer := true, + scalaJSUseMainModuleInitializer := false, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), npmPackageOutputFilename := "index.js", npmPackageStage := Stage.FullOpt diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/build.sbt b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/build.sbt similarity index 58% rename from sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/build.sbt rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/build.sbt index a36239a4..40f4ad3b 100644 --- a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/build.sbt +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/build.sbt @@ -2,6 +2,4 @@ import _root_.io.circe.syntax._ scalaVersion := "2.13.8" enablePlugins(LambdaJSPlugin) -scalaJSUseMainModuleInitializer := false scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule)) -npmPackageAdditionalNpmConfig := Map("type" -> "module".asJson) diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/build.properties b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/build.properties similarity index 100% rename from sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/build.properties rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/build.properties diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/plugins.sbt b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/plugins.sbt similarity index 100% rename from sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/project/plugins.sbt rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/plugins.sbt diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/src/main/scala/mySimpleHandler.scala b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/src/main/scala/mySimpleHandler.scala similarity index 87% rename from sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/src/main/scala/mySimpleHandler.scala rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/src/main/scala/mySimpleHandler.scala index dc93deb0..358473d4 100644 --- a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/src/main/scala/mySimpleHandler.scala +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/src/main/scala/mySimpleHandler.scala @@ -5,6 +5,6 @@ import scala.scalajs.js.annotation._ object mySimpleHandler extends IOLambda.Simple[Unit, INothing] { def apply(event: Unit, context: Context[IO], init: Init): IO[Option[INothing]] = IO.none - @JSExportTopLevel("exportedHandler") + @JSExportTopLevel("mySimpleHandler") val impl: HandlerFn = handlerFn } diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test similarity index 100% rename from sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test-export.mjs b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test-export.mjs similarity index 73% rename from sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test-export.mjs rename to sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test-export.mjs index a31dd1cc..0a35dd1e 100644 --- a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test-export.mjs +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test-export.mjs @@ -1,4 +1,4 @@ -if (typeof (await import('./target/scala-2.13/npm-package/index.js')).exportedHandler === 'function') { +if (typeof (await import('./target/scala-2.13/npm-package/index.js')).mySimpleHandler === 'function') { process.exit(0); } else { process.exit(1); diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala index 16a54e88..358473d4 100644 --- a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala @@ -1,6 +1,10 @@ import cats.effect._ import feral.lambda._ +import scala.scalajs.js.annotation._ object mySimpleHandler extends IOLambda.Simple[Unit, INothing] { def apply(event: Unit, context: Context[IO], init: Init): IO[Option[INothing]] = IO.none + + @JSExportTopLevel("mySimpleHandler") + val impl: HandlerFn = handlerFn }