Last active
August 16, 2022 23:25
-
-
Save nafg/6ecce298a0a20f1e4a259cdae5634060 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io._ | |
import java.util.zip.ZipInputStream | |
import geny.Generator | |
import mill._ | |
import mill.define.Target | |
import mill.scalajslib._ | |
object WebpackLib { | |
case class JsDeps(dependencies: List[(String, String)] = Nil, | |
devDependencies: List[(String, String)] = Nil, | |
jsSources: Map[String, String] = Map.empty) { | |
def ++(that: JsDeps): JsDeps = | |
JsDeps( | |
dependencies ++ that.dependencies, | |
devDependencies ++ that.devDependencies, | |
jsSources ++ that.jsSources) | |
} | |
object JsDeps { | |
def apply(dependencies: (String, String)*): JsDeps = JsDeps(dependencies = dependencies.toList) | |
implicit def rw: upickle.default.ReadWriter[JsDeps] = upickle.default.macroRW | |
} | |
case class WebpackParams(inputFile: os.Path, | |
jsDeps: JsDeps, | |
outputDirectory: os.Path, | |
opt: Boolean, | |
libraryName: Option[String]) { | |
lazy val copiedInputFile = outputDirectory / inputFile.last | |
} | |
private def writePkgJson(params: WebpackParams, | |
deps: JsDeps, | |
webpackVersion: String, | |
webpackCliVersion: String, | |
webpackDevServerVersion: String) = { | |
val webpackDevDependencies = Seq( | |
"webpack" -> webpackVersion, | |
"webpack-cli" -> webpackCliVersion, | |
"webpack-dev-server" -> webpackDevServerVersion, | |
"source-map-loader" -> "0.2.3" | |
) | |
os.write.over( | |
params.outputDirectory / "package.json", | |
ujson.Obj( | |
"dependencies" -> deps.dependencies, | |
"devDependencies" -> (deps.devDependencies ++ webpackDevDependencies) | |
).render(2) + "\n" | |
) | |
} | |
@scala.annotation.tailrec | |
private def readAllBytes(in: InputStream, | |
buffer: Array[Byte] = new Array[Byte](8192), | |
out: ByteArrayOutputStream = new ByteArrayOutputStream): String = { | |
val byteCount = in.read(buffer) | |
if (byteCount < 0) | |
out.toString | |
else { | |
out.write(buffer, 0, byteCount) | |
readAllBytes(in, buffer, out) | |
} | |
} | |
private def jsDepsFromJar(jar: File): Seq[JsDeps] = { | |
val stream = new ZipInputStream(new BufferedInputStream(new FileInputStream(jar))) | |
try | |
Iterator.continually(stream.getNextEntry) | |
.takeWhile(_ != null) | |
.collect { | |
case z if z.getName == "NPM_DEPENDENCIES" => | |
val contentsAsJson = ujson.read(readAllBytes(stream)).obj | |
def dependenciesOfType(key: String): List[(String, String)] = | |
contentsAsJson.getOrElse(key, ujson.Arr()).arr.flatMap(_.obj.map { case (s, v) => s -> v.str }).toList | |
JsDeps( | |
dependenciesOfType("compileDependencies") ++ dependenciesOfType("compile-dependencies"), | |
dependenciesOfType("compileDevDependencies") ++ dependenciesOfType("compile-devDependencies") | |
) | |
case z if z.getName.endsWith(".js") && !z.getName.startsWith("scala/") => | |
JsDeps(Nil, Nil, Map(z.getName -> readAllBytes(stream))) | |
} | |
.toList | |
finally | |
stream.close() | |
} | |
private def writeEntrypoint0(dst: os.Path, depNames: Iterable[String]) = { | |
val path = dst / "entrypoint.js" | |
os.write.over( | |
path, | |
s""" | |
|module.exports = { | |
| "require": (function(moduleName) { | |
| return { | |
| ${depNames.map { name => s"'$name': require('$name')" }.mkString(",\n ")} | |
| }[moduleName] | |
| }) | |
|} | |
|""".stripMargin | |
.trim | |
) | |
PathRef(path) | |
} | |
val webpackConfigFilename = "webpack.config.js" | |
def writeWpConfig(params: WebpackParams, bundleFilename: String) = { | |
val libraryOutputCfg = | |
params.libraryName.map(n => Map("library" -> n, "libraryTarget" -> "var")).getOrElse(Map.empty) | |
val outputCfg = | |
libraryOutputCfg ++ Map("path" -> params.outputDirectory.toString, "filename" -> bundleFilename) | |
os.write.over( | |
params.outputDirectory / webpackConfigFilename, | |
"module.exports = " + ujson.Obj( | |
"mode" -> (if (params.opt) "production" else "development"), | |
"devtool" -> "source-map", | |
"entry" -> params.copiedInputFile.toString, | |
"output" -> ujson.Obj.from(outputCfg.view.mapValues(ujson.Str)) | |
).render(2) + ";\n" | |
) | |
} | |
trait ScalaJSDepsModule extends ScalaJSModule { | |
def moduleDepJsDepsTarget = | |
T.sequence(recursiveModuleDeps.collect { case mod: ScalaJSDepsModule => mod.jsDeps }) | |
def jsDeps: Target[JsDeps] = T { | |
val jsDepsFromIvyDeps = | |
resolveDeps(transitiveIvyDeps)().iterator.toList.flatMap(pathRef => jsDepsFromJar(pathRef.path.toIO)) | |
val allJsDeps = jsDepsFromIvyDeps ++ moduleDepJsDepsTarget() | |
allJsDeps.foldLeft(JsDeps())(_ ++ _) | |
} | |
} | |
trait ScalaJSWebpackBaseModule extends ScalaJSDepsModule { | |
def webpackVersion: Target[String] = "4.17.1" | |
def webpackCliVersion: Target[String] = "3.1.0" | |
def webpackDevServerVersion: Target[String] = "3.1.7" | |
def writePackageJson = T.task { params: WebpackParams => | |
writePkgJson(params, params.jsDeps, webpackVersion(), webpackCliVersion(), webpackDevServerVersion()) | |
} | |
def bundleFilename = T { | |
"out-bundle.js" | |
} | |
def webpack = T.task { params: WebpackParams => | |
val _bundleFilename = bundleFilename() | |
if (params.inputFile != params.copiedInputFile) | |
os.copy.over(params.inputFile, params.copiedInputFile) | |
params.jsDeps.jsSources foreach { case (n, s) => os.write.over(params.outputDirectory / n, s) } | |
writeWpConfig(params, _bundleFilename) | |
writePackageJson().apply(params) | |
val logger = T.ctx().log | |
val npmInstall = os.proc("npm", "install").call(params.outputDirectory) | |
logger.debug(npmInstall.out.text()) | |
val webpackPath = params.outputDirectory / "node_modules" / "webpack" / "bin" / "webpack" | |
val webpack = | |
os.proc("node", webpackPath, "--bail", "--profile", "--config", webpackConfigFilename) | |
.call(params.outputDirectory) | |
logger.debug(webpack.out.text()) | |
if (params.inputFile != params.copiedInputFile) | |
os.remove(params.copiedInputFile) | |
List( | |
PathRef(params.outputDirectory / _bundleFilename), | |
PathRef(params.outputDirectory / (_bundleFilename + ".map")) | |
) | |
} | |
def devWebpack: Target[Seq[PathRef]] | |
def prodWebpack: Target[Seq[PathRef]] | |
} | |
trait ScalaJSWebpackApplicationModule extends ScalaJSWebpackBaseModule { | |
override def devWebpack: Target[Seq[PathRef]] = T.persistent { | |
webpack().apply(WebpackParams(fastOpt().path, jsDeps(), T.ctx().dest, opt = false, None)) | |
} | |
override def prodWebpack: Target[Seq[PathRef]] = T.persistent { | |
webpack().apply(WebpackParams(fullOpt().path, jsDeps(), T.ctx().dest, opt = true, None)) | |
} | |
} | |
trait ScalaJSWebpackLibraryModule extends ScalaJSWebpackBaseModule { | |
private val regex = """require\("([^"]*)"\)""".r | |
def writeEntrypoint = T.task { (src: PathRef, dest: os.Path) => | |
val requires = | |
os.read.lines.stream(src.path) | |
.flatMap(line => Generator.from(regex.findAllMatchIn(line).map(_.group(1)))) | |
.toList | |
writeEntrypoint0(dest, requires) | |
} | |
def webpackLibraryName = T { | |
"app" | |
} | |
override def devWebpack: Target[Seq[PathRef]] = T.persistent { | |
val dest = T.ctx().dest | |
val deps = jsDeps() | |
val src = fastOpt() | |
val entrypoint = writeEntrypoint().apply(src, dest).path | |
webpack().apply(WebpackParams(entrypoint, deps, dest, opt = false, Some(webpackLibraryName()))) :+ src | |
} | |
override def prodWebpack: Target[Seq[PathRef]] = T.persistent { | |
val dest = T.ctx().dest | |
val deps = jsDeps() | |
val src = fullOpt() | |
val entrypoint = writeEntrypoint().apply(src, dest).path | |
webpack().apply(WebpackParams(entrypoint, deps, dest, opt = true, Some(webpackLibraryName()))) :+ | |
src | |
} | |
} | |
} |
Hmm, I wonder if you could leave the main part as json then append another statement, module.exports.plugins = ..., to the string (after the ;\n)
If that's not valid to webpack then change it to work with a local var, then set module.exports to it.
Thanks for the tip. I will try it out.
Ended up using a separate file like
os.write.over(
params.outputDirectory / webpackConfigFilename,
// Generate webpack config
/* "module.exports = " + ujson
.Obj(
"mode" -> (if (params.opt) "production" else "development"),
"devtool" -> "source-map",
"entry" -> params.copiedInputFile.toString,
"output" -> ujson.Obj.from(outputCfg.mapValues(ujson.Str))
)
.render(2) + ";\n" */
// or read from separate file
os.read(os.pwd / "webpack.config.js")
)
Thanks @nafg for this file. It solved a great deal of issue for me.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
At I didn't understand the issue. Indeed that's not json so there's no way to express it with ujson. If you have a better way (than using ujson I guess) please share (e.g. by forking and modifying the gist and linking back)