@runwell/shopify-toolkit 0.14.2 → 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.
@@ -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/5 validate-config');
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/5 doctor (manifest drift)');
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/5 template-integrity');
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/5 orphan-assets');
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: theme-check via Shopify CLI (parse --output json for accuracy)
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('5/5 theme-check');
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-rain-forrest: {{brand.rain-forrest}};
19
+ --runwell-tertiary: {{brand.tertiary}};
20
+ --runwell-rain-forrest: {{brand.tertiary}};
14
21
  }
@@ -30,7 +30,7 @@
30
30
  {%- endif -%}
31
31
 
32
32
  <div style="overflow-x: auto;">
33
- <table style="width: 100%; border-collapse: collapse; min-width: 600px; font-family: var(--font-body-family); font-size: var(--runwell-meta-size);">
33
+ <table style="width: 100%; border-collapse: collapse; min-width: 600px; font-family: var(--font-body-family); font-size: var(--runwell-body-size);">
34
34
  <thead>
35
35
  <tr>
36
36
  <th style="text-align: left; padding: 0.8rem 1rem 0.8rem 0; font-family: var(--font-body-family); font-size: var(--runwell-eyebrow-size); font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; opacity: 0.6; vertical-align: bottom; border-bottom: 2px solid currentColor;">
@@ -48,13 +48,13 @@
48
48
  </h3>
49
49
  {%- endif -%}
50
50
  {%- if block.settings.body != blank -%}
51
- <p style="font-size: var(--runwell-meta-size); line-height: 1.6; opacity: 0.85; margin: 0;">
51
+ <p style="font-size: var(--runwell-body-size); line-height: 1.6; opacity: 0.85; margin: 0;">
52
52
  {{ block.settings.body }}
53
53
  </p>
54
54
  {%- endif -%}
55
55
  {%- if block.settings.link_label != blank -%}
56
56
  <a href="{{ block.settings.link_url | default: '#' }}"
57
- style="display: inline-block; margin-top: 0.8rem; font-weight: 700; font-size: var(--runwell-caption-size); text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--runwell-blue);">
57
+ style="display: inline-block; margin-top: 0.8rem; font-weight: 700; font-size: var(--runwell-cta-size); text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--runwell-blue);">
58
58
  {{ block.settings.link_label }} &rarr;
59
59
  </a>
60
60
  {%- endif -%}
@@ -44,7 +44,7 @@
44
44
  {{ ing.name }}
45
45
  </h3>
46
46
  {%- if ing.role != blank -%}
47
- <p style="font-family: var(--font-body-family); font-size: var(--runwell-meta-size); line-height: 1.6; opacity: 0.85; margin: 0;">
47
+ <p style="font-family: var(--font-body-family); font-size: var(--runwell-body-size); line-height: 1.6; opacity: 0.85; margin: 0;">
48
48
  {{ ing.role }}
49
49
  </p>
50
50
  {%- endif -%}
@@ -62,7 +62,7 @@
62
62
  {{ block.settings.name }}
63
63
  </h3>
64
64
  {%- if block.settings.role != blank -%}
65
- <p style="font-family: var(--font-body-family); font-size: var(--runwell-meta-size); line-height: 1.6; opacity: 0.85; margin: 0;">
65
+ <p style="font-family: var(--font-body-family); font-size: var(--runwell-body-size); line-height: 1.6; opacity: 0.85; margin: 0;">
66
66
  {{ block.settings.role }}
67
67
  </p>
68
68
  {%- endif -%}
@@ -36,7 +36,7 @@
36
36
  <h3 style="font-family: var(--font-heading-family); font-style: italic; font-weight: 400; font-size: calc(var(--font-heading-scale) * 1.25rem); line-height: 1.2; margin: 0 0 0.5rem 0;">
37
37
  {{ block.settings.title }}
38
38
  </h3>
39
- <p style="font-family: var(--font-body-family); font-size: var(--runwell-meta-size); line-height: 1.6; opacity: 0.85; margin: 0;">
39
+ <p style="font-family: var(--font-body-family); font-size: var(--runwell-body-size); line-height: 1.6; opacity: 0.85; margin: 0;">
40
40
  {{ block.settings.body }}
41
41
  </p>
42
42
  </div>
@@ -46,7 +46,7 @@
46
46
  {%- if section.settings.link_label != blank -%}
47
47
  <div style="margin-top: 2.5rem;">
48
48
  <a href="{{ section.settings.link_url | default: '/pages/standards' }}"
49
- style="font-family: var(--font-body-family); font-weight: 700; font-size: var(--runwell-caption-size); letter-spacing: 0.04em; text-transform: uppercase; text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--runwell-blue);">
49
+ style="font-family: var(--font-body-family); font-weight: 700; font-size: var(--runwell-cta-size); letter-spacing: 0.04em; text-transform: uppercase; text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--runwell-blue);">
50
50
  {{ section.settings.link_label }} &rarr;
51
51
  </a>
52
52
  </div>
@@ -48,8 +48,20 @@
48
48
  </ul>
49
49
  {%- else -%}
50
50
  <div class="runwell-reviews__empty">
51
- {%- comment -%}brand.support_email is interpolated at sync time from runwell.config.json; falls back to Shopify admin shop.email if the brand var is unset.{%- endcomment -%}
52
- {%- assign reply_to = '{{brand.support_email}}' | default: shop.email | default: shop.url -%}
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 -%}
@@ -22,13 +22,13 @@
22
22
  </h3>
23
23
  {%- endif -%}
24
24
  {%- if section.settings.body != blank -%}
25
- <p style="font-family: var(--font-body-family); font-size: var(--runwell-meta-size); line-height: 1.55; margin: 0; opacity: 0.85;">
25
+ <p style="font-family: var(--font-body-family); font-size: var(--runwell-body-size); line-height: 1.55; margin: 0; opacity: 0.85;">
26
26
  {{ section.settings.body }}
27
27
  </p>
28
28
  {%- endif -%}
29
29
  {%- if section.settings.link_label != blank -%}
30
30
  <a href="{{ section.settings.link_url | default: '/policies/refund-policy' }}"
31
- style="display: inline-block; margin-top: 0.5rem; font-family: var(--font-body-family); font-weight: 700; font-size: var(--runwell-caption-size); letter-spacing: 0.04em; text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--runwell-blue);">
31
+ style="display: inline-block; margin-top: 0.5rem; font-family: var(--font-body-family); font-weight: 700; font-size: var(--runwell-cta-size); letter-spacing: 0.04em; text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--runwell-blue);">
32
32
  {{ section.settings.link_label }} &rarr;
33
33
  </a>
34
34
  {%- endif -%}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runwell/shopify-toolkit",
3
- "version": "0.14.2",
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",