vite-plugin-react-shopify 2.2.1 → 2.2.3

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,35 @@
1
1
  # vite-plugin-react-shopify
2
2
 
3
- 使用 React 组件编写 Shopify 主题的 Section、Block、Snippet 和 Template。在构建时通过 SSG(Static Site Generation)将 React 组件编译为 Shopify Liquid 文件,运行时由 React 进行水合(hydration)。
3
+ React 组件编写 Shopify 主题的 Section、Block、Snippet 和 Template。构建时通过 SSG React 组件编译为 Shopify Liquid 文件,运行时由 React 进行 hydration
4
+
5
+ ## 背景
6
+
7
+ Shopify 主题开发长期依赖 Liquid 模板语言 + 原生 JS,存在几个痛点:
8
+
9
+ **AI 开发成本高**。主流 AI 模型对 Liquid 的训练数据不足,每次会话需要注入大量 Liquid 语法、filter、对象模型等上下文,Token 消耗大且容易出错,大量优质 Context 被浪费在语法上。React/TypeScript 生态完善,模型理解更准确。
10
+
11
+ **无法做单元测试**。Liquid 模板文件无法独立运行测试,逻辑正确性只能在 Shopify 环境中验证,调试周期长。React 组件可完整写单元测试,CI 中即可发现问题。
12
+
13
+ **学习曲线和产物质量**。Liquid 语法与主流前端框架差异大,新开发者上手成本高。原生 JS 缺乏模块系统和 tree-shaking,产物体积难以控制。React 的 JSX 语法、组件化模型、生态工具链在这些方面有显著优势。
14
+
15
+ 本插件通过 SSG(Static Site Generation)在构建时把 React 组件预渲染为 Liquid 模板,保留与 Shopify 生态的完全兼容,同时让开发者用上 React 全栈工具链。
16
+
17
+ ---
18
+
19
+ ## 目录
20
+
21
+ 1. [安装](#安装)
22
+ 2. [使用](#使用)
23
+ 3. [目录结构](#目录结构)
24
+ 4. [组件开发](#组件开发)
25
+ 5. [数据读取 API](#数据读取-api)
26
+ 6. [CSS 样式](#css-样式)
27
+ 7. [配置选项](#配置选项)
28
+ 8. [开发工作流](#开发工作流)
29
+ 9. [水合注意事项](#水合注意事项)
30
+ 10. [常见问题](#常见问题)
31
+
32
+ ---
4
33
 
5
34
  ## 安装
6
35
 
@@ -8,24 +37,61 @@
8
37
  pnpm add vite-plugin-react-shopify
9
38
  ```
10
39
 
11
- ## 快速开始
40
+ 此外还需要安装 React 和 Vite(peer dependency):
41
+
42
+ ```bash
43
+ pnpm add react react-dom vite
44
+ pnpm add -D @types/react @types/react-dom typescript
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 使用
50
+
51
+ 前提:已有 Shopify 主题(包含 `layout/`、`sections/`、`templates/` 等目录)。
52
+
53
+ ```bash
54
+ # 1. 安装依赖
55
+ pnpm add vite-plugin-react-shopify react react-dom vite
56
+ pnpm add -D @types/react @types/react-dom typescript
57
+ ```
12
58
 
13
59
  ```ts
14
- // vite.config.ts
60
+ // 2. vite.config.ts
15
61
  import vitePluginShopify from "vite-plugin-react-shopify";
16
62
 
17
63
  export default {
18
64
  plugins: [
19
65
  vitePluginShopify({
20
- themeRoot: ".", // 主题根目录,默认 "./"
21
- sourceCodeDir: "frontend", // 源码目录,默认 "frontend"
66
+ sourceCodeDir: "frontend", // React 源码目录,默认 "frontend",可改为 "react" 等
22
67
  }),
23
68
  ],
24
69
  };
25
70
  ```
26
71
 
72
+ [`template/`](https://github.com/He110te4m/react-shopify/tree/main/template) 提供了 `frontend/` 骨架(可重命名为 `sourceCodeDir` 的值)、`tsconfig.json`、`_gitignore` 等样板文件,可直接复制到主题目录:
73
+
74
+ ```bash
75
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/He110te4m/react-shopify.git _tmp
76
+ cd _tmp && git sparse-checkout set template && cd ..
77
+ cp -r _tmp/template/* my-shopify-theme/
78
+ # 如果 sourceCodeDir 不是 "frontend",重命名模板目录,并同步修改 tsconfig.json 的 include 路径
79
+ mv my-shopify-theme/frontend my-shopify-theme/react
80
+ rm -rf _tmp
81
+ ```
82
+
83
+ 在 `layout/theme.liquid` 的 `<head>` 中添加:
84
+
85
+ ```liquid
86
+ {% render 'shopify-importmap' %}
87
+ ```
88
+
89
+ 将 `template/_gitignore` 内容追加到主题 `.gitignore`。
90
+
91
+ 编写组件:
92
+
27
93
  ```tsx
28
- // frontend/sections/HelloWorld.tsx
94
+ // {sourceCodeDir}/sections/HelloWorld.tsx
29
95
  import type { ShopifyMeta } from "vite-plugin-react-shopify";
30
96
  import { useSectionSettings } from "vite-plugin-react-shopify/runtime";
31
97
 
@@ -43,19 +109,34 @@ export default function HelloWorld() {
43
109
  }
44
110
  ```
45
111
 
46
- 构建后生成 `sections/react-hello-world.liquid`。
47
-
48
- ### 开发
112
+ 构建:
49
113
 
50
114
  ```bash
51
- # 终端 1: 启动 Vite 构建监听
52
- pnpm dev # → vite build --watch
115
+ pnpm dev # vite build --watch,配合 shopify theme dev 使用
116
+ ```
53
117
 
54
- # 终端 2: 启动 Shopify 主题开发
55
- shopify theme dev
118
+ 构建后生成 `sections/react-hello-world.liquid`,在 Shopify 管理后台「添加 Section」即可找到。
119
+
120
+ ### 文件命名冲突
121
+
122
+ 插件生成的文件名格式为 `<prefix><组件名-kebab>.liquid`,默认 prefix:
123
+
124
+ | 类型 | prefix | 示例 |
125
+ |------|--------|------|
126
+ | section | `react-` | `react-hello-world.liquid` |
127
+ | block | `react-` | `react-text-block.liquid` |
128
+ | snippet | `react-` | `react-my-snippet.liquid` |
129
+ | template | `page.react-` | `page.react-index.liquid` |
130
+
131
+ 如需修改,通过 `ssg.prefix` 配置:
132
+
133
+ ```ts
134
+ vitePluginShopify({
135
+ ssg: { prefix: { section: "r-", block: "r-", snippet: "r-", template: "page.r-" } },
136
+ });
56
137
  ```
57
138
 
58
- Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测到变化 → 推送主题 → 编辑器热更新。
139
+ ---
59
140
 
60
141
  ---
61
142
 
@@ -63,200 +144,316 @@ Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测
63
144
 
64
145
  ```
65
146
  my-theme/
66
- ├── frontend/ ← React 源码
147
+ ├── frontend/ ← React 源码
67
148
  │ ├── sections/
68
- │ │ └── HelloWorld.tsx
149
+ │ │ ├── HeroBanner.tsx
150
+ │ │ └── HeroBanner.css
69
151
  │ ├── blocks/
70
152
  │ │ └── TextBlock.tsx
71
153
  │ ├── snippets/
72
- │ │ └── MySnippet.tsx
73
- └── templates/
74
- └── index.tsx
75
- ├── sections/ 生成的 Liquid + 原生 Liquid
154
+ │ │ └── ProductCard.tsx
155
+ ├── templates/
156
+ └── index.tsx
157
+ │ └── components/ 共享 React 组件
158
+ │ └── SharedCard.tsx
159
+ ├── sections/ ← 生成的 Liquid + 原生 Liquid
160
+ │ ├── react-hero-banner.liquid ← 由插件生成
161
+ │ └── header.liquid ← 原生 Liquid
76
162
  ├── blocks/
163
+ │ ├── react-text-block.liquid ← 由插件生成
164
+ │ └── text.liquid ← 原生 Liquid
77
165
  ├── snippets/
166
+ │ ├── react-product-card.liquid ← 由插件生成
167
+ │ ├── css-SharedCard.liquid ← 自动提取的共享 CSS
168
+ │ └── shopify-importmap.liquid ← 自动生成的 importmap
78
169
  ├── templates/
79
- └── assets/ Vite 构建产物(可通过 buildDir 配置子目录)
170
+ └── page.react-index.liquid 由插件生成
171
+ ├── assets/ ← Vite 构建产物(可通过 buildDir 配置子目录)
172
+ │ └── build/
173
+ │ ├── react.js
174
+ │ ├── react-dom.js
175
+ │ ├── hero-banner-xxx.js
176
+ │ └── manifest.json
177
+ ├── layout/
178
+ │ └── theme.liquid ← 需手动添加 {% render 'shopify-importmap' %}
179
+ ├── vite.config.ts
180
+ ├── tsconfig.json
181
+ └── package.json
182
+ ```
183
+
184
+ ---
185
+
186
+ ## 组件开发
187
+
188
+ ### shopifyMeta 导出
189
+
190
+ 每个 React 组件文件必须导出一个 `shopifyMeta` 对象,定义 Shopify Schema:
191
+
192
+ ```tsx
193
+ import type { ShopifyMeta } from "vite-plugin-react-shopify";
194
+
195
+ export const shopifyMeta = {
196
+ // === 基本信息 ===
197
+ name: "组件名称", // 必填,≤ 25 字符
198
+ type: "section", // 可选,覆盖目录推断
199
+ tag: "section", // 可选,外层 HTML 标签(默认 "div")
200
+ class: "custom-class", // 可选,外层 CSS 类名
201
+ limit: 1, // 可选,同一页面最多出现次数
202
+
203
+ // === Settings ===
204
+ settings: [
205
+ { type: "text", id: "title", label: "Title", default: "Hello" },
206
+ { type: "checkbox", id: "show_banner", label: "Show Banner", default: false },
207
+ ],
208
+
209
+ // === Blocks(子块嵌套) ===
210
+ blocks: [{ type: "@theme" }],
211
+ max_blocks: 10,
212
+
213
+ // === Presets(在管理后台添加时的预设) ===
214
+ presets: [
215
+ { name: "Hero (Light)", category: "Banners" },
216
+ ],
217
+
218
+ // === 其他 ===
219
+ enabled_on: { templates: ["index", "product"] },
220
+ disabled_on: { templates: ["cart"] },
221
+ } satisfies ShopifyMeta;
80
222
  ```
81
223
 
224
+ ### 默认导出
225
+
226
+ 必须有一个 `default export` 作为 React 组件:
227
+
228
+ ```tsx
229
+ export default function MySection() {
230
+ return <div>...</div>;
231
+ }
232
+ ```
233
+
234
+ ### 类型映射
235
+
236
+ 插件根据组件文件所在的目录自动推断类型:
237
+
238
+ | 源码路径 | 推断类型 | 生成文件 |
239
+ |----------|----------|----------|
240
+ | `frontend/sections/X.tsx` | `section` | `sections/react-x.liquid` |
241
+ | `frontend/blocks/X.tsx` | `block` | `blocks/react-x.liquid` |
242
+ | `frontend/templates/X.tsx` | `template` | `templates/page.react-x.liquid` |
243
+ | `frontend/snippets/X.tsx` | `snippet` | `snippets/react-x.liquid` |
244
+
245
+ 可在 `shopifyMeta.type` 中显式指定覆盖。
246
+
82
247
  ---
83
248
 
84
- ## Settings 和 Liquid 数据读取
249
+ ## 数据读取 API
85
250
 
86
- ### 核心 API
251
+ 所有 Shopify Liquid 数据通过 runtime hooks 读取,导入路径为 `vite-plugin-react-shopify/runtime`。
87
252
 
88
- 所有 Liquid 数据的读取通过统一的基础 hook `useLiquid(expr)`:
253
+ ### API 总览
89
254
 
90
255
  ```tsx
91
256
  import {
92
- useLiquid,
257
+ useLiquidValue,
93
258
  useLiquidValues,
94
259
  useSectionSettings,
95
260
  useBlockSettings,
96
261
  useSnippetParams,
97
262
  useBlockParams,
263
+ parseLiquidBoolean,
264
+ parseLiquidNumber,
265
+ LiquidDataProvider,
266
+ LiquidDataContext,
98
267
  } from "vite-plugin-react-shopify/runtime";
99
268
  ```
100
269
 
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")` |
270
+ | Hook | 签名 | 说明 |
271
+ |------|------|------|
272
+ | `useLiquidValue(expr)` | `[string \| undefined, setter]` | 读取任意 Liquid 表达式 |
273
+ | `useLiquidValue(expr, "number")` | `[number, setter]` | 读取并解析为数字 |
274
+ | `useLiquidValue(expr, "boolean")` | `[boolean, setter]` | 读取并解析为布尔 |
275
+ | `useLiquidValues(map, types?)` | 推断类型对象 | 批量读取多个表达式 |
276
+ | `useSectionSettings(key)` | `{ value: string \| undefined }` | 读取 Section setting |
277
+ | `useBlockSettings(key)` | `{ value: string \| undefined }` | 读取 Block setting |
278
+ | `useSnippetParams(key)` | `{ value: string \| undefined }` | 读取 Snippet 参数 |
279
+ | `useBlockParams(key)` | `{ value: string \| undefined }` | 读取 Block 参数 |
280
+
281
+ ### 辅助函数
282
+
283
+ | 函数 | 说明 |
284
+ |------|------|
285
+ | `parseLiquidBoolean(value)` | 安全解析布尔值(`""` → false,`"false"` → false) |
286
+ | `parseLiquidNumber(value, defaultVal?)` | 安全解析数字(NaN → defaultVal) |
109
287
 
110
- 所有 hook 返回 `{ value: string | undefined }`(`useLiquidValues` 返回 `{ values: Record }`)。
288
+ ### 使用示例
289
+
290
+ **读取 Section Settings:**
111
291
 
112
292
  ```tsx
113
293
  export default function ProductBanner() {
114
294
  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
- });
295
+ return <h1>{title}</h1>;
296
+ }
297
+ ```
298
+
299
+ **读取任意 Liquid 值:**
120
300
 
301
+ ```tsx
302
+ export default function ProductPrice() {
303
+ const [price] = useLiquidValue("product.price");
304
+ const [comparePrice] = useLiquidValue("product.compare_at_price");
121
305
  return (
122
306
  <div>
123
- <h1>{title}</h1>
124
307
  <span>{price}</span>
125
- <p>{p.name}</p>
126
- <div dangerouslySetInnerHTML={{ __html: p.desc || "" }} />
308
+ {comparePrice && <s>{comparePrice}</s>}
127
309
  </div>
128
310
  );
129
311
  }
130
312
  ```
131
313
 
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` 读取
148
-
149
- ---
150
-
151
- ## 组件约定
152
-
153
- 每个 React 组件必须导出:
154
-
155
- ### 1. `shopifyMeta` — Shopify Schema 定义
314
+ **带类型解析:**
156
315
 
157
316
  ```tsx
158
- export const shopifyMeta = {
159
- name: "组件名称",
160
- type: "section", // 可选,覆盖目录推断
161
- tag: "section", // 可选,外层 HTML 标签,默认 "div"
162
- class: "custom-class", // 可选,外层 CSS 类名
163
- limit: 1,
164
- max_blocks: 10,
165
- settings: [...],
166
- blocks: [...],
167
- presets: [...],
168
- } satisfies ShopifyMeta;
317
+ export default function Counter() {
318
+ const [initial] = useLiquidValue("section.settings.initial_count", "number");
319
+ const [show] = useLiquidValue("section.settings.show_banner", "boolean");
320
+
321
+ return (
322
+ <div>
323
+ <p>Initial: {initial}</p>
324
+ {show && <p>Banner visible</p>}
325
+ </div>
326
+ );
327
+ }
169
328
  ```
170
329
 
171
- ### 2. `default export` — React 组件
330
+ **批量读取:**
172
331
 
173
332
  ```tsx
174
- export default function MyComponent() {
175
- return <div>...</div>;
333
+ export default function ProductInfo() {
334
+ const p = useLiquidValues(
335
+ { name: "product.title", desc: "product.description" },
336
+ { desc: "string" }
337
+ );
338
+ return (
339
+ <div>
340
+ <h2>{p.name}</h2>
341
+ <div dangerouslySetInnerHTML={{ __html: p.desc || "" }} />
342
+ </div>
343
+ );
176
344
  }
177
345
  ```
178
346
 
179
- ### 类型推断
347
+ ### 数据流 / 水合流程
180
348
 
181
- | 源码路径 | 目标类型 | 生成文件 |
182
- |----------|----------|----------|
183
- | `frontend/sections/X.tsx` | `section` | `sections/react-x.liquid` |
184
- | `frontend/blocks/X.tsx` | `block` | `blocks/react-x.liquid` |
185
- | `frontend/templates/X.tsx` | `template` | `templates/page.react-x.liquid` |
186
- | `frontend/snippets/X.tsx` | `snippet` | `snippets/react-x.liquid` |
349
+ ```
350
+ 1. SSR 阶段(构建时 Node.js)
351
+ useLiquidValue("section.settings.title")
352
+ 返回字符串 "{{ section.settings.title }}" ← Liquid 模板变量
353
+ 同时追踪该表达式到 __shopify_ssg_liquid_track
354
+
355
+ 2. Liquid 组装
356
+ → 生成 <script type="application/json" data-ssg-liquid>
357
+ { "section.settings.title": {{ section.settings.title | json }} }
358
+
359
+ 3. Shopify 服务端渲染
360
+ → Liquid 引擎将表达式替换为实际值
361
+ → JSON bridge 包含实际数据
362
+
363
+ 4. 客户端 hydration
364
+ → LiquidDataProvider 接收 JSON bridge
365
+ → useLiquidValue 从 context 读取实际值
366
+ → hydrateRoot 完成 React 水合
367
+ ```
187
368
 
188
369
  ---
189
370
 
190
- ## 设置 (Settings)
191
-
192
- ```tsx
193
- import type { SettingSchema } from "vite-plugin-react-shopify";
194
-
195
- const settings = [
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 },
200
- ] satisfies SettingSchema[];
201
- ```
371
+ ## Settings 设置类型
202
372
 
203
373
  ### 基本输入类型
204
374
 
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` |
375
+ | 类型 | 额外字段 | 说明 |
376
+ |------|----------|------|
377
+ | `checkbox` | `default`(`boolean`) | 复选框 |
378
+ | `number` | `placeholder` | 数字输入 |
379
+ | `radio` | `options`(必填) | 单选框 |
380
+ | `range` | `min`(必填)、`max`(必填)、`step`、`unit` | 范围滑块 |
381
+ | `select` | `options`(必填) | 下拉选择 |
382
+ | `text` | `placeholder` | 单行文本 |
383
+ | `textarea` | `placeholder` | 多行文本 |
214
384
 
215
385
  ### 专用输入类型
216
386
 
217
387
  `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`
218
388
 
219
- ### 侧边栏类型
389
+ 完整类型定义可从 `vite-plugin-react-shopify` 导入:
390
+
391
+ ```ts
392
+ import type { TextSetting, SelectSetting, RangeSetting } from "vite-plugin-react-shopify";
393
+ ```
220
394
 
221
- `header`、`paragraph`、`line_break`。使用 `content` 字段,不保存值。
395
+ ### 侧边栏元素
396
+
397
+ 不保存值,仅用于在管理后台侧边栏中展示信息:
398
+
399
+ ```tsx
400
+ settings: [
401
+ { type: "header", content: "Layout Settings" },
402
+ { type: "paragraph", content: "Choose how this section displays." },
403
+ { type: "line_break" },
404
+ { type: "select", id: "layout", label: "Layout", ... },
405
+ ] satisfies SettingSchema[]
406
+ ```
222
407
 
223
408
  ---
224
409
 
225
- ## 预设 (Presets)
410
+ ## Presets 预设
226
411
 
227
412
  ```tsx
228
413
  export const shopifyMeta = {
229
414
  name: "Hero Banner",
230
415
  presets: [
231
416
  { name: "Hero (Light)", category: "Banners" },
232
- { name: "Hero (Dark)", category: "Banners",
233
- settings: { bg_color: "#000", text_color: "#fff" } },
417
+ {
418
+ name: "Hero (Dark)",
419
+ category: "Banners",
420
+ settings: { bg_color: "#000", text_color: "#fff" },
421
+ },
234
422
  ],
235
423
  } satisfies ShopifyMeta;
236
424
  ```
237
425
 
426
+ - Section 必须有至少一个 preset 才能在管理后台通过「添加 Section」找到
427
+ - `category` 用于在添加面板中分组
428
+ - `settings` 覆盖 setting 的默认值
429
+
238
430
  ---
239
431
 
240
- ## Block(嵌套)
432
+ ## Block 嵌套
433
+
434
+ Section 可包含子 Block:
241
435
 
242
436
  ```tsx
243
437
  export const shopifyMeta = {
244
- name: "Group",
245
- blocks: [{ type: "@theme" }],
438
+ name: "Product Grid",
439
+ blocks: [{ type: "@theme" }], // "@theme" 接受主题中所有已注册的 block
440
+ max_blocks: 10,
246
441
  } satisfies ShopifyMeta;
247
442
  ```
248
443
 
249
- - `"@theme"` 接受当前主题中所有已注册的 block
250
- - 插件自动插入 `{% content_for 'blocks' %}`
444
+ - `"@theme"` 接受当前主题中所有已注册的 block 类型
445
+ - 也可指定具体 block 类型:`blocks: [{ type: "react-text-block" }]`
446
+ - 插件自动在 Section 模板中插入 `{% content_for 'blocks' %}`
251
447
 
252
448
  ---
253
449
 
254
450
  ## Snippet
255
451
 
256
- 通过 `params` 传参(而非 `settings`):
452
+ Snippet 通过 `params` 传参(而非 `settings`):
257
453
 
258
454
  ```tsx
259
455
  // frontend/snippets/ProductCard.tsx
456
+ import type { ShopifyMeta } from "vite-plugin-react-shopify";
260
457
  import { useSnippetParams } from "vite-plugin-react-shopify/runtime";
261
458
 
262
459
  export const shopifyMeta = {
@@ -268,50 +465,80 @@ export const shopifyMeta = {
268
465
  export default function ProductCard() {
269
466
  const { value: title } = useSnippetParams("title");
270
467
  const { value: price } = useSnippetParams("price");
271
- return <div><h3>{title}</h3><span>{price}</span></div>;
468
+ return (
469
+ <div>
470
+ <h3>{title}</h3>
471
+ <span>{price}</span>
472
+ </div>
473
+ );
272
474
  }
273
475
  ```
274
476
 
275
- 调用方式:`{% render 'react-product-card', title: 'Hello', price: '$10' %}`
477
+ 调用方式:
478
+
479
+ ```liquid
480
+ {% render 'react-product-card', title: product.title, price: product.price %}
481
+ ```
276
482
 
277
483
  ---
278
484
 
279
485
  ## CSS 样式
280
486
 
281
- 使用普通 CSS 文件为组件添加样式。CSS 会在构建时被内联到 Liquid 的 `{% stylesheet %}` 块中。多个组件共享的 CSS 自动提取为 snippet:
487
+ ### 组件级 CSS
488
+
489
+ 创建与组件同名的 CSS 文件,在组件中导入即可。CSS 会自动内联到 Liquid 的 `{% stylesheet %}` 块中:
282
490
 
283
491
  ```css
284
- /* frontend/sections/Hero.css */
492
+ /* frontend/sections/HeroBanner.css */
285
493
  .hero { display: grid; padding: 2rem; }
286
494
  ```
287
495
 
288
496
  ```tsx
289
- import "./Hero.css";
290
- export default function Hero() { return <div className="hero">...</div>; }
497
+ import "./HeroBanner.css";
498
+ export default function HeroBanner() {
499
+ return <div className="hero">...</div>;
500
+ }
291
501
  ```
292
502
 
503
+ ### 共享 CSS
504
+
505
+ 被多个组件同时使用的 CSS 文件会自动提取为独立的 snippet(如 `snippets/css-SharedCard.liquid`),避免代码重复。
506
+
507
+ ### CSS Modules
508
+
509
+ 暂不支持 CSS Modules(`.module.css`),建议使用 BEM 或其他命名约定。
510
+
293
511
  ---
294
512
 
295
513
  ## 配置选项
296
514
 
297
515
  ```ts
516
+ import type { Options } from "vite-plugin-react-shopify";
517
+
298
518
  vitePluginShopify({
299
- themeRoot: ".", // 主题根目录
300
- sourceCodeDir: "frontend", // React 源码目录
301
- snippetFile: "shopify-importmap.liquid",// importmap 片段文件名
302
- buildDir: "assets", // 构建产物输出目录
303
- debug: false, // 详细日志
519
+ // === 路径配置 ===
520
+ themeRoot: ".", // 主题根目录,默认 "./"
521
+ sourceCodeDir: "frontend", // React 源码目录,默认 "frontend"
522
+ buildDir: "assets", // 构建产物输出目录,默认 "assets"
523
+
524
+ // === 调试 ===
525
+ debug: false, // 详细日志,也可用环境变量 DEBUG="vite-plugin-shopify:*"
526
+
527
+ // === 生成配置 ===
528
+ snippetFile: "shopify-importmap.liquid",// importmap snippet 文件名
304
529
  ssg: {
305
530
  directories: ["sections", "blocks", "templates", "snippets"],
306
531
  prefix: {
307
- template: "page.react-",
308
- section: "react-",
309
- block: "react-",
310
- snippet: "react-",
532
+ template: "page.react-", // template 前缀
533
+ section: "react-", // section 前缀
534
+ block: "react-", // block 前缀
535
+ snippet: "react-", // snippet 前缀
311
536
  },
312
- outputName: "", // 自定义输出模板
313
- cssPrefix: "css", // 共享 CSS snippet 前缀
537
+ outputName: "", // 自定义输出文件名模板
538
+ cssPrefix: "css", // 共享 CSS snippet 前缀
314
539
  },
540
+
541
+ // === 依赖映射 ===
315
542
  importMap: {
316
543
  react: "{{ 'react.js' | asset_url }}",
317
544
  reactDomClient: "{{ 'react-dom.js' | asset_url }}",
@@ -319,40 +546,59 @@ vitePluginShopify({
319
546
  });
320
547
  ```
321
548
 
549
+ ### outputName 模板变量
550
+
551
+ 设置 `ssg.outputName` 可自定义输出文件名,支持以下变量:
552
+
553
+ | 变量 | 说明 | 示例 |
554
+ |------|------|------|
555
+ | `{type}` | 组件类型 | `section` |
556
+ | `{kebab}` | kebab-case 组件名 | `hero-banner` |
557
+ | `{pascal}` | PascalCase 组件名 | `HeroBanner` |
558
+ | `{target}` | 目标目录名 | `sections` |
559
+
560
+ ```ts
561
+ // 示例:去掉 react- 前缀
562
+ ssg: { outputName: "{kebab}.liquid" }
563
+ ```
564
+
322
565
  ---
323
566
 
324
- ## 水合与编辑器事件
567
+ ## 开发工作流
325
568
 
326
- ### 水合流程
569
+ ### 开发模式
327
570
 
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`
571
+ ```bash
572
+ # 终端 1: Vite 构建监听
573
+ pnpm dev
331
574
 
332
- ### 编辑器事件
575
+ # 终端 2: Shopify CLI(需要 Shopify CLI 已安装并登录)
576
+ shopify theme dev
577
+ ```
333
578
 
334
- | 事件 | 行为 |
335
- |------|------|
336
- | `shopify:section:load` | 重新 hydrate 组件 |
337
- | `shopify:section:unload` | unmount React 根节点 |
579
+ Vite 监听文件变化 增量构建 → 写入磁盘 → Shopify CLI 检测变化 → 推送主题 → 热更新。
338
580
 
339
- ---
581
+ ### 生产构建
582
+
583
+ ```bash
584
+ pnpm build # → vite build
585
+ ```
340
586
 
341
- ## 调试
587
+ ### 调试
342
588
 
343
589
  ```bash
344
- DEBUG=vite-plugin-shopify:* npx vite build
590
+ DEBUG=vite-plugin-shopify:* pnpm dev
345
591
  ```
346
592
 
347
593
  ---
348
594
 
349
- ## 开发规范
595
+ ## 水合注意事项
350
596
 
351
- > 详见 `docs/hydration-issues.md`
597
+ > 详细原理见 `docs/hydration-issues.md`。以下是开发时必须遵守的规范。
352
598
 
353
- ### JSX 子节点中相邻文本+表达式
599
+ ### 1. JSX 中相邻文本 + 表达式
354
600
 
355
- **必须使用模板字面量包裹**:
601
+ 相邻文本和表达式必须用模板字面量合并:
356
602
 
357
603
  ```tsx
358
604
  // ❌ 水合失败:相邻文本节点不匹配
@@ -364,12 +610,14 @@ DEBUG=vite-plugin-shopify:* npx vite build
364
610
  <li>{`title = ${title}`}</li>
365
611
  ```
366
612
 
367
- ### useState 初始化
613
+ > 插件内置了 `hydration-fix` 模块,在构建时会**自动修复**大多数此类问题。但推荐在源码层面直接使用模板字面量。
614
+
615
+ ### 2. useState 初始化
368
616
 
369
- **不能依赖 `useLiquid` 返回值**:
617
+ 不要依赖 Liquid 值初始化 useState:
370
618
 
371
619
  ```tsx
372
- // ❌ SSR 时 Number("{{ expr }}") = NaN → 不匹配
620
+ // ❌ SSR 时 Number("{{ expr }}") = NaN
373
621
  const [count, setCount] = useState(Number(s.initial) || 0);
374
622
 
375
623
  // ✅ 固定默认值 + useEffect 同步
@@ -377,9 +625,11 @@ const [count, setCount] = useState(0);
377
625
  useEffect(() => { setCount(parseLiquidNumber(s.initial, 0)); }, []);
378
626
  ```
379
627
 
380
- ### 条件渲染
628
+ > 使用 `useLiquidValue(expr, "number")` 可避免此问题,它已在内部处理了 SSR/客户端的同步。
629
+
630
+ ### 3. 条件渲染
381
631
 
382
- **不用 `{value && <Element />}`**,用 `hidden` 属性:
632
+ 避免 `{cond && <Element />}`,用 `hidden` 属性代替:
383
633
 
384
634
  ```tsx
385
635
  // ❌ SSR 时表达式字符串始终 truthy → 结构不匹配
@@ -389,26 +639,75 @@ useEffect(() => { setCount(parseLiquidNumber(s.initial, 0)); }, []);
389
639
  <section hidden={!parseLiquidBoolean(showBannerRaw)}>...</section>
390
640
  ```
391
641
 
392
- ### inline style 颜色值
642
+ ### 4. 内联颜色值
393
643
 
394
- **用 CSS 自定义属性代替内联颜色**:
644
+ CSS 自定义属性代替内联颜色:
395
645
 
396
646
  ```tsx
397
- // ❌ 浏览器规范化 hex rgb → 不匹配
647
+ // ❌ 浏览器将 hex 规范化为 rgb → 不匹配
398
648
  <div style={{ backgroundColor: color }} />
399
649
 
400
650
  // ✅ CSS 变量不归一化
401
651
  <div style={{ "--accent": color } as React.CSSProperties} />
402
- // CSS: .accent { background-color: var(--accent); }
652
+ ```
653
+
654
+ ```css
655
+ /* 配套 CSS */
656
+ .accent-bg { background-color: var(--accent, #6c63ff); }
657
+ ```
658
+
659
+ ---
660
+
661
+ ## 常见问题
662
+
663
+ ### Q: 构建后看不到生成的 Liquid 文件?
664
+
665
+ 检查终端是否有错误日志,特别是:
666
+ - `shopifyMeta.name` 是否超过 25 字符
667
+ - 是否有 setting 的 `default` 为空字符串 `""`
668
+
669
+ ### Q: 添加新 Section 后管理后台找不到?
670
+
671
+ Section 需要至少一个 `presets` 条目。检查 `shopifyMeta.presets`。
672
+
673
+ ### Q: 页面水合报错(Minified React error #418)?
674
+
675
+ 水合不匹配。常见原因见[水合注意事项](#水合注意事项),或逐个排查:
676
+
677
+ 1. 检查是否有相邻文本+表达式(场景 4)
678
+ 2. 检查是否有内联颜色值(场景 2)
679
+ 3. 检查 useState 是否依赖 Liquid 值(场景 5)
680
+ 4. 检查是否有条件渲染导致 DOM 结构差异(场景 3)
681
+
682
+ ### Q: 如何让 React 组件与原生 Liquid 文件共存?
683
+
684
+ 插件只生成带 `react-` 前缀的文件,不会覆盖已有的原生 Liquid 文件。你可以在模板和 Section Group 中自由混合使用。
685
+
686
+ ### Q: 如何修改构建产物输出路径?
687
+
688
+ 通过 `buildDir` 配置:
689
+
690
+ ```ts
691
+ vitePluginShopify({
692
+ buildDir: "assets/react-app", // 输出到 assets/react-app/
693
+ });
694
+ ```
695
+
696
+ ### Q: 生成的 JS 文件太大怎么办?
697
+
698
+ 生产构建会自动压缩。Vendor chunks(`react.js`、`react-dom.js`)被提取为独立文件,可被浏览器缓存。
699
+
700
+ ### Q: HTML 富文本内容如何渲染?
701
+
702
+ ```tsx
703
+ const { value: html } = useSectionSettings("richtext_content");
704
+ return <div dangerouslySetInnerHTML={{ __html: html || "" }} />;
403
705
  ```
404
706
 
405
707
  ---
406
708
 
407
- ## 注意事项
709
+ ## 相关文档
408
710
 
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
711
+ - [水合问题详解](docs/hydration-issues.md)
712
+ - [架构设计文档](packages/vite-plugin-react-shopify/docs/design.md)
713
+ - [Shopify Theme Architecture](https://shopify.dev/docs/storefronts/themes/architecture)
package/dist/index.js CHANGED
@@ -632,14 +632,17 @@ function renderEntry(tmpFile, entry, projectRoot) {
632
632
  globalThis.__shopify_ssg_liquid_filters = filterMap;
633
633
  const trackedExpressions = /* @__PURE__ */ new Set();
634
634
  globalThis.__shopify_ssg_liquid_track = trackedExpressions;
635
+ const liquidBlocks = [];
636
+ globalThis.__shopify_ssg_liquid_blocks = liquidBlocks;
635
637
  const element = createElement(Component);
636
638
  let html = renderToStaticMarkup(element);
637
639
  delete globalThis.__shopify_ssg_liquid_track;
640
+ delete globalThis.__shopify_ssg_liquid_blocks;
638
641
  delete globalThis.__shopify_ssg_liquid_filters;
639
642
  html = normalizeVoidElements(html);
640
643
  html = normalizeStyleAttributes(html);
641
644
  html = unwrapHtmlEntities(html);
642
- return { html, trackedExpressions, entryMeta: entry.meta };
645
+ return { html, trackedExpressions, liquidBlocks, entryMeta: entry.meta };
643
646
  });
644
647
  }
645
648
  function resolveScriptAsset(kebabName, manifest) {
@@ -747,24 +750,25 @@ function resolveFileName(entry, type, options) {
747
750
 
748
751
  // src/ssg/liquid-assembler.ts
749
752
  var DISCLAIMER = "{% comment %}\n IMPORTANT: This file is automatically generated by vite-plugin-shopify.\n Do not attempt to modify this file directly, as any changes will be overwritten by the next build.\n{% endcomment %}\n";
750
- function assembleLiquidFile(html, entry, scriptAsset, cssContents, options, trackedExpressions = []) {
753
+ function assembleLiquidFile(html, entry, scriptAsset, cssContents, options, trackedExpressions = [], liquidBlocks = []) {
751
754
  const type = entry.meta.type ?? entry.targetType;
752
755
  const parts = [DISCLAIMER];
756
+ const liquidPrepend = liquidBlocks.length > 0 ? liquidBlocks.join("\n") : "";
753
757
  switch (type) {
754
758
  case "template":
755
759
  parts.push(html);
756
760
  break;
757
761
  case "section":
758
- parts.push(...buildSection(html, entry, trackedExpressions));
762
+ parts.push(...buildSection(html, entry, trackedExpressions, liquidPrepend));
759
763
  break;
760
764
  case "block":
761
- parts.push(...buildBlock(html, entry, trackedExpressions));
765
+ parts.push(...buildBlock(html, entry, trackedExpressions, liquidPrepend));
762
766
  break;
763
767
  case "snippet":
764
- parts.push(...buildSnippet(html, entry, trackedExpressions));
768
+ parts.push(...buildSnippet(html, entry, trackedExpressions, liquidPrepend));
765
769
  break;
766
770
  default:
767
- parts.push(...buildSection(html, entry, trackedExpressions));
771
+ parts.push(...buildSection(html, entry, trackedExpressions, liquidPrepend));
768
772
  break;
769
773
  }
770
774
  for (const snippet of cssContents.snippets) {
@@ -805,7 +809,7 @@ function buildLiquidBridge(trackedExpressions) {
805
809
  " </script>"
806
810
  ].join("\n");
807
811
  }
808
- function buildSection(html, entry, trackedExpressions) {
812
+ function buildSection(html, entry, trackedExpressions, liquidPrepend = "") {
809
813
  const tag = entry.meta.tag ?? "div";
810
814
  const cls = entry.meta.class ?? "";
811
815
  const lines = [
@@ -817,6 +821,7 @@ function buildSection(html, entry, trackedExpressions) {
817
821
  ];
818
822
  if (cls) lines.push(` class="${cls}"`);
819
823
  lines.push(`>`);
824
+ if (liquidPrepend) lines.push(liquidPrepend);
820
825
  const liquidBridge = buildLiquidBridge(trackedExpressions);
821
826
  if (liquidBridge) lines.push(liquidBridge);
822
827
  lines.push(
@@ -826,7 +831,7 @@ function buildSection(html, entry, trackedExpressions) {
826
831
  lines.push(`</${tag}>`);
827
832
  return lines;
828
833
  }
829
- function buildBlock(html, entry, trackedExpressions) {
834
+ function buildBlock(html, entry, trackedExpressions, liquidPrepend = "") {
830
835
  const tag = entry.meta.tag ?? "div";
831
836
  const cls = entry.meta.class ?? "";
832
837
  const lines = [
@@ -846,6 +851,7 @@ function buildBlock(html, entry, trackedExpressions) {
846
851
  ` {{ block.shopify_attributes }}`,
847
852
  `>`
848
853
  );
854
+ if (liquidPrepend) lines.push(liquidPrepend);
849
855
  const liquidBridge = buildLiquidBridge(trackedExpressions);
850
856
  if (liquidBridge) lines.push(liquidBridge);
851
857
  lines.push(
@@ -855,11 +861,12 @@ function buildBlock(html, entry, trackedExpressions) {
855
861
  lines.push(`</${tag}>`);
856
862
  return lines;
857
863
  }
858
- function buildSnippet(html, entry, trackedExpressions) {
864
+ function buildSnippet(html, entry, trackedExpressions, liquidPrepend = "") {
859
865
  const lines = [
860
866
  "",
861
867
  `<div data-ssg-component="${entry.kebabName}">`
862
868
  ];
869
+ if (liquidPrepend) lines.push(liquidPrepend);
863
870
  const liquidBridge = buildLiquidBridge(trackedExpressions);
864
871
  if (liquidBridge) lines.push(liquidBridge);
865
872
  lines.push(
@@ -892,7 +899,10 @@ var log7 = logger("validate");
892
899
  function validateShopifyMeta(meta, context) {
893
900
  const warnings = [];
894
901
  const nameWarning = checkNameLength(meta, context.kebabName);
895
- if (nameWarning) warnings.push(nameWarning);
902
+ if (nameWarning) {
903
+ warnings.push(nameWarning);
904
+ meta.name = meta.name.slice(0, MAX_NAME_LENGTH);
905
+ }
896
906
  if (meta.settings) {
897
907
  for (const s of meta.settings) {
898
908
  const w = checkEmptyStringDefault(s);
@@ -935,7 +945,7 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, en
935
945
  try {
936
946
  const renderResult = await renderEntry(bundleResult.tmpFile, entry, projectRoot);
937
947
  if (!renderResult) return;
938
- const { html, trackedExpressions } = renderResult;
948
+ const { html, trackedExpressions, liquidBlocks } = renderResult;
939
949
  validateShopifyMeta(entry.meta, { kebabName: entry.kebabName, filePath: entry.filePath });
940
950
  const cssFiles = entryCssFiles.get(entry.kebabName) || [];
941
951
  const { inline: cssInlineFiles, snippets: cssSnippets } = categorizeCss(cssFiles, cssSnippetMap);
@@ -955,7 +965,7 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, en
955
965
  prefix: options.ssg.prefix,
956
966
  outputName: options.ssg.outputName || void 0,
957
967
  buildDir: options.buildDir
958
- }, [...trackedExpressions]);
968
+ }, [...trackedExpressions], liquidBlocks);
959
969
  const outputPath = getOutputPath(entry, {
960
970
  prefix: options.ssg.prefix,
961
971
  outputName: options.ssg.outputName || void 0,
@@ -1011,7 +1021,7 @@ function shopifySSG(options) {
1011
1021
  if (id === "\0vite-plugin-shopify:runtime") {
1012
1022
  const exports = [
1013
1023
  `export { LiquidDataProvider, LiquidDataContext } from 'vite-plugin-shopify/runtime'`,
1014
- `export { useLiquidValue, useLiquidValues, useSectionSettings, useBlockSettings, useSnippetParams, useBlockParams } from 'vite-plugin-shopify/runtime'`
1024
+ `export { useLiquidValue, useLiquidValues, useLiquidBlock, useSectionSettings, useBlockSettings, useSnippetParams, useBlockParams } from 'vite-plugin-shopify/runtime'`
1015
1025
  ];
1016
1026
  return exports.join("\n");
1017
1027
  }
@@ -28,8 +28,9 @@ declare function useSnippetParams(key: string): {
28
28
  declare function useBlockParams(key: string): {
29
29
  value: string | undefined;
30
30
  };
31
+ declare function useLiquidBlock(code: string): string;
31
32
 
32
33
  declare const LiquidDataContext: react.Context<Record<string, any>>;
33
34
  declare const LiquidDataProvider: react.Provider<Record<string, any>>;
34
35
 
35
- export { LiquidDataContext, LiquidDataProvider, type LiquidTypeMode, parseLiquidBoolean, parseLiquidNumber, useBlockParams, useBlockSettings, useLiquidValue, useLiquidValues, useSectionSettings, useSnippetParams };
36
+ export { LiquidDataContext, LiquidDataProvider, type LiquidTypeMode, parseLiquidBoolean, parseLiquidNumber, useBlockParams, useBlockSettings, useLiquidBlock, useLiquidValue, useLiquidValues, useSectionSettings, useSnippetParams };
@@ -1,5 +1,5 @@
1
1
  // src/runtime/hooks.ts
2
- import { useContext, useEffect, useState } from "react";
2
+ import { useContext, useEffect, useState, useMemo } from "react";
3
3
 
4
4
  // src/runtime/provider.ts
5
5
  import { createContext } from "react";
@@ -123,6 +123,26 @@ function useSnippetParams(key) {
123
123
  function useBlockParams(key) {
124
124
  return { value: useLiquidRaw(key) };
125
125
  }
126
+ var LIQUID_EXPR_RE = /\{\{([^{}]+)\}\}/g;
127
+ function useLiquidBlock(code) {
128
+ const isSSR = typeof globalThis.document === "undefined";
129
+ useMemo(() => {
130
+ if (!isSSR) return;
131
+ const blocks = globalThis.__shopify_ssg_liquid_blocks;
132
+ if (blocks) blocks.push(code);
133
+ const tracker = globalThis.__shopify_ssg_liquid_track;
134
+ if (tracker) {
135
+ let match;
136
+ while ((match = LIQUID_EXPR_RE.exec(code)) !== null) {
137
+ const fullExpr = match[1].trim();
138
+ const pipeIdx = fullExpr.indexOf("|");
139
+ const exprName = pipeIdx >= 0 ? fullExpr.substring(0, pipeIdx).trim() : fullExpr;
140
+ if (exprName) tracker.add(exprName);
141
+ }
142
+ }
143
+ }, [code, isSSR]);
144
+ return "";
145
+ }
126
146
  export {
127
147
  LiquidDataContext,
128
148
  LiquidDataProvider,
@@ -130,6 +150,7 @@ export {
130
150
  parseLiquidNumber,
131
151
  useBlockParams,
132
152
  useBlockSettings,
153
+ useLiquidBlock,
133
154
  useLiquidValue,
134
155
  useLiquidValues,
135
156
  useSectionSettings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-react-shopify",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Vite plugin for React Shopify themes",
5
5
  "files": [
6
6
  "dist"