Skip to main content
FreshJuice DEV uses a modern build pipeline that compiles Tailwind CSS, bundles JavaScript with esbuild, and syncs files to HubSpot using the HubSpot CLI.

Build Pipeline Overview

The build process consists of three main tools working together:

Tailwind CSS

Compiles utility-first CSS from source files

esbuild

Bundles JavaScript modules with Alpine.js

HubSpot CLI

Uploads and watches theme files

NPM Scripts

All build commands are defined in package.json and orchestrated using npm-run-all:
{
  "scripts": {
    "clean": "rimraf './_temp' './_dist' && mkdir './_temp' './_dist'",
    
    "fetch:hubspot": "hs fetch --overwrite /FreshJuice ./theme && ./scripts/post-fetch.sh",
    "upload:hubspot": "hs upload ./theme /FreshJuiceDEV",
    "watch:hubspot": "hs watch ./theme /FreshJuiceDEV",
    
    "watch:tailwind": "npx @tailwindcss/cli -i ./source/css/tailwind.css -o ./theme/css/tailwind.css --watch",
    "build:tailwind": "npx @tailwindcss/cli -i ./source/css/tailwind.css -o ./theme/css/tailwind.css",
    
    "watch:js": "npx esbuild ./source/js/main.js --outfile=./theme/js/main.js --bundle --watch",
    "build:js": "npx esbuild ./source/js/main.js --outfile=./theme/js/main.js --bundle",
    
    "build:zip": "./scripts/build-zip.sh",
    
    "version:patch": "./scripts/bump-theme-version.sh patch",
    "version:minor": "./scripts/bump-theme-version.sh minor",
    "version:major": "./scripts/bump-theme-version.sh major",
    
    "start": "npm-run-all --parallel watch:*",
    "build": "npm-run-all --serial clean build:js build:tailwind build:zip"
  }
}

Development Workflow

Starting Development

The npm run start command runs all watch tasks in parallel:
npm run start
This executes three processes simultaneously:
1

watch:tailwind

Watches for changes in CSS files and recompiles Tailwind
2

watch:js

Watches for changes in JavaScript files and rebuilds the bundle
3

watch:hubspot

Watches the theme directory and uploads changes to HubSpot
The npm-run-all --parallel command ensures all three processes run concurrently, providing instant feedback during development.

File Watching Flow

Tailwind CSS Compilation

Input Configuration

Tailwind scans multiple sources for class names:
@import "tailwindcss" source(none);

/* Tailwind scans these paths for class names */
@source "../../theme/**/*.html";
@source "../../theme/**/*.js";
@source "../js/farmerswife.js";
@source "./**/*.css";

@theme {
  /* Custom CSS variables */
  --color-cursor: #FFFFFF;
  --color-terminal: #000000;
  --font-cursive: "Caveat", cursive;
}

/* Layer imports */
@import "./base/reset.css";
@import "./base/typography.css";
@import "./components/system.css";

Compilation Process

Development mode with watch flag:
npx @tailwindcss/cli \
  -i ./source/css/tailwind.css \
  -o ./theme/css/tailwind.css \
  --watch
Features:
  • Fast incremental builds
  • Source maps for debugging
  • Instant recompilation on file changes

Custom Theme Configuration

The @theme directive extends Tailwind with custom design tokens:
@theme {
  /* Custom colors */
  --color-cursor: #FFFFFF;
  --color-cursor-500: #FFFFFF;
  --color-terminal: #000000;
  --color-terminal-500: #000000;

  /* Custom fonts */
  --font-cursive: "Caveat", cursive;

  /* Custom spacing */
  --spacing-screenSinNav: calc(100vh - 5rem);

  /* Aspect ratios */
  --aspect-video: 16 / 9;
  --aspect-golden: 1.6180 / 1;
  --aspect-macbook: 16 / 10;
}
Use CSS custom properties in the @theme directive to create reusable design tokens that can be referenced throughout your theme.

JavaScript Bundling with esbuild

Bundle Configuration

esbuild bundles all JavaScript modules into a single file:
npx esbuild ./source/js/main.js \
  --outfile=./theme/js/main.js \
  --bundle \
  --watch  # Only in development

Entry Point

The main JavaScript file imports all dependencies:
import debugLog from './modules/_debugLog';
import loadScript from './modules/_loadScript';
import flyingPages from "flying-pages-module";
import Alpine from "alpinejs";
import intersect from "@alpinejs/intersect";
import collapse from "@alpinejs/collapse";
import focus from "@alpinejs/focus";
import dataDOM from "./modules/Alpine.data/DOM";

// Make Alpine available globally
window.Alpine = Alpine;

// Register Alpine plugins
Alpine.plugin(intersect);
Alpine.plugin(collapse);
Alpine.plugin(focus);

// Register Alpine data
Alpine.data("xDOM", dataDOM);

// Initialize on DOM ready
domReady(() => {
  Alpine.start();
  flyingPages({});
});

Bundled Dependencies

All npm packages are bundled into the output:
"alpinejs": "^3.14.9"
Minimal JavaScript framework for declarative interactivity
"@alpinejs/collapse": "^3.14.9",
"@alpinejs/focus": "^3.14.9",
"@alpinejs/intersect": "^3.14.9"
Official Alpine.js plugins for common patterns
"flying-pages-module": "^2.1.4"
Prefetches pages to speed up navigation
"lite-youtube-embed": "^0.3.3"
Lightweight YouTube embeds for better performance

Bundle Output

The compiled bundle is placed in theme/js/main.js and included in the base template:
{{ require_js(get_asset_url("../../js/main.js"), { position: "footer", defer: true }) }}
esbuild is extremely fast, typically bundling in milliseconds. This makes the watch mode highly responsive during development.

HubSpot CLI Integration

Authentication

Before using HubSpot CLI commands, authenticate with your portal:
hs auth

Common Operations

Continuously upload changes during development:
npm run watch:hubspot
# Or directly:
hs watch ./theme /FreshJuiceDEV
  • Monitors ./theme directory
  • Uploads changes automatically
  • Shows upload status in terminal

Portal Configuration

The HubSpot CLI uses hubspot.config.yml for portal settings:
defaultPortal: production
portals:
  - name: production
    portalId: 12345678
    authType: personalaccesskey
    auth:
      tokenInfo:
        accessToken: your-access-token
Never commit hubspot.config.yml with authentication tokens. Add it to .gitignore to prevent accidental exposure.

Production Build

Full Build Process

Run a complete production build:
npm run build
This executes the following steps sequentially:
1

clean

Remove _temp/ and _dist/ directories, then recreate them
rimraf './_temp' './_dist' && mkdir './_temp' './_dist'
2

build:js

Bundle JavaScript with esbuild (production mode)
npx esbuild ./source/js/main.js --outfile=./theme/js/main.js --bundle
3

build:tailwind

Compile Tailwind CSS (production mode with minification)
npx @tailwindcss/cli -i ./source/css/tailwind.css -o ./theme/css/tailwind.css
4

build:zip

Create distribution ZIP file
./scripts/build-zip.sh

Distribution Package

The build creates a versioned ZIP file ready for distribution:
_dist/
└── freshjuice-dev-hubspot-theme-v3.0.0-dev.zip
This ZIP contains the complete theme/ directory and can be:
  • Uploaded to HubSpot Design Manager
  • Shared with clients or team members
  • Archived for version control

Version Management

Bumping Version Numbers

Use semantic versioning scripts:
npm run version:patch
The version script updates:
  • package.json version number
  • Theme metadata files
  • Git tags (if in a git repository)

Build Optimization

Performance Tips

Tailwind Optimization

  • Only import needed CSS layers
  • Use @source to limit scan paths
  • Leverage JIT mode for smaller builds
  • Remove unused @import statements

JavaScript Optimization

  • Import only used Alpine plugins
  • Tree-shake unused dependencies
  • Use dynamic imports for heavy modules
  • Minimize third-party dependencies

HubSpot CLI Tips

  • Use watch during active development
  • Upload only when making large changes
  • Exclude files with .hsignore
  • Test locally before uploading

Caching Strategy

  • Leverage HubSpot’s CDN caching
  • Use versioned asset URLs
  • Set appropriate cache headers
  • Invalidate cache after major updates

Troubleshooting

Cause: Files not included in @source directivesSolution: Add the file path to source/css/tailwind.css:
@source "../../theme/path/to/your/file.html";
Cause: Missing dependencies or import errorsSolution: Check that all imports are valid:
npm install  # Reinstall dependencies
npm run build:js  # Run build to see detailed errors
Cause: Authentication issues or file permissionsSolution: Re-authenticate and check file paths:
hs auth  # Re-authenticate
hs upload ./theme /FreshJuiceDEV --debug  # Upload with debug info
Cause: Too many files being scanned or watchedSolution: Optimize @source paths and use .hsignore:
.hsignore
node_modules/
_temp/
_dist/
.git/

Best Practices

Always run npm run start during active development to ensure changes are compiled and uploaded in real-time.
1

Use watch mode

Keep all watchers running during development for instant feedback
2

Test builds locally

Run npm run build before deploying to catch compilation errors
3

Version consistently

Use the version scripts to maintain semantic versioning
4

Clean before major builds

Run npm run clean if you encounter caching issues

Project Structure

Understand where files are located

Theme Architecture

Learn how components work together

Build docs developers (and LLMs) love