-
-
Save tmslnz/1d025baaa7557a2d994032aa88fb61b3 to your computer and use it in GitHub Desktop.
/* | |
Streamlined Shopify theme development. | |
NOTE: depends on module gulp-shopify-theme | |
npm install --save-dev yargs gulp gulp-sass gulp-changed gulp-sourcemaps gulp-autoprefixer gulp-uglify gulp-concat gulp-replace gulp-plumber gulp-babel browser-sync gulp-if del gulp-add-src gulp-rename gulp-yaml gulp-shopify-theme | |
Highlights: | |
- https proxying via BrowserSync | |
- autoreload | |
- sourcemaps support | |
- YAML support for Shopify config/ and locales/ files | |
*/ | |
const argv = require('yargs').argv; | |
const gulp = require( 'gulp' ); | |
const sass = require( 'gulp-sass' ); | |
const changed = require( 'gulp-changed' ); | |
const sourcemaps = require( 'gulp-sourcemaps' ); | |
const uglify = require( 'gulp-uglify' ); | |
const concat = require( 'gulp-concat' ); | |
const replace = require( 'gulp-replace' ); | |
const plumber = require( 'gulp-plumber' ); | |
const babel = require( 'gulp-babel' ); | |
const browsersync = require( 'browser-sync' ).create(); | |
const gulpif = require( 'gulp-if' ); | |
const del = require( 'del' ); | |
const addsrc = require( 'gulp-add-src' ); | |
const postcss = require('gulp-postcss'); | |
const autoprefixer = require('autoprefixer'); | |
const rename = require( 'gulp-rename' ); | |
const yaml = require( 'gulp-yaml' ); | |
const jsyaml = require( 'js-yaml' ); | |
const theme = require( 'gulp-shopify-theme' ).create(); | |
const shopifyconfig = require( './~shopifyconfig.json' ); | |
var DESTINATION = argv.dest || 'dist'; | |
var USE_JS_UGLIFY = !!(argv.uglify || process.env.USE_JS_UGLIFY); | |
var USE_SOURCEMAPS = !(argv.nomaps || process.env.DISABLE_SOURCEMAPS); | |
var USE_BROWSER_SYNC = !!(argv.browsersync || argv.bs || process.env.USE_BROWSER_SYNC); | |
var BROWSER_SYNC_PORT = parseInt(argv.browsersync) || parseInt(argv.bs) || parseInt(process.env.BROWSER_SYNC_PORT) || '3000'; | |
const sourceMappingURLCSSregExp = new RegExp('(.*?[/*]{2,}# sourceMappingURL=)(.*?)([/*]{2})', 'g'); | |
const sourceMappingURLJSregExp = new RegExp('(.*?[/*]{2,}# sourceMappingURL=)(.*?)', 'g'); | |
const sourceMappingURLCSSreplace = '{% raw %}$1{% endraw %}$2{% raw %}$3{% endraw %}'; | |
const sourceMappingURLJSreplace = '{% raw %}$1{% endraw %}$2'; | |
shopifyconfig.root = process.cwd() + '/' + DESTINATION; | |
gulp.task( 'default', [ 'css', 'js', 'js-libs', 'fonts', 'images', 'copy', 'configs' ] ); | |
gulp.task( 'dev', [ 'theme', 'default', 'browsersync', 'watch' ] ); | |
gulp.task( 'clean', function () { | |
return del(DESTINATION); | |
}); | |
gulp.task( 'purge', [ 'theme' ], function (done) { | |
theme.purge(); | |
done(); | |
}); | |
gulp.task( 'theme', function () { | |
theme.init( shopifyconfig ); | |
}); | |
gulp.task( 'watch', function () { | |
USE_JS_UGLIFY = false; | |
// Watch & run tasks | |
gulp.watch( 'src/assets/{css,css-libs}/**/*.{css,less,scss,liquid}', [ 'reload-on-css' ] ); | |
gulp.watch( 'src/assets/js/**/*.js', [ 'reload-on-js' ] ); | |
gulp.watch( 'src/assets/js-libs/**/*.js', [ 'reload-on-js-libs' ] ); | |
gulp.watch( 'src/assets/fonts/**/*', [ 'reload-on-fonts' ] ); | |
gulp.watch( 'src/assets/images/**/*', [ 'reload-on-images' ] ); | |
gulp.watch( 'src/{layout,config,snippets,sections,templates,locales}/**/*', [ 'reload-on-copy' ]); | |
}); | |
gulp.task( 'css', function () { | |
return gulp.src( 'src/assets/css/all.scss' ) | |
.pipe( plumber() ) | |
.pipe( sourcemaps.init() ) | |
.pipe( sass().on('error', sass.logError) ) | |
.pipe( replace( /({{|}}|{%|%})/g, '/*!$1*/' ) ) // Comment out Liquid tags, so post-css doesn't trip out | |
.pipe( postcss( [ | |
autoprefixer({browsers: [ 'last 2 versions', 'Explorer >= 9' ]}), | |
] ) ) | |
.pipe( replace( /\/\*!({{|}}|{%|%})\*\//g, '$1' ) ) // Re-enable Liquid tags | |
.pipe( rename( 'css_all.css' ) ) | |
.pipe( sourcemaps.write('.', {sourceMappingURL: makeLiquidSourceMappingURL})) // css_all.css.map | |
.pipe( rename(appendLiquidExt)) // css_all.css.liquid | |
.pipe( replace( sourceMappingURLCSSregExp, sourceMappingURLCSSreplace ) ) | |
.pipe( gulp.dest( DESTINATION + '/assets' ) ) | |
.pipe( theme.stream() ); | |
}); | |
gulp.task( 'js', function () { | |
return gulp.src( [ 'src/assets/js/!(main)*.js', 'src/assets/js/main.js' ] ) | |
.pipe( plumber() ) | |
.pipe( sourcemaps.init() ) | |
.pipe( babel({presets: ['es2015']}) ) | |
.pipe( concat( 'js_script.js' ) ) | |
.pipe( gulpif( USE_JS_UGLIFY, uglify() ) ) | |
.pipe( sourcemaps.write('.', {sourceMappingURL: makeLiquidSourceMappingURL})) | |
.pipe( rename(appendLiquidExt)) | |
.pipe( replace( sourceMappingURLJSregExp, sourceMappingURLJSreplace ) ) | |
.pipe( gulp.dest( DESTINATION + '/assets' ) ) | |
.pipe( theme.stream() ); | |
}); | |
gulp.task( 'js-libs', function () { | |
return gulp.src( [ 'src/assets/js-libs/jquery.js' , 'src/assets/js-libs/*.js' ] ) | |
.pipe( plumber() ) | |
.pipe( concat( 'js_libs.js' ) ) | |
.pipe( gulpif( USE_JS_UGLIFY, uglify() ) ) | |
.pipe( gulp.dest( DESTINATION + '/assets' ) ) | |
.pipe( theme.stream() ); | |
}); | |
gulp.task( 'fonts', function () { | |
return gulp.src( [ 'src/assets/fonts/**/*.{ttf,woff,woff2,eof,eot,otf,svg}' ] ) | |
.pipe( plumber() ) | |
.pipe( changed( DESTINATION, {hasChanged: changed.compareSha1Digest} ) ) | |
.pipe( rename( flatten )) | |
.pipe( rename({ dirname: '', prefix: 'fonts_' })) | |
.pipe( gulp.dest( DESTINATION + '/assets' ) ) | |
.pipe( theme.stream() ); | |
}); | |
gulp.task( 'images', function () { | |
return gulp.src( [ 'src/assets/images/**/*.{svg,png,jpg,jpeg,gif,ico}', '!src/assets/images/src/**/*' ] ) | |
.pipe( plumber() ) | |
.pipe( changed( DESTINATION, {hasChanged: changed.compareSha1Digest} ) ) | |
.pipe( rename( flatten )) | |
.pipe( rename({ dirname: '', prefix: 'images_' })) | |
.pipe( gulp.dest( DESTINATION + '/assets' ) ) | |
.pipe( theme.stream() ); | |
}); | |
gulp.task( 'copy', function () { | |
return gulp.src( [ 'src/{layout,snippets,templates,sections}/**/*.*' ] ) | |
.pipe( plumber() ) | |
.pipe( replace( /{% schema %}([^]*.+[^]*){% endschema %}/gi, replaceYAMLwithJSON ) ) | |
.pipe( replace(/({%)(?!\s*?(?:end)?(?:raw|schema|javascript|stylesheet)\s*?)(.+?)(%})/g, '$1- $2 -$3') ) // make whitespace-insensitive tags {% -> {%- | |
.pipe( replace( /^\s*[\r\n]/gm, '' ) ) // remove empty lines | |
.pipe( changed( DESTINATION, {hasChanged: changed.compareSha1Digest} ) ) | |
.pipe( gulp.dest( DESTINATION ) ) | |
.pipe( theme.stream() ); | |
}); | |
gulp.task( 'configs', function () { | |
return gulp.src( [ 'src/{config,locales}/**/*.*' ] ) | |
.pipe( plumber() ) | |
.pipe( yaml({space: 2}) ) | |
.pipe( changed( DESTINATION, {hasChanged: changed.compareSha1Digest} ) ) | |
.pipe( gulp.dest( DESTINATION ) ) | |
.pipe( theme.stream() ); | |
}); | |
gulp.task('reload-on-css', ['css'], reload); | |
gulp.task('reload-on-js', ['js'], reload); | |
gulp.task('reload-on-js-libs', ['js-libs'], reload); | |
gulp.task('reload-on-fonts', ['fonts'], reload); | |
gulp.task('reload-on-images', ['images'], reload); | |
gulp.task('reload-on-copy', ['copy', 'configs'], reload); | |
function reload (done) { | |
if (!USE_BROWSER_SYNC) return done(); | |
browsersync.reload(); done(); | |
} | |
gulp.task( 'browsersync', function (done) { | |
if (!USE_BROWSER_SYNC) return done(); | |
browsersync.init({ | |
port: BROWSER_SYNC_PORT, ui: { port: BROWSER_SYNC_PORT + 1 }, | |
proxy: 'https://'+ shopifyconfig.shop_name +'.myshopify.com', | |
browser: [], | |
notify: false, | |
startPath: "/?preview_theme_id=" + shopifyconfig.theme_id, | |
}, done); | |
}); | |
console.log('DESTINATION', DESTINATION); | |
console.log('USE_JS_UGLIFY', USE_JS_UGLIFY); | |
console.log('USE_SOURCEMAPS', USE_SOURCEMAPS); | |
console.log('USE_BROWSER_SYNC', USE_BROWSER_SYNC); | |
console.log('BROWSER_SYNC_PORT', BROWSER_SYNC_PORT); | |
function replaceYAMLwithJSON (match, g1) { | |
if (match) { | |
var yamlString = g1.replace(/{% (end)?schema %}/, ''); | |
var parsedYaml = jsyaml.safeLoad(yamlString); | |
var jsonString = JSON.stringify(parsedYaml, null, ' '); | |
return '{% schema %}\n' + jsonString + '\n{% endschema %}'; | |
} | |
} | |
function makeLiquidSourceMappingURL (file) { | |
return '{{"' + file.relative + '.map" | asset_url }}'; | |
} | |
function appendLiquidExt (path) { | |
if (path.extname === '.map') return; | |
if (path.extname === '.css') { | |
path.extname = '.scss'; | |
} | |
path.basename += path.extname; | |
path.extname = '.liquid'; | |
} | |
function flatten (path) { | |
if (path.dirname !== '.') { | |
path.basename = path.dirname.replace('/', '_') + '_' + path.basename; | |
} | |
} | |
@superfein The workflow can certainly be updated to V4—I have done so for a recent project in fact. I am reluctant to update the gist until other things are ready. Some of the utility functions that appear at the bottom of the current gist have been moved off to the gulp-shopify-theme
module, on which this workflow depends on. I have not yet published the changes to the module on NPM though. Regardless, below is what I currently have on the most recent project.
As per the update speed, my gulp-shopify-theme
attempts to keep the back-and-forth to a minimum to reduce refresh times. I haven't measured "performance" though. My experience with it is hit and miss, but great compared to the alternatives. Sometimes it's really quick, others it takes its sweet time. A lot depends on Shopify's API too.
That said this whole workflow is ripe for improvement. I don't do a lot of Shopify dev myself anymore, but I am planning to eventually release a large amount of utility code, template functions, etc. that may be useful to other devs. Time and other commitments are always the problem though :)
const gulp = require('gulp');
const concat = require( 'gulp-concat' );
const replace = require( 'gulp-replace' );
const babel = require( 'gulp-babel' );
const rename = require( 'gulp-rename' );
const changed = require( 'gulp-changed' );
const postcss = require('gulp-postcss');
const yaml = require('gulp-yaml');
const sass = require('gulp-sass');
const shopifyTheme = require( 'gulp-shopify-theme' ).create();
const shopifyUtil = require( 'gulp-shopify-theme' ).util;
const postcssPresetEnv = require('postcss-preset-env');
const autoprefixer = require('autoprefixer');
const browserSync = require('browser-sync');
const server = browserSync.create();
const del = require('del');
const SRC_OPTIONS = {
allowEmpty: true,
sourcemaps: true,
since: Date.now(),
}
const DESTINATION = 'dist/';
const PACKAGE = require('./package.json');
const SHOPIFY_CONFIG = require( './~shopifyconfig.json' );
SHOPIFY_CONFIG.root = process.cwd() + '/' + DESTINATION;
SHOPIFY_CONFIG.theme_id = PACKAGE.shopify_dev.theme_id;
SHOPIFY_CONFIG.shop_name = PACKAGE.shopify_dev.shop_name;
const POST_CSS_PRESET_ENV_OPTIONS = {
stage: 0,
features: {
'nesting-rules': true
}
}
const POST_CSS_AUTOPREFIXER_OPTIONS = {}
gulp.task( 'default', gulp.series(clean, theme, gulp.parallel(css, js, jsLibs, fonts, images, copy, configs)) );
gulp.task( 'dev', gulp.series('default', theme, proxy, watch) );
gulp.task( 'purge', purge );
function clean() {
return del(DESTINATION);
}
function theme(done) {
shopifyTheme.init( SHOPIFY_CONFIG );
done();
}
function css() {
return gulp.src( 'src/assets/css/main.scss' )
.pipe( changed( DESTINATION ) )
.pipe( sass().on('error', sass.logError) )
.pipe( replace( /({{|}}|{%|%})/g, '/*!$1*/' ) ) // Comment out Liquid tags, so post-css doesn't trip out
.pipe( postcss( [
postcssPresetEnv(POST_CSS_PRESET_ENV_OPTIONS),
autoprefixer(POST_CSS_AUTOPREFIXER_OPTIONS),
]))
.pipe( replace( /\/\*!({{|}}|{%|%})\*\//g, '$1' ) ) // Re-enable Liquid tags
.pipe( rename( 'css_all.css' ) )
.pipe( rename(shopifyUtil.appendLiquidExt)) // css_all.css.liquid
.pipe( replace( shopifyUtil.sourceMappingURLCSSregExp, shopifyUtil.sourceMappingURLCSSreplace ) )
.pipe( gulp.dest( DESTINATION + '/assets' ) )
.pipe( shopifyTheme.stream({batchMode: true}) );
}
function js() {
return gulp.src( [ 'src/assets/js/!(main)*.js', 'src/assets/js/main.js' ], {
allowEmpty: true,
sourcemaps: true,
})
.pipe( babel() )
.pipe( concat( 'js_script.js' ) )
.pipe( rename(shopifyUtil.appendLiquidExt))
.pipe( replace( shopifyUtil.sourceMappingURLJSregExp, shopifyUtil.sourceMappingURLJSreplace ) )
.pipe( gulp.dest( DESTINATION + '/assets' ) )
.pipe( shopifyTheme.stream({batchMode: true}) );
}
function jsLibs() {
return gulp.src( [ 'src/assets/js-libs/jquery-2.2.3.min.js' , 'src/assets/js-libs/*.js' ] )
.pipe( changed( DESTINATION ) )
.pipe( concat( 'js_libs.js' ) )
.pipe( gulp.dest( DESTINATION + '/assets' ) )
.pipe( shopifyTheme.stream({batchMode: true}) );
}
function fonts() {
return gulp.src( [ 'src/assets/fonts/**/*.{ttf,woff,woff2,eof,eot,otf,svg}' ] )
.pipe( changed( DESTINATION ) )
.pipe( rename( shopifyUtil.flatten ))
.pipe( rename({ dirname: '', prefix: 'fonts_' }))
.pipe( gulp.dest( DESTINATION + '/assets' ) )
.pipe( shopifyTheme.stream({batchMode: true}) );
}
function images() {
return gulp.src( [ 'src/assets/images/**/*.{svg,png,jpg,jpeg,gif,ico}', '!src/assets/images/src/**/*' ] )
.pipe( changed( DESTINATION ) )
.pipe( rename( shopifyUtil.flatten ))
.pipe( rename({ dirname: '', prefix: 'images_' }))
.pipe( gulp.dest( DESTINATION + '/assets' ) )
.pipe( shopifyTheme.stream({batchMode: true}) );
}
function copy() {
return gulp.src( [ 'src/{layout,snippets,templates,sections}/**/*.*' ] )
.pipe( changed( DESTINATION ) )
.pipe( replace( /{% schema %}([^]*.+[^]*){% endschema %}/gi, shopifyUtil.replaceYAMLwithJSON ) )
.pipe( replace(/({%)(?!-|\s*?(?:end)?(?:raw|schema|javascript|stylesheet)\s*?)(.+?)(%})/g, '$1- $2 -$3') ) // make whitespace-insensitive tags {% -> {%-
.pipe( replace( /^\s*[\r\n]/gm, '' ) ) // remove empty lines
.pipe( gulp.dest( DESTINATION ) )
.pipe( shopifyTheme.stream({batchMode: true}) );
}
function watch(done) {
gulp.watch( 'src/assets/{css,css-libs}/**/*.{css,less,scss,liquid}', css );
gulp.watch( 'src/assets/js/**/*.js', js);
gulp.watch( 'src/assets/js-libs/**/*.js', jsLibs );
gulp.watch( 'src/assets/fonts/**/*', fonts );
gulp.watch( 'src/assets/images/**/*', images );
gulp.watch( 'src/{layout,snippets,sections,templates}/**/*', copy );
gulp.watch( 'src/{config,locales}/**/*', configs );
// Since we are using batchMode: true
shopifyTheme.on('done', (response)=>{
server.reload();
});
done();
}
function configs() {
return gulp.src( [ 'src/{config,locales}/**/*.*' ] )
.pipe( changed( DESTINATION ) )
.pipe( yaml({space: 2}) )
.pipe( gulp.dest( DESTINATION ) )
.pipe( shopifyTheme.stream({batchMode: true}) );
}
function purge(done) {
shopifyTheme.init( SHOPIFY_CONFIG );
shopifyTheme.purge();
done();
}
function proxy(done) {
server.init({
port: 3000,
proxy: PACKAGE.shopify_dev.shop_url,
// browser: [],
// notify: false,
// startPath: "/?preview_theme_id=" + SHOPIFY_CONFIG.theme_id,
}, done);
}
module.exports = {
js,
}
Thank you so much for sharing your updated code! I'm excited to start using this workflow, and totally understand you're very busy and don't have time to update the node module and Github gist.
I have a few questions due to errors that came up:
-
These two lines:
SHOPIFY_CONFIG.theme_id = PACKAGE.shopify_dev.theme_id;
SHOPIFY_CONFIG.shop_name = PACKAGE.shopify_dev.shop_name;
...where is shopify_dev defined? Is that an addition you made to the node module that hasn't been published? It's referenced several times in the gulpfile.js code you shared, but it causes errors in the console when I try to run "gulp dev". - this line as well:
proxy: PACKAGE.shopify_dev.shop_url,
-
This line:
const shopifyUtil = require( 'gulp-shopify-theme' ).util;
...is this .util function part of the current gulp-shopify-theme module? I can see it's being used throughout the gulpfile.js, but is causing errors in the console.
It's used in the css() and js() functions:
.pipe( rename(shopifyUtil.appendLiquidExt)) // css_all.css.liquid
.pipe( replace( shopifyUtil.sourceMappingURLCSSregExp, shopifyUtil.sourceMappingURLCSSreplace ) )
This line is from the font() function:
.pipe( rename( shopifyUtil.flatten ))
This line is from the copy() function:
.pipe( replace( /{% schema %}([^]*.+[^]*){% endschema %}/gi, shopifyUtil.replaceYAMLwithJSON ) )
Everything else seems to be working, at least as far as I can tell from the command line.
Lastly, just double checking I did this right since it isn't mentioned in your Github repo docs:
I created this file ~shopifyconfig.json
in the root and put all the sensitive shop related info in there, like api_key, shop_name etc.
Thanks again Tommaso!
assert.js:374
throw err;
^
AssertionError [ERR_ASSERTION]: Task function must be specified
at Gulp.set [as _setTask] (dev/node_modules/undertaker/lib/set-task.js:10:3)
at Gulp.task (dev/node_modules/undertaker/lib/task.js:13:8)
at Object. (dev/gulpfile.js:44:6)
at Module._compile (internal/modules/cjs/loader.js:959:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
at Module.load (internal/modules/cjs/loader.js:815:32)
at Function.Module._load (internal/modules/cjs/loader.js:727:14)
at Module.require (internal/modules/cjs/loader.js:852:19)
at require (internal/modules/cjs/helpers.js:74:18)
at execute (/usr/local/lib/node_modules/gulp-cli/lib/versioned/^4.0.0/index.js:36:18) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '=='
That you have to create yourself with the API secrets
I think the problem in version of gulp.
[22:37:52] gulp-postcss: revolution.settings.css
autoprefixer: dev/assets/revolution.settings.css:478:3: Gradient has outdated direction syntax. New syntax is like to left
instead of right
.
Error in plugin "sass"
Message:
assets/awemenu.css.liquid
Error: Invalid CSS after " top: 0;": expected "}", was "{% if settings.enab"
on line 378 of assets/awemenu.css.liquid
top: 0;
---------^
This workflow seems even more important now that Slate has been shelved permanently. Any plans on updating this gulpfile.js example for Gulp v4? Rewriting the tasks to be functions with series and parallel? Would really like to see that if possible.
Tried using Prepros for live reloading, but found themekit took a full 3 seconds to update the files on the Shopify server, so the live reload feature had to be delayed by 3 seconds. Somewhat slow and really didn't make for efficient development. Does this workflow suffer from the same issues?
Thanks.