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 +468 -169
- package/dist/index.js +23 -13
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +22 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
# vite-plugin-react-shopify
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
### 开发
|
|
112
|
+
构建:
|
|
49
113
|
|
|
50
114
|
```bash
|
|
51
|
-
#
|
|
52
|
-
|
|
115
|
+
pnpm dev # → vite build --watch,配合 shopify theme dev 使用
|
|
116
|
+
```
|
|
53
117
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
139
|
+
---
|
|
59
140
|
|
|
60
141
|
---
|
|
61
142
|
|
|
@@ -63,200 +144,316 @@ Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测
|
|
|
63
144
|
|
|
64
145
|
```
|
|
65
146
|
my-theme/
|
|
66
|
-
├── frontend/
|
|
147
|
+
├── frontend/ ← React 源码
|
|
67
148
|
│ ├── sections/
|
|
68
|
-
│ │
|
|
149
|
+
│ │ ├── HeroBanner.tsx
|
|
150
|
+
│ │ └── HeroBanner.css
|
|
69
151
|
│ ├── blocks/
|
|
70
152
|
│ │ └── TextBlock.tsx
|
|
71
153
|
│ ├── snippets/
|
|
72
|
-
│ │ └──
|
|
73
|
-
│
|
|
74
|
-
│
|
|
75
|
-
|
|
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
|
-
└──
|
|
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
|
-
##
|
|
249
|
+
## 数据读取 API
|
|
85
250
|
|
|
86
|
-
|
|
251
|
+
所有 Shopify Liquid 数据通过 runtime hooks 读取,导入路径为 `vite-plugin-react-shopify/runtime`。
|
|
87
252
|
|
|
88
|
-
|
|
253
|
+
### API 总览
|
|
89
254
|
|
|
90
255
|
```tsx
|
|
91
256
|
import {
|
|
92
|
-
|
|
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
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
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
|
-
|
|
288
|
+
### 使用示例
|
|
289
|
+
|
|
290
|
+
**读取 Section Settings:**
|
|
111
291
|
|
|
112
292
|
```tsx
|
|
113
293
|
export default function ProductBanner() {
|
|
114
294
|
const { value: title } = useSectionSettings("title");
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
<
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
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
|
-
|
|
330
|
+
**批量读取:**
|
|
172
331
|
|
|
173
332
|
```tsx
|
|
174
|
-
export default function
|
|
175
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
## 预设
|
|
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
|
-
{
|
|
233
|
-
|
|
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
|
-
##
|
|
432
|
+
## Block 嵌套
|
|
433
|
+
|
|
434
|
+
Section 可包含子 Block:
|
|
241
435
|
|
|
242
436
|
```tsx
|
|
243
437
|
export const shopifyMeta = {
|
|
244
|
-
name: "
|
|
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
|
-
-
|
|
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
|
|
468
|
+
return (
|
|
469
|
+
<div>
|
|
470
|
+
<h3>{title}</h3>
|
|
471
|
+
<span>{price}</span>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
272
474
|
}
|
|
273
475
|
```
|
|
274
476
|
|
|
275
|
-
|
|
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
|
-
|
|
487
|
+
### 组件级 CSS
|
|
488
|
+
|
|
489
|
+
创建与组件同名的 CSS 文件,在组件中导入即可。CSS 会自动内联到 Liquid 的 `{% stylesheet %}` 块中:
|
|
282
490
|
|
|
283
491
|
```css
|
|
284
|
-
/* frontend/sections/
|
|
492
|
+
/* frontend/sections/HeroBanner.css */
|
|
285
493
|
.hero { display: grid; padding: 2rem; }
|
|
286
494
|
```
|
|
287
495
|
|
|
288
496
|
```tsx
|
|
289
|
-
import "./
|
|
290
|
-
export default function
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
buildDir: "assets", //
|
|
303
|
-
|
|
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",
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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:*
|
|
590
|
+
DEBUG=vite-plugin-shopify:* pnpm dev
|
|
345
591
|
```
|
|
346
592
|
|
|
347
593
|
---
|
|
348
594
|
|
|
349
|
-
##
|
|
595
|
+
## 水合注意事项
|
|
350
596
|
|
|
351
|
-
>
|
|
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
|
-
|
|
613
|
+
> 插件内置了 `hydration-fix` 模块,在构建时会**自动修复**大多数此类问题。但推荐在源码层面直接使用模板字面量。
|
|
614
|
+
|
|
615
|
+
### 2. useState 初始化
|
|
368
616
|
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
642
|
+
### 4. 内联颜色值
|
|
393
643
|
|
|
394
|
-
|
|
644
|
+
用 CSS 自定义属性代替内联颜色:
|
|
395
645
|
|
|
396
646
|
```tsx
|
|
397
|
-
// ❌
|
|
647
|
+
// ❌ 浏览器将 hex 规范化为 rgb → 不匹配
|
|
398
648
|
<div style={{ backgroundColor: color }} />
|
|
399
649
|
|
|
400
650
|
// ✅ CSS 变量不归一化
|
|
401
651
|
<div style={{ "--accent": color } as React.CSSProperties} />
|
|
402
|
-
|
|
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
|
-
-
|
|
410
|
-
-
|
|
411
|
-
-
|
|
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)
|
|
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
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/runtime/index.js
CHANGED
|
@@ -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,
|