vite-plugin-react-shopify 1.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 +407 -0
- package/dist/index.d.ts +261 -0
- package/dist/index.js +638 -0
- package/dist/runtime/Liquid.client.d.ts +6 -0
- package/dist/runtime/Liquid.client.js +7 -0
- package/dist/runtime/Liquid.d.ts +11 -0
- package/dist/runtime/Liquid.js +10 -0
- package/dist/runtime/settings.d.ts +8 -0
- package/dist/runtime/settings.js +44 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
// src/options.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
var defaultImportMap = {
|
|
4
|
+
react: "https://esm.sh/react@19",
|
|
5
|
+
reactDomClient: "https://esm.sh/react-dom@19/client"
|
|
6
|
+
};
|
|
7
|
+
var defaultPrefix = {
|
|
8
|
+
template: "page.react-",
|
|
9
|
+
section: "react-",
|
|
10
|
+
block: "react-"
|
|
11
|
+
};
|
|
12
|
+
var resolveOptions = (options = {}) => {
|
|
13
|
+
const themeRoot = options.themeRoot ?? "./";
|
|
14
|
+
const sourceCodeDir = options.sourceCodeDir ?? "frontend";
|
|
15
|
+
const snippetFile = options.snippetFile ?? "shopify-importmap.liquid";
|
|
16
|
+
const buildDir = options.buildDir ?? "assets";
|
|
17
|
+
const ssg = {
|
|
18
|
+
directories: options.ssg?.directories ?? ["sections", "blocks", "templates"],
|
|
19
|
+
prefix: {
|
|
20
|
+
template: options.ssg?.prefix?.template ?? defaultPrefix.template,
|
|
21
|
+
section: options.ssg?.prefix?.section ?? defaultPrefix.section,
|
|
22
|
+
block: options.ssg?.prefix?.block ?? defaultPrefix.block
|
|
23
|
+
},
|
|
24
|
+
outputName: options.ssg?.outputName ?? ""
|
|
25
|
+
};
|
|
26
|
+
const importMap = {
|
|
27
|
+
react: options.importMap?.react ?? defaultImportMap.react,
|
|
28
|
+
reactDomClient: options.importMap?.reactDomClient ?? defaultImportMap.reactDomClient
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
themeRoot: path.resolve(themeRoot),
|
|
32
|
+
sourceCodeDir,
|
|
33
|
+
snippetFile,
|
|
34
|
+
buildDir,
|
|
35
|
+
ssg,
|
|
36
|
+
importMap
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/config.ts
|
|
41
|
+
import path2 from "path";
|
|
42
|
+
import createDebugger from "debug";
|
|
43
|
+
var debug = createDebugger("vite-plugin-shopify:config");
|
|
44
|
+
function shopifyConfig(options) {
|
|
45
|
+
const isDebug = process.env.VITE_SHOPIFY_DEBUG === "true";
|
|
46
|
+
return {
|
|
47
|
+
name: "vite-plugin-shopify:config",
|
|
48
|
+
config(config) {
|
|
49
|
+
const sourceDirAbs = path2.resolve(options.themeRoot, options.sourceCodeDir);
|
|
50
|
+
const generated = {
|
|
51
|
+
base: config.base ?? "./",
|
|
52
|
+
publicDir: config.publicDir ?? false,
|
|
53
|
+
build: {
|
|
54
|
+
outDir: config.build?.outDir ?? path2.join(options.themeRoot, options.buildDir),
|
|
55
|
+
assetsDir: config.build?.assetsDir ?? "",
|
|
56
|
+
emptyOutDir: config.build?.emptyOutDir ?? false,
|
|
57
|
+
manifest: config.build?.manifest ?? true,
|
|
58
|
+
minify: config.build?.minify ?? (isDebug ? false : void 0),
|
|
59
|
+
sourcemap: config.build?.sourcemap ?? (isDebug ? true : void 0),
|
|
60
|
+
rollupOptions: {
|
|
61
|
+
...config.build?.rollupOptions,
|
|
62
|
+
external: [
|
|
63
|
+
...Array.isArray(config.build?.rollupOptions?.external) ? config.build.rollupOptions.external : [],
|
|
64
|
+
"react",
|
|
65
|
+
"react-dom/client"
|
|
66
|
+
],
|
|
67
|
+
output: {
|
|
68
|
+
...config.build?.rollupOptions?.output,
|
|
69
|
+
entryFileNames: "[name]-[hash].js",
|
|
70
|
+
chunkFileNames: "[name]-[hash].js",
|
|
71
|
+
assetFileNames: "[name]-[hash][extname]"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
resolve: {
|
|
76
|
+
alias: Array.isArray(config.resolve?.alias) ? [
|
|
77
|
+
...config.resolve.alias,
|
|
78
|
+
{ find: "~", replacement: sourceDirAbs },
|
|
79
|
+
{ find: "@", replacement: sourceDirAbs }
|
|
80
|
+
] : {
|
|
81
|
+
"~": sourceDirAbs,
|
|
82
|
+
"@": sourceDirAbs,
|
|
83
|
+
...config.resolve?.alias
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
server: {
|
|
87
|
+
host: config.server?.host ?? "localhost",
|
|
88
|
+
https: config.server?.https,
|
|
89
|
+
port: config.server?.port ?? 5173,
|
|
90
|
+
cors: config.server?.cors ?? {
|
|
91
|
+
origin: [
|
|
92
|
+
/^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/,
|
|
93
|
+
/\.myshopify\.com$/
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
css: {
|
|
98
|
+
modules: config.css?.modules
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
debug(generated);
|
|
102
|
+
return generated;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/entries.ts
|
|
108
|
+
import path4 from "path";
|
|
109
|
+
import { normalizePath as normalizePath2 } from "vite";
|
|
110
|
+
|
|
111
|
+
// src/ssg/scanner.ts
|
|
112
|
+
import path3 from "path";
|
|
113
|
+
import glob from "fast-glob";
|
|
114
|
+
import { normalizePath } from "vite";
|
|
115
|
+
var TYPE_BY_DIR = {
|
|
116
|
+
templates: "template",
|
|
117
|
+
sections: "section",
|
|
118
|
+
blocks: "block"
|
|
119
|
+
};
|
|
120
|
+
function scanEntries(options) {
|
|
121
|
+
const sourceDir = path3.resolve(options.themeRoot, options.sourceCodeDir);
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const dir of options.ssg.directories) {
|
|
124
|
+
const scanPath = normalizePath(path3.join(sourceDir, dir, "**/*.{tsx,jsx}"));
|
|
125
|
+
const files = glob.sync(scanPath, { onlyFiles: true });
|
|
126
|
+
for (const filePath of files) {
|
|
127
|
+
const absPath = path3.resolve(filePath);
|
|
128
|
+
const fileName = path3.basename(filePath, path3.extname(filePath));
|
|
129
|
+
const componentName = fileName;
|
|
130
|
+
const kebabName = toKebabCase(fileName);
|
|
131
|
+
const targetType = TYPE_BY_DIR[dir] ?? "section";
|
|
132
|
+
entries.push({
|
|
133
|
+
filePath: absPath,
|
|
134
|
+
componentName,
|
|
135
|
+
kebabName,
|
|
136
|
+
targetType,
|
|
137
|
+
meta: { name: componentName }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
function toKebabCase(str) {
|
|
144
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/entries.ts
|
|
148
|
+
function shopifyEntries(options) {
|
|
149
|
+
let entries = [];
|
|
150
|
+
return {
|
|
151
|
+
name: "vite-plugin-shopify:entries",
|
|
152
|
+
config(config) {
|
|
153
|
+
entries = scanEntries(options);
|
|
154
|
+
if (entries.length === 0) return {};
|
|
155
|
+
const input = {};
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
input[entry.kebabName] = `\0shopify:entry:${entry.kebabName}`;
|
|
158
|
+
}
|
|
159
|
+
const existingInput = config.build?.rollupOptions?.input;
|
|
160
|
+
const merged = existingInput ? { ...existingInput, ...input } : input;
|
|
161
|
+
return {
|
|
162
|
+
build: {
|
|
163
|
+
rollupOptions: { input: merged }
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
resolveId(id) {
|
|
168
|
+
if (id.startsWith("\0shopify:entry:")) return `\0${id}`;
|
|
169
|
+
},
|
|
170
|
+
load(id) {
|
|
171
|
+
if (!id.startsWith("\0\0shopify:entry:")) return;
|
|
172
|
+
const kebabName = id.replace("\0\0shopify:entry:", "");
|
|
173
|
+
const entry = entries.find((e) => e.kebabName === kebabName);
|
|
174
|
+
if (!entry) return;
|
|
175
|
+
const sourceDir = path4.resolve(options.themeRoot, options.sourceCodeDir);
|
|
176
|
+
const componentRel = normalizePath2(path4.relative(sourceDir, entry.filePath));
|
|
177
|
+
return [
|
|
178
|
+
`import { createElement } from 'react'`,
|
|
179
|
+
`import Component from '~/${componentRel}'`,
|
|
180
|
+
`import { hydrateRoot } from 'react-dom/client'`,
|
|
181
|
+
`import { SettingsProvider } from 'vite-plugin-react-shopify/runtime/settings'`,
|
|
182
|
+
`import { ParamsProvider } from 'vite-plugin-react-shopify/runtime/settings'`,
|
|
183
|
+
``,
|
|
184
|
+
`const SELECTOR = '[data-ssg-component="${kebabName}"]'`,
|
|
185
|
+
`const roots = new Map()`,
|
|
186
|
+
``,
|
|
187
|
+
`function hydrate(el) {`,
|
|
188
|
+
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
189
|
+
` if (!h || roots.has(h)) return`,
|
|
190
|
+
` const propsEl = el.querySelector(':scope > script[data-ssg-props]')`,
|
|
191
|
+
` const props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {}`,
|
|
192
|
+
` const paramsEl = el.querySelector(':scope > script[data-ssg-params]')`,
|
|
193
|
+
` const params = paramsEl ? JSON.parse(paramsEl.textContent || '{}') : {}`,
|
|
194
|
+
` roots.set(h, hydrateRoot(h, createElement(SettingsProvider, { value: props }, createElement(ParamsProvider, { value: params }, createElement(Component)))))`,
|
|
195
|
+
`}`,
|
|
196
|
+
``,
|
|
197
|
+
`function unmount(el) {`,
|
|
198
|
+
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
199
|
+
` if (h && roots.has(h)) { roots.get(h).unmount(); roots.delete(h) }`,
|
|
200
|
+
`}`,
|
|
201
|
+
``,
|
|
202
|
+
`function scan(target) {`,
|
|
203
|
+
` if (target.matches?.(SELECTOR)) hydrate(target)`,
|
|
204
|
+
` target.querySelectorAll(SELECTOR).forEach(hydrate)`,
|
|
205
|
+
`}`,
|
|
206
|
+
``,
|
|
207
|
+
`function sweep(target) {`,
|
|
208
|
+
` if (target.matches?.(SELECTOR)) unmount(target)`,
|
|
209
|
+
` target.querySelectorAll(SELECTOR).forEach(unmount)`,
|
|
210
|
+
`}`,
|
|
211
|
+
``,
|
|
212
|
+
`scan(document)`,
|
|
213
|
+
``,
|
|
214
|
+
`document.addEventListener('shopify:section:load', (e) => {`,
|
|
215
|
+
` scan(e.target)`,
|
|
216
|
+
`})`,
|
|
217
|
+
``,
|
|
218
|
+
`document.addEventListener('shopify:section:unload', (e) => {`,
|
|
219
|
+
` sweep(e.target)`,
|
|
220
|
+
`})`
|
|
221
|
+
].join("\n");
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/ssg/index.ts
|
|
227
|
+
import fs2 from "fs";
|
|
228
|
+
import path7 from "path";
|
|
229
|
+
|
|
230
|
+
// src/ssg/compiler.ts
|
|
231
|
+
import fs from "fs";
|
|
232
|
+
import path6 from "path";
|
|
233
|
+
import { createRequire } from "module";
|
|
234
|
+
|
|
235
|
+
// src/ssg/post-process.ts
|
|
236
|
+
var REACT_LIQUID_REGEX = /<\/?react-liquid>/g;
|
|
237
|
+
function stripReactLiquidTags(html) {
|
|
238
|
+
return html.replace(REACT_LIQUID_REGEX, "");
|
|
239
|
+
}
|
|
240
|
+
function unwrapHtmlEntities(html) {
|
|
241
|
+
return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/ssg/schema-gen.ts
|
|
245
|
+
function serializeSetting(setting) {
|
|
246
|
+
const s = { type: setting.type };
|
|
247
|
+
if ("id" in setting) s.id = setting.id;
|
|
248
|
+
if ("label" in setting) s.label = setting.label;
|
|
249
|
+
if ("default" in setting && setting.default !== void 0) {
|
|
250
|
+
s.default = setting.default;
|
|
251
|
+
}
|
|
252
|
+
if ("info" in setting && setting.info) s.info = setting.info;
|
|
253
|
+
if ("placeholder" in setting && setting.placeholder) {
|
|
254
|
+
s.placeholder = setting.placeholder;
|
|
255
|
+
}
|
|
256
|
+
if ("options" in setting && setting.options) s.options = setting.options;
|
|
257
|
+
if ("min" in setting && setting.min !== void 0) s.min = setting.min;
|
|
258
|
+
if ("max" in setting && setting.max !== void 0) s.max = setting.max;
|
|
259
|
+
if ("step" in setting && setting.step !== void 0) s.step = setting.step;
|
|
260
|
+
if ("unit" in setting && setting.unit) s.unit = setting.unit;
|
|
261
|
+
if ("accept" in setting && setting.accept) s.accept = setting.accept;
|
|
262
|
+
if ("metaobject_type" in setting && setting.metaobject_type) {
|
|
263
|
+
s.metaobject_type = setting.metaobject_type;
|
|
264
|
+
}
|
|
265
|
+
if ("limit" in setting && setting.limit !== void 0) s.limit = setting.limit;
|
|
266
|
+
if ("content" in setting && setting.content) s.content = setting.content;
|
|
267
|
+
if ("definition" in setting && setting.definition) {
|
|
268
|
+
s.definition = setting.definition.map(serializeSetting);
|
|
269
|
+
}
|
|
270
|
+
if ("role" in setting && setting.role) s.role = setting.role;
|
|
271
|
+
return s;
|
|
272
|
+
}
|
|
273
|
+
function generateSchema(meta) {
|
|
274
|
+
const schema = {
|
|
275
|
+
name: meta.name
|
|
276
|
+
};
|
|
277
|
+
if (meta.tag) schema.tag = meta.tag;
|
|
278
|
+
if (meta.class) schema.class = meta.class;
|
|
279
|
+
if (meta.limit !== void 0) schema.limit = meta.limit;
|
|
280
|
+
if (meta.max_blocks !== void 0) schema.max_blocks = meta.max_blocks;
|
|
281
|
+
if (meta.settings && meta.settings.length > 0) {
|
|
282
|
+
schema.settings = meta.settings.map(serializeSetting);
|
|
283
|
+
}
|
|
284
|
+
if (meta.blocks && meta.blocks.length > 0) {
|
|
285
|
+
schema.blocks = meta.blocks.map((block) => {
|
|
286
|
+
const b = { type: block.type };
|
|
287
|
+
if (block.name) b.name = block.name;
|
|
288
|
+
if (block.settings) b.settings = block.settings;
|
|
289
|
+
return b;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (meta.presets && meta.presets.length > 0) {
|
|
293
|
+
schema.presets = meta.presets.map((preset) => {
|
|
294
|
+
const p = { name: preset.name };
|
|
295
|
+
if (preset.category) p.category = preset.category;
|
|
296
|
+
if (preset.settings) p.settings = preset.settings;
|
|
297
|
+
if (preset.blocks) p.blocks = preset.blocks;
|
|
298
|
+
return p;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (meta.enabled_on) schema.enabled_on = meta.enabled_on;
|
|
302
|
+
if (meta.disabled_on) schema.disabled_on = meta.disabled_on;
|
|
303
|
+
if (meta.templates) schema.templates = meta.templates;
|
|
304
|
+
const json = JSON.stringify(schema, null, 2);
|
|
305
|
+
return `
|
|
306
|
+
{% schema %}
|
|
307
|
+
${json}
|
|
308
|
+
{% endschema %}
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/ssg/liquid.ts
|
|
313
|
+
import path5 from "path";
|
|
314
|
+
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";
|
|
315
|
+
function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
316
|
+
const type = entry.meta.type ?? entry.targetType;
|
|
317
|
+
const parts = [DISCLAIMER];
|
|
318
|
+
switch (type) {
|
|
319
|
+
case "template":
|
|
320
|
+
parts.push(html);
|
|
321
|
+
break;
|
|
322
|
+
case "section":
|
|
323
|
+
parts.push(...buildSection(html, entry));
|
|
324
|
+
break;
|
|
325
|
+
case "block":
|
|
326
|
+
parts.push(...buildBlock(html, entry));
|
|
327
|
+
break;
|
|
328
|
+
default:
|
|
329
|
+
parts.push(...buildSection(html, entry));
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (cssContents.length > 0) {
|
|
333
|
+
parts.push(
|
|
334
|
+
"",
|
|
335
|
+
"{% stylesheet %}",
|
|
336
|
+
...cssContents.map((c) => c.trim()),
|
|
337
|
+
"{% endstylesheet %}"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (scriptAsset) {
|
|
341
|
+
const assetPath = getAssetRelativePath(options.buildDir, scriptAsset);
|
|
342
|
+
parts.push(
|
|
343
|
+
"",
|
|
344
|
+
`<script type="module" src="{{ '${assetPath}' | asset_url }}"></script>`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
parts.push(generateSchema(entry.meta));
|
|
348
|
+
return parts.join("\n") + "\n";
|
|
349
|
+
}
|
|
350
|
+
var hasBlocks = (entry) => !!entry.meta.blocks && entry.meta.blocks.length > 0;
|
|
351
|
+
var SETTINGS_SECTION = ` <script type="application/json" data-ssg-props>{{ section.settings | json }}</script>`;
|
|
352
|
+
var SETTINGS_BLOCK = ` <script type="application/json" data-ssg-props>{{ block.settings | json }}</script>`;
|
|
353
|
+
function buildParamsBridge(params) {
|
|
354
|
+
const entries = params.map((p) => ` "${p}": {{ ${p} | json }}`).join(",\n");
|
|
355
|
+
return ` <script type="application/json" data-ssg-params>
|
|
356
|
+
{
|
|
357
|
+
${entries}
|
|
358
|
+
}
|
|
359
|
+
</script>`;
|
|
360
|
+
}
|
|
361
|
+
function buildSection(html, entry) {
|
|
362
|
+
const tag = entry.meta.tag ?? "div";
|
|
363
|
+
const cls = entry.meta.class ?? "";
|
|
364
|
+
const lines = [
|
|
365
|
+
"",
|
|
366
|
+
`<${tag}`,
|
|
367
|
+
` id="{{ section.id }}"`,
|
|
368
|
+
` data-section-id="{{ section.id }}"`,
|
|
369
|
+
` data-ssg-component="${entry.kebabName}"`
|
|
370
|
+
];
|
|
371
|
+
if (cls) lines.push(` class="${cls}"`);
|
|
372
|
+
lines.push(
|
|
373
|
+
`>`,
|
|
374
|
+
SETTINGS_SECTION
|
|
375
|
+
);
|
|
376
|
+
if (entry.meta.params?.length) {
|
|
377
|
+
lines.push(buildParamsBridge(entry.meta.params));
|
|
378
|
+
}
|
|
379
|
+
lines.push(
|
|
380
|
+
` <div data-ssg-hydrate>`,
|
|
381
|
+
` ${html}`,
|
|
382
|
+
` </div>`
|
|
383
|
+
);
|
|
384
|
+
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
385
|
+
lines.push(`</${tag}>`);
|
|
386
|
+
return lines;
|
|
387
|
+
}
|
|
388
|
+
function buildBlock(html, entry) {
|
|
389
|
+
const tag = entry.meta.tag ?? "div";
|
|
390
|
+
const cls = entry.meta.class ?? "";
|
|
391
|
+
const lines = [
|
|
392
|
+
"",
|
|
393
|
+
`{%- doc -%}`,
|
|
394
|
+
` @name ${entry.meta.name}`,
|
|
395
|
+
` @context theme-block`,
|
|
396
|
+
`{%- enddoc -%}`,
|
|
397
|
+
"",
|
|
398
|
+
`<${tag}`,
|
|
399
|
+
` id="{{ block.id }}"`,
|
|
400
|
+
` data-section-id="{{ section.id }}"`,
|
|
401
|
+
` data-ssg-component="${entry.kebabName}"`
|
|
402
|
+
];
|
|
403
|
+
if (cls) lines.push(` class="${cls}"`);
|
|
404
|
+
lines.push(
|
|
405
|
+
` {{ block.shopify_attributes }}`,
|
|
406
|
+
`>`,
|
|
407
|
+
SETTINGS_BLOCK
|
|
408
|
+
);
|
|
409
|
+
if (entry.meta.params?.length) {
|
|
410
|
+
lines.push(buildParamsBridge(entry.meta.params));
|
|
411
|
+
}
|
|
412
|
+
lines.push(
|
|
413
|
+
` <div data-ssg-hydrate>`,
|
|
414
|
+
` ${html}`,
|
|
415
|
+
` </div>`
|
|
416
|
+
);
|
|
417
|
+
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
418
|
+
lines.push(`</${tag}>`);
|
|
419
|
+
return lines;
|
|
420
|
+
}
|
|
421
|
+
function getOutputPath(entry, options) {
|
|
422
|
+
const type = entry.meta.type ?? entry.targetType;
|
|
423
|
+
const dirName = type === "block" ? "blocks" : `${type}s`;
|
|
424
|
+
const fileName = resolveFileName(entry, type, options);
|
|
425
|
+
return path5.join(options.themeRoot, dirName, fileName);
|
|
426
|
+
}
|
|
427
|
+
function getAssetRelativePath(buildDir, filename) {
|
|
428
|
+
if (!buildDir.startsWith("assets/")) return filename;
|
|
429
|
+
const prefix = buildDir.slice("assets/".length);
|
|
430
|
+
return prefix ? `${prefix}/${filename}` : filename;
|
|
431
|
+
}
|
|
432
|
+
function resolveFileName(entry, type, options) {
|
|
433
|
+
if (options.outputName) {
|
|
434
|
+
return options.outputName.replace(/\{type\}/g, type).replace(/\{kebab\}/g, entry.kebabName).replace(/\{pascal\}/g, entry.componentName).replace(/\{target\}/g, entry.targetType) + ".liquid";
|
|
435
|
+
}
|
|
436
|
+
const prefix = options.prefix[type] ?? "react-";
|
|
437
|
+
return `${prefix}${entry.kebabName}.liquid`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/ssg/compiler.ts
|
|
441
|
+
async function compileAllEntries(options, manifest) {
|
|
442
|
+
const entries = scanEntries(options);
|
|
443
|
+
if (entries.length === 0) return;
|
|
444
|
+
const projectRoot = path6.resolve(options.themeRoot);
|
|
445
|
+
const sourceDir = path6.resolve(options.themeRoot, options.sourceCodeDir);
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
try {
|
|
448
|
+
await compileEntry(entry, options, manifest, projectRoot, sourceDir);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.error(`[vite-plugin-shopify] Failed to compile ${entry.filePath}:`, err);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
console.log(`[vite-plugin-shopify] Compiled ${entries.length} entries`);
|
|
454
|
+
}
|
|
455
|
+
async function compileEntry(entry, options, manifest, projectRoot, sourceDir) {
|
|
456
|
+
const projectRequire = createRequire(path6.join(projectRoot, "package.json"));
|
|
457
|
+
let createElement;
|
|
458
|
+
let renderToStaticMarkup;
|
|
459
|
+
try {
|
|
460
|
+
createElement = projectRequire("react").createElement;
|
|
461
|
+
renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
|
|
462
|
+
} catch {
|
|
463
|
+
console.warn(`[vite-plugin-shopify] react/react-dom not found, skipping SSR`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const sourceCode = fs.readFileSync(entry.filePath, "utf-8");
|
|
467
|
+
const ssgSource = sourceCode.replace(
|
|
468
|
+
/import\s+(\w+)\s+from\s+["'][^"']*\.module\.css["'];?\s*/g,
|
|
469
|
+
(_, name) => `const ${name} = new Proxy({},{get:(_,k)=>k});`
|
|
470
|
+
).replace(
|
|
471
|
+
/import\s+["'][^"']*\.css["'];?\s*/g,
|
|
472
|
+
""
|
|
473
|
+
);
|
|
474
|
+
let esbuild;
|
|
475
|
+
try {
|
|
476
|
+
esbuild = projectRequire("esbuild");
|
|
477
|
+
} catch {
|
|
478
|
+
console.warn(`[vite-plugin-shopify] esbuild not found, skipping SSR`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const result = await esbuild.transform(ssgSource, {
|
|
482
|
+
loader: path6.extname(entry.filePath).slice(1),
|
|
483
|
+
format: "esm",
|
|
484
|
+
jsx: "automatic",
|
|
485
|
+
sourcefile: entry.filePath
|
|
486
|
+
});
|
|
487
|
+
const ts = Date.now();
|
|
488
|
+
const tmpFile = path6.join(sourceDir, ".ssg-tmp-" + ts + ".mjs");
|
|
489
|
+
fs.writeFileSync(tmpFile, result.code);
|
|
490
|
+
try {
|
|
491
|
+
const mod = await import(pathToFileURL(tmpFile));
|
|
492
|
+
const Component = mod.default;
|
|
493
|
+
const shopifyMeta = mod.shopifyMeta;
|
|
494
|
+
if (!Component) {
|
|
495
|
+
console.warn(
|
|
496
|
+
`[vite-plugin-shopify] No default export found in ${entry.filePath}, skipping`
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (shopifyMeta) {
|
|
501
|
+
entry.meta = { ...entry.meta, ...shopifyMeta };
|
|
502
|
+
}
|
|
503
|
+
globalThis.__shopify_ssg_target = entry.targetType;
|
|
504
|
+
const element = createElement(Component);
|
|
505
|
+
let html = renderToStaticMarkup(element);
|
|
506
|
+
html = stripReactLiquidTags(html);
|
|
507
|
+
html = unwrapHtmlEntities(html);
|
|
508
|
+
const scriptAsset = resolveScriptAsset(entry.kebabName, manifest);
|
|
509
|
+
const cssContents = readCssAssets(entry.kebabName, manifest, options.buildDir, options.themeRoot);
|
|
510
|
+
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, cssContents, {
|
|
511
|
+
prefix: options.ssg.prefix,
|
|
512
|
+
outputName: options.ssg.outputName || void 0,
|
|
513
|
+
buildDir: options.buildDir
|
|
514
|
+
});
|
|
515
|
+
const outputPath = getOutputPath(entry, {
|
|
516
|
+
prefix: options.ssg.prefix,
|
|
517
|
+
outputName: options.ssg.outputName || void 0,
|
|
518
|
+
themeRoot: options.themeRoot
|
|
519
|
+
});
|
|
520
|
+
const dir = path6.dirname(outputPath);
|
|
521
|
+
if (!fs.existsSync(dir)) {
|
|
522
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
523
|
+
}
|
|
524
|
+
fs.writeFileSync(outputPath, liquidContent);
|
|
525
|
+
} finally {
|
|
526
|
+
try {
|
|
527
|
+
fs.unlinkSync(tmpFile);
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function resolveScriptAsset(kebabName, manifest) {
|
|
533
|
+
const manifestKey = `shopify:entry:${kebabName}`;
|
|
534
|
+
const entryChunk = manifest[manifestKey];
|
|
535
|
+
if (!entryChunk) return null;
|
|
536
|
+
const file = entryChunk.file;
|
|
537
|
+
if (!file) return null;
|
|
538
|
+
return path6.basename(file);
|
|
539
|
+
}
|
|
540
|
+
function readCssAssets(kebabName, manifest, buildDir, themeRoot) {
|
|
541
|
+
const manifestKey = `shopify:entry:${kebabName}`;
|
|
542
|
+
const entryChunk = manifest[manifestKey];
|
|
543
|
+
if (!entryChunk) return [];
|
|
544
|
+
const css = entryChunk.css;
|
|
545
|
+
if (!css || !Array.isArray(css)) return [];
|
|
546
|
+
const assetsDir = path6.resolve(themeRoot, buildDir);
|
|
547
|
+
return css.map((file) => {
|
|
548
|
+
const cssPath = path6.join(assetsDir, file);
|
|
549
|
+
try {
|
|
550
|
+
return fs.readFileSync(cssPath, "utf-8");
|
|
551
|
+
} catch {
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
}).filter(Boolean);
|
|
555
|
+
}
|
|
556
|
+
function pathToFileURL(filePath) {
|
|
557
|
+
const absPath = path6.resolve(filePath);
|
|
558
|
+
if (process.platform === "win32") {
|
|
559
|
+
return "file:///" + absPath.replace(/\\/g, "/");
|
|
560
|
+
}
|
|
561
|
+
return "file://" + absPath;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/ssg/index.ts
|
|
565
|
+
function shopifySSG(options) {
|
|
566
|
+
return {
|
|
567
|
+
name: "vite-plugin-shopify:ssg",
|
|
568
|
+
enforce: "post",
|
|
569
|
+
config() {
|
|
570
|
+
return {};
|
|
571
|
+
},
|
|
572
|
+
async closeBundle() {
|
|
573
|
+
const manifestPath = path7.resolve(
|
|
574
|
+
options.themeRoot,
|
|
575
|
+
options.buildDir,
|
|
576
|
+
".vite",
|
|
577
|
+
"manifest.json"
|
|
578
|
+
);
|
|
579
|
+
if (!fs2.existsSync(manifestPath)) {
|
|
580
|
+
console.warn("[vite-plugin-shopify] No manifest.json found, skipping SSG");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
584
|
+
console.log("[vite-plugin-shopify] Starting SSG compilation...");
|
|
585
|
+
await compileAllEntries(options, manifest);
|
|
586
|
+
console.log("[vite-plugin-shopify] SSG compilation complete");
|
|
587
|
+
writeImportMapSnippet(options);
|
|
588
|
+
},
|
|
589
|
+
resolveId(id) {
|
|
590
|
+
if (id === "vite-plugin-shopify/runtime") {
|
|
591
|
+
return "\0vite-plugin-shopify:runtime";
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
load(id) {
|
|
595
|
+
if (id === "\0vite-plugin-shopify:runtime") {
|
|
596
|
+
return `export { Liquid } from 'vite-plugin-shopify/runtime/Liquid'`;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function writeImportMapSnippet(options) {
|
|
602
|
+
const snippetPath = path7.resolve(
|
|
603
|
+
options.themeRoot,
|
|
604
|
+
"snippets",
|
|
605
|
+
options.snippetFile
|
|
606
|
+
);
|
|
607
|
+
const content = [
|
|
608
|
+
"{% comment %}IMPORTANT: Auto-generated by vite-plugin-react-shopify{% endcomment %}",
|
|
609
|
+
'<script type="importmap">',
|
|
610
|
+
"{",
|
|
611
|
+
' "imports": {',
|
|
612
|
+
` "react": "${options.importMap.react}",`,
|
|
613
|
+
` "react-dom/client": "${options.importMap.reactDomClient}"`,
|
|
614
|
+
" }",
|
|
615
|
+
"}",
|
|
616
|
+
"</script>",
|
|
617
|
+
""
|
|
618
|
+
].join("\n");
|
|
619
|
+
const dir = path7.dirname(snippetPath);
|
|
620
|
+
if (!fs2.existsSync(dir)) {
|
|
621
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
622
|
+
}
|
|
623
|
+
fs2.writeFileSync(snippetPath, content);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/index.ts
|
|
627
|
+
var vitePluginShopify = (options = {}) => {
|
|
628
|
+
const resolvedOptions = resolveOptions(options);
|
|
629
|
+
return [
|
|
630
|
+
shopifyConfig(resolvedOptions),
|
|
631
|
+
shopifyEntries(resolvedOptions),
|
|
632
|
+
shopifySSG(resolvedOptions)
|
|
633
|
+
];
|
|
634
|
+
};
|
|
635
|
+
var src_default = vitePluginShopify;
|
|
636
|
+
export {
|
|
637
|
+
src_default as default
|
|
638
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
declare const SettingsProvider: react.Provider<Record<string, any>>;
|
|
4
|
+
declare function useShopifySettings<T = Record<string, any>>(): T;
|
|
5
|
+
declare const ParamsProvider: react.Provider<Record<string, any>>;
|
|
6
|
+
declare function useShopifyParams<T = Record<string, any>>(): T;
|
|
7
|
+
|
|
8
|
+
export { ParamsProvider, SettingsProvider, useShopifyParams, useShopifySettings };
|