
Shopify
- 422 installs
- 2.2k repo stars
- Updated April 3, 2026
- mrgoonie/claudekit-skills
shopify is an agent skill that teaches Shopify app development with GraphQL Admin API, Shopify CLI, Polaris UI, Liquid themes, checkout extensions, webhooks, and billing for developers building merchant integrations.
About
shopify is a ClaudeKit agent skill for building on the Shopify platform with @shopify/cli, GraphQL Admin API, REST legacy endpoints, Polaris design system, and Liquid templating. The skill covers OAuth app setup via shopify.app.toml scopes, checkout_ui_extension and admin_block extension types, POS UI extensions, Shopify Functions for discounts and validation, and theme development with shopify theme dev on port 9292. Three reference guides cover app development, extensions, and themes, plus a shopify_init.py scaffold script. API examples use the 2025-01 Admin API version with pagination, bulk operations, and webhook signature verification patterns. Developers reach for shopify when creating public or custom apps, customizing checkout, managing products and orders via API, or building Liquid storefront sections. Best practices emphasize minimal scopes, GraphQL field selection to control query cost, and development-store testing before production deployment.
- Admin GraphQL API
- Webhook handlers
- App OAuth setup
- Storefront extensions
Shopify by the numbers
- 422 all-time installs (skills.sh)
- +5 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #1,003 of 4,348 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mrgoonie/claudekit-skills --skill shopifyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 422 |
|---|---|
| repo stars | ★ 2.2k |
| Last updated | April 3, 2026 |
| Repository | mrgoonie/claudekit-skills ↗ |
How do you build a Shopify app with GraphQL Admin API?
Build Shopify apps, Admin API flows, webhooks, and storefront extensions for products, checkout, orders, and merchant automation.
Who is it for?
Developers building Shopify public apps, checkout customizations, admin dashboard extensions, or theme features who need CLI workflows, GraphQL patterns, and Polaris UI conventions.
Skip if: Developers building generic ecommerce backends without Shopify or teams only needing bulk CSV product imports—use shopify-products or shopify-setup skills in specialized plugin bundles.
When should I use this skill?
The user asks to build a Shopify app, implement checkout UI extensions, query products via Admin API, configure webhooks, or develop Liquid theme sections.
What you get
Shopify app project with shopify.app.toml scopes, OAuth flow, GraphQL queries, extension manifests, webhook handlers, and optional Liquid theme files.
- shopify.app.toml with OAuth scopes
- GraphQL Admin API integration code
- Extension or theme project ready for shopify app deploy
By the numbers
- References Shopify Admin API version 2025-01
- Includes 3 reference guides plus shopify_init.py scaffold script
- Documents 5 Shopify extension types including checkout UI and Functions
Files
Shopify Development
Comprehensive guide for building on Shopify platform: apps, extensions, themes, and API integrations.
Platform Overview
Core Components:
- Shopify CLI - Development workflow tool
- GraphQL Admin API - Primary API for data operations (recommended)
- REST Admin API - Legacy API (maintenance mode)
- Polaris UI - Design system for consistent interfaces
- Liquid - Template language for themes
Extension Points:
- Checkout UI - Customize checkout experience
- Admin UI - Extend admin dashboard
- POS UI - Point of Sale customization
- Customer Account - Post-purchase pages
- Theme App Extensions - Embedded theme functionality
Quick Start
Prerequisites
# Install Shopify CLI
npm install -g @shopify/cli@latest
# Verify installation
shopify versionCreate New App
# Initialize app
shopify app init
# Start development server
shopify app dev
# Generate extension
shopify app generate extension --type checkout_ui_extension
# Deploy
shopify app deployTheme Development
# Initialize theme
shopify theme init
# Start local preview
shopify theme dev
# Pull from store
shopify theme pull --live
# Push to store
shopify theme push --developmentDevelopment Workflow
1. App Development
Setup:
shopify app init
cd my-appConfigure Access Scopes (shopify.app.toml):
[access_scopes]
scopes = "read_products,write_products,read_orders"Start Development:
shopify app dev # Starts local server with tunnelAdd Extensions:
shopify app generate extension --type checkout_ui_extensionDeploy:
shopify app deploy # Builds and uploads to Shopify2. Extension Development
Available Types:
- Checkout UI -
checkout_ui_extension - Admin Action -
admin_action - Admin Block -
admin_block - POS UI -
pos_ui_extension - Function -
function(discounts, payment, delivery, validation)
Workflow:
shopify app generate extension
# Select type, configure
shopify app dev # Test locally
shopify app deploy # Publish3. Theme Development
Setup:
shopify theme init
# Choose Dawn (reference theme) or start freshLocal Development:
shopify theme dev
# Preview at localhost:9292
# Auto-syncs to development themeDeployment:
shopify theme push --development # Push to dev theme
shopify theme publish --theme=123 # Set as liveWhen to Build What
Build an App When:
- Integrating external services
- Adding functionality across multiple stores
- Building merchant-facing admin tools
- Managing store data programmatically
- Implementing complex business logic
- Charging for functionality
Build an Extension When:
- Customizing checkout flow
- Adding fields/features to admin pages
- Creating POS actions for retail
- Implementing discount/payment/shipping rules
- Extending customer account pages
Build a Theme When:
- Creating custom storefront design
- Building unique shopping experiences
- Customizing product/collection pages
- Implementing brand-specific layouts
- Modifying homepage/content pages
Combination Approach:
App + Theme Extension:
- App handles backend logic and data
- Theme extension provides storefront UI
- Example: Product reviews, wishlists, size guides
Essential Patterns
GraphQL Product Query
query GetProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
variants(first: 5) {
edges {
node {
id
price
inventoryQuantity
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}Checkout Extension (React)
import { reactExtension, BlockStack, TextField, Checkbox } from '@shopify/ui-extensions-react/checkout';
export default reactExtension('purchase.checkout.block.render', () => <Extension />);
function Extension() {
const [message, setMessage] = useState('');
return (
<BlockStack>
<TextField label="Gift Message" value={message} onChange={setMessage} />
</BlockStack>
);
}Liquid Product Display
{% for product in collection.products %}
<div class="product-card">
<img src="{{ product.featured_image | img_url: 'medium' }}" alt="{{ product.title }}">
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
<a href="{{ product.url }}">View Details</a>
</div>
{% endfor %}Best Practices
API Usage:
- Prefer GraphQL over REST for new development
- Request only needed fields to reduce costs
- Implement pagination for large datasets
- Use bulk operations for batch processing
- Respect rate limits (cost-based for GraphQL)
Security:
- Store API credentials in environment variables
- Verify webhook signatures
- Use OAuth for public apps
- Request minimal access scopes
- Implement session tokens for embedded apps
Performance:
- Cache API responses when appropriate
- Optimize images in themes
- Minimize Liquid logic complexity
- Use async loading for extensions
- Monitor query costs in GraphQL
Testing:
- Use development stores for testing
- Test across different store plans
- Verify mobile responsiveness
- Check accessibility (keyboard, screen readers)
- Validate GDPR compliance
Reference Documentation
Detailed guides for advanced topics:
- [App Development](references/app-development.md) - OAuth, APIs, webhooks, billing
- [Extensions](references/extensions.md) - Checkout, Admin, POS, Functions
- [Themes](references/themes.md) - Liquid, sections, deployment
Scripts
[shopify_init.py](scripts/shopify_init.py) - Initialize Shopify projects interactively
python scripts/shopify_init.pyTroubleshooting
Rate Limit Errors:
- Monitor
X-Shopify-Shop-Api-Call-Limitheader - Implement exponential backoff
- Use bulk operations for large datasets
Authentication Failures:
- Verify access token validity
- Check required scopes granted
- Ensure OAuth flow completed
Extension Not Appearing:
- Verify extension target correct
- Check extension published
- Ensure app installed on store
Webhook Not Receiving:
- Verify webhook URL accessible
- Check signature validation
- Review logs in Partner Dashboard
Resources
Official Documentation:
- Shopify Docs: https://shopify.dev/docs
- GraphQL API: https://shopify.dev/docs/api/admin-graphql
- Shopify CLI: https://shopify.dev/docs/api/shopify-cli
- Polaris: https://polaris.shopify.com
Tools:
- GraphiQL Explorer (Admin → Settings → Apps → Develop apps)
- Partner Dashboard (app management)
- Development stores (free testing)
API Versioning:
- Quarterly releases (YYYY-MM format)
- Current: 2025-01
- 12-month support per version
- Test before version updates
---
Note: This skill covers Shopify platform as of January 2025. Refer to official documentation for latest updates.
Shopify API Research Documentation
This directory contains comprehensive research and analysis of various APIs for integration purposes.
Contents
Shopify GraphQL Admin API Analysis
File: shopify-graphql-admin-api-analysis.md Date: 2025-10-25 Status: Complete Thoroughness: Very Thorough
A comprehensive analysis of Shopify's GraphQL Admin API covering:
- API overview and capabilities
- Key features and operations
- Common query and mutation patterns
- Best practices and optimization strategies
- Authentication and security considerations
- Rate limiting and performance optimization
- Typical use cases and implementation patterns
- Troubleshooting guide
- Code examples in multiple languages
- Resources and further learning
Key Sections: 1. Executive Summary 2. API Overview 3. Key Features 4. Common Operations (Queries & Mutations) 5. API Structure (Types, Connections, Errors) 6. Best Practices 7. Typical Use Cases 8. API Versions and Deprecation 9. Tools and SDKs 10. Security Considerations 11. Performance Optimization 12. Common Patterns 13. Troubleshooting 14. Resources and Further Learning
Size: 1,348 lines, ~26KB
---
Usage
These documents are intended for:
- Development planning and architecture decisions
- Team onboarding and training
- Integration implementation reference
- Best practices guidance
- Troubleshooting support
---
Maintenance
- Documents should be reviewed quarterly
- Update when API versions change
- Add new findings from implementation experience
- Keep code examples current with latest SDK versions
---
Last Updated: 2025-10-25 Maintained By: Claude Code Engineering Team
App Development Reference
Guide for building Shopify apps with OAuth, GraphQL/REST APIs, webhooks, and billing.
OAuth Authentication
OAuth 2.0 Flow
1. Redirect to Authorization URL:
https://{shop}.myshopify.com/admin/oauth/authorize?
client_id={api_key}&
scope={scopes}&
redirect_uri={redirect_uri}&
state={nonce}2. Handle Callback:
app.get('/auth/callback', async (req, res) => {
const { code, shop, state } = req.query;
// Verify state to prevent CSRF
if (state !== storedState) {
return res.status(403).send('Invalid state');
}
// Exchange code for access token
const accessToken = await exchangeCodeForToken(shop, code);
// Store token securely
await storeAccessToken(shop, accessToken);
res.redirect(`https://${shop}/admin/apps/${appHandle}`);
});3. Exchange Code for Token:
async function exchangeCodeForToken(shop, code) {
const response = await fetch(`https://${shop}/admin/oauth/access_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.SHOPIFY_API_KEY,
client_secret: process.env.SHOPIFY_API_SECRET,
code
})
});
const { access_token } = await response.json();
return access_token;
}Access Scopes
Common Scopes:
read_products,write_products- Product catalogread_orders,write_orders- Order managementread_customers,write_customers- Customer dataread_inventory,write_inventory- Stock levelsread_fulfillments,write_fulfillments- Order fulfillmentread_shipping,write_shipping- Shipping ratesread_analytics- Store analyticsread_checkouts,write_checkouts- Checkout data
Full list: https://shopify.dev/api/usage/access-scopes
Session Tokens (Embedded Apps)
For embedded apps using App Bridge:
import { getSessionToken } from '@shopify/app-bridge/utilities';
async function authenticatedFetch(url, options = {}) {
const app = createApp({ ... });
const token = await getSessionToken(app);
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
}GraphQL Admin API
Making Requests
async function graphqlRequest(shop, accessToken, query, variables = {}) {
const response = await fetch(
`https://${shop}/admin/api/2025-01/graphql.json`,
{
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, variables })
}
);
const data = await response.json();
if (data.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
}
return data.data;
}Product Operations
Create Product:
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
}
userErrors {
field
message
}
}
}Variables:
{
"input": {
"title": "New Product",
"productType": "Apparel",
"vendor": "Brand",
"status": "ACTIVE",
"variants": [
{ "price": "29.99", "sku": "SKU-001", "inventoryQuantity": 100 }
]
}
}Update Product:
mutation UpdateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product { id title }
userErrors { field message }
}
}Query Products:
query GetProducts($first: Int!, $query: String) {
products(first: $first, query: $query) {
edges {
node {
id
title
status
variants(first: 5) {
edges {
node { id price inventoryQuantity }
}
}
}
}
pageInfo { hasNextPage endCursor }
}
}Order Operations
Query Orders:
query GetOrders($first: Int!) {
orders(first: $first) {
edges {
node {
id
name
createdAt
displayFinancialStatus
totalPriceSet {
shopMoney { amount currencyCode }
}
customer { email firstName lastName }
}
}
}
}Fulfill Order:
mutation FulfillOrder($input: FulfillmentInput!) {
fulfillmentCreate(input: $input) {
fulfillment { id status trackingInfo { number url } }
userErrors { field message }
}
}Webhooks
Configuration
In shopify.app.toml:
[webhooks]
api_version = "2025-01"
[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "/webhooks/orders/create"
[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "/webhooks/products/update"
[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks/app/uninstalled"
# GDPR mandatory webhooks
[webhooks.privacy_compliance]
customer_data_request_url = "/webhooks/gdpr/data-request"
customer_deletion_url = "/webhooks/gdpr/customer-deletion"
shop_deletion_url = "/webhooks/gdpr/shop-deletion"Webhook Handler
import crypto from 'crypto';
function verifyWebhook(req) {
const hmac = req.headers['x-shopify-hmac-sha256'];
const body = req.rawBody; // Raw body buffer
const hash = crypto
.createHmac('sha256', process.env.SHOPIFY_API_SECRET)
.update(body, 'utf8')
.digest('base64');
return hmac === hash;
}
app.post('/webhooks/orders/create', async (req, res) => {
if (!verifyWebhook(req)) {
return res.status(401).send('Unauthorized');
}
const order = req.body;
console.log('New order:', order.id, order.name);
// Process order...
res.status(200).send('OK');
});Common Webhook Topics
Orders:
orders/create,orders/updated,orders/deleteorders/paid,orders/cancelled,orders/fulfilled
Products:
products/create,products/update,products/delete
Customers:
customers/create,customers/update,customers/delete
Inventory:
inventory_levels/update
App:
app/uninstalled(critical for cleanup)
Billing Integration
App Charges
One-time Charge:
mutation CreateCharge($input: AppPurchaseOneTimeInput!) {
appPurchaseOneTimeCreate(input: $input) {
appPurchaseOneTime {
id
name
price { amount }
status
confirmationUrl
}
userErrors { field message }
}
}Variables:
{
"input": {
"name": "Premium Feature",
"price": { "amount": 49.99, "currencyCode": "USD" },
"returnUrl": "https://your-app.com/billing/callback"
}
}Recurring Charge (Subscription):
mutation CreateSubscription($input: AppSubscriptionCreateInput!) {
appSubscriptionCreate(input: $input) {
appSubscription {
id
name
status
confirmationUrl
}
userErrors { field message }
}
}Variables:
{
"input": {
"name": "Monthly Subscription",
"returnUrl": "https://your-app.com/billing/callback",
"lineItems": [
{
"plan": {
"appRecurringPricingDetails": {
"price": { "amount": 29.99, "currencyCode": "USD" },
"interval": "EVERY_30_DAYS"
}
}
}
]
}
}Usage-based Billing:
mutation CreateUsageCharge($input: AppUsageRecordCreateInput!) {
appUsageRecordCreate(input: $input) {
appUsageRecord {
id
price { amount }
description
}
userErrors { field message }
}
}Metafields
Create Metafield
mutation CreateMetafield($input: MetafieldInput!) {
metafieldsSet(metafields: [$input]) {
metafields {
id
namespace
key
value
}
userErrors { field message }
}
}Variables:
{
"input": {
"ownerId": "gid://shopify/Product/123",
"namespace": "custom",
"key": "instructions",
"value": "Handle with care",
"type": "single_line_text_field"
}
}Metafield Types:
single_line_text_field,multi_line_text_fieldnumber_integer,number_decimaldate,date_timeurl,jsonfile_reference,product_reference
Rate Limiting
GraphQL Cost-Based Limits
Limits:
- Available points: 2000
- Restore rate: 100 points/second
- Max query cost: 2000
Check Cost:
const response = await graphqlRequest(shop, token, query);
const cost = response.extensions?.cost;
console.log(`Cost: ${cost.actualQueryCost}/${cost.throttleStatus.maximumAvailable}`);Handle Throttling:
async function graphqlWithRetry(shop, token, query, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await graphqlRequest(shop, token, query);
} catch (error) {
if (error.message.includes('Throttled') && i < retries - 1) {
await sleep(Math.pow(2, i) * 1000); // Exponential backoff
continue;
}
throw error;
}
}
}Best Practices
Security:
- Store credentials in environment variables
- Verify webhook HMAC signatures
- Validate OAuth state parameter
- Use HTTPS for all endpoints
- Implement rate limiting on your endpoints
Performance:
- Cache access tokens securely
- Use bulk operations for large datasets
- Implement pagination for queries
- Monitor GraphQL query costs
Reliability:
- Implement exponential backoff for retries
- Handle webhook delivery failures
- Log errors for debugging
- Monitor app health metrics
Compliance:
- Implement GDPR webhooks (mandatory)
- Handle customer data deletion requests
- Provide data export functionality
- Follow data retention policies
Extensions Reference
Guide for building UI extensions and Shopify Functions.
Checkout UI Extensions
Customize checkout and thank-you pages with native-rendered components.
Extension Points
Block Targets (Merchant-Configurable):
purchase.checkout.block.render- Main checkoutpurchase.thank-you.block.render- Thank you page
Static Targets (Fixed Position):
purchase.checkout.header.render-afterpurchase.checkout.contact.render-beforepurchase.checkout.shipping-option-list.render-afterpurchase.checkout.payment-method-list.render-afterpurchase.checkout.footer.render-before
Setup
shopify app generate extension --type checkout_ui_extensionConfiguration (shopify.extension.toml):
api_version = "2025-01"
name = "gift-message"
type = "ui_extension"
[[extensions.targeting]]
target = "purchase.checkout.block.render"
[capabilities]
network_access = true
api_access = trueBasic Example
import { reactExtension, BlockStack, TextField, Checkbox, useApi } from '@shopify/ui-extensions-react/checkout';
export default reactExtension('purchase.checkout.block.render', () => <Extension />);
function Extension() {
const [message, setMessage] = useState('');
const [isGift, setIsGift] = useState(false);
const { applyAttributeChange } = useApi();
useEffect(() => {
if (isGift) {
applyAttributeChange({
type: 'updateAttribute',
key: 'gift_message',
value: message
});
}
}, [message, isGift]);
return (
<BlockStack spacing="loose">
<Checkbox checked={isGift} onChange={setIsGift}>
This is a gift
</Checkbox>
{isGift && (
<TextField
label="Gift Message"
value={message}
onChange={setMessage}
multiline={3}
/>
)}
</BlockStack>
);
}Common Hooks
useApi:
const { extensionPoint, shop, storefront, i18n, sessionToken } = useApi();useCartLines:
const lines = useCartLines();
lines.forEach(line => {
console.log(line.merchandise.product.title, line.quantity);
});useShippingAddress:
const address = useShippingAddress();
console.log(address.city, address.countryCode);useApplyCartLinesChange:
const applyChange = useApplyCartLinesChange();
async function addItem() {
await applyChange({
type: 'addCartLine',
merchandiseId: 'gid://shopify/ProductVariant/123',
quantity: 1
});
}Core Components
Layout:
BlockStack- Vertical stackingInlineStack- Horizontal layoutGrid,GridItem- Grid layoutView- ContainerDivider- Separator
Input:
TextField- Text inputCheckbox- BooleanSelect- DropdownDatePicker- Date selectionForm- Form wrapper
Display:
Text,Heading- TypographyBanner- MessagesBadge- StatusImage- ImagesLink- HyperlinksList,ListItem- Lists
Interactive:
Button- ActionsModal- OverlaysPressable- Click areas
Admin UI Extensions
Extend Shopify admin interface.
Admin Action
Custom actions on resource pages.
shopify app generate extension --type admin_actionimport { reactExtension, AdminAction, Button } from '@shopify/ui-extensions-react/admin';
export default reactExtension('admin.product-details.action.render', () => <Extension />);
function Extension() {
const { data } = useData();
async function handleExport() {
const response = await fetch('/api/export', {
method: 'POST',
body: JSON.stringify({ productId: data.product.id })
});
console.log('Exported:', await response.json());
}
return (
<AdminAction
title="Export Product"
primaryAction={<Button onPress={handleExport}>Export</Button>}
/>
);
}Targets:
admin.product-details.action.renderadmin.order-details.action.renderadmin.customer-details.action.render
Admin Block
Embedded content in admin pages.
import { reactExtension, BlockStack, Text, Badge } from '@shopify/ui-extensions-react/admin';
export default reactExtension('admin.product-details.block.render', () => <Extension />);
function Extension() {
const { data } = useData();
const [analytics, setAnalytics] = useState(null);
useEffect(() => {
fetchAnalytics(data.product.id).then(setAnalytics);
}, []);
return (
<BlockStack>
<Text variant="headingMd">Product Analytics</Text>
<Text>Views: {analytics?.views || 0}</Text>
<Text>Conversions: {analytics?.conversions || 0}</Text>
<Badge tone={analytics?.trending ? "success" : "info"}>
{analytics?.trending ? "Trending" : "Normal"}
</Badge>
</BlockStack>
);
}Targets:
admin.product-details.block.renderadmin.order-details.block.renderadmin.customer-details.block.render
POS UI Extensions
Customize Point of Sale experience.
Smart Grid Tile
Quick access action on POS home screen.
import { reactExtension, SmartGridTile } from '@shopify/ui-extensions-react/pos';
export default reactExtension('pos.home.tile.render', () => <Extension />);
function Extension() {
function handlePress() {
// Navigate to custom workflow
}
return (
<SmartGridTile
title="Gift Cards"
subtitle="Manage gift cards"
onPress={handlePress}
/>
);
}POS Modal
Full-screen workflow.
import { reactExtension, Screen, BlockStack, Button, TextField } from '@shopify/ui-extensions-react/pos';
export default reactExtension('pos.home.modal.render', () => <Extension />);
function Extension() {
const { navigation } = useApi();
const [amount, setAmount] = useState('');
function handleIssue() {
// Issue gift card
navigation.pop();
}
return (
<Screen name="Gift Card" title="Issue Gift Card">
<BlockStack>
<TextField label="Amount" value={amount} onChange={setAmount} />
<TextField label="Recipient Email" />
<Button onPress={handleIssue}>Issue</Button>
</BlockStack>
</Screen>
);
}Customer Account Extensions
Customize customer account pages.
Order Status Extension
import { reactExtension, BlockStack, Text, Button } from '@shopify/ui-extensions-react/customer-account';
export default reactExtension('customer-account.order-status.block.render', () => <Extension />);
function Extension() {
const { order } = useApi();
function handleReturn() {
// Initiate return
}
return (
<BlockStack>
<Text variant="headingMd">Need to return?</Text>
<Text>Start return for order {order.name}</Text>
<Button onPress={handleReturn}>Start Return</Button>
</BlockStack>
);
}Targets:
customer-account.order-status.block.rendercustomer-account.order-index.block.rendercustomer-account.profile.block.render
Shopify Functions
Serverless backend customization.
Function Types
Discounts:
order_discount- Order-level discountsproduct_discount- Product-specific discountsshipping_discount- Shipping discounts
Payment Customization:
- Hide/rename/reorder payment methods
Delivery Customization:
- Custom shipping options
- Delivery rules
Validation:
- Cart validation rules
- Checkout validation
Create Function
shopify app generate extension --type functionOrder Discount Function
// input.graphql
query Input {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
product {
hasTag(tag: "bulk-discount")
}
}
}
}
}
}
// function.js
export default function orderDiscount(input) {
const targets = input.cart.lines
.filter(line => line.merchandise.product.hasTag)
.map(line => ({
productVariant: { id: line.merchandise.id }
}));
if (targets.length === 0) {
return { discounts: [] };
}
return {
discounts: [{
targets,
value: {
percentage: {
value: 10 // 10% discount
}
}
}]
};
}Payment Customization Function
export default function paymentCustomization(input) {
const hidePaymentMethods = input.cart.lines.some(
line => line.merchandise.product.hasTag
);
if (!hidePaymentMethods) {
return { operations: [] };
}
return {
operations: [{
hide: {
paymentMethodId: "gid://shopify/PaymentMethod/123"
}
}]
};
}Validation Function
export default function cartValidation(input) {
const errors = [];
// Max 5 items per cart
if (input.cart.lines.length > 5) {
errors.push({
localizedMessage: "Maximum 5 items allowed per order",
target: "cart"
});
}
// Min $50 for wholesale
const isWholesale = input.cart.lines.some(
line => line.merchandise.product.hasTag
);
if (isWholesale && input.cart.cost.totalAmount.amount < 50) {
errors.push({
localizedMessage: "Wholesale orders require $50 minimum",
target: "cart"
});
}
return { errors };
}Network Requests
Extensions can call external APIs.
import { useApi } from '@shopify/ui-extensions-react/checkout';
function Extension() {
const { sessionToken } = useApi();
async function fetchData() {
const token = await sessionToken.get();
const response = await fetch('https://your-app.com/api/data', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return await response.json();
}
}Best Practices
Performance:
- Lazy load data
- Memoize expensive computations
- Use loading states
- Minimize re-renders
UX:
- Provide clear error messages
- Show loading indicators
- Validate inputs
- Support keyboard navigation
Security:
- Verify session tokens on backend
- Sanitize user input
- Use HTTPS for all requests
- Don't expose sensitive data
Testing:
- Test on development stores
- Verify mobile/desktop
- Check accessibility
- Test edge cases
Resources
- Checkout Extensions: https://shopify.dev/docs/api/checkout-extensions
- Admin Extensions: https://shopify.dev/docs/apps/admin/extensions
- Functions: https://shopify.dev/docs/apps/functions
- Components: https://shopify.dev/docs/api/checkout-ui-extensions/components
Themes Reference
Guide for developing Shopify themes with Liquid templating.
Liquid Templating
Syntax Basics
Objects (Output):
{{ product.title }}
{{ product.price | money }}
{{ customer.email }}Tags (Logic):
{% if product.available %}
<button>Add to Cart</button>
{% else %}
<p>Sold Out</p>
{% endif %}
{% for product in collection.products %}
{{ product.title }}
{% endfor %}
{% case product.type %}
{% when 'Clothing' %}
<span>Apparel</span>
{% when 'Shoes' %}
<span>Footwear</span>
{% else %}
<span>Other</span>
{% endcase %}Filters (Transform):
{{ product.title | upcase }}
{{ product.price | money }}
{{ product.description | strip_html | truncate: 100 }}
{{ product.image | img_url: 'medium' }}
{{ 'now' | date: '%B %d, %Y' }}Common Objects
Product:
{{ product.id }}
{{ product.title }}
{{ product.handle }}
{{ product.description }}
{{ product.price }}
{{ product.compare_at_price }}
{{ product.available }}
{{ product.type }}
{{ product.vendor }}
{{ product.tags }}
{{ product.images }}
{{ product.variants }}
{{ product.featured_image }}
{{ product.url }}Collection:
{{ collection.title }}
{{ collection.handle }}
{{ collection.description }}
{{ collection.products }}
{{ collection.products_count }}
{{ collection.image }}
{{ collection.url }}Cart:
{{ cart.item_count }}
{{ cart.total_price }}
{{ cart.items }}
{{ cart.note }}
{{ cart.attributes }}Customer:
{{ customer.email }}
{{ customer.first_name }}
{{ customer.last_name }}
{{ customer.orders_count }}
{{ customer.total_spent }}
{{ customer.addresses }}
{{ customer.default_address }}Shop:
{{ shop.name }}
{{ shop.email }}
{{ shop.domain }}
{{ shop.currency }}
{{ shop.money_format }}
{{ shop.enabled_payment_types }}Common Filters
String:
upcase,downcase,capitalizestrip_html,strip_newlinestruncate: 100,truncatewords: 20replace: 'old', 'new'
Number:
money- Format currencyround,ceil,floortimes,divided_by,plus,minus
Array:
join: ', 'first,lastsizemap: 'property'where: 'property', 'value'
URL:
img_url: 'size'- Image URLurl_for_type,url_for_vendorlink_to,link_to_type
Date:
date: '%B %d, %Y'
Theme Architecture
Directory Structure
theme/
├── assets/ # CSS, JS, images
├── config/ # Theme settings
│ ├── settings_schema.json
│ └── settings_data.json
├── layout/ # Base templates
│ └── theme.liquid
├── locales/ # Translations
│ └── en.default.json
├── sections/ # Reusable blocks
│ ├── header.liquid
│ ├── footer.liquid
│ └── product-grid.liquid
├── snippets/ # Small components
│ ├── product-card.liquid
│ └── icon.liquid
└── templates/ # Page templates
├── index.json
├── product.json
├── collection.json
└── cart.liquidLayout
Base template wrapping all pages (layout/theme.liquid):
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{ page_title }}</title>
{{ content_for_header }}
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
</head>
<body>
{% section 'header' %}
<main>
{{ content_for_layout }}
</main>
{% section 'footer' %}
<script src="{{ 'theme.js' | asset_url }}"></script>
</body>
</html>Templates
Page-specific structures (templates/product.json):
{
"sections": {
"main": {
"type": "product-template",
"settings": {
"show_vendor": true,
"show_quantity_selector": true
}
},
"recommendations": {
"type": "product-recommendations"
}
},
"order": ["main", "recommendations"]
}Legacy format (templates/product.liquid):
<div class="product">
<div class="product-images">
<img src="{{ product.featured_image | img_url: 'large' }}" alt="{{ product.title }}">
</div>
<div class="product-details">
<h1>{{ product.title }}</h1>
<p class="price">{{ product.price | money }}</p>
{% form 'product', product %}
<select name="id">
{% for variant in product.variants %}
<option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option>
{% endfor %}
</select>
<button type="submit">Add to Cart</button>
{% endform %}
</div>
</div>Sections
Reusable content blocks (sections/product-grid.liquid):
<div class="product-grid">
{% for product in section.settings.collection.products %}
<div class="product-card">
<a href="{{ product.url }}">
<img src="{{ product.featured_image | img_url: 'medium' }}" alt="{{ product.title }}">
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</a>
</div>
{% endfor %}
</div>
{% schema %}
{
"name": "Product Grid",
"settings": [
{
"type": "collection",
"id": "collection",
"label": "Collection"
},
{
"type": "range",
"id": "products_per_row",
"min": 2,
"max": 5,
"step": 1,
"default": 4,
"label": "Products per row"
}
],
"presets": [
{
"name": "Product Grid"
}
]
}
{% endschema %}Snippets
Small reusable components (snippets/product-card.liquid):
<div class="product-card">
<a href="{{ product.url }}">
{% if product.featured_image %}
<img src="{{ product.featured_image | img_url: 'medium' }}" alt="{{ product.title }}">
{% endif %}
<h3>{{ product.title }}</h3>
<p class="price">{{ product.price | money }}</p>
{% if product.compare_at_price > product.price %}
<p class="sale-price">{{ product.compare_at_price | money }}</p>
{% endif %}
</a>
</div>Include snippet:
{% render 'product-card', product: product %}Development Workflow
Setup
# Initialize new theme
shopify theme init
# Choose Dawn (reference theme) or blankLocal Development
# Start local server
shopify theme dev
# Preview at http://localhost:9292
# Changes auto-sync to development themePull Theme
# Pull live theme
shopify theme pull --live
# Pull specific theme
shopify theme pull --theme=123456789
# Pull only templates
shopify theme pull --only=templatesPush Theme
# Push to development theme
shopify theme push --development
# Create new unpublished theme
shopify theme push --unpublished
# Push specific files
shopify theme push --only=sections,snippetsTheme Check
Lint theme code:
shopify theme check
shopify theme check --auto-correctCommon Patterns
Product Form with Variants
{% form 'product', product %}
{% unless product.has_only_default_variant %}
{% for option in product.options_with_values %}
<div class="product-option">
<label>{{ option.name }}</label>
<select name="options[{{ option.name }}]">
{% for value in option.values %}
<option value="{{ value }}">{{ value }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
{% endunless %}
<input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
<input type="number" name="quantity" value="1" min="1">
<button type="submit" {% unless product.available %}disabled{% endunless %}>
{% if product.available %}Add to Cart{% else %}Sold Out{% endif %}
</button>
{% endform %}Pagination
{% paginate collection.products by 12 %}
{% for product in collection.products %}
{% render 'product-card', product: product %}
{% endfor %}
{% if paginate.pages > 1 %}
<div class="pagination">
{% if paginate.previous %}
<a href="{{ paginate.previous.url }}">Previous</a>
{% endif %}
{% for part in paginate.parts %}
{% if part.is_link %}
<a href="{{ part.url }}">{{ part.title }}</a>
{% else %}
<span class="current">{{ part.title }}</span>
{% endif %}
{% endfor %}
{% if paginate.next %}
<a href="{{ paginate.next.url }}">Next</a>
{% endif %}
</div>
{% endif %}
{% endpaginate %}Cart AJAX
// Add to cart
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: variantId,
quantity: 1
})
})
.then(res => res.json())
.then(item => console.log('Added:', item));
// Get cart
fetch('/cart.js')
.then(res => res.json())
.then(cart => console.log('Cart:', cart));
// Update cart
fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: lineItemKey,
quantity: 2
})
})
.then(res => res.json());Metafields in Themes
Access custom data:
{{ product.metafields.custom.care_instructions }}
{{ product.metafields.custom.material.value }}
{% if product.metafields.custom.featured %}
<span class="badge">Featured</span>
{% endif %}Best Practices
Performance:
- Optimize images (use appropriate sizes)
- Minimize Liquid logic complexity
- Use lazy loading for images
- Defer non-critical JavaScript
Accessibility:
- Use semantic HTML
- Include alt text for images
- Support keyboard navigation
- Ensure sufficient color contrast
SEO:
- Use descriptive page titles
- Include meta descriptions
- Structure content with headings
- Implement schema markup
Code Quality:
- Follow Shopify theme guidelines
- Use consistent naming conventions
- Comment complex logic
- Keep sections focused and reusable
Resources
- Theme Development: https://shopify.dev/docs/themes
- Liquid Reference: https://shopify.dev/docs/api/liquid
- Dawn Theme: https://github.com/Shopify/dawn
- Theme Check: https://shopify.dev/docs/themes/tools/theme-check
SQLite format 3@
.�
V�^
O-�
&�m ��a c��Q�}tabletracertracer
CREATE TABLE tracer (
-- A row per file indicating the tracer used for that file.
file_id integer primary key,
tracer text,
foreign key (file_id) references file (id)
)�
�etablearcarcCREATE TABLE arc (
-- If recording branches, a row per context per from/to line transition executed.
file_id integer, -- foreign key to `file`.
context_id integer, -- foreign key to `context`.
fromno integer, -- line number jumped from.
tono integer, -- line number jumped to.
foreign key (file_id) references file (id),
foreign key (context_id) references context (id),
unique (file_id, context_id, fromno, tono)
)%9indexsqlite_autoindex_arc_1arc��qtableline_bitsline_bits CREATE TABLE line_bits (
-- If recording lines, a row per context per file executed.
-- All of the line numbers for that file/context are in one numbits.
file_id integer, -- foreign key to `file`.
context_id integer, -- foreign key to `context`.
numbits blob, -- see the numbits functions in coverage.numbits
foreign key (file_id) references file (id),
foreign key (context_id) references context (id),
unique (file_id, context_id)
)1 Eindexsqlite_autoindex_line_bits_1line_bits
�� tablecontextcontextCREATE TABLE context (
-- A row per context measured.
id integer primary key,
context text,
unique (context)
)-Aindexsqlite_autoindex_context_1context��qtablefilefileCREATE TABLE file (
-- A row per file measured.
id integer primary key,
path text,
unique (path)
)';indexsqlite_autoindex_file_1file�[�tablemetametaCREATE TABLE meta (
-- Key-value pairs, to record metadata about the data
key text,
value text,
unique (key)
-- Possible keys:
-- 'has_arcs' boolean -- Is this data recording branches?
-- 'sys_argv' text -- The coverage command line that recorded the data.
-- 'version' text -- The version of coverage.py that made the file.
-- 'when' text -- Datetime when the file was created.
)';indexsqlite_autoindex_meta_1meta�++�utablecoverage_schemacoverage_schemaCREATE TABLE coverage_schema (
-- One row, to record the version of the schema in this db.
version integer
)
��
���has_arcs0version7.11.0
���has_arcs
version
=�=Z�9/mnt/d/www/claudekit/claudekit-engineer/.claude/skills/shopify/scripts/shopify_init.pye�O/mnt/d/www/claudekit/claudekit-engineer/.claude/skills/shopify/scripts/tests/test_shopify_init.py
>>�[�9/mnt/d/www/claudekit/claudekit-engineer/.claude/skills/shopify/scripts/shopify_init.pye�O /mnt/d/www/claudekit/claudekit-engineer/.claude/skills/shopify/scripts/tests/test_shopify_init.py
��
��
���5 n�?e
�5�֠��z]�T=�gu]���j�vu���9w)�������պZV��: v����@ι@��J(/�/� .�� �\m�ϾZk���35�O
���
# Shopify Skill Dependencies
# Python 3.10+ required
# No Python package dependencies - uses only standard library
# Testing dependencies (dev)
pytest>=8.0.0
pytest-cov>=4.1.0
pytest-mock>=3.12.0
# Note: This script requires the Shopify CLI tool
# Install Shopify CLI:
# npm install -g @shopify/cli @shopify/theme
# or via Homebrew (macOS):
# brew tap shopify/shopify
# brew install shopify-cli
#
# Authenticate with:
# shopify auth login
#!/usr/bin/env python3
"""
Shopify Project Initialization Script
Interactive script to scaffold Shopify apps, extensions, or themes.
Supports environment variable loading from multiple locations.
"""
import os
import sys
import json
import subprocess
from pathlib import Path
from typing import Dict, Optional, List
from dataclasses import dataclass
@dataclass
class EnvConfig:
"""Environment configuration container."""
shopify_api_key: Optional[str] = None
shopify_api_secret: Optional[str] = None
shop_domain: Optional[str] = None
scopes: Optional[str] = None
class EnvLoader:
"""Load environment variables from multiple sources in priority order."""
@staticmethod
def load_env_file(filepath: Path) -> Dict[str, str]:
"""
Load environment variables from .env file.
Args:
filepath: Path to .env file
Returns:
Dictionary of environment variables
"""
env_vars = {}
if not filepath.exists():
return env_vars
try:
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
env_vars[key.strip()] = value.strip().strip('"').strip("'")
except Exception as e:
print(f"Warning: Failed to load {filepath}: {e}")
return env_vars
@staticmethod
def get_env_paths(skill_dir: Path) -> List[Path]:
"""
Get list of .env file paths in priority order.
Priority: process.env > skill/.env > skills/.env > .claude/.env
Args:
skill_dir: Path to skill directory
Returns:
List of .env file paths
"""
paths = []
# skill/.env
skill_env = skill_dir / '.env'
if skill_env.exists():
paths.append(skill_env)
# skills/.env
skills_env = skill_dir.parent / '.env'
if skills_env.exists():
paths.append(skills_env)
# .claude/.env
claude_env = skill_dir.parent.parent / '.env'
if claude_env.exists():
paths.append(claude_env)
return paths
@staticmethod
def load_config(skill_dir: Path) -> EnvConfig:
"""
Load configuration from environment variables.
Priority: process.env > skill/.env > skills/.env > .claude/.env
Args:
skill_dir: Path to skill directory
Returns:
EnvConfig object
"""
config = EnvConfig()
# Load from .env files (reverse priority order)
for env_path in reversed(EnvLoader.get_env_paths(skill_dir)):
env_vars = EnvLoader.load_env_file(env_path)
if 'SHOPIFY_API_KEY' in env_vars:
config.shopify_api_key = env_vars['SHOPIFY_API_KEY']
if 'SHOPIFY_API_SECRET' in env_vars:
config.shopify_api_secret = env_vars['SHOPIFY_API_SECRET']
if 'SHOP_DOMAIN' in env_vars:
config.shop_domain = env_vars['SHOP_DOMAIN']
if 'SCOPES' in env_vars:
config.scopes = env_vars['SCOPES']
# Override with process environment (highest priority)
if 'SHOPIFY_API_KEY' in os.environ:
config.shopify_api_key = os.environ['SHOPIFY_API_KEY']
if 'SHOPIFY_API_SECRET' in os.environ:
config.shopify_api_secret = os.environ['SHOPIFY_API_SECRET']
if 'SHOP_DOMAIN' in os.environ:
config.shop_domain = os.environ['SHOP_DOMAIN']
if 'SCOPES' in os.environ:
config.scopes = os.environ['SCOPES']
return config
class ShopifyInitializer:
"""Initialize Shopify projects."""
def __init__(self, config: EnvConfig):
"""
Initialize ShopifyInitializer.
Args:
config: Environment configuration
"""
self.config = config
def prompt(self, message: str, default: Optional[str] = None) -> str:
"""
Prompt user for input.
Args:
message: Prompt message
default: Default value
Returns:
User input or default
"""
if default:
message = f"{message} [{default}]"
user_input = input(f"{message}: ").strip()
return user_input if user_input else (default or '')
def select_option(self, message: str, options: List[str]) -> str:
"""
Prompt user to select from options.
Args:
message: Prompt message
options: List of options
Returns:
Selected option
"""
print(f"\n{message}")
for i, option in enumerate(options, 1):
print(f"{i}. {option}")
while True:
try:
choice = int(input("Select option: ").strip())
if 1 <= choice <= len(options):
return options[choice - 1]
print(f"Please select 1-{len(options)}")
except (ValueError, KeyboardInterrupt):
print("Invalid input")
def check_cli_installed(self) -> bool:
"""
Check if Shopify CLI is installed.
Returns:
True if installed, False otherwise
"""
try:
result = subprocess.run(
['shopify', 'version'],
capture_output=True,
text=True,
timeout=5
)
return result.returncode == 0
except (subprocess.SubprocessError, FileNotFoundError):
return False
def create_app_config(self, project_dir: Path, app_name: str, scopes: str) -> None:
"""
Create shopify.app.toml configuration file.
Args:
project_dir: Project directory
app_name: Application name
scopes: Access scopes
"""
config_content = f"""# Shopify App Configuration
name = "{app_name}"
client_id = "{self.config.shopify_api_key or 'YOUR_API_KEY'}"
application_url = "https://your-app.com"
embedded = true
[build]
automatically_update_urls_on_dev = true
dev_store_url = "{self.config.shop_domain or 'your-store.myshopify.com'}"
[access_scopes]
scopes = "{scopes}"
[webhooks]
api_version = "2025-01"
[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks/app/uninstalled"
[webhooks.privacy_compliance]
customer_data_request_url = "/webhooks/gdpr/data-request"
customer_deletion_url = "/webhooks/gdpr/customer-deletion"
shop_deletion_url = "/webhooks/gdpr/shop-deletion"
"""
config_path = project_dir / 'shopify.app.toml'
config_path.write_text(config_content)
print(f"✓ Created {config_path}")
def create_extension_config(self, project_dir: Path, extension_name: str, extension_type: str) -> None:
"""
Create shopify.extension.toml configuration file.
Args:
project_dir: Project directory
extension_name: Extension name
extension_type: Extension type
"""
target_map = {
'checkout': 'purchase.checkout.block.render',
'admin_action': 'admin.product-details.action.render',
'admin_block': 'admin.product-details.block.render',
'pos': 'pos.home.tile.render'
}
config_content = f"""name = "{extension_name}"
type = "ui_extension"
handle = "{extension_name.lower().replace(' ', '-')}"
[extension_points]
api_version = "2025-01"
[[extension_points.targets]]
target = "{target_map.get(extension_type, 'purchase.checkout.block.render')}"
[capabilities]
network_access = true
api_access = true
"""
config_path = project_dir / 'shopify.extension.toml'
config_path.write_text(config_content)
print(f"✓ Created {config_path}")
def create_readme(self, project_dir: Path, project_type: str, project_name: str) -> None:
"""
Create README.md file.
Args:
project_dir: Project directory
project_type: Project type (app/extension/theme)
project_name: Project name
"""
content = f"""# {project_name}
Shopify {project_type.capitalize()} project.
## Setup
```bash
# Install dependencies
npm install
# Start development
shopify {project_type} dev
```
## Deployment
```bash
# Deploy to Shopify
shopify {project_type} deploy
```
## Resources
- [Shopify Documentation](https://shopify.dev/docs)
- [Shopify CLI](https://shopify.dev/docs/api/shopify-cli)
"""
readme_path = project_dir / 'README.md'
readme_path.write_text(content)
print(f"✓ Created {readme_path}")
def init_app(self) -> None:
"""Initialize Shopify app project."""
print("\n=== Shopify App Initialization ===\n")
app_name = self.prompt("App name", "my-shopify-app")
scopes = self.prompt("Access scopes", self.config.scopes or "read_products,write_products")
project_dir = Path.cwd() / app_name
project_dir.mkdir(exist_ok=True)
print(f"\nCreating app in {project_dir}...")
self.create_app_config(project_dir, app_name, scopes)
self.create_readme(project_dir, "app", app_name)
# Create basic package.json
package_json = {
"name": app_name.lower().replace(' ', '-'),
"version": "1.0.0",
"scripts": {
"dev": "shopify app dev",
"deploy": "shopify app deploy"
}
}
(project_dir / 'package.json').write_text(json.dumps(package_json, indent=2))
print(f"✓ Created package.json")
print(f"\n✓ App '{app_name}' initialized successfully!")
print(f"\nNext steps:")
print(f" cd {app_name}")
print(f" npm install")
print(f" shopify app dev")
def init_extension(self) -> None:
"""Initialize Shopify extension project."""
print("\n=== Shopify Extension Initialization ===\n")
extension_types = ['checkout', 'admin_action', 'admin_block', 'pos']
extension_type = self.select_option("Select extension type", extension_types)
extension_name = self.prompt("Extension name", "my-extension")
project_dir = Path.cwd() / extension_name
project_dir.mkdir(exist_ok=True)
print(f"\nCreating extension in {project_dir}...")
self.create_extension_config(project_dir, extension_name, extension_type)
self.create_readme(project_dir, "extension", extension_name)
print(f"\n✓ Extension '{extension_name}' initialized successfully!")
print(f"\nNext steps:")
print(f" cd {extension_name}")
print(f" shopify app dev")
def init_theme(self) -> None:
"""Initialize Shopify theme project."""
print("\n=== Shopify Theme Initialization ===\n")
theme_name = self.prompt("Theme name", "my-theme")
print(f"\nInitializing theme '{theme_name}'...")
print("\nRecommended: Use 'shopify theme init' for full theme scaffolding")
print(f"\nRun: shopify theme init {theme_name}")
def run(self) -> None:
"""Run interactive initialization."""
print("=" * 60)
print("Shopify Project Initializer")
print("=" * 60)
# Check CLI
if not self.check_cli_installed():
print("\n⚠ Shopify CLI not found!")
print("Install: npm install -g @shopify/cli@latest")
sys.exit(1)
# Select project type
project_types = ['app', 'extension', 'theme']
project_type = self.select_option("Select project type", project_types)
# Initialize based on type
if project_type == 'app':
self.init_app()
elif project_type == 'extension':
self.init_extension()
elif project_type == 'theme':
self.init_theme()
def main() -> None:
"""Main entry point."""
try:
# Get skill directory
script_dir = Path(__file__).parent
skill_dir = script_dir.parent
# Load configuration
config = EnvLoader.load_config(skill_dir)
# Initialize project
initializer = ShopifyInitializer(config)
initializer.run()
except KeyboardInterrupt:
print("\n\nAborted.")
sys.exit(0)
except Exception as e:
print(f"\n✗ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
SQLite format 3@
.�
V�^
O-�
&�m ��a c��Q�}tabletracertracer
CREATE TABLE tracer (
-- A row per file indicating the tracer used for that file.
file_id integer primary key,
tracer text,
foreign key (file_id) references file (id)
)�
�etablearcarcCREATE TABLE arc (
-- If recording branches, a row per context per from/to line transition executed.
file_id integer, -- foreign key to `file`.
context_id integer, -- foreign key to `context`.
fromno integer, -- line number jumped from.
tono integer, -- line number jumped to.
foreign key (file_id) references file (id),
foreign key (context_id) references context (id),
unique (file_id, context_id, fromno, tono)
)%9indexsqlite_autoindex_arc_1arc��qtableline_bitsline_bits CREATE TABLE line_bits (
-- If recording lines, a row per context per file executed.
-- All of the line numbers for that file/context are in one numbits.
file_id integer, -- foreign key to `file`.
context_id integer, -- foreign key to `context`.
numbits blob, -- see the numbits functions in coverage.numbits
foreign key (file_id) references file (id),
foreign key (context_id) references context (id),
unique (file_id, context_id)
)1 Eindexsqlite_autoindex_line_bits_1line_bits
�� tablecontextcontextCREATE TABLE context (
-- A row per context measured.
id integer primary key,
context text,
unique (context)
)-Aindexsqlite_autoindex_context_1context��qtablefilefileCREATE TABLE file (
-- A row per file measured.
id integer primary key,
path text,
unique (path)
)';indexsqlite_autoindex_file_1file�[�tablemetametaCREATE TABLE meta (
-- Key-value pairs, to record metadata about the data
key text,
value text,
unique (key)
-- Possible keys:
-- 'has_arcs' boolean -- Is this data recording branches?
-- 'sys_argv' text -- The coverage command line that recorded the data.
-- 'version' text -- The version of coverage.py that made the file.
-- 'when' text -- Datetime when the file was created.
)';indexsqlite_autoindex_meta_1meta�++�utablecoverage_schemacoverage_schemaCREATE TABLE coverage_schema (
-- One row, to record the version of the schema in this db.
version integer
)
��
��version7.11.0
��
version
"""
Tests for shopify_init.py
Run with: pytest test_shopify_init.py -v --cov=shopify_init --cov-report=term-missing
"""
import os
import sys
import json
import pytest
import subprocess
from pathlib import Path
from unittest.mock import Mock, patch, mock_open, MagicMock
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from shopify_init import EnvLoader, EnvConfig, ShopifyInitializer
class TestEnvLoader:
"""Test EnvLoader class."""
def test_load_env_file_success(self, tmp_path):
"""Test loading valid .env file."""
env_file = tmp_path / ".env"
env_file.write_text("""
SHOPIFY_API_KEY=test_key
SHOPIFY_API_SECRET=test_secret
SHOP_DOMAIN=test.myshopify.com
# Comment line
SCOPES=read_products,write_products
""")
result = EnvLoader.load_env_file(env_file)
assert result['SHOPIFY_API_KEY'] == 'test_key'
assert result['SHOPIFY_API_SECRET'] == 'test_secret'
assert result['SHOP_DOMAIN'] == 'test.myshopify.com'
assert result['SCOPES'] == 'read_products,write_products'
def test_load_env_file_with_quotes(self, tmp_path):
"""Test loading .env file with quoted values."""
env_file = tmp_path / ".env"
env_file.write_text("""
SHOPIFY_API_KEY="test_key"
SHOPIFY_API_SECRET='test_secret'
""")
result = EnvLoader.load_env_file(env_file)
assert result['SHOPIFY_API_KEY'] == 'test_key'
assert result['SHOPIFY_API_SECRET'] == 'test_secret'
def test_load_env_file_nonexistent(self, tmp_path):
"""Test loading non-existent .env file."""
result = EnvLoader.load_env_file(tmp_path / "nonexistent.env")
assert result == {}
def test_load_env_file_invalid_format(self, tmp_path):
"""Test loading .env file with invalid lines."""
env_file = tmp_path / ".env"
env_file.write_text("""
VALID_KEY=value
INVALID_LINE_NO_EQUALS
ANOTHER_VALID=test
""")
result = EnvLoader.load_env_file(env_file)
assert result['VALID_KEY'] == 'value'
assert result['ANOTHER_VALID'] == 'test'
assert 'INVALID_LINE_NO_EQUALS' not in result
def test_get_env_paths(self, tmp_path):
"""Test getting .env file paths."""
# Create directory structure
claude_dir = tmp_path / ".claude"
skills_dir = claude_dir / "skills"
skill_dir = skills_dir / "shopify"
skill_dir.mkdir(parents=True)
# Create .env files
(skill_dir / ".env").write_text("SKILL=1")
(skills_dir / ".env").write_text("SKILLS=1")
(claude_dir / ".env").write_text("CLAUDE=1")
paths = EnvLoader.get_env_paths(skill_dir)
assert len(paths) == 3
assert skill_dir / ".env" in paths
assert skills_dir / ".env" in paths
assert claude_dir / ".env" in paths
def test_load_config_priority(self, tmp_path, monkeypatch):
"""Test configuration loading priority."""
skill_dir = tmp_path / "skill"
skills_dir = tmp_path
claude_dir = tmp_path.parent
skill_dir.mkdir(parents=True)
# Create .env files with different values
(skill_dir / ".env").write_text("SHOPIFY_API_KEY=skill_key")
(skills_dir / ".env").write_text("SHOPIFY_API_KEY=skills_key\nSHOP_DOMAIN=skills.myshopify.com")
# Override with process env
monkeypatch.setenv("SHOPIFY_API_KEY", "process_key")
config = EnvLoader.load_config(skill_dir)
# Process env should win
assert config.shopify_api_key == "process_key"
# Shop domain from skills/.env
assert config.shop_domain == "skills.myshopify.com"
def test_load_config_no_files(self, tmp_path):
"""Test configuration loading with no .env files."""
config = EnvLoader.load_config(tmp_path)
assert config.shopify_api_key is None
assert config.shopify_api_secret is None
assert config.shop_domain is None
assert config.scopes is None
class TestShopifyInitializer:
"""Test ShopifyInitializer class."""
@pytest.fixture
def config(self):
"""Create test config."""
return EnvConfig(
shopify_api_key="test_key",
shopify_api_secret="test_secret",
shop_domain="test.myshopify.com",
scopes="read_products,write_products"
)
@pytest.fixture
def initializer(self, config):
"""Create initializer instance."""
return ShopifyInitializer(config)
def test_prompt_with_default(self, initializer):
"""Test prompt with default value."""
with patch('builtins.input', return_value=''):
result = initializer.prompt("Test", "default_value")
assert result == "default_value"
def test_prompt_with_input(self, initializer):
"""Test prompt with user input."""
with patch('builtins.input', return_value='user_input'):
result = initializer.prompt("Test", "default_value")
assert result == "user_input"
def test_select_option_valid(self, initializer):
"""Test select option with valid choice."""
options = ['app', 'extension', 'theme']
with patch('builtins.input', return_value='2'):
result = initializer.select_option("Choose", options)
assert result == 'extension'
def test_select_option_invalid_then_valid(self, initializer):
"""Test select option with invalid then valid choice."""
options = ['app', 'extension']
with patch('builtins.input', side_effect=['5', 'invalid', '1']):
result = initializer.select_option("Choose", options)
assert result == 'app'
def test_check_cli_installed_success(self, initializer):
"""Test CLI installed check - success."""
mock_result = Mock()
mock_result.returncode = 0
with patch('subprocess.run', return_value=mock_result):
assert initializer.check_cli_installed() is True
def test_check_cli_installed_failure(self, initializer):
"""Test CLI installed check - failure."""
with patch('subprocess.run', side_effect=FileNotFoundError):
assert initializer.check_cli_installed() is False
def test_create_app_config(self, initializer, tmp_path):
"""Test creating app configuration file."""
initializer.create_app_config(tmp_path, "test-app", "read_products")
config_file = tmp_path / "shopify.app.toml"
assert config_file.exists()
content = config_file.read_text()
assert 'name = "test-app"' in content
assert 'scopes = "read_products"' in content
assert 'client_id = "test_key"' in content
def test_create_extension_config(self, initializer, tmp_path):
"""Test creating extension configuration file."""
initializer.create_extension_config(tmp_path, "test-ext", "checkout")
config_file = tmp_path / "shopify.extension.toml"
assert config_file.exists()
content = config_file.read_text()
assert 'name = "test-ext"' in content
assert 'purchase.checkout.block.render' in content
def test_create_extension_config_admin_action(self, initializer, tmp_path):
"""Test creating admin action extension config."""
initializer.create_extension_config(tmp_path, "admin-ext", "admin_action")
config_file = tmp_path / "shopify.extension.toml"
content = config_file.read_text()
assert 'admin.product-details.action.render' in content
def test_create_readme(self, initializer, tmp_path):
"""Test creating README file."""
initializer.create_readme(tmp_path, "app", "Test App")
readme_file = tmp_path / "README.md"
assert readme_file.exists()
content = readme_file.read_text()
assert '# Test App' in content
assert 'shopify app dev' in content
@patch('builtins.input')
@patch('builtins.print')
def test_init_app(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
"""Test app initialization."""
monkeypatch.chdir(tmp_path)
# Mock user inputs
mock_input.side_effect = ['my-app', 'read_products,write_products']
initializer.init_app()
# Check directory created
app_dir = tmp_path / "my-app"
assert app_dir.exists()
# Check files created
assert (app_dir / "shopify.app.toml").exists()
assert (app_dir / "README.md").exists()
assert (app_dir / "package.json").exists()
# Check package.json content
package_json = json.loads((app_dir / "package.json").read_text())
assert package_json['name'] == 'my-app'
assert 'dev' in package_json['scripts']
@patch('builtins.input')
@patch('builtins.print')
def test_init_extension(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
"""Test extension initialization."""
monkeypatch.chdir(tmp_path)
# Mock user inputs: type selection (1 = checkout), name
mock_input.side_effect = ['1', 'my-extension']
initializer.init_extension()
# Check directory and files created
ext_dir = tmp_path / "my-extension"
assert ext_dir.exists()
assert (ext_dir / "shopify.extension.toml").exists()
assert (ext_dir / "README.md").exists()
@patch('builtins.input')
@patch('builtins.print')
def test_init_theme(self, mock_print, mock_input, initializer):
"""Test theme initialization."""
mock_input.return_value = 'my-theme'
# Should just print instructions
initializer.init_theme()
# Verify print was called (instructions shown)
assert mock_print.called
@patch('builtins.print')
def test_run_no_cli(self, mock_print, initializer):
"""Test run when CLI not installed."""
with patch.object(initializer, 'check_cli_installed', return_value=False):
with pytest.raises(SystemExit) as exc_info:
initializer.run()
assert exc_info.value.code == 1
@patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
@patch.object(ShopifyInitializer, 'init_app')
@patch('builtins.input')
@patch('builtins.print')
def test_run_app_selected(self, mock_print, mock_input, mock_init_app, mock_cli_check, initializer):
"""Test run with app selection."""
mock_input.return_value = '1' # Select app
initializer.run()
mock_init_app.assert_called_once()
@patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
@patch.object(ShopifyInitializer, 'init_extension')
@patch('builtins.input')
@patch('builtins.print')
def test_run_extension_selected(self, mock_print, mock_input, mock_init_ext, mock_cli_check, initializer):
"""Test run with extension selection."""
mock_input.return_value = '2' # Select extension
initializer.run()
mock_init_ext.assert_called_once()
class TestMain:
"""Test main function."""
@patch('shopify_init.ShopifyInitializer')
@patch('shopify_init.EnvLoader')
def test_main_success(self, mock_loader, mock_initializer):
"""Test main function success path."""
from shopify_init import main
mock_config = Mock()
mock_loader.load_config.return_value = mock_config
mock_init_instance = Mock()
mock_initializer.return_value = mock_init_instance
with patch('builtins.print'):
main()
mock_init_instance.run.assert_called_once()
@patch('shopify_init.ShopifyInitializer')
@patch('sys.exit')
def test_main_keyboard_interrupt(self, mock_exit, mock_initializer):
"""Test main function with keyboard interrupt."""
from shopify_init import main
mock_initializer.return_value.run.side_effect = KeyboardInterrupt
with patch('builtins.print'):
main()
mock_exit.assert_called_with(0)
@patch('shopify_init.ShopifyInitializer')
@patch('sys.exit')
def test_main_exception(self, mock_exit, mock_initializer):
"""Test main function with exception."""
from shopify_init import main
mock_initializer.return_value.run.side_effect = Exception("Test error")
with patch('builtins.print'):
main()
mock_exit.assert_called_with(1)
class TestEnvConfig:
"""Test EnvConfig dataclass."""
def test_env_config_defaults(self):
"""Test EnvConfig default values."""
config = EnvConfig()
assert config.shopify_api_key is None
assert config.shopify_api_secret is None
assert config.shop_domain is None
assert config.scopes is None
def test_env_config_with_values(self):
"""Test EnvConfig with values."""
config = EnvConfig(
shopify_api_key="key",
shopify_api_secret="secret",
shop_domain="test.myshopify.com",
scopes="read_products"
)
assert config.shopify_api_key == "key"
assert config.shopify_api_secret == "secret"
assert config.shop_domain == "test.myshopify.com"
assert config.scopes == "read_products"
Related skills
How it compares
Use the shopify skill for full-platform app, extension, and theme guidance; pick narrower shopify-setup or shopify-products skills when the task is only API credentials or bulk CSV catalog import.
FAQ
Which Shopify API does the shopify skill recommend?
The shopify skill recommends GraphQL Admin API for new development, with REST Admin API noted as legacy maintenance mode. Examples reference the 2025-01 API version with quarterly Shopify release cadence and 12-month support per version.
What Shopify extension types does the skill cover?
The shopify skill documents checkout_ui_extension, admin_action, admin_block, pos_ui_extension, and function extensions for discounts, payment, delivery, and cart validation, generated via shopify app generate extension.
How do you start local Shopify theme development?
The shopify skill runs shopify theme init, then shopify theme dev for localhost:9292 preview with auto-sync to a development theme. Use shopify theme push --development before publishing with shopify theme publish.