@runwell/shopify-toolkit 0.8.0 → 0.9.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/package.json +1 -1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runwell/shopify-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|