mirror of
https://github.com/boostorg/website-v2-docs.git
synced 2026-01-19 04:42:17 +00:00
build: gulp tasks and helpers use named functions
This commit is contained in:
committed by
Alan de Freitas
parent
05458871ae
commit
bed2a231d3
@@ -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 '<series>'
|
||||
or '<parallel>', 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 '<series>' or '<parallel>', update the label of the function's metadata
|
||||
if (displayName === '<series>' || displayName === '<parallel>') {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('<package-name-or-require-path>'), opts).pipe(concat('js/vendor/<library-name>.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user