Development
Development setup, build process, and deployment for SOG Knives BigCommerce theme
Overview
SOG Knives theme development uses Stencil CLI v8.10.5 for local development and Webpack for asset compilation. A custom Vite-based admin panel provides additional management functionality.
Local Development: Stencil CLI
Build Tool: Webpack (via Stencil)
Admin Panel: Vite
Version Control: Git
Package Manager: npm
Prerequisites
Required Software
| Software | Version | Purpose |
|---|---|---|
| Node.js | 18.x or 20.x | JavaScript runtime |
| npm | 9.x+ | Package manager |
| Stencil CLI | 8.10.5 | BigCommerce theme development |
| Git | Latest | Version control |
Installation
# Install Node.js (via nvm recommended)
nvm install 20
nvm use 20
# Install Stencil CLI globally
npm install -g @bigcommerce/[email protected]
# Verify installation
stencil --version
# Expected: 8.10.5
Local Development Setup
1. Clone Theme Repository
git clone https://github.com/gsm-outdoors/sog-theme.git
cd sog-theme
2. Install Dependencies
npm install
3. Configure Stencil
Create .stencil configuration file:
stencil init
Configuration prompts:
? What is the URL of your store? https://sog-knives-store.mybigcommerce.com
? What is your username? [email protected]
? What is your API token? your-api-token
? What port would you like to run the server on? 3000
Generate API token: BigCommerce Admin → Advanced Settings → API Accounts → Create API Account
Required scopes:
- Themes:
modify,read-only - Storefront:
read-only
4. Start Development Server
stencil start
The site will be available at http://localhost:3000
Auto-reload features:
- Template changes (Handlebars): Automatic refresh
- JavaScript changes: Automatic rebuild and refresh
- Sass changes: Automatic recompile and inject
Project Structure
sog-theme/
├── .stencil # Stencil CLI configuration
├── config.json # Theme configuration
├── schema.json # Theme customizer schema
├── package.json # Node dependencies
├── webpack.config.js # Custom Webpack config
├── assets/ # Static assets
│ ├── fonts/ # Stratum2, custom fonts
│ ├── icons/ # SVG icons
│ ├── img/ # Images
│ ├── js/ # JavaScript
│ │ ├── theme/ # Theme JS modules
│ │ ├── engraving/ # Preact engraving tool
│ │ ├── contentful.js # Contentful API client
│ │ └── app.js # Main entry point
│ └── scss/ # Sass stylesheets
│ ├── settings/ # Variables
│ ├── tools/ # Mixins
│ ├── generic/ # Resets
│ ├── elements/ # Base elements
│ ├── components/ # Components
│ ├── utilities/ # Utilities
│ └── theme.scss # Main entry
├── lang/ # Translations
├── meta/ # Theme metadata
├── templates/ # Handlebars templates
│ ├── components/ # Reusable components
│ ├── layout/ # Layout templates
│ └── pages/ # Page templates
├── admin-panel/ # Vite admin panel
│ ├── src/
│ ├── public/
│ ├── index.html
│ ├── vite.config.js
│ └── package.json
└── scripts/ # Build scripts
├── build.js
└── deploy.js
Build Process
Webpack Configuration
Stencil CLI uses Webpack under the hood. Custom configuration in webpack.config.js:
// webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: {
'theme': './assets/js/app.js',
'theme-bundle': './assets/scss/theme.scss'
},
output: {
path: path.resolve(__dirname, 'assets/dist'),
filename: 'js/[name].js'
},
module: {
rules: [
// JavaScript (Babel for ES6+)
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: '> 0.25%, not dead' }],
['@babel/preset-react', { pragma: 'h' }] // For Preact
]
}
}
},
// Sass/SCSS
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
'cssnano'
]
}
}
},
'sass-loader'
]
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name][ext]'
}
},
// Images
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // Inline images < 8kb
}
},
generator: {
filename: 'img/[name].[hash][ext]'
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: 'css/[name].css'
})
],
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'react': 'preact/compat',
'react-dom': 'preact/compat'
}
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
},
devtool: process.env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map'
};
Build Commands
// package.json scripts
{
"scripts": {
"start": "stencil start",
"build": "webpack --mode=production",
"watch": "webpack --mode=development --watch",
"lint:js": "eslint assets/js/**/*.js",
"lint:scss": "stylelint assets/scss/**/*.scss",
"test": "jest",
"bundle": "stencil bundle",
"push": "stencil push",
"release": "stencil release"
}
}
Building for Production
# Build assets
npm run build
# Bundle theme
npm run bundle
# Output: sog-theme-v6.0.34.zip
Vite Admin Panel
Purpose
The custom admin panel provides:
- Engraving order management
- Contentful content preview
- Analytics dashboard
- Custom reports
- Theme settings management
Setup
cd admin-panel
npm install
npm run dev
Admin panel runs on http://localhost:5173
Vite Configuration
// admin-panel/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'https://api.gsmoutdoors.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
'/bigcommerce': {
target: 'https://api.bigcommerce.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/bigcommerce/, '')
}
}
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'vendor': ['react', 'react-dom', 'react-router-dom'],
'charts': ['recharts'],
'ui': ['@headlessui/react', 'framer-motion']
}
}
}
}
});
Admin Panel Structure
admin-panel/
├── src/
│ ├── components/
│ │ ├── Dashboard.jsx
│ │ ├── EngravingOrders.jsx
│ │ ├── ContentfulPreview.jsx
│ │ └── Analytics.jsx
│ ├── services/
│ │ ├── api.js
│ │ ├── bigcommerce.js
│ │ └── contentful.js
│ ├── hooks/
│ │ ├── useAuth.js
│ │ └── useFetch.js
│ ├── App.jsx
│ └── main.jsx
├── public/
├── index.html
├── vite.config.js
└── package.json
Code Quality Tools
ESLint
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:react/recommended'
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
rules: {
'no-console': 'warn',
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off' // Not needed with Preact
}
};
Stylelint
// .stylelintrc.js
module.exports = {
extends: [
'stylelint-config-standard-scss',
'stylelint-config-prettier-scss'
],
rules: {
'selector-class-pattern': '^[a-z][a-zA-Z0-9_-]+$',
'scss/at-rule-no-unknown': [
true,
{
ignoreAtRules: ['tailwind']
}
]
}
};
Prettier
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "avoid"
}
Testing
Jest Configuration
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'\\.(css|scss|sass)$': 'identity-obj-proxy',
'^@/(.*)$': '<rootDir>/assets/js/$1'
},
transform: {
'^.+\\.(js|jsx)$': 'babel-jest'
},
collectCoverageFrom: [
'assets/js/**/*.{js,jsx}',
'!assets/js/**/*.test.{js,jsx}'
]
};
Example Tests
// assets/js/engraving/__tests__/ValidationService.test.js
import { ValidationService } from '../services/ValidationService';
describe('ValidationService', () => {
describe('validate', () => {
it('rejects empty text', () => {
const result = ValidationService.validate('', 20);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Engraving text cannot be empty');
});
it('rejects text exceeding max length', () => {
const longText = 'A'.repeat(25);
const result = ValidationService.validate(longText, 20);
expect(result.valid).toBe(false);
});
it('accepts valid text', () => {
const result = ValidationService.validate('John Smith', 20);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
describe('sanitizeText', () => {
it('removes invalid characters', () => {
const result = ValidationService.sanitizeText('Hello<script>');
expect(result).toBe('Helloscript');
});
it('trims whitespace', () => {
const result = ValidationService.sanitizeText(' Hello ');
expect(result).toBe('Hello');
});
});
});
Running Tests
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run tests with coverage
npm test -- --coverage
Environment Variables
.env Configuration
# .env.local (not committed to Git)
# BigCommerce API
BIGCOMMERCE_STORE_HASH=abc123def456
BIGCOMMERCE_ACCESS_TOKEN=your-access-token
# Contentful
CONTENTFUL_SPACE_ID=zg6h9gisshv3
CONTENTFUL_ACCESS_TOKEN=your-delivery-token
CONTENTFUL_PREVIEW_TOKEN=your-preview-token
# GSM Outdoors API
GSM_API_BASE_URL=https://api.gsmoutdoors.com/v1
GSM_API_KEY=your-api-key
# Analytics
GA_MEASUREMENT_ID=G-XXXXXXXXXX
FB_PIXEL_ID=1234567890
# Environment
NODE_ENV=development
Loading Environment Variables
// assets/js/config.js
export const config = {
contentful: {
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: 'master'
},
gsm: {
apiUrl: process.env.GSM_API_BASE_URL,
apiKey: process.env.GSM_API_KEY
}
};
Deployment
Deployment Process
1. Prepare Release
# Update version in config.json
# Update CHANGELOG.md
# Commit changes
git add .
git commit -m "Release v6.0.34"
git tag v6.0.34
git push origin main --tags
2. Build Production Assets
npm run build
3. Bundle Theme
npm run bundle
Output: sog-theme-v6.0.34.zip
4. Upload to BigCommerce
Option A: Stencil CLI
stencil push
Option B: Manual Upload
- Login to BigCommerce Admin
- Go to Storefront → Themes
- Click Upload Theme
- Select
sog-theme-v6.0.34.zip - Click Upload
- Click Apply to make live
Deployment Checklist
- All tests passing
- Linting passes (ESLint, Stylelint)
- Version bumped in
config.json - CHANGELOG.md updated
- Git tag created
- Production build successful
- Theme bundle created
- Tested on staging environment
- Backup of current live theme created
- Theme uploaded to BigCommerce
- Visual regression tests passed
- Performance metrics checked (Lighthouse)
- Smoke tests on production
Rollback Procedure
If issues are discovered post-deployment:
- BigCommerce Admin → Storefront → Themes
- Find previous theme version in "My Themes"
- Click Apply to revert
- Investigate issues in development
- Prepare hotfix release
CI/CD Pipeline
GitHub Actions Workflow
# .github/workflows/deploy.yml
name: Deploy Theme
on:
push:
tags:
- 'v*'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Lint
run: |
npm run lint:js
npm run lint:scss
- name: Build assets
run: npm run build
- name: Bundle theme
run: |
npm install -g @bigcommerce/[email protected]
stencil bundle
- name: Upload to BigCommerce
env:
STORE_URL: ${{ secrets.STORE_URL }}
USERNAME: ${{ secrets.BC_USERNAME }}
API_TOKEN: ${{ secrets.BC_API_TOKEN }}
run: |
echo "{\"normalStoreUrl\": \"$STORE_URL\", \"username\": \"$USERNAME\", \"accessToken\": \"$API_TOKEN\", \"port\": 3000}" > .stencil
stencil push --activate
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
Troubleshooting
Common Issues
Issue: Stencil CLI Not Starting
Error: EADDRINUSE: address already in use :::3000
Solution:
# Kill process using port 3000
lsof -ti:3000 | xargs kill -9
# Or use different port
stencil start --port 3001
Issue: Asset Compilation Errors
Error: Can't resolve './components/missing-file.js'
Solution:
- Check file path is correct
- Ensure file exists
- Clear webpack cache:
rm -rf node_modules/.cache - Rebuild:
npm run build
Issue: Font Not Loading
Solution:
- Verify font file exists in
assets/fonts/ - Check
@font-facedeclaration in SCSS - Ensure font is preloaded in
<head> - Clear browser cache
Issue: Contentful Content Not Displaying
Solution:
- Verify API token in
.env - Check Space ID is correct
- Test API connection:
curl https://cdn.contentful.com/spaces/zg6h9gisshv3/entries?access_token=YOUR_TOKEN - Check browser console for errors
- Clear LocalStorage cache
Debug Mode
Enable Stencil debug mode:
stencil start --debug
Enable verbose webpack output:
npm run build -- --stats verbose
Performance Optimization
Bundle Analysis
Analyze webpack bundle size:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ... other config
plugins: [
// ... other plugins
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE ? 'server' : 'disabled'
})
]
};
ANALYZE=true npm run build
Code Splitting
Split large bundles:
// assets/js/app.js
// Dynamic imports for heavy components
const loadEngravingTool = () => import(/* webpackChunkName: "engraving" */ './engraving/app');
const loadContentful = () => import(/* webpackChunkName: "contentful" */ './contentful');
// Load only when needed
if (document.querySelector('[data-engraving-tool]')) {
loadEngravingTool().then(module => module.init());
}
Image Optimization
# Install image optimization tools
npm install --save-dev imagemin imagemin-mozjpeg imagemin-pngquant imagemin-svgo
// scripts/optimize-images.js
const imagemin = require('imagemin');
const imageminMozjpeg = require('imagemin-mozjpeg');
const imageminPngquant = require('imagemin-pngquant');
const imageminSvgo = require('imagemin-svgo');
(async () => {
await imagemin(['assets/img/*.{jpg,png,svg}'], {
destination: 'assets/img/optimized',
plugins: [
imageminMozjpeg({ quality: 80 }),
imageminPngquant({ quality: [0.6, 0.8] }),
imageminSvgo()
]
});
})();
Documentation
JSDoc Comments
Document JavaScript functions:
/**
* Validates engraving text input
* @param {string} text - Text to validate
* @param {number} maxCharacters - Maximum allowed characters
* @returns {Object} Validation result
* @returns {boolean} returns.valid - Whether text is valid
* @returns {string[]} returns.errors - Array of error messages
*/
export function validateEngravingText(text, maxCharacters) {
// ...
}
Sass Documentation
/// Mixin to generate responsive font sizes
/// @param {Number} $min-size - Minimum font size in px
/// @param {Number} $max-size - Maximum font size in px
/// @example
/// .heading {
/// @include fluid-type(16, 24);
/// }
@mixin fluid-type($min-size, $max-size) {
// ...
}