@runwell/shopify-toolkit 0.8.0 → 0.10.0
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/bin/runwell-shopify +13 -0
- package/lib/baseline.js +203 -0
- package/lib/init.js +122 -0
- package/lib/sync.js +16 -1
- package/lib/upgrade-baseline.js +25 -0
- package/modules/INDEX.md +17 -16
- package/modules/cart-usps/module.json +3 -3
- package/modules/comparison-table/module.json +1 -1
- package/modules/delivery-estimate/module.json +1 -1
- package/modules/editorial-block/module.json +3 -3
- package/modules/editorial-hero/module.json +6 -6
- package/modules/exit-intent/module.json +2 -2
- package/modules/faq/module.json +1 -1
- package/modules/how-it-works/module.json +3 -3
- package/modules/inventory-urgency/module.json +1 -1
- package/modules/loyalty-tiers/module.json +1 -1
- package/modules/pdp-ingredients/module.json +1 -1
- package/modules/pdp-journal-link/module.json +3 -3
- package/modules/pdp-trust-checks/module.json +2 -2
- package/modules/press-bar/module.json +1 -1
- package/modules/recently-viewed/module.json +1 -1
- package/modules/reviews/module.json +1 -1
- package/modules/risk-reversal/module.json +1 -1
- package/modules/shipping-bar/module.json +1 -1
- package/modules/testimonials/module.json +1 -1
- package/modules/trust-badges/module.json +1 -1
- package/package.json +2 -2
package/bin/runwell-shopify
CHANGED
|
@@ -8,6 +8,8 @@ import { remove } from '../lib/remove.js';
|
|
|
8
8
|
import { doctor } from '../lib/doctor.js';
|
|
9
9
|
import { validate } from '../lib/validate.js';
|
|
10
10
|
import { qa } from '../lib/qa.js';
|
|
11
|
+
import { init } from '../lib/init.js';
|
|
12
|
+
import { upgradeBaseline } from '../lib/upgrade-baseline.js';
|
|
11
13
|
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -49,6 +51,11 @@ Commands:
|
|
|
49
51
|
qa Code-first QA pipeline (validate-config, doctor,
|
|
50
52
|
template-integrity, orphan-assets, theme-check).
|
|
51
53
|
Run before any visual QA or "done" declaration.
|
|
54
|
+
init <client> Scaffold a new tenant theme directory pinned to
|
|
55
|
+
the dawn-runwell baseline + toolkit. Use --baseline
|
|
56
|
+
to override the default pin.
|
|
57
|
+
upgrade-baseline <pkg@version> Bump the baseline pin in runwell.config.json.
|
|
58
|
+
Run "runwell-shopify sync" after to apply.
|
|
52
59
|
help Show this help
|
|
53
60
|
|
|
54
61
|
Common options:
|
|
@@ -86,6 +93,12 @@ flags.toolkitRoot = TOOLKIT_ROOT;
|
|
|
86
93
|
case 'qa':
|
|
87
94
|
await qa(flags);
|
|
88
95
|
break;
|
|
96
|
+
case 'init':
|
|
97
|
+
await init(flags);
|
|
98
|
+
break;
|
|
99
|
+
case 'upgrade-baseline':
|
|
100
|
+
await upgradeBaseline(flags);
|
|
101
|
+
break;
|
|
89
102
|
case undefined:
|
|
90
103
|
case 'help':
|
|
91
104
|
case '--help':
|
package/lib/baseline.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { interpolate } from './template.js';
|
|
6
|
+
|
|
7
|
+
/* baseline.js: resolve, fetch, and apply the @runwell/dawn-runwell
|
|
8
|
+
baseline (or another baseline package) into a tenant theme dir.
|
|
9
|
+
|
|
10
|
+
Resolution order:
|
|
11
|
+
1. flags.baselinePath (CLI override)
|
|
12
|
+
2. config.baseline_path (local dev override in runwell.config.json)
|
|
13
|
+
3. config.baseline (e.g. "@runwell/dawn-runwell@^1.0.0") via the
|
|
14
|
+
cache at ~/.runwell/baseline-cache/<pkg>/<version>/
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const CACHE_DIR = path.join(os.homedir(), '.runwell', 'baseline-cache');
|
|
18
|
+
|
|
19
|
+
export function parseBaselinePin(pin) {
|
|
20
|
+
if (!pin) return null;
|
|
21
|
+
// accept "@runwell/dawn-runwell@^1.0.0" or "@runwell/dawn-runwell@latest"
|
|
22
|
+
const m = pin.match(/^(@?[^@]+)@(.+)$/);
|
|
23
|
+
if (!m) return { name: pin, version: 'latest' };
|
|
24
|
+
return { name: m[1], version: m[2] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveBaseline(flags, config) {
|
|
28
|
+
if (flags && flags.baselinePath) return flags.baselinePath;
|
|
29
|
+
if (config && config.baseline_path) return path.resolve(config.baseline_path);
|
|
30
|
+
if (!config || !config.baseline) return null;
|
|
31
|
+
|
|
32
|
+
const pin = parseBaselinePin(config.baseline);
|
|
33
|
+
if (!pin) return null;
|
|
34
|
+
const safeName = pin.name.replace('/', '__');
|
|
35
|
+
const versionDir = path.join(CACHE_DIR, safeName, pin.version);
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(path.join(versionDir, 'baseline-manifest.json'))) {
|
|
38
|
+
return versionDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Cache miss: fetch via npm pack
|
|
42
|
+
console.log(`Fetching baseline: ${pin.name}@${pin.version} ...`);
|
|
43
|
+
fs.mkdirSync(versionDir, { recursive: true });
|
|
44
|
+
const packResult = spawnSync('npm', ['pack', `${pin.name}@${pin.version}`, '--pack-destination', versionDir], {
|
|
45
|
+
stdio: 'pipe',
|
|
46
|
+
encoding: 'utf8'
|
|
47
|
+
});
|
|
48
|
+
if (packResult.status !== 0) {
|
|
49
|
+
throw new Error(`npm pack failed: ${packResult.stderr || packResult.stdout}`);
|
|
50
|
+
}
|
|
51
|
+
const tarball = packResult.stdout.trim().split('\n').pop();
|
|
52
|
+
const tarPath = path.join(versionDir, tarball);
|
|
53
|
+
// npm pack delivers a tarball that extracts to ./package/
|
|
54
|
+
const tarResult = spawnSync('tar', ['-xzf', tarPath, '-C', versionDir, '--strip-components=1'], { stdio: 'inherit' });
|
|
55
|
+
if (tarResult.status !== 0) throw new Error('tar extract failed');
|
|
56
|
+
fs.unlinkSync(tarPath);
|
|
57
|
+
return versionDir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadBaselineManifest(baselineRoot) {
|
|
61
|
+
const p = path.join(baselineRoot, 'baseline-manifest.json');
|
|
62
|
+
if (!fs.existsSync(p)) {
|
|
63
|
+
throw new Error(`baseline-manifest.json not found at ${p}`);
|
|
64
|
+
}
|
|
65
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function syncBaseline({ baselineRoot, targetDir, config, dryRun, written }) {
|
|
69
|
+
const manifest = loadBaselineManifest(baselineRoot);
|
|
70
|
+
console.log(`\n[baseline: ${manifest.name}@${manifest.version}]`);
|
|
71
|
+
|
|
72
|
+
const interpolationVars = {
|
|
73
|
+
config: {},
|
|
74
|
+
brand: config.brand || {},
|
|
75
|
+
client: { name: config.client, store: config.store }
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// 1. Owned files (verbatim or interpolated)
|
|
79
|
+
for (const [bucket, files] of Object.entries(manifest.files || {})) {
|
|
80
|
+
if (!Array.isArray(files)) continue;
|
|
81
|
+
const targetSubdir = bucketToTargetDir(bucket, targetDir);
|
|
82
|
+
if (!targetSubdir) continue;
|
|
83
|
+
for (const rel of files) {
|
|
84
|
+
const sourcePath = path.join(baselineRoot, bucket, rel);
|
|
85
|
+
if (!fs.existsSync(sourcePath)) {
|
|
86
|
+
console.warn(` [warn] missing baseline source: ${bucket}/${rel}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const filename = path.basename(rel);
|
|
90
|
+
const targetPath = path.join(targetSubdir, filename);
|
|
91
|
+
const isText = isTextFile(filename);
|
|
92
|
+
let content = fs.readFileSync(sourcePath, isText ? 'utf8' : null);
|
|
93
|
+
if (isText) content = interpolate(content, interpolationVars);
|
|
94
|
+
if (!dryRun) {
|
|
95
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
96
|
+
fs.writeFileSync(targetPath, content);
|
|
97
|
+
}
|
|
98
|
+
console.log(` + ${path.relative(targetDir, targetPath)}`);
|
|
99
|
+
written.push({ source: 'baseline', target: targetPath, bucket, baseline_version: manifest.version });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Patches (deep-merge into existing tenant files)
|
|
104
|
+
for (const [tenantRel, patchRel] of Object.entries(manifest.patches || {})) {
|
|
105
|
+
const patchPath = path.join(baselineRoot, patchRel);
|
|
106
|
+
const tenantPath = path.join(targetDir, tenantRel);
|
|
107
|
+
if (!fs.existsSync(patchPath)) {
|
|
108
|
+
console.warn(` [warn] missing baseline patch: ${patchRel}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!fs.existsSync(tenantPath)) {
|
|
112
|
+
console.warn(` [warn] tenant file ${tenantRel} not present; skipping patch (run shopify theme pull first)`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const patchData = JSON.parse(fs.readFileSync(patchPath, 'utf8'));
|
|
116
|
+
const tenantData = JSON.parse(fs.readFileSync(tenantPath, 'utf8'));
|
|
117
|
+
if (tenantRel === 'config/settings_schema.json') {
|
|
118
|
+
applySettingsPatches(tenantData, patchData);
|
|
119
|
+
} else if (tenantRel.startsWith('locales/')) {
|
|
120
|
+
deepMerge(tenantData, patchData.patches || {});
|
|
121
|
+
} else {
|
|
122
|
+
deepMerge(tenantData, patchData);
|
|
123
|
+
}
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
fs.writeFileSync(tenantPath, JSON.stringify(tenantData, null, 2) + '\n');
|
|
126
|
+
}
|
|
127
|
+
console.log(` ~ patched ${tenantRel}`);
|
|
128
|
+
written.push({ source: 'baseline-patch', target: tenantPath, bucket: 'patch', baseline_version: manifest.version });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function applySettingsPatches(tenantSchema, patchDoc) {
|
|
133
|
+
// tenantSchema is an array of setting groups. patchDoc.patches is an array.
|
|
134
|
+
const patches = patchDoc.patches || [];
|
|
135
|
+
for (const patchGroup of patches) {
|
|
136
|
+
const existing = tenantSchema.find(g => g.name === patchGroup.name);
|
|
137
|
+
if (!existing) {
|
|
138
|
+
tenantSchema.push(patchGroup);
|
|
139
|
+
} else {
|
|
140
|
+
// Merge settings, skipping duplicates by id (or by content for header/paragraph)
|
|
141
|
+
const existingIds = new Set((existing.settings || []).map(s => s.id).filter(Boolean));
|
|
142
|
+
for (const s of patchGroup.settings || []) {
|
|
143
|
+
if (s.id && existingIds.has(s.id)) continue;
|
|
144
|
+
existing.settings = existing.settings || [];
|
|
145
|
+
existing.settings.push(s);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function deepMerge(target, source) {
|
|
152
|
+
for (const key of Object.keys(source)) {
|
|
153
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
154
|
+
target[key] = target[key] || {};
|
|
155
|
+
deepMerge(target[key], source[key]);
|
|
156
|
+
} else {
|
|
157
|
+
target[key] = source[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function bucketToTargetDir(bucket, targetDir) {
|
|
163
|
+
switch (bucket) {
|
|
164
|
+
case 'assets': return path.join(targetDir, 'assets');
|
|
165
|
+
case 'snippets': return path.join(targetDir, 'snippets');
|
|
166
|
+
case 'sections': return path.join(targetDir, 'sections');
|
|
167
|
+
case 'templates': return path.join(targetDir, 'templates');
|
|
168
|
+
case 'layout': return path.join(targetDir, 'layout');
|
|
169
|
+
case 'locales': return path.join(targetDir, 'locales');
|
|
170
|
+
case 'config': return path.join(targetDir, 'config');
|
|
171
|
+
default: return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isTextFile(filename) {
|
|
176
|
+
const ext = path.extname(filename).toLowerCase();
|
|
177
|
+
return ['.liquid', '.js', '.css', '.json', '.html', '.svg', '.md', '.txt'].includes(ext);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function applyTenantOverrides({ targetDir, dryRun, written }) {
|
|
181
|
+
const overridesDir = path.join(targetDir, 'tenant-overrides');
|
|
182
|
+
if (!fs.existsSync(overridesDir)) return;
|
|
183
|
+
console.log(`\n[tenant-overrides]`);
|
|
184
|
+
walkAndCopy(overridesDir, targetDir, overridesDir, dryRun, written);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function walkAndCopy(srcRoot, dstRoot, currentDir, dryRun, written) {
|
|
188
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
189
|
+
const srcPath = path.join(currentDir, entry.name);
|
|
190
|
+
const rel = path.relative(srcRoot, srcPath);
|
|
191
|
+
const dstPath = path.join(dstRoot, rel);
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
walkAndCopy(srcRoot, dstRoot, srcPath, dryRun, written);
|
|
194
|
+
} else {
|
|
195
|
+
if (!dryRun) {
|
|
196
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
197
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
198
|
+
}
|
|
199
|
+
console.log(` + ${rel}`);
|
|
200
|
+
written.push({ source: 'tenant-override', target: dstPath, bucket: 'override' });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/* runwell-shopify init <client> [--baseline <pin>]: scaffold a new
|
|
5
|
+
tenant theme directory with the minimum required files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export async function init(flags) {
|
|
9
|
+
const client = flags.positional && flags.positional[0];
|
|
10
|
+
if (!client) {
|
|
11
|
+
console.error('Usage: runwell-shopify init <client-name> [--baseline @runwell/dawn-runwell@^1.0.0] [--target ./<dir>]');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const targetDir = path.resolve(flags.target || `./${client}-shopify`);
|
|
16
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
17
|
+
console.error(`Target directory ${targetDir} is not empty. Choose another path or remove existing contents.`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
22
|
+
for (const d of ['layout', 'sections', 'snippets', 'assets', 'config', 'locales', 'templates', 'tenant-overrides']) {
|
|
23
|
+
fs.mkdirSync(path.join(targetDir, d), { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const baseline = flags.baseline || '@runwell/dawn-runwell@^1.0.0';
|
|
27
|
+
|
|
28
|
+
const config = {
|
|
29
|
+
client,
|
|
30
|
+
store: 'TODO.myshopify.com',
|
|
31
|
+
theme_path: '.',
|
|
32
|
+
baseline,
|
|
33
|
+
toolkit_version: '^0.9.0',
|
|
34
|
+
brand: {
|
|
35
|
+
name: client.charAt(0).toUpperCase() + client.slice(1),
|
|
36
|
+
primary: '#1A1A1A',
|
|
37
|
+
accent: '#D4AF37',
|
|
38
|
+
cream: '#F5EFE6',
|
|
39
|
+
currency: 'USD',
|
|
40
|
+
support_email: `support@${client}.com`,
|
|
41
|
+
tagline: ''
|
|
42
|
+
},
|
|
43
|
+
modules: {
|
|
44
|
+
'_shared/css-tokens': { enabled: true, config: {} }
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fs.writeFileSync(path.join(targetDir, 'runwell.config.json'), JSON.stringify(config, null, 2) + '\n');
|
|
49
|
+
|
|
50
|
+
// Scaffold templates
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(targetDir, 'templates', 'index.json'),
|
|
53
|
+
JSON.stringify({ sections: {}, order: [] }, null, 2) + '\n'
|
|
54
|
+
);
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(targetDir, 'templates', 'product.json'),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
sections: {
|
|
59
|
+
main: {
|
|
60
|
+
type: 'main-product',
|
|
61
|
+
blocks: {
|
|
62
|
+
title: { type: 'title' },
|
|
63
|
+
price: { type: 'price' },
|
|
64
|
+
buy_buttons: { type: 'buy_buttons', settings: { show_dynamic_checkout: true } }
|
|
65
|
+
},
|
|
66
|
+
block_order: ['title', 'price', 'buy_buttons'],
|
|
67
|
+
settings: {}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
order: ['main']
|
|
71
|
+
}, null, 2) + '\n'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// README
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(targetDir, 'README.md'),
|
|
77
|
+
`# ${client} Shopify theme
|
|
78
|
+
|
|
79
|
+
Tenant theme for ${client}, built on @runwell/dawn-runwell + @runwell/shopify-toolkit.
|
|
80
|
+
|
|
81
|
+
## Lock context (always state these before any work)
|
|
82
|
+
|
|
83
|
+
- Client: ${client}
|
|
84
|
+
- Store: TODO.myshopify.com
|
|
85
|
+
- Theme ID: TODO
|
|
86
|
+
- Storefront password: TODO
|
|
87
|
+
|
|
88
|
+
## Quick start
|
|
89
|
+
|
|
90
|
+
\`\`\`bash
|
|
91
|
+
# 1. Edit runwell.config.json with the real store handle + brand values
|
|
92
|
+
# 2. Sync baseline + toolkit + tenant overrides into the theme dir
|
|
93
|
+
runwell-shopify sync
|
|
94
|
+
|
|
95
|
+
# 3. Code QA before any push
|
|
96
|
+
runwell-shopify qa
|
|
97
|
+
|
|
98
|
+
# 4. Push to a NEW unpublished theme on the store
|
|
99
|
+
shopify theme push -u -s <store>.myshopify.com
|
|
100
|
+
|
|
101
|
+
# 5. Once happy, push to the live theme
|
|
102
|
+
shopify theme push -t <THEME_ID> -l -a -s <store>.myshopify.com
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
## Layers
|
|
106
|
+
|
|
107
|
+
1. \`@runwell/dawn-runwell\` baseline (pinned in runwell.config.json).
|
|
108
|
+
2. \`@runwell/shopify-toolkit\` modules (declared in runwell.config.json modules block).
|
|
109
|
+
3. This repo: \`tenant-overrides/\` directory + \`templates/*.json\` + bespoke sections + brand assets.
|
|
110
|
+
|
|
111
|
+
\`runwell-manifest.json\` is regenerated on every sync and tracks file provenance.
|
|
112
|
+
`
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
console.log(`Scaffolded ${client} tenant theme at ${targetDir}`);
|
|
116
|
+
console.log(`\nNext steps:`);
|
|
117
|
+
console.log(` 1. cd ${path.relative(process.cwd(), targetDir) || '.'}`);
|
|
118
|
+
console.log(` 2. Edit runwell.config.json: set store handle + brand values`);
|
|
119
|
+
console.log(` 3. runwell-shopify sync`);
|
|
120
|
+
console.log(` 4. runwell-shopify qa`);
|
|
121
|
+
console.log(` 5. shopify theme push -u -s <store>`);
|
|
122
|
+
}
|
package/lib/sync.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadConfig, loadModuleManifest, listAvailableModules, resolveVariant } from './config-loader.js';
|
|
4
4
|
import { interpolate } from './template.js';
|
|
5
|
+
import { resolveBaseline, syncBaseline, applyTenantOverrides } from './baseline.js';
|
|
5
6
|
|
|
6
7
|
export async function sync(flags) {
|
|
7
8
|
const configPath = flags.config || './runwell.config.json';
|
|
@@ -18,6 +19,14 @@ export async function sync(flags) {
|
|
|
18
19
|
const writtenFiles = [];
|
|
19
20
|
const removedFiles = [];
|
|
20
21
|
|
|
22
|
+
// Stage 1: baseline (if configured). Owned files + patches first;
|
|
23
|
+
// toolkit module files in stage 2 will overlay on top, and tenant
|
|
24
|
+
// overrides in stage 3 win last.
|
|
25
|
+
const baselineRoot = resolveBaseline(flags, config);
|
|
26
|
+
if (baselineRoot) {
|
|
27
|
+
await syncBaseline({ baselineRoot, targetDir, config, dryRun, written: writtenFiles });
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
const moduleNames = Object.keys(config.modules);
|
|
22
31
|
for (const moduleName of moduleNames) {
|
|
23
32
|
const cfgEntry = config.modules[moduleName];
|
|
@@ -77,15 +86,21 @@ export async function sync(flags) {
|
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
//
|
|
89
|
+
// Stage 3: tenant overrides (last; wins over baseline + toolkit).
|
|
90
|
+
await applyTenantOverrides({ targetDir, dryRun, written: writtenFiles });
|
|
91
|
+
|
|
92
|
+
// Manifest with provenance per file.
|
|
81
93
|
const manifestPath = path.join(targetDir, 'runwell-manifest.json');
|
|
82
94
|
const manifestData = {
|
|
83
95
|
generated_at: new Date().toISOString(),
|
|
84
96
|
toolkit_version: getToolkitVersion(flags.toolkitRoot),
|
|
97
|
+
baseline_version: writtenFiles.find(f => f.source === 'baseline')?.baseline_version || null,
|
|
85
98
|
client: config.client,
|
|
86
99
|
files: writtenFiles.map(f => ({
|
|
100
|
+
source: f.source || 'toolkit',
|
|
87
101
|
module: f.module,
|
|
88
102
|
bucket: f.bucket,
|
|
103
|
+
baseline_version: f.baseline_version,
|
|
89
104
|
path: path.relative(targetDir, f.target)
|
|
90
105
|
}))
|
|
91
106
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig } from './config-loader.js';
|
|
4
|
+
|
|
5
|
+
/* runwell-shopify upgrade-baseline <pin>: bump the baseline version pin
|
|
6
|
+
in runwell.config.json. Subsequent runwell-shopify sync uses the new
|
|
7
|
+
pin to fetch the new baseline. Caller is responsible for running sync
|
|
8
|
+
+ qa + push afterwards.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export async function upgradeBaseline(flags) {
|
|
12
|
+
const newPin = flags.positional && flags.positional[0];
|
|
13
|
+
if (!newPin) {
|
|
14
|
+
console.error('Usage: runwell-shopify upgrade-baseline <package@version>');
|
|
15
|
+
console.error('Example: runwell-shopify upgrade-baseline @runwell/dawn-runwell@1.1.0');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const configPath = path.resolve(flags.config || './runwell.config.json');
|
|
19
|
+
const { config } = loadConfig(configPath);
|
|
20
|
+
const oldPin = config.baseline || '(unset)';
|
|
21
|
+
config.baseline = newPin;
|
|
22
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
23
|
+
console.log(`baseline: ${oldPin} -> ${newPin}`);
|
|
24
|
+
console.log(`Run 'runwell-shopify sync' to apply, then 'runwell-shopify qa' before pushing.`);
|
|
25
|
+
}
|
package/modules/INDEX.md
CHANGED
|
@@ -26,7 +26,7 @@ Total modules: 34.
|
|
|
26
26
|
| `delivery-estimate` | pdp | (native build) | snippets:1 | (none) | (none) | (none) |
|
|
27
27
|
| `editorial-block` | storefront | (native build) | sections:1 | eyebrow, heading, lede, background_color, text_color | (none) | (none) |
|
|
28
28
|
| `editorial-hero` | storefront | Dawn's image-banner / video-banner | | eyebrow, heading, subheading | video-bg, hero-with-card | (none) |
|
|
29
|
-
| `exit-intent` | conversion |
|
|
29
|
+
| `exit-intent` | conversion | Privy / Justuno display layer | sections:1 assets:1 | eyebrow, heading, lede | (none) | (none) |
|
|
30
30
|
| `faq` | storefront | (native build) | sections:1 | eyebrow, heading, background_color, text_color | (none) | (none) |
|
|
31
31
|
| `gift-with-purchase` | conversion | (native build) | snippets:1 assets:1 | threshold_cents, gift_handle, unlocked_message, locked_message_suffix | (none) | create-gift-product + create-gift-discount |
|
|
32
32
|
| `how-it-works` | storefront | (native build) | sections:1 | eyebrow, heading, lede, image, asset_filename, background_color, text_color, cta_label, cta_link | (none) | (none) |
|
|
@@ -97,13 +97,13 @@ Total modules: 34.
|
|
|
97
97
|
### delivery-estimate
|
|
98
98
|
|
|
99
99
|
- Category: pdp
|
|
100
|
-
- What: delivery-estimate
|
|
100
|
+
- What: PDP delivery-date estimate (JS-rendered date range based on cutoff time and standard transit).
|
|
101
101
|
- Files: snippets:1
|
|
102
102
|
|
|
103
103
|
### editorial-block
|
|
104
104
|
|
|
105
105
|
- Category: storefront
|
|
106
|
-
- What:
|
|
106
|
+
- What: Editorial introduction block: eyebrow + serif italic heading + lede + body.
|
|
107
107
|
- Files: sections:1
|
|
108
108
|
- Config: eyebrow, heading, lede, background_color, text_color
|
|
109
109
|
|
|
@@ -118,14 +118,15 @@ Total modules: 34.
|
|
|
118
118
|
### exit-intent
|
|
119
119
|
|
|
120
120
|
- Category: conversion
|
|
121
|
-
-
|
|
121
|
+
- Replaces: Privy / Justuno display layer
|
|
122
|
+
- What: Exit-intent popup with email capture.
|
|
122
123
|
- Files: sections:1 assets:1
|
|
123
124
|
- Config: eyebrow, heading, lede
|
|
124
125
|
|
|
125
126
|
### faq
|
|
126
127
|
|
|
127
128
|
- Category: storefront
|
|
128
|
-
- What:
|
|
129
|
+
- What: FAQ accordion.
|
|
129
130
|
- Files: sections:1
|
|
130
131
|
- Config: eyebrow, heading, background_color, text_color
|
|
131
132
|
|
|
@@ -140,14 +141,14 @@ Total modules: 34.
|
|
|
140
141
|
### how-it-works
|
|
141
142
|
|
|
142
143
|
- Category: storefront
|
|
143
|
-
- What:
|
|
144
|
+
- What: How-it-works section: side-image + multi-step explainer.
|
|
144
145
|
- Files: sections:1
|
|
145
146
|
- Config: eyebrow, heading, lede, image, asset_filename, background_color, text_color, cta_label, cta_link
|
|
146
147
|
|
|
147
148
|
### inventory-urgency
|
|
148
149
|
|
|
149
150
|
- Category: pdp
|
|
150
|
-
- What: inventory
|
|
151
|
+
- What: PDP inventory urgency message (Only N left) when stock tracking is enabled and quantity is low.
|
|
151
152
|
- Files: snippets:1
|
|
152
153
|
|
|
153
154
|
### loyalty-tiers
|
|
@@ -161,21 +162,21 @@ Total modules: 34.
|
|
|
161
162
|
### pdp-ingredients
|
|
162
163
|
|
|
163
164
|
- Category: pdp
|
|
164
|
-
- What:
|
|
165
|
+
- What: PDP ingredients block (sources from product metafield list.metaobject).
|
|
165
166
|
- Files: sections:1
|
|
166
167
|
- Config: eyebrow, heading, lede, background_color, text_color
|
|
167
168
|
|
|
168
169
|
### pdp-journal-link
|
|
169
170
|
|
|
170
171
|
- Category: pdp
|
|
171
|
-
- What:
|
|
172
|
+
- What: PDP journal link.
|
|
172
173
|
- Files: sections:1
|
|
173
174
|
- Config: eyebrow, article, background_color, text_color, fallback_title, fallback_body, fallback_link_label, fallback_link_url
|
|
174
175
|
|
|
175
176
|
### pdp-trust-checks
|
|
176
177
|
|
|
177
178
|
- Category: pdp
|
|
178
|
-
- What:
|
|
179
|
+
- What: PDP trust.
|
|
179
180
|
- Files: sections:1
|
|
180
181
|
- Config: eyebrow, heading, lede, background_color, text_color, link_label, link_url
|
|
181
182
|
|
|
@@ -191,7 +192,7 @@ Total modules: 34.
|
|
|
191
192
|
### press-bar
|
|
192
193
|
|
|
193
194
|
- Category: social-proof
|
|
194
|
-
- What:
|
|
195
|
+
- What: press bar.
|
|
195
196
|
- Files: sections:1
|
|
196
197
|
- Config: eyebrow, background_color, text_color
|
|
197
198
|
|
|
@@ -219,21 +220,21 @@ Total modules: 34.
|
|
|
219
220
|
### recently-viewed
|
|
220
221
|
|
|
221
222
|
- Category: pdp
|
|
222
|
-
- What:
|
|
223
|
+
- What: recently viewed.
|
|
223
224
|
- Files: sections:1 assets:1
|
|
224
225
|
- Config: eyebrow, heading, background_color
|
|
225
226
|
|
|
226
227
|
### reviews
|
|
227
228
|
|
|
228
229
|
- Category: social-proof
|
|
229
|
-
- What:
|
|
230
|
+
- What: PDP reviews.
|
|
230
231
|
- Files: sections:1
|
|
231
232
|
- Config: heading
|
|
232
233
|
|
|
233
234
|
### risk-reversal
|
|
234
235
|
|
|
235
236
|
- Category: conversion
|
|
236
|
-
- What:
|
|
237
|
+
- What: risk reversal.
|
|
237
238
|
- Files: sections:1
|
|
238
239
|
- Config: icon, heading, body, link_label, link_url, background_color, text_color
|
|
239
240
|
|
|
@@ -247,7 +248,7 @@ Total modules: 34.
|
|
|
247
248
|
### shipping-bar
|
|
248
249
|
|
|
249
250
|
- Category: conversion
|
|
250
|
-
- What:
|
|
251
|
+
- What: shipping bar.
|
|
251
252
|
- Files: sections:1
|
|
252
253
|
- Config: threshold_cents, message_below, message_qualified, message_default, background_color, text_color
|
|
253
254
|
|
|
@@ -292,7 +293,7 @@ Total modules: 34.
|
|
|
292
293
|
### trust-badges
|
|
293
294
|
|
|
294
295
|
- Category: social-proof
|
|
295
|
-
- What:
|
|
296
|
+
- What: trust badges.
|
|
296
297
|
- Files: sections:1
|
|
297
298
|
- Config: background_color, text_color
|
|
298
299
|
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"config": {
|
|
10
10
|
"schema": {
|
|
11
11
|
"icon": { "type": "string", "default": "✓", "label": "Bullet icon (HTML entity or emoji)" },
|
|
12
|
-
"usp_1": { "type": "string", "default": "
|
|
13
|
-
"usp_2": { "type": "string", "default": "
|
|
14
|
-
"usp_3": { "type": "string", "default": "
|
|
12
|
+
"usp_1": { "type": "string", "default": "Free shipping over $50" },
|
|
13
|
+
"usp_2": { "type": "string", "default": "30-day satisfaction guarantee" },
|
|
14
|
+
"usp_3": { "type": "string", "default": "Secure checkout" }
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"variants": {
|
|
17
17
|
"feature-columns": {
|
|
18
|
-
"description": "Theme Editor blocks are columns (e.g.
|
|
18
|
+
"description": "Theme Editor blocks are columns (e.g. Brand pick / Generic / Competitor). Rows come from a textarea (pipe-delimited). Best when comparing many products across the same attributes.",
|
|
19
19
|
"files": {
|
|
20
20
|
"sections": ["variants/feature-columns/sections/runwell-comparison-table.liquid"]
|
|
21
21
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "delivery-estimate",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "pdp",
|
|
5
|
-
"description": "delivery-estimate
|
|
5
|
+
"description": "PDP delivery-date estimate (JS-rendered date range based on cutoff time and standard transit).",
|
|
6
6
|
"files": {
|
|
7
7
|
"snippets": [
|
|
8
8
|
"snippets/runwell-delivery-estimate.liquid"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "editorial-block",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "storefront",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Editorial introduction block: eyebrow + serif italic heading + lede + body. Native replacement for Dawn rich-text section with brand-aware typography.",
|
|
6
6
|
"files": {
|
|
7
7
|
"sections": [
|
|
8
8
|
"sections/runwell-editorial-block.liquid"
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
},
|
|
18
18
|
"heading": {
|
|
19
19
|
"type": "string",
|
|
20
|
-
"default": "
|
|
20
|
+
"default": "Why we built this.",
|
|
21
21
|
"label": "Heading"
|
|
22
22
|
},
|
|
23
23
|
"lede": {
|
|
24
24
|
"type": "string",
|
|
25
|
-
"default": "
|
|
25
|
+
"default": "Brief introduction text. Replace with your brand voice.",
|
|
26
26
|
"label": "Lede"
|
|
27
27
|
},
|
|
28
28
|
"background_color": {
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"name": "editorial-hero",
|
|
3
3
|
"version": "0.2.0",
|
|
4
4
|
"category": "storefront",
|
|
5
|
-
"description": "Editorial hero section. Two variants: video-bg (default) renders a full-bleed background video with eyebrow + heading + subhead + dual CTA, ideal for
|
|
5
|
+
"description": "Editorial hero section. Two variants: video-bg (default) renders a full-bleed background video with eyebrow + heading + subhead + dual CTA, ideal for editorial brands; hero-with-card renders a background image with a centered text card (Hero with Card pattern), ideal for product-led brands. Replaces Dawn's image-banner / video-banner.",
|
|
6
6
|
"default_variant": "video-bg",
|
|
7
7
|
"config": {
|
|
8
8
|
"schema": {
|
|
9
|
-
"eyebrow": { "type": "string", "default": "
|
|
10
|
-
"heading": { "type": "string", "default": "
|
|
11
|
-
"subheading": { "type": "string", "default": "
|
|
9
|
+
"eyebrow": { "type": "string", "default": "Brand eyebrow", "label": "Eyebrow text" },
|
|
10
|
+
"heading": { "type": "string", "default": "Tagline goes here.", "label": "Headline" },
|
|
11
|
+
"subheading": { "type": "string", "default": "Short brand subhead. Replace with your value proposition.", "label": "Subheading" }
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"variants": {
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"poster_image": { "type": "string", "label": "Poster image (and fallback)" },
|
|
23
23
|
"fallback_asset": { "type": "string", "label": "Bundled fallback asset filename (e.g. lushi-hero-bg.jpg)" },
|
|
24
24
|
"min_height": { "type": "number", "default": 100, "label": "Hero min-height (vh)" },
|
|
25
|
-
"button_label_primary": { "type": "string", "default": "Shop
|
|
25
|
+
"button_label_primary": { "type": "string", "default": "Shop now", "label": "Primary button label" },
|
|
26
26
|
"button_link_primary": { "type": "string", "label": "Primary button link" },
|
|
27
|
-
"button_label_secondary": { "type": "string", "default": "
|
|
27
|
+
"button_label_secondary": { "type": "string", "default": "Learn more", "label": "Secondary button label" },
|
|
28
28
|
"button_link_secondary": { "type": "string", "label": "Secondary button link" }
|
|
29
29
|
}
|
|
30
30
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "exit-intent",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "conversion",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Exit-intent popup with email capture. Replaces Privy / Justuno display layer.",
|
|
6
6
|
"files": {
|
|
7
7
|
"sections": [
|
|
8
8
|
"sections/runwell-exit-intent.liquid"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"lede": {
|
|
27
27
|
"type": "string",
|
|
28
|
-
"default": "
|
|
28
|
+
"default": "Subscribe for early access and exclusive offers.",
|
|
29
29
|
"label": "Lede"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/modules/faq/module.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "faq",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "storefront",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "FAQ accordion. Native replacement for FAQ widgets in Vitals and similar apps.",
|
|
6
6
|
"files": {
|
|
7
7
|
"sections": [
|
|
8
8
|
"sections/runwell-faq.liquid"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "how-it-works",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "storefront",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "How-it-works section: side-image + multi-step explainer.",
|
|
6
6
|
"files": {
|
|
7
7
|
"sections": [
|
|
8
8
|
"sections/runwell-how-it-works.liquid"
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"schema": {
|
|
13
13
|
"eyebrow": {
|
|
14
14
|
"type": "string",
|
|
15
|
-
"default": "How
|
|
15
|
+
"default": "How it works",
|
|
16
16
|
"label": "Eyebrow"
|
|
17
17
|
},
|
|
18
18
|
"heading": {
|
|
19
19
|
"type": "string",
|
|
20
|
-
"default": "
|
|
20
|
+
"default": "How we work.",
|
|
21
21
|
"label": "Heading"
|
|
22
22
|
},
|
|
23
23
|
"lede": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "inventory-urgency",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "pdp",
|
|
5
|
-
"description": "inventory
|
|
5
|
+
"description": "PDP inventory urgency message (Only N left) when stock tracking is enabled and quantity is low.",
|
|
6
6
|
"files": {
|
|
7
7
|
"snippets": [
|
|
8
8
|
"snippets/runwell-inventory-urgency.liquid"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"config": {
|
|
10
10
|
"schema": {
|
|
11
|
-
"tier_0_name": { "type": "string", "default": "
|
|
11
|
+
"tier_0_name": { "type": "string", "default": "Member" },
|
|
12
12
|
"tier_1_name": { "type": "string", "default": "Insider" },
|
|
13
13
|
"tier_1_threshold": { "type": "number", "default": 250 },
|
|
14
14
|
"tier_1_perk_pct": { "type": "number", "default": 10 },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "pdp-ingredients",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "pdp",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "PDP ingredients block (sources from product metafield list.metaobject).",
|
|
6
6
|
"files": {
|
|
7
7
|
"sections": [
|
|
8
8
|
"sections/runwell-ingredients.liquid"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "pdp-journal-link",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "pdp",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "PDP journal link.",
|
|
6
6
|
"files": {
|
|
7
7
|
"sections": [
|
|
8
8
|
"sections/runwell-pdp-journal.liquid"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"schema": {
|
|
13
13
|
"eyebrow": {
|
|
14
14
|
"type": "string",
|
|
15
|
-
"default": "
|
|
15
|
+
"default": "Editorial",
|
|
16
16
|
"label": "Eyebrow"
|
|
17
17
|
},
|
|
18
18
|
"article": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"fallback_body": {
|
|
38
38
|
"type": "string",
|
|
39
|
-
"default": "Every product
|
|
39
|
+
"default": "Every product pairs with a journal article. Browse the full archive to dig deeper.",
|
|
40
40
|
"label": "Fallback body"
|
|
41
41
|
},
|
|
42
42
|
"fallback_link_label": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "pdp-trust-checks",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"category": "pdp",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "PDP trust.",
|
|
6
6
|
"files": {
|
|
7
7
|
"sections": [
|
|
8
8
|
"sections/runwell-pdp-trust.liquid"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"lede": {
|
|
24
24
|
"type": "string",
|
|
25
|
-
"default": "We do not stock everything. Every product
|
|
25
|
+
"default": "We do not stock everything. Every product has to clear these four checks before it gets a page.",
|
|
26
26
|
"label": "Lede"
|
|
27
27
|
},
|
|
28
28
|
"background_color": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"variants": {
|
|
16
16
|
"grid-quotes": {
|
|
17
|
-
"description": "3 to 6 customer quote cards in a responsive grid. Each block is a quote with author + context + 0-5 star rating. Best for editorial
|
|
17
|
+
"description": "3 to 6 customer quote cards in a responsive grid. Each block is a quote with author + context + 0-5 star rating. Best for editorial brands.",
|
|
18
18
|
"files": {
|
|
19
19
|
"sections": ["variants/grid-quotes/sections/runwell-testimonials.liquid"]
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runwell/shopify-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Reusable Shopify theme modules from Runwell. Replaces typically app-driven features (reviews, wishlist, urgency, FAQ, post-purchase upsell, exit popups, free-ship progress, sticky ATC, testimonials, badges, bundles) with native Liquid + JS + CSS that ship across multiple client themes via a config-driven sync CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -42,4 +42,4 @@
|
|
|
42
42
|
"type": "git",
|
|
43
43
|
"url": "https://github.com/louayelbiche/runwell-shopify-toolkit.git"
|
|
44
44
|
}
|
|
45
|
-
}
|
|
45
|
+
}
|