diff --git a/modules/cli/src/main/scala/scala/cli/commands/CommandUtils.scala b/modules/cli/src/main/scala/scala/cli/commands/CommandUtils.scala index 797f149986..6375f3b064 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/CommandUtils.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/CommandUtils.scala @@ -36,4 +36,8 @@ object CommandUtils { .getOrElse(programName) lazy val shouldCheckUpdate: Boolean = scala.util.Random.nextInt() % 10 == 1 + + def printablePath(path: os.Path): String = + if (path.startsWith(Os.pwd)) "." + File.separator + path.relativeTo(Os.pwd).toString + else path.toString } diff --git a/modules/cli/src/main/scala/scala/cli/commands/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/Package.scala index 0ec7013b0b..3ee29f87ae 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Package.scala @@ -186,10 +186,8 @@ object Package extends ScalaCommand[PackageOptions] { .map(_ + extension) } .getOrElse(defaultName) - val destPath = os.Path(dest, Os.pwd) - val printableDest = - if (destPath.startsWith(Os.pwd)) "." + File.separator + destPath.relativeTo(Os.pwd).toString - else destPath.toString + val destPath = os.Path(dest, Os.pwd) + val printableDest = CommandUtils.printablePath(destPath) def alreadyExistsCheck(): Unit = { val alreadyExists = !force && @@ -214,36 +212,43 @@ object Package extends ScalaCommand[PackageOptions] { val packageOptions = build.options.notForBloopOptions.packageOptions - packageType match { + val outputPath = packageType match { case PackageType.Bootstrap => bootstrap(build, destPath, value(mainClass), () => alreadyExistsCheck()) + destPath case PackageType.LibraryJar => val content = Library.libraryJar(build) alreadyExistsCheck() if (force) os.write.over(destPath, content) else os.write(destPath, content) + destPath case PackageType.SourceJar => val now = System.currentTimeMillis() val content = sourceJar(build, now) alreadyExistsCheck() if (force) os.write.over(destPath, content) else os.write(destPath, content) + destPath case PackageType.DocJar => val content = value(docJar(build, logger, extraArgs)) alreadyExistsCheck() if (force) os.write.over(destPath, content) else os.write(destPath, content) + destPath case PackageType.Assembly => assembly(build, destPath, value(mainClass), () => alreadyExistsCheck()) + destPath case PackageType.Js => value(buildJs(build, destPath, value(mainClass), logger)) case PackageType.Native => buildNative(build, destPath, value(mainClass), logger) + destPath case PackageType.GraalVMNativeImage => buildGraalVMNativeImage(build, destPath, value(mainClass), extraArgs, logger) + destPath case nativePackagerType: PackageType.NativePackagerType => val bootstrapPath = os.temp.dir(prefix = "scala-packager") / "app" @@ -319,20 +324,24 @@ object Package extends ScalaCommand[PackageOptions] { case PackageType.Msi => WindowsPackage(windowsSettings).build() } + destPath case PackageType.Docker => docker(build, value(mainClass), logger) + destPath } + val printableOutput = CommandUtils.printablePath(outputPath) + if (packageType.runnable.nonEmpty) logger.message { if (packageType.runnable.contains(true)) - s"Wrote $dest, run it with" + System.lineSeparator() + - " " + printableDest + s"Wrote $outputPath, run it with" + System.lineSeparator() + + " " + printableOutput else if (packageType == PackageType.Js) - s"Wrote $dest, run it with" + System.lineSeparator() + - " node " + printableDest + s"Wrote $outputPath, run it with" + System.lineSeparator() + + " node " + printableOutput else - s"Wrote $dest" + s"Wrote $outputPath" } val mTimeDestPathOpt = if (packageType.runnable.isEmpty) None else Some(os.mtime(destPath)) @@ -525,7 +534,7 @@ object Package extends ScalaCommand[PackageOptions] { destPath: os.Path, mainClass: String, logger: Logger - ): Either[BuildException, Unit] = { + ): Either[BuildException, os.Path] = { val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) linkJs( build, @@ -672,7 +681,7 @@ object Package extends ScalaCommand[PackageOptions] { fullOpt: Boolean, noOpt: Boolean, logger: Logger - ): Either[BuildException, Unit] = + ): Either[BuildException, os.Path] = Library.withLibraryJar(build, dest.last.toString.stripSuffix(".jar")) { mainJar => val classPath = os.Path(mainJar, os.pwd) +: build.artifacts.classPath val linkingDir = os.temp.dir(prefix = "scala-cli-js-linking") @@ -698,16 +707,37 @@ object Package extends ScalaCommand[PackageOptions] { val relSourceMapJs = os.rel / "main.js.map" val mainJs = linkingDir / relMainJs val sourceMapJs = linkingDir / relSourceMapJs - if (os.exists(mainJs)) { - os.copy(mainJs, dest, replaceExisting = true) - if (build.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) { - val sourceMapDest = - build.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map")) - os.copy(sourceMapJs, sourceMapDest, replaceExisting = true) - logger.message(s"Emitted js source maps to: $sourceMapDest") + + if (os.exists(mainJs)) + if ( + os.walk.stream(linkingDir) + .filter(_ != mainJs).filter(_ != sourceMapJs) + .headOption.nonEmpty + ) { + // copy linking dir to dest + os.copy( + linkingDir, + dest, + createFolders = true, + replaceExisting = true, + mergeFolders = true + ) + logger.debug( + s"Scala.js linker generate multiple files for js multi-modules. Copy files to $dest directory." + ) + dest / "main.js" + } + else { + os.copy(mainJs, dest, replaceExisting = true) + if (build.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) { + val sourceMapDest = + build.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map")) + os.copy(sourceMapJs, sourceMapDest, replaceExisting = true) + logger.message(s"Emitted js source maps to: $sourceMapDest") + } + os.remove.all(linkingDir) + dest } - os.remove.all(linkingDir) - } else { val found = os.walk(linkingDir).map(_.relativeTo(linkingDir)) value(Left(new ScalaJsLinkingError(relMainJs, found))) diff --git a/modules/cli/src/main/scala/scala/cli/commands/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/Run.scala index 24d24bd631..230d89b1c7 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Run.scala @@ -221,9 +221,9 @@ object Run extends ScalaCommand[RunOptions] { build.options.scalaJsOptions.fullOpt.getOrElse(false), build.options.scalaJsOptions.noOpt.getOrElse(false), logger - ).map { _ => + ).map { outputPath => val process = Runner.runJs( - jsDest.toIO, + outputPath.toIO, args, logger, allowExecve = allowExecve, @@ -282,8 +282,8 @@ object Run extends ScalaCommand[RunOptions] { fullOpt, noOpt, logger - ).map { _ => - f(dest) + ).map { outputPath => + f(outputPath) } finally if (os.exists(dest)) os.remove(dest) } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index 9a699e54bb..ecef5c0a4c 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -239,6 +239,39 @@ abstract class PackageTestDefinitions(val scalaVersionOpt: Option[String]) } } + def multiModulesJsTest(): Unit = { + val fileName = "Hello.scala" + val message = "Hello World from JS" + val inputs = TestInputs( + Seq( + os.rel / fileName -> + s"""|//> using jsModuleKind "es" + |//> using jsModuleSplitStyleStr "smallestmodules" + | + |case class Foo(bar: String) + | + |object Hello extends App { + | println(Foo("$message").bar) + |} + |""".stripMargin + ) + ) + val destDir = fileName.stripSuffix(".scala") + inputs.fromRoot { root => + os.proc(TestUtil.cli, "package", extraOptions, fileName, "--js", "-o", destDir).call( + cwd = root, + stdin = os.Inherit, + stdout = os.Inherit + ) + + val launcher = root / destDir / "main.js" + val nodePath = TestUtil.fromPath("node").getOrElse("node") + os.write(root / "package.json", "{\n\n \"type\": \"module\"\n\n}") // enable es module + val output = os.proc(nodePath, launcher.toString).call(cwd = root).out.text().trim + expect(output == message) + } + } + if (!TestUtil.isNativeCli || !Properties.isWin) { test("simple JS") { simpleJsTest() @@ -246,6 +279,9 @@ abstract class PackageTestDefinitions(val scalaVersionOpt: Option[String]) test("source maps js") { sourceMapJsTest() } + test("multi modules js") { + multiModulesJsTest() + } } def simpleNativeTest(): Unit = {