climaybe 3.2.0 → 3.4.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/README.md +16 -8
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/build-scripts.js +7 -1
- package/src/cursor/rules/00-rule-index.mdc +1 -0
- package/src/cursor/rules/javascript-standards.mdc +3 -1
- package/src/cursor/rules/js-refactor-tasks.mdc +1 -1
- package/src/cursor/rules/project-overview.mdc +20 -2
- package/src/cursor/rules/tailwindcss-rules.mdc +2 -1
- package/src/index.js +6 -2
- package/src/lib/build-scripts.js +36 -3
- package/src/lib/config.js +17 -0
- package/src/lib/dev-runtime.js +39 -9
- package/src/lib/prompts.js +22 -0
- package/src/lib/serve-multi-store.js +81 -0
- package/src/workflows/preview/cleanup-orphan-preview-themes.yml +59 -0
- package/src/workflows/preview/pr-close.yml +103 -62
- package/src/workflows/preview/pr-update.yml +87 -76
- package/src/workflows/preview/reusable-cleanup-themes.yml +91 -6
- package/src/workflows/preview/reusable-comment-on-pr.yml +83 -19
- package/src/workflows/preview/reusable-publish-pr-preview-store.yml +162 -0
package/README.md
CHANGED
|
@@ -94,7 +94,11 @@ Switch your local dev environment to a specific store (multi-store only).
|
|
|
94
94
|
npx climaybe switch voldt-norway
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Copies `stores/<alias>/` JSON files to the repo root so you can preview that store locally, and sets `default_store` in `climaybe.config.json` so `climaybe serve` / `shopify theme dev` targets that store. If your current git branch is `staging-<alias>` or `live-<alias>`, `climaybe serve`
|
|
97
|
+
Copies `stores/<alias>/` JSON files to the repo root so you can preview that store locally, and sets `default_store` in `climaybe.config.json` so `climaybe serve` / `shopify theme dev` targets that store. If your current git branch is `staging-<alias>` or `live-<alias>`, `climaybe serve` still passes that branch’s store to Shopify for `--store` (even if `default_store` differs), matching CI preview behavior.
|
|
98
|
+
|
|
99
|
+
### `climaybe serve` / `climaybe theme serve`
|
|
100
|
+
|
|
101
|
+
Runs Tailwind/scripts/schema watchers and `shopify theme dev` (see the theme dev kit section for flags). In **multi-store** mode, the first step asks **which store to serve** (interactive terminal); the default choice is the store matching `default_store` (same as after `climaybe switch`). If you pick a **different** store, the CLI saves the current root `config/`, `templates/`, and `sections/` JSON files into `stores/<previous-alias>/` (same paths as `climaybe sync`), then copies the selected store’s JSONs to the root and updates `default_store`, so work in progress is not lost. In **CI** or when **stdin is not a TTY**, there is no prompt: it uses `default_store` unless you set **`CLIMAYBE_SERVE_STORE=<alias>`** to a configured alias.
|
|
98
102
|
|
|
99
103
|
### `climaybe sync [alias]` / `climaybe theme sync`
|
|
100
104
|
|
|
@@ -227,12 +231,14 @@ Enabled via `climaybe init` prompt (`Enable preview + cleanup workflows?`; defau
|
|
|
227
231
|
|
|
228
232
|
| Workflow | Trigger | What it does |
|
|
229
233
|
|----------|---------|-------------|
|
|
230
|
-
| `pr-update.yml` | PR opened/synchronize/reopened (base: main, staging, develop, staging-*, live-*) | Shares draft theme, renames with `-PR<number>`, comments preview + customize URLs
|
|
231
|
-
| `pr-close.yml` | PR closed (same branch set) | Deletes
|
|
232
|
-
| `
|
|
233
|
-
| `reusable-
|
|
234
|
-
| `reusable-
|
|
235
|
-
| `reusable-
|
|
234
|
+
| `pr-update.yml` | PR opened/synchronize/reopened (base: main, staging, develop, staging-*, live-*) | Shares draft theme, renames with `-PR<number>`, comments preview + customize URLs. **Multi-store + base not** `staging-<alias>` **or** `live-<alias>`**:** publishes to **every** configured store (matrix) and posts **all** links in one PR comment. For `staging-<alias>` / `live-<alias>` bases, only that store is used. **Path filter:** theme paths only (`assets/`, `blocks/`, `config/`, `layout/`, `locales/`, `sections/`, `snippets/`, `templates/`, `_scripts/`, `_styles/`, `shopify.theme.toml`, `stores/**`). |
|
|
235
|
+
| `pr-close.yml` | PR closed (same branch set) | Deletes this PR’s preview themes using the **same store matrix rule** as `pr-update`; PR comment shows **total** deleted count across stores. |
|
|
236
|
+
| `cleanup-orphan-preview-themes.yml` | Weekly (Mon 06:00 UTC) + `workflow_dispatch` | Per store: deletes themes ending with `-PR<n>` when PR `#n` is **not** open (merged/closed without cleanup). Uses `gh pr list` (limit 1000 open PRs). |
|
|
237
|
+
| `reusable-publish-pr-preview-store.yml` | workflow_call | Share + rename + upload comment fragment for **one** store (matrix leg in `pr-update`). |
|
|
238
|
+
| `reusable-share-theme.yml` | workflow_call | Shares Shopify draft theme and returns `theme_id` (still available if you call it elsewhere) |
|
|
239
|
+
| `reusable-rename-theme.yml` | workflow_call | Renames shared theme to include `PR<number>` (still available for custom flows) |
|
|
240
|
+
| `reusable-comment-on-pr.yml` | workflow_call | Posts preview comment; aggregates `preview-fragment-*` artifacts when `use_preview_fragments` is true |
|
|
241
|
+
| `reusable-cleanup-themes.yml` | workflow_call | `cleanup_mode: by_pr` deletes names ending with `-PR{padded}`; `orphan_pr` deletes `-PR<n>` when PR `n` is not open. Optional `result_artifact_prefix` for matrix fan-in on `pr-close` |
|
|
236
242
|
| `reusable-extract-pr-number.yml` | workflow_call | Extracts padded/unpadded PR number outputs for naming and API-safe usage |
|
|
237
243
|
|
|
238
244
|
### Optional build + Lighthouse package
|
|
@@ -244,6 +250,8 @@ When enabled, builds are **resilient**:
|
|
|
244
250
|
- `init` may offer to create entrypoints; **default answer is No**.
|
|
245
251
|
- 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
252
|
- Script bundles are written to `assets/*.js` (readable by default; use `climaybe build-scripts --minify` if you want minified output).
|
|
253
|
+
- `assets/*.js`, `assets/style.css`, and the injected `{% schema %}` blocks are **generated outputs** — edit the source in `_scripts/`, `_styles/`, and `_schemas/` instead, since the watcher/build regenerate (and overwrite) the outputs on save and in CI.
|
|
254
|
+
- **Orphan cleanup:** when `_scripts/` is in use, a full build (`climaybe build`, `climaybe build-scripts`, and the `serve` watcher) deletes any `assets/*.js` that no longer has a matching `_scripts/` source. Add new JS via a top-level `_scripts/<name>.js` entry, not by hand-editing `assets/`. Liquid-processed `*.js.liquid` assets and non-JS files are left untouched; targeted single-entry builds (`build-scripts <entry>`) skip pruning.
|
|
247
255
|
- Live minified `assets/*` changes are intentionally excluded from hotfix backports to `main` (no branch-specific `.gitignore` split required).
|
|
248
256
|
|
|
249
257
|
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).
|
|
@@ -374,7 +382,7 @@ Add the following secrets to your GitHub repository (or use **GitLab CI/CD varia
|
|
|
374
382
|
|
|
375
383
|
**Store URL:** During `climaybe init` (or `add-store`), store URL secret(s) are set from your configured store domain(s); theme tokens are optional prompts.
|
|
376
384
|
|
|
377
|
-
**Multi-store:** Per-store secrets `SHOPIFY_STORE_URL_<ALIAS>` and `SHOPIFY_THEME_ACCESS_TOKEN_<ALIAS>` — the URL is set from config; you must provide the theme token per store. `<ALIAS>` is uppercase with hyphens as underscores (e.g. `voldt-norway` → `SHOPIFY_STORE_URL_VOLDT_NORWAY`). Preview
|
|
385
|
+
**Multi-store:** Per-store secrets `SHOPIFY_STORE_URL_<ALIAS>` and `SHOPIFY_THEME_ACCESS_TOKEN_<ALIAS>` — the URL is set from config; you must provide the theme token per store. `<ALIAS>` is uppercase with hyphens as underscores (e.g. `voldt-norway` → `SHOPIFY_STORE_URL_VOLDT_NORWAY`). **Preview:** for PRs targeting **staging-<alias>** or **live-<alias>** only that store is used. For **main**, **staging**, **develop**, or other bases with **multiple** stores in `climaybe.config.json`, **pr-update** publishes the preview theme to **every** store (each must have secrets); **pr-close** cleans that PR’s preview themes on **all** stores. For a **single** store in config, behavior matches the default-store case. Optional **cleanup-orphan-preview-themes** (weekly + manual) removes stale `-PR<n>` themes when PR `n` is no longer open.
|
|
378
386
|
|
|
379
387
|
## Directory Structure (Multi-store)
|
|
380
388
|
|
package/bin/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.4.0
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ export async function buildScriptsCommand(opts = {}) {
|
|
|
9
9
|
try {
|
|
10
10
|
const minify = opts.minify === true;
|
|
11
11
|
if (global.gc) global.gc();
|
|
12
|
-
const { bundles } = buildScripts({ cwd: process.cwd(), minify });
|
|
12
|
+
const { bundles, removed } = buildScripts({ cwd: process.cwd(), minify });
|
|
13
13
|
if (!bundles || bundles.length === 0) {
|
|
14
14
|
console.log(pc.yellow(' No _scripts/*.js entrypoints found; nothing to build.'));
|
|
15
15
|
return;
|
|
@@ -20,6 +20,12 @@ export async function buildScriptsCommand(opts = {}) {
|
|
|
20
20
|
for (const b of bundles) {
|
|
21
21
|
console.log(pc.dim(` - ${b.entryFile} → ${b.outputPath}`));
|
|
22
22
|
}
|
|
23
|
+
if (removed && removed.length > 0) {
|
|
24
|
+
console.log(pc.yellow(` Removed ${removed.length} orphan asset(s) with no _scripts source:`));
|
|
25
|
+
for (const name of removed) {
|
|
26
|
+
console.log(pc.dim(` - assets/${name}`));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
23
29
|
if (global.gc) global.gc();
|
|
24
30
|
} catch (err) {
|
|
25
31
|
console.log(pc.red(` Build error: ${err.message}`));
|
|
@@ -8,6 +8,7 @@ alwaysApply: true
|
|
|
8
8
|
|
|
9
9
|
When you perform any of the following, **read and apply** the corresponding rule file from `.cursor/rules/`:
|
|
10
10
|
|
|
11
|
+
- **Editing `assets/*.js` or `assets/style.css`, build outputs vs source, where to edit JS/CSS** → `project-overview.mdc` (Build Outputs vs Source — edit `_scripts/`/`_styles/`, never the generated `assets/` files)
|
|
11
12
|
- **Git commits, commit messages** → `commit-rules.mdc`
|
|
12
13
|
- **Accessibility, a11y, focus, WCAG, UI behavior** → `accessibility-rules.mdc`
|
|
13
14
|
- **JavaScript, web components, _scripts/, *.js** → `javascript-standards.mdc`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Professional JavaScript standards for Electric Maybe Shopify Theme. All JavaScript files must follow these patterns for consistency, performance, and maintainability.
|
|
3
3
|
globs:
|
|
4
|
-
- "
|
|
4
|
+
- "_scripts/**/*.js"
|
|
5
5
|
- "sections/*.liquid"
|
|
6
6
|
- "snippets/*.liquid"
|
|
7
7
|
alwaysApply: false
|
|
@@ -9,6 +9,8 @@ alwaysApply: false
|
|
|
9
9
|
|
|
10
10
|
# JavaScript Development Standards
|
|
11
11
|
|
|
12
|
+
> **Source vs build output:** Edit JavaScript in `_scripts/`. The CLI bundles `_scripts/*.js` into `assets/*.js` (`main.js` → `assets/index.js`) on every save and in CI, so `assets/*.js` is a **generated artifact** — never edit it directly, your changes will be overwritten. See `project-overview.mdc` (Build Outputs vs Source).
|
|
13
|
+
|
|
12
14
|
## Core Principles
|
|
13
15
|
|
|
14
16
|
- **Zero external dependencies** - Use native browser APIs
|
|
@@ -55,8 +55,10 @@ Use `_scripts/electric-modal.js` as the gold standard for all web components.
|
|
|
55
55
|
|
|
56
56
|
## File Organization
|
|
57
57
|
```
|
|
58
|
-
_scripts/ #
|
|
59
|
-
|
|
58
|
+
_scripts/ # Source: web components and JS utilities (edit here)
|
|
59
|
+
_styles/ # Source: Tailwind/CSS entrypoint (_styles/main.css)
|
|
60
|
+
_schemas/ # Source: section/block schema definitions
|
|
61
|
+
assets/ # GENERATED build outputs + static assets (do not hand-edit generated files)
|
|
60
62
|
sections/ # Liquid section files
|
|
61
63
|
snippets/ # Reusable Liquid snippets
|
|
62
64
|
templates/ # Page templates
|
|
@@ -64,6 +66,22 @@ locales/ # Translation files
|
|
|
64
66
|
config/ # Theme settings
|
|
65
67
|
```
|
|
66
68
|
|
|
69
|
+
## Build Outputs vs Source
|
|
70
|
+
|
|
71
|
+
`assets/` contains both static assets and **generated** build outputs. The CLI (`climaybe serve` / `climaybe build`) compiles sources into `assets/` and regenerates them on every save and in CI. Always edit the **source**, never the generated output:
|
|
72
|
+
|
|
73
|
+
| Source (edit this) | Generated output (do NOT edit) |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `_scripts/main.js` | `assets/index.js` |
|
|
76
|
+
| `_scripts/<name>.js` (top-level entry) | `assets/<name>.js` |
|
|
77
|
+
| `_styles/main.css` | `assets/style.css` |
|
|
78
|
+
| `_schemas/<name>.js` / `.json` | injected `{% schema %}` block in `sections/*.liquid` & `blocks/*.liquid` |
|
|
79
|
+
|
|
80
|
+
Notes:
|
|
81
|
+
- JS bundling follows `import` statements: files imported by a top-level `_scripts/*.js` entry are **inlined** into that bundle, not emitted separately.
|
|
82
|
+
- Any manual edit to `assets/index.js`, `assets/*.js`, or `assets/style.css` is **overwritten** on the next save (watcher) or build. Make JS changes in `_scripts/`, CSS in `_styles/`, and schema changes in `_schemas/`.
|
|
83
|
+
- **Orphan cleanup:** on a full build (`climaybe build` / `climaybe build-scripts`, and the `serve` watcher), any `assets/*.js` that the current `_scripts/` build does not produce is **deleted** as a stale artifact. To add a new JS asset, create a top-level `_scripts/<name>.js` entry — do not drop a hand-written `.js` into `assets/` (it will be removed). Liquid-processed assets (`*.js.liquid`) and non-JS files are never touched.
|
|
84
|
+
|
|
67
85
|
## Code Review Requirements
|
|
68
86
|
All code must pass the checklist in `javascript-standards.mdc` before merge.
|
|
69
87
|
|
|
@@ -3,13 +3,14 @@ description: Tailwind CSS and theme token standards for Liquid and theme styles.
|
|
|
3
3
|
globs:
|
|
4
4
|
- "**/*.liquid"
|
|
5
5
|
- "_styles/**/*.css"
|
|
6
|
-
- "assets/*.css"
|
|
7
6
|
alwaysApply: false
|
|
8
7
|
---
|
|
9
8
|
# Tailwind CSS Development Rules
|
|
10
9
|
|
|
11
10
|
Apply when editing Liquid templates or theme CSS that use Tailwind classes or theme tokens. Use semantic tokens (surface, subtle, emphasis, accent, text-primary, border-secondary) from this project's `@theme`; see `_styles/02_base/02.02_colors.css`.
|
|
12
11
|
|
|
12
|
+
> **Source vs build output:** Edit CSS in `_styles/` (entrypoint `_styles/main.css`). The CLI compiles it into `assets/style.css` on every save and in CI, so `assets/style.css` is a **generated artifact** — never edit it directly, your changes will be overwritten. See `project-overview.mdc` (Build Outputs vs Source).
|
|
13
|
+
|
|
13
14
|
## Core Principles
|
|
14
15
|
|
|
15
16
|
### 1. Static Class Generation
|
package/src/index.js
CHANGED
|
@@ -64,8 +64,12 @@ function registerThemeCommands(cmd) {
|
|
|
64
64
|
.command('serve')
|
|
65
65
|
.description('Run local theme dev (Shopify + assets; Theme Check off by default)')
|
|
66
66
|
.option('--theme-check', 'Enable Theme Check watcher')
|
|
67
|
-
.action((opts) =>
|
|
68
|
-
|
|
67
|
+
.action(async (opts) => {
|
|
68
|
+
await serveAll({ includeThemeCheck: opts.themeCheck === true });
|
|
69
|
+
});
|
|
70
|
+
cmd.command('serve:shopify').description('Run Shopify theme dev server').action(async () => {
|
|
71
|
+
await serveShopify();
|
|
72
|
+
});
|
|
69
73
|
cmd
|
|
70
74
|
.command('serve:assets')
|
|
71
75
|
.description('Run assets watch (Tailwind + scripts; Theme Check off by default)')
|
package/src/lib/build-scripts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join, basename, dirname, normalize } from 'node:path';
|
|
3
3
|
|
|
4
4
|
function extractImportRecords(content) {
|
|
@@ -161,6 +161,30 @@ function collectFilesToIsolate({ scriptsDir, entryFile }) {
|
|
|
161
161
|
return isolateFiles;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
function removeOrphanScriptAssets({ cwd, keepNames }) {
|
|
165
|
+
// Delete assets/*.js that this build did not produce. The Electric Maybe build
|
|
166
|
+
// model treats assets/*.js as generated output of _scripts/, so any *.js without
|
|
167
|
+
// a matching bundle output is stale and should be removed. Only plain *.js files
|
|
168
|
+
// are considered — Liquid-processed assets (e.g. *.js.liquid) are left untouched.
|
|
169
|
+
const assetsDir = join(cwd, 'assets');
|
|
170
|
+
if (!existsSync(assetsDir)) return [];
|
|
171
|
+
|
|
172
|
+
const removed = [];
|
|
173
|
+
for (const dirent of readdirSync(assetsDir, { withFileTypes: true })) {
|
|
174
|
+
if (!dirent.isFile()) continue;
|
|
175
|
+
const name = dirent.name;
|
|
176
|
+
if (!name.endsWith('.js')) continue;
|
|
177
|
+
if (keepNames.has(name)) continue;
|
|
178
|
+
try {
|
|
179
|
+
unlinkSync(join(assetsDir, name));
|
|
180
|
+
removed.push(name);
|
|
181
|
+
} catch {
|
|
182
|
+
// Best effort: a failed unlink shouldn't break the build.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return removed;
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
function listTopLevelEntrypoints(scriptsDir) {
|
|
165
189
|
if (!existsSync(scriptsDir)) return [];
|
|
166
190
|
return readdirSync(scriptsDir, { withFileTypes: true })
|
|
@@ -234,9 +258,18 @@ export function buildScripts({ cwd = process.cwd(), entry = null, minify = false
|
|
|
234
258
|
}
|
|
235
259
|
}
|
|
236
260
|
if (entrypoints.length === 0) {
|
|
237
|
-
return { bundles: [] };
|
|
261
|
+
return { bundles: [], removed: [] };
|
|
238
262
|
}
|
|
239
263
|
const bundles = entrypoints.map((entryFile) => buildSingleEntrypoint({ cwd, entryFile, minify }));
|
|
240
|
-
|
|
264
|
+
|
|
265
|
+
// Only prune orphans on a full build (no explicit entry). A targeted single-entry
|
|
266
|
+
// build must not delete the other bundles' outputs.
|
|
267
|
+
let removed = [];
|
|
268
|
+
if (!entry) {
|
|
269
|
+
const keepNames = new Set(bundles.map((b) => basename(b.outputPath)));
|
|
270
|
+
removed = removeOrphanScriptAssets({ cwd, keepNames });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { bundles, removed };
|
|
241
274
|
}
|
|
242
275
|
|
package/src/lib/config.js
CHANGED
|
@@ -180,6 +180,23 @@ export function getStoreAliases(cwd = process.cwd()) {
|
|
|
180
180
|
return Object.keys(config.stores);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the store alias for `default_store` (the domain last set by `climaybe switch` or serve).
|
|
185
|
+
* @param {string} [cwd]
|
|
186
|
+
* @returns {string | null}
|
|
187
|
+
*/
|
|
188
|
+
export function getAliasForDefaultStore(cwd = process.cwd()) {
|
|
189
|
+
const config = readConfig(cwd);
|
|
190
|
+
const domain = config?.default_store;
|
|
191
|
+
const stores = config?.stores;
|
|
192
|
+
if (!domain || !stores || typeof stores !== 'object') return null;
|
|
193
|
+
const normalized = String(domain).trim();
|
|
194
|
+
for (const [alias, storeDomain] of Object.entries(stores)) {
|
|
195
|
+
if (String(storeDomain).trim() === normalized) return alias;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
/**
|
|
184
201
|
* Determine the current mode: 'single' or 'multi'.
|
|
185
202
|
*/
|
package/src/lib/dev-runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { watchTree } from './watch.js';
|
|
|
4
4
|
import { isAbsolute, join, relative } from 'node:path';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { readConfig, getStoreDomainFromBranch } from './config.js';
|
|
7
|
+
import { prepareMultiStoreForServe } from './serve-multi-store.js';
|
|
7
8
|
import { buildScripts } from './build-scripts.js';
|
|
8
9
|
import { buildSchemas } from './schema-builder.js';
|
|
9
10
|
import { runShopify } from './shopify-cli.js';
|
|
@@ -209,7 +210,15 @@ function runThemeCheckFiltered({ cwd = process.cwd() } = {}) {
|
|
|
209
210
|
return child;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
/**
|
|
214
|
+
* @param {{ cwd?: string; skipMultiStorePrep?: boolean }} [opts]
|
|
215
|
+
* @returns {Promise<import('node:child_process').ChildProcess | null>} Null when multi-store prep failed or was cancelled.
|
|
216
|
+
*/
|
|
217
|
+
export async function serveShopify({ cwd = process.cwd(), skipMultiStorePrep = false } = {}) {
|
|
218
|
+
if (!skipMultiStorePrep) {
|
|
219
|
+
const ok = await prepareMultiStoreForServe(cwd);
|
|
220
|
+
if (!ok) return null;
|
|
221
|
+
}
|
|
213
222
|
const config = readConfig(cwd) || {};
|
|
214
223
|
const branchStore = getStoreDomainFromBranch(cwd);
|
|
215
224
|
const store = branchStore || config.default_store || config.store || '';
|
|
@@ -244,8 +253,11 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } =
|
|
|
244
253
|
const scriptsDir = join(cwd, '_scripts');
|
|
245
254
|
if (existsSync(scriptsDir)) {
|
|
246
255
|
try {
|
|
247
|
-
buildScripts({ cwd });
|
|
256
|
+
const { removed } = buildScripts({ cwd });
|
|
248
257
|
writeTaggedLine('scripts', pc.yellow, 'built (initial)');
|
|
258
|
+
if (removed?.length) {
|
|
259
|
+
writeTaggedLine('scripts', pc.yellow, `removed ${removed.length} orphan asset(s): ${removed.join(', ')}`);
|
|
260
|
+
}
|
|
249
261
|
} catch (err) {
|
|
250
262
|
writeTaggedLine('scripts', pc.yellow, `initial build failed: ${err.message}`, process.stderr);
|
|
251
263
|
}
|
|
@@ -259,8 +271,11 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } =
|
|
|
259
271
|
debounceMs: 300,
|
|
260
272
|
onChange: () => {
|
|
261
273
|
try {
|
|
262
|
-
buildScripts({ cwd });
|
|
274
|
+
const { removed } = buildScripts({ cwd });
|
|
263
275
|
writeTaggedLine('scripts', pc.yellow, 'rebuilt');
|
|
276
|
+
if (removed?.length) {
|
|
277
|
+
writeTaggedLine('scripts', pc.yellow, `removed ${removed.length} orphan asset(s): ${removed.join(', ')}`);
|
|
278
|
+
}
|
|
264
279
|
} catch (err) {
|
|
265
280
|
writeTaggedLine('scripts', pc.yellow, `build failed: ${err.message}`, process.stderr);
|
|
266
281
|
}
|
|
@@ -365,16 +380,28 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } =
|
|
|
365
380
|
return { tailwind, devMcp, scriptsWatch, schemasWatch, themeCheckWatch, cleanup };
|
|
366
381
|
}
|
|
367
382
|
|
|
368
|
-
export function serveAll({ cwd = process.cwd(), includeThemeCheck = false } = {}) {
|
|
383
|
+
export async function serveAll({ cwd = process.cwd(), includeThemeCheck = false } = {}) {
|
|
384
|
+
const prepOk = await prepareMultiStoreForServe(cwd);
|
|
385
|
+
if (!prepOk) {
|
|
386
|
+
const noop = () => {};
|
|
387
|
+
return { cleanup: noop };
|
|
388
|
+
}
|
|
389
|
+
|
|
369
390
|
// Start assets first, then bring up Shopify after a short delay.
|
|
370
391
|
const assets = serveAssets({ cwd, includeThemeCheck });
|
|
371
392
|
let shopify = null;
|
|
372
393
|
const shopifyStartDelayMs = 2500;
|
|
373
394
|
const shopifyTimer = setTimeout(() => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
395
|
+
void (async () => {
|
|
396
|
+
shopify = await serveShopify({ cwd, skipMultiStorePrep: true });
|
|
397
|
+
if (shopify) {
|
|
398
|
+
shopify.on('exit', () => {
|
|
399
|
+
cleanup();
|
|
400
|
+
});
|
|
401
|
+
} else {
|
|
402
|
+
cleanup();
|
|
403
|
+
}
|
|
404
|
+
})();
|
|
378
405
|
}, shopifyStartDelayMs);
|
|
379
406
|
console.log(pc.dim(` Waiting ${shopifyStartDelayMs}ms before starting Shopify...`));
|
|
380
407
|
|
|
@@ -428,7 +455,10 @@ export function buildAll({ cwd = process.cwd() } = {}) {
|
|
|
428
455
|
|
|
429
456
|
let scriptsOk = true;
|
|
430
457
|
try {
|
|
431
|
-
buildScripts({ cwd });
|
|
458
|
+
const { removed } = buildScripts({ cwd });
|
|
459
|
+
if (removed?.length) {
|
|
460
|
+
writeTaggedLine('scripts', pc.yellow, `removed ${removed.length} orphan asset(s): ${removed.join(', ')}`);
|
|
461
|
+
}
|
|
432
462
|
} catch (err) {
|
|
433
463
|
console.log(pc.red(`\n build-scripts failed: ${err.message}\n`));
|
|
434
464
|
scriptsOk = false;
|
package/src/lib/prompts.js
CHANGED
|
@@ -229,6 +229,28 @@ export async function promptProjectName(cwd = process.cwd()) {
|
|
|
229
229
|
return String(projectName || '').trim();
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Ask which store to use for local theme dev (multi-store `climaybe serve`).
|
|
234
|
+
* @param {{ aliases: string[]; stores: Record<string, string>; suggestedAlias: string }} opts
|
|
235
|
+
* @returns {Promise<string | null>} Selected alias, or null if cancelled.
|
|
236
|
+
*/
|
|
237
|
+
export async function promptServeStore({ aliases, stores, suggestedAlias }) {
|
|
238
|
+
const sorted = [...aliases].sort();
|
|
239
|
+
const choices = sorted.map((alias) => ({
|
|
240
|
+
title: `${alias} (${stores[alias]})`,
|
|
241
|
+
value: alias,
|
|
242
|
+
}));
|
|
243
|
+
const initialIndex = sorted.indexOf(suggestedAlias);
|
|
244
|
+
const { alias } = await prompts({
|
|
245
|
+
type: 'select',
|
|
246
|
+
name: 'alias',
|
|
247
|
+
message: 'Which store should we serve?',
|
|
248
|
+
choices,
|
|
249
|
+
initial: initialIndex >= 0 ? initialIndex : 0,
|
|
250
|
+
});
|
|
251
|
+
return alias ?? null;
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
/**
|
|
233
255
|
* Prompt for a single new store (used by add-store command).
|
|
234
256
|
* Takes existing aliases to prevent duplicates.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { readConfig, getMode, getProjectType, getStoreAliases, writeConfig, getAliasForDefaultStore } from './config.js';
|
|
3
|
+
import { rootToStores, storesToRoot } from './store-sync.js';
|
|
4
|
+
import { promptServeStore } from './prompts.js';
|
|
5
|
+
|
|
6
|
+
function readExplicitServeAlias(cwd) {
|
|
7
|
+
const raw = process.env.CLIMAYBE_SERVE_STORE?.trim();
|
|
8
|
+
if (!raw) return { ok: true, alias: null };
|
|
9
|
+
const aliases = getStoreAliases(cwd);
|
|
10
|
+
if (aliases.includes(raw)) return { ok: true, alias: raw };
|
|
11
|
+
console.log(pc.red(` CLIMAYBE_SERVE_STORE="${raw}" is not a known store alias.`));
|
|
12
|
+
console.log(pc.dim(` Available: ${aliases.join(', ')}\n`));
|
|
13
|
+
return { ok: false, alias: null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isNonInteractiveServe() {
|
|
17
|
+
if (process.env.CLIMAYBE_SERVE_STORE?.trim()) return true;
|
|
18
|
+
if (process.env.CI === 'true') return true;
|
|
19
|
+
return process.stdin.isTTY !== true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* In multi-store theme repos, ensure root JSONs and `default_store` match the store
|
|
24
|
+
* the user wants to serve. If the choice differs from the previous switch target,
|
|
25
|
+
* saves current root → `stores/<previous>/` then loads `stores/<selected>/` → root.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} [cwd]
|
|
28
|
+
* @returns {Promise<boolean>} False if the user cancelled or `CLIMAYBE_SERVE_STORE` is invalid.
|
|
29
|
+
*/
|
|
30
|
+
export async function prepareMultiStoreForServe(cwd = process.cwd()) {
|
|
31
|
+
if (getProjectType(cwd) === 'app') return true;
|
|
32
|
+
if (getMode(cwd) !== 'multi') return true;
|
|
33
|
+
|
|
34
|
+
const config = readConfig(cwd);
|
|
35
|
+
if (!config?.stores || typeof config.stores !== 'object') return true;
|
|
36
|
+
|
|
37
|
+
const aliases = getStoreAliases(cwd);
|
|
38
|
+
if (aliases.length < 2) return true;
|
|
39
|
+
|
|
40
|
+
const stores = config.stores;
|
|
41
|
+
const suggestedAlias = getAliasForDefaultStore(cwd) ?? aliases[0];
|
|
42
|
+
|
|
43
|
+
const explicit = readExplicitServeAlias(cwd);
|
|
44
|
+
if (!explicit.ok) return false;
|
|
45
|
+
|
|
46
|
+
let selected = explicit.alias;
|
|
47
|
+
if (!selected) {
|
|
48
|
+
if (isNonInteractiveServe()) {
|
|
49
|
+
selected = suggestedAlias;
|
|
50
|
+
} else {
|
|
51
|
+
console.log(pc.bold('\n climaybe — serve (store)\n'));
|
|
52
|
+
selected = await promptServeStore({ aliases, stores, suggestedAlias });
|
|
53
|
+
if (!selected) {
|
|
54
|
+
console.log(pc.dim(' Cancelled.\n'));
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const previousAlias = getAliasForDefaultStore(cwd);
|
|
61
|
+
|
|
62
|
+
if (selected === previousAlias) return true;
|
|
63
|
+
|
|
64
|
+
if (previousAlias) {
|
|
65
|
+
rootToStores(previousAlias, cwd);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(
|
|
68
|
+
pc.yellow(
|
|
69
|
+
' Warning: default_store did not match any store alias; current root JSONs are not auto-saved to a store folder.'
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ok = storesToRoot(selected, cwd);
|
|
75
|
+
if (!ok) return false;
|
|
76
|
+
|
|
77
|
+
const domain = stores[selected];
|
|
78
|
+
if (domain) writeConfig({ default_store: domain }, cwd);
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# climaybe — Cleanup orphan preview themes (optional)
|
|
2
|
+
# Weekly (and manual): on each configured store, delete themes whose name ends with -PR<n>
|
|
3
|
+
# when PR #n is not open in this repo (merged/closed without theme cleanup).
|
|
4
|
+
|
|
5
|
+
name: Cleanup orphan preview themes
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: '0 6 * * 1'
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
pull-requests: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
configure:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
outputs:
|
|
20
|
+
preview_targets_json: ${{ steps.cfg.outputs.preview_targets_json }}
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Build store matrix from climaybe.config.json
|
|
25
|
+
id: cfg
|
|
26
|
+
run: |
|
|
27
|
+
if [ ! -f climaybe.config.json ]; then
|
|
28
|
+
{
|
|
29
|
+
echo 'preview_targets_json<<PT_JSON'
|
|
30
|
+
echo '[]'
|
|
31
|
+
echo 'PT_JSON'
|
|
32
|
+
} >> "$GITHUB_OUTPUT"
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
node <<'NODE' >>"$GITHUB_OUTPUT"
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const cfg = JSON.parse(fs.readFileSync('climaybe.config.json', 'utf8'));
|
|
38
|
+
const stores = cfg?.stores || {};
|
|
39
|
+
const keys = Object.keys(stores);
|
|
40
|
+
const aliasToSecret = (a) => String(a).toUpperCase().replace(/-/g, '_');
|
|
41
|
+
const targets = keys.map((alias) => ({ alias, alias_secret: aliasToSecret(alias) }));
|
|
42
|
+
console.log('preview_targets_json<<PT_JSON');
|
|
43
|
+
console.log(JSON.stringify(targets));
|
|
44
|
+
console.log('PT_JSON');
|
|
45
|
+
NODE
|
|
46
|
+
|
|
47
|
+
cleanup-orphans:
|
|
48
|
+
needs: [configure]
|
|
49
|
+
if: needs.configure.outputs.preview_targets_json != '[]'
|
|
50
|
+
strategy:
|
|
51
|
+
fail-fast: false
|
|
52
|
+
matrix:
|
|
53
|
+
include: ${{ fromJson(needs.configure.outputs.preview_targets_json) }}
|
|
54
|
+
uses: ./.github/workflows/reusable-cleanup-themes.yml
|
|
55
|
+
with:
|
|
56
|
+
cleanup_mode: orphan_pr
|
|
57
|
+
pr_number: ''
|
|
58
|
+
store_alias_secret: ${{ matrix.alias_secret }}
|
|
59
|
+
secrets: inherit
|