From bed2a231d38b4cdce420373e537d365bc88f1254 Mon Sep 17 00:00:00 2001 From: alandefreitas Date: Mon, 26 Aug 2024 19:03:30 -0300 Subject: [PATCH] build: gulp tasks and helpers use named functions --- antora-ui/gulp.d/lib/create-task.js | 50 +++- antora-ui/gulp.d/lib/export-tasks.js | 37 ++- antora-ui/gulp.d/tasks/build-preview-pages.js | 248 +++++++++++++----- antora-ui/gulp.d/tasks/build.js | 192 +++++++++++--- antora-ui/gulp.d/tasks/format-css.js | 45 +++- antora-ui/gulp.d/tasks/format-js.js | 32 ++- antora-ui/gulp.d/tasks/index.js | 19 +- antora-ui/gulp.d/tasks/lint-css.js | 35 ++- antora-ui/gulp.d/tasks/lint-js.js | 39 ++- antora-ui/gulp.d/tasks/pack.js | 35 ++- antora-ui/gulp.d/tasks/remove.js | 46 +++- antora-ui/gulp.d/tasks/serve.js | 52 +++- 12 files changed, 684 insertions(+), 146 deletions(-) diff --git a/antora-ui/gulp.d/lib/create-task.js b/antora-ui/gulp.d/lib/create-task.js index 8c90062..3df8ba3 100644 --- a/antora-ui/gulp.d/lib/create-task.js +++ b/antora-ui/gulp.d/lib/create-task.js @@ -3,14 +3,54 @@ const metadata = require('undertaker/lib/helpers/metadata') const { watch } = require('gulp') -module.exports = ({ name, desc, opts, call: fn, loop }) => { +/** This function creates a Gulp task from an object with the task properties. + + A Gulp task is a function that can be run from the command line + using Gulp CLI. + The task can perform various operations such as file manipulation, + code transpiling, etc. + + This function takes an object as an argument, which contains + properties that define the task. + The properties include the task's name, description, options, + the function to be called when the task is run, and the files + or directories to watch. + + The returned task is a function object with extra fields + defining values required to export the task as a Gulp task. + + The function does the following operations: + - If a name is provided, it sets the displayName of the + function to the provided name. + - If the displayName of the function is '' + or '', it updates the label of the function's metadata. + - If a loop is provided, it creates a watch task. + The watch task will run the original task whenever the specified + files or directories change. + - If a description is provided, it sets the description of the function. + - If options are provided, it sets the flags of the function. + + @param {Object} taskObj - The object containing the task properties. + @param {string} taskObj.name - The name of the task. + @param {string} taskObj.desc - The description of the task. + @param {Object} taskObj.opts - The options for the task. + @param {Function} taskObj.call - The function to be called when the task is run. + @param {Array} taskObj.loop - The files or directories to watch. + + @returns {Function} The created Gulp task. + */ +function createTask ({ name, desc, opts, call: fn, loop }) { + // If a name is provided, set the displayName of the function to the provided name if (name) { const displayName = fn.displayName + // If the displayName of the function is '' or '', update the label of the function's metadata if (displayName === '' || displayName === '') { metadata.get(fn).tree.label = `${displayName} ${name}` } fn.displayName = name } + + // If a loop is provided, create a watch task if (loop) { const delegate = fn name = delegate.displayName @@ -18,7 +58,15 @@ module.exports = ({ name, desc, opts, call: fn, loop }) => { fn = () => watch(loop, { ignoreInitial: false }, delegate) fn.displayName = name } + + // If a description is provided, set the description of the function if (desc) fn.description = desc + + // If options are provided, set the flags of the function if (opts) fn.flags = opts + + // Return the created task return fn } + +module.exports = createTask diff --git a/antora-ui/gulp.d/lib/export-tasks.js b/antora-ui/gulp.d/lib/export-tasks.js index 7c9de48..8ce11e1 100644 --- a/antora-ui/gulp.d/lib/export-tasks.js +++ b/antora-ui/gulp.d/lib/export-tasks.js @@ -1,14 +1,49 @@ 'use strict' -module.exports = (...tasks) => { +/** This function exports multiple Gulp tasks at once. + + If the first task appears more than once in the list of tasks, + it removes the first task from the list and assigns it to the + `default` property of the returned object. + + This means that the first task in the list will be the + default task that Gulp runs when no task name is provided + in the command line. + + If no tasks are provided, it simply returns an empty object. + + @param {...Function} tasks - The tasks to be exported. + + @returns {Object} An object where each task is a property + of the object. + + The property key is the task's name, and the value is + the task itself. + + */ +function exportTasks (...tasks) { + // Initialize an empty object const seed = {} + + // Check if there are tasks provided if (tasks.length) { + // Check if the first task appears more than once in the list of tasks if (tasks.lastIndexOf(tasks[0]) > 0) { + // Remove the first task from the list const task1 = tasks.shift() + + // Assign the first task to the `default` property of the `seed` object seed.default = Object.assign(task1.bind(null), { description: `=> ${task1.displayName}`, displayName: 'default' }) } + + // Reduce the remaining tasks into the `seed` object + // Each task is added as a property of the `seed` object + // The task's name is the property key and the task itself is the value return tasks.reduce((acc, it) => (acc[it.displayName || it.name] = it) && acc, seed) } else { + // If no tasks are provided, return the `seed` object return seed } } + +module.exports = exportTasks diff --git a/antora-ui/gulp.d/tasks/build-preview-pages.js b/antora-ui/gulp.d/tasks/build-preview-pages.js index 364f561..25706a4 100644 --- a/antora-ui/gulp.d/tasks/build-preview-pages.js +++ b/antora-ui/gulp.d/tasks/build-preview-pages.js @@ -14,70 +14,26 @@ const yaml = require('js-yaml') const ASCIIDOC_ATTRIBUTES = { experimental: '', icons: 'font', sectanchors: '', 'source-highlighter': 'highlight.js' } -module.exports = (src, previewSrc, previewDest, sink = () => map()) => (done) => - Promise.all([ - loadSampleUiModel(previewSrc), - toPromise( - merge(compileLayouts(src), registerPartials(src), registerHelpers(src), copyImages(previewSrc, previewDest)) - ), - ]) - .then(([baseUiModel, { layouts }]) => { - const extensions = ((baseUiModel.asciidoc || {}).extensions || []).map((request) => { - ASCIIDOC_ATTRIBUTES[request.replace(/^@|\.js$/, '').replace(/[/]/g, '-') + '-loaded'] = '' - const extension = require(request) - extension.register.call(Asciidoctor.Extensions) - return extension - }) - const asciidoc = { extensions } - for (const component of baseUiModel.site.components) { - for (const version of component.versions || []) version.asciidoc = asciidoc - } - baseUiModel = { ...baseUiModel, env: process.env } - delete baseUiModel.asciidoc - return [baseUiModel, layouts] - }) - .then(([baseUiModel, layouts]) => - vfs - .src('**/*.adoc', { base: previewSrc, cwd: previewSrc }) - .pipe( - map((file, enc, next) => { - const siteRootPath = path.relative(ospath.dirname(file.path), ospath.resolve(previewSrc)) - const uiModel = { ...baseUiModel } - uiModel.page = { ...uiModel.page } - uiModel.siteRootPath = siteRootPath - uiModel.uiRootPath = path.join(siteRootPath, '_') - if (file.stem === '404') { - uiModel.page = { layout: '404', title: 'Page Not Found' } - } else { - const doc = Asciidoctor.load(file.contents, { safe: 'safe', attributes: ASCIIDOC_ATTRIBUTES }) - uiModel.page.attributes = Object.entries(doc.getAttributes()) - .filter(([name, val]) => name.startsWith('page-')) - .reduce((accum, [name, val]) => { - accum[name.substr(5)] = val - return accum - }, {}) - uiModel.page.layout = doc.getAttribute('page-layout', 'default') - uiModel.page.title = doc.getDocumentTitle() - uiModel.page.contents = Buffer.from(doc.convert()) - } - file.extname = '.html' - try { - file.contents = Buffer.from(layouts.get(uiModel.page.layout)(uiModel)) - next(null, file) - } catch (e) { - next(transformHandlebarsError(e, uiModel.page.layout)) - } - }) - ) - .pipe(vfs.dest(previewDest)) - .on('error', done) - .pipe(sink()) - ) +/** Load the sample UI model from a YAML file. + This function reads the 'ui-model.yml' file from the + specified source directory and parses its contents as YAML. + + @param {string} src - The source directory. + @returns {Promise} A promise that resolves with the parsed YAML data. + */ function loadSampleUiModel (src) { return fs.readFile(ospath.join(src, 'ui-model.yml'), 'utf8').then((contents) => yaml.safeLoad(contents)) } +/** Register Handlebars partials. + + This function reads Handlebars partial files from the specified source directory + and registers them as partials. + + @param {string} src - The source directory. + @returns {Stream} A stream that ends when all partials have been registered. + */ function registerPartials (src) { return vfs.src('partials/*.hbs', { base: src, cwd: src }).pipe( map((file, enc, next) => { @@ -87,6 +43,14 @@ function registerPartials (src) { ) } +/** Register Handlebars helpers. + + This function reads JavaScript files from the specified source directory + and registers them as Handlebars helpers. + + @param {string} src - The source directory. + @returns {Stream} A stream that ends when all helpers have been registered. + */ function registerHelpers (src) { handlebars.registerHelper('resolvePage', resolvePage) handlebars.registerHelper('resolvePageURL', resolvePageURL) @@ -98,6 +62,14 @@ function registerHelpers (src) { ) } +/** Compile Handlebars layouts. + + This function reads Handlebars layout files from the specified source directory + and compiles them. + + @param {string} src - The source directory. + @returns {Stream} A stream that ends when all layouts have been compiled. + */ function compileLayouts (src) { const layouts = new Map() return vfs.src('layouts/*.hbs', { base: src, cwd: src }).pipe( @@ -115,6 +87,15 @@ function compileLayouts (src) { ) } +/** Copy images. + + This function copies image files from the specified source directory + to the specified destination directory. + + @param {string} src - The source directory. + @param {string} dest - The destination directory. + @returns {Stream} A stream that ends when all images have been copied. + */ function copyImages (src, dest) { return vfs .src('**/*.{png,svg}', { base: src, cwd: src }) @@ -122,14 +103,38 @@ function copyImages (src, dest) { .pipe(map((file, enc, next) => next())) } +/** Resolve a page. + + This function resolves a page specification to a URL. + + @param {string} spec - The page specification. + @param {Object} context - The context (not used). + @returns {Object} An object with a 'pub' property containing the resolved URL. + */ function resolvePage (spec, context = {}) { if (spec) return { pub: { url: resolvePageURL(spec) } } } +/** Resolve a page URL. + + This function resolves a page specification to a URL. + + @param {string} spec - The page specification. + @param {Object} context - The context (not used). + @returns {string} The resolved URL. + */ function resolvePageURL (spec, context = {}) { if (spec) return '/' + (spec = spec.split(':').pop()).slice(0, spec.lastIndexOf('.')) + '.html' } +/** Transform a Handlebars error. + + This function transforms a Handlebars error into a more readable format. + + @param {Object} error - The error object. + @param {string} layout - The layout that caused the error. + @returns {Error} The transformed error. + */ function transformHandlebarsError ({ message, stack }, layout) { const m = stack.match(/^ *at Object\.ret \[as (.+?)\]/m) const templatePath = `src/${m ? 'partials/' + m[1] : 'layouts/' + layout}.hbs` @@ -138,6 +143,13 @@ function transformHandlebarsError ({ message, stack }, layout) { return err } +/** Convert a stream to a promise. + + This function converts a stream to a promise that resolves when the stream ends. + + @param {Stream} stream - The stream. + @returns {Promise} A promise that resolves when the stream ends. + */ function toPromise (stream) { return new Promise((resolve, reject, data = {}) => stream @@ -146,3 +158,121 @@ function toPromise (stream) { .on('finish', () => resolve(data)) ) } + +/** Process base UI model. + + This function processes the base UI model and returns it along with the layouts. + + @param {Object} baseUiModel - The base UI model. + @param {Object} layouts - The layouts. + @returns {Array} An array containing the processed base UI model and the layouts. + */ +function processBaseUiModel (baseUiModel, layouts) { + const extensions = ((baseUiModel.asciidoc || {}).extensions || []).map((request) => { + ASCIIDOC_ATTRIBUTES[request.replace(/^@|\.js$/, '').replace(/[/]/g, '-') + '-loaded'] = '' + const extension = require(request) + extension.register.call(Asciidoctor.Extensions) + return extension + }) + const asciidoc = { extensions } + for (const component of baseUiModel.site.components) { + for (const version of component.versions || []) version.asciidoc = asciidoc + } + baseUiModel = { ...baseUiModel, env: process.env } + delete baseUiModel.asciidoc + return [baseUiModel, layouts] +} + +/** Build pages. + + This function builds the pages and writes the converted files back + to their original location. + + @param {string} previewSrc - The preview source directory. + @param {Object} baseUiModel - The base UI model. + @param {Object} layouts - The layouts. + @param {string} previewDest - The preview destination directory. + @param {Function} done - The done callback. + @param {Function} sink - The sink function. + */ +function buildAsciidocPages (previewSrc, baseUiModel, layouts, previewDest, done, sink) { + return vfs + .src('**/*.adoc', { base: previewSrc, cwd: previewSrc }) + .pipe( + map((file, enc, next) => { + const siteRootPath = path.relative(ospath.dirname(file.path), ospath.resolve(previewSrc)) + const uiModel = { ...baseUiModel } + uiModel.page = { ...uiModel.page } + uiModel.siteRootPath = siteRootPath + uiModel.uiRootPath = path.join(siteRootPath, '_') + if (file.stem === '404') { + uiModel.page = { layout: '404', title: 'Page Not Found' } + } else { + const doc = Asciidoctor.load(file.contents, { safe: 'safe', attributes: ASCIIDOC_ATTRIBUTES }) + uiModel.page.attributes = Object.entries(doc.getAttributes()) + .filter(([name, val]) => name.startsWith('page-')) + .reduce((accum, [name, val]) => { + accum[name.substr(5)] = val + return accum + }, {}) + uiModel.page.layout = doc.getAttribute('page-layout', 'default') + uiModel.page.title = doc.getDocumentTitle() + uiModel.page.contents = Buffer.from(doc.convert()) + } + file.extname = '.html' + try { + file.contents = Buffer.from(layouts.get(uiModel.page.layout)(uiModel)) + next(null, file) + } catch (e) { + next(transformHandlebarsError(e, uiModel.page.layout)) + } + }) + ) + .pipe(vfs.dest(previewDest)) + .on('error', done) + .pipe(sink()) +} + +/** Build preview pages. + + This function uses Asciidoctor to convert AsciiDoc files to HTML for preview. + It creates a stream of AsciiDoc files from the specified glob pattern, converts + the files to HTML using Asciidoctor, and then writes the converted files back + to their original location. + + @param {string} src - The source directory. + @param {string} previewSrc - The preview source directory. + @param {string} previewDest - The preview destination directory. + @param {Function} sink - The sink function. + @returns {Function} A function that builds the preview pages when called. + */ +function buildPreviewPages (src, previewSrc, previewDest, sink = () => map()) { + async function buildPages (done) { + try { + // Use Promise.all to run multiple basic tasks in parallel + const [baseUiModel, { layouts }] = await Promise.all([ + // Load the sample UI model from a YAML file + loadSampleUiModel(previewSrc), + // Merge multiple streams into one and convert it to a promise + // The streams are created by compiling layouts, registering partials + // and helpers, and copying images + toPromise( + merge(compileLayouts(src), registerPartials(src), registerHelpers(src), copyImages(previewSrc, previewDest)) + ), + ]) + + // Process the base UI model and get the processed base UI model and layouts + const [processedBaseUiModel, processedLayouts] = processBaseUiModel(baseUiModel, layouts) + + // Build the AsciiDoc pages and write the converted files back to their original location + await buildAsciidocPages(previewSrc, processedBaseUiModel, processedLayouts, previewDest, done, sink) + } catch (error) { + // If an error occurs during the execution of the promises, it will be caught here + done(error) + } + } + + return buildPages +} + +module.exports = buildPreviewPages diff --git a/antora-ui/gulp.d/tasks/build.js b/antora-ui/gulp.d/tasks/build.js index 3c7e8e7..c255d37 100644 --- a/antora-ui/gulp.d/tasks/build.js +++ b/antora-ui/gulp.d/tasks/build.js @@ -21,12 +21,88 @@ const through = () => map((file, enc, next) => next(null, file)) const uglify = require('gulp-uglify') const vfs = require('vinyl-fs') -module.exports = (src, dest, preview) => () => { - const opts = { base: src, cwd: src } - const sourcemaps = preview || process.env.SOURCEMAPS === 'true' - const postcssPlugins = [ +/** 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 @@ -36,6 +112,7 @@ module.exports = (src, dest, preview) => () => { 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?)$'), @@ -49,37 +126,62 @@ module.exports = (src, dest, preview) => () => { }, }, ]), + // Process CSS custom properties postcssVar({ preserve: preview }), - // NOTE to make vars.css available to all top-level stylesheets, use the next line in place of the previous one - //postcssVar({ importFrom: path.join(src, 'css', 'vars.css'), 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)), ] +} - return merge( +/** 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(bundle(opts)) + .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(bundle(opts)) + .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(''), opts).pipe(concat('js/vendor/.js')), 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() @@ -98,41 +200,53 @@ module.exports = (src, dest, preview) => () => { ].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), - vfs.src('static/**/*[!~]', { ...opts, base: ospath.join(src, 'static'), dot: true }) - ).pipe(vfs.dest(dest, { sourcemaps: sourcemaps && '.' })) + // Task for getting static files + vfs.src('static/**/*[!~]', { ...opts, base: ospath.join(src, 'static'), dot: true }), + ] } -function bundle ({ base: basedir, ext: bundleExt = '.bundle.js' }) { - return map((file, enc, next) => { - if (bundleExt && file.relative.endsWith(bundleExt)) { - const mtimePromises = [] - const bundlePath = file.path - browserify(file.relative, { basedir, detectGlobals: false }) - .plugin('browser-pack-flat/plugin') - .on('file', (bundledPath) => { - if (bundledPath !== bundlePath) mtimePromises.push(fs.stat(bundledPath).then(({ mtime }) => mtime)) - }) - .bundle((bundleError, bundleBuffer) => - 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 - } - fs.readFile(file.path, 'UTF-8').then((contents) => { - next(null, Object.assign(file, { contents: Buffer.from(contents) })) +/** + 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' + + // 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 }) - }) -} + } -function postcssPseudoElementFixer (css, result) { - css.walkRules(/(?:^|[^:]):(?:before|after)/, (rule) => { - rule.selector = rule.selectors.map((it) => it.replace(/(^|[^:]):(before|after)$/, '$1::$2')).join(',') - }) + // Return the `UiAssetsBuilder` function + return UiAssetsBuilder } +module.exports = buildUiAssets diff --git a/antora-ui/gulp.d/tasks/format-css.js b/antora-ui/gulp.d/tasks/format-css.js index def5e19..26b9df6 100644 --- a/antora-ui/gulp.d/tasks/format-css.js +++ b/antora-ui/gulp.d/tasks/format-css.js @@ -3,17 +3,38 @@ const stylelint = require('gulp-stylelint') const vfs = require('vinyl-fs') -module.exports = (files) => async () => { - const prettier = (await import('gulp-prettier')).default +/** + Format CSS files. - vfs - .src(files) - .pipe(prettier()) // First format the CSS files with Prettier - .pipe( - stylelint({ - fix: true, // Automatically fix Stylelint issues - reporters: [{ formatter: 'string', console: true }], - }) - ) - .pipe(vfs.dest((file) => file.base)) // Write the changes back to the files + This function uses gulp-prettier-eslint and gulp-stylelint to format and lint the specified CSS files. + It creates a stream of files from the specified glob pattern, pipes + the files to gulp-prettier-eslint to format them, then pipes them to gulp-stylelint to lint them, + and finally writes the formatted and linted files back to their original location. + + @param {Array|string} files - The glob pattern(s) of the files to format. + @returns {Function} A function that formats the files when called. +*/ +function formatCss (files) { + async function formatFiles () { + const prettier = (await import('gulp-prettier')).default + + // Create a stream of files from the specified glob pattern + vfs + .src(files) + // Pipe the files to gulp-prettier-eslint to format them + .pipe(prettier()) + // Pipe the files to gulp-stylelint to lint them + .pipe( + stylelint({ + fix: true, // Automatically fix Stylelint issues + reporters: [{ formatter: 'string', console: true }], + }) + ) + // Write the formatted and linted files back to their original location + .pipe(vfs.dest((file) => file.base)) + } + + return formatFiles } + +module.exports = formatCss diff --git a/antora-ui/gulp.d/tasks/format-js.js b/antora-ui/gulp.d/tasks/format-js.js index 2d50496..8cb0841 100644 --- a/antora-ui/gulp.d/tasks/format-js.js +++ b/antora-ui/gulp.d/tasks/format-js.js @@ -3,8 +3,30 @@ const prettier = require('../lib/gulp-prettier-eslint') const vfs = require('vinyl-fs') -module.exports = (files) => () => - vfs - .src(files) - .pipe(prettier()) - .pipe(vfs.dest((file) => file.base)) +/** Format JavaScript files. + + This function uses gulp-prettier-eslint to format the specified JavaScript files. + It creates a stream of files from the specified glob pattern, pipes + the files to gulp-prettier-eslint to format them, and then writes + the formatted files back to their original location. + + @param {Array|string} files - The glob pattern(s) of the files to format. + @returns {Function} A function that formats the files when called. +*/ +function formatJs (files) { + function formatFiles () { + // Create a stream of files from the specified glob pattern + return ( + vfs + .src(files) + // Pipe the files to gulp-prettier-eslint to format them + .pipe(prettier()) + // Write the formatted files back to their original location + .pipe(vfs.dest((file) => file.base)) + ) + } + + return formatFiles +} + +module.exports = formatJs diff --git a/antora-ui/gulp.d/tasks/index.js b/antora-ui/gulp.d/tasks/index.js index a5795fc..48e65c2 100644 --- a/antora-ui/gulp.d/tasks/index.js +++ b/antora-ui/gulp.d/tasks/index.js @@ -1,5 +1,20 @@ 'use strict' -const camelCase = (name) => name.replace(/[-]./g, (m) => m.substr(1).toUpperCase()) +/** Converts a string to camel case. -module.exports = require('require-directory')(module, __dirname, { recurse: false, rename: camelCase }) + @param {string} name - The string to convert to camel case. + @returns {string} The string converted to camel case. + */ +function camelCase (name) { + return name.replace(/[-]./g, (m) => m.substr(1).toUpperCase()) +} + +/** Exports all modules in the current directory. + + @module index + */ +function exportModules () { + return require('require-directory')(module, __dirname, { recurse: false, rename: camelCase }) +} + +module.exports = exportModules() diff --git a/antora-ui/gulp.d/tasks/lint-css.js b/antora-ui/gulp.d/tasks/lint-css.js index d684014..37f8f40 100644 --- a/antora-ui/gulp.d/tasks/lint-css.js +++ b/antora-ui/gulp.d/tasks/lint-css.js @@ -3,8 +3,33 @@ const stylelint = require('gulp-stylelint') const vfs = require('vinyl-fs') -module.exports = (files) => (done) => - vfs - .src(files) - .pipe(stylelint({ reporters: [{ formatter: 'string', console: true }], failAfterError: true })) - .on('error', done) +/** Lint CSS files. + + This function uses gulp-stylelint to lint the specified CSS files. + + It creates a stream of files from the specified glob pattern, pipes + the files to gulp-stylelint to lint them, and then formats and prints + the linting results. + + If any linting errors are found, it emits an error event. + + @param {Array|string} files - The glob pattern(s) of the files to lint. + @returns {Function} A function that lints the files when called. +*/ +function lintCss (files) { + function lintFiles (done) { + // Create a stream of files from the specified glob pattern + return ( + vfs + .src(files) + // Pipe the files to gulp-stylelint to lint them + .pipe(stylelint({ reporters: [{ formatter: 'string', console: true }], failAfterError: true })) + // If any linting errors are found, emit an error event + .on('error', done) + ) + } + + return lintFiles +} + +module.exports = lintCss diff --git a/antora-ui/gulp.d/tasks/lint-js.js b/antora-ui/gulp.d/tasks/lint-js.js index ef4f3c9..ddaab4e 100644 --- a/antora-ui/gulp.d/tasks/lint-js.js +++ b/antora-ui/gulp.d/tasks/lint-js.js @@ -3,10 +3,35 @@ const eslint = require('gulp-eslint') const vfs = require('vinyl-fs') -module.exports = (files) => (done) => - vfs - .src(files) - .pipe(eslint()) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()) - .on('error', done) +/** Lint JavaScript files. + + This function uses gulp-eslint to lint the specified JavaScript files. + It creates a stream of files from the specified glob pattern, pipes + the files to gulp-eslint to lint them, and then formats and prints + the linting results. + + If any linting errors are found, it emits an error event. + + @param {Array|string} files - The glob pattern(s) of the files to lint. + @returns {Function} A function that lints the files when called. +*/ +function lintJs (files) { + function lintFiles (done) { + // Create a stream of files from the specified glob pattern + return ( + vfs + .src(files) + // Pipe the files to gulp-eslint to lint them + .pipe(eslint()) + // Format and print the linting results + .pipe(eslint.format()) + // If any linting errors are found, emit an error event + .pipe(eslint.failAfterError()) + .on('error', done) + ) + } + + return lintFiles +} + +module.exports = lintJs diff --git a/antora-ui/gulp.d/tasks/pack.js b/antora-ui/gulp.d/tasks/pack.js index a792e72..afd34d7 100644 --- a/antora-ui/gulp.d/tasks/pack.js +++ b/antora-ui/gulp.d/tasks/pack.js @@ -4,8 +4,33 @@ const vfs = require('vinyl-fs') const zip = require('gulp-vinyl-zip') const path = require('path') -module.exports = (src, dest, bundleName, onFinish) => () => - vfs - .src('**/*', { base: src, cwd: src }) - .pipe(zip.dest(path.join(dest, `${bundleName}-bundle.zip`))) - .on('finish', () => onFinish && onFinish(path.resolve(dest, `${bundleName}-bundle.zip`))) +/** Creates a zip file from the specified source directory. + + This function uses gulp-vinyl-zip to create a zip file from the specified source directory. + The zip file is saved in the specified destination directory with a name based on the provided bundle name. + When the zip file is created, it calls the onFinish callback with the path of the zip file. + + @param {string} src - The source directory. + @param {string} dest - The destination directory. + @param {string} bundleName - The name of the bundle. + @param {Function} onFinish - The callback function to call when the zip file is created. + + @returns {Function} A function that creates the zip file when called. + */ +function pack (src, dest, bundleName, onFinish) { + function createZipFile () { + // Create a stream of files from the source directory + return ( + vfs + .src('**/*', { base: src, cwd: src }) + // Pipe the files to gulp-vinyl-zip to create a zip file + .pipe(zip.dest(path.join(dest, `${bundleName}-bundle.zip`))) + // When the zip file is created, call the onFinish callback with the path of the zip file + .on('finish', () => onFinish && onFinish(path.resolve(dest, `${bundleName}-bundle.zip`))) + ) + } + + return createZipFile +} + +module.exports = pack diff --git a/antora-ui/gulp.d/tasks/remove.js b/antora-ui/gulp.d/tasks/remove.js index 71a8dac..7a5bcea 100644 --- a/antora-ui/gulp.d/tasks/remove.js +++ b/antora-ui/gulp.d/tasks/remove.js @@ -2,8 +2,48 @@ const fs = require('fs-extra') const { Transform } = require('stream') -const map = (transform) => new Transform({ objectMode: true, transform }) const vfs = require('vinyl-fs') -module.exports = (files) => () => - vfs.src(files, { allowEmpty: true }).pipe(map((file, enc, next) => fs.remove(file.path, next))) +/** Creates a transform stream that applies a given function to each file. + + @param {Function} transformFunction - The function to be applied to each file. + @returns {Transform} A transform stream. + */ +function createTransformStream (transformFunction) { + return new Transform({ objectMode: true, transform: transformFunction }) +} + +/** Removes a file. + + @param {Object} file - The file to be removed. + @param {string} enc - The encoding used. + @param {Function} next - The callback function to be called after the file is removed. + */ +function removeFile (file, enc, next) { + fs.remove(file.path, next) +} + +/** Creates a stream of files and applies a given function to each file. + + @param {Array} files - The files to be processed. + @param {Function} processFunction - The function to be applied to each file. + @returns {Stream} A stream of files. + */ +function processFiles (files, processFunction) { + return vfs.src(files, { allowEmpty: true }).pipe(processFunction) +} + +/** Exports a function that removes given files. + + @param {Array} files - The files to be removed. + @returns {Function} A function that removes the given files when called. + */ +function removeFiles (files) { + function removeFilesFunction () { + return processFiles(files, createTransformStream(removeFile)) + } + + return removeFilesFunction +} + +module.exports = removeFiles diff --git a/antora-ui/gulp.d/tasks/serve.js b/antora-ui/gulp.d/tasks/serve.js index 942c673..a1d3117 100644 --- a/antora-ui/gulp.d/tasks/serve.js +++ b/antora-ui/gulp.d/tasks/serve.js @@ -3,19 +3,26 @@ const connect = require('gulp-connect') const os = require('os') +// Constants const ANY_HOST = '0.0.0.0' const URL_RX = /(https?):\/\/(?:[^/: ]+)(:\d+)?/ -module.exports = (root, opts = {}, watch = undefined) => (done) => { - connect.server({ ...opts, middleware: opts.host === ANY_HOST ? decorateLog : undefined, root }, function () { - this.server.on('close', done) - if (watch) watch() - }) -} +/** Decorate the log messages of gulp-connect. + This function replaces the log function of gulp-connect + with a new function that modifies the log messages. + + If a message starts with 'Server started ', + it replaces the URL in the message with 'localhost' and + the local IP address. + + @param {Object} _ - The request object (not used). + @param {Object} app - The gulp-connect app. + @returns {Array} An empty array (required by gulp-connect). + */ function decorateLog (_, app) { const _log = app.log - app.log = (msg) => { + app.log = function modifyLogMessage (msg) { if (msg.startsWith('Server started ')) { const localIp = getLocalIp() const replacement = '$1://localhost$2' + (localIp ? ` and $1://${localIp}$2` : '') @@ -26,6 +33,14 @@ function decorateLog (_, app) { return [] } +/** Get the local IP address. + + This function iterates over the network interfaces of + the OS and returns the first non-internal IPv4 address it finds. + If no such address is found, it returns 'localhost'. + + @returns {string} The local IP address or 'localhost'. + */ function getLocalIp () { for (const records of Object.values(os.networkInterfaces())) { for (const record of records) { @@ -34,3 +49,26 @@ function getLocalIp () { } return 'localhost' } + +/** Serve a directory using gulp-connect. + + This function starts a server using gulp-connect to serve the specified root directory. + It also sets up a middleware to decorate the log messages if the host is ANY_HOST. + When the server is closed, it calls the done callback. + If a watch function is provided, it is called after the server is started. + + @param {string} root - The root directory to serve. + @param {Object} opts - The options for gulp-connect. + @param {Function} watch - The function to call after the server is started. + @returns {Function} A function that starts the server when called. + */ +function serve (root, opts = {}, watch = undefined) { + return function startServer (done) { + connect.server({ ...opts, middleware: opts.host === ANY_HOST ? decorateLog : undefined, root }, function () { + this.server.on('close', done) + if (watch) watch() + }) + } +} + +module.exports = serve