Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8a4048c
feat!: work on webkit compatibility with scala3
farmdawgnation Oct 8, 2025
b68c4d5
feat(lift-webkit)!: add Scala 3.3.6 compatibility (99% complete)
farmdawgnation Oct 9, 2025
7f7bc8b
fix(lift-webkit): fix some type headaches
farmdawgnation Oct 9, 2025
4d0baa9
fix(lift-webkit): fix type inference differences in scala3
farmdawgnation Oct 10, 2025
ee3671c
fix(lift-webkit): add parens to finalize for cross compile
farmdawgnation Oct 10, 2025
d5d67e2
fix(lift-webkit)!: remove structural reflection in liftservlet
farmdawgnation Oct 10, 2025
5cb6c2d
fix(lift-webkit): fix all Scala 3 test compilation errors
farmdawgnation Oct 13, 2025
1728c8e
refactor(lift-webkit): create Scala version-specific test files
farmdawgnation Oct 14, 2025
6b5e9ef
fix(lift-webkit): fix test context initialization for Scala 2.13 and …
farmdawgnation Nov 24, 2025
5f3a6bd
fix(lift-webkit): fix BeforeEach test isolation for Scala 2.13 and 3.3.6
farmdawgnation Nov 25, 2025
cb52022
fix(lift-webkit): fix SnippetSpec test failures for Scala 2.13 and 3.3.6
farmdawgnation Nov 25, 2025
d9ca023
fix(lift-webkit): fix MemoizeSpec for Scala 2.13 and 3.3.6
farmdawgnation Nov 25, 2025
ce77ac9
fix(lift-webkit): fix WebSpecSpec for Scala 2.13 and 3.3.6
farmdawgnation Nov 25, 2025
e962a8d
fix(lift-webkit): various box equality issues
farmdawgnation Nov 25, 2025
7186a49
fix(lift-webkit): more box equality issues
farmdawgnation Nov 25, 2025
c8d32e4
fix(lift-webkit): create version-specific tests for Box equality
farmdawgnation Nov 25, 2025
2fc536b
fix(lift-webkit): fix Scala 3 snippet instantiation with type-safe re…
farmdawgnation Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ val scala3LTSVersion = "3.3.6"
ThisBuild / scalaVersion := scala213Version
ThisBuild / crossScalaVersions := Seq(scala213Version, scala3LTSVersion)

ThisBuild / libraryDependencies ++= Seq(specs2, specs2Matchers, scalacheck, scalactic, scalatest)
ThisBuild / libraryDependencies ++= Seq(
specs2(scalaVersion.value),
specs2Matchers(scalaVersion.value),
scalacheck(scalaVersion.value),
scalactic,
scalatest
)

ThisBuild / scalacOptions ++= Seq("-deprecation")

Expand Down Expand Up @@ -124,16 +130,16 @@ lazy val webkit =
commons_fileupload,
rhino,
servlet_api,
specs2Prov,
specs2MatchersProv,
specs2Prov(scalaVersion.value),
specs2MatchersProv(scalaVersion.value),
jetty11,
jettywebapp,
jwebunit,
mockito_scalatest,
mockito_scalatest(scalaVersion.value),
jquery,
jasmineCore,
jasmineAjax,
specs2Mock
specs2Mock(scalaVersion.value)
),
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
Expand Down
35 changes: 28 additions & 7 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,38 @@ object Dependencies {
lazy val derby = "org.apache.derby" % "derby" % "10.7.1.1" % Test
lazy val h2database = "com.h2database" % "h2" % "1.2.147" % Test

lazy val specs2 = "org.specs2" %% "specs2-core" % "4.21.0" % Test
lazy val scalacheck = "org.specs2" %% "specs2-scalacheck" % specs2.revision % Test
lazy val specs2Prov = "org.specs2" %% "specs2-core" % specs2.revision % Provided
lazy val specs2Matchers = "org.specs2" %% "specs2-matcher-extra" % specs2.revision % Test
lazy val specs2MatchersProv = "org.specs2" %% "specs2-matcher-extra" % specs2.revision % Provided
lazy val specs2Mock = "org.specs2" %% "specs2-mock" % specs2.revision % Test
// Specs2 versions differ between Scala 2 and Scala 3
def specs2Version(scalaVersion: String): String = {
CrossVersion.partialVersion(scalaVersion) match {
case Some((2, 13)) => "4.21.0"
case Some((3, _)) => "5.6.4"
case _ => "4.21.0"
}
}

lazy val specs2: ModuleMap = (version: String) => "org.specs2" %% "specs2-core" % specs2Version(version) % Test
lazy val scalacheck: ModuleMap = (version: String) => "org.specs2" %% "specs2-scalacheck" % specs2Version(version) % Test
lazy val specs2Prov: ModuleMap = (version: String) => "org.specs2" %% "specs2-core" % specs2Version(version) % Provided
lazy val specs2Matchers: ModuleMap = (version: String) => "org.specs2" %% "specs2-matcher-extra" % specs2Version(version) % Test
lazy val specs2MatchersProv: ModuleMap = (version: String) => "org.specs2" %% "specs2-matcher-extra" % specs2Version(version) % Provided
lazy val specs2Mock: ModuleMap = (version: String) => {
CrossVersion.partialVersion(version) match {
case Some((2, 13)) => "org.specs2" %% "specs2-mock" % specs2Version(version) % Test
case Some((3, _)) => "org.scalatestplus" %% "mockito-5-18" % "3.2.19.0" % Test
case _ => "org.specs2" %% "specs2-mock" % specs2Version(version) % Test
}
}

lazy val scalactic = "org.scalactic" %% "scalactic" % "3.2.19" % Test
lazy val scalatest = "org.scalatest" %% "scalatest" % "3.2.19" % Test
lazy val scalatest_junit = "org.scalatestplus" %% "junit-4-12" % "3.1.2.0" % Test
lazy val mockito_scalatest = "org.mockito" %% "mockito-scala-scalatest" % "1.14.3" % Test
lazy val mockito_scalatest: ModuleMap = (version: String) => {
CrossVersion.partialVersion(version) match {
case Some((2, 13)) => "org.mockito" %% "mockito-scala-scalatest" % "1.14.3" % Test
case Some((3, _)) => "org.scalatestplus" %% "mockito-5-18" % "3.2.19.0" % Test
case _ => "org.mockito" %% "mockito-scala-scalatest" % "1.14.3" % Test
}
}

lazy val scalamock = "org.scalamock" %% "scalamock" % "7.4.1" % Test

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,6 @@ object Menu extends DispatchSnippet {
for {
name <- S.attr("name").toList
} yield {
type T = Q forSome { type Q }

// Builds a link for the given loc
def buildLink[T](loc : Loc[T]) = {
Group(SiteMap.buildLink(name, text) match {
Expand All @@ -441,8 +439,8 @@ object Menu extends DispatchSnippet {
}

(S.originalRequest.flatMap(_.location), S.attr("param"), SiteMap.findAndTestLoc(name)) match {
case (_, Full(param), Full(loc: Loc[_] with ConvertableLoc[_])) => {
val typedLoc = loc.asInstanceOf[Loc[T] with ConvertableLoc[T]]
case (_, Full(param), Full(loc: Loc[t] with ConvertableLoc[_])) => {
val typedLoc = loc.asInstanceOf[Loc[t] with ConvertableLoc[t]]

(for {
pv <- typedLoc.convert(param)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ object Head extends DispatchSnippet {
case e: Elem if (null eq e.prefix) => NodeSeq.Empty
case x => x
}

val xhtml = validHeadTagsOnly(_xhtml)

<head>{
val xhtml = validHeadTagsOnly(_xhtml)

<head>{
if ((S.attr("withResourceId") or S.attr("withresourceid")).filter(Helpers.toBoolean).isDefined) {
WithResourceId.render(xhtml)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,7 @@ trait BaseCometActor extends LiftActor with LiftCometActor with CssBindImplicits
Empty
}

new RenderOut(Full(in: NodeSeq), internalFixedRender, additionalJs, Empty, false)
new RenderOut(Full(in: NodeSeq), internalFixedRender, additionalJs, Empty, false)
}

protected implicit def jsToXmlOrJsCmd(in: JsCmd): RenderOut = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ object ContentParser {
* @return your parser wrapped up to handle an `InputStream`
*/
def toInputStreamParser(simpleParser: String=>Box[NodeSeq]): InputStream=>Box[NodeSeq] = {
is:InputStream =>
(is: InputStream) =>
for {
bytes <- Helpers.tryo(Helpers.readWholeStream(is))
elems <- simpleParser(new String(bytes, "UTF-8"))
Expand Down
106 changes: 53 additions & 53 deletions web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala
Original file line number Diff line number Diff line change
Expand Up @@ -160,59 +160,59 @@ private[http] trait LiftMerge {
startingState.copy(headChild = false, headInBodyChild = false, tailInBodyChild = false, bodyChild = false)
}

val bodyHead = childInfo.headInBodyChild && ! headInBodyChild
val bodyTail = childInfo.tailInBodyChild && ! tailInBodyChild

HtmlNormalizer
.normalizeNode(node, contextPath, stripComments, LiftRules.extractInlineJavaScript)
.map {
case normalized @ NodeAndEventJs(normalizedElement: Elem, _) =>
val normalizedChildren =
normalizeMergeAndExtractEvents(normalizedElement.child, childInfo)

normalized.copy(
normalizedElement.copy(child = normalizedChildren.nodes),
js = normalized.js & normalizedChildren.js
)

case other =>
other
}
.map { normalizedResults: NodeAndEventJs =>
node match {
case e: Elem if e.label == "node" &&
e.prefix == "lift_deferred" =>
val deferredNodes: Seq[NodesAndEventJs] = {
for {
idAttribute <- e.attributes("id").take(1)
id = idAttribute.text
nodes <- processedSnippets.get(id)
} yield {
normalizeMergeAndExtractEvents(nodes, startingState)
}}.toSeq

deferredNodes.foldLeft(soFar.append(normalizedResults))(_ append _)

case _ =>
if (headChild) {
headChildren ++= normalizedResults.node
} else if (headInBodyChild) {
addlHead ++= normalizedResults.node
} else if (tailInBodyChild) {
addlTail ++= normalizedResults.node
} else if (_bodyChild && ! bodyHead && ! bodyTail) {
bodyChildren ++= normalizedResults.node
}

if (bodyHead || bodyTail) {
soFar.append(normalizedResults.js)
} else {
soFar.append(normalizedResults)
}
}
} getOrElse {
soFar
val bodyHead = childInfo.headInBodyChild && ! headInBodyChild
val bodyTail = childInfo.tailInBodyChild && ! tailInBodyChild

HtmlNormalizer
.normalizeNode(node, contextPath, stripComments, LiftRules.extractInlineJavaScript)
.map {
case normalized @ NodeAndEventJs(normalizedElement: Elem, _) =>
val normalizedChildren =
normalizeMergeAndExtractEvents(normalizedElement.child, childInfo)

normalized.copy(
normalizedElement.copy(child = normalizedChildren.nodes),
js = normalized.js & normalizedChildren.js
)

case other =>
other
}
.map { (normalizedResults: NodeAndEventJs) =>
node match {
case e: Elem if e.label == "node" &&
e.prefix == "lift_deferred" =>
val deferredNodes: Seq[NodesAndEventJs] = {
for {
idAttribute <- e.attributes("id").take(1)
id = idAttribute.text
nodes <- processedSnippets.get(id)
} yield {
normalizeMergeAndExtractEvents(nodes, startingState)
}}.toSeq

deferredNodes.foldLeft(soFar.append(normalizedResults))(_ append _)

case _ =>
if (headChild) {
headChildren ++= normalizedResults.node
} else if (headInBodyChild) {
addlHead ++= normalizedResults.node
} else if (tailInBodyChild) {
addlTail ++= normalizedResults.node
} else if (_bodyChild && ! bodyHead && ! bodyTail) {
bodyChildren ++= normalizedResults.node
}

if (bodyHead || bodyTail) {
soFar.append(normalizedResults.js)
} else {
soFar.append(normalizedResults)
}
}
} getOrElse {
soFar
}
}
}

Expand Down Expand Up @@ -248,7 +248,7 @@ private[http] trait LiftMerge {
}

// Appends ajax script to body
if (LiftRules.autoIncludeAjaxCalc.vend().apply(this)) {
if (LiftRules.autoIncludeAjaxCalc.vend()(this)) {
bodyChildren +=
<script src={S.encodeURL(contextPath + "/"+LiftRules.resourceServerPath+"/lift.js")}
type="text/javascript"/>
Expand Down
4 changes: 2 additions & 2 deletions web/webkit/src/main/scala/net/liftweb/http/LiftResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import js._
import net.liftweb.util.Helpers._
import org.json4s._
import org.json4s.native._
import java.io.{OutputStream, OutputStreamWriter, Writer, ByteArrayOutputStream}
import java.io.{InputStream, OutputStream, OutputStreamWriter, Writer, ByteArrayOutputStream}

/**
* 200 response but without body.
Expand Down Expand Up @@ -361,7 +361,7 @@ final case class InMemoryResponse(data: Array[Byte], headers: List[(String, Stri
override def toString = "InMemoryResponse(" + (new String(data, "UTF-8")) + ", " + headers + ", " + cookies + ", " + code + ")"
}

final case class StreamingResponse(data: {def read(buf: Array[Byte]): Int}, onEnd: () => Unit, size: Long, headers: List[(String, String)], cookies: List[HTTPCookie], code: Int) extends BasicResponse {
final case class StreamingResponse(data: InputStream, onEnd: () => Unit, size: Long, headers: List[(String, String)], cookies: List[HTTPCookie], code: Int) extends BasicResponse {
def toResponse = this

override def toString = "StreamingResponse( steaming_data , " + headers + ", " + cookies + ", " + code + ")"
Expand Down
32 changes: 16 additions & 16 deletions web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
* sure to set them early in the boot process.
*/
@volatile var securityRules: () => SecurityRules = () => defaultSecurityRules
private[http] lazy val lockedSecurityRules = securityRules()
lazy val lockedSecurityRules = securityRules()

/**
* Defines the resources that are protected by authentication and authorization. If this function
Expand Down Expand Up @@ -463,10 +463,11 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
val statelessReqTest = RulesSeq[StatelessReqTestPF]

val statelessSession: FactoryMaker[Req => LiftSession with StatelessSession] =
new FactoryMaker((req: Req) => new LiftSession(req.contextPath,
Helpers.nextFuncName,
Empty) with
StatelessSession) {}
new FactoryMaker((req: Req) =>
new LiftSession(req.contextPath,
Helpers.nextFuncName,
Empty) with StatelessSession
) {}


/**
Expand Down Expand Up @@ -538,7 +539,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
/**
* For each unload hook registered, run them during destroy()
*/
private[http] def runUnloadHooks(): Unit = {
def runUnloadHooks(): Unit = {
unloadHooks.toList.foreach{f =>
tryo{f()}
}
Expand Down Expand Up @@ -671,7 +672,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
*/
@volatile var siteMapFailRedirectLocation: List[String] = List()

private[http] def notFoundOrIgnore(requestState: Req, session: Box[LiftSession]): Box[LiftResponse] = {
def notFoundOrIgnore(requestState: Req, session: Box[LiftSession]): Box[LiftResponse] = {
if (passNotFoundToChain) Empty
else session match {
case Full(session) => Full(session.checkRedirect(requestState.createNotFound))
Expand Down Expand Up @@ -920,7 +921,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
*/
val viewDispatch = RulesSeq[ViewDispatchPF]

private[http] def snippet(name: String): Box[DispatchSnippet] = NamedPF.applyBox(name, snippetDispatch.toList)
def snippet(name: String): Box[DispatchSnippet] = NamedPF.applyBox(name, snippetDispatch.toList)

/**
* If the request times out (or returns a non-Response) you can
Expand Down Expand Up @@ -1243,7 +1244,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
*/
val deferredSnippetFailure: FactoryMaker[Failure => NodeSeq] =
new FactoryMaker(() => {
failure: Failure => {
(failure: Failure) => {
if (Props.devMode)
<div style="border: red solid 2px">A lift:parallel snippet failed to render.Message:{failure.msg}{failure.exception match {
case Full(e) =>
Expand Down Expand Up @@ -1292,11 +1293,10 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {



private[http] val reqCnt = new AtomicInteger(0)
@volatile var ending = false
val reqCnt = new AtomicInteger(0)

@volatile private[http] var ending = false

private[http] def bootFinished(): Unit = {
def bootFinished(): Unit = {
_doneBoot = true
}

Expand All @@ -1323,7 +1323,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
val allAround = RulesSeq[LoanWrapper]


private[http] def dispatchTable(req: HTTPRequest): List[DispatchPF] = {
def dispatchTable(req: HTTPRequest): List[DispatchPF] = {
req match {
case null => dispatch.toList
case _ => SessionMaster.getSession(req, Empty) match {
Expand Down Expand Up @@ -1616,7 +1616,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
* Runs responseTransformers
*/
def performTransform(in: LiftResponse): LiftResponse = responseTransformers.toList.foldLeft(in) {
case (in, pf: PartialFunction[_, _]) =>
case (in, pf: PartialFunction[LiftResponse, LiftResponse]) =>
if (pf.isDefinedAt(in)) pf(in) else in
case (in, f) => f(in)
}
Expand Down Expand Up @@ -1975,7 +1975,7 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
*/
def mimeHeaders = _mimeHeaders.get

private[http] def withMimeHeaders[T](map: Map[String, List[String]])(f: => T): T = _mimeHeaders.doWith(Full(map))(f)
def withMimeHeaders[T](map: Map[String, List[String]])(f: => T): T = _mimeHeaders.doWith(Full(map))(f)

@volatile var templateCache: Box[TemplateCache[(Locale, List[String]), NodeSeq]] = {
if (Props.productionMode) {
Expand Down
4 changes: 2 additions & 2 deletions web/webkit/src/main/scala/net/liftweb/http/LiftScreen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,7 @@ trait ScreenWizardRendered extends Loggable {
}
}

val myNotices = notices.filter(fi => fi._3.isDefined && fi._3 == curId)
val myNotices = notices.filter(fi => fi._3.isDefined && fi._3.contains(curId))

def bindLabel(): CssBindFunc = {
val basicLabel = sel(_.label, ".%s [for]") #> curId & nsSetChildren(_.label, f.text ++ labelSuffix)
Expand Down Expand Up @@ -1501,7 +1501,7 @@ trait LiftScreen extends AbstractScreen with StatefulSnippet with ScreenWizardRe
override protected def __nameSalt = randomString(20)
}

protected def createSnapshot = {
protected def createSnapshot: ScreenSnapshot = {
val prev = PrevSnapshot.get
new ScreenSnapshot(ScreenVars.get, prev)
}
Expand Down
Loading