Gulp¶
CKAN theme extensions often contain stylesheets that grow over time.
For initial simplification of CSS files you can use available CSS features,
such as @scope, nested rules or
CSS-functions, that are widely supported in modern
browsers. For anything else, use LESS, SASS, or
Stylus. SCSS is the most common choice across extensions, but nothing
stops you from using other preprocessors, as long as you don't need to modify
existing CSS framework.
To further improve stylesheet processing, you can use task runner, such as Gulp. For example, it can be used to compile, prettify, modernize, combine and sort your SCSS files.
Installation¶
In order to use it, you need Gulp itself, and a number of additional packages. All of them can be installed via NPM or similar package manager.
npm i -d gulp \
gulp-clean-css \
gulp-if \
gulp-postcss \
gulp-sass \
gulp-sourcemaps \
gulp-touch-fd \
postcss-sort-media-queries \
sass
This command will create package.json in the current directory, if you don't
have one yet. And it will add all the dependencies to it. Additionally, it will
create package-lock.json and node_modules/ folder where downloaded packages
are stored.
Configuration (gulpfile.js)¶
Gulp is a task runner(somewhat similar to GNU Make, or python's
Click). Tasks are defined in the gulpfile.js which can be created next to
the package.json:
ckanext-midnight-blue-theme/
├── package.json
├── gulpfile.js
└── ckanext/
└── myextension/
├── assets/
│ ├── css/
│ │ └── main.css # Compiled output
│ └── scss/ # SASS source files
│ ├── main.scss
│ └── main-rtl.scss
Inside gulpfile.js you define "tasks" - functions that are executed when you
"run task". For SCSS processing we are going to create two tasks:
build: compile and minify SCSS into CSSdev: start a watcher that recompiles CSS whenever source SCSS is changed. Additionally it will include sourcemaps into output, so that you'll see the original SCSS inside your browser's development tools.
gulpfile.js configuration used to compile and optimize SASS code.
const { resolve } = require("path");
const { src, dest, watch } = require("gulp");
const if_ = require("gulp-if");
const sourcemaps = require("gulp-sourcemaps");
const sass = require("gulp-sass")(require("sass"));
const postcss = require("gulp-postcss");
const combineQueries = require("postcss-sort-media-queries");
const touch = require("gulp-touch-fd");
const cleanCSS = require("gulp-clean-css");
// Debug/Development flag
const isDev = () => !!process.env.DEBUG;
// compute absolute path to the source and target compilation directories
const assetsDir = resolve(__dirname, "ckanext/myextension/assets");
const srcDir = resolve(assetsDir, "scss");
const destDir = resolve(assetsDir, "css");
const build = () =>
src([resolve(srcDir, "main.scss")]) // (1)!
.pipe(if_(isDev, sourcemaps.init())) // (2)!
.pipe(
sass({
loadPaths: ["node_modules"], // (3)!
silenceDeprecations: ["import", "legacy-js-api", "color-functions"],
}).on("error", sass.logError),
)
.pipe(postcss([combineQueries])) // (4)!
// Build optimization and minification
.pipe(
if_(
isDev,
sourcemaps.write(), // (5)!
cleanCSS({ // (6)!
level: 2,
format: {
breaks: { afterProperty: true, afterRuleBegins: true },
},
}),
),
)
.pipe(dest(destDir))
// Force CKAN cache invalidation
.pipe(touch()); // (7)!
const watchStyles = () =>
watch(resolve(srcDir, "**/*.scss"), { ignoreInitial: false }, build);
exports.dev = watchStyles;
exports.build = build;
- Specify source's entrypoint. If there are multiple entrypoints, like
main.scssandmain-rtl.scss, then can be provided as an array of filepaths. - Start collecting sourcemaps in development mode. This will be used by the
devtask. - Allow including dependencies directly from
node_modules/. In this way you can install bootstrap and include it without copying it intoassets/vendor/of your extension. - Combine media-queries. For example, you can specify desktop styles via
@media (min-width: ...) {...}in different places, and all of them will be aggregated inside a single media-query. Additionally, they will be sorted in mobile-first manner. - Add sourcemaps to the compiled CSS in dev mode.
- Minify output in production mode, but keep line-breaks. This does not affect the filesize much, but reduces number of conflicts when merging pull-requests.
- Sometimes gulp tends to skip updates of the file's modification time. Because of it, webassets may assume that styles were not changed and old version of styles will be served on the portal. To avoid this issue, enforce updating the modification time of the compiled file.
Key Settings¶
A. Environment Debugging (isDev)¶
The build pipeline checks the presence of the DEBUG environment variable
(DEBUG=1 gulp build).
- In Development: SASS source mapping is compiled alongside the CSS
(
sourcemaps.write()) to make inspection and debugging in the browser straightforward. - In Production: Stylesheets are optimized and minified using
cleanCSS(optimizations Level 2) for minimum file sizes.
B. Dependency SASS Imports (loadPaths)¶
By configuring loadPaths: ["node_modules"] in the sass options, developers
can directly @import or @use design systems, fonts, or frameworks installed
via NPM without hardcoding absolute file paths (e.g., @import
"bootstrap/scss/bootstrap").
C. Media Query Sorting (postcss-sort-media-queries)¶
In modular responsive designs, @media rules are defined inline within
multiple components. PostCSS groups all identical media query breakpoints into
a single rule block at the end of the compiled CSS file, sorting them
mobile-first to ensure proper CSS specificity.
D. WebAssets Cache Invalidation (touch())¶
CKAN's WebAssets library packages assets and caches them aggressively. WebAssets uses the file modification time (mtime) on disk to detect changes.
Because Gulp compilation runs quickly, files may sometimes be written without changing the timestamps significantly. The gulp-touch-fd pipeline (via .pipe(touch())) updates the modification date of the output CSS files, forcing CKAN to invalidate its internal cache and serve the updated stylesheet immediately.
Run tasks¶
To run the task execute
You can also check the list of available tasks:
According to the gulpfile.js, when you run any of these commands, Gulp will
compile ckanext/myextension/assets/scss/main.scss into
ckanext/myextension/assets/css/main.css.
You can also add these commands to package.json:
and execute tasks as npm run build/npm run dev. Note, you don't need to
specify DEBUG=1 anymore, because it's added by the command definition inside
package.json.
Or, if you are using Makefile to unify task execution, it's also possible to
add rules there: