vite-plugin-react-shopify 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -215
- package/dist/dev-server-index.html +278 -0
- package/dist/index.d.ts +23 -2
- package/dist/index.js +401 -127
- package/dist/runtime/index.d.ts +32 -0
- package/dist/runtime/index.js +73 -0
- package/package.json +14 -26
- package/dist/runtime/Liquid.client.d.ts +0 -6
- package/dist/runtime/Liquid.client.js +0 -7
- package/dist/runtime/Liquid.d.ts +0 -11
- package/dist/runtime/Liquid.js +0 -10
- package/dist/runtime/settings.d.ts +0 -8
- package/dist/runtime/settings.js +0 -44
package/dist/index.js
CHANGED
|
@@ -1,74 +1,116 @@
|
|
|
1
|
-
// src/options.ts
|
|
1
|
+
// src/plugin/options.ts
|
|
2
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
3
|
var defaultPrefix = {
|
|
8
4
|
template: "page.react-",
|
|
9
5
|
section: "react-",
|
|
10
|
-
block: "react-"
|
|
6
|
+
block: "react-",
|
|
7
|
+
snippet: "react-"
|
|
11
8
|
};
|
|
9
|
+
function assetRef(buildDir, filename) {
|
|
10
|
+
if (buildDir === "assets") return filename;
|
|
11
|
+
const sub = buildDir.startsWith("assets/") ? buildDir.slice(7) : buildDir;
|
|
12
|
+
return `${sub}/${filename}`;
|
|
13
|
+
}
|
|
14
|
+
function liquidAssetUrl(ref) {
|
|
15
|
+
return `{{ '${ref}' | asset_url }}`;
|
|
16
|
+
}
|
|
12
17
|
var resolveOptions = (options = {}) => {
|
|
13
18
|
const themeRoot = options.themeRoot ?? "./";
|
|
14
19
|
const sourceCodeDir = options.sourceCodeDir ?? "frontend";
|
|
15
20
|
const snippetFile = options.snippetFile ?? "shopify-importmap.liquid";
|
|
16
21
|
const buildDir = options.buildDir ?? "assets";
|
|
17
22
|
const ssg = {
|
|
18
|
-
directories: options.ssg?.directories ?? ["sections", "blocks", "templates"],
|
|
23
|
+
directories: options.ssg?.directories ?? ["sections", "blocks", "templates", "snippets"],
|
|
19
24
|
prefix: {
|
|
20
25
|
template: options.ssg?.prefix?.template ?? defaultPrefix.template,
|
|
21
26
|
section: options.ssg?.prefix?.section ?? defaultPrefix.section,
|
|
22
|
-
block: options.ssg?.prefix?.block ?? defaultPrefix.block
|
|
27
|
+
block: options.ssg?.prefix?.block ?? defaultPrefix.block,
|
|
28
|
+
snippet: options.ssg?.prefix?.snippet ?? defaultPrefix.snippet
|
|
23
29
|
},
|
|
24
|
-
outputName: options.ssg?.outputName ?? ""
|
|
30
|
+
outputName: options.ssg?.outputName ?? "",
|
|
31
|
+
cssPrefix: options.ssg?.cssPrefix ?? "css"
|
|
25
32
|
};
|
|
26
33
|
const importMap = {
|
|
27
|
-
react: options.importMap?.react ??
|
|
28
|
-
reactDomClient: options.importMap?.reactDomClient ??
|
|
34
|
+
react: options.importMap?.react ?? liquidAssetUrl(assetRef(buildDir, "react.js")),
|
|
35
|
+
reactDomClient: options.importMap?.reactDomClient ?? liquidAssetUrl(assetRef(buildDir, "react-dom.js"))
|
|
29
36
|
};
|
|
30
37
|
return {
|
|
31
38
|
themeRoot: path.resolve(themeRoot),
|
|
32
39
|
sourceCodeDir,
|
|
33
40
|
snippetFile,
|
|
34
41
|
buildDir,
|
|
42
|
+
debug: options.debug ?? false,
|
|
35
43
|
ssg,
|
|
36
44
|
importMap
|
|
37
45
|
};
|
|
38
46
|
};
|
|
39
47
|
|
|
40
|
-
// src/
|
|
41
|
-
import path2 from "path";
|
|
48
|
+
// src/plugin/logger.ts
|
|
42
49
|
import createDebugger from "debug";
|
|
43
|
-
var
|
|
50
|
+
var NAMESPACE = "vite-plugin-shopify";
|
|
51
|
+
var _debugEnabled = false;
|
|
52
|
+
function enableDebug() {
|
|
53
|
+
if (_debugEnabled) return;
|
|
54
|
+
_debugEnabled = true;
|
|
55
|
+
const existing = process.env.DEBUG;
|
|
56
|
+
createDebugger.enable(existing ? `${existing},${NAMESPACE}:*` : `${NAMESPACE}:*`);
|
|
57
|
+
}
|
|
58
|
+
function logger(ns) {
|
|
59
|
+
const dbg = createDebugger(`${NAMESPACE}:${ns}`);
|
|
60
|
+
return {
|
|
61
|
+
debug: (formatter, ...args) => {
|
|
62
|
+
if (_debugEnabled) dbg(formatter, ...args);
|
|
63
|
+
},
|
|
64
|
+
info: (msg, ...args) => console.log(`[${NAMESPACE}] ${msg}`, ...args),
|
|
65
|
+
warn: (msg, ...args) => console.warn(`[${NAMESPACE}] ${msg}`, ...args),
|
|
66
|
+
error: (msg, ...args) => console.error(`[${NAMESPACE}] ${msg}`, ...args)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/plugin/config.ts
|
|
71
|
+
import path2 from "path";
|
|
72
|
+
var log = logger("config");
|
|
73
|
+
function isWatchMode() {
|
|
74
|
+
return process.argv.includes("--watch") || process.env.SHOPIFY_DEV_WATCH === "1";
|
|
75
|
+
}
|
|
44
76
|
function shopifyConfig(options) {
|
|
45
|
-
const isDebug = process.env.VITE_SHOPIFY_DEBUG === "true";
|
|
46
77
|
return {
|
|
47
78
|
name: "vite-plugin-shopify:config",
|
|
48
79
|
config(config) {
|
|
49
80
|
const sourceDirAbs = path2.resolve(options.themeRoot, options.sourceCodeDir);
|
|
81
|
+
const watch = isWatchMode();
|
|
82
|
+
log.debug("watch=%s", watch);
|
|
50
83
|
const generated = {
|
|
51
84
|
base: config.base ?? "./",
|
|
52
85
|
publicDir: config.publicDir ?? false,
|
|
53
86
|
build: {
|
|
54
87
|
outDir: config.build?.outDir ?? path2.join(options.themeRoot, options.buildDir),
|
|
55
88
|
assetsDir: config.build?.assetsDir ?? "",
|
|
56
|
-
emptyOutDir: config.build?.emptyOutDir ??
|
|
89
|
+
emptyOutDir: config.build?.emptyOutDir ?? true,
|
|
57
90
|
manifest: config.build?.manifest ?? true,
|
|
58
|
-
minify: config.build?.minify ?? (
|
|
59
|
-
sourcemap: config.build?.sourcemap ?? (
|
|
60
|
-
|
|
61
|
-
...config.build?.rollupOptions,
|
|
62
|
-
external: [
|
|
63
|
-
...Array.isArray(config.build?.rollupOptions?.external) ? config.build.rollupOptions.external : [],
|
|
64
|
-
"react",
|
|
65
|
-
"react-dom/client"
|
|
66
|
-
],
|
|
91
|
+
minify: config.build?.minify ?? (watch || options.debug ? false : void 0),
|
|
92
|
+
sourcemap: config.build?.sourcemap ?? (watch || options.debug ? "inline" : void 0),
|
|
93
|
+
rolldownOptions: {
|
|
94
|
+
...config.build?.rolldownOptions ?? config.build?.rollupOptions,
|
|
95
|
+
external: Array.isArray((config.build?.rolldownOptions ?? config.build?.rollupOptions)?.external) ? (config.build?.rolldownOptions ?? config.build?.rollupOptions).external : [],
|
|
67
96
|
output: {
|
|
68
|
-
...config.build?.rollupOptions?.output,
|
|
97
|
+
...(config.build?.rolldownOptions ?? config.build?.rollupOptions)?.output,
|
|
69
98
|
entryFileNames: "[name]-[hash].js",
|
|
70
|
-
chunkFileNames
|
|
71
|
-
|
|
99
|
+
chunkFileNames(chunkInfo) {
|
|
100
|
+
if (["react", "react-dom"].includes(chunkInfo.name)) {
|
|
101
|
+
return `${chunkInfo.name}.js`;
|
|
102
|
+
}
|
|
103
|
+
return "[name]-[hash].js";
|
|
104
|
+
},
|
|
105
|
+
assetFileNames: "[name]-[hash][extname]",
|
|
106
|
+
manualChunks(id) {
|
|
107
|
+
if (id.includes("/node_modules/react-dom/")) {
|
|
108
|
+
return "react-dom";
|
|
109
|
+
}
|
|
110
|
+
if (id.includes("/node_modules/react/") || id.includes("/node_modules/scheduler/")) {
|
|
111
|
+
return "react";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
72
114
|
}
|
|
73
115
|
}
|
|
74
116
|
},
|
|
@@ -98,24 +140,25 @@ function shopifyConfig(options) {
|
|
|
98
140
|
modules: config.css?.modules
|
|
99
141
|
}
|
|
100
142
|
};
|
|
101
|
-
debug(generated);
|
|
143
|
+
log.debug("generated config: %O", generated);
|
|
102
144
|
return generated;
|
|
103
145
|
}
|
|
104
146
|
};
|
|
105
147
|
}
|
|
106
148
|
|
|
107
|
-
// src/entries.ts
|
|
149
|
+
// src/plugin/entries.ts
|
|
108
150
|
import path4 from "path";
|
|
109
151
|
import { normalizePath as normalizePath2 } from "vite";
|
|
110
152
|
|
|
111
|
-
// src/ssg/scanner.ts
|
|
153
|
+
// src/plugin/ssg/scanner.ts
|
|
112
154
|
import path3 from "path";
|
|
113
155
|
import glob from "fast-glob";
|
|
114
156
|
import { normalizePath } from "vite";
|
|
115
157
|
var TYPE_BY_DIR = {
|
|
116
158
|
templates: "template",
|
|
117
159
|
sections: "section",
|
|
118
|
-
blocks: "block"
|
|
160
|
+
blocks: "block",
|
|
161
|
+
snippets: "snippet"
|
|
119
162
|
};
|
|
120
163
|
function scanEntries(options) {
|
|
121
164
|
const sourceDir = path3.resolve(options.themeRoot, options.sourceCodeDir);
|
|
@@ -144,13 +187,19 @@ function toKebabCase(str) {
|
|
|
144
187
|
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
188
|
}
|
|
146
189
|
|
|
147
|
-
// src/entries.ts
|
|
190
|
+
// src/plugin/entries.ts
|
|
191
|
+
var log2 = logger("entries");
|
|
148
192
|
function shopifyEntries(options) {
|
|
149
193
|
let entries = [];
|
|
150
194
|
return {
|
|
151
195
|
name: "vite-plugin-shopify:entries",
|
|
152
196
|
config(config) {
|
|
153
197
|
entries = scanEntries(options);
|
|
198
|
+
const byType = {};
|
|
199
|
+
for (const e of entries) {
|
|
200
|
+
byType[e.targetType] = (byType[e.targetType] || 0) + 1;
|
|
201
|
+
}
|
|
202
|
+
log2.debug("scanned %d entries: %s", entries.length, JSON.stringify(byType));
|
|
154
203
|
if (entries.length === 0) return {};
|
|
155
204
|
const input = {};
|
|
156
205
|
for (const entry of entries) {
|
|
@@ -178,20 +227,22 @@ function shopifyEntries(options) {
|
|
|
178
227
|
`import { createElement } from 'react'`,
|
|
179
228
|
`import Component from '~/${componentRel}'`,
|
|
180
229
|
`import { hydrateRoot } from 'react-dom/client'`,
|
|
181
|
-
`import {
|
|
182
|
-
`import { ParamsProvider } from 'vite-plugin-react-shopify/runtime/settings'`,
|
|
230
|
+
`import { LiquidDataProvider } from 'vite-plugin-react-shopify/runtime'`,
|
|
183
231
|
``,
|
|
184
232
|
`const SELECTOR = '[data-ssg-component="${kebabName}"]'`,
|
|
185
233
|
`const roots = new Map()`,
|
|
186
234
|
``,
|
|
235
|
+
`function readLiquidData(el) {`,
|
|
236
|
+
` const script = el.querySelector(':scope > script[data-ssg-liquid]')`,
|
|
237
|
+
` if (!script) return {}`,
|
|
238
|
+
` try { return JSON.parse(script.textContent || '{}') } catch { return {} }`,
|
|
239
|
+
`}`,
|
|
240
|
+
``,
|
|
187
241
|
`function hydrate(el) {`,
|
|
188
242
|
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
189
243
|
` if (!h || roots.has(h)) return`,
|
|
190
|
-
` const
|
|
191
|
-
`
|
|
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)))))`,
|
|
244
|
+
` const liquidData = readLiquidData(el)`,
|
|
245
|
+
` roots.set(h, hydrateRoot(h, createElement(LiquidDataProvider, { value: liquidData }, createElement(Component))))`,
|
|
195
246
|
`}`,
|
|
196
247
|
``,
|
|
197
248
|
`function unmount(el) {`,
|
|
@@ -223,30 +274,45 @@ function shopifyEntries(options) {
|
|
|
223
274
|
};
|
|
224
275
|
}
|
|
225
276
|
|
|
226
|
-
// src/ssg/index.ts
|
|
277
|
+
// src/plugin/ssg/index.ts
|
|
227
278
|
import fs2 from "fs";
|
|
228
279
|
import path7 from "path";
|
|
229
280
|
|
|
230
|
-
// src/ssg/compiler.ts
|
|
281
|
+
// src/plugin/ssg/compiler.ts
|
|
231
282
|
import fs from "fs";
|
|
232
283
|
import path6 from "path";
|
|
233
284
|
import { createRequire } from "module";
|
|
234
285
|
|
|
235
|
-
// src/ssg/post-process.ts
|
|
236
|
-
var
|
|
237
|
-
function
|
|
238
|
-
return html.replace(
|
|
286
|
+
// src/plugin/ssg/post-process.ts
|
|
287
|
+
var VOID_ELEMENTS = /<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)\/>/g;
|
|
288
|
+
function normalizeVoidElements(html) {
|
|
289
|
+
return html.replace(VOID_ELEMENTS, "<$1$2>");
|
|
290
|
+
}
|
|
291
|
+
function normalizeStyleAttributes(html) {
|
|
292
|
+
return html.replace(/ style="([^"]+)"/g, (_match, content) => {
|
|
293
|
+
const normalized = content.replace(/:(\S)/g, ": $1").replace(/;\s*$/, "");
|
|
294
|
+
return ` style="${normalized};"`;
|
|
295
|
+
});
|
|
239
296
|
}
|
|
240
297
|
function unwrapHtmlEntities(html) {
|
|
241
298
|
return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
242
299
|
}
|
|
243
300
|
|
|
244
|
-
// src/ssg/
|
|
301
|
+
// src/plugin/ssg/liquid.ts
|
|
302
|
+
import path5 from "path";
|
|
303
|
+
|
|
304
|
+
// src/plugin/ssg/schema-gen.ts
|
|
305
|
+
var log3 = logger("schema-gen");
|
|
245
306
|
function serializeSetting(setting) {
|
|
246
307
|
const s = { type: setting.type };
|
|
247
308
|
if ("id" in setting) s.id = setting.id;
|
|
248
309
|
if ("label" in setting) s.label = setting.label;
|
|
249
310
|
if ("default" in setting && setting.default !== void 0) {
|
|
311
|
+
if (setting.default === "") {
|
|
312
|
+
log3.warn(
|
|
313
|
+
`Setting "${"id" in setting ? setting.id : "(no id)"}" has empty string default. Use a non-empty string or remove the default.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
250
316
|
s.default = setting.default;
|
|
251
317
|
}
|
|
252
318
|
if ("info" in setting && setting.info) s.info = setting.info;
|
|
@@ -309,10 +375,9 @@ ${json}
|
|
|
309
375
|
`;
|
|
310
376
|
}
|
|
311
377
|
|
|
312
|
-
// src/ssg/liquid.ts
|
|
313
|
-
import path5 from "path";
|
|
378
|
+
// src/plugin/ssg/liquid.ts
|
|
314
379
|
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) {
|
|
380
|
+
function assembleLiquidFile(html, entry, scriptAsset, cssContents, options, trackedExpressions = []) {
|
|
316
381
|
const type = entry.meta.type ?? entry.targetType;
|
|
317
382
|
const parts = [DISCLAIMER];
|
|
318
383
|
switch (type) {
|
|
@@ -320,20 +385,26 @@ function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
|
320
385
|
parts.push(html);
|
|
321
386
|
break;
|
|
322
387
|
case "section":
|
|
323
|
-
parts.push(...buildSection(html, entry));
|
|
388
|
+
parts.push(...buildSection(html, entry, trackedExpressions));
|
|
324
389
|
break;
|
|
325
390
|
case "block":
|
|
326
|
-
parts.push(...buildBlock(html, entry));
|
|
391
|
+
parts.push(...buildBlock(html, entry, trackedExpressions));
|
|
392
|
+
break;
|
|
393
|
+
case "snippet":
|
|
394
|
+
parts.push(...buildSnippet(html, entry, trackedExpressions));
|
|
327
395
|
break;
|
|
328
396
|
default:
|
|
329
|
-
parts.push(...buildSection(html, entry));
|
|
397
|
+
parts.push(...buildSection(html, entry, trackedExpressions));
|
|
330
398
|
break;
|
|
331
399
|
}
|
|
332
|
-
|
|
400
|
+
for (const snippet of cssContents.snippets) {
|
|
401
|
+
parts.push("", `{% render '${snippet}' %}`);
|
|
402
|
+
}
|
|
403
|
+
if (cssContents.inline.length > 0) {
|
|
333
404
|
parts.push(
|
|
334
405
|
"",
|
|
335
406
|
"{% stylesheet %}",
|
|
336
|
-
...cssContents.map((c) => c.trim()),
|
|
407
|
+
...cssContents.inline.map((c) => c.trim()),
|
|
337
408
|
"{% endstylesheet %}"
|
|
338
409
|
);
|
|
339
410
|
}
|
|
@@ -344,21 +415,27 @@ function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
|
344
415
|
`<script type="module" src="{{ '${assetPath}' | asset_url }}"></script>`
|
|
345
416
|
);
|
|
346
417
|
}
|
|
347
|
-
|
|
418
|
+
if (type !== "snippet") {
|
|
419
|
+
parts.push(generateSchema(entry.meta));
|
|
420
|
+
}
|
|
348
421
|
return parts.join("\n") + "\n";
|
|
349
422
|
}
|
|
350
423
|
var hasBlocks = (entry) => !!entry.meta.blocks && entry.meta.blocks.length > 0;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
424
|
+
function buildLiquidBridge(trackedExpressions) {
|
|
425
|
+
if (trackedExpressions.length === 0) return "";
|
|
426
|
+
const entries = trackedExpressions.map((expr, i) => {
|
|
427
|
+
const comma = i < trackedExpressions.length - 1 ? "," : "";
|
|
428
|
+
return ` "${expr}": {{ ${expr} | json }}${comma}`;
|
|
429
|
+
});
|
|
430
|
+
return [
|
|
431
|
+
' <script type="application/json" data-ssg-liquid>',
|
|
432
|
+
" {",
|
|
433
|
+
entries.join("\n"),
|
|
434
|
+
" }",
|
|
435
|
+
" </script>"
|
|
436
|
+
].join("\n");
|
|
360
437
|
}
|
|
361
|
-
function buildSection(html, entry) {
|
|
438
|
+
function buildSection(html, entry, trackedExpressions) {
|
|
362
439
|
const tag = entry.meta.tag ?? "div";
|
|
363
440
|
const cls = entry.meta.class ?? "";
|
|
364
441
|
const lines = [
|
|
@@ -369,23 +446,17 @@ function buildSection(html, entry) {
|
|
|
369
446
|
` data-ssg-component="${entry.kebabName}"`
|
|
370
447
|
];
|
|
371
448
|
if (cls) lines.push(` class="${cls}"`);
|
|
449
|
+
lines.push(`>`);
|
|
450
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
451
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
372
452
|
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>`
|
|
453
|
+
` <div data-ssg-hydrate>${html}</div>`
|
|
383
454
|
);
|
|
384
455
|
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
385
456
|
lines.push(`</${tag}>`);
|
|
386
457
|
return lines;
|
|
387
458
|
}
|
|
388
|
-
function buildBlock(html, entry) {
|
|
459
|
+
function buildBlock(html, entry, trackedExpressions) {
|
|
389
460
|
const tag = entry.meta.tag ?? "div";
|
|
390
461
|
const cls = entry.meta.class ?? "";
|
|
391
462
|
const lines = [
|
|
@@ -403,27 +474,43 @@ function buildBlock(html, entry) {
|
|
|
403
474
|
if (cls) lines.push(` class="${cls}"`);
|
|
404
475
|
lines.push(
|
|
405
476
|
` {{ block.shopify_attributes }}`,
|
|
406
|
-
|
|
407
|
-
SETTINGS_BLOCK
|
|
477
|
+
`>`
|
|
408
478
|
);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
479
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
480
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
412
481
|
lines.push(
|
|
413
|
-
` <div data-ssg-hydrate
|
|
414
|
-
` ${html}`,
|
|
415
|
-
` </div>`
|
|
482
|
+
` <div data-ssg-hydrate>${html}</div>`
|
|
416
483
|
);
|
|
417
484
|
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
418
485
|
lines.push(`</${tag}>`);
|
|
419
486
|
return lines;
|
|
420
487
|
}
|
|
488
|
+
function buildSnippet(html, entry, trackedExpressions) {
|
|
489
|
+
const lines = [
|
|
490
|
+
"",
|
|
491
|
+
`<div data-ssg-component="${entry.kebabName}">`
|
|
492
|
+
];
|
|
493
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
494
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
495
|
+
lines.push(
|
|
496
|
+
` <div data-ssg-hydrate>`,
|
|
497
|
+
` ${html}`,
|
|
498
|
+
` </div>`,
|
|
499
|
+
`</div>`
|
|
500
|
+
);
|
|
501
|
+
return lines;
|
|
502
|
+
}
|
|
421
503
|
function getOutputPath(entry, options) {
|
|
422
504
|
const type = entry.meta.type ?? entry.targetType;
|
|
423
|
-
const dirName = type
|
|
505
|
+
const dirName = typeToDir(type);
|
|
424
506
|
const fileName = resolveFileName(entry, type, options);
|
|
425
507
|
return path5.join(options.themeRoot, dirName, fileName);
|
|
426
508
|
}
|
|
509
|
+
function typeToDir(type) {
|
|
510
|
+
if (type === "snippet") return "snippets";
|
|
511
|
+
if (type === "block") return "blocks";
|
|
512
|
+
return `${type}s`;
|
|
513
|
+
}
|
|
427
514
|
function getAssetRelativePath(buildDir, filename) {
|
|
428
515
|
if (!buildDir.startsWith("assets/")) return filename;
|
|
429
516
|
const prefix = buildDir.slice("assets/".length);
|
|
@@ -437,22 +524,118 @@ function resolveFileName(entry, type, options) {
|
|
|
437
524
|
return `${prefix}${entry.kebabName}.liquid`;
|
|
438
525
|
}
|
|
439
526
|
|
|
440
|
-
// src/ssg/
|
|
527
|
+
// src/plugin/ssg/hydration-fix.ts
|
|
528
|
+
var log4 = logger("hydration-fix");
|
|
529
|
+
function autoFixAdjacentText(source, filePath) {
|
|
530
|
+
let fixCount = 0;
|
|
531
|
+
const lines = source.split("\n");
|
|
532
|
+
const fixed = [];
|
|
533
|
+
for (let i = 0; i < lines.length; i++) {
|
|
534
|
+
const line = lines[i];
|
|
535
|
+
const replaced = line.replace(
|
|
536
|
+
/<(\w+)([^>]*?)>([^<]*?\{[^}]*\}[^<]*?)<\/\1>/g,
|
|
537
|
+
(match, tagName, attrs, content) => {
|
|
538
|
+
const trimmed = content.trim();
|
|
539
|
+
if (!needsFix(trimmed)) return match;
|
|
540
|
+
fixCount++;
|
|
541
|
+
const tpl = trimmed.replace(/\{([^}]+)\}/g, "${$1}");
|
|
542
|
+
return `<${tagName}${attrs}>{\`${tpl}\`}</${tagName}>`;
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
fixed.push(replaced);
|
|
546
|
+
}
|
|
547
|
+
if (fixCount > 0) {
|
|
548
|
+
log4.warn(
|
|
549
|
+
`auto-fixed ${fixCount} adjacent text+expression issue(s) in ${filePath}`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return { result: fixed.join("\n"), fixCount };
|
|
553
|
+
}
|
|
554
|
+
function needsFix(content) {
|
|
555
|
+
const trimmed = content.trim();
|
|
556
|
+
if (!trimmed) return false;
|
|
557
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
558
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
559
|
+
if (inner.startsWith("`") && inner.endsWith("`")) return false;
|
|
560
|
+
if (inner.length > 0 && !/<[a-zA-Z]/.test(inner)) return false;
|
|
561
|
+
}
|
|
562
|
+
if (!/\{/.test(trimmed)) return false;
|
|
563
|
+
if (/<[a-zA-Z]/.test(trimmed)) return false;
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/plugin/ssg/compiler.ts
|
|
568
|
+
var log5 = logger("ssg:compiler");
|
|
441
569
|
async function compileAllEntries(options, manifest) {
|
|
442
570
|
const entries = scanEntries(options);
|
|
443
571
|
if (entries.length === 0) return;
|
|
572
|
+
log5.debug("found %d entries to compile", entries.length);
|
|
444
573
|
const projectRoot = path6.resolve(options.themeRoot);
|
|
445
574
|
const sourceDir = path6.resolve(options.themeRoot, options.sourceCodeDir);
|
|
575
|
+
const entryCssFiles = /* @__PURE__ */ new Map();
|
|
576
|
+
const cssRefCount = /* @__PURE__ */ new Map();
|
|
577
|
+
for (const entry of entries) {
|
|
578
|
+
const manifestKey = `shopify:entry:${entry.kebabName}`;
|
|
579
|
+
const files = collectCssFiles(manifestKey, manifest);
|
|
580
|
+
entryCssFiles.set(entry.kebabName, files);
|
|
581
|
+
for (const f of files) {
|
|
582
|
+
cssRefCount.set(f, (cssRefCount.get(f) || 0) + 1);
|
|
583
|
+
}
|
|
584
|
+
log5.debug("entry %s has %d CSS files", entry.kebabName, files.length);
|
|
585
|
+
}
|
|
586
|
+
const cssSnippetMap = /* @__PURE__ */ new Map();
|
|
587
|
+
for (const [cssFile, count] of cssRefCount) {
|
|
588
|
+
if (count > 1) {
|
|
589
|
+
const snippetName = `${options.ssg.cssPrefix}-${getCssBaseName(cssFile)}`;
|
|
590
|
+
cssSnippetMap.set(cssFile, snippetName);
|
|
591
|
+
const snippetPath = path6.join(
|
|
592
|
+
path6.resolve(options.themeRoot),
|
|
593
|
+
"snippets",
|
|
594
|
+
`${snippetName}.liquid`
|
|
595
|
+
);
|
|
596
|
+
const cssPath = path6.join(
|
|
597
|
+
path6.resolve(options.themeRoot, options.buildDir),
|
|
598
|
+
cssFile
|
|
599
|
+
);
|
|
600
|
+
try {
|
|
601
|
+
const cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
602
|
+
fs.mkdirSync(path6.dirname(snippetPath), { recursive: true });
|
|
603
|
+
fs.writeFileSync(snippetPath, `{% stylesheet %}
|
|
604
|
+
${cssContent.trim()}
|
|
605
|
+
{% endstylesheet %}
|
|
606
|
+
`);
|
|
607
|
+
log5.debug("generated shared CSS snippet %s (used by %d entries)", snippetName, count);
|
|
608
|
+
} catch {
|
|
609
|
+
log5.warn("failed to write CSS snippet for %s", cssFile);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
446
613
|
for (const entry of entries) {
|
|
447
614
|
try {
|
|
448
|
-
|
|
615
|
+
const cssFiles = entryCssFiles.get(entry.kebabName) || [];
|
|
616
|
+
const cssSnippets = cssFiles.filter((f) => cssSnippetMap.has(f)).map((f) => cssSnippetMap.get(f));
|
|
617
|
+
const cssInlineFiles = cssFiles.filter((f) => !cssSnippetMap.has(f));
|
|
618
|
+
const cssInline = readCssFileContents(cssInlineFiles, options.buildDir, options.themeRoot);
|
|
619
|
+
log5.debug(
|
|
620
|
+
"compiling %s (type=%s, css inline=%d, css snippets=%d)",
|
|
621
|
+
entry.kebabName,
|
|
622
|
+
entry.targetType,
|
|
623
|
+
cssInline.length,
|
|
624
|
+
cssSnippets.length
|
|
625
|
+
);
|
|
626
|
+
await compileEntry(entry, options, manifest, projectRoot, sourceDir, cssInline, cssSnippets);
|
|
449
627
|
} catch (err) {
|
|
450
|
-
|
|
628
|
+
log5.error("Failed to compile %s:", entry.filePath, err);
|
|
451
629
|
}
|
|
452
630
|
}
|
|
453
|
-
|
|
631
|
+
log5.info("Compiled %d entries", entries.length);
|
|
632
|
+
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
633
|
+
try {
|
|
634
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
454
637
|
}
|
|
455
|
-
async function compileEntry(entry, options, manifest, projectRoot, sourceDir) {
|
|
638
|
+
async function compileEntry(entry, options, manifest, projectRoot, sourceDir, cssInline, cssSnippets) {
|
|
456
639
|
const projectRequire = createRequire(path6.join(projectRoot, "package.json"));
|
|
457
640
|
let createElement;
|
|
458
641
|
let renderToStaticMarkup;
|
|
@@ -460,58 +643,112 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir) {
|
|
|
460
643
|
createElement = projectRequire("react").createElement;
|
|
461
644
|
renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
|
|
462
645
|
} catch {
|
|
463
|
-
|
|
646
|
+
log5.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
|
|
464
647
|
return;
|
|
465
648
|
}
|
|
466
649
|
const sourceCode = fs.readFileSync(entry.filePath, "utf-8");
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
(_, name) => `const ${name} = new Proxy({},{get:(_,k)=>k});`
|
|
470
|
-
).replace(
|
|
471
|
-
/import\s+["'][^"']*\.css["'];?\s*/g,
|
|
472
|
-
""
|
|
473
|
-
);
|
|
650
|
+
const { result: fixedSource, fixCount } = autoFixAdjacentText(sourceCode, entry.filePath);
|
|
651
|
+
const finalSource = fixCount > 0 ? fixedSource : sourceCode;
|
|
474
652
|
let esbuild;
|
|
475
653
|
try {
|
|
476
654
|
esbuild = projectRequire("esbuild");
|
|
477
655
|
} catch {
|
|
478
|
-
|
|
656
|
+
log5.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
|
|
479
657
|
return;
|
|
480
658
|
}
|
|
481
|
-
const
|
|
482
|
-
|
|
659
|
+
const ts = Date.now();
|
|
660
|
+
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
661
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
662
|
+
const tmpFile = path6.join(tmpDir, `.ssg-entry-${ts}.mjs`);
|
|
663
|
+
log5.debug("bundling %s via esbuild", entry.kebabName);
|
|
664
|
+
const startBundled = Date.now();
|
|
665
|
+
await esbuild.build({
|
|
666
|
+
stdin: {
|
|
667
|
+
contents: finalSource,
|
|
668
|
+
resolveDir: path6.dirname(entry.filePath),
|
|
669
|
+
loader: path6.extname(entry.filePath).slice(1)
|
|
670
|
+
},
|
|
671
|
+
outfile: tmpFile,
|
|
672
|
+
bundle: true,
|
|
483
673
|
format: "esm",
|
|
484
674
|
jsx: "automatic",
|
|
485
|
-
|
|
675
|
+
platform: "node",
|
|
676
|
+
external: [
|
|
677
|
+
"react",
|
|
678
|
+
"react-dom",
|
|
679
|
+
"react-dom/*",
|
|
680
|
+
"vite-plugin-react-shopify",
|
|
681
|
+
"vite-plugin-react-shopify/*"
|
|
682
|
+
],
|
|
683
|
+
write: true,
|
|
684
|
+
allowOverwrite: true,
|
|
685
|
+
plugins: [
|
|
686
|
+
{
|
|
687
|
+
name: "ssg-hydration-fix",
|
|
688
|
+
setup(build) {
|
|
689
|
+
build.onLoad({ filter: /\.(tsx|jsx)$/ }, (args) => {
|
|
690
|
+
try {
|
|
691
|
+
const source = fs.readFileSync(args.path, "utf-8");
|
|
692
|
+
const { result, fixCount: fixCount2 } = autoFixAdjacentText(source, args.path);
|
|
693
|
+
if (fixCount2 > 0) {
|
|
694
|
+
return { contents: result, loader: args.path.endsWith(".tsx") ? "tsx" : "jsx" };
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
return void 0;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "ssg-strip-css",
|
|
704
|
+
setup(build) {
|
|
705
|
+
build.onResolve({ filter: /\.module\.css$/ }, (args) => ({
|
|
706
|
+
namespace: "ssg-css-module",
|
|
707
|
+
path: args.path
|
|
708
|
+
}));
|
|
709
|
+
build.onResolve({ filter: /\.css$/ }, (args) => ({
|
|
710
|
+
namespace: "ssg-css-plain",
|
|
711
|
+
path: args.path
|
|
712
|
+
}));
|
|
713
|
+
build.onLoad({ filter: /.*/, namespace: "ssg-css-module" }, () => ({
|
|
714
|
+
contents: "export default new Proxy({},{get:(_,k)=>k});",
|
|
715
|
+
loader: "js"
|
|
716
|
+
}));
|
|
717
|
+
build.onLoad({ filter: /.*/, namespace: "ssg-css-plain" }, () => ({
|
|
718
|
+
contents: "",
|
|
719
|
+
loader: "js"
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
]
|
|
486
724
|
});
|
|
487
|
-
|
|
488
|
-
const tmpFile = path6.join(sourceDir, ".ssg-tmp-" + ts + ".mjs");
|
|
489
|
-
fs.writeFileSync(tmpFile, result.code);
|
|
725
|
+
log5.debug("esbuild bundle took %dms", Date.now() - startBundled);
|
|
490
726
|
try {
|
|
491
727
|
const mod = await import(pathToFileURL(tmpFile));
|
|
492
728
|
const Component = mod.default;
|
|
493
729
|
const shopifyMeta = mod.shopifyMeta;
|
|
494
730
|
if (!Component) {
|
|
495
|
-
|
|
496
|
-
`[vite-plugin-shopify] No default export found in ${entry.filePath}, skipping`
|
|
497
|
-
);
|
|
731
|
+
log5.warn("No default export found in %s, skipping", entry.filePath);
|
|
498
732
|
return;
|
|
499
733
|
}
|
|
500
734
|
if (shopifyMeta) {
|
|
501
735
|
entry.meta = { ...entry.meta, ...shopifyMeta };
|
|
502
736
|
}
|
|
503
737
|
globalThis.__shopify_ssg_target = entry.targetType;
|
|
738
|
+
const trackedExpressions = /* @__PURE__ */ new Set();
|
|
739
|
+
globalThis.__shopify_ssg_liquid_track = trackedExpressions;
|
|
504
740
|
const element = createElement(Component);
|
|
505
741
|
let html = renderToStaticMarkup(element);
|
|
506
|
-
|
|
742
|
+
delete globalThis.__shopify_ssg_liquid_track;
|
|
743
|
+
html = normalizeVoidElements(html);
|
|
744
|
+
html = normalizeStyleAttributes(html);
|
|
507
745
|
html = unwrapHtmlEntities(html);
|
|
508
746
|
const scriptAsset = resolveScriptAsset(entry.kebabName, manifest);
|
|
509
|
-
const
|
|
510
|
-
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, cssContents, {
|
|
747
|
+
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, { inline: cssInline, snippets: cssSnippets }, {
|
|
511
748
|
prefix: options.ssg.prefix,
|
|
512
749
|
outputName: options.ssg.outputName || void 0,
|
|
513
750
|
buildDir: options.buildDir
|
|
514
|
-
});
|
|
751
|
+
}, [...trackedExpressions]);
|
|
515
752
|
const outputPath = getOutputPath(entry, {
|
|
516
753
|
prefix: options.ssg.prefix,
|
|
517
754
|
outputName: options.ssg.outputName || void 0,
|
|
@@ -537,22 +774,49 @@ function resolveScriptAsset(kebabName, manifest) {
|
|
|
537
774
|
if (!file) return null;
|
|
538
775
|
return path6.basename(file);
|
|
539
776
|
}
|
|
540
|
-
function
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
777
|
+
function collectCssFiles(manifestKey, manifest) {
|
|
778
|
+
const collected = /* @__PURE__ */ new Set();
|
|
779
|
+
const visited = /* @__PURE__ */ new Set();
|
|
780
|
+
collectCssFilesRecursive(manifestKey, manifest, collected, visited);
|
|
781
|
+
return [...collected];
|
|
782
|
+
}
|
|
783
|
+
function collectCssFilesRecursive(chunkKey, manifest, collected, visited) {
|
|
784
|
+
if (visited.has(chunkKey)) return;
|
|
785
|
+
visited.add(chunkKey);
|
|
786
|
+
const chunk = manifest[chunkKey];
|
|
787
|
+
if (!chunk) return;
|
|
788
|
+
if (chunk.css && Array.isArray(chunk.css)) {
|
|
789
|
+
for (const cssFile of chunk.css) {
|
|
790
|
+
collected.add(cssFile);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (chunk.imports && Array.isArray(chunk.imports)) {
|
|
794
|
+
for (const imported of chunk.imports) {
|
|
795
|
+
collectCssFilesRecursive(imported, manifest, collected, visited);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function readCssFileContents(cssFiles, buildDir, themeRoot) {
|
|
546
800
|
const assetsDir = path6.resolve(themeRoot, buildDir);
|
|
547
|
-
return
|
|
548
|
-
const cssPath = path6.join(assetsDir, file);
|
|
801
|
+
return cssFiles.map((file) => {
|
|
549
802
|
try {
|
|
550
|
-
return fs.readFileSync(
|
|
803
|
+
return fs.readFileSync(path6.join(assetsDir, file), "utf-8");
|
|
551
804
|
} catch {
|
|
552
805
|
return "";
|
|
553
806
|
}
|
|
554
807
|
}).filter(Boolean);
|
|
555
808
|
}
|
|
809
|
+
function getCssBaseName(cssFile) {
|
|
810
|
+
const name = cssFile.replace(/\.css$/, "");
|
|
811
|
+
const lastHyphen = name.lastIndexOf("-");
|
|
812
|
+
if (lastHyphen > 0) {
|
|
813
|
+
const possibleHash = name.slice(lastHyphen + 1);
|
|
814
|
+
if (/^[A-Za-z0-9_-]{8,}$/.test(possibleHash)) {
|
|
815
|
+
return name.slice(0, lastHyphen);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return name;
|
|
819
|
+
}
|
|
556
820
|
function pathToFileURL(filePath) {
|
|
557
821
|
const absPath = path6.resolve(filePath);
|
|
558
822
|
if (process.platform === "win32") {
|
|
@@ -561,7 +825,8 @@ function pathToFileURL(filePath) {
|
|
|
561
825
|
return "file://" + absPath;
|
|
562
826
|
}
|
|
563
827
|
|
|
564
|
-
// src/ssg/index.ts
|
|
828
|
+
// src/plugin/ssg/index.ts
|
|
829
|
+
var log6 = logger("ssg");
|
|
565
830
|
function shopifySSG(options) {
|
|
566
831
|
return {
|
|
567
832
|
name: "vite-plugin-shopify:ssg",
|
|
@@ -577,14 +842,16 @@ function shopifySSG(options) {
|
|
|
577
842
|
"manifest.json"
|
|
578
843
|
);
|
|
579
844
|
if (!fs2.existsSync(manifestPath)) {
|
|
580
|
-
|
|
845
|
+
log6.warn("No manifest.json found, skipping SSG");
|
|
581
846
|
return;
|
|
582
847
|
}
|
|
848
|
+
log6.debug("reading manifest from %s", manifestPath);
|
|
583
849
|
const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
584
|
-
|
|
850
|
+
log6.info("Starting SSG compilation...");
|
|
585
851
|
await compileAllEntries(options, manifest);
|
|
586
|
-
|
|
852
|
+
log6.info("SSG compilation complete");
|
|
587
853
|
writeImportMapSnippet(options);
|
|
854
|
+
log6.debug("wrote import map snippet");
|
|
588
855
|
},
|
|
589
856
|
resolveId(id) {
|
|
590
857
|
if (id === "vite-plugin-shopify/runtime") {
|
|
@@ -593,7 +860,11 @@ function shopifySSG(options) {
|
|
|
593
860
|
},
|
|
594
861
|
load(id) {
|
|
595
862
|
if (id === "\0vite-plugin-shopify:runtime") {
|
|
596
|
-
|
|
863
|
+
const exports = [
|
|
864
|
+
`export { LiquidDataProvider, LiquidDataContext } from 'vite-plugin-shopify/runtime'`,
|
|
865
|
+
`export { useLiquid, useLiquidValues, useSectionSettings, useBlockSettings, useSnippetParams, useBlockParams } from 'vite-plugin-shopify/runtime'`
|
|
866
|
+
];
|
|
867
|
+
return exports.join("\n");
|
|
597
868
|
}
|
|
598
869
|
}
|
|
599
870
|
};
|
|
@@ -626,6 +897,9 @@ function writeImportMapSnippet(options) {
|
|
|
626
897
|
// src/index.ts
|
|
627
898
|
var vitePluginShopify = (options = {}) => {
|
|
628
899
|
const resolvedOptions = resolveOptions(options);
|
|
900
|
+
if (resolvedOptions.debug || process.env.DEBUG?.includes("vite-plugin-shopify")) {
|
|
901
|
+
enableDebug();
|
|
902
|
+
}
|
|
629
903
|
return [
|
|
630
904
|
shopifyConfig(resolvedOptions),
|
|
631
905
|
shopifyEntries(resolvedOptions),
|