diff --git a/.gitignore b/.gitignore index 472442c..14fdab7 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,6 @@ GitHub.sublime-settings /.idea/ /outwatch-router/yarn.lock + +/.bsp +metals.sbt \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index 3557379..c02758e 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,3 +1,4 @@ +version = "2.7.4" style = default maxColumn = 100 diff --git a/build.sbt b/build.sbt index 06d4e5b..3308963 100644 --- a/build.sbt +++ b/build.sbt @@ -2,14 +2,9 @@ import xerial.sbt.Sonatype._ cancelable in Global := true -val compilerPlugins = Seq( - addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.2"), - addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) -) - val versions = new { - val scalatest = "3.1.0-SNAP11" - val outwatch = "676f94a" + val scalatest = "3.2.2" + val outwatch = "61deece8" } val commonSettings = Seq( @@ -18,8 +13,8 @@ val commonSettings = Seq( scalaVersion := Version.scalaVersion, scalacOptions ++= options.scalac, scalacOptions in (Compile, console) := options.scalacConsole, - updateOptions := updateOptions.value.withLatestSnapshots(false) -) ++ compilerPlugins + updateOptions := updateOptions.value.withLatestSnapshots(false), +) lazy val publishSettings = Seq( useGpg := true, @@ -29,15 +24,14 @@ lazy val publishSettings = Seq( homepage := Some(url("https://github.com/clovellytech/outwatch-router")), pomIncludeRepository := Function.const(false), sonatypeProfileName := "com.clovellytech", - // License of your choice licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT")), - // Where is the source code hosted - sonatypeProjectHosting := Some(GitHubHosting("clovellytech", "outwatch-router", "pattersonzak@gmail.com")) + sonatypeProjectHosting := Some( + GitHubHosting("clovellytech", "outwatch-router", "pattersonzak@gmail.com"), + ), ) - lazy val docs = project .in(file("./router-docs")) .settings(commonSettings) @@ -55,18 +49,18 @@ lazy val docs = project micrositeCompilingDocsTool := WithMdoc, micrositeGithubOwner := "clovellytech", micrositeGithubRepo := "outwatch-router", - scalacOptions := options.scalacConsole + scalacOptions := options.scalacConsole, ) .settings( mdocVariables := Map( - "VERSION" -> version.value - ) + "VERSION" -> version.value, + ), ) .dependsOn(router) lazy val copyFastOptJS = TaskKey[Unit]("copyFastOptJS", "Copy javascript files to target directory") -lazy val router = project +lazy val router = project .in(file("./outwatch-router")) .settings(name := "outwatch-router") .enablePlugins(ScalaJSPlugin) @@ -81,21 +75,29 @@ lazy val router = project webpackConfigFile in fastOptJS := Some(baseDirectory.value / "webpack.config.dev.js"), // https://scalacenter.github.io/scalajs-bundler/cookbook.html#performance webpackBundlingMode in fastOptJS := BundlingMode.LibraryOnly(), - resolvers += "jitpack" at "https://jitpack.io", + resolvers += "jitpack".at("https://jitpack.io"), libraryDependencies ++= Seq( - "io.github.outwatch" % "outwatch" % versions.outwatch, + "com.github.outwatch.outwatch" %%% "outwatch" % versions.outwatch, + "com.github.outwatch.outwatch" %%% "outwatch-util" % versions.outwatch, "org.scalatest" %%% "scalatest" % versions.scalatest % Test, ), copyFastOptJS := { val inDir = (crossTarget in (Compile, fastOptJS)).value val outDir = (crossTarget in (Compile, fastOptJS)).value / "dev" - val files = Seq("outwatch-router-fastopt-loader.js", "outwatch-router-frontend-fastopt.js", "outwatch-router-frontend-fastopt.js.map") map { p => (inDir / p, outDir / p) } + val files = Seq( + "outwatch-router-fastopt-loader.js", + "outwatch-router-frontend-fastopt.js", + "outwatch-router-frontend-fastopt.js.map", + ).map(p => (inDir / p, outDir / p)) IO.copy(files, overwrite = true, preserveLastModified = true, preserveExecutable = true) }, // hot reloading configuration: // https://github.com/scalacenter/scalajs-bundler/issues/180 - addCommandAlias("dev", "; compile; fastOptJS::startWebpackDevServer; devwatch; fastOptJS::stopWebpackDevServer"), - addCommandAlias("devwatch", "~; fastOptJS; copyFastOptJS") + addCommandAlias( + "dev", + "; compile; fastOptJS::startWebpackDevServer; devwatch; fastOptJS::stopWebpackDevServer", + ), + addCommandAlias("devwatch", "~; fastOptJS; copyFastOptJS"), ) .settings(publishSettings) @@ -108,5 +110,3 @@ lazy val root = project ) .dependsOn(router) .aggregate(router) - - diff --git a/outwatch-router/src/main/scala/outwatch/router/Router.scala b/outwatch-router/src/main/scala/outwatch/router/Router.scala index bbe077e..c072b03 100644 --- a/outwatch-router/src/main/scala/outwatch/router/Router.scala +++ b/outwatch-router/src/main/scala/outwatch/router/Router.scala @@ -1,60 +1,85 @@ package outwatch.router -import cats.effect.IO -import monix.execution.Scheduler +import cats.effect.Sync +import colibri.Observable import org.scalajs.dom.window -import outwatch.dom._ -import monix.reactive.Observable +import outwatch._ +import outwatch.dsl._ +import outwatch.util.Reducer import outwatch.util.Store sealed trait Action final case class Replace(path: Path) extends Action +final case class HistoryEvent(path: Path) extends Action final case class RouterState[P](page: P) -/** - * An AppRouter handles parsing of URLs and mapping to pages of the given type. +/** An AppRouter handles parsing of URLs and mapping to pages of the given type. * @param siteRoot - The prefix part of a pathname, or the subpath at which your site is applied. * Usually this is just Root, but your site might need a prefix as in /my_site/[parsed pathname] * @param parent - The parent path at which this router is mounted. You can have routers contained in subroots of your site. * @param f - a mapping function from a Path to a page P. + * @tparam F - the effect type * @tparam P - Your page type, such as a sealed trait root type. */ -class AppRouter[P](siteRoot: Path, parent: Path, f: Path => P) { +class AppRouter[F[_]: Sync, P](siteRoot: Path, parent: Path, f: Path => P) { // Sync from the required page to the window.location - def routerReducer(state: RouterState[P], action: Action): RouterState[P] = action match { - case Replace(path) => - window.history.pushState("", "", Path(siteRoot, Path(parent, path)).toUrlString) - state.copy(page = f(path)) - case _ => state - } + def routerReducer(state: RouterState[P], action: Action): RouterState[P] = + action match { + case Replace(path) => + window.history.pushState("", "", Path(siteRoot, Path(parent, path)).toUrlString) + state.copy(page = f(path)) + case HistoryEvent(path) => + state.copy(page = f(path)) + case _ => state + } - def store(implicit S : Scheduler): IO[RouterStore[P]] = { + def store: F[RouterStore[P]] = { val startingPath = Path(window.location.pathname) - Store.create[Action, RouterState[P]]( + Store.create[F, Action, RouterState[P]]( Replace(startingPath), RouterState(f(startingPath)), - Store.Reducer.justState(routerReducer _) + Reducer(routerReducer _), ) } + + def link(linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode = + a(href := linkHref)( + onClick.preventDefault.useLazy(Replace(Path(linkHref))) --> store.sink, + attrs, + ) + + def render(resolver: RouterResolve[P])(implicit store: RouterStore[P]): Observable[VDomModifier] = + store.map { case (_, RouterState(p)) => resolver(p) } + + def watch()(implicit store: RouterStore[P]) = + emitter(outwatch.dsl.events.window.onPopState) + .useLazy(HistoryEvent(Path(org.scalajs.dom.window.location.pathname))) --> store.sink + } -object AppRouter{ - def render[P](resolver: RouterResolve[P])(implicit store: RouterStore[P]): Observable[VDomModifier] = - store.map{ case (_, RouterState(p)) => resolver(p) } +object AppRouter { + def render[P](resolver: RouterResolve[P])(implicit + store: RouterStore[P], + ): Observable[VDomModifier] = store.map { case (_, RouterState(p)) => resolver(p) } + + def watch[P]()(implicit store: RouterStore[P]) = emitter( + outwatch.dsl.events.window.onPopState, + ).useLazy(HistoryEvent(Path(org.scalajs.dom.window.location.pathname))) --> store.sink - def create[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = - create[P](Root, notFound)(f) + def create[F[_]: Sync, P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[F, P] = + create[F, P](Root, notFound)(f) - def create[P](parent: Path, notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = - new AppRouter[P](Root, parent, f.lift.andThen(_.getOrElse(notFound))) + def create[F[_]: Sync, P](parent: Path, notFound: P)( + f: PartialFunction[Path, P], + ): AppRouter[F, P] = new AppRouter[F, P](Root, parent, f.lift.andThen(_.getOrElse(notFound))) - def createParseSiteRoot[P](notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = - createParseSiteRoot[P](Root, notFound)(f) + def createParseSiteRoot[F[_]: Sync, P](notFound: P)( + f: PartialFunction[Path, P], + ): AppRouter[F, P] = createParseSiteRoot[F, P](Root, notFound)(f) - /** - * Automatically determine what siteroot we're using, based on the current URL and expected parent. + /** Automatically determine what siteroot we're using, based on the current URL and expected parent. * For example, your site could be deployed at /example/directory/, your router root path could be /names, * and the current url could be /example/directory/names/alice. So given the call: * createParseSubRoot[Page](Path("/names"), NotFound)(f), the router will work out that the window location @@ -64,7 +89,9 @@ object AppRouter{ * @param f - a router function from Path to instances of your page type * @tparam P - your page type */ - def createParseSiteRoot[P](parent: Path, notFound: P)(f: PartialFunction[Path, P]): AppRouter[P] = { + def createParseSiteRoot[F[_]: Sync, P](parent: Path, notFound: P)( + f: PartialFunction[Path, P], + ): AppRouter[F, P] = { val initUrl = window.location.pathname // url is of form /sra/srb/src/pa/pb/pc... // so just drop the parent part from the right of the url if it exists. @@ -75,6 +102,6 @@ object AppRouter{ } val routerFun: Path => P = f.lift.andThen(_.getOrElse(notFound)) - new AppRouter[P](siteRoot, parent, routerFun) + new AppRouter[F, P](siteRoot, parent, routerFun) } } diff --git a/outwatch-router/src/main/scala/outwatch/router/dsl/C.scala b/outwatch-router/src/main/scala/outwatch/router/dsl/C.scala index 73e15df..930f59c 100644 --- a/outwatch-router/src/main/scala/outwatch/router/dsl/C.scala +++ b/outwatch-router/src/main/scala/outwatch/router/dsl/C.scala @@ -1,13 +1,13 @@ package outwatch.router package dsl -import outwatch.dom.VDomModifier -import outwatch.dom.{dsl => O, _} +import outwatch._ +import outwatch.dsl._ object C { - def a[P](linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode = - O.a( - O.href := linkHref, - O.onClick.preventDefault.mapTo(Replace(Path(linkHref))) --> store - )(attrs) + def link[P](linkHref: String)(attrs: VDomModifier*)(implicit store: RouterStore[P]): BasicVNode = + a(href := linkHref)( + onClick.preventDefault.useLazy(Replace(Path(linkHref))) --> store.sink, + attrs, + ) } diff --git a/outwatch-router/src/main/scala/outwatch/router/package.scala b/outwatch-router/src/main/scala/outwatch/router/package.scala index 1087418..652ff66 100644 --- a/outwatch-router/src/main/scala/outwatch/router/package.scala +++ b/outwatch-router/src/main/scala/outwatch/router/package.scala @@ -1,9 +1,10 @@ package outwatch -import outwatch.dom.VDomModifier +import outwatch._ +import colibri.ProSubject package object router { - type RouterStore[P] = ProHandler[Action, (Action, RouterState[P])] + type RouterStore[P] = ProSubject[Action, (Action, RouterState[P])] type RouterResolve[P] = PartialFunction[P, VDomModifier] } diff --git a/outwatch-router/src/test/scala/outwatch/router/AppRouterTestSpec.scala b/outwatch-router/src/test/scala/outwatch/router/AppRouterTestSpec.scala index e0092e2..73796cd 100644 --- a/outwatch-router/src/test/scala/outwatch/router/AppRouterTestSpec.scala +++ b/outwatch-router/src/test/scala/outwatch/router/AppRouterTestSpec.scala @@ -1,15 +1,14 @@ package outwatch package router -import org.scalatest._ import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers class PathTestSpec extends AnyFlatSpec with Matchers { - class PathTest(url: String, path: Path){ - path.toUrlString should equal (url) + class PathTest(url: String, path: Path) { + path.toUrlString should equal(url) } "Root url" should "be /" in new PathTest("/", Root) "A 1 part path" should "be correct" in new PathTest("/search", Root / "search") } - diff --git a/project/Options.scala b/project/Options.scala index 46016de..64cf7f8 100644 --- a/project/Options.scala +++ b/project/Options.scala @@ -14,16 +14,13 @@ object options { "-unchecked", // Enable additional warnings where generated code depends on assumptions. "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. "-Xfatal-warnings", // Fail the compilation if there are any warnings. - "-Xfuture", // Turn on future language features. "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. - "-Xlint:by-name-right-associative", // By-name parameter of right associative operator. "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. "-Xlint:delayedinit-select", // Selecting member of DelayedInit. "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`. "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. - "-Xlint:nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. "-Xlint:nullary-unit", // Warn when nullary methods return Unit. "-Xlint:option-implicit", // Option.apply used implicit view. "-Xlint:package-object-classes", // Class or object defined in package object. @@ -31,15 +28,8 @@ object options { "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. - "-Xlint:unsound-match", // Pattern match may not be typesafe. - "-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. - "-Ypartial-unification", // Enable partial unification in type constructor inference "-Ywarn-dead-code", // Warn when dead code is identified. "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. - "-Ywarn-inaccessible", // Warn about inaccessible types in method signatures. - "-Ywarn-infer-any", // Warn when a type argument is inferred to be `Any`. - "-Ywarn-nullary-unit", // Warn when nullary methods return Unit. - "-Ywarn-nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. "-Ywarn-numeric-widen", // Warn when numerics are widened. "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. "-Ywarn-unused:imports", // Warn if an import selector is not referenced. diff --git a/project/Version.scala b/project/Version.scala index ce5614f..ea1a6bb 100644 --- a/project/Version.scala +++ b/project/Version.scala @@ -1,4 +1,4 @@ -object Version{ - val version = "0.0.9" - val scalaVersion = "2.12.8" +object Version { + val version = "0.0.10" + val scalaVersion = "2.13.4" } diff --git a/project/build.properties b/project/build.properties index c9f2946..947bdd3 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.0-RC2 +sbt.version=1.4.3 diff --git a/project/plugins.sbt b/project/plugins.sbt index 2961453..4e739e8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") -addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.15.0-0.6") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.2.8" ) -addSbtPlugin("com.47deg" % "sbt-microsites" % "0.8.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.1.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.2.8") +addSbtPlugin("com.47deg" % "sbt-microsites" % "1.1.2") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2")