Files
website-v2-docs/antora-ui/gulp.d/tasks/build.js
Julio C. Estrada 36e6ab7728 simplify boostlook css importing (#432)
remove css/branch specific handling in favor of single source
2025-04-01 16:14:02 -04:00

325 lines
13 KiB
JavaScript

'use strict'
const autoprefixer = require('autoprefixer')
const browserify = require('browserify')
const concat = require('gulp-concat')
const cssnano = require('cssnano')
const fs = require('fs-extra')
const imagemin = require('gulp-imagemin')
const merge = require('merge-stream')
const ospath = require('path')
const path = ospath.posix
const postcss = require('gulp-postcss')
const postcssCalc = require('postcss-calc')
const postcssImport = require('postcss-import')
const postcssUrl = require('postcss-url')
const postcssVar = require('postcss-custom-properties')
const { Transform } = require('stream')
const map = (transform) => new Transform({ objectMode: true, transform })
const tailwind = require('tailwindcss')
const through = () => map((file, enc, next) => next(null, file))
const uglify = require('gulp-uglify')
const vfs = require('vinyl-fs')
const axios = require('axios')
const log = require('fancy-log')
/** Bundles JavaScript files using Browserify.
This function takes an object as a parameter with the base directory and
the bundle extension. It returns a map function that processes each file.
If the file ends with the bundle extension, it uses Browserify to bundle the file.
It also updates the modification time of the file if any of its dependencies
have a newer modification time.
@param {Object} options - The options for bundling.
@param {string} options.base - The base directory.
@param {string} options.ext - The bundle extension.
@returns {Function} A function that processes each file.
*/
function bundleJsFiles ({ base: basedir, ext: bundleExt = '.bundle.js' }) {
// Return a map function that processes each file
return map((file, enc, next) => {
// If the file ends with the bundle extension, bundle it
if (bundleExt && file.relative.endsWith(bundleExt)) {
const mtimePromises = []
const bundlePath = file.path
// Use Browserify to bundle the file
browserify(file.relative, { basedir, detectGlobals: false })
.plugin('browser-pack-flat/plugin')
.on('file', (bundledPath) => {
// If the bundled file is not the original file, add its modification time to the promises
if (bundledPath !== bundlePath) mtimePromises.push(fs.stat(bundledPath).then(({ mtime }) => mtime))
})
.bundle((bundleError, bundleBuffer) =>
// When all modification times are available, update the file's modification time if necessary
Promise.all(mtimePromises).then((mtimes) => {
const newestMtime = mtimes.reduce((max, curr) => (curr > max ? curr : max), file.stat.mtime)
if (newestMtime > file.stat.mtime) file.stat.mtimeMs = +(file.stat.mtime = newestMtime)
if (bundleBuffer !== undefined) file.contents = bundleBuffer
next(bundleError, Object.assign(file, { path: file.path.slice(0, file.path.length - 10) + '.js' }))
})
)
return
}
// If the file does not end with the bundle extension, just read its contents
fs.readFile(file.path, 'UTF-8').then((contents) => {
next(null, Object.assign(file, { contents: Buffer.from(contents) }))
})
})
}
/** Fixes pseudo elements in CSS.
This function walks through the CSS rules and replaces single colons with double colons
for pseudo elements. It uses a regular expression to match the pseudo elements and then
replaces the single colon with a double colon.
@param {Object} css - The PostCSS CSS object.
@param {Object} result - The result object.
*/
function postcssPseudoElementFixer (css, result) {
// Walk through the CSS rules
css.walkRules(/(?:^|[^:]):(?:before|after)/, (rule) => {
// For each rule, replace single colon with double colon for pseudo elements
rule.selector = rule.selectors.map((it) => it.replace(/(^|[^:]):(before|after)$/, '$1::$2')).join(',')
})
}
/** Returns an array of PostCSS plugins.
This function takes the destination directory and a boolean
indicating whether to generate a preview as parameters.
It returns an array of PostCSS plugins to be used for
processing CSS files.
@param {string} dest - The destination directory.
@param {boolean} preview - Whether to generate a preview.
@returns {Array} An array of PostCSS plugins.
*/
function getPostCssPlugins (dest, preview) {
// Return an array of PostCSS plugins
return [
// Import CSS files
postcssImport,
// Use Tailwind CSS
tailwind,
// Custom plugin to update the modification time of the file
(css, { messages, opts: { file } }) =>
Promise.all(
messages
.reduce((accum, { file: depPath, type }) => (type === 'dependency' ? accum.concat(depPath) : accum), [])
.map((importedPath) => fs.stat(importedPath).then(({ mtime }) => mtime))
).then((mtimes) => {
const newestMtime = mtimes.reduce((max, curr) => (!max || curr > max ? curr : max), file.stat.mtime)
if (newestMtime > file.stat.mtime) file.stat.mtimeMs = +(file.stat.mtime = newestMtime)
}),
// Resolve URLs in CSS files
postcssUrl([
{
filter: new RegExp('^src/css/[~][^/]*(?:font|face)[^/]*/.*/files/.+[.](?:ttf|woff2?)$'),
url: (asset) => {
const relpath = asset.pathname.substr(1)
const abspath = require.resolve(relpath)
const basename = ospath.basename(abspath)
const destpath = ospath.join(dest, 'font', basename)
if (!fs.pathExistsSync(destpath)) fs.copySync(abspath, destpath)
return path.join('..', 'font', basename)
},
},
]),
// Process CSS custom properties
postcssVar({ preserve: preview }),
// Calculate CSS values (only in preview mode)
preview ? postcssCalc : () => {}, // cssnano already applies postcssCalc
// Add vendor prefixes to CSS rules
autoprefixer,
// Minify CSS (only in non-preview mode)
preview
? () => {}
: (css, result) => cssnano({ preset: 'default' })(css, result).then(() => postcssPseudoElementFixer(css, result)),
]
}
/** Returns an array of tasks for building the UI.
This function takes several parameters including options for the vfs source,
whether to generate source maps, PostCSS plugins to use, whether to generate a preview,
and the source directory. It returns an array of tasks that are used to build the UI.
@param {Object} opts - The options for the vfs source.
@param {boolean} sourcemaps - Whether to generate source maps.
@param {Array} postcssPlugins - The PostCSS plugins to use.
@param {boolean} preview - Whether to generate a preview.
@param {string} src - The source directory.
@returns {Array} An array of tasks for building the UI.
*/
function getAllTasks (opts, sourcemaps, postcssPlugins, preview, src) {
// Return an array of tasks
return [
// Task for getting the 'ui.yml' file
vfs.src('ui.yml', { ...opts, allowEmpty: true }),
// Task for bundling JavaScript files
vfs
.src('js/+([0-9])-*.js', { ...opts, read: false, sourcemaps })
.pipe(bundleJsFiles(opts))
.pipe(uglify({ output: { comments: /^! / } }))
// NOTE concat already uses stat from newest combined file
.pipe(concat('js/site.js')),
// Task for bundling vendor JavaScript files
vfs
.src('js/vendor/*([^.])?(.bundle).js', { ...opts, read: false })
.pipe(bundleJsFiles(opts))
.pipe(uglify({ output: { comments: /^! / } })),
// Task for getting vendor minified JavaScript files
vfs
.src('js/vendor/*.min.js', opts)
.pipe(map((file, enc, next) => next(null, Object.assign(file, { extname: '' }, { extname: '.js' })))),
// Task for processing CSS files
// NOTE use the next line to bundle a JavaScript library that cannot be browserified, like jQuery
//vfs.src(require.resolve('<package-name-or-require-path>'), opts).pipe(concat('js/vendor/<library-name>.js')),
vfs.src('css/boostlook.css', opts).pipe(
postcss([
autoprefixer,
preview
? () => {}
: cssnano({
preset: 'default',
}),
])
),
vfs
.src(['css/site.css', 'css/vendor/*.css'], { ...opts, sourcemaps })
.pipe(postcss((file) => ({ plugins: postcssPlugins, options: { file } }))),
// Task for getting font files
vfs.src('font/*.{ttf,woff*(2)}', opts),
// Task for getting image files
vfs.src('img/**/*.{gif,ico,jpg,png,svg}', opts).pipe(
preview
? through()
: imagemin(
[
imagemin.gifsicle(),
imagemin.jpegtran(),
// imagemin.optipng(),
imagemin.svgo({
plugins: [
{ cleanupIDs: { preservePrefixes: ['icon-', 'view-'] } },
{ removeViewBox: false },
{ removeDesc: false },
],
}),
].reduce((accum, it) => (it ? accum.concat(it) : accum), [])
)
),
// Task for getting helper JavaScript files
vfs.src('helpers/*.js', opts),
// Task for getting layout handlebars files
vfs.src('layouts/*.hbs', opts),
// Task for getting partial handlebars files
vfs.src('partials/*.hbs', opts),
// Task for getting static files
vfs.src('static/**/*[!~]', { ...opts, base: ospath.join(src, 'static'), dot: true }),
]
}
/** Fetches the boostlook.css file.
This function uses axios to download the boostlook.css file.
It then writes the file to the `../../src/css` directory.
However, it first checks if the --skip-boostlook flag was set in the
command line.
If it was, it skips the download process.
This can be used to work locally on the boostlook.css file
without this process overwriting the file.
It also checks if the file already exists and was updated in
the last hour.
If it was, it also skips the download process.
This ensures contributors can work on the content with
a recent version of the boostlook.css file while still
avoiding unnecessary downloads.
In CI, the file won't be available so it always
uses the most recent version.
*/
async function fetchBoostlookCss () {
log('Fetching boostlook.css file...')
const skipBoostlook = process.argv.includes('--skip-boostlook')
const cssDir = ospath.join(__dirname, '..', '..', 'src', 'css')
const cssFilePath = ospath.join(cssDir, 'boostlook.css')
const boostlookBranch = process.env.BOOSTLOOK_BRANCH || 'master'
const url = `https://raw.githubusercontent.com/boostorg/boostlook/${boostlookBranch}/boostlook.css`
if (skipBoostlook) {
log('Skipping boostlook.css download due to --skip-boostlook flag.')
return
}
try {
const fileExists = await fs.pathExists(cssFilePath)
if (fileExists) {
const stats = await fs.stat(cssFilePath)
const oneHourAgo = Date.now() - 3600000 // 3600000 milliseconds = 1 hour
if (stats.mtimeMs > oneHourAgo) {
log('boostlook.css file already exists and was updated in the last hour. Skipping download.')
log(`boostlook.css file path: ${cssFilePath}`)
return
}
}
log('Downloading boostlook.css file...')
const response = await axios.get(url)
log('Writing boostlook.css file to ' + cssFilePath)
await fs.outputFile(cssFilePath, response.data)
log(`boostlook.css file written to ${cssFilePath} successfully.`)
} catch (error) {
console.error('Error fetching boostlook.css file:', error)
}
}
/**
Builds UI assets.
This function takes the source directory, destination directory, and a boolean
indicating whether to generate a preview as parameters.
@param {string} src - The source directory.
@param {string} dest - The destination directory.
@param {boolean} preview - Whether to generate a preview.
@returns {Function} The `UiAssetsBuilder` function.
*/
function buildUiAssets (src, dest, preview) {
// Define an async function `UiAssetsBuilder`
async function UiAssetsBuilder () {
// Prepare the options
const opts = { base: src, cwd: src }
const sourcemaps = preview || process.env.SOURCEMAPS === 'true'
// Fetch the boostlook.css file
await fetchBoostlookCss()
// Get the PostCSS plugins
const postcssPlugins = getPostCssPlugins(dest, preview)
// Get all tasks
const tasks = getAllTasks(opts, sourcemaps, postcssPlugins, preview, src)
// Merge all tasks and pipe them to the destination directory
// Return a Promise that resolves when the stream ends and
// rejects if an error occurs in the stream
return new Promise((resolve, reject) => {
merge(...tasks)
.pipe(vfs.dest(dest, { sourcemaps: sourcemaps && '.' }))
.on('end', resolve) // Resolve the Promise when the stream ends
.on('error', reject) // Reject the Promise if an error occurs in the stream
})
}
// Return the `UiAssetsBuilder` function
return UiAssetsBuilder
}
module.exports = buildUiAssets