├── .gitignore ├── .jsbeautifyrc ├── .jshintrc ├── LICENSE ├── README.md ├── grunt-shopify-LICENSE ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | *.log -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 2, 3 | "indent_char": " ", 4 | "other": " ", 5 | "indent_level": 0, 6 | "indent_with_tabs": false, 7 | "preserve_newlines": true, 8 | "max_preserve_newlines": 2, 9 | "jslint_happy": true, 10 | "indent_handlebars": true 11 | } 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "jquery": true 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Able Sense Media 2 | Developed by Michael Northorp 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gulp-shopify-upload 2 | =================== 3 | 4 | ## Introduction 5 | 6 | **gulp-shopify-upload** is a [Gulpjs](https://github.com/gulpjs/gulp) plugin used to watch and upload theme files to Shopify. 7 | By using this plugin you can watch and deploy all of the different folders in a Shopify theme and have them automatically deploy to your Shopify store. This is more lightweight than using Shopifys inline theme editor or desktop theme editor, and works on all platforms that support Node (Windows, Mac and Linux). 8 | 9 | This is a port of a similar plugin using Grunt called [grunt-shopify](https://github.com/wilr/grunt-shopify), thank you to the author for making a great plugin for Shopify. 10 | 11 | ## Features 12 | 13 | - Uploads any file changes to Shopify in the folders: `assets, layout, config, snippets, templates, locales`. 14 | - Automatically uploads changes to the current working theme in Shopify unless a themeid is specified. 15 | - Supports incremental file changes as well as a full site deploy for continuous integration. 16 | - Lightweight and fast, changes are uploaded instantly. 17 | 18 | ## Basic Usage 19 | 20 | 1. Download whatever theme you are working on from Shopify to a local directory 21 | 2. Create a [private app](http://docs.shopify.com/api/authentication/creating-a-private-app) in Shopify and get the API Key and Password for it. 22 | 3. Your folder structure and gulpfile.js should have look something like below 23 | ``` 24 | shopifyTheme/ 25 | |-- gulpfile.js 26 | |-- assets/ 27 | |-- config/ 28 | |-- layout/ 29 | |-- locales/ 30 | |-- snippets/ 31 | |-- templates/ 32 | ``` 33 | 34 | **Example Gulpfile** 35 | ``` 36 | // Gulp plugin setup 37 | var gulp = require('gulp'); 38 | // Watches single files 39 | var watch = require('gulp-watch'); 40 | var gulpShopify = require('gulp-shopify-upload'); 41 | 42 | gulp.task('shopifywatch', function() { 43 | return watch('./+(assets|layout|config|snippets|templates|locales)/**') 44 | .pipe(gulpShopify('API KEY', 'PASSWORD', 'MYSITE.myshopify.com', 'THEME ID')); 45 | }); 46 | 47 | // Default gulp action when gulp is run 48 | gulp.task('default', [ 49 | 'shopifywatch' 50 | ]); 51 | ``` 52 | 4. The basic function call looks like 53 | ``` 54 | gulpShopify('API KEY', 'PASSWORD', 'MYSITE.myshopify.com', 'THEME ID') 55 | ``` 56 | - `API KEY` is the API Key generated when creating a private app in Shopify 57 | - `PASSWORD` is the Password generated when creating a private app in Shopify 58 | - `MYSITE.myshopify.com` is the URL of your shop 59 | - `THEME ID` is the ID of your theme and is **OPTIONAL**, if not passed in, the current working theme will be used 60 | 4. Run `npm install gulp gulp-watch gulp-shopify-upload` 61 | 5. Run `gulp` and edit one of your theme files, it should automatically be uploaded to Shopify 62 | 63 | ## Advanced Usage 64 | **Customize Your Base Deployment Path** 65 | If your project structure is different (perhaps you use Gulpjs to compile your theme to another directory), you can change the directory from which the plugin picks up files. 66 | To do so, simply provide an additional options hash to function call, with a `basePath` property. 67 | 68 | ``` 69 | var options = { 70 | "basePath": "some/other-directory/" 71 | }; 72 | 73 | // With a theme id 74 | gulpShopify('API KEY', 'PASSWORD', 'MYSITE.myshopify.com', 'THEME ID', options) 75 | 76 | // Without a theme id 77 | gulpShopify('API KEY', 'PASSWORD', 'MYSITE.myshopify.com', null, options) 78 | ``` 79 | 80 | **Deploy the Entire Site** 81 | You can also deploy the entire site for use with continuous integration. 82 | ``` 83 | gulp.task('deploy', ['build'], function() { 84 | return gulp.src('./+(assets|layout|config|snippets|templates|locales)/**') 85 | .pipe(gulpShopify('API KEY', 'PASSWORD', 'MYSITE.myshopify.com', 'THEME ID')); 86 | ; 87 | }); 88 | ``` 89 | 90 | *Created by [Able Sense Media](http://ablesense.com) - 2015* 91 | -------------------------------------------------------------------------------- /grunt-shopify-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Will Rossiter 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | Neither the name of the Fullscreen Interactive Limited nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var through = require('through2'), 3 | gutil = require('gulp-util'), 4 | path = require('path'), 5 | isBinaryFile = require('isbinaryfile'), 6 | ShopifyApi = require('shopify-api'), 7 | PluginError = gutil.PluginError, 8 | shopify = {}, 9 | shopifyAPI, 10 | PLUGIN_NAME = 'gulp-shopify-upload'; 11 | 12 | // Set up shopify API information 13 | shopify._api = false; 14 | shopify._basePath = false; 15 | 16 | /* 17 | * Get the Shopify API instance. 18 | * 19 | * @return {ShopifyApi} 20 | */ 21 | shopify._getApi = function (apiKey, password, host) { 22 | if (!shopify._api) { 23 | var opts = { 24 | auth: apiKey + ':' + password, 25 | host: host, 26 | port: '443', 27 | timeout: 120000 28 | }; 29 | 30 | shopify._api = new ShopifyApi(opts); 31 | } 32 | 33 | return shopify._api; 34 | }; 35 | 36 | /* 37 | * Convert a file path on the local file system to an asset path in shopify 38 | * as you may run gulp at a higher directory locally. 39 | * 40 | * The original path to a file may be something like shop/assets/site.css 41 | * whereas we require assets/site.css in the API. To customize the base 42 | * set shopify.options.base config option. 43 | * 44 | * @param {string} 45 | * @return {string} 46 | */ 47 | shopify._makeAssetKey = function (filepath, base) { 48 | filepath = shopify._makePathRelative(filepath, base); 49 | 50 | return encodeURI(filepath); 51 | }; 52 | 53 | /* 54 | * Get the base path. 55 | * 56 | * @return {string} 57 | */ 58 | shopify._getBasePath = function (filebase) { 59 | if (!shopify._basePath) { 60 | var base = filebase; 61 | 62 | shopify._basePath = (base.length > 0) ? path.resolve(base) : process.cwd(); 63 | } 64 | 65 | return shopify._basePath; 66 | }; 67 | 68 | /** 69 | * Sets the base path 70 | * 71 | * @param {string} basePath 72 | * @return {void} 73 | */ 74 | shopify._setBasePath = function (basePath) { 75 | shopify._basePath = basePath; 76 | }; 77 | 78 | /** 79 | * Make a path relative to base path. 80 | * 81 | * @param {string} filepath 82 | * @return {string} 83 | */ 84 | shopify._makePathRelative = function (filepath, base) { 85 | var basePath = shopify._getBasePath(base); 86 | 87 | filepath = path.relative(basePath, filepath); 88 | 89 | return filepath.replace(/\\/g, '/'); 90 | }; 91 | 92 | /** 93 | * Applies options to plugin 94 | * 95 | * @param {object} options 96 | * @return {void} 97 | */ 98 | shopify._setOptions = function (options) { 99 | if (!options) { 100 | return; 101 | } 102 | 103 | if (options.hasOwnProperty('basePath')) { 104 | shopify._setBasePath(options.basePath); 105 | } 106 | }; 107 | 108 | /* 109 | * Upload a given file path to Shopify 110 | * 111 | * Assets need to be in a suitable directory. 112 | * - Liquid templates => 'templates/' 113 | * - Liquid layouts => 'layout/' 114 | * - Liquid snippets => 'snippets/' 115 | * - Theme settings => 'config/' 116 | * - General assets => 'assets/' 117 | * - Language files => 'locales/' 118 | * 119 | * Some requests may fail if those folders are ignored 120 | * @param {string} filepath 121 | * @param {Function} done 122 | */ 123 | shopify.upload = function (filepath, file, host, base, themeid) { 124 | 125 | var api = shopifyAPI, 126 | themeId = themeid, 127 | key = shopify._makeAssetKey(filepath, base), 128 | isBinary = isBinaryFile(filepath), 129 | props = { 130 | asset: { 131 | key: key 132 | } 133 | }, 134 | contents; 135 | 136 | contents = file.contents; 137 | 138 | if (isBinary) { 139 | props.asset.attachment = contents.toString('base64'); 140 | } else { 141 | props.asset.value = contents.toString(); 142 | } 143 | 144 | function onUpdate(err, resp) { 145 | if (err && err.type === 'ShopifyInvalidRequestError') { 146 | gutil.log(gutil.colors.red('Error invalid upload request! ' + filepath + ' not uploaded to ' + host)); 147 | } else if (!err) { 148 | var filename = filepath.replace(/^.*[\\\/]/, ''); 149 | gutil.log(gutil.colors.green('Upload Complete: ' + filename)); 150 | } else { 151 | gutil.log(gutil.colors.red('Error undefined! ' + err.type)); 152 | } 153 | } 154 | 155 | if (themeId) { 156 | api.asset.update(themeId, props, onUpdate); 157 | } else { 158 | api.assetLegacy.update(props, onUpdate); 159 | } 160 | }; 161 | 162 | /* 163 | * Public function for process deployment queue for new files added via the stream. 164 | * The queue is processed based on Shopify's leaky bucket algorithm that allows 165 | * for infrequent bursts calls with a bucket size of 40. This regenerates overtime, 166 | * but offers an unlimited leak rate of 2 calls per second. Use this variable to 167 | * keep track of api call rate to calculate deployment. 168 | * https://docs.shopify.com/api/introduction/api-call-limit 169 | * 170 | * @param {apiKey} string - Shopify developer api key 171 | * @param {password} string - Shopify developer api key password 172 | * @param {host} string - hostname provided from gulp file 173 | * @param {themeid} string - unique id upload to the Shopify theme 174 | * @param {options} object - named array of custom overrides. 175 | */ 176 | function gulpShopifyUpload(apiKey, password, host, themeid, options) { 177 | 178 | // queue files provided in the stream for deployment 179 | var apiBurstBucketSize = 40, 180 | uploadedFileCount = 0, 181 | stream; 182 | 183 | // Set up the API 184 | shopify._setOptions(options); 185 | shopifyAPI = shopify._getApi(apiKey, password, host); 186 | 187 | gutil.log('Ready to upload to ' + gutil.colors.magenta(host)); 188 | 189 | if (typeof apiKey === 'undefined') { 190 | throw new PluginError(PLUGIN_NAME, 'Error, API Key for shopify does not exist!'); 191 | } 192 | if (typeof password === 'undefined') { 193 | throw new PluginError(PLUGIN_NAME, 'Error, password for shopify does not exist!'); 194 | } 195 | if (typeof host === 'undefined') { 196 | throw new PluginError(PLUGIN_NAME, 'Error, host for shopify does not exist!'); 197 | } 198 | 199 | // creating a stream through which each file will pass 200 | stream = through.obj(function (file, enc, cb) { 201 | if (file.isStream()) { 202 | this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported!')); 203 | return cb(); 204 | } 205 | 206 | if (file.isBuffer()) { 207 | // deploy immediately if within the burst bucket size, otherwise queue 208 | if (uploadedFileCount <= apiBurstBucketSize) { 209 | shopify.upload(file.path, file, host, '', themeid); 210 | } else { 211 | // Delay deployment based on position in the array to deploy 2 files per second 212 | // after hitting the initial burst bucket limit size 213 | setTimeout(shopify.upload.bind(null, file.path, file, host, '', themeid), ((uploadedFileCount - apiBurstBucketSize) / 2) * 1000); 214 | } 215 | uploadedFileCount++; 216 | } 217 | 218 | // make sure the file goes through the next gulp plugin 219 | this.push(file); 220 | 221 | // tell the stream engine that we are done with this file 222 | cb(); 223 | }); 224 | 225 | // returning the file stream 226 | return stream; 227 | } 228 | 229 | // exporting the plugin main function 230 | module.exports = gulpShopifyUpload; 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-shopify-upload", 3 | "version": "2.0.0", 4 | "description": "A tool to watch and upload files to Shopify for use in theme editing", 5 | "keywords": [ 6 | "gulp", 7 | "shopify", 8 | "shopify-upload", 9 | "shopify-theme", 10 | "ablesense" 11 | ], 12 | "homepage": "https://github.com/mikenorthorp/gulp-shopify-upload", 13 | "bugs": { 14 | "url": "https://github.com/mikenorthorp/gulp-shopify-upload/issues" 15 | }, 16 | "author": { 17 | "name": "Able Sense Media", 18 | "url": "http://ablesense.com" 19 | }, 20 | "contributors": [ 21 | { 22 | "name": "Mike Northorp", 23 | "email": "mike.northorp@gmail.com", 24 | "url": "https://github.com/mikenorthorp" 25 | }, 26 | { 27 | "name": "Roy Martin", 28 | "email": "roy@roy-martin.com", 29 | "url": "https://github.com/rmartin" 30 | } 31 | ], 32 | "main": "index.js", 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/mikenorthorp/gulp-shopify-upload.git" 36 | }, 37 | "dependencies": { 38 | "isbinaryfile": "2.0.0", 39 | "path": "0.11.14", 40 | "shopify-api": "0.2.2", 41 | "through2": "0.6.3", 42 | "gulp-util": "3.0.2" 43 | }, 44 | "devDependencies": { 45 | "gulp": "3.8.10" 46 | }, 47 | "engines": { 48 | "node": ">=0.8.0", 49 | "npm": ">=1.2.10" 50 | }, 51 | "licenses": [ 52 | { 53 | "type": "MIT" 54 | } 55 | ], 56 | "maintainers": [ 57 | { 58 | "name": "mikenorthorp", 59 | "email": "mike.northorp@gmail.com" 60 | } 61 | ], 62 | "scripts": {} 63 | } --------------------------------------------------------------------------------