build: gulp tasks and helpers use named functions

This commit is contained in:
alandefreitas
2024-08-26 19:03:30 -03:00
committed by Alan de Freitas
parent 05458871ae
commit bed2a231d3
12 changed files with 684 additions and 146 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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