@shop-on/create-theme 0.5.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shop-on/create-theme",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Scaffolds a Shop-On storefront theme: `npm create @shop-on/theme <dir>`.",
5
5
  "license": "SEE LICENSE IN dist/LICENSE.md",
6
6
  "bin": {
@@ -64,6 +64,7 @@ src/
64
64
  config/ settings-*.json theme-wide settings
65
65
  components/ template-renderer.tsx core resolver (don't usually touch)
66
66
  server/ settings.ts (route server fns), enrich-adapter.ts (fetch-depth policy), template-loader.ts (loader config)
67
+ ai/ guide.md + recipes/*.json what the editor's AI tools read — see below
67
68
  ```
68
69
 
69
70
  ## Core rules — read before authoring
@@ -264,6 +265,17 @@ A theme change is not finished when it type-checks — the artifact carries a ve
264
265
 
265
266
  Write the notes **for the merchant, not the diff**: what changes in their store, in their words. Match the file — plain bullets under the heading, no `### Fixed` subsections, no file names, no component names. "Prices now say when tax is included", not "added `taxIncludedCaption` to the price block".
266
267
 
268
+ ## AI guide + recipes (`src/ai/`)
269
+
270
+ AI assistants connected to a store (Claude, ChatGPT), `shop-on theme mcp` and the theme editor's AI chat edit a theme through one set of editor tools. What they know about _this_ theme comes from `src/ai/`: `get_theme_guide` serves `guide.md`, `list_recipes` / `apply_recipe` serve `recipes/<handle>.json`.
271
+
272
+ - **`guide.md`** — a designer's brief for the model, plain Markdown, ≤ 16 KB: what the theme is for, which section does which job, the settings that matter, what to avoid. Section, block and setting handles in backticks.
273
+ - **`recipes/<handle>.json`** — named page builds, ≤ 20, each ≤ 32 KB, handle `[a-z0-9-]`. Shape: `{ "name", "description", "params": { "<id>": { "type": "text" | "collection" | "image" | …, "label", "required" } }, "position": "append" | "prepend" | "replace", "sections": [{ "type", "settings", "blocks" }] }`; a param is used as `"{{id}}"` inside a setting value.
274
+ - **The rule is strict:** `ai/` holds exactly `guide.md` and `recipes/*.json`. No other file, subdir or dotfile — the build and the upload both refuse it.
275
+ - **Recipes are checked at build — but only shallowly.** Every section/block type and setting id must exist, or `pnpm build` fails. Setting values are checked only when a merchant applies the recipe: a select value outside its options, an out-of-range number or a `palette:` ref the shop no longer has makes `apply_recipe` refuse the whole recipe. Keep literal values to the schema's options; leave colours to the theme's defaults.
276
+ - **Optional params drop their line.** The editor tools store a rich-text value with nothing to show as `""` — `<p>{{tagline}}</p>` or `<a href="tel:{{phone}}">{{phone}}</a>` with the param omitted, or `<p>&nbsp;</p>` — so a theme that hides empty rich text drops it. A plain `text` setting, a button label or an FAQ row's question is not emptied for you: make that param required, or check the block renders nothing when it is blank.
277
+ - **Keep it in step with the sections.** Adding, renaming or removing a section or block, or changing a setting the guide names, means editing `guide.md` and any recipe that uses it in the same change.
278
+
267
279
  ## Building in the editor (Save to source)
268
280
 
269
281
  You can also build a theme **visually in the real admin theme-editor** and persist the result to `src/`, instead of hand-editing JSON: `pnpm theme:editor` runs the theme's dev server + a dev-host gateway (`@shop-on/theme-dev-host`) that serves the editor API from `src/` with drafts held in memory.
@@ -130,9 +130,9 @@ templates only; `null` elsewhere blanks it. Each key has one form:
130
130
  stripped, `data-dyn=` attributes removed) so the last inline pass can't turn
131
131
  description content into a live token.
132
132
 
133
- The theme supplies `collection: { title, getDescriptionHtml }` (a
134
- `DynamicSourceCollection`) from `fetchCollectionMeta` +
135
- `fetchCollectionDescriptionHtml`; the provider runs only when
133
+ The theme supplies `collection: routeCollectionSource(meta, locale)` (a
134
+ `DynamicSourceCollection`, from `@shop-on/theme-server/collection`, off the
135
+ `fetchCollectionMeta` read); the provider runs only when
136
136
  `collectCollectionAttributeBindings(template)` includes `description`, so the
137
137
  lean meta read stays lean.
138
138
 
@@ -145,8 +145,16 @@ only**, in text/textarea/richtext like a product attribute.
145
145
  There is no whole-value page source and no body attribute: the page body is
146
146
  rendered from the route page by the theme's own section or block (the starter's
147
147
  `main-page`), so a binding would be a second path to the same HTML. The theme
148
- supplies `page: { title }` off the `fetchPage` read its page loader already
149
- makes, so the source costs no extra request.
148
+ supplies `page: routePageSource(page, locale)` (`@shop-on/theme-server/page`)
149
+ off the `fetchPage` read its page loader already makes, so the title costs no
150
+ extra request.
151
+
152
+ Always pass a route resource through its builder, never an inline
153
+ `{ title }`: the builder also carries the lazy `getMetafields` provider behind
154
+ the `collection_metafield` / `page_metafield` / `blog_metafield` /
155
+ `article_metafield` sources (a merchant's own fields on that resource, same
156
+ grammar and sinks as `product_metafield`). An inline object type-checks and
157
+ silently renders every such binding's literal.
150
158
 
151
159
  ### Blog and article attributes
152
160
 
@@ -156,8 +164,9 @@ blanks everywhere else. Same shape as the page source: one key, `title`,
156
164
  inline token only — the body, the cover and the post list are rendered by the
157
165
  theme's own blocks and sections.
158
166
 
159
- Pass them through the theme's `TemplateRequest` as `article: { title }` (off the
160
- `fetchArticle` read) and `blog: { title }`. **Enrich after the resource
167
+ Pass them through the theme's `TemplateRequest` as
168
+ `article: routeArticleSource(article, locale)` (off the `fetchArticle` read)
169
+ and `blog: routeBlogSource(blog, locale)` (both `@shop-on/theme-server/blog`). **Enrich after the resource
161
170
  resolves**: a loader that runs `loadTemplate` (load + enrich) in a
162
171
  `Promise.all` beside the fetch resolves the token against `null`. Split the load
163
172
  into a raw read and an `enrich` call (a `loadRawTemplate` helper, say). When the
@@ -16,14 +16,15 @@ it will be this file's **review** lines.
16
16
  ## Run the tools
17
17
 
18
18
  ```bash
19
- pnpm theme:check # every check rule; exit 1 on any finding
19
+ pnpm theme:check # every check rule; exit 1 on any finding but a warning
20
20
  pnpm theme:check --release # also theme-info: run before you hand the theme off
21
21
  pnpm theme:check --json # for CI
22
22
  pnpm build # postbuild enforces the build lines
23
23
  ```
24
24
 
25
25
  The output is one line per finding, `file:line rule message`, followed by the
26
- recipe for each rule that fired.
26
+ recipe for each rule that fired. A `(warning)` after the rule id prints but
27
+ doesn't fail the run.
27
28
 
28
29
  The **audit** lines come from `shop-on theme audit`, in the `@shop-on/theme-cli`
29
30
  package:
@@ -96,12 +97,16 @@ anyone else installs the theme.
96
97
  | no default for `collection`, `product`, `product_list`, `metaobject(_list)`, `review(_list)` pickers | check: `tenant-picker-default` | [picker-data.md](picker-data.md#no-defaults-for-tenant-specific-pickers) |
97
98
  | no literal colors in class names; tokens only | check: `literal-color` | [styling.md](styling.md#rule-tokens-never-literals) |
98
99
  | no raw `<a href="/…">` to an internal path | check: `raw-internal-link` | [SKILL.md § Don't do](../SKILL.md#dont-do) |
100
+ | each `closest_*` resource has one unambiguous provider | check: `closest-provider` (warning) | [dynamic-sources.md](dynamic-sources.md#closest-resources--closest_) |
99
101
  | a section with nothing picked still looks intentional | review | [section.md](section.md#preview-readiness) |
100
102
  | the layout holds at 375px | review: the editor's mobile preview | [styling.md](styling.md#responsive) |
101
103
  | settings read the way a merchant would name them | review | [input-settings.md](input-settings.md) |
102
104
 
103
105
  `schema-label-key` skips labels with nothing to translate (`16px`, `/`).
104
106
  `tenant-picker-default` also catches a preset that fills a tenant picker.
107
+ `closest-provider` warns on a second picker of one resource with no
108
+ `provides` for it (only the first provides), one static block in two `each`
109
+ lists, and a `provides` whose `from` or `each` names nothing on the schema.
105
110
 
106
111
  ## Suppressing a finding
107
112
 
@@ -20,8 +20,8 @@ never commit it. A store holds at most 5 of these credentials;
20
20
  `npx @shop-on/theme-cli theme credentials revoke <client-id>` frees one.
21
21
 
22
22
  `theme dev` runs the theme behind a `cloudflared` tunnel (install it first)
23
- and opens the hosted theme editor on a developer preview of it. Confirm the
24
- connection, and keep admin on the same store. `pnpm dev` still runs the
23
+ and opens the hosted theme editor on a developer preview of it, with nothing
24
+ to confirm. Keep admin on the same store. `pnpm dev` still runs the
25
25
  storefront on its own.
26
26
 
27
27
  Without logging in, scaffold with `npm create` and fill in `.env` yourself:
@@ -49,10 +49,15 @@ with a "missing" message naming the variable.
49
49
  | `pnpm theme:check` | the quality bar's static rules; `--release` before handing off |
50
50
  | `pnpm build` | the theme artifact in `.output/`, with `release-notes.md` alongside it |
51
51
 
52
- `pnpm theme:editor --tunnel --env prod --slug <store-handle> --install <install-id>`
53
- exposes the gateway publicly, so the hosted admin's theme editor can preview
54
- your local theme. In the editor, open an **unpublished** install's actions
55
- menu → **Local dev preview** and paste the tunnel URL.
52
+ To sell the theme on the Lite theme store, register at the developer portal
53
+ and submit its source: `npx @shop-on/theme-cli theme source` writes the
54
+ tarball to upload. Lite builds it and reviews the result; see the developer
55
+ portal guide.
56
+
57
+ `npx @shop-on/theme-cli theme dev` runs the theme behind the gateway and a
58
+ tunnel, and opens the hosted admin's theme editor on your local theme. It makes
59
+ a dev install in your store for this, the only kind of install the editor
60
+ connects to a local theme.
56
61
 
57
62
  ## Layout
58
63
 
@@ -10,21 +10,21 @@
10
10
  "dev": "vite dev",
11
11
  "prebuild": "node scripts/backfill-defaults.mjs && oxfmt src",
12
12
  "build": "NITRO_PRESET=cloudflare_module vite build",
13
- "postbuild": "node scripts/postbuild.mjs",
13
+ "postbuild": "node --import tsx scripts/postbuild.mjs",
14
14
  "test": "vitest run --passWithNoTests",
15
15
  "theme:check": "node scripts/theme-check.mjs",
16
16
  "theme:editor": "theme-dev-host"
17
17
  },
18
18
  "dependencies": {
19
19
  "@bprogress/core": "1.3.4",
20
- "@shop-on/i18n": "^0.5.0",
21
- "@shop-on/theme-contract": "^0.5.0",
22
- "@shop-on/theme-loader": "^0.5.0",
23
- "@shop-on/theme-react": "^0.5.0",
24
- "@shop-on/theme-routes": "^0.5.0",
25
- "@shop-on/theme-runtime": "^0.5.0",
26
- "@shop-on/theme-server": "^0.5.0",
27
- "@shop-on/theme-system": "^0.5.0",
20
+ "@shop-on/i18n": "^0.7.0",
21
+ "@shop-on/theme-contract": "^0.7.0",
22
+ "@shop-on/theme-loader": "^0.7.0",
23
+ "@shop-on/theme-react": "^0.7.0",
24
+ "@shop-on/theme-routes": "^0.7.0",
25
+ "@shop-on/theme-runtime": "^0.7.0",
26
+ "@shop-on/theme-server": "^0.7.0",
27
+ "@shop-on/theme-system": "^0.7.0",
28
28
  "@tailwindcss/vite": "4.3.3",
29
29
  "@tanstack/react-router": "1.170.45",
30
30
  "@tanstack/react-start": "1.169.3",
@@ -35,8 +35,8 @@
35
35
  "tailwindcss": "4.3.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@shop-on/theme-config": "^0.5.0",
39
- "@shop-on/theme-dev-host": "^0.5.0",
38
+ "@shop-on/theme-config": "^0.7.0",
39
+ "@shop-on/theme-dev-host": "^0.7.0",
40
40
  "@spiriit/vite-plugin-svg-spritemap": "7.2.0",
41
41
  "@types/node": "26.6.5",
42
42
  "@types/react": "19.3.0",
@@ -45,6 +45,7 @@
45
45
  "gql.tada": "1.11.4",
46
46
  "happy-dom": "21.0.4",
47
47
  "oxfmt": "^0.74.0",
48
+ "tsx": "^4.23.15",
48
49
  "typescript": "7.0.2",
49
50
  "vite": "8.3.5",
50
51
  "vitest": "5.0.5"
@@ -0,0 +1,27 @@
1
+ # Starter: the designer's guide
2
+
3
+ Starter is a new theme in a neutral light palette. Rewrite this guide
4
+ as the theme grows: say what it is for, which section does which job, and what
5
+ to avoid. The editor's AI tools read this file, so a section missing from it is
6
+ one they won't reach for.
7
+
8
+ ## How a page is put together
9
+
10
+ - Templates: `index` (home), `product`, `collection`, `page`, `blog`,
11
+ `article`, `cart`, `search`, `list-collections`, `404`, `password`,
12
+ `policy`. Every template except `404` has its own `main-*` section, fixed to
13
+ it; `404` starts empty.
14
+ - Groups: `header` holds the `header` section and `footer` holds the `footer`
15
+ section. They can be edited but not added to a page.
16
+
17
+ ## Sections by job
18
+
19
+ **Open a page (hero)**
20
+
21
+ - `hero`: an eyebrow line, a headline, body text and one button
22
+ (`cta_label`, `cta_url`). Keep the headline short; leave the button out
23
+ rather than pointing it nowhere.
24
+
25
+ ## What to avoid
26
+
27
+ - Don't stack two `hero` sections.
@@ -107,7 +107,13 @@ export const getThemeSettingsFn = createServerFn({ method: "GET" })
107
107
  // return `null` when the handle misses so the route `notFound()`s. A resource
108
108
  // whose template is merchant-assignable (product / collection / page) should
109
109
  // load through the package's `resolveTemplateVariant` instead of `loadTemplate`,
110
- // so an unshipped `product.<variant>` falls back to the base document.
110
+ // so an unshipped `product.<variant>` falls back to the base document. A
111
+ // template's dynamic sources read its route resource: pass
112
+ // `collection: routeCollectionSource(meta, locale)`, `page: routePageSource(page,
113
+ // locale)`, `blog: routeBlogSource(blog, locale)` and `article:
114
+ // routeArticleSource(article, locale)` (theme-server's `collection`, `page` and
115
+ // `blog` subpaths) rather than building the object inline, so the theme picks
116
+ // up every provider (descriptions, metafields) on upgrade.
111
117
  //
112
118
  // biome-ignore lint/suspicious/noExplicitAny: serializer requires it
113
119
  type TemplatePayload = Promise<any>;