I'm encountering an issue with my Gulpfile: when I save changes, the console indicates that both JS and CSS are successfully compiled. However, the updated results aren't reflected on the frontend until I manually restart the server using "yarn dev." Essentially, to see my code changes, I have to run "yarn dev" after each modification.
I've attached my Gulpfile for reference. I'm seeking insights into what might be causing this issue and how I can resolve it. Any help or suggestions would be greatly appreciated!
'use strict';
const packageFile = require('./package.json');
const version = `v${packageFile.version}`;
const errorsFail = process.argv.indexOf('--errors-fail') > -1;
// Sync is true by default; if --no-sync is passed, then sync is false.
const sync = process.argv.indexOf('--no-sync') === -1;
// Gulp-related tasks.
// Utilities
const del = require('del');
const gulp = require('gulp');
// Fractal
const fractal = require('./fractal.js');
const logger = fractal.cli.console;
// Images and icons
const imagemin = require('gulp-imagemin');
const svgSprite = require('gulp-svg-sprite');
// Misc
const cache = require('gulp-memory-cache');
const changed = require('gulp-changed');
const count = require('gulp-count');
const deleted = require('gulp-deleted');
const { exec, execSync } = require('child_process');
const gulpIf = require('gulp-if');
const path = require('path');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const sourcemaps = require('gulp-sourcemaps');
// JavaScript
const babel = require('gulp-babel');
const concat = require('gulp-concat');
const eslint = require('gulp-eslint');
const uglify = require('gulp-uglify');
// CSS
const autoprefixer = require('gulp-autoprefixer');
const cleanCSS = require('gulp-clean-css');
const criticalSplit = require('postcss-critical-split');
const glob = require('gulp-sass-glob');
const postCSS = require('gulp-postcss');
const sass = require('gulp-sass');
const sassLint = require('gulp-sass-lint');
const Fiber = require('fibers');
// Stubs server
const stubServer = require('./stubs/server');
// opt for primary implementation (dart sass)
sass.compiler = require('sass');
// Files to process.
// Dummy product data.
const productData = [
'src/assets/js/product-data.js',
'src/assets/js/product-data.json'
];
// Icons.
const componentIcons = [
'src/assets/icons/**/*.svg'
];
// Flag Icons.
const flagIcons = [
'src/assets/icons/flags/*.svg'
]
// Images.
const componentImages = [
'src/assets/images/**/*'
];
// Videos.
const componentVideos = [
'src/assets/videos/**/*'
];
// Fonts.
const componentFonts = [
'src/assets/fonts/*'
];
// Pdfs.
const componentPdfs = [
'src/assets/pdfs/*'
];
const staticAssets = [
// This includes:
// - CSS assets we don’t control.
// - Dummy HTML for components w/iframe elements.
'src/assets/static/**/*',
];
// BE component js files--ignores *.json!
const configJs = [
'src/components/**/*.config.js'
];
// FE base js files for all components.
const baseJs = [
'node_modules/ev-emitter/ev-emitter.js',
'node_modules/object-fit-images/dist/ofi.js',
'node_modules/headroom.js/dist/headroom.js',
'src/assets/js/capture-metadata.js',
];
// FE component js files.
const componentJs = baseJs.concat([
'node_modules/imagesloaded/imagesloaded.js',
'node_modules/tiny-slider/dist/tiny-slider.js',
'node_modules/intl-tel-input/build/js/intlTelInput.js',
'node_modules/intl-tel-input/build/js/utils.js',
'node_modules/promise-polyfill/dist/polyfill.js',
'src/assets/js/capture-metadata.js',
'src/assets/js/app.js',
'src/assets/js/trade-app-api.js',
'src/components/**/*.js',
'!src/components/_backlog/**/*.js',
// Deprecated components should not be in the build.
'!src/components/04-deprecated/**/*',
// Dealer profiles JS is handled separately.
'!src/components/03-pages/dealer-profiles/**/*.js',
'!src/components/**/*.config.js'
]);
// FE b2b-specific component js files.
const b2bJs = baseJs.concat([
'src/assets/js/app.js',
'src/components/01-elements/**/checkbox.js',
'src/components/01-elements/**/default-modal.js',
'src/components/01-elements/**/select-normal.js',
'src/components/01-elements/**/shipping-dropdown.js',
'src/components/01-elements/**/tooltip.js',
'src/components/02-components/**/header-flyout.js',
'src/components/02-components/**/mini-cart.js',
'src/components/02-components/**/mini-search.js',
'src/components/02-components/**/shopping-cart.js',
]);
const dealerProfilesJs = [
'src/components/03-pages/dealer-profiles/**/*.js',
'!src/components/03-pages/dealer-profiles/**/*.config.js',
];
const stylesheetSwitcherJs = [
'./node_modules/stylesheet-switcher/public/dist/sss.js',
'./node_modules/stylesheet-switcher/public/dist/sss.js.map'
];
const vendorJs = [
'./node_modules/fg-loadcss/dist/cssrelpreload.min.js',
];
// External sass to include.
const sassIncludePaths = [
// Plugins.
'./node_modules/breakpoint-sass/stylesheets',
'./node_modules/susy/sass',
// Third party styles.
'./node_modules/tiny-slider/dist',
'./node_modules/intl-tel-input/src/css',
];
// Sass.
const allScss = [
'src/**/*.scss',
'!src/components/04-deprecated/**/*.scss',
];
// Fractal theme files.
const themeScss = [
'./src/assets/scss/theme.scss'
];
const componentScss = [
'src/assets/scss/*.scss',
'!src/assets/scss/fractal/*.scss',
`!${themeScss[0]}`
];
const componentGeneratedCss = componentScss
.map(glob => glob.replace(/scss/g, 'css').replace(/src/g, 'tmp'));
componentGeneratedCss.push('!./tmp/assets/css/preview.css');
componentGeneratedCss.push('!./tmp/assets/css/*critical.css');
// Functions.
// Creates a string showing the current tag & commit and # of commits since.
function timestamp() {
try {
// Find out the latest tag in the local repo.
const latestTag = execSync('git tag --sort version:refname | tail -1').toString().trim();
// Get the name of the current branch.
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
// Get the number of commits since the latest tag ON THE CURRENT BRANCH.
const postReleaseCommits = parseInt(execSync(`git rev-list ${latestTag}..${currentBranch} | wc -l`).toString().trim());
// Get the short hash of the current commit.
const currentCommitHash = execSync(`git rev-parse --short --verify ${currentBranch}`).toString().trim();
// Create a string showing how many commits since last tag; this will either
// be e.g. ` +23` if postReleaseCommits > 0, or en empty string.
const postReleaseCount = postReleaseCommits ? ` +${postReleaseCommits}` : '';
// Return it prepended by a newline--we will be appending this to various
// files.
return `\n/* ${latestTag}${postReleaseCount} (${currentCommitHash}) */\n`;
} catch (error) {
logger.error(error);
logger.log(`Failed to create assets timestamp. Carrying on without it.`);
return '';
}
}
// Build the static site for deployment.
function build() {
const builder = fractal.web.builder();
builder.on('progress', (completed, total) => logger.update(`Exported ${completed} of ${total} items.`, 'info'));
builder.on('error', err => logger.error(err.message));
return builder.build().then(() => {
logger.success('Fractal build completed!');
});
}
// After build, tar and gzip www/assets to www/latest-assets.tgz.
function archive() {
const commit = execSync('git rev-parse --short HEAD').toString().trim();
return exec(`tar -czf tmp/${version}-assets.build-${commit}.tgz -C tmp assets`);
}
// Serve the dynamic site for local environments.
function serve() {
const server = fractal.web.server({
sync,
});
server.on('error', err => logger.error(err.message));
return server.start().then(() => {
logger.success(`Fractal server is now running at ${server.url}.`);
});
}
function clean() {
return del([
'dist/',
'tmp/assets/',
]);
}
function products() {
return gulp.src(productData, { since: gulp.lastRun(products) })
.pipe(eslint())
.pipe(eslint.format())
.pipe(gulpIf(errorsFail, eslint.failAfterError()))
.pipe(gulp.dest('./tmp/assets/js'));
}
function icons() {
return gulp.src([...componentIcons, ...flagIcons])
// Copies individual icons over for rare situations when the `use` tag can't
// be used, such as within CSS or `img` tags.
.pipe(gulp.dest('./tmp/assets/icons/individual'))
.pipe(svgSprite({
mode: {
symbol: {
dest: '.',
prefix: '.icon--%s',
dimensions: '%s',
sprite: 'icons.svg',
// Creates an scss file of icon dimensions that is used by the app.
render: {
scss: true
}
}
}
}))
.pipe(gulp.dest('tmp/assets/icons'));
}
function images() {
return gulp.src(componentImages)
// Delete images removed from src.
.pipe(deleted({
src: 'src/assets/images',
dest: 'tmp/assets/images',
patterns: [ '**/*' ],
}))
// Only process image files that have changed.
.pipe(changed('tmp/assets/images'))
.pipe(count('## image files processed', { logFiles: true }))
.pipe(imagemin())
.pipe(gulp.dest('tmp/assets/images'));
}
function videos() {
return gulp.src(componentVideos)
// Delete videos removed from src.
.pipe(deleted({
src: 'src/assets/videos',
dest: 'tmp/assets/videos',
patterns: [ '**/*' ],
}))
// Only process video files that have changed.
.pipe(changed('tmp/assets/videos'))
.pipe(count('## video files processed', { logFiles: true }))
.pipe(gulp.dest('tmp/assets/videos'));
}
function fonts() {
return gulp.src(componentFonts)
// Delete fonts removed from src.
.pipe(deleted({
src: 'src/assets/fonts',
dest: 'tmp/assets/fonts',
patterns: [ '**/*' ],
}))
// Only process font files that have changed.
.pipe(changed('tmp/assets/fonts'))
.pipe(count('## font files processed', { logFiles: true }))
.pipe(gulp.dest('tmp/assets/fonts'));
}
function pdfs() {
return gulp.src(componentPdfs)
// Delete pdfs removed from src.
.pipe(deleted({
src: 'src/assets/pdfs',
dest: 'tmp/assets/pdfs',
patterns: [ '**/*' ],
}))
// Only process pdf files that have changed.
.pipe(changed('tmp/assets/pdfs'))
.pipe(count('## pdf files processed', { logFiles: true }))
.pipe(gulp.dest('tmp/assets/pdfs'));
}
function staticResources() {
return gulp.src(staticAssets)
// Delete html removed from src.
.pipe(deleted({
src: 'src/assets/static',
dest: 'tmp/assets/static',
patterns: [ '**/*' ],
}))
// Only process html files that have changed.
.pipe(changed('tmp/assets/static'))
.pipe(count('## static files processed', { logFiles: true }))
.pipe(gulp.dest('tmp/assets/static'));
}
function configScripts() {
// The BE scripts (e.g. `*.config.js` files) are only loaded during dev and
// build gulp tasks, so we don't need to to minify or concatenate them or
// create sourcemaps. On the other hand, linting them still is no bad idea.
return gulp.src(configJs, { since: gulp.lastRun(configScripts) })
.pipe(eslint())
.pipe(eslint.format())
.pipe(gulpIf(errorsFail, eslint.failAfterError()));
}
// Generic function to process front-end scripts.
function frontEndScripts(scripts, name) {
return gulp.src(scripts, { since: cache.lastMtime(name) })
.pipe(sourcemaps.init())
.pipe(babel({
presets: [['es2015', { modules: false }]],
}))
.pipe(uglify())
.pipe(cache(name))
.pipe(concat(name))
.pipe(sourcemaps.write('./'))
.pipe(replace(/\n$/, timestamp()))
.pipe(gulp.dest('tmp/assets/js'));
}
function componentScripts() {
return frontEndScripts(componentJs, 'app.js');
}
function dealerProfilesScripts() {
return frontEndScripts(dealerProfilesJs, 'dealer-profiles.js');
}
function b2bScripts() {
return frontEndScripts(b2bJs, 'app-b2b.js');
}
function jsLint() {
return gulp.src(componentJs.concat(configJs))
.pipe(gulpIf(isOwnedSrcFile, eslint()))
.pipe(gulpIf(isOwnedSrcFile, eslint.format()))
.pipe(gulpIf(isOwnedSrcFile, gulpIf(errorsFail, eslint.failAfterError())));
}
function isOwnedSrcFile(file) {
return !!file.path.match(new RegExp(`^${path.resolve(__dirname, 'src')}`));
}
function themeScripts() {
return gulp.src(stylesheetSwitcherJs)
.pipe(gulp.dest('./tmp/assets/js'));
}
function vendorScripts() {
return gulp.src(vendorJs)
.pipe(gulp.dest('tmp/assets/js/vendor'));
}
function scssLint() {
return gulp.src(allScss)
.pipe(sassLint({
rules: {
'class-name-format': [
2,
{
convention: 'hyphenatedbem',
},
],
'clean-import-paths': {
'filename-extension': false
},
'declarations-before-nesting': 2,
'empty-line-between-blocks': 2,
'force-attribute-nesting': 0,
'force-element-nesting': 0,
'force-pseudo-nesting': 0,
'indentation': [
2,
{
size: 2,
},
],
'mixins-before-declarations': 2,
'no-color-literals': 0,
// We use CSS comments to indicate critical styles.
// postcss-critical-split supports important comments (/*! critical */),
// which would satisfy this `no-css-comments` rule, but there is a bug
// with the implementation. Once that issue is fixed, we can remove the
// following line.
// See: https://github.com/mrnocreativity/postcss-critical-split/issues/17
'no-css-comments': 0,
'no-duplicate-properties': 2,
'no-ids': 2,
'no-important': 2,
'no-mergeable-selectors': 2,
'no-trailing-whitespace': 2,
'no-qualifying-elements': 0,
'no-trailing-zero': 0,
'no-vendor-prefixes': 0,
'one-declaration-per-line': 2,
'property-sort-order': [
2,
{
'order': 'smacss'
}
],
// The following rule is commented out until we have transitioned all
// stylesheets to use the agreed-upon units. It’s too noisy when left
// uncommented (even as a warning), but we want to keep them to
// uncomment temprarily as we chip away at #1777.
/* 'property-units': [
* 1,
* {
* global: [
* 'rem',
* ],
* 'per-property': {
* border: [
* 'px',
* 'rem',
* ],
* 'border-bottom': [
* 'px',
* 'rem',
* ],
* 'border-radius': [
* 'px',
* 'rem',
* ],
* 'letter-spacing': [
* 'px',
* ],
* },
* },
* ], */
'pseudo-element': 0,
'quotes': 2,
'space-after-colon': 2,
'space-after-comma': 2,
'space-around-operator': 2,
'space-before-brace': 2,
'space-before-colon': 2,
'trailing-semicolon': 2,
'zero-unit': 2,
}
}))
.pipe(sassLint.format())
.pipe(gulpIf(errorsFail, sassLint.failOnError()));
}
function componentStyles() {
let modified = this ? this.modified : null;
if (modified) {
modified = Array.isArray(modified) ? modified : [ modified ];
}
return gulp.src(componentScss)
.pipe(changed('tmp/assets/css', {
hasChanged: determineModifiedStatus(modified),
}))
.pipe(sourcemaps.init())
.pipe(glob())
.pipe(
sass({
// Fiber reduces the async overhead, speeding up compilation
fiber: Fiber,
outputStyle: 'expanded',
includePaths: sassIncludePaths,
}).on('error', function (error) {
if (errorsFail) {
throw error;
} else {
// sass.logError is expected to be passed directly to the `.on()`
// method, where it would be bound with `this` and have access to the
// `emit()` method. Since we’re calling the function explicitly, we
// need to bind `this` to it ourselves:
sass.logError.call(this, error);
}
})
)
.pipe(autoprefixer())
.pipe(sourcemaps.write('./'))
.pipe(replace(/\n$/, timestamp()))
.pipe(gulp.dest('tmp/assets/css'))
.pipe(count('## component stylesheets compiled (including sourcemaps)', { logFiles: true }));
}
function determineModifiedStatus(modified) {
// Determines whether a top-level stylesheet should be compiled depending on
// whether it imports any of the supplied component-level stylesheets.
return function(stream, source, dest) {
// If no modified files are supplied or the modified files include the
// current source path, just add source to the stream and move on.
if (!modified || modified.includes(source.path.replace(`${process.cwd()}/`, ''))) {
return stream.push(source);
}
// Inspect the contents of the source file to determine whether it contains
// any of the supplied submodules.
const contents = source.contents.toString();
const streamIt = modified.some(function (file) {
let baseLayoutRegExp, categoryPath, categoryRegExp,
componentPath, componentRegExp, everythingRegExp, globalRegExp;
// If the modified file is not a component or scss asset file, ignore it.
if (!file.match(/src\/components/) && !file.match(/src\/assets\/scss/)) {
return false;
}
// Determine whether modified file is asset scss file.
const globalMatch = file.match(/src\/assets\/scss\/((?:abstracts|base|layout)\/)/);
const fileIsGlobal = !!globalMatch;
if (fileIsGlobal) {
globalRegExp = new RegExp(globalMatch[1]);
} else {
// Match an _everything_ import.
// e.g. source file has:
// @import '../../components/**/*.scss';
everythingRegExp = /\/components\/\*\*\/\*\.scss/;
// Match whether the submodule’s category is imported wholesale.
// e.g. submodule is `element` and source file has:
// @import '../../components/01-elements/**/*.scss';
categoryPath = file.match(/(components\/\d{2}-[^/]+)/)[1];
categoryRegExp = new RegExp(`${categoryPath}/\\*\\*/\\*`, 'gm');
// Match whether the submodule’s top-level component is imported.
// e.g. submodule is (or belongs to) `01-elements/form-items` and
// source file has:
// @import '../../components/01-elements/form-items/**/*.scss';
componentPath = file.match(/(components\/\d{2}-[^/]+\/[^/]+)/)[1]
componentRegExp = new RegExp(componentPath, 'gm');
}
// Return whether any of the matches is truthy.
return (
(
// File is global file and stylesheet imports it.
fileIsGlobal && contents.match(globalRegExp)
) || (
// Stylesheet imports _everything_.
contents.match(everythingRegExp) ||
// Stylesheet imports submodule’s category.
contents.match(categoryRegExp) ||
// Stylesheet imports submodule’s top-level component.
contents.match(componentRegExp)
)
);
});
if (streamIt) {
return stream.push(source);
}
}
}
function themeStyles() {
return gulp.src(themeScss)
.pipe(sass({
fiber: Fiber,
outputStyle: 'compressed',
}).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cleanCSS())
.pipe(gulp.dest('tmp/assets/css'));
}
function criticalCss() {
return gulp.src(componentGeneratedCss)
// TODO: Use changed (and transformPath) to split only changed stylesheets.
.pipe(postCSS([ criticalSplit({ output: 'critical' }) ]))
.pipe(rename(path => path.basename += '-critical'))
.pipe(cleanCSS())
.pipe(gulp.dest('tmp/assets/css'));
}
function nonCriticalCss() {
return gulp.src(componentGeneratedCss)
// TODO: Use changed (and transformPath) to split only changed stylesheets.
.pipe(postCSS([ criticalSplit({ output: 'rest' }) ]))
.pipe(rename(path => path.basename += '-noncritical'))
.pipe(cleanCSS())
.pipe(gulp.dest('tmp/assets/css'));
}
function optimizeSourceCss() {
return gulp.src(componentGeneratedCss)
.pipe(cleanCSS())
.pipe(gulp.dest('tmp/assets/css'));
}
function initWatch(done) {
gulp.watch('src/assets/icons', icons);
gulp.watch('src/assets/images', images);
gulp.watch('src/assets/videos', videos);
gulp.watch('src/assets/fonts', fonts);
gulp.watch('src/assets/pdfs', pdfs);
gulp.watch('src/assets/static', staticResources);
gulp.watch(productData, products);
gulp.watch(configJs, configScripts);
gulp.watch(componentJs, gulp.series(componentScripts, jsLint))
.on('unlink', (filePath) => {
// Remove deleted files from the cache.
cache.forget('app.js', path.resolve(filePath));
});
gulp.watch(b2bJs, b2bScripts)
.on('unlink', (filePath) => {
// Remove deleted files from the cache.
cache.forget('app-b2b.js', path.resolve(filePath));
});
gulp.watch(dealerProfilesJs, dealerProfilesScripts)
.on('unlink', (filePath) => {
// Remove deleted files from the cache.
cache.forget('dealer-profiles.js', path.resolve(filePath));
});
gulp.watch(vendorJs, vendorScripts);
gulp.watch(stylesheetSwitcherJs, themeScripts);
// Instantiate a watcher and bind file paths of modified files to the
// componentStyles function.
const scssWatcher = gulp.watch('src/**/*.scss');
scssWatcher.on('change', function(filepath, stats) {
return gulp.series([
gulp.parallel([
componentStyles.bind({
modified: filepath,
}), themeStyles,
]),
gulp.parallel([
criticalCss, nonCriticalCss,
]),
optimizeSourceCss,
scssLint
])();
});
done();
}
function stubs(done) {
const port = 4000;
stubServer.listen(port, () => {
logger.success(`Stubs server is listening on port: ${port}.`);
});
done();
}
function copyToDist() {
return gulp.src(['tmp/**/*'])
.pipe(gulp.dest('dist/'));
}
const compile = gulp.series(
icons,
gulp.parallel([
images, videos, fonts, pdfs, staticResources, products, configScripts,
componentScripts, dealerProfilesScripts, b2bScripts, vendorScripts,
themeScripts, componentStyles, themeStyles,
]),
gulp.parallel([
criticalCss, nonCriticalCss,
]),
optimizeSourceCss
);
// Abriged version of compile stream, focused exclusively on the assets that
// are actually used in production B2C and B2B websites. This excludes any
// assets that are used exclusively within the pattern library.
const compileProdAssets = gulp.series(
icons,
gulp.parallel([
fonts, products, componentScripts, dealerProfilesScripts, b2bScripts,
vendorScripts, componentStyles, themeStyles,
]),
gulp.parallel([
criticalCss, nonCriticalCss,
]),
optimizeSourceCss
);
gulp.task('archive', archive);
gulp.task('assets', gulp.series(clean, compileProdAssets, copyToDist));
gulp.task('build', gulp.series(clean, compile, archive, build));
gulp.task('compile', compile);
gulp.task('dev', gulp.series(compile, serve, initWatch));
gulp.task('lint', gulp.parallel(scssLint, jsLint));
gulp.task('preview', serve);
gulp.task('start', gulp.series(compile, serve));
gulp.task('stubs', stubs);