shopify-theme-devtools 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -15
- package/bin/cli.js +396 -0
- package/dist/theme-devtools.js +939 -47
- package/package.json +5 -1
- package/dist/theme-devtools.css +0 -0
package/README.md
CHANGED
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
A powerful in-browser developer tools panel for Shopify theme development. Inspect Liquid objects, evaluate expressions, explore metafields, manipulate cart state, detect Liquid errors, and much more — all without leaving the browser.
|
|
8
8
|
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Features](#features)
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
|
+
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
|
14
|
+
- [Panel Reference](#panel-reference)
|
|
15
|
+
- [Configuration](#configuration)
|
|
16
|
+
- [Development](#development)
|
|
17
|
+
- [How It Works](#how-it-works)
|
|
18
|
+
- [Tech Stack](#tech-stack)
|
|
19
|
+
- [Browser Support](#browser-support)
|
|
20
|
+
- [Contributing](#contributing)
|
|
21
|
+
- [License](#license)
|
|
22
|
+
|
|
9
23
|
## Features
|
|
10
24
|
|
|
11
25
|
### Core Panels
|
|
@@ -82,6 +96,28 @@ The Console panel includes an interactive expression evaluator for testing Liqui
|
|
|
82
96
|
- **Cart History** — View previous cart states with timestamps and restore any snapshot
|
|
83
97
|
- **Attribute Editor** — Modify cart attributes and notes
|
|
84
98
|
- **Diff View** — See what changed between cart states
|
|
99
|
+
- **Scenario Builder** — Create and save cart scenarios for quick testing
|
|
100
|
+
- **Cart Tests** — Write validation rules to automatically test cart items
|
|
101
|
+
|
|
102
|
+
### Cart Tests
|
|
103
|
+
|
|
104
|
+
The Cart Tests feature lets you create custom validation rules to ensure cart items meet your business requirements. Tests can be run manually or automatically whenever the cart changes.
|
|
105
|
+
|
|
106
|
+
**Rule Types:**
|
|
107
|
+
|
|
108
|
+
| Type | Description | Example |
|
|
109
|
+
|------|-------------|---------|
|
|
110
|
+
| **Property Dependency** | If item has property X, must have properties Y, Z | GWP items require `_gwp_price` and `_gwp_source` |
|
|
111
|
+
| **Field Value** | If field equals X, another field must meet condition | Gift Cards must have quantity = 1 |
|
|
112
|
+
| **Cart Composition** | If item X exists, item Y must also exist | Electronics require warranty item |
|
|
113
|
+
| **Quantity** | Min/max/multiple constraints per-item or cart-total | Max 10 per item, cart total max 50 |
|
|
114
|
+
|
|
115
|
+
**Features:**
|
|
116
|
+
|
|
117
|
+
- **Pre-built Templates** — 16 ready-to-use templates for common scenarios
|
|
118
|
+
- **Auto-run** — Toggle to run all tests whenever cart changes
|
|
119
|
+
- **Import/Export** — Save and share test configurations as JSON
|
|
120
|
+
- **Inline Results** — Pass/fail badges show test status at a glance
|
|
85
121
|
|
|
86
122
|
### Liquid Error Detection
|
|
87
123
|
|
|
@@ -118,29 +154,65 @@ Customize your devtools layout like Chrome DevTools:
|
|
|
118
154
|
|
|
119
155
|
## Quick Start
|
|
120
156
|
|
|
121
|
-
###
|
|
122
|
-
|
|
123
|
-
Copy the Liquid bridge to your theme's snippets folder:
|
|
157
|
+
### Option A: Using npm (Recommended)
|
|
124
158
|
|
|
125
159
|
```bash
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
https://raw.githubusercontent.com/yakohere/shopify-theme-devtools/main/src/liquid/theme-devtools-bridge.liquid
|
|
160
|
+
# Run directly with npx (no install needed)
|
|
161
|
+
npx shopify-theme-devtools init
|
|
129
162
|
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
163
|
+
# Or install globally for repeated use
|
|
164
|
+
npm install -g shopify-theme-devtools
|
|
165
|
+
shopify-theme-devtools init
|
|
133
166
|
```
|
|
134
167
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Add this line to `layout/theme.liquid` just before `</body>`:
|
|
168
|
+
This copies the Liquid snippet to your `snippets/` folder. Then add this line to `layout/theme.liquid` just before `</body>`:
|
|
138
169
|
|
|
139
170
|
```liquid
|
|
140
171
|
{% render 'theme-devtools-bridge' %}
|
|
141
172
|
```
|
|
142
173
|
|
|
143
|
-
|
|
174
|
+
Or use `--inject` to add it automatically:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx shopify-theme-devtools init --inject
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### CLI Options
|
|
181
|
+
|
|
182
|
+
| Option | Description |
|
|
183
|
+
|--------|-------------|
|
|
184
|
+
| `--local` | Copy JS/CSS to `assets/` folder instead of using CDN |
|
|
185
|
+
| `--inject` | Automatically add render tag to `layout/theme.liquid` |
|
|
186
|
+
| `--force` | Overwrite existing files without prompting |
|
|
187
|
+
|
|
188
|
+
The CLI automatically detects and injects your `metafields.json` schema from `.shopify/metafields.json` (or theme root), enabling devtools to show all defined metafields.
|
|
189
|
+
|
|
190
|
+
#### Syncing Metafields
|
|
191
|
+
|
|
192
|
+
When your metafields change over time, run the sync command to update the schema without overwriting other customizations:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx shopify-theme-devtools sync
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Examples
|
|
200
|
+
npx shopify-theme-devtools init # Uses CDN (recommended)
|
|
201
|
+
npx shopify-theme-devtools init --local # Self-hosted assets
|
|
202
|
+
npx shopify-theme-devtools init --local --inject # Self-hosted + auto-inject
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Option B: Manual Download
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Download directly
|
|
209
|
+
curl -o snippets/theme-devtools-bridge.liquid \
|
|
210
|
+
https://raw.githubusercontent.com/yakohere/shopify-theme-devtools/main/src/liquid/theme-devtools-bridge.liquid
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Then add `{% render 'theme-devtools-bridge' %}` to `layout/theme.liquid` before `</body>`.
|
|
214
|
+
|
|
215
|
+
### You're done!
|
|
144
216
|
|
|
145
217
|
The devtools panel automatically appears on **unpublished/development themes only**. It's safe to commit — it won't render on your live published theme.
|
|
146
218
|
|
|
@@ -162,7 +234,7 @@ The devtools panel automatically appears on **unpublished/development themes onl
|
|
|
162
234
|
|-------|-------------|
|
|
163
235
|
| **Objects** | Inspect all Liquid objects with search and tree navigation |
|
|
164
236
|
| **Metafields** | Browse metafields by resource (product, collection, shop, etc.) |
|
|
165
|
-
| **Cart** | Live cart state, history
|
|
237
|
+
| **Cart** | Live cart state, history, scenarios, tests, and manipulation tools |
|
|
166
238
|
| **Locale** | Markets, currencies, languages with locale switching |
|
|
167
239
|
| **Analytics** | Detected tracking codes and analytics configuration |
|
|
168
240
|
| **SEO** | Meta tags, Open Graph, Twitter Cards, JSON-LD structured data |
|
|
@@ -245,6 +317,8 @@ src/
|
|
|
245
317
|
│ │ ├── theme-devtools.js # Main component with tab management
|
|
246
318
|
│ │ ├── object-inspector.js # Tree view inspector
|
|
247
319
|
│ │ └── panels/ # Panel components
|
|
320
|
+
│ ├── lib/
|
|
321
|
+
│ │ └── cart-test-templates.js # Pre-built cart test templates
|
|
248
322
|
│ ├── services/
|
|
249
323
|
│ │ ├── cart.js # Cart API with history
|
|
250
324
|
│ │ ├── product.js # Product API (variants/images)
|
|
@@ -278,7 +352,6 @@ src/
|
|
|
278
352
|
- [Lit](https://lit.dev/) — Lightweight Web Components
|
|
279
353
|
- [LiquidJS](https://liquidjs.com/) — Liquid template engine
|
|
280
354
|
- [Vite](https://vitejs.dev/) — Fast build tooling
|
|
281
|
-
- Single IIFE bundle (~55KB gzipped)
|
|
282
355
|
|
|
283
356
|
## Browser Support
|
|
284
357
|
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const packageRoot = resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
const COLORS = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
green: '\x1b[32m',
|
|
14
|
+
yellow: '\x1b[33m',
|
|
15
|
+
blue: '\x1b[34m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
dim: '\x1b[2m',
|
|
18
|
+
bold: '\x1b[1m'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function log(message, color = '') {
|
|
22
|
+
console.log(`${color}${message}${COLORS.reset}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function success(message) {
|
|
26
|
+
log(`✓ ${message}`, COLORS.green);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function warn(message) {
|
|
30
|
+
log(`⚠ ${message}`, COLORS.yellow);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function error(message) {
|
|
34
|
+
log(`✗ ${message}`, COLORS.red);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function info(message) {
|
|
38
|
+
log(` ${message}`, COLORS.dim);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function printHelp() {
|
|
42
|
+
console.log(`
|
|
43
|
+
${COLORS.bold}Shopify Theme Devtools CLI${COLORS.reset}
|
|
44
|
+
|
|
45
|
+
${COLORS.bold}Usage:${COLORS.reset}
|
|
46
|
+
npx shopify-theme-devtools <command> [options]
|
|
47
|
+
|
|
48
|
+
${COLORS.bold}Commands:${COLORS.reset}
|
|
49
|
+
init Initialize devtools in your Shopify theme
|
|
50
|
+
sync Sync metafields schema from .shopify/metafields.json
|
|
51
|
+
|
|
52
|
+
${COLORS.bold}Options for init:${COLORS.reset}
|
|
53
|
+
--local Copy JS/CSS to assets folder instead of using CDN
|
|
54
|
+
--inject Add render tag to layout/theme.liquid automatically
|
|
55
|
+
--force Overwrite existing files without prompting
|
|
56
|
+
--help, -h Show this help message
|
|
57
|
+
|
|
58
|
+
${COLORS.bold}Examples:${COLORS.reset}
|
|
59
|
+
npx shopify-theme-devtools init
|
|
60
|
+
npx shopify-theme-devtools init --local
|
|
61
|
+
npx shopify-theme-devtools init --local --inject
|
|
62
|
+
npx shopify-theme-devtools sync
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ensureDir(dir) {
|
|
67
|
+
if (!existsSync(dir)) {
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function copyFile(src, dest, force = false) {
|
|
73
|
+
if (existsSync(dest) && !force) {
|
|
74
|
+
warn(`File already exists: ${dest}`);
|
|
75
|
+
info('Use --force to overwrite');
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
copyFileSync(src, dest);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findMetafieldsSchema(themeRoot) {
|
|
83
|
+
// Look for metafields.json in common locations
|
|
84
|
+
const possiblePaths = [
|
|
85
|
+
join(themeRoot, '.shopify', 'metafields.json'),
|
|
86
|
+
join(themeRoot, 'metafields.json'),
|
|
87
|
+
join(themeRoot, 'config', 'metafields.json'),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const schemaPath of possiblePaths) {
|
|
91
|
+
if (existsSync(schemaPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
94
|
+
// Validate it's valid JSON
|
|
95
|
+
JSON.parse(content);
|
|
96
|
+
return { path: schemaPath, content };
|
|
97
|
+
} catch {
|
|
98
|
+
// Invalid JSON, skip
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function injectMetafieldsSchema(liquidContent, schemaContent) {
|
|
107
|
+
// Replace the default empty schema with the actual schema
|
|
108
|
+
const defaultSchema = `{%- capture devtools_metafields_schema -%}
|
|
109
|
+
{
|
|
110
|
+
"article": [],
|
|
111
|
+
"blog": [],
|
|
112
|
+
"collection": [],
|
|
113
|
+
"company": [],
|
|
114
|
+
"company_location": [],
|
|
115
|
+
"location": [],
|
|
116
|
+
"market": [],
|
|
117
|
+
"order": [],
|
|
118
|
+
"page": [],
|
|
119
|
+
"product": [],
|
|
120
|
+
"variant": [],
|
|
121
|
+
"shop": [],
|
|
122
|
+
"customer": []
|
|
123
|
+
}
|
|
124
|
+
{%- endcapture -%}`;
|
|
125
|
+
|
|
126
|
+
const newSchema = `{%- capture devtools_metafields_schema -%}
|
|
127
|
+
${schemaContent.trim()}
|
|
128
|
+
{%- endcapture -%}`;
|
|
129
|
+
|
|
130
|
+
return liquidContent.replace(defaultSchema, newSchema);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function patchLiquidForLocal(content) {
|
|
134
|
+
// Change CDN to asset_url
|
|
135
|
+
let patched = content.replace(
|
|
136
|
+
/{%- assign devtools_cdn_base = '[^']+' -%}/,
|
|
137
|
+
"{%- assign devtools_cdn_base = '' -%}"
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Replace the script loading section for local assets
|
|
141
|
+
patched = patched.replace(
|
|
142
|
+
/<script>\s*window\.__THEME_DEVTOOLS_CSS_URL__[^<]+<\/script>\s*<script src="{{ devtools_cdn_base }}\/theme-devtools\.js" defer><\/script>/s,
|
|
143
|
+
`<script>
|
|
144
|
+
window.__THEME_DEVTOOLS_CSS_URL__ = '{{ 'theme-devtools.css' | asset_url }}';
|
|
145
|
+
</script>
|
|
146
|
+
<script src="{{ 'theme-devtools.js' | asset_url }}" defer></script>`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return patched;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function injectRenderTag(themeLiquidPath) {
|
|
153
|
+
if (!existsSync(themeLiquidPath)) {
|
|
154
|
+
warn(`layout/theme.liquid not found at: ${themeLiquidPath}`);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let content = readFileSync(themeLiquidPath, 'utf-8');
|
|
159
|
+
const renderTag = "{% render 'theme-devtools-bridge' %}";
|
|
160
|
+
|
|
161
|
+
if (content.includes('theme-devtools-bridge')) {
|
|
162
|
+
info('Render tag already exists in theme.liquid');
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Insert before </body>
|
|
167
|
+
const bodyCloseRegex = /<\/body>/i;
|
|
168
|
+
if (!bodyCloseRegex.test(content)) {
|
|
169
|
+
warn('Could not find </body> tag in theme.liquid');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
content = content.replace(bodyCloseRegex, ` ${renderTag}\n </body>`);
|
|
174
|
+
writeFileSync(themeLiquidPath, content);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function detectThemeRoot(cwd) {
|
|
179
|
+
// Check if we're in a Shopify theme directory
|
|
180
|
+
const markers = ['layout', 'snippets', 'templates', 'config'];
|
|
181
|
+
const hasMarkers = markers.filter(dir => existsSync(join(cwd, dir)));
|
|
182
|
+
|
|
183
|
+
if (hasMarkers.length >= 2) {
|
|
184
|
+
return cwd;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if there's a theme subdirectory
|
|
188
|
+
const possibleThemeDirs = ['theme', 'shopify', 'src'];
|
|
189
|
+
for (const dir of possibleThemeDirs) {
|
|
190
|
+
const themePath = join(cwd, dir);
|
|
191
|
+
if (existsSync(themePath)) {
|
|
192
|
+
const subMarkers = markers.filter(m => existsSync(join(themePath, m)));
|
|
193
|
+
if (subMarkers.length >= 2) {
|
|
194
|
+
return themePath;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function init(args) {
|
|
203
|
+
const cwd = process.cwd();
|
|
204
|
+
const useLocal = args.includes('--local');
|
|
205
|
+
const shouldInject = args.includes('--inject');
|
|
206
|
+
const force = args.includes('--force');
|
|
207
|
+
|
|
208
|
+
console.log();
|
|
209
|
+
log('Shopify Theme Devtools', COLORS.bold);
|
|
210
|
+
console.log();
|
|
211
|
+
|
|
212
|
+
// Detect theme root
|
|
213
|
+
const themeRoot = detectThemeRoot(cwd);
|
|
214
|
+
|
|
215
|
+
if (!themeRoot) {
|
|
216
|
+
error('Could not detect Shopify theme directory');
|
|
217
|
+
info('Make sure you run this command from your theme root');
|
|
218
|
+
info('(should contain layout/, snippets/, templates/ folders)');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (themeRoot !== cwd) {
|
|
223
|
+
info(`Detected theme root: ${themeRoot}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const snippetsDir = join(themeRoot, 'snippets');
|
|
227
|
+
const assetsDir = join(themeRoot, 'assets');
|
|
228
|
+
const layoutDir = join(themeRoot, 'layout');
|
|
229
|
+
|
|
230
|
+
// Ensure directories exist
|
|
231
|
+
ensureDir(snippetsDir);
|
|
232
|
+
if (useLocal) {
|
|
233
|
+
ensureDir(assetsDir);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Source files
|
|
237
|
+
const liquidSrc = join(packageRoot, 'src', 'liquid', 'theme-devtools-bridge.liquid');
|
|
238
|
+
const jsSrc = join(packageRoot, 'dist', 'theme-devtools.js');
|
|
239
|
+
const cssSrc = join(packageRoot, 'dist', 'theme-devtools.css');
|
|
240
|
+
|
|
241
|
+
// Destination files
|
|
242
|
+
const liquidDest = join(snippetsDir, 'theme-devtools-bridge.liquid');
|
|
243
|
+
const jsDest = join(assetsDir, 'theme-devtools.js');
|
|
244
|
+
const cssDest = join(assetsDir, 'theme-devtools.css');
|
|
245
|
+
|
|
246
|
+
let liquidContent = readFileSync(liquidSrc, 'utf-8');
|
|
247
|
+
|
|
248
|
+
// Set devtools_local to false for production use
|
|
249
|
+
liquidContent = liquidContent.replace(
|
|
250
|
+
/{%- assign devtools_local = true -%}/,
|
|
251
|
+
'{%- assign devtools_local = false -%}'
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Auto-detect and inject metafields schema
|
|
255
|
+
const metafieldsSchema = findMetafieldsSchema(themeRoot);
|
|
256
|
+
if (metafieldsSchema) {
|
|
257
|
+
liquidContent = injectMetafieldsSchema(liquidContent, metafieldsSchema.content);
|
|
258
|
+
success(`Injected metafields schema from ${metafieldsSchema.path.replace(themeRoot + '/', '')}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (useLocal) {
|
|
262
|
+
liquidContent = patchLiquidForLocal(liquidContent);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Copy liquid snippet
|
|
266
|
+
if (existsSync(liquidDest) && !force) {
|
|
267
|
+
warn(`Snippet already exists: snippets/theme-devtools-bridge.liquid`);
|
|
268
|
+
info('Use --force to overwrite');
|
|
269
|
+
} else {
|
|
270
|
+
writeFileSync(liquidDest, liquidContent);
|
|
271
|
+
success('Created snippets/theme-devtools-bridge.liquid');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Copy assets if --local
|
|
275
|
+
if (useLocal) {
|
|
276
|
+
if (!existsSync(jsSrc)) {
|
|
277
|
+
error('dist/theme-devtools.js not found. Run npm run build first.');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (copyFile(jsSrc, jsDest, force)) {
|
|
282
|
+
success('Copied assets/theme-devtools.js');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (existsSync(cssSrc)) {
|
|
286
|
+
if (copyFile(cssSrc, cssDest, force)) {
|
|
287
|
+
success('Copied assets/theme-devtools.css');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Inject render tag if --inject
|
|
293
|
+
if (shouldInject) {
|
|
294
|
+
const themeLiquid = join(layoutDir, 'theme.liquid');
|
|
295
|
+
if (injectRenderTag(themeLiquid)) {
|
|
296
|
+
success('Added render tag to layout/theme.liquid');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log();
|
|
301
|
+
log('Setup complete!', COLORS.green + COLORS.bold);
|
|
302
|
+
console.log();
|
|
303
|
+
|
|
304
|
+
if (!shouldInject) {
|
|
305
|
+
log('Next step:', COLORS.bold);
|
|
306
|
+
info("Add this line to layout/theme.liquid before </body>:");
|
|
307
|
+
console.log();
|
|
308
|
+
log(" {% render 'theme-devtools-bridge' %}", COLORS.blue);
|
|
309
|
+
console.log();
|
|
310
|
+
info('Or run with --inject to do this automatically');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log();
|
|
314
|
+
info('The devtools panel will only appear on unpublished/development themes.');
|
|
315
|
+
console.log();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function sync() {
|
|
319
|
+
const cwd = process.cwd();
|
|
320
|
+
|
|
321
|
+
console.log();
|
|
322
|
+
log('Shopify Theme Devtools', COLORS.bold);
|
|
323
|
+
console.log();
|
|
324
|
+
|
|
325
|
+
// Detect theme root
|
|
326
|
+
const themeRoot = detectThemeRoot(cwd);
|
|
327
|
+
|
|
328
|
+
if (!themeRoot) {
|
|
329
|
+
error('Could not detect Shopify theme directory');
|
|
330
|
+
info('Make sure you run this command from your theme root');
|
|
331
|
+
info('(should contain layout/, snippets/, templates/ folders)');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const snippetsDir = join(themeRoot, 'snippets');
|
|
336
|
+
const liquidDest = join(snippetsDir, 'theme-devtools-bridge.liquid');
|
|
337
|
+
|
|
338
|
+
// Check if snippet exists
|
|
339
|
+
if (!existsSync(liquidDest)) {
|
|
340
|
+
error('Devtools snippet not found');
|
|
341
|
+
info('Run "npx shopify-theme-devtools init" first');
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Find metafields schema
|
|
346
|
+
const metafieldsSchema = findMetafieldsSchema(themeRoot);
|
|
347
|
+
|
|
348
|
+
if (!metafieldsSchema) {
|
|
349
|
+
error('No metafields.json found');
|
|
350
|
+
info('Looked in: .shopify/metafields.json, metafields.json, config/metafields.json');
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Read current snippet
|
|
355
|
+
let liquidContent = readFileSync(liquidDest, 'utf-8');
|
|
356
|
+
|
|
357
|
+
// Replace the metafields schema using a regex that matches any JSON content
|
|
358
|
+
const schemaRegex = /\{%- capture devtools_metafields_schema -%\}[\s\S]*?\{%- endcapture -%\}/;
|
|
359
|
+
|
|
360
|
+
if (!schemaRegex.test(liquidContent)) {
|
|
361
|
+
error('Could not find metafields schema section in snippet');
|
|
362
|
+
info('The snippet may be corrupted. Run "npx shopify-theme-devtools init --force" to regenerate');
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const newSchema = `{%- capture devtools_metafields_schema -%}
|
|
367
|
+
${metafieldsSchema.content.trim()}
|
|
368
|
+
{%- endcapture -%}`;
|
|
369
|
+
|
|
370
|
+
liquidContent = liquidContent.replace(schemaRegex, newSchema);
|
|
371
|
+
|
|
372
|
+
// Write updated snippet
|
|
373
|
+
writeFileSync(liquidDest, liquidContent);
|
|
374
|
+
success(`Synced metafields schema from ${metafieldsSchema.path.replace(themeRoot + '/', '')}`);
|
|
375
|
+
|
|
376
|
+
console.log();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Parse arguments
|
|
380
|
+
const args = process.argv.slice(2);
|
|
381
|
+
const command = args[0];
|
|
382
|
+
|
|
383
|
+
if (!command || command === '--help' || command === '-h') {
|
|
384
|
+
printHelp();
|
|
385
|
+
process.exit(0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (command === 'init') {
|
|
389
|
+
init(args);
|
|
390
|
+
} else if (command === 'sync') {
|
|
391
|
+
sync();
|
|
392
|
+
} else {
|
|
393
|
+
error(`Unknown command: ${command}`);
|
|
394
|
+
printHelp();
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|