An extensible sbt plugin for bundling Scala.js with non-Scala dependencies. Currently supports vite.
Informed largely by https://github.com/ptrdom/scalajs-vite
This plugin requires sbt 1.0.0+.
To use sbt-vite in your project, add the following line to projects/plugins.sbt
:
addSbtPlugin("io.github.johnhungerford" % "sbt-jsbundler" % "0.0.4")
In build.sbt
, include JSBundlerPlugin
in .enablePlugins(...)
in any Scala.js project
that needs to be bundled with JavaScript dependencies. Add the following setting to your
jsbundler project as well:
bundlerManagedSources ++= Seq(
...
)
where Seq(...)
contains a list of directories and files that should be included in your
bundle. This should include your package.json
/package-lock.json
as well as your any
assets and non-Scala source you want to include.
Add the following files on of your bundlerManagedSources
directories in your project or
sub-project:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>sbt-vite</title>
</head>
<body>
<script type="module" src="/main.js"></script>
</body>
</html>
If your Scala.js project is runnable (i.e., if it has a def main
entrypoint and
scalaJSUseMainModuleInitializer := true
in build.sbt), you can simply import the
application to be run as follows:
main.js
:
// 'scalajs:' will be resolved by vite to the output of the Scala.js linker
import 'scalajs:'
Otherwise you can import any exported objects from your Scala.js project as follows:
main.js
:
import { someLib, otherLib } from 'scalajs:';
...
someLib.doSomething();
otherLib.doSomethingElse();
...
Once you have your html and js entrypoints in place, you can run the following to generate a web bundle:
sbt bundle
This will compile your project, generate an appropriate vite configuration, and run
vite on all artifacts. By default, the bundle will persisted at
[project-directory]/target/scala-[x.x.x]/jsbundler-opt/dist
. Use sbt fastLinkJS/bundle
to run build in development mode and skip optimizations.
To launch a development server, you can run:
sbt "~startDevServer" stopDevServer
or generate a script to launch a dev server without having to use the sbt console:
sbt generateDevServerScript
This will output a shell script start-dev-server.sh
at your project root. It's
recommended to use this script to launch the dev server rather than sbt so that you
can use your sbt console to run ~prepareBundlerSources
. This will update your build
as you edit your Scala.js files so that vite can reload the page.
Run tests using the usual command:
sbt test
This will use vite to bundle the linked JavaScript test executable with any dependencies prior to running it.
This plugin would not be of much use if it did not resolve dependencies properly. One of the advantages of sbt-jsbundler is that it provides a consistent pattern for resolving imports in both your Scala.js and your JS/TS code.
As long as package.json
is included in your bundlerManagedSources
, jsbundler will
be able to resolve any usual npm package imports, both in your Scala.js and JS/TS code.
In addition to bundling npm modules, sbt-jsbundler will bundle Scala.js outputs with
local imports, such as JavaScript
, TypeScript
, css
, less
, and others.
Source files and directories should be declared for inclusion using the
bundlerManagedSources
setting:
bundlerManagedSources := Seq(
file("modules/frontend-project/src/main/typescript"),
file("modules/frontend-project/src/main/styles"),
file("modules/frontend-project/src/main/entrypoint"),
file("common/typescript"),
file("common/styles"),
)
In the above scenario, some TypeScript and CSS sources are included from sub-project source directories, while others are included from a common source directory at the project root.
For any declared source that points to a directory, sbt-jsbundler will copy all the files
and directories within it to the build directory prior to running vite
. Any declared
source that is a file will be copied directly to the build directory.
Accordingly, any sources declared in your build can be imported as expected:
// This will import either from [project]/src/main/typescript/someDir/someTypeScriptModule.ts
// or from common/typescript/someDir/someTypeScriptModule.ts
@js.native
@JSImport("/someDir/someTypeScriptModule", JSImport.Default)
object TypeScriptImport extends js.Object
// This will import either from [project]/src/main/styles/someStyle.css or from
// common/styles/someStyle.css
@js.native
@JSImport("/someStyle.css?inline", JSImport.Namespace)
object CssImport extends js.Object
These imports will work correctly in JS and TS sources as well:
import someModule from '/someDir/someTypeScriptModule';
import '/someStyle.css';
To import Scala.js artifacts into JS/TS sources, simply prefix the imported path with scalajs:
.
For example, if you Scala.js codebase includes the following export:
JSExportTopLevel("myExportedFunction", "myModule")
def myExportedFunction(i: Int, j: Int): Int = ???
You would import it in a JavaScript file as follows:
import { myExportedFunction } from 'scalajs:myModule.js';
Note that if the moduleId
"myModule" is left out of JSExportTopLevel()
, it will be
exported from a main.js
script. Imports from main.js
do not need to be stated explicitly,
so you could just import it as:
import { myExportedFuction } from 'scalajs:';
Note the colon is still required to for the plugin to resolve it correctly.
To customize the commands used to execute the various vite
tasks supported by sbt-jsbundler,
you can configure the bundler implementation:
fastLinkJS / bundlerImplementation := sbtjsbundler.vite.ViteJSBundler(
sbtjsbundler.vite.ViteJSBundler.Config()
.addEnv("NODE_ENV" -> "development") // This can be necessary for testing
.addArgs("--mode=development")
)
Note that we have only scoped the above implementation for fastLinkJS
, which means it
will only apply to development builds. fullLinkJS / bundlerImplementation
will retain the
default value, which is ViteJSBundler(ViteJSBundler.Config())
.
You can similarly add arguments and environment variables to the npm install commands
by customizing bundlerNpmManager
:
bundlerNpmManager := NpmManager(
NpmManager.Config().addArgs("--legacy-peer-deps")
)
The vite implementation of sbt-jsbundler generates configuration scripts with reasonable
defaults for full builds (i.e., bundle
, which is an alias for Compile / fullLinkJS/ bundle
),
development builds (i.e., fastLinkJS / bundler
), and test builds (i.e.,
Test / fastLinkJS / bundle
, which prepares a bundle to be executed by Test / test
).
To override these defaults, you can use the setting bundlerConfigSources
to provide
one or more configuration scripts that will be merged with the defaults, allowing
you to override various settings. Note that the following configuration properties
cannot be overridden:
root
build.outDir
build.rollupOptions.input
(for tests only)
bundlerConfigSources
must specify valid javascript files that provide a default export
of one of the following two forms:
- a simple vite configuration object
- a function that consumes a vite environment configuration and returns a vite configuration.
Note that neither of these should be wrapped in defineConfig
, as this will be called
after merging imported overrides.
Note also that the viteConfigSources
will be merged in order, so later sources in the
Seq
will have precedence over prior sources.
bundlerConfigSources
can be scoped to both Compile
/Test
and fullLinkJS
/fastLinkJS
to
provide different customizations for different build types.
The following vite config source provides overrides to support JSX (React), bundle source maps (disabled by default except in tests), and break out several library dependencies into separate chunks:
build.sbt
:
bundlerConfigSources += file("vite.config-build.js")
'vite.config-build.js':
import react from '@vitejs/plugin-react';
import sourcemaps from 'rollup-plugin-sourcemaps';
export default (env)=> ({
// Array properties will concat on merge, so this will be added
// to plugins, instead of overwriting
plugins: [
react(),
],
build: {
sourcemap: true,
rollupOptions: {
plugins: [sourcemaps()],
output: {
strict: false,
chunkFileNames: '[name]-[hash:10].js',
manualChunks: {
lodash: ['lodash'],
react: ['react'],
'react-dom': ['react-dom'],
'react-router-dom': ['react-router-dom'],
}
}
},
},
});
See src/sbt-test/sbt-jsbundler for an example project. It includes
a README.md
with further documentation.