The modern asset pipeline for Sails.js, powered by Rsbuild.
Shipwright replaces the legacy Grunt-based asset pipeline with a fast, modern bundler that supports TypeScript, ES modules, LESS/SASS, and Hot Module Replacement out of the box.
| Feature | Grunt (legacy) | Shipwright |
|---|---|---|
| Build speed | ~16s | ~1.4s |
| JS bundle size | 3.0MB | 229KB |
| CSS bundle size | 733KB | 551KB |
| Hot Module Replacement | No | Yes |
| TypeScript | No | Yes |
| ES Modules | No | Yes |
| Tree Shaking | No | Yes |
Benchmarks from fleetdm.com migration (fleetdm/fleet#38079)
npm install sails-hook-shipwright --saveShipwright uses Rsbuild 2.x and requires Node.js 20.19+ or 22.12+.
This matches Rsbuild's supported runtime floor now that Node.js 18 is no
longer supported by Rsbuild.
Disable the grunt hook in .sailsrc:
{
"hooks": {
"grunt": false
}
}Shipwright works with zero configuration for most apps. Just create your entry point:
assets/
js/
app.js # Auto-detected entry point
styles/
importer.less # Auto-detected styles entry
In your layout, use the shipwright helpers:
<!DOCTYPE html>
<html>
<head>
<%- shipwright.styles() %>
</head>
<body>
<!-- your content -->
<%- shipwright.scripts() %>
</body>
</html>That's it! Shipwright will bundle your JS, compile your styles, and inject the appropriate tags.
Create config/shipwright.js to customize behavior:
module.exports.shipwright = {
js: {
entry: 'assets/js/app.js' // optional, auto-detected by default
},
styles: {
entry: 'assets/styles/app.css' // optional, auto-detected by default
},
build: {
// Rsbuild configuration - see https://rsbuild.dev/config/
}
}Most apps don't need a config file at all - shipwright auto-detects entry points and uses sensible defaults:
- JS inject default:
['dependencies/**/*.js'] - CSS inject default:
['dependencies/**/*.css']
Shipwright auto-detects entry points in this order:
JavaScript:
assets/js/app.jsassets/js/main.jsassets/js/index.js
Styles:
assets/styles/importer.lessassets/styles/importer.scssassets/styles/importer.cssassets/styles/main.lessassets/styles/main.scssassets/styles/main.cssassets/styles/app.lessassets/styles/app.scssassets/styles/app.cssassets/css/app.cssassets/css/main.css
For new apps or apps using import/export:
// assets/js/app.js
import { setupCloud } from './cloud.setup'
import { formatDate } from './utilities/format'
setupCloud()Shipwright detects the single entry point and bundles all imports.
For existing apps that concatenate scripts without ES modules (like Grunt's pipeline.js):
// config/shipwright.js
module.exports.shipwright = {
js: {
entry: [
'js/cloud.setup.js',
'js/components/**/*.js',
'js/utilities/**/*.js',
'js/pages/**/*.js'
]
}
}Files are concatenated in the specified order, preserving the global scope behavior of the legacy pipeline. This is a drop-in replacement for tasks/pipeline.js.
- entry - Files bundled together by Rsbuild (minified, tree-shaken, hashed)
- inject - Files loaded as separate
<script>or<link>tags before the bundle
Use inject for vendor libraries that need to be loaded separately:
module.exports.shipwright = {
js: {
inject: [
'dependencies/sails.io.js',
'dependencies/lodash.js',
'dependencies/jquery.min.js',
'dependencies/vue.js',
'dependencies/**/*.js' // catch remaining dependencies
]
}
}The order is preserved, and duplicates are automatically removed.
Shipwright supports TypeScript out of the box. Just use .ts or .tsx files:
// config/shipwright.js
module.exports.shipwright = {
js: {
entry: 'assets/js/app.ts'
// or with glob patterns:
// entry: ['js/**/*.ts', 'js/**/*.tsx']
}
}No tsconfig.json required for basic usage. Add one if you want strict type checking.
Install the appropriate plugin:
# For LESS
npm install @rsbuild/plugin-less --save-dev
# For SASS/SCSS
npm install @rsbuild/plugin-sass --save-devAdd the plugin to your config:
const { pluginLess } = require('@rsbuild/plugin-less')
module.exports.shipwright = {
build: {
plugins: [pluginLess()]
}
}Shipwright auto-detects your styles entry point (importer.less, main.scss, etc.).
In development, Shipwright provides HMR via Rsbuild's dev server. Changes to your JS and CSS files are instantly reflected in the browser without a full page reload.
HMR is enabled automatically when NODE_ENV !== 'production'.
In production (NODE_ENV=production), Shipwright:
- Minifies JS and CSS
- Adds content hashes for cache busting (
app.a1b2c3d4.js) - Enables tree shaking to remove unused code
- Generates a manifest for asset versioning
.tmp/public/
js/
app.js # development
app.a1b2c3d4.js # production (with hash)
css/
styles.css
styles.b2c3d4e5.css
manifest.json # maps entry names to hashed filenames
dependencies/ # copied from assets/dependencies
images/ # copied from assets/images
...
Shipwright configures these aliases by default:
@→assets/js~→assets
// In your JS files
import utils from '@/utilities/helpers'
import styles from '~/styles/components.css'Pass any Rsbuild configuration via the build key:
const { pluginLess } = require('@rsbuild/plugin-less')
const { pluginReact } = require('@rsbuild/plugin-react')
module.exports.shipwright = {
build: {
plugins: [pluginLess(), pluginReact()],
output: {
// Custom output options
},
splitChunks: {
// Custom chunk splitting options
}
}
}See Rsbuild Configuration for all available options.
Shipwright is built on Rsbuild 2.x, which includes Rspack 2.x. Most apps can
continue using the same config/shipwright.js, but a few advanced Rsbuild
options changed:
performance.chunkSplitis deprecated. Migrate custom chunk-splitting config tosplitChunks.server.proxyuseshttp-proxy-middlewarev4. If you configure proxies, replacecontextwithpathFilterand move proxy event callbacks under theonoption.core-jsis no longer installed by Rsbuild by default. Installcore-jsdirectly if you enableoutput.polyfill.- Module Federation runtime packages are no longer installed implicitly. Install the federation runtime tooling your app uses before enabling Module Federation config.
- Bundle analysis is no longer provided by
performance.bundleAnalyze. Use Rsdoctor or add an explicit analyzer plugin inbuild.plugins. - Webpack provider support was removed by Rsbuild. Shipwright's defaults use Rspack only.
- Rsbuild's default web targets are more modern. Add a browserslist config if your app needs the older Rsbuild 1 browser baseline.
- Node builds target Node.js 20 by default and emit ESM by default in Rsbuild 2. Set explicit Rsbuild output options if your app has a custom Node bundle.
- Decorator transforms now default to the
2023-11decorators proposal. Apps using legacy decorators should configure the transform explicitly. - Audit advanced Rsbuild overrides when upgrading. Removed or deprecated options
include
source.alias,source.aliasStrategy,performance.bundleAnalyze,performance.removeMomentLocale,performance.profile, webpack provider tooling,dev.setupMiddlewares,?__inline=false, and customized built-in JS/CSS rules.
Rsbuild 2 is published as ESM. Shipwright loads it through dynamic import()
from the Sails hook, and Node.js 20.19+ can still load Rsbuild plugin packages
from CommonJS Sails config files.
- Install shipwright and disable grunt:
npm install sails-hook-shipwright --save
npm install @rsbuild/plugin-less --save-dev # if using LESS// .sailsrc
{
"hooks": {
"grunt": false
}
}- Create
config/shipwright.jsbased on yourtasks/pipeline.js:
// If your pipeline.js has:
// var jsFilesToInject = [
// 'dependencies/sails.io.js',
// 'dependencies/lodash.js',
// 'js/cloud.setup.js',
// 'js/**/*.js'
// ]
// Your shipwright.js becomes:
const { pluginLess } = require('@rsbuild/plugin-less')
module.exports.shipwright = {
js: {
entry: [
'js/cloud.setup.js',
'js/components/**/*.js',
'js/utilities/**/*.js',
'js/pages/**/*.js'
],
inject: [
'dependencies/sails.io.js',
'dependencies/lodash.js',
'dependencies/**/*.js'
]
},
build: {
plugins: [pluginLess()]
}
}- Update your layout to use shipwright helpers:
- <!--STYLES-->
- <!--STYLES END-->
+ <%- shipwright.styles() %>
- <!--SCRIPTS-->
- <!--SCRIPTS END-->
+ <%- shipwright.scripts() %>- Remove the
tasks/directory (optional, but recommended).
Returns <script> tags for:
- Injected files (from
js.injectpatterns) - Bundled initial files (from manifest)
By default, Shipwright emits the app entry. Pass an entry name to emit a
different initial entry from the Rsbuild manifest:
<%- shipwright.scripts('admin') %>Returns <link> tags for:
- Injected files (from
styles.injectpatterns) - Compiled initial styles (from manifest)
By default, Shipwright emits the app entry. Pass an entry name to emit a
different initial entry from the Rsbuild manifest:
<%- shipwright.styles('admin') %>Install the required plugin:
npm install @rsbuild/plugin-less --save-devAnd add it to your config:
const { pluginLess } = require('@rsbuild/plugin-less')
module.exports.shipwright = {
build: { plugins: [pluginLess()] }
}Check that your inject patterns don't overlap with files in the bundle. Shipwright automatically deduplicates, but explicit is better than implicit.
Ensure NODE_ENV is not set to production in development.
The Sails framework is free and open-source under the MIT License.