惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
WordPress大学
WordPress大学
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
N
Netflix TechBlog - Medium
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
H
Hacker News: Front Page
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
AWS News Blog
AWS News Blog
AI
AI
G
GRAHAM CLULEY
IT之家
IT之家
P
Privacy & Cybersecurity Law Blog
L
Lohrmann on Cybersecurity
Last Week in AI
Last Week in AI
D
Docker
Recent Announcements
Recent Announcements
O
OpenAI News
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
S
Security @ Cisco Blogs
T
Troy Hunt's Blog
C
Check Point Blog
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic

Bin Wang - My Personal Blog

Travel Back to China: 2026 Edition | Bin Wang TCode: An AI Coding Agent Leverages Neovim and Tmux | Bin Wang My 2025 in Review | Bin Wang Music Video Generation with AI | Bin Wang Home Network Setup with OpenWrt and VLANs | Bin Wang Fix ZFS Linux Kernel Dependency on Arch Linux | Bin Wang A Rust CLI Program, Use It with LLM and Convert It to Web UI | Bin Wang My First Rust Project | Bin Wang Download Message Images from Seesaw | Bin Wang My Workflow to Review Articles with LLMs | Bin Wang Why Consensus Shortcuts Fail in Distributed Systems | Bin Wang Improve Books Section of My Blog | Bin Wang Use OpenAPI Instead of MCP for LLM Tools | Bin Wang Travel Back To China: 2025 Edition | Bin Wang Replace A Dead Node in My High Availability Cluster | Bin Wang A 2-Year Reflection for 2023 and 2024 | Bin Wang Jepsen Test on Patroni: A PostgreSQL High Availability Solution | Bin Wang My MacOS Essentials | Bin Wang Source Code of RSS Brain is Available | Bin Wang
SBT Task to Build Frontend Components | Bin Wang
2024-09-13 · via Bin Wang - My Personal Blog

Table of Contents

  1. Frontend Package Management and Build
  2. SBT Task to Trigger Build and Package Dist Files
  3. Serve Resource Files in Http Server

ScalaSBTJavascriptCSSfrontendwebpacknpm

Even when writing a website using something other than Javascript to render content from the server, sometimes it’s inevitable to have some Javascript or CSS code. So managing Javascript dependencies and build packages is needed. The easiest way may be to just not use any tool: download all the dependency files into a directory and import them in the html file directly. That’s what I was doing for RSS Brain before. But it gets messy pretty quickly and it’s hard to keep track of the dependencies. So it’s time for me to resolve the problem. Since the project is written in Scala, I’ll note down how I do it with Scala’s build tool SBT.

Frontend Package Management and Build

I put all the frontend related code into a separate sub-directory and treat it like a frontend project. This makes things much easier and less hacky. I use npm to manage the dependencies and use webpack to build it. Here is a simplified example of the code tree structure from my project RSS Brain:

▾ js/
  ▾ css/
      google-fonts.css
      main.css
      pico.jade.min.css
  ▾ dist/
      f20305dee9d396fea5c7.ttf
      f5ef242406fdcf40a232.otf
      main.css
      main.js
      main.js.LICENSE.txt
  ▾ fonts/
      google-material-icons-outlined.otf
      google-material-icons.ttf
  ▸ node_modules/
  ▾ src/
      boolean-checkbox.js
      error-handler.js
      global-htmx.js
      index.js
      match-id.js
      popover-menu.js
      register-service-worker.js
      service-worker.js
      set-theme.js
      source-images.js
    package-lock.json
    package.json
    readme.md
    webpack.config.js
▸ project/
▸ src/
  build.sbt
  LICENSE.txt
	readme.md

You can see other than the js directory, it’s a pretty standard structure for a Scala project managed by SBT.

When you look into the js directory, it’s a frontend project managed by npm and built with webpack.

js/src/index.js bundles all the dependencies in node modules and local files. Here is an example:

// css

import 'somment/somment.css';
import 'lite-youtube-embed/src/lite-yt-embed.css';
import 'toastify-js/src/toastify.css';
import '../css/google-fonts.css';
import '../css/pico.jade.min.css';
import '../css/main.css';

// js
import './boolean-checkbox.js';

import 'htmx.org';
import './global-htmx.js';

import Alpine from 'alpinejs';
window.Alpine = Alpine;

import * as FloatingUIDOM from '@floating-ui/dom';
window.FloatingUIDOM = FloatingUIDOM;

import 'lite-youtube-embed';
import '@splidejs/splide';
import Toastify from 'toastify-js';
window.Toastify = Toastify;

import DOMPurify from 'dompurify';
window.DOMPurify = DOMPurify;

import 'imgs-html';
import 'somment';

import './error-handler.js';
import './popover-menu.js';
import './match-id.js';
import './set-theme.js';
import './source-images.js';
import './register-service-worker.js';

Alpine.start();

Here is an example of webpack.config.js:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        // If you enable `experiments.css` or `experiments.futureDefaults`, please uncomment line below
        // type: "javascript/auto",
        test: /\.(sa|sc|c)ss$/i,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader",
        ],
      },
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

Since this is more related to frontend tech and is very basic, I will not go too much into details. But the point is, when you run npx webpack under the js directory, it will build bundled files into js/dist. We will write a SBT task to trigger this command and copy the dist files into resources to package.

SBT Task to Trigger Build and Package Dist Files

SBT is very flexible since you can basically write Scala code to define the tasks. Here we define the first task to install npm dependencies and trigger webpack build (in build.sbt):

lazy val webpack = taskKey[Unit]("Run webpack in js directory")
webpack :=  {
  val workDir = new File("./js")
  Process("npm" :: "install" :: Nil, workDir) #&& Process("npx" :: "webpack" :: Nil, workDir) !
}

It defines a task called webpack, so when you run sbt webpack, it will run npm install && npx webpack under js.

Then we define another task to copy all the dist files to generated resource directory:

Compile / resourceGenerators += Def.task {
  webpack.value
  val file = (Compile / resourceManaged).value / "webview" / "static" / "dist"
  IO.copyDirectory(new File("./js/dist"), file, overwrite = true)
  IO.listFiles(file).toSeq
}.taskValue

Here we added some steps when SBT generates resource files: first we let it run the webpack task we defined above, then copy all the files under js/dist to webview/static/dist under generated resources. Here resources means Java resource files, like the files under src/main/resources, but auto-generated to target/scala-2.13/resource_managed and will be packaged together as resource files.

So when you run sbt package here, the generated jar package will include all those files as resource files. For example, in my project, the generated jar package has these if you open it with vim (which can view zipped packages):

81663 webview/static/dist/f20305dee9d396fea5c7.ttf
81664 webview/static/dist/f5ef242406fdcf40a232.otf
81665 webview/static/dist/main.css
81666 webview/static/dist/main.js
81667 webview/static/dist/main.js.LICENSE.txt

Serve Resource Files in Http Server

Now you can serve the files under webview/static/dist with your web server. Different web servers or frameworks do it differently. Here is an example of http4s:

// include the following route into the http4s web server
// IMPORTANT: every resource file under `/webview` will be publicly accessible
val assetsRoutes = resourceServiceBuilder[IO]("/webview").toRoutes

Then you can use them in HTML:

<link rel="stylesheet" href="/static/dist/main.css">
<script src="/static/dist/main.js" defer="defer"></script>