Scala.js Modifiers
Code fences with the scala mdoc:js
modifier are compiled with Scala.js and run
in the browser.
```scala mdoc:js
import org.scalajs.dom.window._
import jsdocs._
val progress = new Progress()
setInterval({ () =>
// `node` variable is a DOM element in scope.
node.innerHTML = progress.tick(5)
}, 100)
```
Becomes
import org.scalajs.dom.window._
import jsdocs._
val progress = new Progress()
setInterval({ () =>
// `node` variable is a DOM element in scope.
node.innerHTML = progress.tick(5)
}, 100)
Installation
The mdoc:js
modifier requires custom installation steps.
sbt-mdoc
First, install sbt-mdoc using the regular installation instructions.
Next, update the mdocJS
setting to point to a Scala.js project that has
scalajs-dom
as a library dependency.
// build.sbt
lazy val jsdocs = project
.settings(
+ libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.0.0"
)
.enablePlugins(ScalaJSPlugin)
lazy val docs = project
.in(file("myproject-docs"))
.settings(
+ mdocJS := Some(jsdocs)
)
.enablePlugins(MdocPlugin)
Command-line
First, install Coursier.
Next, construct an mdoc.properties
file with the necessary Scala.js
configuration
cat <<EOT > mdoc.properties
js-classpath=$(coursier fetch org.scala-js:scalajs-library_2.12:1.16.0 org.scala-js:scalajs-dom_sjs1_2.12:2.0.0 -p)
js-scalac-options=-Xplugin:$(coursier fetch --intransitive org.scala-js:scalajs-compiler_2.12.20:1.16.0)
js-linker-classpath=$(coursier fetch org.scalameta:mdoc-js-worker_2.12:2.6.2 org.scala-js:scalajs-linker_2.12:1.16.0 -p)
EOT
Note: For scala 3, you may need js-scalac-options=-scalajs
- instead of the -XPlugin
flag - Doc.
Next, create a basic Markdown files with an mdoc:js
modifier:
mkdir docs
cat <<EOT > docs/index.md
\`\`\`scala mdoc:js
org.scalajs.dom.window.setInterval(() => {
node.innerHTML = new java.util.Date().toString
}, 1000)
\`\`\`
EOT
Next, launch mdoc with the org.scalameta:mdoc-js
module instead of
org.scalameta:mdoc
:
coursier launch org.scalameta:mdoc-js_2.12:2.6.2 --extra-jars $(pwd) -- --watch
Open the URL http://localhost:4001/index.md to see a live preview of the generated Markdown.
Some notes:
- The
--extra-jars $(pwd)
flag is needed to pick up themdoc.properties
configuration. - The
js-scalacOptions
field inmdoc.properties
must contain-Xplugin:path/to/scalajs-compiler.jar
to enable the Scala.js compiler. - The
js-classpath
field inmdoc.properties
must include a dependency on the libraryorg.scala-js:scalajs-dom
- There is a js-html-header field in
mdoc.properties
which can be used to inject custom HTML, scripts or stylesheets into the output.
js-html-header=<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css" /><script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace-autoloader.js"></script>
- There's an
--import-map
command line flag, commonly set to ;--import-map-path $(pwd)/importmap.json
, which will remap ESModule imports at link time. As a reminderimportmap.json
could look like:
{
"imports": {
"@shoelace-style/shoelace/dist/": "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.13.1/cdn/"
}
}
which will remap the shoelace import to resolve out of a CDN - negating the need for a bundler.
Modifiers
The following modifiers can be combined with mdoc:js
code fences to customize
the rendered output.
:shared
By default, each code fence is isolated from other code fences. Use the
:shared
modifier to reuse imports or variables between code fences.
```scala mdoc:js
import org.scalajs.dom.window.setInterval
import scala.scalajs.js.Date
```
```scala mdoc:js
setInterval(() => {
node.innerHTML = new Date().toString()
}, 1000)
```
Is a compile error, because the imports are not in the second block. But if you add the shared modifier:
```scala mdoc:js:shared
import org.scalajs.dom.window.setInterval
import scala.scalajs.js.Date
```
And it works!
import org.scalajs.dom.window.setInterval
import scala.scalajs.js.Date
setInterval(() => {
node.innerHTML = new Date().toString()
}, 1000)
:invisible
By default, the original input code is rendered in the output page. Use
:invisible
to hide the code example from the output so that only the div is
generated.
```scala mdoc:js:invisible
var n = 0
org.scalajs.dom.window.setInterval(() => {
n += 1
node.innerHTML = s"Invisible tick: $n"
}, 1000)
```
You'd have to look at the raw markdown to see the fence below.
:invisible
example...:compile-only
Use :compile-only
to validate that a code example compiles successfully
without evaluating it at runtime.
```scala mdoc:js:compile-only
org.scalajs.dom.window.setInterval(() => {
println("I'm only compiled, never executed")
}, 1000)
```
Note that compile-only
blocks do not automatically have access to a node
DOM
element. Create a leading code fence with shared:invisible
to expose a hidden
node
instance in the scope of a compile-only
code block.
Loading HTML
By default, the node
variable points to an empty div element. Prefix the code
fence with custom HTML followed by a ---
separator to set the inner HTML of
the node
div.
```scala mdoc:js
<p>I am a custom <code>loader</code></p>
---
// Open developer console to see this printed message
println(s"Loading HTML: ${node.innerHTML}")
```
// Open developer console to see this printed message
println(s"Loading HTML: ${node.innerHTML}")
I am a custom loader
Replace the node's innerHTML
to make the HTML disappear once the document has
loaded.
org.scalajs.dom.window.setTimeout(() => {
node.innerHTML = "I am loaded. Refresh the page to load me again."
}, 3000)
Facades
Let's imagine we want to document a (glorious) facade to ms with mdoc.
import scala.scalajs.js
import scala.scalajs.js.annotation._
@js.native
@JSImport("https://cdn.jsdelivr.net/npm/ms@2.1.3/+esm", JSImport.Default)
object ms extends js.Object {
def apply(s: Double): String = js.native
def apply(s: String): Double = js.native
}
val date = new scala.scalajs.js.Date()
val time = ms( "2 days")
val time2 = ms( 1000)
node.innerHTML = s"Hello from npm package 'ms': $time , $time2 "
If the ms
function is not referenced from jsdocs
you may get a
stacktrace in the browser console like this:
Uncaught TypeError: $i_ms is not a function
at $c_Lmdocjs$.run6__Lorg_scalajs_dom_raw_Element__V (js.md.js:1180)
at js.md.js:3108
at mdoc.js:9
Please note that the CDN is hardcoded here as is a simple way to get started. A "real" facade would normally be constructed as
@JSImport("ms", JSImport.Default)
As that is where it's found in the node_modules
. Consider remapping this import at link time, as described in the configuration section below for a (real) use case.
Configuration
The mdoc:js
modifier supports several site variable options to customize the
rendered output of mdoc:js
.
Add custom HTML header
Update the js-html-header
site variable to insert custom HTML before the
compiled JavaScript. For example, to add React via unpkg add the following
setting.
mdocVariables := Map(
+ "js-html-header" ->
+ """<script crossorigin src="https://unpkg.com/react@16.5.1/umd/react.production.min.js"></script>"""
)
Generate optimized page
The Scala.js fullOpt
mode is used by default and the fastOpt
mode is used
when the -w
or --watch
flag is used. The fastOpt
mode provides faster
feedback while iterating on documentation at the cost of larger bundle size and
slower code. When publishing a website, the optimized mode should be used.
Update the js-opt
site variables to override the default optimization mode:
js-opt=full
: usefullOpt
regardless of watch modejs-opt=fast
: usefastOpt
regardless of watch mode
node
variable name
Customize By default, each mdoc:js
code fence has access to a node
variable that
points to a DOM element.
Update the site variable js-mount-node=customNode
to use a different variable
name than node
.
Customize output js directory
By default, mdoc generates the javascript next to the output markdown sources.
For example, a foo.md
markdown file with mdoc:js
code fences produces a
foo.md.js
JavaScript file in the same directory.
When using the DocusaurusPlugin
sbt plugin, the output directory is
automatically configured to emit JavaScript in the assets/
directory since the
docs/
directory can only contain markdown sources.
For other site generators than Docusaurus or outside of sbt, update the site
variable js-out-prefix=assets
if you need to generate the output JavaScript
file in a different directory than the markdown source.
Customize module kind
By default, sbt-mdoc uses the scalaJSModuleKind
sbt setting to determine the
module kind.
Outside of sbt, update the js-module-kind
site variables to customize the
module kind:
js-module-kind=NoModule
: don't export modules, the default value.js-module-kind=CommonJSModule
: use CommonJS modules.js-module-kind=ESModule
: use ECMAScript modules, not supported.
Add local JavaScript libraries
In sbt, the mdocJSLibraries
setting allows you to link local *-library.js
and *-loader.js
files produced by webpack via scalajs-bundler.
Outside of sbt, update the js-libraries
site variable to contain a path
separated list of local JavaScript files (same syntax as Java classpaths) to
link local JavaScript library files. Note that mdoc will only copy the following
files:
*-library.js
: this file is copied to the output directory and linked from the markdown output as a<script src="...">
.*-loader.js
: same as*-library.js
but it comes after*-library.js
.*.js.map
: optional source maps.
Batch mode linking
By default, mdoc will re-use the Scala.js linker, exercising its ability to work incrementally. This can speed up linking and optimization dramatically.
If incremental linking is causing issues in your project, use can use js-batch-mode
site variable to enable batch mode (which will discard intermediate linker state
after processing each file):
mdocVariables := Map(
"js-batch-mode" -> "true"
)
Remapping imports at link time
It may be the case that the library you wish to depend on is a facade to some NPM dependency. Mapping the import offers an alterative to bundling, by resolving the dependency out of a CDN. We set mdoc to output ESModules (see ModuleKind
), and load the dependancies directly in the browser.
To mdocs CLI, you'll need to pass this flag;
--import-map-path full/path/to/import-map.json
import-map.json
could look like this;
{
"imports":
{
"@shoelace-style/shoelace/dist/": "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.13.1/cdn/"
}
}
You should then have this dependency available to you.
Legacy
This documentation is preserved here, but it is no longer recommend to work with scalaJS bundler. If you require a bundler you should consider the excellent vite project - integration with vite is a potential improvement for mdoc.
The scalajs-bundler plugin can be used to install npm dependencies and bundle applications with webpack.
Add the following sbt settings if you use scalajs-bundler:
lazy val jsdocs = project
.settings(
+ webpackBundlingMode := BundlingMode.LibraryOnly()
+ scalaJSUseMainModuleInitializer := true,
npmDependencies.in(Compile) ++= List(
// ...
),
)
.enablePlugins(ScalaJSBundlerPlugin)
lazy val docs = project
.settings(
mdocJS := Some(jsdocs),
+ mdocJSLibraries := webpack.in(jsdocs, Compile, fullOptJS).value
)
.enablePlugins(MdocPlugin)
The
webpackBundlingMode
must beLibraryOnly
so that the mdoc generated output can depend on it.
Bundle npm dependencies
It's important that the main function in jsdocs
uses the installed npm
dependencies. If the npm dependencies are not used from the jsdocs
main
function, then webpack thinks they are unused and removes them from the bundled
output even if those dependencies are called from mdoc:js
markdown code
fences.
For example, to use the npm ms
package
start by writing a facade using @JSImport
File: ms.scala
package jsdocs
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
@js.native
@JSImport("ms", JSImport.Namespace)
object ms extends js.Object {
// Facade for the npm package `ms` https://www.npmjs.com/package/ms
def apply(n: Double): String = js.native
}
Next, write a main function that uses the facade. Make sure that the jsdocs
project contains the setting scalaJSUseMainModuleInitializer := true
.
File: Main.scala
package jsdocs
object Main {
def main(args: Array[String]): Unit = {
// Important, the main function must use the Scala.js facade in order for
// the `ms` npm library to be available from mdoc:js code fences.
ms(6000)
}
}
The ms
function can now be used from mdoc:js
.
val date = new scala.scalajs.js.Date()
val time = jsdocs.ms(date.getTime())
node.innerHTML = s"Hello from npm package 'ms': $time"
If the ms
function is not referenced from the jsdocs
main function you get a
stacktrace in the browser console like this:
Uncaught TypeError: $i_ms is not a function
at $c_Lmdocjs$.run6__Lorg_scalajs_dom_raw_Element__V (js.md.js:1180)
at js.md.js:3108
at mdoc.js:9
Validate library.js and loader.js
Validate that the webpack
task provides one *-library.js
file and one
*-loader.js
file.
// sbt shell
> show jsdocs/fullOptJS::webpack
...
[info] * Attributed(.../jsdocs/target/scala-2.12/scalajs-bundler/main/jsdocs-opt-loader.js)
[info] * Attributed(.../jsdocs/target/scala-2.12/scalajs-bundler/main/jsdocs-opt-library.js)
...
These files are required by mdoc in order to use the scalajs-bundler npm
dependencies. The files may be missing if you have custom webpack or
scalajs-bundler configuration. To fix this problem, you may want to try to
create a new jsdocs
Scala.js project with minimal webpack and scalajs-bundler
configuration.