vite-plugin-react-shopify 1.1.0 → 2.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # vite-plugin-react-shopify
2
2
 
3
- 使用 React 组件编写 Shopify 主题的 Section、Block 和 Template。在构建时通过 SSG(Static Site Generation)将 React 组件编译为 Shopify Liquid 文件,运行时由 React 进行水合(hydration)。
3
+ 使用 React 组件编写 Shopify 主题的 Section、Block、Snippet 和 Template。在构建时通过 SSG(Static Site Generation)将 React 组件编译为 Shopify Liquid 文件,运行时由 React 进行水合(hydration)。
4
4
 
5
5
  ## 安装
6
6
 
@@ -17,7 +17,7 @@ import vitePluginShopify from "vite-plugin-react-shopify";
17
17
  export default {
18
18
  plugins: [
19
19
  vitePluginShopify({
20
- themeRoot: ".", // 主题根目录,默认 "./"
20
+ themeRoot: ".", // 主题根目录,默认 "./"
21
21
  sourceCodeDir: "frontend", // 源码目录,默认 "frontend"
22
22
  }),
23
23
  ],
@@ -27,14 +27,19 @@ export default {
27
27
  ```tsx
28
28
  // frontend/sections/HelloWorld.tsx
29
29
  import type { ShopifyMeta } from "vite-plugin-react-shopify";
30
+ import { useSectionSettings } from "vite-plugin-react-shopify/runtime";
30
31
 
31
32
  export const shopifyMeta = {
32
33
  name: "Hello World",
34
+ settings: [
35
+ { type: "text", id: "title", label: "Title", default: "Hello, World!" },
36
+ ],
33
37
  presets: [{ name: "Hello World" }],
34
38
  } satisfies ShopifyMeta;
35
39
 
36
40
  export default function HelloWorld() {
37
- return <h1>Hello, World!</h1>;
41
+ const { value: title } = useSectionSettings("title");
42
+ return <h1>{title}</h1>;
38
43
  }
39
44
  ```
40
45
 
@@ -42,8 +47,6 @@ export default function HelloWorld() {
42
47
 
43
48
  ### 开发
44
49
 
45
- 使用 `vite build --watch` 进行本地开发。Vite 8 的 Rolldown 内置增量构建缓存,文件变更时仅重新构建受影响的模块。
46
-
47
50
  ```bash
48
51
  # 终端 1: 启动 Vite 构建监听
49
52
  pnpm dev # → vite build --watch
@@ -52,17 +55,7 @@ pnpm dev # → vite build --watch
52
55
  shopify theme dev
53
56
  ```
54
57
 
55
- Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测到变化 → 推送主题 → 编辑器热更新。watch 模式下产物不压缩、附带 inline sourcemap,方便在浏览器中调试。
56
-
57
- ```json
58
- // package.json
59
- {
60
- "scripts": {
61
- "dev": "vite build --watch",
62
- "build": "vite build"
63
- }
64
- }
65
- ```
58
+ Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测到变化 → 推送主题 → 编辑器热更新。
66
59
 
67
60
  ---
68
61
 
@@ -75,178 +68,169 @@ my-theme/
75
68
  │ │ └── HelloWorld.tsx
76
69
  │ ├── blocks/
77
70
  │ │ └── TextBlock.tsx
71
+ │ ├── snippets/
72
+ │ │ └── MySnippet.tsx
78
73
  │ └── templates/
79
74
  │ └── index.tsx
80
75
  ├── sections/ ← 生成的 Liquid + 原生 Liquid
81
76
  ├── blocks/
82
- ├── templates/
83
77
  ├── snippets/
84
- │ └── shopify-importmap.liquid ← 自动生成的 importmap
78
+ ├── templates/
85
79
  └── assets/ ← Vite 构建产物(可通过 buildDir 配置子目录)
86
80
  ```
87
81
 
88
- 源码放在 `frontend/` 下,按 Shopify 类型分目录。构建后生成对应的 `.liquid` 文件到主题根目录的 `sections/`、`blocks/`、`templates/` 下。
82
+ ---
83
+
84
+ ## Settings 和 Liquid 数据读取
85
+
86
+ ### 核心 API
87
+
88
+ 所有 Liquid 数据的读取通过统一的基础 hook `useLiquid(expr)`:
89
+
90
+ ```tsx
91
+ import {
92
+ useLiquid,
93
+ useLiquidValues,
94
+ useSectionSettings,
95
+ useBlockSettings,
96
+ useSnippetParams,
97
+ useBlockParams,
98
+ } from "vite-plugin-react-shopify/runtime";
99
+ ```
100
+
101
+ | Hook | 用途 | 内部等价于 |
102
+ |------|------|-----------|
103
+ | `useLiquid(expr)` | 读取任意 Liquid 表达式 | — |
104
+ | `useLiquidValues({ key: expr })` | 批量读取多个表达式 | N 次 `useLiquid` |
105
+ | `useSectionSettings(key)` | Section setting | `useLiquid("section.settings.KEY")` |
106
+ | `useBlockSettings(key)` | Block setting | `useLiquid("block.settings.KEY")` |
107
+ | `useSnippetParams(key)` | Snippet param | `useLiquid("KEY")` |
108
+ | `useBlockParams(key)` | Block param | `useLiquid("KEY")` |
109
+
110
+ 所有 hook 返回 `{ value: string | undefined }`(`useLiquidValues` 返回 `{ values: Record }`)。
111
+
112
+ ```tsx
113
+ export default function ProductBanner() {
114
+ const { value: title } = useSectionSettings("title");
115
+ const { value: price } = useLiquid("product.price");
116
+ const { values: p } = useLiquidValues({
117
+ name: "product.title",
118
+ desc: "product.description",
119
+ });
120
+
121
+ return (
122
+ <div>
123
+ <h1>{title}</h1>
124
+ <span>{price}</span>
125
+ <p>{p.name}</p>
126
+ <div dangerouslySetInnerHTML={{ __html: p.desc || "" }} />
127
+ </div>
128
+ );
129
+ }
130
+ ```
131
+
132
+ ### 工具函数
133
+
134
+ ```ts
135
+ import { parseLiquidBoolean, parseLiquidNumber } from "vite-plugin-react-shopify/runtime";
136
+
137
+ // SSR-safe: Liquid 表达式字符串 → 默认值;客户端 → 实际解析
138
+ const count = parseLiquidNumber(s.initial, 0);
139
+ const show = parseLiquidBoolean(s.show_banner);
140
+ ```
141
+
142
+ ### 水合流程
143
+
144
+ 1. **SSR**:hook 返回 `{{ expr }}` 字符串,同时注册表达式到追踪器
145
+ 2. **Liquid 组装**:生成统一 `<script type="application/json" data-ssg-liquid>` JSON bridge
146
+ 3. **Shopify 渲染**:Liquid 引擎替换表达式为实际值,bridge JSON 包含序列化后的值
147
+ 4. **客户端水合**:`LiquidDataProvider` 接收 bridge JSON → hook 通过 `useContext` 读取
89
148
 
90
149
  ---
91
150
 
92
151
  ## 组件约定
93
152
 
94
- 每个 React 组件必须导出两样东西:
153
+ 每个 React 组件必须导出:
95
154
 
96
155
  ### 1. `shopifyMeta` — Shopify Schema 定义
97
156
 
98
157
  ```tsx
99
158
  export const shopifyMeta = {
100
- name: "组件名称", // 必填,在 Shopify 编辑器中显示的名称
101
- type: "section", // 可选,覆盖目录推断的类型
159
+ name: "组件名称",
160
+ type: "section", // 可选,覆盖目录推断
102
161
  tag: "section", // 可选,外层 HTML 标签,默认 "div"
103
- class: "custom-class", // 可选,外层附加的 CSS 类名
104
- limit: 1, // 可选,每页最大数量
105
- max_blocks: 10, // 可选,最大子 block 数
106
- settings: [...], // 可选,设置项
107
- blocks: [...], // 可选,接受的子 block 类型
108
- presets: [...], // 可选,编辑器预设
109
- enabled_on: {...}, // 可选,启用条件
110
- disabled_on: {...}, // 可选,禁用条件
111
- templates: [...], // 可选,适用的模板
162
+ class: "custom-class", // 可选,外层 CSS 类名
163
+ limit: 1,
164
+ max_blocks: 10,
165
+ settings: [...],
166
+ blocks: [...],
167
+ presets: [...],
112
168
  } satisfies ShopifyMeta;
113
169
  ```
114
170
 
115
171
  ### 2. `default export` — React 组件
116
172
 
117
173
  ```tsx
118
- export default function MyComponent(props) {
174
+ export default function MyComponent() {
119
175
  return <div>...</div>;
120
176
  }
121
177
  ```
122
178
 
123
179
  ### 类型推断
124
180
 
125
- 文件名和目录决定目标类型:
126
-
127
181
  | 源码路径 | 目标类型 | 生成文件 |
128
182
  |----------|----------|----------|
129
183
  | `frontend/sections/X.tsx` | `section` | `sections/react-x.liquid` |
130
184
  | `frontend/blocks/X.tsx` | `block` | `blocks/react-x.liquid` |
131
185
  | `frontend/templates/X.tsx` | `template` | `templates/page.react-x.liquid` |
132
-
133
- 可通过 `shopifyMeta.type` 覆盖。
186
+ | `frontend/snippets/X.tsx` | `snippet` | `snippets/react-x.liquid` |
134
187
 
135
188
  ---
136
189
 
137
190
  ## 设置 (Settings)
138
191
 
139
- 使用 `SettingSchema` 类型定义设置项。`SettingSchema` 是根据 `type` 字段判别(discriminated union)的联合类型 —— 每种 type 只接受 Shopify 文档中合法的字段,无效字段会在编译时报错:
140
-
141
192
  ```tsx
142
193
  import type { SettingSchema } from "vite-plugin-react-shopify";
143
194
 
144
195
  const settings = [
145
- {
146
- type: "text",
147
- id: "title",
148
- label: "Title",
149
- default: "Hello",
150
- },
151
- {
152
- type: "select",
153
- id: "layout",
154
- label: "Layout",
155
- default: "grid",
156
- options: [
157
- { value: "grid", label: "Grid" },
158
- { value: "list", label: "List" },
159
- ],
160
- },
161
- {
162
- type: "range",
163
- id: "columns",
164
- label: "Columns",
165
- default: 3,
166
- min: 1,
167
- max: 6,
168
- step: 1,
169
- },
196
+ { type: "text", id: "title", label: "Title", default: "Hello" },
197
+ { type: "select", id: "layout", label: "Layout", default: "grid",
198
+ options: [{ value: "grid", label: "Grid" }, { value: "list", label: "List" }] },
199
+ { type: "range", id: "columns", label: "Columns", default: 3, min: 1, max: 6, step: 1 },
170
200
  ] satisfies SettingSchema[];
171
-
172
- // 也可以直接引用具体类型:
173
- import type { SelectSetting, RangeSetting } from "vite-plugin-react-shopify";
174
201
  ```
175
202
 
176
203
  ### 基本输入类型
177
204
 
178
- | 类型 | 额外字段 | `default` |
179
- |------|----------|-----------|
180
- | `checkbox` | — | 可选 `boolean` |
181
- | `number` | `placeholder` | 可选 `number` |
182
- | `radio` | `options`(必填) | 可选 `string` |
183
- | `range` | `min`(必填)、`max`(必填)、`step`、`unit` | **必填** `number` |
184
- | `select` | `options`(必填) | 可选 `string` |
185
- | `text` | `placeholder` | 可选 `string` |
186
- | `textarea` | `placeholder` | 可选 `string` |
205
+ | 类型 | 额外字段 |
206
+ |------|----------|
207
+ | `checkbox` | `default`(`boolean`) |
208
+ | `number` | `placeholder` |
209
+ | `radio` | `options`(必填) |
210
+ | `range` | `min`(必填)、`max`(必填)、`step`、`unit` |
211
+ | `select` | `options`(必填) |
212
+ | `text` | `placeholder` |
213
+ | `textarea` | `placeholder` |
187
214
 
188
215
  ### 专用输入类型
189
216
 
190
- | 类型 | 额外字段 |
191
- |------|----------|
192
- | `article` | —(不支持 `default`) |
193
- | `article_list` | `limit` |
194
- | `blog` | —(不支持 `default`) |
195
- | `collection` | —(不支持 `default`) |
196
- | `collection_list` | `limit` |
197
- | `color` | — |
198
- | `color_background` | — |
199
- | `color_scheme` | — |
200
- | `color_scheme_group` | `definition`(必填)、`role`(必填) |
201
- | `font_picker` | —(`default` **必填** `string`) |
202
- | `html` | `placeholder` |
203
- | `image_picker` | —(不支持 `default`) |
204
- | `inline_richtext` | — |
205
- | `link_list` | —(`default` 限制为 `"main-menu"` / `"footer"`) |
206
- | `liquid` | — |
207
- | `metaobject` | `metaobject_type`(必填) |
208
- | `metaobject_list` | `metaobject_type`(必填)、`limit` |
209
- | `page` | —(不支持 `default`) |
210
- | `product` | —(不支持 `default`) |
211
- | `product_list` | `limit` |
212
- | `richtext` | — |
213
- | `text_alignment` | —(`default` 限制为 `"left"` / `"center"` / `"right"`) |
214
- | `url` | — |
215
- | `video` | —(不支持 `default`) |
216
- | `video_url` | `accept`(必填)、`placeholder` |
217
-
218
- ### 侧边栏类型(非输入,仅显示信息)
219
-
220
- `header`、`paragraph`、`line_break`。与输入类型不同,侧边栏类型不保存值,仅用于组织和描述设置项。使用 `content` 字段代替 `id`/`label`:
217
+ `article`、`article_list`、`blog`、`collection`、`collection_list`、`color`、`color_background`、`color_scheme`、`color_scheme_group`、`font_picker`、`html`、`image_picker`、`inline_richtext`、`link_list`、`liquid`、`metaobject`、`metaobject_list`、`page`、`product`、`product_list`、`richtext`、`text_alignment`、`url`、`video`、`video_url`
221
218
 
222
- ```tsx
223
- const settings = [
224
- { type: "header", content: "Typography", info: "Customize text styles below." },
225
- { type: "font_picker", id: "heading_font", label: "Heading font", default: "helvetica_n4" },
226
- { type: "paragraph", content: "Set your brand colors for buttons and accents." },
227
- { type: "color", id: "accent_color", label: "Accent color", default: "#000000" },
228
- { type: "line_break" },
229
- { type: "checkbox", id: "dark_mode", label: "Enable dark mode", default: false },
230
- ] satisfies SettingSchema[];
219
+ ### 侧边栏类型
231
220
 
232
- Setting 的 `id` 会作为 React 组件的 prop 名传入。Shopify 编辑器修改 setting 后,水合层会读取 `{{ section.settings | json }}` 将最新值传给组件。设置值通过 `useShopifySettings()` hook 读取。
221
+ `header`、`paragraph`、`line_break`。使用 `content` 字段,不保存值。
233
222
 
234
223
  ---
235
224
 
236
225
  ## 预设 (Presets)
237
226
 
238
- 预设让组件可以在编辑器中通过"添加 Section/Block"面板直接添加:
239
-
240
227
  ```tsx
241
228
  export const shopifyMeta = {
242
229
  name: "Hero Banner",
243
230
  presets: [
244
231
  { name: "Hero (Light)", category: "Banners" },
245
- {
246
- name: "Hero (Dark)",
247
- category: "Banners",
248
- settings: { bg_color: "#000", text_color: "#fff" },
249
- },
232
+ { name: "Hero (Dark)", category: "Banners",
233
+ settings: { bg_color: "#000", text_color: "#fff" } },
250
234
  ],
251
235
  } satisfies ShopifyMeta;
252
236
  ```
@@ -255,60 +239,56 @@ export const shopifyMeta = {
255
239
 
256
240
  ## 子 Block(嵌套)
257
241
 
258
- Section 或 Block 可以声明接受的子 block 类型:
259
-
260
242
  ```tsx
261
243
  export const shopifyMeta = {
262
244
  name: "Group",
263
- blocks: [{ type: "@theme" }], // 接受所有主题 block
245
+ blocks: [{ type: "@theme" }],
264
246
  } satisfies ShopifyMeta;
265
247
  ```
266
248
 
267
249
  - `"@theme"` 接受当前主题中所有已注册的 block
268
- - `"类型名"` 只接受特定类型的 block(如 `"text"` 匹配 `blocks/text.liquid`)
269
- - 插件会自动在生成的 Liquid 中插入 `{% content_for 'blocks' %}`
250
+ - 插件自动插入 `{% content_for 'blocks' %}`
270
251
 
271
252
  ---
272
253
 
273
- ## CSS 样式
254
+ ## Snippet
274
255
 
275
- 使用 CSS Module 为组件添加样式。CSS 会在构建时被内联到生成的 Liquid 的 `{% stylesheet %}` 块中,由 Shopify 统一注入到 `<head>`,避免 FOUC 和额外 HTTP 请求。
276
-
277
- ```css
278
- /* frontend/sections/Hero.module.css */
279
- .hero {
280
- display: grid;
281
- padding: 2rem;
282
- }
283
- .hero-title {
284
- font-size: 3rem;
285
- font-weight: 700;
286
- }
287
- ```
256
+ 通过 `params` 传参(而非 `settings`):
288
257
 
289
258
  ```tsx
290
- // frontend/sections/Hero.tsx
291
- import styles from "./Hero.module.css";
259
+ // frontend/snippets/ProductCard.tsx
260
+ import { useSnippetParams } from "vite-plugin-react-shopify/runtime";
292
261
 
293
- export default function Hero() {
294
- return (
295
- <div className={styles["hero"]}>
296
- <h1 className={styles["hero-title"]}>Title</h1>
297
- </div>
298
- );
262
+ export const shopifyMeta = {
263
+ type: "snippet",
264
+ name: "Product Card",
265
+ params: ["title", "price"],
266
+ } satisfies ShopifyMeta;
267
+
268
+ export default function ProductCard() {
269
+ const { value: title } = useSnippetParams("title");
270
+ const { value: price } = useSnippetParams("price");
271
+ return <div><h3>{title}</h3><span>{price}</span></div>;
299
272
  }
300
273
  ```
301
274
 
302
- 生成的 Liquid 中会包含:
275
+ 调用方式:`{% render 'react-product-card', title: 'Hello', price: '$10' %}`
276
+
277
+ ---
278
+
279
+ ## CSS 样式
280
+
281
+ 使用普通 CSS 文件为组件添加样式。CSS 会在构建时被内联到 Liquid 的 `{% stylesheet %}` 块中。多个组件共享的 CSS 自动提取为 snippet:
303
282
 
304
- ```liquid
305
- {% stylesheet %}
306
- ._hero_abc123{display:grid;padding:2rem}
307
- ._hero-title_abc123{font-size:3rem;font-weight:700}
308
- {% endstylesheet %}
283
+ ```css
284
+ /* frontend/sections/Hero.css */
285
+ .hero { display: grid; padding: 2rem; }
309
286
  ```
310
287
 
311
- > **注意**:SSG 预渲染的 HTML 使用未经过 Vite hash 处理的类名。浏览器加载后 React 水合会替换为正确的 hash 类名。这是 CSS Module + SSG 的已知权衡。
288
+ ```tsx
289
+ import "./Hero.css";
290
+ export default function Hero() { return <div className="hero">...</div>; }
291
+ ```
312
292
 
313
293
  ---
314
294
 
@@ -316,182 +296,119 @@ export default function Hero() {
316
296
 
317
297
  ```ts
318
298
  vitePluginShopify({
319
- // === 路径配置 ===
320
- themeRoot: ".", // 主题根目录
321
- sourceCodeDir: "frontend", // React 源码目录(相对于 themeRoot)
322
- snippetFile: "shopify-importmap.liquid", // importmap 片段文件名
323
- buildDir: "assets", // Vite 构建产物输出目录(相对于 themeRoot)
324
-
325
- // === 构建配置 ===
326
- hash: false, // 产物文件名是否包含 content hash,默认 false
327
-
328
- // === 调试 ===
329
- debug: false, // 启用详细日志输出
330
-
331
- // === SSG 配置 ===
299
+ themeRoot: ".", // 主题根目录
300
+ sourceCodeDir: "frontend", // React 源码目录
301
+ snippetFile: "shopify-importmap.liquid",// importmap 片段文件名
302
+ buildDir: "assets", // 构建产物输出目录
303
+ debug: false, // 详细日志
332
304
  ssg: {
333
- directories: ["sections", "blocks", "templates"], // 扫描的目录
334
- prefix: { // 生成文件的命名前缀
335
- template: "page.react-", // templates → page.react-xxx.liquid
336
- section: "react-", // sections → react-xxx.liquid
337
- block: "react-", // blocks → react-xxx.liquid
305
+ directories: ["sections", "blocks", "templates", "snippets"],
306
+ prefix: {
307
+ template: "page.react-",
308
+ section: "react-",
309
+ block: "react-",
310
+ snippet: "react-",
338
311
  },
339
- outputName: "", // 自定义输出模板,留空使用前缀规则
312
+ outputName: "", // 自定义输出模板
313
+ cssPrefix: "css", // 共享 CSS snippet 前缀
340
314
  },
341
-
342
- // === Import Map 配置 ===
343
315
  importMap: {
344
- react: "https://esm.sh/react@19",
345
- reactDomClient: "https://esm.sh/react-dom@19/client",
316
+ react: "{{ 'react.js' | asset_url }}",
317
+ reactDomClient: "{{ 'react-dom.js' | asset_url }}",
346
318
  },
347
319
  });
348
320
  ```
349
321
 
350
322
  ---
351
323
 
352
- ## 命名约定
353
-
354
- ### 默认命名规则
355
-
356
- | 源码文件 | 目标类型 | 生成文件 |
357
- |----------|----------|----------|
358
- | `frontend/sections/MySection.tsx` | section | `sections/react-my-section.liquid` |
359
- | `frontend/blocks/MyBlock.tsx` | block | `blocks/react-my-block.liquid` |
360
- | `frontend/templates/Index.tsx` | template | `templates/page.react-index.liquid` |
361
-
362
- - 组件文件名通过 `toKebabCase` 转换为 kebab-case:
363
- - `HelloWorld` → `hello-world`
364
- - `FAQSection` → `faq-section`
365
- - JS 产物默认使用稳定文件名(如 `assets/build/hello-world.js`),设置 `hash: true` 后启用 content hash(如 `assets/build/hello-world-aBc123.js`)
366
-
367
- ### 自定义命名(`outputName`)
368
-
369
- ```ts
370
- ssg: {
371
- outputName: "{target}s/{kebab}" // 按类型分目录,不添加前缀
372
- }
373
- ```
374
-
375
- 支持占位符:`{type}`、`{kebab}`、`{pascal}`、`{target}`。
376
-
377
- ---
378
-
379
- ## 水合(Hydration)与编辑器事件
324
+ ## 水合与编辑器事件
380
325
 
381
326
  ### 水合流程
382
327
 
383
- 1. **SSG 预渲染**:构建时 React 组件渲染为静态 HTML,嵌入 `<div data-ssg-hydrate>`
384
- 2. **Settings 透传**:Liquid `<script type="application/json" data-ssg-props>` 包含 `{{ section.settings | json }}`
385
- 3. **Hydration JS**:Vite 为每个组件生成独立的 hydration chunk,读取 settings JSON 并调用 `hydrateRoot`
328
+ 1. **SSG 预渲染**:React 组件渲染为含 Liquid 表达式的 HTML,Liquid 组装器生成 `data-ssg-liquid` JSON bridge
329
+ 2. **Liquid 渲染**:Shopify 服务端替换表达式为实际值
330
+ 3. **Hydration JS**:Vite 为每个组件生成独立 hydration chunk,读取 JSON bridge 并调用 `hydrateRoot`
386
331
 
387
332
  ### 编辑器事件
388
333
 
389
- Hydration 脚本自动监听 Shopify 编辑器的 Section 生命周期事件:
390
-
391
334
  | 事件 | 行为 |
392
335
  |------|------|
393
- | `shopify:section:load` | Section 被添加或重新渲染时,重新 hydrate 组件 |
394
- | `shopify:section:unload` | Section 被删除或即将重新渲染时,unmount React 根节点 |
395
-
396
- 当用户在编辑器中修改 setting 时:Shopify 重新渲染 Section HTML → 触发 `unload`(清理旧 root)→ 触发 `load`(用新 props 重新 hydrate)。
336
+ | `shopify:section:load` | 重新 hydrate 组件 |
337
+ | `shopify:section:unload` | unmount React 根节点 |
397
338
 
398
339
  ---
399
340
 
400
- ## 运行时
401
-
402
- ### Liquid 组件
403
-
404
- 在 JSX 中嵌入原始 Liquid 代码:
405
-
406
- ```tsx
407
- import { Liquid } from "vite-plugin-react-shopify/runtime";
341
+ ## 调试
408
342
 
409
- export default function Section() {
410
- return (
411
- <div>
412
- <Liquid>{`{% if section.settings.show_title %}`}</Liquid>
413
- <h1>Title</h1>
414
- <Liquid>{`{% endif %}`}</Liquid>
415
- </div>
416
- );
417
- }
343
+ ```bash
344
+ DEBUG=vite-plugin-shopify:* npx vite build
418
345
  ```
419
346
 
420
- ### Import Map
347
+ ---
421
348
 
422
- 插件自动生成 `snippets/shopify-importmap.liquid`,包含 React 和 ReactDOM 的 CDN import map。在主题 `layout/theme.liquid` 的 `<head>` 中引入:
349
+ ## 开发规范
423
350
 
424
- ```liquid
425
- {% render 'shopify-importmap' %}
426
- ```
351
+ > 详见 `docs/hydration-issues.md`
427
352
 
428
- ---
353
+ ### JSX 子节点中相邻文本+表达式
429
354
 
430
- ## 调试
355
+ **必须使用模板字面量包裹**:
431
356
 
432
- 插件提供了两种方式启用详细日志输出,方便诊断构建问题。
433
-
434
- ### 方式一:插件选项
357
+ ```tsx
358
+ // ❌ 水合失败:相邻文本节点不匹配
359
+ <button>-{step}</button>
360
+ <li>title = {title}</li>
435
361
 
436
- ```ts
437
- vitePluginShopify({
438
- debug: true,
439
- });
362
+ // ✅ 模板字面量
363
+ <button>{`-${step}`}</button>
364
+ <li>{`title = ${title}`}</li>
440
365
  ```
441
366
 
442
- ### 方式二:环境变量
367
+ ### useState 初始化
443
368
 
444
- ```bash
445
- DEBUG=vite-plugin-shopify:* npx vite build
369
+ **不能依赖 `useLiquid` 返回值**:
370
+
371
+ ```tsx
372
+ // ❌ SSR 时 Number("{{ expr }}") = NaN → 不匹配
373
+ const [count, setCount] = useState(Number(s.initial) || 0);
374
+
375
+ // ✅ 固定默认值 + useEffect 同步
376
+ const [count, setCount] = useState(0);
377
+ useEffect(() => { setCount(parseLiquidNumber(s.initial, 0)); }, []);
446
378
  ```
447
379
 
448
- 两种方式效果相同,都会输出详细的构建过程信息。
380
+ ### 条件渲染
449
381
 
450
- ### Debug 输出示例
382
+ **不用 `{value && <Element />}`**,用 `hidden` 属性:
451
383
 
452
- 启用 debug 模式后,构建过程会输出:
384
+ ```tsx
385
+ // ❌ SSR 时表达式字符串始终 truthy → 结构不匹配
386
+ {showBanner && <Banner />}
453
387
 
454
- ```
455
- vite-plugin-shopify:entries scanned 5 entries: {"section":3,"block":2}
456
- vite-plugin-shopify:ssg:compiler found 5 entries to compile
457
- vite-plugin-shopify:ssg:compiler entry counter has 1 CSS files
458
- vite-plugin-shopify:ssg:compiler entry hello-world has 1 CSS files
459
- vite-plugin-shopify:ssg:compiler generated shared CSS snippet react-css-SharedCard (used by 2 entries)
460
- vite-plugin-shopify:ssg:compiler compiling counter (type=section, css inline=0, css snippets=1)
461
- vite-plugin-shopify:ssg:compiler bundling counter via esbuild
462
- vite-plugin-shopify:ssg:compiler esbuild bundle took 53ms
463
- ...
464
- [vite-plugin-shopify] Starting SSG compilation...
465
- [vite-plugin-shopify] Compiled 5 entries
466
- [vite-plugin-shopify] SSG compilation complete
388
+ // ✅ DOM 结构不变,仅切换属性
389
+ <section hidden={!parseLiquidBoolean(showBannerRaw)}>...</section>
467
390
  ```
468
391
 
469
- ### 日志级别
392
+ ### inline style 颜色值
470
393
 
471
- | 级别 | 触发条件 | 可见性 |
472
- |------|----------|--------|
473
- | `info` | 始终可见 | 构建阶段摘要、完成计数 |
474
- | `warn` | 始终可见 | 缺少依赖、跳过的组件 |
475
- | `error` | 始终可见 | 编译失败的组件及堆栈 |
476
- | `debug` | 仅 debug 模式 | 入口扫描结果、CSS 分发、esbuild 耗时、配置详情 |
394
+ **用 CSS 自定义属性代替内联颜色**:
477
395
 
478
- ### 诊断场景
396
+ ```tsx
397
+ // ❌ 浏览器规范化 hex → rgb → 不匹配
398
+ <div style={{ backgroundColor: color }} />
479
399
 
480
- - **组件未被识别** 开启 debug,检查 `scanned entries` 输出,确认文件和目录命名
481
- - **CSS 未生效** 开启 debug,检查 `has CSS files` 和 `css inline/snippets` 统计
482
- - **SSG 渲染失败** `error` 级别自动输出完整错误堆栈
483
- - **构建缓慢** → 开启 debug,检查每个组件的 `esbuild bundle took Xms`
400
+ // CSS 变量不归一化
401
+ <div style={{ "--accent": color } as React.CSSProperties} />
402
+ // CSS: .accent { background-color: var(--accent); }
403
+ ```
484
404
 
485
405
  ---
486
406
 
487
407
  ## 注意事项
488
408
 
489
- 1. **React / ReactDOM 不打包进 bundle**:通过 import map CDN 加载,避免重复打包和体积膨胀
490
- 2. **CSS Module hash 差异**:SSG 预渲染的 HTML 使用原始类名,水合后才会替换为 hash 类名。如需完美匹配,建议使用全局 CSS 而非 CSS Module,或接受短暂的样式跳跃
491
- 3. **SSG 使用默认 props 渲染**:构建时的预渲染使用组件的默认 prop 值,编辑器中修改 setting 后会通过水合更新
492
- 4. **Template 类型不包裹**:`type: "template"` 的组件 HTML 直接输出,不添加 section/block 外层结构,适用于整页模板
493
- 5. **Section 必须有预设才能通过编辑器添加**:没有 `presets` section 需要手动在 JSON 模板中引用,编辑器无法直接添加
494
- 6. **`{% content_for 'blocks' %}` 自动插入**:当 `shopifyMeta.blocks` 非空时,插件自动在生成的 Liquid 中插入子 block 渲染标签
495
- 7. **构建产物默认输出到 `assets/`**:如需与其他静态资源隔离,可设置 `buildDir: "assets/build"` 将产物输出到子目录,然后在 `.gitignore` 中添加 `assets/build/` 忽略该目录
496
- 8. **Watch 模式自动关闭压缩**:`vite build --watch` 时自动设置 `minify: false` 并启用 inline sourcemap,方便在浏览器中调试。生产构建(`vite build`)则使用正常压缩
497
- 9. **增量构建依赖 Rolldown 缓存**:watch 模式下 Rolldown 的 `ScanStageCache` 自动缓存模块图,文件变更时仅重新扫描变更模块,大幅加速构建。首次启动执行全量构建,后续为增量构建
409
+ - **SSR 渲染生成 Liquid 模板**:构建时的 SSG 渲染将 settings/params 访问映射为 Liquid tag(如 `{{ section.settings.title }}`),由 Shopify 服务端解析为真实值
410
+ - **Template 类型不包裹**:`type: "template"` HTML 直接输出,不添加 section/block 外层结构
411
+ - **Section 必须有预设才能通过编辑器添加**
412
+ - **构建产物默认输出到 `assets/`**:可通过 `buildDir` 配置子目录
413
+ - **Watch 模式**:`vite build --watch` 时自动关闭压缩并启用 inline sourcemap
414
+ - **Settings 按需追踪**:SSR 阶段追踪组件 render body 中实际访问的 Liquid 表达式,只将访问过的表达式注入 `data-ssg-liquid` JSON bridge