Skip to main content

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

SoftwareVersionPurpose
Node.js18.x or 20.xJavaScript runtime
npm9.x+Package manager
Stencil CLI8.10.5BigCommerce theme development
GitLatestVersion 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

  1. Login to BigCommerce Admin
  2. Go to Storefront → Themes
  3. Click Upload Theme
  4. Select sog-theme-v6.0.34.zip
  5. Click Upload
  6. 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:

  1. BigCommerce Admin → Storefront → Themes
  2. Find previous theme version in "My Themes"
  3. Click Apply to revert
  4. Investigate issues in development
  5. 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:

  1. Check file path is correct
  2. Ensure file exists
  3. Clear webpack cache: rm -rf node_modules/.cache
  4. Rebuild: npm run build

Issue: Font Not Loading

Solution:

  1. Verify font file exists in assets/fonts/
  2. Check @font-face declaration in SCSS
  3. Ensure font is preloaded in <head>
  4. Clear browser cache

Issue: Contentful Content Not Displaying

Solution:

  1. Verify API token in .env
  2. Check Space ID is correct
  3. Test API connection: curl https://cdn.contentful.com/spaces/zg6h9gisshv3/entries?access_token=YOUR_TOKEN
  4. Check browser console for errors
  5. 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) {
// ...
}