Development Environment Setup Intermediate 15 min read

Setting Up Vite for Shopify Theme Development

Configure Vite to compile React components and output directly to your Shopify theme's assets folder. Simple setup with no intermediate build folders.

Vite is a modern build tool that will compile our React code and output it directly to the Shopify theme. In this lesson, we’ll configure Vite to build straight to shopify-theme/assets/—no intermediate folders, no copy steps.

Prerequisites

Before we begin, ensure you have:

  • Node.js 18+ installed
  • The folder structure from the previous lesson
  • Basic familiarity with npm/yarn

Step 1: Initialize the Project

From your project root (not inside shopify-theme/), initialize npm:

Terminal window
npm init -y

Install the core dependencies:

Terminal window
# React and ReactDOM
npm install react react-dom
# Vite and React plugin
npm install -D vite @vitejs/plugin-react
# TypeScript support
npm install -D typescript @types/react @types/react-dom

Step 2: Create the Vite Configuration

Create vite.config.ts in your project root:

vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
/*
* Vite config for Shopify themes
* Key difference from standard React apps: we output directly to the theme folder
* instead of a dist/ directory, so Shopify CLI can sync the files immediately
*/
export default defineConfig({
plugins: [react()],
build: {
// Output directly to shopify-theme/assets - no intermediate folders
// This is what makes the Shopify CLI workflow seamless
outDir: 'shopify-theme/assets',
// CRITICAL: Don't clear the folder! Other theme assets (images, fonts,
// existing CSS) live here. Setting this to true would delete them.
emptyOutDir: false,
// Enable source maps so you can debug original .tsx files in browser DevTools
sourcemap: true,
rollupOptions: {
// Single entry point - Vite will bundle everything this imports
input: resolve(__dirname, 'src/main.tsx'),
output: {
// Fixed filename so Liquid can reference it reliably
// (no hash = same URL on every build = easier caching management)
entryFileNames: 'react-bundle.js',
// CSS extracted to a separate file for better caching
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.css')) {
return 'react-bundle.css';
}
return 'react-[name].[ext]';
},
// Disable code splitting - simpler for Shopify themes
// One bundle loads on all pages, cached by the browser
manualChunks: undefined,
},
},
// Terser for smaller production bundles
minify: 'terser',
terserOptions: {
compress: {
// Remove console.log in production (saves ~2KB and cleans up DevTools)
drop_console: process.env.NODE_ENV === 'production',
},
},
},
resolve: {
// Path aliases for cleaner imports: '@/components/Button' vs '../../components/Button'
// Must match paths in tsconfig.json for TypeScript to understand them
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@hooks': resolve(__dirname, 'src/hooks'),
'@stores': resolve(__dirname, 'src/stores'),
'@utils': resolve(__dirname, 'src/utils'),
'@types': resolve(__dirname, 'src/types'),
},
},
});

Key settings:

  • outDir: 'shopify-theme/assets' — Vite outputs directly to your theme
  • emptyOutDir: false — Preserves other assets (images, base CSS, fonts)
  • No copy plugin needed—files go straight where they belong

Step 3: Configure TypeScript

Create tsconfig.json:

{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"],
"@stores/*": ["src/stores/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

Create tsconfig.node.json for Vite’s Node.js context:

{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

Step 4: Create the Source Directory Structure

Set up your src/ folder:

Terminal window
mkdir -p src/{components,hooks,stores,types,utils,styles}

Create the entry point src/main.tsx:

src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
// Global styles are bundled into react-bundle.css
import './styles/main.css';
/*
* Component Registry Pattern
* Maps DOM element IDs to React components. When the page loads,
* we scan for these IDs and mount the corresponding components.
* This allows Liquid to define mount points: <div id="product-form-root"></div>
*/
const COMPONENTS: Record<string, React.LazyExoticComponent<React.ComponentType>> = {
// Components will be added here as we build them
// Format: 'dom-element-id': lazy(() => import('./path/to/Component')),
// 'product-form-root': lazy(() => import('./components/product/ProductForm')),
};
function mountComponents() {
// For each registered component, find its mount point and render
Object.entries(COMPONENTS).forEach(([elementId, Component]) => {
const element = document.getElementById(elementId);
if (element) {
// createRoot is React 18's new API (replaces ReactDOM.render)
const root = createRoot(element);
root.render(
// StrictMode helps catch common bugs during development
// (double-renders components to detect side effects)
<StrictMode>
<Component />
</StrictMode>
);
}
});
}
// Handle both cases: script loaded before or after DOM is ready
if (document.readyState === 'loading') {
// DOM not ready yet - wait for it
document.addEventListener('DOMContentLoaded', mountComponents);
} else {
// DOM already ready (script was deferred or loaded late)
mountComponents();
}
// Development-only logging (stripped in production build)
if (import.meta.env.DEV) {
console.log('🚀 React bundle initialized');
}

Create a basic styles file src/styles/main.css:

src/styles/main.css
/*
* React component styles
* These are bundled into react-bundle.css
*/
/* CSS Variables (can be overridden by Liquid/theme settings) */
:root {
--react-transition-fast: 150ms;
--react-transition-normal: 300ms;
--react-transition-slow: 500ms;
}
/* Base component resets */
.react-component {
box-sizing: border-box;
}
.react-component *,
.react-component *::before,
.react-component *::after {
box-sizing: inherit;
}

Step 5: Create the Shopify Theme Structure

If you don’t have an existing theme, create the basic structure:

Terminal window
mkdir -p shopify-theme/{assets,config,layout,locales,sections,snippets,templates}

Create a minimal shopify-theme/layout/theme.liquid:

<!DOCTYPE html>
<html lang="{{ shop.locale }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{%- comment -%} React CSS {%- endcomment -%}
{{ 'react-bundle.css' | asset_url | stylesheet_tag }}
{{ content_for_header }}
</head>
<body>
{{ content_for_layout }}
{%- comment -%} React JS - loaded at end of body {%- endcomment -%}
<script src="{{ 'react-bundle.js' | asset_url }}" defer></script>
</body>
</html>

Step 6: Add npm Scripts

Update your package.json:

{
"name": "shopify-react-theme",
"private": true,
"type": "module",
"scripts": {
"dev": "vite build --watch --mode development",
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}

Step 7: Test the Build

Run the development build:

Terminal window
npm run dev

You should see output like:

vite v5.0.0 building for development...
✓ 2 modules transformed.
shopify-theme/assets/react-bundle.js 45.12 kB │ gzip: 14.89 kB
shopify-theme/assets/react-bundle.css 0.15 kB │ gzip: 0.10 kB
✓ built in 234ms
watching for file changes...

Check that the files exist:

Terminal window
ls -la shopify-theme/assets/
# react-bundle.js, react-bundle.css

That’s it! No intermediate folders, no copy steps. Files go directly where Shopify needs them.

Advanced Configuration

Environment Variables

Create environment files:

.env.development
VITE_DEBUG=true
# .env.production
VITE_DEBUG=false

Access in your React code:

if (import.meta.env.VITE_DEBUG === 'true') {
console.log('Debug mode enabled');
}

Multiple Entry Points (Advanced)

For large themes, you might want separate bundles per page type:

vite.config.ts
/*
* Multiple Entry Points: Split code by page type
* Useful for large themes where you don't want to load all React code on every page
* Trade-off: More files to manage, but smaller per-page bundles
*/
export default defineConfig({
build: {
outDir: 'shopify-theme/assets',
emptyOutDir: false,
rollupOptions: {
// Each entry creates a separate bundle
input: {
main: resolve(__dirname, 'src/main.tsx'), // Shared components (header, cart)
product: resolve(__dirname, 'src/entries/product.tsx'), // Product page only
collection: resolve(__dirname, 'src/entries/collection.tsx'), // Collection page only
},
output: {
// [name] is replaced with the key from input (main, product, collection)
entryFileNames: 'react-[name].js',
assetFileNames: 'react-[name].[ext]',
},
},
},
});

Then load conditionally in Liquid:

{%- if template.name == 'product' -%}
<script src="{{ 'react-product.js' | asset_url }}" defer></script>
{%- endif -%}

CSS Modules

Vite supports CSS Modules out of the box:

Button.module.css
.button {
padding: 1rem 2rem;
background: var(--color-primary);
}
.button:hover {
opacity: 0.9;
}
Button.tsx
import styles from './Button.module.css';
export function Button({ children }) {
return <button className={styles.button}>{children}</button>;
}

Production Optimization

For production builds, add these optimizations:

vite.config.ts
/*
* Production Optimization Settings
* Shopify themes benefit from smaller bundles since assets are served via CDN
*/
export default defineConfig({
build: {
// Assets smaller than 4KB are inlined as base64
// Reduces HTTP requests for tiny icons/images
assetsInlineLimit: 4096,
// Warn if any chunk exceeds 50KB (Shopify best practice)
// Large bundles = slower initial page load
chunkSizeWarningLimit: 50,
rollupOptions: {
output: {
// Remove whitespace and comments for smaller output
compact: true,
},
},
},
esbuild: {
// Strip console.log and debugger statements in production
// Saves bytes and keeps DevTools clean for store visitors
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
},
});

Troubleshooting

”Module not found” errors

Ensure your tsconfig.json paths match vite.config.ts aliases:

vite.config.ts
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
}
tsconfig.json
"paths": {
"@/*": ["src/*"]
}

Other assets getting deleted

Make sure emptyOutDir is set to false:

build: {
outDir: 'shopify-theme/assets',
emptyOutDir: false, // IMPORTANT!
}

TypeScript errors not blocking build

Run typecheck separately:

Terminal window
npm run typecheck # Fails on type errors
npm run build # Runs typecheck first via tsc --noEmit

Shopify not serving the new bundle

Shopify aggressively caches assets. Try:

  1. Hard refresh: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows)
  2. Wait a few minutes for CDN propagation
  3. Check that Shopify CLI is syncing the changed files

Complete File Checklist

After this lesson, you should have:

your-project/
├── shopify-theme/
│ ├── assets/
│ │ ├── react-bundle.js ✓ (generated by Vite)
│ │ └── react-bundle.css ✓ (generated by Vite)
│ └── layout/
│ └── theme.liquid ✓
├── src/
│ ├── styles/
│ │ └── main.css ✓
│ └── main.tsx ✓
├── package.json ✓
├── tsconfig.json ✓
├── tsconfig.node.json ✓
└── vite.config.ts ✓

Key Takeaways

  1. Vite outputs directly to shopify-theme/assets/—no intermediate folders
  2. Use emptyOutDir: false to preserve other theme assets
  3. Path aliases keep imports clean and maintainable
  4. Watch mode rebuilds on every save for rapid iteration
  5. Simple is better—fewer moving parts means fewer things to break

In the next lesson, we’ll cover Webpack configuration as an alternative approach.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...