@runwell/shopify-toolkit 0.14.3 → 0.15.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/lib/diff-baseline.js
CHANGED
|
@@ -26,10 +26,6 @@ export async function diffBaseline(flags) {
|
|
|
26
26
|
process.exit(1);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
console.log(`current: ${currentPin}`);
|
|
30
|
-
console.log(`target: ${targetPin}`);
|
|
31
|
-
console.log('');
|
|
32
|
-
|
|
33
29
|
// Resolve both versions
|
|
34
30
|
const currentRoot = resolveBaseline(flags, config);
|
|
35
31
|
const targetRoot = resolveBaseline({}, { baseline: targetPin });
|
|
@@ -45,6 +41,16 @@ export async function diffBaseline(flags) {
|
|
|
45
41
|
const currentMan = loadBaselineManifest(currentRoot);
|
|
46
42
|
const targetMan = loadBaselineManifest(targetRoot);
|
|
47
43
|
|
|
44
|
+
// Disclose actual resolution. Especially important when --baseline-path
|
|
45
|
+
// or config.baseline_path overrides the npm pin: the operator should see
|
|
46
|
+
// which versions are actually being compared, not just the pin strings.
|
|
47
|
+
const currentSource = describeSource(flags, config, currentRoot);
|
|
48
|
+
console.log(`current pin in config: ${currentPin}`);
|
|
49
|
+
console.log(`current resolved: ${currentMan.name}@${currentMan.version} (${currentSource})`);
|
|
50
|
+
console.log(`target pin: ${targetPin}`);
|
|
51
|
+
console.log(`target resolved: ${targetMan.name}@${targetMan.version} (npm cache)`);
|
|
52
|
+
console.log('');
|
|
53
|
+
|
|
48
54
|
// Compare owned files
|
|
49
55
|
const currentFiles = enumerateOwned(currentMan);
|
|
50
56
|
const targetFiles = enumerateOwned(targetMan);
|
|
@@ -129,3 +135,9 @@ function enumerateOwned(manifest) {
|
|
|
129
135
|
function fileHash(p) {
|
|
130
136
|
return crypto.createHash('sha1').update(fs.readFileSync(p)).digest('hex');
|
|
131
137
|
}
|
|
138
|
+
|
|
139
|
+
function describeSource(flags, config, resolvedRoot) {
|
|
140
|
+
if (flags && flags.baselinePath) return `--baseline-path ${flags.baselinePath}`;
|
|
141
|
+
if (config && config.baseline_path) return `config.baseline_path ${path.resolve(config.baseline_path)}`;
|
|
142
|
+
return `npm cache: ${resolvedRoot}`;
|
|
143
|
+
}
|
package/lib/qa.js
CHANGED
|
@@ -19,7 +19,29 @@ import { loadConfig, loadModuleManifest, resolveVariant } from './config-loader.
|
|
|
19
19
|
Caller can skip stages with --skip <name,name>.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
const STAGES = ['validate-config', 'doctor', 'template-integrity', 'orphan-assets', 'theme-check'];
|
|
22
|
+
const STAGES = ['validate-config', 'doctor', 'template-integrity', 'orphan-assets', 'brand-fields', 'theme-check'];
|
|
23
|
+
|
|
24
|
+
/* Walk the toolkit modules tree and collect every {{brand.X}} reference.
|
|
25
|
+
Returns the set of unique X identifiers (e.g. 'primary', 'rain-forrest',
|
|
26
|
+
'support_email'). Used by the brand-fields QA stage to verify each
|
|
27
|
+
tenant config defines all referenced brand keys. */
|
|
28
|
+
function collectBrandFieldReferences(modulesDir) {
|
|
29
|
+
const refs = new Set();
|
|
30
|
+
const re = /\{\{\s*brand\.([\w.-]+)\s*\}\}/g;
|
|
31
|
+
function walk(dir) {
|
|
32
|
+
if (!fs.existsSync(dir)) return;
|
|
33
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const full = path.join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) { walk(full); continue; }
|
|
36
|
+
if (!/\.(liquid|css|js|json)$/.test(entry.name)) continue;
|
|
37
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
38
|
+
let m;
|
|
39
|
+
while ((m = re.exec(content)) !== null) refs.add(m[1]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(modulesDir);
|
|
43
|
+
return refs;
|
|
44
|
+
}
|
|
23
45
|
|
|
24
46
|
export async function qa(flags) {
|
|
25
47
|
const targetDir = path.resolve(flags.target || process.cwd());
|
|
@@ -37,7 +59,7 @@ export async function qa(flags) {
|
|
|
37
59
|
|
|
38
60
|
// Stage 1: validate-config
|
|
39
61
|
if (!skip.has('validate-config')) {
|
|
40
|
-
console.log('1/
|
|
62
|
+
console.log('1/6 validate-config');
|
|
41
63
|
try {
|
|
42
64
|
const { config } = loadConfig(flags.config || './runwell.config.json');
|
|
43
65
|
if (!config.client) log('errors', 'validate-config', 'missing required field "client"');
|
|
@@ -60,7 +82,7 @@ export async function qa(flags) {
|
|
|
60
82
|
|
|
61
83
|
// Stage 2: doctor (manifest drift)
|
|
62
84
|
if (!skip.has('doctor')) {
|
|
63
|
-
console.log('2/
|
|
85
|
+
console.log('2/6 doctor (manifest drift)');
|
|
64
86
|
const manifestPath = path.join(targetDir, 'runwell-manifest.json');
|
|
65
87
|
if (!fs.existsSync(manifestPath)) {
|
|
66
88
|
log('errors', 'doctor', 'no runwell-manifest.json (run "runwell-shopify sync" first)');
|
|
@@ -81,7 +103,7 @@ export async function qa(flags) {
|
|
|
81
103
|
|
|
82
104
|
// Stage 3: template-integrity (every section in templates/*.json must exist)
|
|
83
105
|
if (!skip.has('template-integrity')) {
|
|
84
|
-
console.log('3/
|
|
106
|
+
console.log('3/6 template-integrity');
|
|
85
107
|
const tmplDir = path.join(targetDir, 'templates');
|
|
86
108
|
if (fs.existsSync(tmplDir)) {
|
|
87
109
|
const jsons = fs.readdirSync(tmplDir).filter(f => f.endsWith('.json'));
|
|
@@ -124,7 +146,7 @@ export async function qa(flags) {
|
|
|
124
146
|
|
|
125
147
|
// Stage 4: orphan-assets (runwell-* files in theme that aren't synced from any enabled module)
|
|
126
148
|
if (!skip.has('orphan-assets')) {
|
|
127
|
-
console.log('4/
|
|
149
|
+
console.log('4/6 orphan-assets');
|
|
128
150
|
const manifestPath = path.join(targetDir, 'runwell-manifest.json');
|
|
129
151
|
if (fs.existsSync(manifestPath)) {
|
|
130
152
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
@@ -148,9 +170,38 @@ export async function qa(flags) {
|
|
|
148
170
|
console.log('');
|
|
149
171
|
}
|
|
150
172
|
|
|
151
|
-
// Stage 5:
|
|
173
|
+
// Stage 5: brand-fields (every {{brand.X}} reference in toolkit modules must be defined in tenant config.brand)
|
|
174
|
+
if (!skip.has('brand-fields')) {
|
|
175
|
+
console.log('5/6 brand-fields');
|
|
176
|
+
try {
|
|
177
|
+
const { config } = loadConfig(flags.config || './runwell.config.json');
|
|
178
|
+
const brand = config.brand || {};
|
|
179
|
+
const refs = collectBrandFieldReferences(path.join(flags.toolkitRoot, 'modules'));
|
|
180
|
+
const missing = [];
|
|
181
|
+
for (const key of refs) {
|
|
182
|
+
const segments = key.split('.');
|
|
183
|
+
let val = brand;
|
|
184
|
+
for (const seg of segments) {
|
|
185
|
+
val = val == null ? undefined : val[seg];
|
|
186
|
+
}
|
|
187
|
+
if (val == null || val === '') missing.push(key);
|
|
188
|
+
}
|
|
189
|
+
if (missing.length === 0) {
|
|
190
|
+
log('info', 'brand-fields', `${refs.size} brand fields referenced; all defined`);
|
|
191
|
+
} else {
|
|
192
|
+
for (const key of missing) {
|
|
193
|
+
log('warnings', 'brand-fields', `brand.${key} is referenced by toolkit modules but missing in runwell.config.json brand block`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
log('warnings', 'brand-fields', `could not run check (${err.message})`);
|
|
198
|
+
}
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Stage 6: theme-check via Shopify CLI (parse --output json for accuracy)
|
|
152
203
|
if (!skip.has('theme-check')) {
|
|
153
|
-
console.log('
|
|
204
|
+
console.log('6/6 theme-check');
|
|
154
205
|
await new Promise(resolve => {
|
|
155
206
|
const proc = spawn('shopify', ['theme', 'check', '--path', targetDir, '--output', 'json'], {
|
|
156
207
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
Generated from runwell.config.json brand vars. Every other module
|
|
3
3
|
references these via var(--runwell-X). Do not hand-edit in client
|
|
4
4
|
themes; re-run runwell-shopify sync to update.
|
|
5
|
+
|
|
6
|
+
--runwell-tertiary is the canonical brand-tertiary token; the
|
|
7
|
+
--runwell-rain-forrest alias is preserved for one minor cycle of
|
|
8
|
+
backwards compatibility and will be removed in 0.16.0. Tenants
|
|
9
|
+
should reference --runwell-tertiary going forward and define
|
|
10
|
+
brand.tertiary in runwell.config.json.
|
|
5
11
|
*/
|
|
6
12
|
:root {
|
|
7
13
|
--runwell-primary: {{brand.primary}};
|
|
@@ -10,5 +16,6 @@
|
|
|
10
16
|
--runwell-oat: {{brand.oat}};
|
|
11
17
|
--runwell-celadon: {{brand.celadon}};
|
|
12
18
|
--runwell-blue: {{brand.blue}};
|
|
13
|
-
--runwell-
|
|
19
|
+
--runwell-tertiary: {{brand.tertiary}};
|
|
20
|
+
--runwell-rain-forrest: {{brand.tertiary}};
|
|
14
21
|
}
|
|
@@ -48,8 +48,20 @@
|
|
|
48
48
|
</ul>
|
|
49
49
|
{%- else -%}
|
|
50
50
|
<div class="runwell-reviews__empty">
|
|
51
|
-
{%- comment -%}
|
|
52
|
-
|
|
51
|
+
{%- comment -%}
|
|
52
|
+
brand.support_email is interpolated at sync time from runwell.config.json.
|
|
53
|
+
If the tenant has not set it, the literal string '{{brand.support_email}}'
|
|
54
|
+
survives the sync and is detected at render time so we fall back to
|
|
55
|
+
shop.email (Shopify admin) and finally shop.url. Liquid's `default` only
|
|
56
|
+
triggers on blank, not on the unsubstituted-template literal, hence
|
|
57
|
+
the explicit contains check.
|
|
58
|
+
{%- endcomment -%}
|
|
59
|
+
{%- assign brand_email = '{{brand.support_email}}' -%}
|
|
60
|
+
{%- if brand_email == blank or brand_email contains '{{' -%}
|
|
61
|
+
{%- assign reply_to = shop.email | default: shop.url -%}
|
|
62
|
+
{%- else -%}
|
|
63
|
+
{%- assign reply_to = brand_email -%}
|
|
64
|
+
{%- endif -%}
|
|
53
65
|
<p>No reviews on this one yet. If you have tried it, write us at <a href="mailto:{{ reply_to }}?subject=Review for {{ product.title | escape }}">{{ reply_to }}</a> and we will publish thoughtful reviews on the page.</p>
|
|
54
66
|
</div>
|
|
55
67
|
{%- endif -%}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runwell/shopify-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|