climaybe 3.0.6 → 3.0.8

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 CHANGED
@@ -108,7 +108,7 @@ If no alias is given, syncs to the default store.
108
108
 
109
109
  ### `climaybe ensure-branches` / `climaybe theme ensure-branches`
110
110
 
111
- Create missing `staging` and per-store branches (`staging-<alias>`, `live-<alias>`) from your current branch (usually `main`). Use when the repo only has `main` (e.g. after a fresh clone) so the main staging-&lt;store&gt; sync can run.
111
+ Create missing branches from your current branch (usually `main`). In single-store mode, this creates `staging` only. In multi-store mode, this creates `staging` plus per-store branches (`staging-<alias>`, `live-<alias>`). Use when the repo only has `main` (e.g. after a fresh clone) so the configured sync flow can run.
112
112
 
113
113
  ```bash
114
114
  npx climaybe ensure-branches
@@ -242,8 +242,11 @@ Enabled via `climaybe init` prompt (`Enable build + Lighthouse workflows?`; defa
242
242
  When enabled, builds are **resilient**:
243
243
  - If `_scripts/*.js` or `_styles/main.css` are missing, the build workflow **skips** those steps and continues.
244
244
  - `init` may offer to create entrypoints; **default answer is No**.
245
+ - Script bundling preserves comments/spacing and emits bundles only for root entry files (files imported by other top-level `_scripts/*.js` are inlined, not emitted separately).
246
+ - On `live-<alias>` branches only, script bundles are minified and overwrite `assets/*.js`; `main` and `staging-*` keep readable built JS.
247
+ - Live minified `assets/*` changes are intentionally excluded from hotfix backports to `main` (no branch-specific `.gitignore` split required).
245
248
 
246
- Build workflows run bundling via `npx -y climaybe@latest build-scripts` and Tailwind via `npx -y climaybe@latest build` (no per-repo build script is installed).
249
+ Build workflows install deps with `npm ci` and run `npx --no-install climaybe build-scripts` plus `npx --no-install climaybe build`, so CI uses lockfile-pinned versions (no `@latest` drift).
247
250
 
248
251
  | Workflow | Trigger | What it does |
249
252
  |----------|---------|-------------|
@@ -258,6 +261,9 @@ dev config defaults (`.theme-check.yml`, `.shopifyignore`, `.prettierrc`,
258
261
  `.lighthouserc.js`), writes `climaybe.config.json`, appends a managed `.gitignore` block, and optionally adds
259
262
  `.vscode/tasks.json` (default: yes) wired to run `climaybe` dev commands.
260
263
 
264
+ Local serve commands keep Theme Check disabled by default for faster startup. Enable it explicitly with
265
+ `climaybe serve --theme-check` or `climaybe serve:assets --theme-check`.
266
+
261
267
  You can create optional build entrypoints later with:
262
268
 
263
269
  `climaybe create-entrypoints`
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 3.0.6
1
+ 3.0.8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
4
4
  "description": "Shopify CLI by Electric Maybe for theme CI/CD workflows, branch orchestration, app setup, and dev tooling",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,19 +2,21 @@ import pc from 'picocolors';
2
2
  import { requireThemeProject } from '../lib/theme-guard.js';
3
3
  import { buildScripts } from '../lib/build-scripts.js';
4
4
 
5
- export async function buildScriptsCommand() {
5
+ export async function buildScriptsCommand(opts = {}) {
6
6
  console.log(pc.bold('\n climaybe — Build scripts\n'));
7
7
  if (!requireThemeProject()) return;
8
8
 
9
9
  try {
10
+ const minify = opts.minify === true;
10
11
  if (global.gc) global.gc();
11
- const { bundles } = buildScripts({ cwd: process.cwd() });
12
+ const { bundles } = buildScripts({ cwd: process.cwd(), minify });
12
13
  if (!bundles || bundles.length === 0) {
13
14
  console.log(pc.yellow(' No _scripts/*.js entrypoints found; nothing to build.'));
14
15
  return;
15
16
  }
16
17
  const totalFiles = bundles.reduce((sum, b) => sum + (b.fileCount || 0), 0);
17
- console.log(pc.green(` Scripts built (${bundles.length} bundle(s), ${totalFiles} files total)`));
18
+ const mode = minify ? 'minified' : 'readable';
19
+ console.log(pc.green(` Scripts built (${bundles.length} bundle(s), ${totalFiles} files total, ${mode})`));
18
20
  for (const b of bundles) {
19
21
  console.log(pc.dim(` - ${b.entryFile} → ${b.outputPath}`));
20
22
  }
@@ -11,8 +11,8 @@ import {
11
11
  } from '../lib/git.js';
12
12
 
13
13
  /**
14
- * Create missing staging and per-store branches from current HEAD.
15
- * Use when the repo only has main (e.g. after clone) so the main staging-<store> sync can run.
14
+ * Create missing staging (single-store) or staging + per-store branches (multi-store) from current HEAD.
15
+ * Use when the repo only has main (e.g. after clone) so the mode-appropriate sync flow can run.
16
16
  */
17
17
  export async function ensureBranchesCommand() {
18
18
  console.log(pc.bold('\n climaybe — Ensure Branches\n'));
@@ -39,9 +39,11 @@ export async function ensureBranchesCommand() {
39
39
 
40
40
  ensureStagingBranch();
41
41
  const branchesToPush = ['staging'];
42
- for (const alias of aliases) {
43
- createStoreBranches(alias);
44
- branchesToPush.push(`staging-${alias}`, `live-${alias}`);
42
+ if (mode === 'multi') {
43
+ for (const alias of aliases) {
44
+ createStoreBranches(alias);
45
+ branchesToPush.push(`staging-${alias}`, `live-${alias}`);
46
+ }
45
47
  }
46
48
 
47
49
  console.log(pc.bold(pc.green('\n Branches ensured.\n')));
@@ -125,7 +125,6 @@ async function runInitFlow() {
125
125
  // 5. Create branches
126
126
  if (mode === 'single') {
127
127
  ensureStagingBranch();
128
- createStoreBranches(stores[0].alias);
129
128
  } else {
130
129
  // Multi-store: staging branch + per-store branches
131
130
  ensureStagingBranch();
@@ -181,7 +180,6 @@ async function runInitFlow() {
181
180
 
182
181
  if (mode === 'single') {
183
182
  console.log(pc.dim(' Branches: main, staging'));
184
- console.log(pc.dim(` staging-${stores[0].alias}, live-${stores[0].alias}`));
185
183
  console.log(pc.dim(' Workflow: staging → main with versioning + nightly hotfix tagging'));
186
184
  } else {
187
185
  console.log(pc.dim(' Branches: main, staging'));
package/src/index.js CHANGED
@@ -61,20 +61,24 @@ function registerThemeCommands(cmd) {
61
61
 
62
62
  cmd
63
63
  .command('serve')
64
- .description('Run local theme dev (Shopify + assets + Theme Check)')
65
- .option('--no-theme-check', 'Disable Theme Check watcher')
66
- .action((opts) => serveAll({ includeThemeCheck: opts.themeCheck !== false }));
64
+ .description('Run local theme dev (Shopify + assets; Theme Check off by default)')
65
+ .option('--theme-check', 'Enable Theme Check watcher')
66
+ .action((opts) => serveAll({ includeThemeCheck: opts.themeCheck === true }));
67
67
  cmd.command('serve:shopify').description('Run Shopify theme dev server').action(() => serveShopify());
68
68
  cmd
69
69
  .command('serve:assets')
70
- .description('Run assets watch (Tailwind + scripts + Theme Check)')
71
- .option('--no-theme-check', 'Disable Theme Check watcher')
72
- .action((opts) => serveAssets({ includeThemeCheck: opts.themeCheck !== false }));
70
+ .description('Run assets watch (Tailwind + scripts; Theme Check off by default)')
71
+ .option('--theme-check', 'Enable Theme Check watcher')
72
+ .action((opts) => serveAssets({ includeThemeCheck: opts.themeCheck === true }));
73
73
 
74
74
  cmd.command('lint').description('Run theme linting (liquid, js, css)').action(() => lintAll());
75
75
 
76
76
  cmd.command('build').description('Build assets (Tailwind + scripts build)').action(() => buildAll());
77
- cmd.command('build-scripts').description('Build _scripts → assets/index.js').action(buildScriptsCommand);
77
+ cmd
78
+ .command('build-scripts')
79
+ .description('Build _scripts → assets/index.js')
80
+ .option('--minify', 'Minify output bundles')
81
+ .action(buildScriptsCommand);
78
82
  cmd
79
83
  .command('create-entrypoints')
80
84
  .description('Create _scripts/main.js and _styles/main.css (optional)')
@@ -41,7 +41,24 @@ function stripModuleSyntax(content) {
41
41
  return cleaned;
42
42
  }
43
43
 
44
- function processScriptFile({ scriptsDir, filePath, processedFiles }) {
44
+ function minifyScriptContent(content) {
45
+ let minified = content;
46
+ // Strip block comments first so line-level processing is simpler.
47
+ minified = minified.replace(/\/\*[\s\S]*?\*\//g, '');
48
+ // Strip line comments and trim lines.
49
+ minified = minified
50
+ .split('\n')
51
+ .map((line) => line.replace(/\/\/.*$/g, '').trim())
52
+ .filter(Boolean)
53
+ .join('\n');
54
+ // Collapse excessive whitespace around common tokens.
55
+ minified = minified.replace(/\s*([{}();,:=+\-*/<>[\]])\s*/g, '$1');
56
+ // Keep one space where token concatenation could break identifiers.
57
+ minified = minified.replace(/\b(const|let|var|function|class|return|if|for|while|switch|case|new)\s+/g, '$1 ');
58
+ return minified;
59
+ }
60
+
61
+ function processScriptFile({ scriptsDir, filePath, processedFiles, minify = false }) {
45
62
  if (processedFiles.has(filePath)) return '';
46
63
  processedFiles.add(filePath);
47
64
 
@@ -61,17 +78,11 @@ function processScriptFile({ scriptsDir, filePath, processedFiles }) {
61
78
  for (const importPath of imports) {
62
79
  const resolvedImport = resolveImportPath(filePath, importPath);
63
80
  if (!resolvedImport) continue;
64
- importedContent += processScriptFile({ scriptsDir, filePath: resolvedImport, processedFiles });
81
+ importedContent += processScriptFile({ scriptsDir, filePath: resolvedImport, processedFiles, minify });
65
82
  }
66
83
 
67
84
  content = stripModuleSyntax(content);
68
-
69
- if (process.env.NODE_ENV === 'production') {
70
- content = content.replace(/\/\*\*[\s\S]*?\*\//g, '');
71
- content = content.replace(/^\s*\*.*$/gm, '');
72
- content = content.replace(/console\.(log|warn|error)\([^)]*\);?\s*/g, '');
73
- content = content.replace(/^\s*\n/gm, '');
74
- }
85
+ if (minify) content = minifyScriptContent(content);
75
86
 
76
87
  return importedContent + '\n' + content;
77
88
  }
@@ -115,7 +126,7 @@ function outputNameForEntrypoint(entryFile) {
115
126
  return basename(entryFile);
116
127
  }
117
128
 
118
- function buildSingleEntrypoint({ cwd, entryFile }) {
129
+ function buildSingleEntrypoint({ cwd, entryFile, minify = false }) {
119
130
  const scriptsDir = join(cwd, '_scripts');
120
131
  const entryPath = join(scriptsDir, entryFile);
121
132
  if (!existsSync(entryPath)) {
@@ -123,8 +134,9 @@ function buildSingleEntrypoint({ cwd, entryFile }) {
123
134
  }
124
135
 
125
136
  const processedFiles = new Set();
126
- let finalContent = processScriptFile({ scriptsDir, filePath: entryFile, processedFiles });
137
+ let finalContent = processScriptFile({ scriptsDir, filePath: entryFile, processedFiles, minify });
127
138
  finalContent = stripModuleSyntax(finalContent);
139
+ if (minify) finalContent = minifyScriptContent(finalContent);
128
140
 
129
141
  const assetsDir = join(cwd, 'assets');
130
142
  mkdirSync(assetsDir, { recursive: true });
@@ -136,18 +148,28 @@ function buildSingleEntrypoint({ cwd, entryFile }) {
136
148
  return { entryFile, fileCount: processedFiles.size, outputPath };
137
149
  }
138
150
 
139
- export function buildScripts({ cwd = process.cwd(), entry = null } = {}) {
151
+ export function buildScripts({ cwd = process.cwd(), entry = null, minify = false } = {}) {
140
152
  const scriptsDir = join(cwd, '_scripts');
141
153
  let entrypoints = entry ? [entry.endsWith('.js') ? entry : `${entry}.js`] : listTopLevelEntrypoints(scriptsDir);
142
- if (!entry && entrypoints.includes('main.js')) {
143
- const importedByMain = collectImportedFiles({ scriptsDir, entryFile: 'main.js' });
144
- importedByMain.delete('main.js');
145
- entrypoints = entrypoints.filter((ep) => ep === 'main.js' || !importedByMain.has(ep));
154
+ if (!entry && entrypoints.length > 1) {
155
+ // Emit only root top-level scripts. If one top-level file is imported by another
156
+ // top-level file, it is bundled into the importer and should not be emitted alone.
157
+ const importedByTopLevel = new Set();
158
+ for (const ep of entrypoints) {
159
+ const imported = collectImportedFiles({ scriptsDir, entryFile: ep });
160
+ imported.delete(ep);
161
+ for (const file of imported) importedByTopLevel.add(file);
162
+ }
163
+
164
+ const rootEntrypoints = entrypoints.filter((ep) => !importedByTopLevel.has(ep));
165
+ if (rootEntrypoints.length > 0) {
166
+ entrypoints = rootEntrypoints;
167
+ }
146
168
  }
147
169
  if (entrypoints.length === 0) {
148
170
  return { bundles: [] };
149
171
  }
150
- const bundles = entrypoints.map((entryFile) => buildSingleEntrypoint({ cwd, entryFile }));
172
+ const bundles = entrypoints.map((entryFile) => buildSingleEntrypoint({ cwd, entryFile, minify }));
151
173
  return { bundles };
152
174
  }
153
175
 
@@ -217,7 +217,7 @@ export function serveShopify({ cwd = process.cwd() } = {}) {
217
217
  return runShopify(args, { cwd, name: 'shopify' });
218
218
  }
219
219
 
220
- export function serveAssets({ cwd = process.cwd(), includeThemeCheck = true } = {}) {
220
+ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } = {}) {
221
221
  printServeStartupHeader();
222
222
  const env = { ...process.env, NODE_ENV: 'production' };
223
223
  const styleEntrypoint = join(cwd, '_styles', 'main.css');
@@ -312,7 +312,7 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = true } =
312
312
  return { tailwind, devMcp, scriptsWatch, themeCheckWatch, cleanup };
313
313
  }
314
314
 
315
- export function serveAll({ cwd = process.cwd(), includeThemeCheck = true } = {}) {
315
+ export function serveAll({ cwd = process.cwd(), includeThemeCheck = false } = {}) {
316
316
  // Start assets first, then bring up Shopify after a short delay.
317
317
  const assets = serveAssets({ cwd, includeThemeCheck });
318
318
  let shopify = null;
@@ -67,13 +67,6 @@ function processScriptFile(filePath, processedFiles = new Set()) {
67
67
 
68
68
  content = stripModuleSyntax(content);
69
69
 
70
- if (process.env.NODE_ENV === 'production') {
71
- content = content.replace(/\/\*\*[\s\S]*?\*\//g, '');
72
- content = content.replace(/^\s*\*.*$/gm, '');
73
- content = content.replace(/console\.(log|warn|error)\([^)]*\);?\s*/g, '');
74
- content = content.replace(/^\s*\n/gm, '');
75
- }
76
-
77
70
  return importedContent + '\n' + content;
78
71
  }
79
72
 
@@ -23,6 +23,14 @@ jobs:
23
23
  with:
24
24
  node-version: "24"
25
25
 
26
+ - name: Install dependencies from lockfile
27
+ run: |
28
+ if [ ! -f "package-lock.json" ]; then
29
+ echo "package-lock.json is required for deterministic build workflow installs."
30
+ exit 1
31
+ fi
32
+ npm ci
33
+
26
34
  - name: Detect build entrypoints
27
35
  id: detect
28
36
  run: |
@@ -36,16 +44,27 @@ jobs:
36
44
  fi
37
45
  echo "has_scripts=$HAS_SCRIPTS" >> $GITHUB_OUTPUT
38
46
  echo "has_styles=$HAS_STYLES" >> $GITHUB_OUTPUT
47
+ BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
48
+ if [[ "$BRANCH_NAME" == live-* ]]; then
49
+ echo "scripts_minify=true" >> $GITHUB_OUTPUT
50
+ else
51
+ echo "scripts_minify=false" >> $GITHUB_OUTPUT
52
+ fi
39
53
 
40
54
  - name: Build scripts
41
55
  if: steps.detect.outputs.has_scripts == 'true'
42
- run: npx -y climaybe@latest build-scripts
56
+ run: |
57
+ if [ "${{ steps.detect.outputs.scripts_minify }}" = "true" ]; then
58
+ npx --no-install climaybe build-scripts --minify
59
+ else
60
+ npx --no-install climaybe build-scripts
61
+ fi
43
62
 
44
63
  - name: Run Tailwind build
45
64
  id: build
46
65
  run: |
47
66
  if [ "${{ steps.detect.outputs.has_styles }}" = "true" ]; then
48
- NODE_ENV=production npx -y climaybe@latest build
67
+ NODE_ENV=production npx --no-install climaybe build
49
68
  else
50
69
  echo "No _styles/main.css found; skipping Tailwind build."
51
70
  fi
@@ -92,11 +92,12 @@ jobs:
92
92
  exit 0
93
93
  fi
94
94
 
95
- # Default store: commits outside stores/ (root + code) trigger backport.
95
+ # Default store: commits outside stores/ and assets/ trigger backport.
96
+ # Live-branch minified assets should never backport into main.
96
97
  # Non-default store: only commits under stores/<alias>/ trigger backport; root JSON must not overwrite main.
97
98
  if [ -n "$DEFAULT_ALIAS" ] && [ "$SOURCE_ALIAS" = "$DEFAULT_ALIAS" ]; then
98
- COMMITS=$(git log --oneline ${MERGE_BASE}..origin/$SOURCE -- . ':!stores/' 2>/dev/null | \
99
- grep -v "\[stores-to-root\]" | grep -v "\[root-to-stores\]" | grep -v "chore(release)" || true)
99
+ COMMITS=$(git log --oneline ${MERGE_BASE}..origin/$SOURCE -- . ':!stores/' ':!assets/' 2>/dev/null | \
100
+ grep -v "\[stores-to-root\]" | grep -v "\[root-to-stores\]" | grep -v "chore(release)" | grep -v "chore(assets)" || true)
100
101
  else
101
102
  COMMITS=$(git log --oneline ${MERGE_BASE}..origin/$SOURCE -- "stores/${SOURCE_ALIAS}/" 2>/dev/null | \
102
103
  grep -v "\[stores-to-root\]" | grep -v "\[root-to-stores\]" | grep -v "chore(release)" || true)