Skip to content

Architecture

Anthony Baggett edited this page Nov 21, 2025 · 1 revision

Architecture

Understanding VAST's project structure, file organization, and architectural decisions.


Project Structure

vast-shopify-theme/
├── src/                        # Source files (compiled by Vite)
│   ├── entrypoints/            # Vite entry points
│   │   ├── theme.js            # Main JavaScript entry (all pages)
│   │   └── theme.css           # Main CSS entry (all pages)
│   ├── js/
│   │   ├── alpine/             # Alpine.js components and stores
│   │   │   ├── components/     # Extracted Alpine components
│   │   │   │   ├── product/    # Product page components
│   │   │   │   ├── collection/ # Collection page components
│   │   │   │   ├── blog/       # Blog page components
│   │   │   │   └── shared/     # Shared components
│   │   │   ├── stores/         # Global Alpine stores
│   │   │   │   ├── global.js   # Global state (menu, cart, scroll)
│   │   │   │   ├── recentlyViewed.js # Recently viewed tracking
│   │   │   │   └── debug.js    # Debug utilities
│   │   │   └── index.js        # Auto-registration system
│   │   └── helpers.js          # Utility functions (window.vast.helpers)
│   ├── css/
│   │   ├── base.css            # Base layer styles
│   │   ├── components.css      # Component layer styles
│   │   ├── utilities.css       # Utility layer styles
│   │   └── global.css          # Non-tree-shaken global styles
│   ├── schema/                 # Schematic schema definitions
│   │   ├── *.cjs               # Section schemas
│   │   ├── settings_schema.cjs # Global settings schema
│   │   └── locales/            # Locale schemas
│   │       └── *.cjs           # Translation files
│   ├── fonts/                  # Custom font files
│   └── images/                 # Image assets
├── public/                     # Static assets (copied to assets/)
│   ├── predictive-search.js    # Web component for search
│   └── *.svg, *.png, etc.      # Static images/icons
├── sections/                   # Shopify section files (30+)
├── snippets/                   # Shopify snippet files (35+)
├── templates/                  # Shopify template files (12+)
│   ├── *.json                  # Main templates
│   └── customers/              # Customer account templates
│       └── *.liquid
├── layout/
│   └── theme.liquid            # Main layout file
├── config/
│   ├── settings_schema.json    # Theme settings (generated)
│   └── settings_data.json      # Theme setting values
├── locales/                    # Translation files (generated)
│   └── *.json
├── assets/                     # Built assets (generated by Vite)
│   ├── *.min.js                # Compiled JavaScript
│   ├── *.min.css               # Compiled CSS
│   └── *                       # Static files from public/
├── docs/                       # Contributor documentation
│   ├── ROADMAP.md              # Project roadmap
│   └── STYLING-GUIDE.md        # Styling standards
├── vite.config.js              # Vite configuration
├── tailwind.config.js          # Tailwind configuration
├── package.json                # NPM dependencies and scripts
├── shopify.theme.toml          # Shopify CLI config (gitignored)
├── README.md                   # Project README
└── CLAUDE.md                   # Claude Code instructions


File Organization Conventions

Source Files (src/)

All source files that require compilation live in src/:

JavaScript:

  • Entry points: src/entrypoints/*.js
  • Alpine components: src/js/alpine/components/**/*.js
  • Alpine stores: src/js/alpine/stores/*.js
  • Utilities: src/js/helpers.js

CSS:

  • Entry point: src/entrypoints/theme.css
  • Layer files: src/css/{base,components,utilities,global}.css

Schemas:

  • Section schemas: src/schema/*.cjs
  • Locale schemas: src/schema/locales/*.cjs

Public Directory

Static files that don't need processing go in public/:

  • Web components (predictive-search.js)
  • Static images and icons
  • Font files (if not using @font-face from src/fonts/)

Important: Files in public/ are copied to assets/ as-is during build.

Generated Files

These files are auto-generated and should not be edited directly:

  • assets/*.min.js - Compiled JavaScript from Vite
  • assets/*.min.css - Compiled CSS from Vite
  • snippets/vite.liquid - Auto-generated by vite-plugin-shopify (gitignored)
  • config/settings_schema.json - Generated by Schematic from src/schema/settings_schema.cjs
  • locales/*.json - Generated by Schematic from src/schema/locales/*.cjs
  • Schema in sections - Schematic injects JSON schema into section files

Build System Architecture

Vite Configuration

Key plugins (vite.config.js):

  1. vite-plugin-shopify - Core Shopify integration

    • Compiles entrypoints to hashed assets
    • Generates snippets/vite.liquid for asset references
    • Configured with sourceCodeDir: 'src' and entrypointsDir: 'src/entrypoints'
  2. @tailwindcss/vite - Tailwind CSS v4 processing

    • Processes CSS layers
    • Tree-shakes unused styles
    • Generates optimized CSS bundles
  3. @vitejs/plugin-basic-ssl - Local HTTPS

  4. vite-plugin-page-reload - Liquid file watching

    • Watches /tmp/theme.update for Shopify CLI reload triggers
    • Triggers page reload when Liquid files change
  5. Custom copyPublicToAssetsPlugin - Static file syncing

    • Watches public/ directory in dev mode
    • Copies files to assets/ in real-time

Build Process

Development (npm run dev):

  1. Vite runs in build --watch mode
  2. Compiles source files on change
  3. Writes to assets/ directory
  4. Shopify CLI uploads to development store
  5. Page reload plugin watches for Liquid changes

Production (npm run build):

  1. Vite builds and minifies all assets
  2. Schematic compiles all schema files
  3. Output written to assets/ directory
  4. Ready for deployment

Alpine.js Architecture

Single Entrypoint with Auto-Registration

VAST uses a single-entrypoint architecture where all components are registered automatically at startup.

Auto-registration system (src/js/alpine/index.js):

// Automatically imports all stores
const storeModules = import.meta.glob('./stores/*.js', { eager: true })

// Register each store
for (const path in storeModules) {
  const storeName = path.match(/\.\/stores\/(.+)\.js$/)[1]
  Alpine.store(storeName, storeModules[path].default)
}

// Automatically imports all components
const componentModules = import.meta.glob('./components/**/*.js', { eager: true })

// Register each component
for (const path in componentModules) {
  const componentName = path.match(/\/([^/]+)\.js$/)[1]
  Alpine.data(componentName, componentModules[path].default)
}

Benefits:

  • No manual registration needed
  • Components initialize only when used in DOM
  • Easy to add new components (just create file)
  • Centralized component management

Component Structure

Standard component pattern:

// src/js/alpine/components/product/productGallery.js
export default function productGallery() {
  return {
    // State
    currentImageIndex: 0,
    images: [],

    // Lifecycle
    init() {
      // Read config from data attributes
      const el = this.$el
      this.images = JSON.parse(el.dataset.images || '[]')
      this.galleryLayout = el.dataset.galleryLayout || 'bottom'
    },

    // Methods
    selectImage(index) {
      this.currentImageIndex = index
    }
  }
}

Usage in Liquid:

<div x-data="productGallery"
     data-images='{{ product.images | json }}'
     data-gallery-layout="{{ section.settings.gallery_layout }}">
  <!-- Component markup -->
</div>

Store Organization

Global stores (src/js/alpine/stores/):

Stores provide cross-component state management:

// src/js/alpine/stores/global.js
export default {
  mobileMenuOpen: false,
  minicartOpen: false,
  searchOpen: false,
  scrolled: false,
  cartData: {},

  openMinicart() {
    this.minicartOpen = true
  },

  closeMinicart() {
    this.minicartOpen = false
  }
}

Access stores anywhere:

<div x-show="$store.global.minicartOpen">
  <!-- Minicart content -->
</div>

<button @click="$store.global.openMinicart()">
  Open Cart
</button>

CSS Architecture

Tailwind v4 Layer System

Layer files:

  1. src/css/base.css - Base layer (@layer base)

    • Element defaults (typography, links, buttons)
    • Normalizations
  2. src/css/components.css - Component layer (@layer components)

    • Reusable component classes (.btn, .input-select, .pill-button)
    • Complex multi-property utilities
  3. src/css/utilities.css - Utility layer (@layer utilities)

    • Custom utility classes
    • Variants and modifiers
  4. src/css/global.css - No layer (always included)

    • Styles that should never be tree-shaken
    • @apply directives
    • CSS nesting support via @tailwindcss/nesting

Custom Tailwind Variants

Defined in vite.config.js:

{
  variants: {
    scrolled: '[data-scrolled="true"] &',
    'mobile-menu-visible': '[data-mobile-menu-visible="true"] &'
  }
}

Usage:

<body
  :data-scrolled="$store.global.scrolled"
  :data-mobile-menu-visible="$store.global.mobileMenuOpen">

  <header class="scrolled:shadow-lg mobile-menu-visible:bg-white">
    <!-- Header content -->
  </header>
</body>

Schema Management (Schematic)

Why Schematic?

Shopify sections require JSON schema, which leads to duplication when defining settings, presets, and localization keys. Schematic solves this by:

  1. JavaScript-based schemas - Write schemas in .cjs files instead of JSON
  2. DRY principles - Reuse common settings and patterns
  3. Compilation - Generates JSON and injects into section files
  4. Type safety - Better IDE support and validation

Schema File Structure

Section schema (src/schema/header.cjs):

const { app } = require('@anchovie/schematic');

module.exports = {
    ...app.section("header", {
        class: "section-header sticky top-0 z-20",
        tag: "header"
    }),
    "settings": [
        app.paragraph("t:sections.header.settings.paragraph"),
        app.image("logo", {
            label: "Logo Image"
        }),
        app.range("logo_width", {
            label: "Logo Width",
            min: 50,
            max: 300,
            step: 10,
            default: 150,
            unit: "px"
        })
    ],
    "presets": [
        {
            name: "Header",
            category: "Layout"
        }
    ]
}

Compilation:

npx schematic

This injects the JSON schema into sections/header.liquid between schema tags:

{% schema %}
{
  "name": "Header",
  "class": "section-header sticky top-0 z-20",
  "tag": "header",
  "settings": [...]
}
{% endschema %}

Event-Driven Architecture

Custom Events

Components communicate via custom events:

Dispatching events:

// src/js/helpers.js
export function emitEvent(eventName, detail = {}) {
  window.dispatchEvent(new CustomEvent(eventName, { detail }))
}

// Usage in component
emitEvent('variant:change', {
  variant: selectedVariant,
  product: this.product
})

Listening for events:

// In another component
init() {
  window.addEventListener('variant:change', (e) => {
    this.updatePrice(e.detail.variant)
    this.updateGallery(e.detail.variant)
  })
}

Standard Events

Product events:

  • variant:change - Product variant selection changed

Cart events (from Liquid AJAX Cart):

  • liquid-ajax-cart:request-start - Cart operation started
  • liquid-ajax-cart:request-end - Cart operation completed
  • liquid-ajax-cart:cart-update - Cart state updated

Modal events:

  • show-modal - Open modal dialog
  • close-modal - Close modal dialog

File Naming Conventions

Sections

  • Prefix with purpose: main-* (page content), featured-* (homepage), etc.
  • Use kebab-case: main-product.liquid, featured-collection.liquid

Snippets

  • Descriptive names: product-card.liquid, article-navigation.liquid
  • Icon snippets: icon-{name}.liquid (e.g., icon-cart.liquid)
  • Utility snippets: head-meta.liquid, font-face.liquid

Components

  • camelCase JavaScript files: productGallery.js, quickViewModal.js
  • Match x-data name: x-data="productGallery" → productGallery.js

Schemas

  • Match section filename: header.liquid → header.cjs
  • CommonJS format: module.exports = {...}

Next Steps


← Back to Home